158 lines
7.5 KiB
JavaScript
158 lines
7.5 KiB
JavaScript
// @ts-check
|
|
import { test, expect } from '@playwright/test';
|
|
import { loadTestUser } from '../utils/testUser.js';
|
|
|
|
test.describe('@p0 Educational Programs — handoff & page render', () => {
|
|
test.setTimeout(20000);
|
|
|
|
test('handoff carries selectedCareer; page shows career title, KSA header and school cards; survives reload', async ({ page }) => {
|
|
const user = loadTestUser();
|
|
|
|
// ------- helpers -------
|
|
async function signIn() {
|
|
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 });
|
|
}
|
|
|
|
async function ensureSuggestions() {
|
|
await page.goto('/career-explorer', { waitUntil: 'networkidle' });
|
|
const tile = page.locator('div.grid button').first();
|
|
if (await tile.isVisible({ timeout: 1500 }).catch(() => false)) return;
|
|
|
|
// Cold path: click Reload and wait for cache
|
|
const reloadBtn = page.getByRole('button', { name: /Reload Career Suggestions/i });
|
|
await expect(reloadBtn).toBeVisible();
|
|
await reloadBtn.click();
|
|
// Optional overlay text, do not insist on 100%
|
|
const overlay = page.getByText(/Loading Career Suggestions/i).first();
|
|
await overlay.isVisible({ timeout: 2000 }).catch(() => {});
|
|
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: 60000, message: 'careerSuggestionsCache not populated' })
|
|
.toBeGreaterThan(0);
|
|
await expect(tile).toBeVisible({ timeout: 10000 });
|
|
}
|
|
|
|
// ------- flow -------
|
|
await signIn();
|
|
await ensureSuggestions();
|
|
|
|
// Open a suggestion → Add to Comparison
|
|
const firstTile = page.locator('div.grid button').first();
|
|
await expect(firstTile).toBeVisible({ timeout: 8000 });
|
|
const tileTitle = (await firstTile.textContent())?.replace('⚠️', '').trim() || null;
|
|
await firstTile.click();
|
|
|
|
await expect(page.getByRole('button', { name: /Add to Comparison/i })).toBeVisible({ timeout: 15000 });
|
|
await page.getByRole('button', { name: /Add to Comparison/i }).click();
|
|
|
|
// Ratings modal → Save (neutral)
|
|
{
|
|
const overlay = page.locator('div.fixed.inset-0');
|
|
if (await overlay.isVisible({ timeout: 2000 }).catch(() => false)) {
|
|
const dlg = overlay.locator('div[role="dialog"], div.bg-white').first();
|
|
const selects = dlg.locator('select');
|
|
const sc = await selects.count().catch(() => 0);
|
|
for (let i = 0; i < sc; i++) {
|
|
const sel = selects.nth(i);
|
|
const has3 = await sel.locator('option[value="3"]').count().catch(() => 0);
|
|
if (has3) await sel.selectOption('3');
|
|
}
|
|
const tb = dlg.locator('input, textarea, [role="textbox"]').first();
|
|
if (await tb.isVisible().catch(() => false)) await tb.fill('3');
|
|
const saveBtn = dlg.getByRole('button', { name: /^(Save|Save Ratings|Confirm|Done)$/i });
|
|
if (await saveBtn.isVisible({ timeout: 800 }).catch(() => false)) await saveBtn.click();
|
|
await overlay.waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {});
|
|
}
|
|
}
|
|
|
|
// Row → Plan your Education/Skills
|
|
const table = page.locator('table');
|
|
await table.waitFor({ state: 'attached', timeout: 8000 }).catch(() => {});
|
|
let row = null;
|
|
if (tileTitle) {
|
|
const cell = table.getByText(new RegExp(tileTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i')).first();
|
|
if (await cell.isVisible().catch(() => false)) row = cell.locator('xpath=ancestor::tr').first();
|
|
}
|
|
if (!row) row = table.locator('tbody tr').first();
|
|
await expect(row).toBeVisible({ timeout: 8000 });
|
|
|
|
page.once('dialog', d => d.accept());
|
|
await row.getByRole('button', { name: /Plan your Education\/Skills/i }).click();
|
|
|
|
// Land on Educational Programs
|
|
await expect(page).toHaveURL(/\/educational-programs(\?|$)/, { timeout: 20000 });
|
|
|
|
// -------- Assertions (NO SOC/CIP in UI) --------
|
|
// A) localStorage handoff exists
|
|
const selected = await page.evaluate(() => {
|
|
try { return JSON.parse(localStorage.getItem('selectedCareer') || 'null'); }
|
|
catch { return null; }
|
|
});
|
|
expect(selected).toBeTruthy();
|
|
expect(typeof selected.title).toBe('string');
|
|
|
|
// B) Title shown in a unique place:
|
|
// Prefer the “Schools for: {title}” heading; fallback to “Currently selected: {title}”
|
|
function escRe(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
|
if (selected.title) {
|
|
// get the Schools-for heading, then check it contains the title
|
|
const schoolsH2 = page.getByRole('heading', { level: 2 }).filter({ hasText: /^Schools for:/i }).first();
|
|
let ok = false;
|
|
if (await schoolsH2.isVisible({ timeout: 4000 }).catch(() => false)) {
|
|
const htxt = (await schoolsH2.textContent()) || '';
|
|
ok = new RegExp(`\\b${escRe(selected.title)}\\b`, 'i').test(htxt);
|
|
}
|
|
if (!ok) {
|
|
// fallback to the “Currently selected:” paragraph line
|
|
const line = page.locator('p.text-gray-700')
|
|
.filter({ hasText: /^Currently selected:/i })
|
|
.first();
|
|
if (await line.isVisible({ timeout: 3000 }).catch(() => false)) {
|
|
const txt = (await line.textContent()) || '';
|
|
ok = new RegExp(`\\b${escRe(selected.title)}\\b`, 'i').test(txt);
|
|
}
|
|
}
|
|
expect(ok).toBeTruthy();
|
|
}
|
|
|
|
// C) KSA header appears (tolerant): “Knowledge, Skills, and Abilities needed for: {careerTitle}”
|
|
const ksaHeader = page.getByRole('heading', { name: /Knowledge, Skills, and Abilities needed for:/i });
|
|
const hasKsaHeader = await ksaHeader.first().isVisible({ timeout: 8000 }).catch(() => false);
|
|
expect(hasKsaHeader).toBeTruthy();
|
|
|
|
// D) At least one school card is rendered with “Program:” and “Degree Type:” and a “Select School” button
|
|
const programText = page.getByText(/^Program:\s*/i).first();
|
|
const degreeText = page.getByText(/^Degree Type:\s*/i).first();
|
|
const selectBtn = page.getByRole('button', { name: /^Select School$/i }).first();
|
|
|
|
// Allow a little time for the IPEDS fetch + optional geocode
|
|
await expect(programText).toBeVisible({ timeout: 30000 });
|
|
await expect(degreeText).toBeVisible({ timeout: 30000 });
|
|
await expect(selectBtn).toBeVisible({ timeout: 30000 });
|
|
|
|
// E) Reload: the page should rehydrate from localStorage (title/schools still present)
|
|
await page.reload({ waitUntil: 'networkidle' });
|
|
|
|
// After reload, check that at least the “Schools for:” header (or the currently selected line) is present again
|
|
const schoolsForAfter = page.getByRole('heading', { name: /Schools for:/i }).first();
|
|
const currentSelAfter = page.getByText(/Currently selected:/i).first();
|
|
const headerOk = await schoolsForAfter.isVisible({ timeout: 8000 }).catch(() => false)
|
|
|| await currentSelAfter.isVisible({ timeout: 8000 }).catch(() => false);
|
|
expect(headerOk).toBeTruthy();
|
|
|
|
// And at least one “Program:” line is visible again
|
|
await expect(page.getByText(/^Program:\s*/i).first()).toBeVisible({ timeout: 15000 });
|
|
});
|
|
}); |