fix(dashboard): keep standard salary amounts readable

This commit is contained in:
cesnimda
2026-10-21 23:49:54 +02:00
parent 9604589785
commit 2f6227fb4f
8 changed files with 78 additions and 2 deletions
+3
View File
@@ -10,8 +10,11 @@ Viewports320/390/768/1440px cover editor EN/Bokmål and light/dark; retained das
Forty-two initial Dashboard/editor PNGs were inspected through four contact sheets and representative full-size crops. Final desktop/light/English and320px/dark/Bokmål editor crops were separately inspected after the focus repair; final raw qualification contains60 distinct screenshots including Analytics. This does not claim an individual full-size inspection of every image. Exact metric/month/sample/source-link and persisted API assertions supply behavioral evidence beyond screenshots.
Two final-build captures from the uninterrupted whole-browser run are retained with this checkpoint: [English/light desktop editor](evidence/editor-en-light-1440.png) and [Bokmål/dark320px editor](evidence/editor-nb-dark-320.png). Both were inspected; they show the repaired edit-entry position and short visible size labels with the complete control names preserved in the accessibility tree.
## Reproduced issues and repairs
- A subsequent populated sizing review found salary amounts breaking into digit fragments at standard width despite passing overflow checks. The new diagnostic first failed its actual text-line assertion (one failed, one passed), then passed both complete standard/full cases after the standard personal widget's salary groups became a single column. Full-width and Analytics retain their existing three-column desktop layout. All eight resizable widgets were saved/read back at both sizes and measured at320/768/1024/1440px, with tables/charts contained and no root overflow. Eight captures were produced; the standard1024 capture was inspected and its [salary crop](evidence/salary-standard-1024.png) retained. The complete ordinary browser suite was then rerun against the rebuilt bundle; final results are recorded in acceptance.
- Edit entry focused the panel beneath the fixed app header. A browser geometry check first failed (`focus-red.json`); deliberate preventScroll focus, the existing `--app-shell-header-height` scroll margin and explicit scroll now pass at320/768/1440px. The final reviewed crops show the heading and Save/Cancel controls below the header.
- Pending-save route navigation offered a misleading discard promise. A deferred acknowledgement unit regression first failed, then passed; SPA navigation now waits with a localized status while saving. Browser unload warnings do not promise rollback of server writes.
- Axe's incomplete triage identified a labelled preview `div` without a valid role. A role-based test failed first (`preview-semantics-red.log`); the preview is now a named region. The new editor browser case explicitly rejects `aria-prohibited-attr` incompletes. Final scan totals are in final acceptance; the earlier ten scans had0 automated violations and68 incomplete nodes (61contrast,6preview semantics,1drawer focus), so those earlier results are not presented as the repaired totals.
@@ -0,0 +1,8 @@
import { defineConfig } from '../../../../../job-tracker-ui/node_modules/@playwright/test';
import base from '../../../../../job-tracker-ui/playwright.config';
import path from 'node:path';
const cwd = path.resolve(__dirname, '../../../../../job-tracker-ui');
const servers = Array.isArray(base.webServer) ? base.webServer : [base.webServer!];
export default defineConfig({ ...base, testDir: __dirname, testMatch: 'sizes.spec.ts',
webServer: servers.map(server => ({ ...server, cwd })) });
@@ -0,0 +1,61 @@
import { test, expect, type APIRequestContext } from '../../../../../job-tracker-ui/node_modules/@playwright/test';
import { WIDGETS } from '../../../../../job-tracker-ui/src/dashboardLayout';
const api = 'http://localhost:5302/api';
const password = 'E2ePassword123!';
async function login(request: APIRequestContext, email: string) {
expect((await request.post(`${api}/auth/login`, { data: { email, password } })).status()).toBe(200);
const me = await (await request.get(`${api}/auth/me`)).json();
const csrf = (await request.storageState()).cookies.find(cookie => cookie.name === 'XSRF-TOKEN')!.value;
return { 'X-CSRF-TOKEN': csrf, 'X-Session-Context': me.sessionContext as string };
}
for (const size of ['standard', 'full'] as const) test(`All populated widgets at ${size} across responsive widths`, async ({ page, playwright }, info) => {
test.setTimeout(120000);
const admin = await playwright.request.newContext();
try {
const adminHeaders = await login(admin, 'e2e@example.test');
const email = `p8-sizes-${size}-${Date.now()}@example.test`;
expect((await admin.post(`${api}/users`, { headers: adminHeaders, data: { email, password, roles: [] } })).status()).toBe(200);
const headers = await login(page.request, email);
const created = await page.request.post(`${api}/companies`, { headers, data: { name: 'Very long Norwegian company name for readable widget layouts', source: 'Native size qualification' } });
expect(created.status()).toBe(201);
const companyId = (await created.json()).id;
expect((await page.request.post(`${api}/jobapplications`, { headers, data: { companyId, jobTitle: 'Senior platform developer', status: 'Applied', dateApplied: new Date(Date.now() - 86400000).toISOString(), responseReceived: true, responseDate: new Date().toISOString(), tags: 'TypeScript, Norsk programvareutvikling', salaryMin: 600000, salaryMax: 850000, salaryCurrency: 'NOK', salaryPeriod: 'year' } })).status()).toBe(201);
await page.route('**/*', route => ['localhost', '127.0.0.1'].includes(new URL(route.request().url()).hostname) ? route.continue() : route.abort());
await page.goto('/dashboard');
await page.getByRole('button', { name: 'Customize dashboard', exact: true }).click();
const editor = page.getByRole('region', { name: 'Editing dashboard', exact: true });
for (const widget of WIDGETS) if (widget.sizes.length > 1) {
const select = editor.locator(`[data-layout-row="${widget.id}"] select`);
await select.selectOption(size); await expect(select).toHaveValue(size);
}
await editor.getByRole('button', { name: 'Save layout', exact: true }).click();
await expect(editor).toHaveCount(0);
const readback = await (await page.request.get(`${api}/auth/preferences/dashboard`)).json();
expect(readback.widgets.every((widget: { id: string; size: string }) => widget.size === (widget.id === 'cards' ? 'full' : size))).toBe(true);
for (const summary of await page.locator('[data-dashboard-widget] details > summary').all()) await summary.click();
for (const width of [320, 768, 1024, 1440]) {
await page.setViewportSize({ width, height: 900 });
await expect(page.locator('[data-dashboard-widget]')).toHaveCount(9);
await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1)).toBe(true);
const failures = await page.locator('[data-dashboard-widget]').evaluateAll(widgets => widgets.flatMap(widget => {
const box = widget.getBoundingClientRect();
return [...widget.querySelectorAll('table, svg, th, td')].filter(element => {
const rect = element.getBoundingClientRect();
return rect.width > 0 && (rect.right > box.right + 1 || rect.left < box.left - 1);
}).map(element => ({ widget: widget.getAttribute('data-dashboard-widget'), tag: element.tagName }));
}));
expect(failures).toEqual([]);
const amount = page.locator('[data-dashboard-widget="salary"]').getByText(new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'NOK', maximumFractionDigits: 0 }).format(725000), { exact: true });
await expect(amount).toBeVisible();
expect(await amount.evaluate(element => {
const range = document.createRange(); range.selectNodeContents(element);
return range.getClientRects().length;
})).toBe(1);
const screenshot = info.outputPath(`${size}-${width}.png`);
await page.screenshot({ path: screenshot, fullPage: true });
await info.attach(`${size}-${width}`, { path: screenshot, contentType: 'image/png' });
}
} finally { await admin.dispose(); }
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB