import { test, expect } from '../../../../../job-tracker-ui/node_modules/@playwright/test'; import AxeBuilder from '../../../../../job-tracker-ui/node_modules/@axe-core/playwright'; import { translations } from '../../../../../job-tracker-ui/src/i18n/translations'; const api = 'http://localhost:5302/api'; for (const language of ['en', 'nb'] as const) for (const theme of ['light', 'dark'] as const) { test(`P9 settled badge drawer and editor triage ${language} ${theme}`, async ({ page, context, playwright }, info) => { test.setTimeout(120000); const t = translations[language]; await page.setViewportSize({ width: 320, height: 900 }); await page.emulateMedia({ reducedMotion: 'reduce' }); await context.addInitScript(({ language, theme }) => { localStorage.setItem('uiLanguage', language); localStorage.setItem('jobtracker.themeMode', theme); }, { language, theme }); await context.route('**/*', route => { const url = new URL(route.request().url()); return ['localhost', '127.0.0.1'].includes(url.hostname) || ['data:', 'blob:', 'about:'].includes(url.protocol) ? route.continue() : route.abort(); }); const admin = await playwright.request.newContext(); try { const password = 'E2ePassword123!'; expect((await admin.post(`${api}/auth/login`, { data: { email: 'e2e@example.test', password } })).status()).toBe(200); const adminMe = await (await admin.get(`${api}/auth/me`)).json(); const csrf = (await admin.storageState()).cookies.find(cookie => cookie.name === 'XSRF-TOKEN')!.value; const email = `p9-axe-${language}-${theme}-${Date.now()}@example.test`; expect((await admin.post(`${api}/users`, { headers: { 'X-CSRF-TOKEN': csrf, 'X-Session-Context': adminMe.sessionContext }, data: { email, password, roles: [] } })).status()).toBe(200); expect((await page.request.post(`${api}/auth/login`, { data: { email, password } })).status()).toBe(200); const me = await (await page.request.get(`${api}/auth/me`)).json(); const ownerCsrf = (await page.request.storageState()).cookies.find(cookie => cookie.name === 'XSRF-TOKEN')!.value; expect((await page.request.put(`${api}/auth/preferences/language`, { headers: { 'X-CSRF-TOKEN': ownerCsrf, 'X-Session-Context': me.sessionContext }, data: { language: language === 'nb' ? 'nb-NO' : 'en' } })).status()).toBe(204); await page.goto('/dashboard'); await expect(page.getByRole('button', { name: t.dashboardCustomize, exact: true })).toBeVisible(); const badge = await page.locator('.MuiBadge-invisible').evaluateAll(nodes => nodes.map(node => ({ ariaHidden: node.getAttribute('aria-hidden'), text: node.textContent, width: node.getBoundingClientRect().width, height: node.getBoundingClientRect().height, visibility: getComputedStyle(node).visibility, transform: getComputedStyle(node).transform }))); expect(badge.length).toBeGreaterThan(0); expect(badge.every(node => node.ariaHidden === 'true' && node.width * node.height === 0)).toBe(true); const scans: object[] = []; const scan = async (state: string) => { const result = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze(); scans.push({ state, violations: result.violations, incomplete: result.incomplete }); expect.soft(result.violations, state).toEqual([]); }; await scan('dashboard-settled'); const trigger = page.getByRole('button', { name: t.openNavigation, exact: true }); await trigger.focus(); await trigger.press('Enter'); const drawer = page.getByRole('dialog', { name: t.navigation, exact: true }); await expect(drawer.getByRole('button', { name: t.dashboard, exact: true })).toBeFocused(); await scan('drawer-open'); await page.keyboard.press('Escape'); await expect(drawer).not.toBeVisible(); await expect(trigger).toBeFocused(); // Backdrop/root exit can precede Slide exit; await the complete settled predicate. const inspectDrawer = () => page.locator('.MuiDrawer-modal').evaluateAll(nodes => nodes.map(node => ({ ariaHidden: node.getAttribute('aria-hidden'), visibility: getComputedStyle(node).visibility, containsActive: node.contains(document.activeElement), visibleTabbables: [...node.querySelectorAll('[tabindex="0"],button,a[href],input,select')].filter(element => getComputedStyle(element).visibility !== 'hidden' && element.getClientRects().length > 0).length }))); const drawerAfterEscape = await inspectDrawer(); await expect.poll(async () => (await inspectDrawer()).every(node => node.ariaHidden === 'true' && node.visibility === 'hidden' && !node.containsActive && node.visibleTabbables === 0)).toBe(true); const hiddenDrawer = await inspectDrawer(); expect(hiddenDrawer.every(node => node.ariaHidden === 'true' && !node.containsActive && node.visibleTabbables === 0)).toBe(true); for (let index = 0; index < 24; index++) { await page.keyboard.press('Tab'); expect(await page.evaluate(() => !!document.activeElement?.closest('.MuiDrawer-modal[aria-hidden="true"]'))).toBe(false); } await scan('drawer-closed-settled'); await page.getByRole('button', { name: t.dashboardCustomize, exact: true }).click(); const editor = page.getByRole('region', { name: t.dashboardEditing, exact: true }); await expect(editor.getByRole('combobox')).toHaveCount(9); await scan('editor-settled'); await info.attach('phase9-settled-axe', { body: JSON.stringify({ language, theme, badge, drawerAfterEscape, hiddenDrawer, scans, limits: 'Finite native DOM/keyboard evidence; remaining overlapping background contrast and actual screen-reader perception still require human assessment.' }, null, 2), contentType: 'application/json' }); } finally { await admin.dispose(); } }); }