diff --git a/docs/redesign/phase-8/README.md b/docs/redesign/phase-8/README.md index e6f9901..e0daaac 100644 --- a/docs/redesign/phase-8/README.md +++ b/docs/redesign/phase-8/README.md @@ -34,6 +34,10 @@ Extend the existing account preference owner (`ApplicationUser`) with nullable l SQLite and MariaDB use provider-neutral EF mapping and conditional updates. Explicitly check the existing SQLite startup schema reconciliation/migration infrastructure and the complete migration chain. Legacy accounts gain no layout until first Save. Language, notifications, profile, security stamps and all job/document data remain unchanged. +### Adding or changing widgets later + +Add a stable ID to both `src/dashboardLayout.ts` and the backend `DashboardLayout` allowlist, supply EN/NB titles and the existing report/data owner, then prove allowed widths, empty/error states and ownership. Do not recycle an ID for another meaning. Existing saved layouts append missing known IDs hidden; unsaved accounts receive catalogue defaults. Unknown stored IDs are ignored on read and rejected on write, so preserving metadata across an incompatible client requires a schema-version increment. Future versions are read-only in an older client and cannot be downgraded by its PUT. A genuine rename/removal needs an explicit versioned ID migration and regression proof; there is no speculative alias framework. Feature availability must remain enforced by the data endpoint, independently of layout metadata. + ## Risk and dependency register - Phase 7 is CONDITIONAL GO, not unconditional release acceptance. Read [final acceptance](../phase-7/p7-12/FINAL_ACCEPTANCE.md) and [handoff](../phase-7/p7-12/PHASE8_HANDOFF.md). @@ -49,7 +53,7 @@ SQLite and MariaDB use provider-neutral EF mapping and conditional updates. Expl - [x] P8-01: editing, visibility, draft/cancel, empty recovery, localisation; focused component tests and shared P8-01/02 checkpoint commit/push. - [x] P8-02: existing drag helper, named move buttons, DOM ordering and discrete grid sizing; normalisation/focus tests and shared P8-01/02 checkpoint commit/push. - [x] P8-03: additive account storage, validated endpoint, revision conflict, retry/reset/session isolation; backend/frontend integration proof and checkpoint commit/push. -- [ ] P8-04: actual browser desktop/tablet/mobile, EN/NB, themes, keyboard, failures and axe; inspect captures, retain human qualifications; commit/push. +- [x] P8-04: actual browser desktop/tablet/mobile, EN/NB, themes, keyboard, failures and axe; inspected captures and populated sizing regression; human qualifications retained; checkpoint commit/push. - [ ] P8-05: full backend/frontend/browser, separate standard contracts/sidecar, types/lint/build/security; 57-group reconciliation, final decisions and handoff; commit/push and direct remote verification. ## Implementation and verification plan diff --git a/docs/redesign/phase-8/p8-04/README.md b/docs/redesign/phase-8/p8-04/README.md index 944b1d7..d24c9fa 100644 --- a/docs/redesign/phase-8/p8-04/README.md +++ b/docs/redesign/phase-8/p8-04/README.md @@ -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. diff --git a/docs/redesign/phase-8/p8-04/diagnostics/playwright.config.ts b/docs/redesign/phase-8/p8-04/diagnostics/playwright.config.ts new file mode 100644 index 0000000..4e36b6b --- /dev/null +++ b/docs/redesign/phase-8/p8-04/diagnostics/playwright.config.ts @@ -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 })) }); diff --git a/docs/redesign/phase-8/p8-04/diagnostics/sizes.spec.ts b/docs/redesign/phase-8/p8-04/diagnostics/sizes.spec.ts new file mode 100644 index 0000000..c36d0b2 --- /dev/null +++ b/docs/redesign/phase-8/p8-04/diagnostics/sizes.spec.ts @@ -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(); } +}); diff --git a/docs/redesign/phase-8/p8-04/evidence/editor-en-light-1440.png b/docs/redesign/phase-8/p8-04/evidence/editor-en-light-1440.png new file mode 100644 index 0000000..e9ebd08 Binary files /dev/null and b/docs/redesign/phase-8/p8-04/evidence/editor-en-light-1440.png differ diff --git a/docs/redesign/phase-8/p8-04/evidence/editor-nb-dark-320.png b/docs/redesign/phase-8/p8-04/evidence/editor-nb-dark-320.png new file mode 100644 index 0000000..fb7f1aa Binary files /dev/null and b/docs/redesign/phase-8/p8-04/evidence/editor-nb-dark-320.png differ diff --git a/docs/redesign/phase-8/p8-04/evidence/salary-standard-1024.png b/docs/redesign/phase-8/p8-04/evidence/salary-standard-1024.png new file mode 100644 index 0000000..09953af Binary files /dev/null and b/docs/redesign/phase-8/p8-04/evidence/salary-standard-1024.png differ diff --git a/job-tracker-ui/src/components/DashboardView.tsx b/job-tracker-ui/src/components/DashboardView.tsx index ea8e053..2f1fffd 100644 --- a/job-tracker-ui/src/components/DashboardView.tsx +++ b/job-tracker-ui/src/components/DashboardView.tsx @@ -273,7 +273,7 @@ export default function DashboardView({ mode = "dashboard" }: { mode?: "dashboar )} : null, salary: summaryReady && (overview?.salaryInsights?.length ?? 0) > 0 ? - + widget.id === "salary")?.size === "full" ? "repeat(3, minmax(0, 1fr))" : "1fr" }, gap: 2 }}> {overview!.salaryInsights!.map(item => { const money = new Intl.NumberFormat(locale, { style: "currency", currency: item.currency, maximumFractionDigits: 0 }); return