// tests/e2e/07-education-handoff.spec.mjs // @ts-check import { test, expect } from '@playwright/test'; import { loadTestUser } from '../utils/testUser.js'; test.describe('@p1 Education/Skills handoff', () => { test.setTimeout(20000); test('add to comparison → Plan your Education/Skills → navigates with selectedCareer stored', async ({ page }) => { const user = loadTestUser(); const TIME = { overlayAppear: 2000, cache: 60000, tile: 8000, confirm: 7000, route: 20000, }; // Helpers async function closeAnyOverlay() { const overlay = page.locator('div.fixed.inset-0'); if (!(await overlay.isVisible({ timeout: 500 }).catch(() => false))) return; const dlg = overlay.locator('div[role="dialog"], div.bg-white').first(); // Fill selects with neutral '3' (or first non-empty) 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'); else { 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 textbox if present const tb = dlg.locator('input, textarea, [role="textbox"]').first(); if (await tb.isVisible().catch(() => false)) await tb.fill('3'); const save = dlg.getByRole('button', { name: /(Save|Continue|Done|OK)/i }); const cancel = dlg.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(() => {}); } 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); } async function ensureSuggestions() { const firstTile = page.locator('div.grid button').first(); if (await firstTile.isVisible({ timeout: 1500 }).catch(() => false)) return; const reloadBtn = page.getByRole('button', { name: /Reload Career Suggestions/i }); await expect(reloadBtn).toBeVisible(); await closeAnyOverlay(); await reloadBtn.click(); // Let overlay mount if it appears (we don't require 100%) const overlayText = page.getByText(/Loading Career Suggestions/i).first(); await overlayText.isVisible({ timeout: TIME.overlayAppear }).catch(() => {}); await waitForSuggestionsCache(); } // 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 }); // Explorer await page.goto('/career-explorer', { waitUntil: 'networkidle' }); await expect(page.getByRole('heading', { name: /Explore Careers - use these tools/i })).toBeVisible(); await closeAnyOverlay(); // Make sure we have suggestions await ensureSuggestions(); // Open a suggestion tile → CareerModal const firstTile = page.locator('div.grid button').first(); await expect(firstTile).toBeVisible({ timeout: TIME.tile }); let clickedTitle = (await firstTile.textContent())?.replace('⚠️', '').trim() || null; await firstTile.click(); // Prefer modal H2 for authoritative title const modalH2 = page.getByRole('heading', { level: 2 }); if (await modalH2.isVisible({ timeout: TIME.confirm }).catch(() => false)) { const t = await modalH2.first().textContent().catch(() => null); if (t) clickedTitle = t.trim(); } // Add to Comparison await expect(page.getByRole('button', { name: /Add to Comparison/i })).toBeVisible({ timeout: TIME.confirm }); await page.getByRole('button', { name: /Add to Comparison/i }).click(); // Ratings modal → Save (must persist the row) { 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(); // Fill selects to '3' if present 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'); } // Fill textbox if present 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: 1000 }).catch(() => false)) { await saveBtn.click(); } else { await page.keyboard.press('Enter'); } await overlay.waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {}); } } // Find the row for clickedTitle (fallback: first row) const table = page.locator('table'); await table.waitFor({ state: 'attached', timeout: 5000 }).catch(() => {}); let targetRow; if (clickedTitle) { const cell = table.getByText(new RegExp(clickedTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i')).first(); if (await cell.isVisible().catch(() => false)) { targetRow = cell.locator('xpath=ancestor::tr').first(); } } if (!targetRow) { targetRow = table.locator('tbody tr').first(); } // Click “Plan your Education/Skills” with confirm page.once('dialog', d => d.accept()); await targetRow.getByRole('button', { name: /Plan your Education\/Skills/i }).click(); // Route change await page.waitForURL('**/educational-programs**', { timeout: TIME.route }); // Assert selectedCareer is stored with expected shape const selected = await page.evaluate(() => { try { return JSON.parse(localStorage.getItem('selectedCareer') || 'null'); } catch { return null; } }); expect(selected).toBeTruthy(); expect(typeof selected.soc_code).toBe('string'); expect(Array.isArray(selected.cip_code)).toBeTruthy(); }); });