179 lines
7.1 KiB
JavaScript
179 lines
7.1 KiB
JavaScript
// 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();
|
|
});
|
|
});
|