dev1/tests/e2e/05-career-explorer.reload.spec.mjs

152 lines
6.6 KiB
JavaScript

import { test, expect } from '@playwright/test';
import { loadTestUser } from '../utils/testUser.js';
test.describe('@p0 Career Explorer — Reload Suggestions', () => {
test.setTimeout(40000);
test('clears cache → Reload → cache & tiles repopulated (submit_answers fired)', async ({ page }) => {
const user = loadTestUser();
const TIME = {
overlayAppear: 2000, // overlay should mount quickly
cache: 30000, // allow cold path to populate cache
tile: 8000, // find a tile soon after cache
};
// Track server calls (prove reload hits submit_answers)
let sawSubmitAnswers = false;
page.on('response', (r) => {
if (r.request().method() === 'POST' && r.url().includes('/api/onet/submit_answers')) {
sawSubmitAnswers = true;
}
});
// Helper: close any blocking overlay (priorities/meaning) by saving neutral defaults.
async function closeAnyOverlay() {
const overlay = page.locator('div.fixed.inset-0');
if (!(await overlay.isVisible({ timeout: 500 }).catch(() => false))) return;
const dialog = overlay.locator('div[role="dialog"], div.bg-white').first();
// Select first non-empty option in each <select> (if any)
const selects = dialog.locator('select');
const sc = await selects.count().catch(() => 0);
for (let i = 0; i < sc; i++) {
const sel = selects.nth(i);
const opts = sel.locator('option');
const n = await opts.count().catch(() => 0);
for (let j = 0; j < n; j++) {
const v = await opts.nth(j).getAttribute('value');
if (v) { await sel.selectOption(v); break; }
}
}
// Fill any text/number inputs with neutral '3'
const inputs = dialog.locator('input[type="number"], input[type="text"], [role="textbox"]');
const ic = await inputs.count().catch(() => 0);
for (let i = 0; i < ic; i++) {
const inp = inputs.nth(i);
if (await inp.isVisible().catch(() => false)) {
const val = (await inp.inputValue().catch(() => '')) || '';
if (!val) await inp.fill('3');
}
}
// Save/Continue; else Cancel/Close; else Escape
const save = dialog.getByRole('button', { name: /(Save|Continue|Done|OK)/i });
const cancel = dialog.getByRole('button', { name: /(Cancel|Close)/i });
if (await save.isVisible({ timeout: 500 }).catch(() => false)) await save.click();
else if (await cancel.isVisible({ timeout: 500 }).catch(() => false)) await cancel.click();
else await page.keyboard.press('Escape');
await overlay.waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {});
}
// Helper: wait for cache > 0 (source of truth for suggestions readiness)
async function waitForSuggestionsCache() {
await expect
.poll(async () => {
return await page.evaluate(() => {
try {
const s = localStorage.getItem('careerSuggestionsCache');
const arr = s ? JSON.parse(s) : [];
return Array.isArray(arr) ? arr.length : 0;
} catch { return 0; }
});
}, { timeout: TIME.cache, message: 'careerSuggestionsCache not populated' })
.toBeGreaterThan(0);
}
// Helper: complete Interest Inventory quickly if server demands it
async function completeInventoryIfNeeded() {
// If a dialog alert fires (Playwright 'dialog' event), we cannot peek msg here reliably,
// so we proactively check the inventory route if the reload yields no cache.
await page.goto('/interest-inventory', { waitUntil: 'networkidle' });
await expect(page.getByRole('heading', { name: /Interest Inventory/i })).toBeVisible();
const randomizeBtn = page.getByRole('button', { name: /Randomize Answers/i });
if (await randomizeBtn.isVisible().catch(() => false)) {
await randomizeBtn.click();
await expect(page.getByText(/60\s*\/\s*60\s*answered/i)).toBeVisible();
} else {
for (let p = 0; p < 10; p++) {
const selects = page.locator('select');
const n = await selects.count();
for (let i = 0; i < n; i++) await selects.nth(i).selectOption('3');
if (p < 9) await page.getByRole('button', { name: /^Next$/ }).click();
}
}
await page.getByRole('button', { name: /^Submit$/ }).click();
await page.waitForURL('**/career-explorer**', { timeout: 20000 });
await closeAnyOverlay();
}
// Sign in
await page.context().clearCookies();
await page.goto('/signin', { waitUntil: 'networkidle' });
await page.getByPlaceholder('Username', { exact: true }).fill(user.username);
await page.getByPlaceholder('Password', { exact: true }).fill(user.password);
await page.getByRole('button', { name: /^Sign In$/ }).click();
await page.waitForURL('**/signin-landing**', { timeout: 15000 });
// Go to Career Explorer
await page.goto('/career-explorer', { waitUntil: 'networkidle' });
await expect(page.getByRole('heading', { name: /Explore Careers - use these tools/i })).toBeVisible();
await closeAnyOverlay();
// 1) Clear cache and reload page → grid should have no tiles
await page.evaluate(() => localStorage.removeItem('careerSuggestionsCache'));
await page.reload({ waitUntil: 'networkidle' });
await closeAnyOverlay();
const tile = page.locator('div.grid button').first();
// Short check: no tile should be visible yet
const tileVisiblePre = await tile.isVisible({ timeout: 1000 }).catch(() => false);
expect(tileVisiblePre).toBeFalsy();
// 2) Click Reload Career Suggestions
const reloadBtn = page.getByRole('button', { name: /Reload Career Suggestions/i });
await expect(reloadBtn).toBeVisible();
await reloadBtn.click();
// If overlay mounts, let it mount; we don't require 100% display
const overlayText = page.getByText(/Loading Career Suggestions/i).first();
await overlayText.isVisible({ timeout: TIME.overlayAppear }).catch(() => {});
// 3) Wait for suggestions cache
try {
await waitForSuggestionsCache();
} catch {
// If cache didn't populate, complete inventory once and retry reload
await completeInventoryIfNeeded();
await expect(reloadBtn).toBeVisible();
await reloadBtn.click();
await overlayText.isVisible({ timeout: TIME.overlayAppear }).catch(() => {});
await waitForSuggestionsCache();
}
// 4) Tiles should now be present
await expect(tile).toBeVisible({ timeout: TIME.tile });
// 5) Assert submit_answers POST fired during reload
expect(sawSubmitAnswers).toBeTruthy();
});
});