fix(ui): verify responsive accessibility and restore Career label
This commit is contained in:
@@ -166,8 +166,34 @@ for (const [language, theme, width] of [['en', 'light', 1920], ['nb', 'dark', 36
|
||||
});
|
||||
}
|
||||
|
||||
for (const [language, theme, width] of [['nb', 'dark', 320], ['en', 'light', 390]] as const) {
|
||||
test(`[P6-03] ${language}/${theme} Career selector floating label is not clipped at ${width}px`, async ({ page, owner }, info) => {
|
||||
const sample: Sample = { area: 'profile', path: '/career?section=profile', width, language, theme };
|
||||
await enter(page, owner, sample);
|
||||
const navigation = page.getByRole('navigation', { name: translations[language].careerWorkspaceNavigation, exact: true });
|
||||
const label = navigation.locator('#career-workspace-section-label');
|
||||
const bounds = await label.evaluate(element => { const label = element.getBoundingClientRect(); const nav = element.closest('nav')!.getBoundingClientRect(); return { labelTop: label.top, labelBottom: label.bottom, navTop: nav.top, navBottom: nav.bottom }; });
|
||||
await info.attach('career-selector-label-geometry', { body: JSON.stringify({ sample, bounds, proofKind: 'Native CSS label bounds versus clipping navigation container; no human/device acceptance' }), contentType: 'application/json' });
|
||||
expect(bounds.labelTop).toBeGreaterThanOrEqual(bounds.navTop);
|
||||
expect(bounds.labelBottom).toBeLessThanOrEqual(bounds.navBottom);
|
||||
const evidence = await collect(page, info, sample, 'career-selector-label');
|
||||
expect(evidence.geometry.pageHorizontalOverflow).toBe(0);
|
||||
await navigation.getByRole('combobox').press('Enter');
|
||||
await page.getByRole('option', { name: translations[language].careerWorkspaceOverviewTab, exact: true }).press('Enter');
|
||||
await expect(page).toHaveURL(/\/career\/?$/);
|
||||
await expect(navigation.getByRole('combobox')).toHaveText(translations[language].careerWorkspaceOverviewTab);
|
||||
});
|
||||
}
|
||||
|
||||
test('[P6-04] measured native route, long CV and complete large Board workflows', async ({ page, owner, context }, info) => {
|
||||
test.setTimeout(360_000);
|
||||
const profileResponse = await page.request.get(`${apiUrl}/career/profile`);
|
||||
expect(profileResponse.status()).toBe(200);
|
||||
const profile = (await profileResponse.json()).profile;
|
||||
const sentence = 'Documented accessible services and reliable user workflows. Dokumenterte tilgjengelige tjenester og pålitelige arbeidsflyter. ';
|
||||
profile.jobs = Array.from({ length: 8 }, (_, index) => ({ id: `performance-role-${index}`, title: `Measured platform developer ${index + 1}`, company: 'Nordlys Audit AS', startDate: '2020-01', endDate: '2025-12',
|
||||
bullets: Array.from({ length: 4 }, (_, bullet) => `Evidence ${index + 1}.${bullet + 1}: ${sentence.repeat(3)}`) }));
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: { profile } })).status()).toBe(200);
|
||||
await context.addInitScript(() => {
|
||||
const observations = { longTasks: [] as number[], layoutShift: 0 };
|
||||
Object.assign(window, { phase6Performance: observations });
|
||||
@@ -206,7 +232,7 @@ test('[P6-04] measured native route, long CV and complete large Board workflows'
|
||||
const heading = (name: string) => async () => { await expect(page.getByRole('heading', { level: 1, name, exact: true })).toBeVisible(); };
|
||||
const t = translations.en;
|
||||
await measure('initial Jobs load', '/jobs', heading(t.jobApplications));
|
||||
await measure('Dashboard', '/dashboard', async () => { await expect(page.getByRole('heading', { name: t.dashboard, exact: true })).toBeVisible(); await expect(page.getByRole('progressbar')).toHaveCount(0); });
|
||||
await measure('Dashboard', '/dashboard', async () => { await expect(page.getByRole('heading', { name: t.dashboard, exact: true })).toBeVisible(); await expect(page.getByRole('link', { name: 'Tracked jobs: 6', exact: true })).toBeVisible(); });
|
||||
await measure('Analytics 6 months', '/analytics?months=6', heading(t.analytics));
|
||||
await measure('Analytics 24 months', '/analytics?months=24', heading(t.analytics));
|
||||
await measure('Job details', `/jobs/${owner.jobId}`, async () => { await expect(page.getByRole('heading', { name: /^Audit platform engineer 1/ })).toBeVisible(); });
|
||||
@@ -244,6 +270,40 @@ test('[P6-04] measured native route, long CV and complete large Board workflows'
|
||||
await measure(`Table ${size}`, '/jobs?view=table', heading(t.jobApplications));
|
||||
await measure(`Filtered Board ${size}`, '/jobs?view=board&q=Phase6%20capacity', async () => { await expect(page.locator('[data-kanban-card-id]')).toHaveCount(size - 6); });
|
||||
}
|
||||
const clientTimeOrigin = await page.evaluate(() => performance.timeOrigin);
|
||||
const clientRequests: string[] = [];
|
||||
const trackClient = (request: import('@playwright/test').Request) => { const url = new URL(request.url()); if (url.pathname.startsWith('/api/')) clientRequests.push(`${request.method()} ${url.pathname}${url.search}`); };
|
||||
page.on('request', trackClient);
|
||||
const tableLoaded = page.waitForResponse(response => new URL(response.url()).pathname === '/api/jobapplications' && response.request().method() === 'GET');
|
||||
let interactionStart = Date.now();
|
||||
await page.getByRole('link', { name: t.jobsTableView, exact: true }).click();
|
||||
const tableResponse = await tableLoaded;
|
||||
expect(tableResponse.status()).toBe(200);
|
||||
expect((await tableResponse.json()).total).toBe(994);
|
||||
await expect(page.locator('[data-job-row-id]').first()).toBeVisible();
|
||||
observations.push({ name: 'Client Board to Table retaining the large query', readyMs: Date.now() - interactionStart });
|
||||
interactionStart = Date.now();
|
||||
await page.getByRole('link', { name: t.jobsBoardView, exact: true }).click();
|
||||
await expect(page.locator('[data-kanban-card-id]')).toHaveCount(994);
|
||||
observations.push({ name: 'Client Table to Board retaining the large query', readyMs: Date.now() - interactionStart });
|
||||
const searched = page.waitForResponse(response => { const url = new URL(response.url()); return url.pathname === '/api/jobapplications/board' && url.searchParams.get('q') === 'Phase6 capacity 1000'; });
|
||||
interactionStart = Date.now();
|
||||
await page.getByRole('textbox', { name: t.jobTableSearch, exact: true }).fill('Phase6 capacity 1000');
|
||||
const searchResponse = await searched;
|
||||
expect(searchResponse.status()).toBe(200);
|
||||
expect(await searchResponse.json()).toHaveLength(1);
|
||||
await expect(page.locator('[data-kanban-card-id]')).toHaveCount(1);
|
||||
await expect(page.locator('[data-kanban-card-id]')).toContainText('Phase6 capacity 1000');
|
||||
observations.push({ name: 'Large Jobs search including existing debounce', readyMs: Date.now() - interactionStart });
|
||||
const dashboardLoaded = page.waitForResponse(response => new URL(response.url()).pathname === '/api/jobapplications/stats');
|
||||
interactionStart = Date.now();
|
||||
await page.getByRole('navigation', { name: t.navigation, exact: true }).getByRole('button', { name: t.dashboard, exact: true }).click();
|
||||
expect((await dashboardLoaded).status()).toBe(200);
|
||||
await expect(page.getByRole('link', { name: 'Tracked jobs: 1,000', exact: true })).toBeVisible();
|
||||
observations.push({ name: 'Client Jobs to Dashboard navigation', readyMs: Date.now() - interactionStart });
|
||||
expect(await page.evaluate(() => performance.timeOrigin)).toBe(clientTimeOrigin);
|
||||
page.off('request', trackClient);
|
||||
observations.push({ name: 'Client interactions request queries; unchanged document timeOrigin', timeOrigin: clientTimeOrigin, requests: clientRequests });
|
||||
await info.attach('phase6-performance', { body: JSON.stringify({ browserVersion: context.browser()?.version(), viewport: page.viewportSize(), environment: { platform: process.platform, application: process.env.PLAYWRIGHT_STATIC_EXPORT === 'true' ? 'production static export' : 'Next development server', prebuiltExport: process.env.PLAYWRIGHT_PREBUILT_EXPORT === 'true', serverReuseAllowed: process.env.PLAYWRIGHT_REUSE_SERVERS === 'true', database: process.env.PLAYWRIGHT_REUSE_SERVERS === 'true' ? 'reused server database not independently verified' : 'isolated synthetic SQLite from native test configuration', api: 'Release API from native test configuration' }, observations, limits: ['Single reproducible diagnostic, no production budget or device qualification', 'Hard-navigation measurements include boot/auth; no React render-count inference', 'CDP heap samples are observations, not a leak diagnosis'] }), contentType: 'application/json' });
|
||||
});
|
||||
|
||||
@@ -586,6 +646,11 @@ test.describe('Chromium emulated touch; not physical-device acceptance', () => {
|
||||
const navigation = page.getByRole('button', { name: t.openNavigation, exact: true });
|
||||
await navigation.tap(); await expect(page.getByRole('dialog', { name: t.navigation, exact: true })).toBeVisible();
|
||||
await collect(page, info, { ...sample, area: 'shell' }, 'emulated-touch-navigation');
|
||||
const drawer = page.getByRole('dialog', { name: t.navigation, exact: true });
|
||||
await expect(drawer.getByRole('button', { name: t.dashboard, exact: true })).toBeFocused();
|
||||
for (const name of [t.discoverJobs, t.companies, t.offerComparisonNav, t.trash]) await expect(drawer.getByRole('button', { name, exact: true })).toBeVisible();
|
||||
const settled = await collect(page, info, { ...sample, area: 'shell' }, 'emulated-touch-navigation-settled');
|
||||
expect(settled.axe.violations.filter(rule => rule.id === 'aria-command-name')).toEqual([]);
|
||||
await page.keyboard.press('Escape'); await expect.soft(navigation).toBeFocused();
|
||||
await info.attach('emulated-touch-boundary', { body: JSON.stringify({ hasTouch: true, input: 'Playwright Chromium touch tap', portraitLandscape: 'CSS viewport changes, not physical orientation sensor', physicalTouch: 'NOT VERIFIED', nativeTouchDrag: 'NOT VERIFIED', softwareKeyboard: 'NOT VERIFIED', cvUnchanged: true }), contentType: 'application/json' });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user