test(dashboard): qualify responsive layout and preview semantics

This commit is contained in:
cesnimda
2026-10-21 21:21:22 +02:00
parent 77968da90c
commit 9604589785
8 changed files with 491 additions and 12 deletions
@@ -1,6 +1,7 @@
import { expect, test, type APIRequestContext, type Page, type TestInfo } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
import { translations } from '../src/i18n/translations';
import { defaultDashboardLayout } from '../src/dashboardLayout';
const apiUrl = 'http://localhost:5302/api';
const password = 'E2ePassword123!';
@@ -79,8 +80,11 @@ test('[PRES-S01 S02 E07 E08 E09 J05 J09] Analytics native reports, direct entry,
await page.getByRole('link', { name: 'Nordlys langsiktige karrieremuligheter æøå', exact: true }).click();
await expect(page).toHaveURL(new RegExp(`/jobs\\?companyId=${companyId}$`));
await expect(page.locator('[data-job-row-id]')).toHaveCount(6); await page.goBack();
// Retained Dashboard flags must not remove the independent report destination.
// A fully hidden saved Dashboard must not remove the independent report destination.
await page.evaluate(owner => localStorage.setItem(`dashboardPrefs:${owner}`, JSON.stringify({ cards: false, activity: false, funnel: false, companies: false, skills: false })), me.id);
const hidden = defaultDashboardLayout();
hidden.widgets.forEach(widget => { widget.visible = false; });
expect((await page.request.put(`${apiUrl}/auth/preferences/dashboard`, { headers, data: hidden })).status()).toBe(200);
await page.goto('/dashboard'); await expect(page.getByText('Your dashboard sections are hidden.')).toBeVisible();
await page.getByRole('button', { name: 'Analytics', exact: true }).click();
await expect(activity.getByRole('table')).toBeVisible();
@@ -89,16 +89,18 @@ test('[PRES-S01 S02 E07 E08 E09] Dashboard native data, visibility and responsiv
}
const customize = page.getByRole('button', { name: t.dashboardCustomize, exact: true });
await customize.focus(); await page.keyboard.press('Enter');
await expect(page.getByRole('menuitemcheckbox').first()).toBeFocused();
const editor = page.getByRole('region', { name: t.dashboardEditing, exact: true });
await expect(editor).toBeVisible();
await expect(editor.locator('..')).toBeFocused();
await expect(editor.getByRole('list', { name: t.dashboardManageWidgets })).toBeVisible();
await expect(editor.getByRole('checkbox')).toHaveCount(9);
if (language === 'en') {
// Measure settled colors, not the menu's entry-opacity transition.
await expect.poll(() => page.getByRole('menu').evaluate(menu => getComputedStyle(menu.parentElement!).opacity)).toBe('1');
const menuScan = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze();
await info.attach(`dashboard-${mode}-menu-axe`, { body: JSON.stringify({ violations: menuScan.violations, incomplete: menuScan.incomplete }), contentType: 'application/json' });
expect(menuScan.violations).toEqual([]);
const editorScan = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze();
await info.attach(`dashboard-${mode}-editor-axe`, { body: JSON.stringify({ violations: editorScan.violations, incomplete: editorScan.incomplete }), contentType: 'application/json' });
expect(editorScan.violations).toEqual([]);
}
await page.keyboard.press('Escape'); await expect(customize).toBeFocused();
await expect(page.locator('[role="menuitemcheckbox"]')).toHaveCount(0);
await expect(editor).toHaveCount(0);
if (language === 'en') {
const result = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze();
await info.attach(`dashboard-${mode}-axe`, { body: JSON.stringify({ violations: result.violations, incomplete: result.incomplete }), contentType: 'application/json' });
@@ -115,8 +117,9 @@ test('[PRES-S01 S02 E07 E08 E09] Dashboard native data, visibility and responsiv
expect((await page.request.put(`${apiUrl}/auth/preferences/language`, { headers, data: { language: 'en' } })).status()).toBe(204);
await page.evaluate(() => localStorage.setItem('uiLanguage', 'en')); await page.reload();
await page.getByRole('button', { name: 'Customize dashboard' }).click();
for (const item of await page.getByRole('menuitemcheckbox').all()) await item.click();
await page.keyboard.press('Escape');
for (const item of await page.getByRole('region', { name: 'Editing dashboard', exact: true }).getByRole('checkbox').all()) await item.uncheck();
await page.getByRole('button', { name: 'Save layout', exact: true }).click();
await expect(page.getByRole('region', { name: 'Editing dashboard', exact: true })).toHaveCount(0);
await expect(page.getByText('Your dashboard sections are hidden.')).toBeVisible();
await page.reload();
await expect(page.getByText('Your dashboard sections are hidden.')).toBeVisible();
@@ -137,7 +140,9 @@ test('[PRES-S01 S02 E07 E08 E09] Dashboard native data, visibility and responsiv
await expect(page.getByText('Nordlys teknologi og fellestjenester æøå', { exact: true })).toHaveCount(0);
await capture(page, info, 'dashboard-new-account');
await page.getByRole('button', { name: 'Customize dashboard' }).click();
for (const item of await page.getByRole('menuitemcheckbox').all()) await expect(item).toHaveAttribute('aria-checked', 'true');
const newAccountEditor = page.getByRole('region', { name: 'Editing dashboard', exact: true });
await expect(newAccountEditor.getByRole('checkbox')).toHaveCount(9);
for (const item of await newAccountEditor.getByRole('checkbox').all()) await expect(item).toBeChecked();
await page.keyboard.press('Escape');
const b = await login(page.request, emailB);
const bCompany = await (await page.request.post(`${apiUrl}/companies`, { headers: b.headers, data: { name: 'New search' } })).json();
@@ -0,0 +1,330 @@
import { expect, test as base, type APIRequestContext, type BrowserContext, type Page, type TestInfo } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
import { translations } from '../src/i18n/translations';
import type { DashboardLayout } from '../src/dashboardLayout';
const apiUrl = 'http://localhost:5302/api';
const layoutUrl = `${apiUrl}/auth/preferences/dashboard`;
const password = 'E2ePassword123!';
const ids = ['cards', 'attention', 'funnel', 'activity', 'stageAge', 'sources', 'salary', 'companies', 'skills'];
type Account = { email: string; headers: Record<string, string> };
async function login(api: APIRequestContext, email: string) {
expect((await api.post(`${apiUrl}/auth/login`, { data: { email, password } })).status()).toBe(200);
const me = await (await api.get(`${apiUrl}/auth/me`)).json();
const csrf = (await api.storageState()).cookies.find(cookie => cookie.name === 'XSRF-TOKEN')!.value;
return { 'X-CSRF-TOKEN': csrf, 'X-Session-Context': me.sessionContext as string };
}
async function localOnly(context: BrowserContext) {
await context.route('**/*', route => ['localhost', '127.0.0.1'].includes(new URL(route.request().url()).hostname) ? route.continue() : route.abort());
}
async function readLayout(api: APIRequestContext): Promise<DashboardLayout> {
const response = await api.get(layoutUrl);
expect(response.status()).toBe(200);
return response.json();
}
const editor = (page: Page) => page.getByRole('region', { name: 'Editing dashboard', exact: true });
async function openEditor(page: Page) {
await page.getByRole('button', { name: 'Customize dashboard', exact: true }).click();
await expect(editor(page).getByRole('checkbox')).toHaveCount(9);
}
async function save(page: Page) {
const response = page.waitForResponse(response => response.url() === layoutUrl && response.request().method() === 'PUT');
await editor(page).getByRole('button', { name: 'Save layout', exact: true }).click();
expect((await response).status()).toBe(200);
await expect(editor(page)).toHaveCount(0);
}
async function confirm(page: Page, name: string) {
await page.getByRole('dialog', { name, exact: true }).getByRole('button', { name, exact: true }).click();
}
async function capture(page: Page, info: TestInfo, name: string) {
const path = info.outputPath(`${name}.png`);
await page.screenshot({ path, fullPage: true });
await info.attach(name, { path, contentType: 'image/png' });
}
async function signOut(page: Page) {
await page.getByRole('button', { name: 'User', exact: true }).click();
await page.getByRole('menuitem', { name: 'Sign out', exact: true }).click();
await expect(page).toHaveURL(/\/login\?sessionChanged=1$/);
}
async function signIn(page: Page, email: string) {
await page.getByLabel('Username or email', { exact: true }).fill(email);
await page.getByLabel('Current password', { exact: true }).fill(password);
await page.getByRole('button', { name: 'Sign in', exact: true }).click();
await expect(page).toHaveURL(/\/dashboard$/);
}
const test = base.extend<{ account: Account }>({
account: async ({ page, playwright }, provide) => {
const admin = await playwright.request.newContext();
try {
const adminHeaders = await login(admin, 'e2e@example.test');
const email = `phase8-${Date.now()}-${Math.random().toString(36).slice(2)}@example.test`;
expect((await admin.post(`${apiUrl}/users`, { headers: adminHeaders, data: { email, password, roles: [] } })).status()).toBe(200);
const headers = await login(page.request, email);
const company = await page.request.post(`${apiUrl}/companies`, { headers, data: { name: 'Phase eight fixture' } });
expect(company.status()).toBe(201);
const companyId = (await company.json()).id;
expect((await page.request.post(`${apiUrl}/jobapplications`, { headers, data: { companyId, jobTitle: 'Local fixture developer', status: 'Applied', dateApplied: new Date().toISOString(), tags: 'TypeScript', source: 'NAV' } })).status()).toBe(201);
await localOnly(page.context());
await page.goto('/dashboard');
await expect(page.getByRole('link', { name: 'Tracked jobs: 1', exact: true })).toBeVisible();
const dismiss = page.getByRole('button', { name: 'Dismiss setup checklist' });
if (await dismiss.isVisible()) await dismiss.click();
await provide({ email, headers });
} finally { await admin.dispose(); }
},
});
test('[P8] Saved order, size and visibility survive HTTP readback, a fresh browser and logout/login', async ({ page, browser, account }, info) => {
await openEditor(page);
await editor(page).getByRole('button', { name: 'Move Current pipeline earlier', exact: true }).click();
await editor(page).getByRole('combobox', { name: 'Size for Current pipeline', exact: true }).selectOption('full');
await editor(page).getByRole('checkbox', { name: 'Application activity', exact: true }).uncheck();
await save(page);
const saved = await readLayout(page.request);
expect(saved.version).toBe(1); expect(saved.revision).toBe(1);
expect(saved.widgets.map(widget => widget.id)).toEqual(['cards', 'funnel', 'attention', 'activity', 'stageAge', 'sources', 'salary', 'companies', 'skills']);
expect(saved.widgets.find(widget => widget.id === 'funnel')).toEqual({ id: 'funnel', visible: true, size: 'full' });
expect(saved.widgets.find(widget => widget.id === 'activity')?.visible).toBe(false);
const assertSaved = async (target: Page) => {
await expect(target.locator('[data-dashboard-widget="activity"]')).toHaveCount(0);
await expect(target.locator('[data-dashboard-widget="funnel"]')).toHaveAttribute('data-widget-size', 'full');
await expect.poll(() => target.locator('[data-dashboard-widget]').evaluateAll(nodes => nodes.map(node => node.getAttribute('data-dashboard-widget')))).toEqual(saved.widgets.filter(widget => widget.visible).map(widget => widget.id));
};
await page.reload(); await assertSaved(page);
const fresh = await browser.newContext({ baseURL: 'http://localhost:3300' });
try {
await localOnly(fresh);
await login(fresh.request, account.email);
const other = await fresh.newPage(); await other.goto('/dashboard'); await assertSaved(other);
expect(await readLayout(fresh.request)).toEqual(saved);
await capture(other, info, 'fresh-browser-server-layout');
} finally { await fresh.close(); }
await signOut(page); await signIn(page, account.email); await assertSaved(page);
expect(await readLayout(page.request)).toEqual(saved);
});
test('[P8] Entering edit mode keeps its heading and save controls below the fixed app header', async ({ page, account }) => {
expect(account.email).toContain('phase8-');
for (const width of [320, 768, 1440]) {
await page.setViewportSize({ width, height: 900 });
await openEditor(page);
const heading = editor(page).getByRole('heading', { name: 'Editing dashboard', exact: true });
await expect.poll(() => heading.evaluate(element => {
const header = document.querySelector('.MuiAppBar-root');
return element.getBoundingClientRect().top >= (header?.getBoundingClientRect().bottom ?? 0);
})).toBe(true);
await editor(page).getByRole('button', { name: 'Cancel', exact: true }).click();
}
});
test('[P8] Cancel, rejected navigation and reset change only the draft until explicit Save', async ({ page, account }) => {
void account;
await openEditor(page);
await editor(page).getByRole('checkbox', { name: 'Job tags', exact: true }).uncheck();
await save(page);
const before = await readLayout(page.request);
let writes = 0;
page.on('request', request => { if (request.url() === layoutUrl && request.method() === 'PUT') writes++; });
await openEditor(page);
await editor(page).getByRole('checkbox', { name: 'Application activity', exact: true }).uncheck();
await page.getByRole('link', { name: 'Tracked jobs: 1', exact: true }).click();
await page.getByRole('dialog', { name: 'Discard dashboard changes?', exact: true }).getByRole('button', { name: 'Keep editing', exact: true }).click();
await expect(page).toHaveURL(/\/dashboard$/);
await expect(editor(page).getByRole('checkbox', { name: 'Application activity', exact: true })).not.toBeChecked();
await editor(page).getByRole('button', { name: 'Cancel', exact: true }).click();
await page.getByRole('dialog', { name: 'Discard dashboard changes?', exact: true }).getByRole('button', { name: 'Discard changes', exact: true }).click();
await expect(page.getByRole('button', { name: 'Customize dashboard', exact: true })).toBeFocused();
expect(await readLayout(page.request)).toEqual(before); expect(writes).toBe(0);
await openEditor(page);
await editor(page).getByRole('button', { name: 'Reset to default', exact: true }).click();
await page.getByRole('dialog', { name: 'Reset to default', exact: true }).getByRole('button', { name: 'Cancel', exact: true }).click();
await expect(editor(page).getByRole('checkbox', { name: 'Job tags', exact: true })).not.toBeChecked();
await editor(page).getByRole('button', { name: 'Reset to default', exact: true }).click(); await confirm(page, 'Reset to default');
for (const checkbox of await editor(page).getByRole('checkbox').all()) await expect(checkbox).toBeChecked();
expect(await readLayout(page.request)).toEqual(before); expect(writes).toBe(0);
await save(page);
const reset = await readLayout(page.request);
expect(reset.revision).toBe(before.revision + 1); expect(reset.widgets.map(widget => widget.id)).toEqual(ids);
expect(reset.widgets.every(widget => widget.visible)).toBe(true); expect(writes).toBe(1);
});
test('[P8] HTTP 503 and offline transport failure retain draft and support an acknowledged retry', async ({ page, account }, info) => {
void account;
const before = await readLayout(page.request);
await openEditor(page);
await editor(page).getByRole('checkbox', { name: 'Job tags', exact: true }).uncheck();
await editor(page).getByRole('combobox', { name: 'Size for Current pipeline', exact: true }).selectOption('full');
let failure: '503' | 'offline' | null = '503';
await page.route(layoutUrl, route => route.request().method() !== 'PUT' || !failure ? route.continue()
: failure === '503' ? route.fulfill({ status: 503, json: { message: 'Controlled unavailable' } }) : route.abort('internetdisconnected'));
for (const mode of ['503', 'offline'] as const) {
failure = mode;
const failed = mode === '503'
? page.waitForResponse(response => response.url() === layoutUrl && response.status() === 503)
: page.waitForEvent('requestfailed', request => request.url() === layoutUrl && request.method() === 'PUT');
await editor(page).getByRole('button', { name: 'Save layout', exact: true }).click();
await failed;
await expect(editor(page).getByRole('alert')).toContainText('Your layout could not be saved');
await expect(editor(page).getByRole('button', { name: 'Save layout', exact: true })).toBeEnabled();
await expect(editor(page).getByRole('checkbox', { name: 'Job tags', exact: true })).not.toBeChecked();
await expect(editor(page).getByRole('combobox', { name: 'Size for Current pipeline', exact: true })).toHaveValue('full');
expect(await readLayout(page.request)).toEqual(before);
await capture(page, info, `draft-retained-${mode}`);
}
failure = null;
let release!: () => void;
const acknowledgement = new Promise<void>(resolve => { release = resolve; });
await page.route(layoutUrl, async route => {
if (route.request().method() === 'PUT') await acknowledgement;
await route.continue();
});
const saving = save(page);
try {
await expect(editor(page).getByRole('button', { name: 'Saving layout…', exact: true })).toBeDisabled();
await expect(editor(page).getByRole('button', { name: 'Cancel', exact: true })).toBeDisabled();
for (const checkbox of await editor(page).getByRole('checkbox').all()) await expect(checkbox).toBeDisabled();
for (const select of await editor(page).getByRole('combobox').all()) await expect(select).toBeDisabled();
expect(await readLayout(page.request)).toEqual(before);
} finally { release(); }
await saving;
const saved = await readLayout(page.request);
expect(saved.revision).toBe(1); expect(saved.widgets.find(widget => widget.id === 'skills')?.visible).toBe(false);
});
test('[P8] Two authenticated sessions cannot overwrite a stale revision and explicitly reload before saving', async ({ page, browser, account }, info) => {
const second = await browser.newContext({ baseURL: 'http://localhost:3300' });
try {
await localOnly(second); await login(second.request, account.email);
const other = await second.newPage(); await other.goto('/dashboard');
await openEditor(page); await openEditor(other);
await editor(page).getByRole('checkbox', { name: 'Job tags', exact: true }).uncheck(); await save(page);
const firstSaved = await readLayout(page.request);
await editor(other).getByRole('checkbox', { name: 'Application activity', exact: true }).uncheck();
const conflict = other.waitForResponse(response => response.url() === layoutUrl && response.request().method() === 'PUT');
await editor(other).getByRole('button', { name: 'Save layout', exact: true }).click();
expect((await conflict).status()).toBe(409);
await expect(editor(other).getByRole('button', { name: 'Reload latest layout', exact: true })).toBeVisible();
expect(await readLayout(second.request)).toEqual(firstSaved);
await capture(other, info, 'conflicting-layout-draft');
await editor(other).getByRole('button', { name: 'Reload latest layout', exact: true }).click(); await confirm(other, 'Reload latest layout');
await expect(editor(other).getByRole('checkbox', { name: 'Job tags', exact: true })).not.toBeChecked();
await expect(editor(other).getByRole('checkbox', { name: 'Application activity', exact: true })).toBeChecked();
await editor(other).getByRole('checkbox', { name: 'Application activity', exact: true }).uncheck(); await save(other);
const final = await readLayout(page.request);
expect(final.revision).toBe(2);
expect(final.widgets.filter(widget => !widget.visible).map(widget => widget.id)).toEqual(['activity', 'skills']);
} finally { await second.close(); }
});
test('[P8] Anonymous requests fail, ordinary accounts stay independent and administrator data stays forbidden', async ({ page, playwright, account }) => {
const anonymous = await playwright.request.newContext();
const administrator = await playwright.request.newContext();
const other = await playwright.request.newContext();
try {
expect((await anonymous.get(layoutUrl)).status()).toBe(401);
expect((await anonymous.put(layoutUrl, { data: { version: 1, revision: 0, widgets: [] } })).status()).toBe(401);
const adminHeaders = await login(administrator, 'e2e@example.test');
const emailB = `phase8-b-${Date.now()}@example.test`;
expect((await administrator.post(`${apiUrl}/users`, { headers: adminHeaders, data: { email: emailB, password, roles: [] } })).status()).toBe(200);
const headersB = await login(other, emailB);
const untouchedB = await readLayout(other);
expect(untouchedB).toEqual({ version: 1, revision: 0, widgets: [] });
await openEditor(page); await editor(page).getByRole('checkbox', { name: 'Job tags', exact: true }).uncheck(); await save(page);
const savedA = await readLayout(page.request);
expect(await readLayout(other)).toEqual(untouchedB);
expect((await other.put(layoutUrl, { headers: headersB, data: { ...savedA, revision: 0, widgets: savedA.widgets.map(widget => ({ ...widget, visible: widget.id === 'cards' })) } })).status()).toBe(200);
expect(await readLayout(page.request)).toEqual(savedA);
expect((await page.request.get(`${apiUrl}/admin/system`)).status()).toBe(403);
expect((await other.get(`${apiUrl}/admin/system`)).status()).toBe(403);
await signOut(page); await signIn(page, emailB); await openEditor(page);
await expect(editor(page).getByRole('checkbox', { name: 'Summary cards', exact: true })).toBeChecked();
await expect(editor(page).getByRole('checkbox', { name: 'Current pipeline', exact: true })).not.toBeChecked();
await expect(editor(page).getByRole('checkbox', { name: 'Job tags', exact: true })).not.toBeChecked();
expect((await readLayout(page.request)).widgets.filter(widget => widget.visible).map(widget => widget.id)).toEqual(['cards']);
await editor(page).getByRole('button', { name: 'Cancel', exact: true }).click();
await signOut(page); await signIn(page, account.email);
expect(await readLayout(page.request)).toEqual(savedA);
} finally { await anonymous.dispose(); await administrator.dispose(); await other.dispose(); }
});
test('[P8] Five legacy hidden flags migrate only on explicit save; all nine hidden have recovery', async ({ page, account }, info) => {
void account;
expect(await readLayout(page.request)).toEqual({ version: 1, revision: 0, widgets: [] });
await page.evaluate(() => {
const owner = localStorage.getItem('authUserKey');
if (!owner || owner === 'anon') throw new Error('Expected authenticated per-account legacy owner');
localStorage.setItem(`dashboardPrefs:${owner}`, JSON.stringify({ cards: false, activity: false, funnel: false, companies: false, skills: false }));
});
await page.reload(); await openEditor(page);
for (const name of ['Summary cards', 'Application activity', 'Current pipeline', 'Top companies by activity', 'Job tags']) await expect(editor(page).getByRole('checkbox', { name, exact: true })).not.toBeChecked();
expect(await readLayout(page.request)).toEqual({ version: 1, revision: 0, widgets: [] });
await save(page);
expect((await readLayout(page.request)).widgets.filter(widget => !widget.visible).map(widget => widget.id)).toEqual(['cards', 'funnel', 'activity', 'companies', 'skills']);
await page.evaluate(() => { for (const key of Object.keys(localStorage)) if (key.startsWith('dashboardPrefs:')) localStorage.removeItem(key); });
await page.reload(); await openEditor(page);
await expect(editor(page).getByRole('checkbox', { name: 'Summary cards', exact: true })).not.toBeChecked();
for (const checkbox of await editor(page).getByRole('checkbox').all()) await checkbox.uncheck();
await save(page); await page.reload();
await expect(page.getByText('Your dashboard sections are hidden.', { exact: true })).toBeVisible();
await expect(page.locator('[data-dashboard-widget]')).toHaveCount(0);
expect((await readLayout(page.request)).widgets.every(widget => !widget.visible)).toBe(true);
await capture(page, info, 'all-nine-hidden');
await openEditor(page); await editor(page).getByRole('checkbox', { name: 'Current pipeline', exact: true }).check(); await save(page);
await expect(page.locator('[data-dashboard-widget="funnel"]')).toBeVisible();
await expect(page.getByText('Your dashboard sections are hidden.', { exact: true })).toHaveCount(0);
});
test('[P8] Editor reflows across EN/NB themes and desktop/tablet/mobile with native accessible controls', async ({ page, account }, info) => {
test.setTimeout(180000);
for (const language of ['en', 'nb'] as const) for (const mode of ['light', 'dark'] as const) {
const t = translations[language];
await page.evaluate(({ language, mode }) => { localStorage.setItem('uiLanguage', language); localStorage.setItem('jobtracker.themeMode', mode); }, { language, mode });
expect((await page.request.put(`${apiUrl}/auth/preferences/language`, { headers: account.headers, data: { language: language === 'nb' ? 'nb-NO' : 'en' } })).status()).toBe(204);
await page.reload();
await page.getByRole('button', { name: t.dashboardCustomize, exact: true }).click();
const panel = page.getByRole('region', { name: t.dashboardEditing, exact: true });
await expect(panel.getByRole('list', { name: t.dashboardManageWidgets })).toBeVisible();
await expect(panel.getByRole('checkbox')).toHaveCount(9);
await expect(panel.getByRole('combobox')).toHaveCount(9);
await expect(panel.getByRole('combobox').first()).toBeDisabled();
expect(await panel.getByRole('combobox').evaluateAll(nodes => nodes.every(node => node.tagName === 'SELECT'))).toBe(true);
for (const [width, height] of [[1440, 900], [768, 1024], [390, 844], [320, 740]]) {
await page.setViewportSize({ width, height });
await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1)).toBe(true);
await capture(page, info, `editor-${language}-${mode}-${width}`);
}
const axe = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze();
await info.attach(`editor-${language}-${mode}-axe`, { body: JSON.stringify({ violations: axe.violations, incomplete: axe.incomplete }), contentType: 'application/json' });
expect(axe.violations).toEqual([]);
expect(axe.incomplete.filter(result => result.id === 'aria-prohibited-attr')).toEqual([]);
await panel.getByRole('button', { name: t.cancel, exact: true }).click();
await expect(page.getByRole('button', { name: t.dashboardCustomize, exact: true })).toBeFocused();
}
});
test('[P8] Keyboard move focus/live status, pointer drag and emulated touch buttons preserve DOM order', async ({ page, browser, account }, info) => {
await page.setViewportSize({ width: 1440, height: 1000 });
await openEditor(page);
const earlier = editor(page).getByRole('button', { name: 'Move Current pipeline earlier', exact: true });
await earlier.focus(); await page.keyboard.press('Enter');
await expect(earlier).toBeFocused();
await expect(editor(page).getByRole('status')).toHaveText('Current pipeline moved to position 2 of 9.');
await expect.poll(() => editor(page).locator('[data-layout-row]').evaluateAll(nodes => nodes.map(node => node.getAttribute('data-layout-row')))).toEqual(['cards', 'funnel', 'attention', 'activity', 'stageAge', 'sources', 'salary', 'companies', 'skills']);
await editor(page).getByRole('button', { name: 'Drag Current pipeline to reorder', exact: true }).dragTo(editor(page).locator('[data-layout-row="cards"]'));
await expect(editor(page).getByRole('status')).toHaveText('Current pipeline moved to position 1 of 9.');
await expect(editor(page).getByRole('button', { name: 'Move Current pipeline earlier', exact: true })).toBeDisabled();
await save(page);
expect((await readLayout(page.request)).widgets[0].id).toBe('funnel');
const touch = await browser.newContext({ baseURL: 'http://localhost:3300', viewport: { width: 390, height: 844 }, hasTouch: true, isMobile: true });
try {
await localOnly(touch); await login(touch.request, account.email);
const mobile = await touch.newPage(); await mobile.goto('/dashboard');
await mobile.getByRole('button', { name: 'Customize dashboard', exact: true }).tap();
await editor(mobile).getByRole('button', { name: 'Move Current pipeline later', exact: true }).tap();
await expect(editor(mobile).getByRole('status')).toHaveText('Current pipeline moved to position 2 of 9.');
await save(mobile);
expect((await readLayout(touch.request)).widgets.slice(0, 2).map(widget => widget.id)).toEqual(['cards', 'funnel']);
await capture(mobile, info, 'emulated-touch-button-order');
await info.attach('device-evidence-limit', { body: 'Chromium desktop pointer drag and emulated touch button input only. Physical touch, software keyboards and human screen reader acceptance remain untested. Axe incomplete items require review.', contentType: 'text/plain' });
} finally { await touch.close(); }
});