// @ts-nocheck import { test, expect } from '@playwright/test'; import { loadTestUser } from '../utils/testUser.js'; test.describe('@p0 Educational Programs — handoff & page render', () => { test.setTimeout(15000); 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 }); }); });