From ad7b4196af638f3a8c79e261ffd96507bd002835 Mon Sep 17 00:00:00 2001 From: cesnimda Date: Sun, 11 Oct 2026 21:16:05 +0200 Subject: [PATCH] feat(auth): migrate account security presentation --- docs/redesign/phase-7/COMMITS.md | 2 + docs/redesign/phase-7/DEBT.md | 14 + docs/redesign/phase-7/PRESERVATION.md | 27 + docs/redesign/phase-7/README.md | 6 +- docs/redesign/phase-7/p7-01/README.md | 54 +- .../p7-01/evidence/account-results.json | 34082 ++++++++++++++++ .../p7-01/evidence/public-auth-results.json | 10 +- .../p7-01/evidence/public-static-checks.json | 104 + .../phase-7/p7-01/evidence/static-checks.json | 30 +- docs/roadmap/todo.md | 2 +- .../e2e/phase7-account-security.spec.ts | 336 + job-tracker-ui/src/account-providers.test.tsx | 284 + .../src/account-security-flows.test.tsx | 249 + .../account-security-presentation.test.tsx | 112 + .../src/components/AuthStatusCard.tsx | 47 +- .../src/components/GoogleAuthCard.tsx | 15 +- .../src/components/MicrosoftAuthCard.tsx | 15 +- .../src/components/SessionsSettingsCard.tsx | 43 +- .../src/components/TwoFactorSettingsCard.tsx | 46 +- job-tracker-ui/src/i18n/translations.ts | 10 +- job-tracker-ui/src/profile-page.test.tsx | 92 +- job-tracker-ui/src/views/ProfilePage.tsx | 95 +- 22 files changed, 35532 insertions(+), 143 deletions(-) create mode 100644 docs/redesign/phase-7/p7-01/evidence/account-results.json create mode 100644 docs/redesign/phase-7/p7-01/evidence/public-static-checks.json create mode 100644 job-tracker-ui/e2e/phase7-account-security.spec.ts create mode 100644 job-tracker-ui/src/account-providers.test.tsx create mode 100644 job-tracker-ui/src/account-security-flows.test.tsx create mode 100644 job-tracker-ui/src/account-security-presentation.test.tsx diff --git a/docs/redesign/phase-7/COMMITS.md b/docs/redesign/phase-7/COMMITS.md index bd5429f..84f4172 100644 --- a/docs/redesign/phase-7/COMMITS.md +++ b/docs/redesign/phase-7/COMMITS.md @@ -7,6 +7,8 @@ Entry is the completed Phase6 successor receipt `b5319981ad3d1bd635daff861453d23 | Logical unit | Commit | Validation / push receipt | |---|---|---| | P7-00 inventory, preservation/debt and unchanged-source baseline | `27c54146ad6f9059295d169b533db7b1b6689210` | [P7-00](p7-00/README.md), [fresh finite baseline](p7-00/evidence/baseline.json) and [static checks](p7-00/evidence/static-checks.json); inherited runtime explicitly separated. Normal development push verified2026-10-11 14:44:45UTC: localHEAD=origin=live development27c5414; local/origin/live main unchanged27657f3; clean worktree. P7-01 entry dependency satisfied | +| P7-01 public authentication sub-slice | `8ad720da5db5999ecda787575dc45621d18a5498` | [Public report](p7-01/README.md#public-authentication-implementation-and-acceptance), [immutable-source/runtime evidence](p7-01/evidence/public-auth-results.json): affected61/61, native22/22, isolated SMTP2/2, SMTP helper6/6 and unchanged-backend auth subset87/87; final types/lint/build pass. Public slice CONDITIONAL GO with31 contrast incompletes/manual/provider conditions. Normal push verified2026-10-11 18:50:24UTC: localHEAD=origin=live development8ad720d; local/origin/live main27657f3 unchanged. No completed public work remains uncommitted; independent account work in flight. Whole P7-01 remains PENDING | +| P7-01 account identity/security and final checkpoint outcome | PENDING normal account commit/push | [Account report](p7-01/README.md#account-identity-and-security-implementation), [frozen account source/runtime](p7-01/evidence/account-results.json): full573/89 (+52/+4 from521/85), native account6/public22/auth-boundaries10, types/lint/build all pass; actual eight final PNGs inspected. Implemented P7-01 scope CONDITIONAL GO, account85/public31 incompletes and manual/provider/production gates retained;57/32 statuses unchanged. Final staged finite hygiene and actual account push/direct remote receipt pending; no push inferred. Source/export frozen independently from public8ad720d | Hashes and remote receipts are appended by a successor to avoid self-referential commit hashes. Each row records actual commands/results, source scope, remaining conditions, normal push and immediate direct remote identity; partial or blocked evidence is recorded without inventing success. Subsequent receipt metadata is a separately reviewable documentation unit, with its own hash reported after push rather than an endless receipt chain. diff --git a/docs/redesign/phase-7/DEBT.md b/docs/redesign/phase-7/DEBT.md index 653d466..20a4eee 100644 --- a/docs/redesign/phase-7/DEBT.md +++ b/docs/redesign/phase-7/DEBT.md @@ -67,6 +67,20 @@ Settled touch still retains contrast4 and hidden-focus1 (#root) incomplete; it i Use the established [accessibility plan](../phase-3/ACCESSIBILITY_PLAN.md): WCAG2.2 AA project target, ordinary text4.5:1/large text3:1/control-focus3:1, visible focus/reflow/labels/errors/live state/reduced motion;44px physical-touch aim distinct from AA24px/spacing rules. No automated conformance certification. +### P7-01 public disposition + +[Public results](p7-01/evidence/public-auth-results.json) are pushed in8ad720d with direct development equality verified2026-10-11 18:50:24UTC, main unchanged. Its22 new all-enabled scans independently retain0 violation nodes/31 `color-contrast` incomplete nodes, all background-overlap checks on input/button contexts. Keep this separate from the319 mixed-scope Phase6 snapshots; repeated nodes are not unique defects or a comparative conformance score. M09 spoken errors/status/route announcements and M11 contextual contrast/forced colors remain mandatory; human reader/browser zoom/physical input are not supplied by the eight inspected PNGs or native geometry. + +RD25 has new **bounded local proof**: actual application registration/SMTP verification/password reset/replay and old-session invalidation pass in fresh synthetic SQLite/keys with a loopback mail capture server. Actual production mail, OAuth/Turnstile/provider consent/privacy/reconnect and delivery remain unverified. Existing English server-authored auth errors in NB samples and possible duplicate route/error announcements remain explicit reader/localisation qualifications. RD06/RD07/RD25 are not closed. At the public gate account identity/security/session/source-isolation work remained in flight; the later conditional disposition is below. No deferred AI activation is implied. + +RD28 public runtime provenance is retained in the immutable `.artifacts/phase7/p7-01/public-final/` snapshot, including earlier failed SMTP attempts and final export BUILD_ID. Its manifest file references point to that snapshot while raw reports preserve original internal absolute execution paths; commands/hashes/counts are unchanged. This does not erase the separate historical Phase6 attachment-loss qualification. + +### P7-01 account conditional disposition + +[Account results](p7-01/evidence/account-results.json) add fresh573/89 frontend, account6/public22/auth-boundaries10 and types/lint/build pass. The account8 all-enabled scans retain0 violation nodes/85 incomplete occurrences: contrast84 (74 overlap/8 short-text/2 partial overlap) and hidden-focus1 targeting`#root` in the failed revoke-others modal. Actual keyboard trap/return and modal-local error/retry assertions pass; **M09/M11 remain required**, including spoken state and contextual contrast/forced colors. The account-build public22 scans separately retain31 contrast incompletes. Keep these30/116 current finite occurrences separate from the319/2,473 inherited mixed-scope Phase6 records and earlier pre-copy account8/84. + +Copy fixes following actual visual review truthfully describe stored profile CV text and natural Microsoft unavailability, without changing Career CV labels, fetching the CV library or exposing setup variables to the product user. Identity-only persist/reload and canonical/sibling no-write evidence do not close Career concurrency/import/override/render/production debts. Fresh full573 supersedes the historical pre-copy571 runtime count; no test is skipped to obtain acceptance. All eight final post-copy PNGs were actually inspected and implemented scope is CONDITIONAL GO; finite hygiene/account push receipt remains pending. Exact runtime/attempt/build/unit/source/export evidence is frozen under `.artifacts/phase7/p7-01/account-final/`, with206 path/hash references verified. The immutable public static record is separate from the latest account static checks. RD06/RD07/RD25 and all manual/provider/release obligations stay open. + ## Evidence and production limits The early Phase6 runner retained exact failure commands/reports/logs but reused result folders, so not all old screenshots/traces/error-context paths remain resolvable. Later runs archive the whole previous output; current successful attachments have exact provenance. Preserve that limit rather than claiming complete old failure-trace retention. Native Windows Chromium/local SQLite/synthetic fixtures and finite PDF oracles do not establish deployed Linux, human semantics, physical devices, real provider quality or production load. diff --git a/docs/redesign/phase-7/PRESERVATION.md b/docs/redesign/phase-7/PRESERVATION.md index 5a2aa5c..176236c 100644 --- a/docs/redesign/phase-7/PRESERVATION.md +++ b/docs/redesign/phase-7/PRESERVATION.md @@ -72,6 +72,33 @@ The migration-owner column assigns the approved slice while retaining completed ## Route and acceptance invariants +### P7-01 public scoped evidence, no full-group upgrade + +[Public source/runtime results](p7-01/evidence/public-auth-results.json) and [acceptance/limitations](p7-01/README.md#public-authentication-implementation-and-acceptance) are pushed in8ad720d; immediate localHEAD=origin=live development equality was verified2026-10-11 18:50:24UTC, main unchanged. All57 statuses remain0 PASS/56 PARTIAL/0 FAIL/1 DEFERRED. At that public gate the account/private-tree migration was a separate in-flight boundary; its later finite result is recorded below. + +| Protected groups | Fresh finite public evidence | Remaining mandatory boundary | +|---|---|---| +| E02 | Retained33 Login tests; real registration/verification/reset/replay and old-password denial through the application Identity + loopback SMTP path; invalid/missing link and no-write local validation; remember-me/return semantics retained | Actual production mail, Turnstile, authenticator/provider/human and broader expiry/rate-limit combinations; local SMTP is not external delivery acceptance | +| E03 | Shared real provider sign-in consumers remain covered by Login tests; relink retry returns to the configured public policy; no provider call or protocol change | Actual grants, origin/popups/cancellation/provider account-link acceptance; finite account-card proof is below; actual provider grant/popups remain unqualified and mailbox grants remain separate | +| E05/E09 | Real password reset invalidates the earlier authenticated session; failed sign-in retains draft and exact protected return before explicit successful retry | Finite account session/revoke/cancel/modal/private-disposal proof is below; broader account switching/BFCache/offline/old-bundle/deployed cases still require evidence | +| E06 | Missing/invalid email-change confirmation remains named, recoverable and owner-command bound; real password recovery readback is passing | Finite identity-only save/canonical/sibling and retained-password recovery proof is below; complete email-change success/cancel and avatar crop/remove qualification remains | +| E08 | EN/NB/light/dark finite public frames, named main/H1, labelled errors/focus/status/reflow;22 all-enabled scans0 violation nodes and31 retained contrast incomplete nodes | Full cross-product, actual spoken state/name/error and contrast/forced-color/zoom/physical keyboard/device proof; existing server-authored English errors retained explicitly | + +The22 public native cases and two isolated recovery cases are new separate packs; affected61 unit cases contain38 inherited cases plus23 additions. They are not summed as new full-suite evidence. Public product/pricing E01, shell navigation E07 and account setup E04 are not declared migrated or complete by this public result. + +### P7-01 account scoped evidence, no full-group upgrade + +[Account working-tree results](p7-01/evidence/account-results.json) and [account report](p7-01/README.md#account-identity-and-security-implementation) record fresh573/89 full frontend, account6/public22/auth-boundaries10 and types/lint/build pass. Actual eight-image/context review establishes implemented account scope CONDITIONAL GO; final finite hygiene and normal account push/receipt remain pending. All57 statuses and32 whole-route qualifications remain unchanged. + +| Protected groups | Fresh finite account evidence | Remaining mandatory boundary | +|---|---|---| +| E02/E03/E06/E09 | Real default Google/Microsoft account consumers retain current-password/no-exchange/cancel/error/retry/unlink/forced-sign-in safeguards; explicit UI-locale dates. Actual identity PUT204/reload sends only four identity fields and keeps separate email/password/provider owners | Actual provider popup/grant/reconnect/real delivery and full avatar/email-change/device/human combinations remain qualified | +| C01/C07 | Four native identity samples read canonical Career data and two sibling CVs before/after save/reload and assert no canonical/CV browser write. Stored-text status is labelled truthfully without a CV-library fetch | Full Career save/import/history/concurrency and CV overrides/render/frozen/PDF acceptance remain their own migrations and integrated gate | +| E04/E08 | Unknown/failed2FA status is read-only until explicit status retry; known enabled/disabled/recovery/trusted-device cases retain ordinary unit assertions. Named H2 sections and eight finite geometry/axe contexts are retained | Real authenticator/device/reader/zoom and contextual contrast/focus qualification; account8 scans retain0 violation nodes/85 incompletes | +| E05/E08/E09 | Real multiple-login sessions prove canceled confirmation/no write, modal-local failed revoke, keyboard trap/cancel restoration, deliberate retry/readback/other-session401 and current logout/private-tree disposal; existing10 auth/isolation browser boundaries pass | Deployed BFCache/offline/old-bundle and full real-device/assistive combinations; no security protocol or whole-group acceptance inferred | + +Full frontend521→573 adds52 cases across four new suites with no removed/renamed cases: public23 and account29 (providers12, card presentation7, flows5, Profile5). Focused subsets and native packs overlap and are not added to that total. Earlier571 frontend and pre-copy native runs remain historical; the exact post-copy build/source identity is in the account manifest. P2-D01/sidecar/backend/rendering execution remains inherited here. + [INVENTORY.md](p7-00/INVENTORY.md#current-route-coverage) reconciles exactly32 current App patterns against [route acceptance](../phase-5/p5-15/ROUTE_ACCEPTANCE.md); all whole-route groups remain PARTIAL. Preserve Board/Application/mail aliases and actual query/hash/state behavior, section/sub precedence, safe internal from, numeric navigation-helper output, capture consumption, focusJobId and provider/token destinations. No Documents/Notifications/aggregate Admin/new Career destination is inferred without contract/content evidence. P2-D01 remains the ordinary passing [frozen-workspace test](../../../job-tracker-ui/e2e/redesign-preservation.spec.ts#L73), retaining native capture/readback/archive bytes/canonical/sibling proof. No skip, expected-failure marker or relaxed oracle may replace it. Preserve server roles/account/tenant/owned-resource denial; canonical Career versus CV overrides; revision/conflict/recoverable drafts; private/public PDF admission/revocation; unchanged physical pagination; immutable packages; existing grounding/proposal/stale guards and Production C11 withholding. diff --git a/docs/redesign/phase-7/README.md b/docs/redesign/phase-7/README.md index 36e5ffa..ac800a7 100644 --- a/docs/redesign/phase-7/README.md +++ b/docs/redesign/phase-7/README.md @@ -9,7 +9,7 @@ The entry commit is `b5319981ad3d1bd635daff861453d23c0de5320b` on `codex/ui-rede | Checkpoint | Approved scope | Entry dependency / current status | |---|---|---| | [P7-00](p7-00/README.md) | Fresh scope inventory and fixture baseline | Complete finite identity/route/matrix/static checks; inherited runtime separated. Push receipt recorded by the successor before page changes | -| P7-01 | Authentication and account-security presentation | Existing public auth, account identity, 2FA/session owners; planned | +| [P7-01](p7-01/README.md) | Authentication and account-security presentation | Public sub-slice CONDITIONAL GO, pushed8ad720d with direct receipt; account implementation/runtime/actual8-image review CONDITIONAL GO (573/89 +6/22/10); final finite hygiene/account push receipt pending | | P7-02 | Onboarding and Career overview/evidence | Existing checklist/canonical Career owners; planned | | P7-03 | Career structured editing and import review | P7-02 fixtures; resolve or explicitly bound concurrency before changing save/history ownership; planned | | P7-04 | CV library and public entry | Existing gallery/editor and P7-03 source language; planned | @@ -33,6 +33,10 @@ P7-00 adopts the actual unchanged-source Phase 6 runtime results: backend1,033, Keep the 52 all-enabled and 267 inherited-rule axe inventories separate: combined319 mixed-scope snapshots,11 violation-node occurrences and2,473 incomplete occurrences. These are repeated contexts, not unique defects or conformance certification. Applicable manual M01–M12 remain NOT VERIFIED; M13/P5-13 is deferred do-not-execute. +The [P7-01 public sub-slice](p7-01/evidence/public-auth-results.json) adds fresh affected61/61, ordinary native22/22, isolated real Identity/loopback SMTP2/2 and helper6/6 proof. Its unchanged-backend87-case auth subset is not additive to1,033; its affected61 cases do not stand in for a fresh full frontend run. Twenty-two all-enabled public scans separately retain0 violation nodes/31 contrast incomplete nodes. Eight exact final matrix PNGs were inspected; reader/zoom/device/provider/production requirements remain open. Push8ad720d was directly verified at2026-10-11 18:50:24UTC with main unchanged. At that public gate, account presentation/source isolation/session-security acceptance was pending; the later account result below supersedes that status. + +The separate [account runtime manifest](p7-01/evidence/account-results.json) records actual fresh573/89 full frontend, account6/public22/auth-boundaries10 and app/browser TypeScript/lint/build pass after truthful account-only stored-CV-text/unavailable-provider copy repairs. Full frontend521→573 adds52 cases/+4 suites without removals; exact file/name deltas remain recorded. Account8 scans retain0 violation nodes/85 incompletes and current public22 retain31 contrast incompletes, separately from the frozen public/Phase6 baselines. All eight final post-copy PNGs were actually inspected, with contextual axe/manual qualifications retained. Implemented P7-01 scope is CONDITIONAL GO; final finite hygiene and normal account push receipt remain pending. No whole-group/route upgrade follows from these finite results. + ## Working constraints Use the current MUI/Emotion theme, PageLayout/section primitives, ViewStateNotice/useViewResource, Router, i18n, notification and save owners. Preserve query/history/aliases, account/tenant/role boundaries, canonical Career facts versus CV overrides, revision recovery, frozen packages and original PDF pagination/oracles. Do not infer new Documents/Notifications/aggregate Admin routes from illustrative navigation. No new hard-coded user-facing strings. diff --git a/docs/redesign/phase-7/p7-01/README.md b/docs/redesign/phase-7/p7-01/README.md index 2a18be0..33518d4 100644 --- a/docs/redesign/phase-7/p7-01/README.md +++ b/docs/redesign/phase-7/p7-01/README.md @@ -1,6 +1,6 @@ # P7-01 — Authentication and account-security presentation -**Status: public authentication sub-slice CONDITIONAL GO; account-security implementation/acceptance PENDING.** This is the bounded execution/acceptance plan from the [approved handoff](../../phase-6/p6-05/PHASE7_HANDOFF.md). P7-00 `27c54146ad6f9059295d169b533db7b1b6689210` is pushed and direct development/localHEAD/origin equality verified at2026-10-11 14:44:45UTC; main unchanged and worktree clean at entry. Its [baseline](../p7-00/evidence/baseline.json) and [ledger](../COMMITS.md) satisfy entry. No new routes, identity/provider protocols or capability activation are included. Public source/results below are measured before their commit; the parent records its push receipt separately. This sub-slice does not complete P7-01. +**Status: implemented P7-01 scope CONDITIONAL GO; public slice pushed, account final hygiene/push receipt pending.** This is the bounded execution/acceptance plan from the [approved handoff](../../phase-6/p6-05/PHASE7_HANDOFF.md). P7-00 `27c54146ad6f9059295d169b533db7b1b6689210` is pushed and direct development/localHEAD/origin equality verified at2026-10-11 14:44:45UTC; main unchanged and worktree clean at entry. Its [baseline](../p7-00/evidence/baseline.json) and [ledger](../COMMITS.md) satisfy entry. No new routes, identity/provider protocols or capability activation are included. Public8ad720da5db5999ecda787575dc45621d18a5498 is normally pushed; localHEAD=origin=live development directly verified2026-10-11 18:50:24UTC, local/origin/live main27657f3 unchanged. No completed public work remains uncommitted. The separate account implementation/runtime/visual outcome is CONDITIONAL GO below; final finite hygiene and normal account push/receipt remain pending. The public sub-slice alone does not complete P7-01. ## Baseline and bounded commit scope @@ -65,7 +65,7 @@ Visual review of the first passing export found registration describing its pass | Backend auth/security subset | **87/87**, zero failed/not-executed | Auth/session/local identity/context/2FA/email-ownership/authorization contracts at unchanged backend source; a subset of inherited1,033, not an additive total | | App and browser TypeScript, lint, production frontend build | **All exit0** | Final sequence18:40:04–18:40:39UTC; lint retains zero-warning policy. Browser config contents and command/log hashes are portable in the manifest | -Authoritative final build ID is `rd_SQeVLwVwER9VMCc6XL`. Canonical public six-file source digest is `b330b233508ad5202a1f622ade4464103f7a986ee53648c8f16047f2a1d32586`; the manifest records each file and its precise ordered digest algorithm. Static export74 files have measured raw-file digest `dff031a52740a09359715a31456c9cec70f5aae6f9932c018af078215e973d67`. These identify the tested public working tree, not a newly deployed or remotely pushed identity. P7-00 remains the recorded entry commit until a successor push receipt is recorded in the ledger. +Authoritative public build ID is `rd_SQeVLwVwER9VMCc6XL`, with BUILD_ID bytes retained in the immutable public snapshot. Canonical public six-file source digest is `b330b233508ad5202a1f622ade4464103f7a986ee53648c8f16047f2a1d32586`; the manifest records each file and its precise ordered digest algorithm. Static export74 files have measured raw-file digest `dff031a52740a09359715a31456c9cec70f5aae6f9932c018af078215e973d67`. These identify the tested public source subsequently pushed in8ad720d, not a deployment or the later account build. The manifest preserves entry observation27c5414 and adds the actual successor push receipt separately. ### Responsive, localisation and visual evidence @@ -85,6 +85,52 @@ Intermediate public unit attempts are also retained, including the59-case run wi The finite public checks preserve E01–E06/E09 and E07/E08/S03 boundaries, exact safe return/query/hash handling, real token failure/replay and old-session rejection. Backend routes/protocols and established link/exchange guards are retained. All **57 groups remain56 PARTIAL + C11 DEFERRED**, and all32 routes remain retained with their existing qualifications. No whole-group upgrade or fresh full frontend/backend/browser/fidelity acceptance is inferred from these subsets. Ordinary P2-D01 remains required by the inherited baseline and future integrated gate; it was not rerun in this public pack. -Public sub-slice acceptance is **CONDITIONAL GO** for its completed development commit, with M01–M12, server-error language/reader/contrast/provider qualifications and inherited debt carried. **Production remains NO-GO**, main unchanged; M13/P5-13 remains deferred do-not-execute. Commit/push/remote receipt and finite hygiene checks are recorded by the parent in the ledger before account product work proceeds. +Public sub-slice acceptance is **CONDITIONAL GO** for pushed8ad720d, with M01–M12, server-error language/reader/contrast/provider qualifications and inherited debt carried. **Production remains NO-GO**, main unchanged; M13/P5-13 remains deferred do-not-execute. The [ledger](../COMMITS.md) and manifest contain the actual normal push/direct remote receipt; entry/public source and runtime history remain separate from account work. -Account/security acceptance is **PENDING**: five new account-presentation RED cases and12 real-provider cases are prepared (eight existing provider behaviors pass; four provider presentation cases fail), plus six meaningful native account cases not yet executed. The parent owns those product repairs and the later fresh affected build/native evidence. This public result neither completes P7-01 nor changes the approved checkpoint order. +The [public static record](evidence/public-static-checks.json) checked2026-10-11 18:48:17UTC records secret scan and working/staged/Phase7 whitespace commands all exit0, all57 statuses and32 routes preserved. An earlier checker parsed56 rows because it did not accept bold `DEFERRED`; accepting optional Markdown emphasis repaired the checker, with no matrix/status change. Later account/successor documentation requires its own fresh finite hygiene run. + +At the public gate, five account-presentation RED cases and12 real-provider cases were prepared; those historical counts remain in the frozen public manifest. Subsequent account results are recorded separately below. Focused provider sets overlap existing public cases and cannot be added to a full-suite result. The public slice does not change the approved checkpoint order. + +## Account identity and security implementation + +**Implemented account scope: CONDITIONAL GO after fresh runtime and actual final visual/context review; final finite hygiene/push receipt pending.** The [account manifest](evidence/account-results.json) records the actual fresh runtime results below, exact seven-file source/export identity, all native attachment bodies and geometry, intermediate failures and case-name deltas. Its public entry reference preserves the already pushed public slice. The whole final UI runtime tree including attempts, focused unit records, BUILD_ID, runner/browser config, inherited frontend baseline, seven source snapshots and exact export are frozen under `.artifacts/phase7/p7-01/account-final/`. All206 recorded path/hash references were verified; only file references were rebased, preserving raw bytes, hashes, commands and original report execution paths. Source identity retains original logical source paths with separate immutable snapshots. Do not recollect this slice after subsequent builds. No backend source, authentication protocol, schema, provider capability or external service was changed. + +AuthStatus, 2FA, Sessions and the two real account-provider consumers use the existing themed PageSection with named H2 sections. The shell owns H1; Profile's existing identity/account/password sections keep their save boundaries. Signed-in wording and security/provider dates follow the selected EN/NB locale. Long identity/session metadata wraps within the existing responsive layout. + +Unknown or failed 2FA status announces loading/failure and offers a deliberate read-only retry; it cannot suggest setup until an actual status response is known. Sessions loading failure no longer looks like a known empty list. A failed revoke-others request keeps its durable alert inside the named confirmation and preserves explicit retry/cancel. Pending confirmation blocks duplicate commands; current-session logout still clears the authenticated private tree. Existing enable/disable, recovery acknowledgement/export/clear, device revoke, password and provider-link/unlink owners remain in place. + +Profile uses the existing API-error helper and durable scoped save/password feedback, retaining the draft after failure for explicit retry. Identity save still sends exactly firstName/lastName/displayName/userName; account email, canonical Career data and CV variants retain their separate owners. Native fixtures read canonical data and two sibling CVs before/after an actual identity PUT204 and reload, and assert no browser canonical/CV write. + +Visual inspection of the first passing account export exposed two copy defects: a disabled Microsoft account card instructed the product user to set a NEXT_PUBLIC environment variable, and the account chip called a CV missing even when canonical Career data and two CV variants existed. The chip's actual source is only stored `profileCvText`. Two ordinary EN/NB Profile cases and a strengthened existing provider assertion reproduced these failures (33 pass/3 fail, no selected-out cases). Account-only `profileCvTextAvailable/Missing` keys now describe that stored text truthfully; the existing Microsoft unavailable hint uses natural EN/NB wording. Existing Career CV keys/data, CV-library fetching and provider configuration semantics are unchanged. + +### Fresh account regression and source identity + +| Evidence | Actual result | Scope / count explanation | +|---|---|---| +| Full frontend Jest | **573/573 in89 suites**, zero fail/pending/todo | Fresh19:03:13–19:06:30UTC. Inherited521/85 becomes+52 cases/+4 suites: public23, account providers12, card presentation7, security flows5 and Profile5 (19→24). No removed/renamed cases; exact names are retained in the manifest | +| Account native | **6/6**, zero skip/flaky/unexpected/retry | Fresh19:04:29–19:05:01UTC. Four actual identity-save/reload/isolation samples, failed-2FA-read→explicit-retry/no-write, and real multiple-session cancel/failure/retry/readback/current-logout workflow | +| Public native regression | **22/22**, zero skip/flaky/unexpected/retry | Fresh19:05:01–19:05:33UTC on the account export; separate from the immutable public acceptance run and its SMTP evidence | +| Existing auth/account-isolation browser boundaries | **10/10**, zero skip/flaky/unexpected/retry | Fresh19:05:33–19:06:16UTC on the same account export. Current/private-tree disposal, canonical account separation, safe return and expired authentication retain their ordinary assertions | +| App/browser TypeScript, lint, frontend production build | **All exit0** | Fresh19:03:46–19:04:29UTC, lint zero-warning policy retained. Earlier unknown-response spread in a new test was corrected with a test-only structural assertion; the diagnostic failed typecheck is retained | + +The measured account export is `SbTVDn-CSb21uPKSzFqOv`. Seven product owners have canonical-LF digest `91589b37e8efe9e0a089987158af44e5f846d0b56cc78782423f38508404f915`; the74 exported files have raw digest `04926e1ee0c75884a8a61b70b3b65e158ad237d65c2714e196ace6d692655bda`. The manifest records per-file hashes and the ordered digest algorithms. These identify the tested working tree after the19:02:57UTC product freeze, before the account commit, with entry HEAD8ad720d. The final test-only structural assertion does not change product source. Backend1,033, sidecar49, ordinary integrated159 and rendering corpora remain inherited; this account slice does not claim fresh executions of them or ordinary P2-D01. + +### Account accessibility and responsive evidence + +Eight all-enabled account snapshots cover NB/dark320, EN/light390, NB/light768 and EN/dark1440 identity persistence, plus unknown/recovered 2FA and failed/settled session revocation at320/390. All eight record root overflow0 and eight named sections within the viewport. Actual identity fields save/reload, long names and security dates are included. The six account cases do not vary density; both Comfortable/Compact are not claimed. These finite contexts do not establish every breakpoint/theme/language combination, physical touch/software keyboards, browser zoom or a human screen-reader result. + +Account scans retain **0 violation nodes and85 incomplete nodes**:84 color-contrast (74 background overlap,8 short-text,2 partial overlap) and1 aria-hidden-focus. The failed revoke-others modal targets `#root` with `focusable-modal-open`: “Check that focusable elements are not tabbable in the current state.” The ordinary native test proves named dialog/description, installed MUI initial focus container, subsequent forward/backward keyboard trap cycle, cancel focus restoration, failure retained inside the modal and deliberate retry. That bounded keyboard evidence does not replace **M09** spoken navigation/state or **M11** contextual contrast/forced-colors qualification. Exact nodes/check messages/data and source contexts remain in the manifest and hashed raw report; no rule was disabled. + +The public regression adds22 separate all-enabled scans with0 violation nodes/31 contrast incompletes. Combined30 account-build snapshots have0 violation nodes/116 incomplete occurrences, but the8 account and22 public contexts remain separate; they are not like-for-like substitutes for Phase6's mixed-scope319 snapshots. The earlier account export had84 incompletes; the final failed-modal snapshot has one additional overlap node. No incomplete was hidden to preserve an earlier count. + +Eight PNGs are extracted byte-for-byte from the inline base64 report attachments into a report-SHA directory; their exact raw hash and original embedded-report reference are recorded. **The parent actually opened all eight final post-copy PNGs with `view_image`**: four persisted identity samples, failed/recovered2FA read, failed revoke dialog and surviving-session view. Stored-text wording is truthful, the unavailable Microsoft hint contains no operator variable, and controls/layout remain clear and in bounds. Sticky appbar/skip-link positions in stitched full-page captures reflect their actual scroll/focus capture context. Earlier inspected identity images and their two copy defects remain historical. The manifest contains exact image hashes and the direct review receipt; extraction performs no editing and visual review does not provide human assistive acceptance. + +### Historical failures and remaining acceptance + +The first native account attempt passed1/6. Four cases assumed Profile PUT returned200, whereas the established AuthController contract returns204. Another assertion expected initial focus inside the Paper role=dialog, although installed MUI FocusTrap initially focuses its containing dialog container. Fixtures now require204 and the actual initial trap container; subsequent Tab/Shift+Tab still must cycle inside the named dialog and cancel restores the invoking control. This corrected test assumption did not alter product authentication/save semantics or relax keyboard containment. Its whole report/log/output folder is retained. + +The provider RED8 pass/4 fail, original card RED0/5, strengthened card RED1/7, focused GREEN sets and copy RED33/36 are retained with their exact counts. Earlier full571/89 and passing account/public/boundary native runs precede the copy repair and remain historical, not final acceptance. The fresh full573 run has no pending/skipped/expected-failure masking. Whole previous runtime folders are retained by the runner; raw reports still contain their original execution paths. + +All57 groups remain56 PARTIAL + C11 DEFERRED, all32 routes retained with existing qualifications. This slice adds finite E02–E06/E08/E09 and C01/C07/S03 evidence; it does not upgrade whole groups. Human reader/zoom/physical and software-keyboard, real OAuth/Turnstile/authenticator/device and production delivery gates remain open. Existing server-authored English errors in NB remain an explicit localisation qualification. M01–M12 and inherited debt carry forward; M13/P5-13 stays deferred do-not-execute, Phase8 stays separate, production NO-GO and main unchanged. Implemented account and whole-P7-01 scope are CONDITIONAL GO with these explicit qualifications. Final finite hygiene and normal account commit/push receipt remain pending; no account push is inferred from runtime or visual review. The latest account static record uses [static-checks.json](evidence/static-checks.json); its final metadata will be mirrored after the parent's staged checks. + +The final account [static checks](evidence/static-checks.json) passed2026-10-11 19:15:38UTC: tracked/untracked secret scan, working/staged/Phase7 whitespace,303 relative documentation links,57 groups and32 original routes. Root independently verified all206 account manifest file/hash references. Normal account commit/push remains the next step; the successor records its actual receipt. diff --git a/docs/redesign/phase-7/p7-01/evidence/account-results.json b/docs/redesign/phase-7/p7-01/evidence/account-results.json new file mode 100644 index 0000000..bc76eca --- /dev/null +++ b/docs/redesign/phase-7/p7-01/evidence/account-results.json @@ -0,0 +1,34082 @@ +{ + "schemaVersion": 1, + "checkpoint": "P7-01", + "slice": "Account identity/security presentation and shared-provider regression", + "collectedUtc": "2026-10-11T19:11:36.174966+00:00", + "acceptance": "CONDITIONAL GO", + "runtimeFrozenAndCompleteConfirmedByParent": true, + "production": "NO-GO; main unchanged; no deployment", + "entry": { + "publicCommit": "8ad720da5db5999ecda787575dc45621d18a5498", + "publicEvidence": { + "path": "docs/redesign/phase-7/p7-01/evidence/public-auth-results.json", + "sha256": "2ee4046f001d1c1071580bea1593ca6bb1d36fb67d3cdec8784cc15ec6f5c431", + "bytes": 242240, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + "qualification": "Immutable public slice remains distinct; this collector never rewrites it" + }, + "identity": { + "headObserved": "8ad720da5db5999ecda787575dc45621d18a5498", + "accountSourceFiles": [ + { + "path": "job-tracker-ui/src/views/ProfilePage.tsx", + "sha256": "7be1af4be994fa6475afaebcede19e8abb5dbe872c1fb56214b422f274551444", + "bytes": 18740, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + { + "path": "job-tracker-ui/src/components/AuthStatusCard.tsx", + "sha256": "7a397cc55db165e85794b12be7c21984759bca5fbc83a6ed75e574744f59187c", + "bytes": 3392, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + { + "path": "job-tracker-ui/src/components/TwoFactorSettingsCard.tsx", + "sha256": "2945f262db833978a12653e0a155f5e979a8bd39b240ece3991649fc0cde888f", + "bytes": 15722, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + { + "path": "job-tracker-ui/src/components/SessionsSettingsCard.tsx", + "sha256": "057397bed9c98131f2913ec597f636af359e6d70984f37216816b0b7b78f5442", + "bytes": 5664, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + { + "path": "job-tracker-ui/src/components/GoogleAuthCard.tsx", + "sha256": "747afc361183bff1c558110eae9e2108d1e0ddb793dc789719623a7f3f38f4df", + "bytes": 11069, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + { + "path": "job-tracker-ui/src/components/MicrosoftAuthCard.tsx", + "sha256": "36558ba83de1bca2a5b60d55c891c18a0d87ca9795404027e332ea1c67545ca5", + "bytes": 9513, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + }, + { + "path": "job-tracker-ui/src/i18n/translations.ts", + "sha256": "b81af232b6b5d44c16bb673e59fa742187585b7c852ace821514629ee88e0366", + "bytes": 307768, + "hashPolicy": "UTF-8 text with CRLF normalized to LF" + } + ], + "accountSourceSha256Lf": "91589b37e8efe9e0a089987158af44e5f846d0b56cc78782423f38508404f915", + "sourceHashAlgorithm": "SHA256 of ordered path + TAB + canonical-LF file SHA256 + LF records", + "buildId": "SbTVDn-CSb21uPKSzFqOv", + "buildIdRaw": { + "path": ".artifacts/phase7/p7-01/account-final/BUILD_ID", + "sha256": "73b0baee1dc4e5014c968db9c211f040709a486186d71d30b1d7a53849be7e04", + "bytes": 21, + "hashPolicy": "Exact measured raw bytes" + }, + "staticExportFileCount": 74, + "staticExportSha256": "04926e1ee0c75884a8a61b70b3b65e158ad237d65c2714e196ace6d692655bda", + "staticExportHashAlgorithm": "SHA256 of sorted repository-relative path + TAB + raw exported file SHA256 + LF records", + "qualification": "Measured final working-tree source/export before eventual account commit; no commit self-reference or inferred remote receipt", + "accountSourceSnapshots": [ + { + "originalSourcePath": "job-tracker-ui/src/views/ProfilePage.tsx", + "path": ".artifacts/phase7/p7-01/account-final/source/job-tracker-ui/src/views/ProfilePage.tsx", + "sha256": "2b521c464331c10f0680b2b75c5e6065089cfcf6a40e5db3392c7a25b61b916f", + "bytes": 19115, + "hashPolicy": "Exact measured raw bytes", + "canonicalLfSha256": "7be1af4be994fa6475afaebcede19e8abb5dbe872c1fb56214b422f274551444" + }, + { + "originalSourcePath": "job-tracker-ui/src/components/AuthStatusCard.tsx", + "path": ".artifacts/phase7/p7-01/account-final/source/job-tracker-ui/src/components/AuthStatusCard.tsx", + "sha256": "cf33dd501dc3d4b5db925d8d3eb445c86fb98dfbe65043b0af2ac2848fdefa9f", + "bytes": 3486, + "hashPolicy": "Exact measured raw bytes", + "canonicalLfSha256": "7a397cc55db165e85794b12be7c21984759bca5fbc83a6ed75e574744f59187c" + }, + { + "originalSourcePath": "job-tracker-ui/src/components/TwoFactorSettingsCard.tsx", + "path": ".artifacts/phase7/p7-01/account-final/source/job-tracker-ui/src/components/TwoFactorSettingsCard.tsx", + "sha256": "4977001fd64d50666668b2324cfc3712b6545406241d01c79087c9443da78568", + "bytes": 16120, + "hashPolicy": "Exact measured raw bytes", + "canonicalLfSha256": "2945f262db833978a12653e0a155f5e979a8bd39b240ece3991649fc0cde888f" + }, + { + "originalSourcePath": "job-tracker-ui/src/components/SessionsSettingsCard.tsx", + "path": ".artifacts/phase7/p7-01/account-final/source/job-tracker-ui/src/components/SessionsSettingsCard.tsx", + "sha256": "b3568ca9f71cfb623617f00c584e8851a813e7c72faf85dc852d0fa65668038e", + "bytes": 5822, + "hashPolicy": "Exact measured raw bytes", + "canonicalLfSha256": "057397bed9c98131f2913ec597f636af359e6d70984f37216816b0b7b78f5442" + }, + { + "originalSourcePath": "job-tracker-ui/src/components/GoogleAuthCard.tsx", + "path": ".artifacts/phase7/p7-01/account-final/source/job-tracker-ui/src/components/GoogleAuthCard.tsx", + "sha256": "5dc802fa3eb3618534d90c625a365d2a361c3ce1ab1b3877923b86f84efc4ab3", + "bytes": 11366, + "hashPolicy": "Exact measured raw bytes", + "canonicalLfSha256": "747afc361183bff1c558110eae9e2108d1e0ddb793dc789719623a7f3f38f4df" + }, + { + "originalSourcePath": "job-tracker-ui/src/components/MicrosoftAuthCard.tsx", + "path": ".artifacts/phase7/p7-01/account-final/source/job-tracker-ui/src/components/MicrosoftAuthCard.tsx", + "sha256": "fcb84c79afca5a2c585f73336f2ce74f33f0059a9bcd257e1b452f8d4f67dcc4", + "bytes": 9756, + "hashPolicy": "Exact measured raw bytes", + "canonicalLfSha256": "36558ba83de1bca2a5b60d55c891c18a0d87ca9795404027e332ea1c67545ca5" + }, + { + "originalSourcePath": "job-tracker-ui/src/i18n/translations.ts", + "path": ".artifacts/phase7/p7-01/account-final/source/job-tracker-ui/src/i18n/translations.ts", + "sha256": "47f97b713db4d7f8a0138d45f1c243003b08ead86dac1d84eb8cc54b910e6a5e", + "bytes": 312946, + "hashPolicy": "Exact measured raw bytes", + "canonicalLfSha256": "b81af232b6b5d44c16bb673e59fa742187585b7c852ace821514629ee88e0366" + } + ], + "staticExportSnapshot": { + "root": ".artifacts/phase7/p7-01/account-final/export", + "qualification": "Exact raw files copied from job-tracker-ui/out; original path/digest algorithm and source observations remain unchanged" + } + }, + "environment": { + "appliedRunnerEnvironment": { + "NEXT_PUBLIC_API_BASE_URL": "http://localhost:5302/api", + "NEXT_TELEMETRY_DISABLED": "1", + "DOTNET_ROOT": "C:/Users/Connor/.dotnet", + "DOTNET_HOST_PATH": "C:/Users/Connor/.dotnet/dotnet.exe", + "PLAYWRIGHT_STATIC_EXPORT": "true", + "PLAYWRIGHT_PREBUILT_EXPORT": "true", + "PLAYWRIGHT_REUSE_SERVERS": "false", + "Workers__SavedJobSearchAlertsEnabled": "false", + "CV_FIDELITY_OUTPUT": ".artifacts/phase7/current/ui/fidelity", + "CV_SETTINGS_FIDELITY_OUTPUT": ".artifacts/phase7/current/ui/settings-fidelity" + }, + "runnerRaw": { + "path": ".artifacts/phase7/p7-01/account-final/run-ui.py", + "sha256": "39d47a4ef49f905ec9958395e385bd9bf95ba2408f1a3b978bf5dde0c7856518", + "bytes": 3850, + "hashPolicy": "Exact measured raw bytes" + }, + "browserTypeScriptConfig": { + "raw": { + "path": ".artifacts/phase7/p7-01/account-final/e2e-tsconfig.json", + "sha256": "55fa6fffaed1e76a32657edfffcc501cd8bd87330bda2c3a3944f8f89d4019e2", + "bytes": 572, + "hashPolicy": "Exact measured raw bytes" + }, + "contents": "{\n \"extends\": \"../../job-tracker-ui/tsconfig.json\",\n \"compilerOptions\": {\n \"incremental\": false,\n \"types\": [\"node\", \"jest\"],\n \"typeRoots\": [\"../../job-tracker-ui/node_modules/@types\"]\n },\n \"include\": [\n \"../../job-tracker-ui/e2e/**/*.ts\",\n \"../../job-tracker-ui/ai-tests/**/*.ts\",\n \"../../job-tracker-ui/auth-tests/**/*.ts\",\n \"../../job-tracker-ui/playwright.*.config.ts\",\n \"../../job-tracker-ui/src/mui-theme.d.ts\",\n \"../../docs/redesign/phase-5/p5-15/p5-15-01/diagnostics/**/*.ts\"\n ],\n \"exclude\": [\"../../job-tracker-ui/node_modules\"]\n}\n" + }, + "qualification": "Local native Chromium/static export/.NET9/owned synthetic SQLite fixtures. No actual OAuth/Turnstile/authenticator/production delivery or human qualification" + }, + "commands": { + "jest": { + "record": { + "name": "jest", + "command": [ + "rtk", + "proxy", + "npm.cmd", + "test", + "--", + "--runInBand", + "--watchAll=false", + "--json", + "--outputFile=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\ui\\jest.json" + ], + "startedUtc": "2026-10-11T19:03:13.194179+00:00", + "finishedUtc": "2026-10-11T19:06:30.358940+00:00", + "exitCode": 0 + }, + "recordRaw": { + "path": ".artifacts/phase7/p7-01/account-final/runtime/jest-command.json", + "sha256": "68c98cc0eba406215bbbb200fe4364ec13572f402a0bef89e5a5a30e196742c5", + "bytes": 404, + "hashPolicy": "Exact measured raw bytes" + }, + "logRaw": { + "path": ".artifacts/phase7/p7-01/account-final/runtime/jest.log", + "sha256": "4cbeb768604bd4032dc2bf68b6811c78217a65366153c3e7a27f99d66464c40e", + "bytes": 443, + "hashPolicy": "Exact measured raw bytes" + } + }, + "typecheck": { + "record": { + "name": "typecheck", + "command": [ + "rtk", + "proxy", + "npm.cmd", + "run", + "typecheck" + ], + "startedUtc": "2026-10-11T19:03:46.262808+00:00", + "finishedUtc": "2026-10-11T19:03:56.024691+00:00", + "exitCode": 0 + }, + "recordRaw": { + "path": ".artifacts/phase7/p7-01/account-final/runtime/typecheck-command.json", + "sha256": "86d25427f923e74d21abb685bc5380e1b372fa95adf9b4e435127ad19069d250", + "bytes": 247, + "hashPolicy": "Exact measured raw bytes" + }, + "logRaw": { + "path": ".artifacts/phase7/p7-01/account-final/runtime/typecheck.log", + "sha256": "89b0ec97b537eca6560c13ca2dfea9e3a97dd1bb35580c4ff39d2462db25e080", + "bytes": 148, + "hashPolicy": "Exact measured raw bytes" + } + }, + "browser-typecheck": { + "record": { + "name": "browser-typecheck", + "command": [ + "rtk", + "proxy", + "npm.cmd", + "exec", + "tsc", + "--", + "--project", + "../.artifacts/phase7/e2e-tsconfig.json", + "--noEmit" + ], + "startedUtc": "2026-10-11T19:03:56.027208+00:00", + "finishedUtc": "2026-10-11T19:03:59.825409+00:00", + "exitCode": 0 + }, + "recordRaw": { + "path": ".artifacts/phase7/p7-01/account-final/runtime/browser-typecheck-command.json", + "sha256": "eeb2e367c21a03cbc3623566a7256b200652947b2f705a2a53af9ffa22d9f16e", + "bytes": 343, + "hashPolicy": "Exact measured raw bytes" + }, + "logRaw": { + "path": ".artifacts/phase7/p7-01/account-final/runtime/browser-typecheck.log", + "sha256": "d2993bc68ad9a63f711c8f5473155e2ba00fdcd461220cc3849185f2f9ad93e6", + "bytes": 78, + "hashPolicy": "Exact measured raw bytes" + } + }, + "lint": { + "record": { + "name": "lint", + "command": [ + "rtk", + "proxy", + "npm.cmd", + "run", + "lint" + ], + "startedUtc": "2026-10-11T19:03:59.827421+00:00", + "finishedUtc": "2026-10-11T19:04:16.947534+00:00", + "exitCode": 0 + }, + "recordRaw": { + "path": ".artifacts/phase7/p7-01/account-final/runtime/lint-command.json", + "sha256": "cfe923ac8df3e9bf2a4e577ce52520cb25404083f9caf7562e53a3f1f3020f64", + "bytes": 237, + "hashPolicy": "Exact measured raw bytes" + }, + "logRaw": { + "path": ".artifacts/phase7/p7-01/account-final/runtime/lint.log", + "sha256": "b040dbeca9b4adb3f6f4c25ac49de0d37e921375b2d9b57e87b542a10f599913", + "bytes": 136, + "hashPolicy": "Exact measured raw bytes" + } + }, + "build": { + "record": { + "name": "build", + "command": [ + "rtk", + "proxy", + "npm.cmd", + "run", + "build" + ], + "startedUtc": "2026-10-11T19:04:16.950047+00:00", + "finishedUtc": "2026-10-11T19:04:29.732862+00:00", + "exitCode": 0 + }, + "recordRaw": { + "path": ".artifacts/phase7/p7-01/account-final/runtime/build-command.json", + "sha256": "496363cf2083f2e0f49b3034c7e99d25e7c762504a59588a06141479bc36a0b9", + "bytes": 239, + "hashPolicy": "Exact measured raw bytes" + }, + "logRaw": { + "path": ".artifacts/phase7/p7-01/account-final/runtime/build.log", + "sha256": "6bf39ae2370c6e857ab993234cab3db8d9a61b6519d557580342acee1d8f28e5", + "bytes": 670, + "hashPolicy": "Exact measured raw bytes" + } + } + }, + "fullFrontend": { + "raw": { + "path": ".artifacts/phase7/p7-01/account-final/runtime/jest.json", + "sha256": "319679b96644fbde2ec466faa5055ac3dce0f85053f3c92c7dbfbc9d763186f0", + "bytes": 241073, + "hashPolicy": "Exact measured raw bytes" + }, + "startedUnixMs": 1791745394142, + "passed": 573, + "failed": 0, + "pending": 0, + "total": 573, + "suites": 89, + "cases": [ + { + "name": "meaningful identity initials use a readable foreground/background pair in [Function CareerProfilePage]", + "status": "passed" + }, + { + "name": "meaningful identity initials use a readable foreground/background pair in [Function ProfilePage]", + "status": "passed" + }, + { + "name": "Career Profile section headings describe the visible hierarchy without changing editing fields", + "status": "passed" + }, + { + "name": "account headings separate identity from security sections", + "status": "passed" + }, + { + "name": "account CV text status in en does not claim the CV library is empty", + "status": "passed" + }, + { + "name": "account CV text status in nb does not claim the CV library is empty", + "status": "passed" + }, + { + "name": "[PRES-E02/E09/C01] account save carries four identity fields and keeps career, email and CV writes separate", + "status": "passed" + }, + { + "name": "[PRES-E02/E08/E09] failed account save keeps a scoped error and the identity draft for explicit retry", + "status": "passed" + }, + { + "name": "[PRES-E02/E08] failed password update keeps a scoped error and secret draft until explicit retry succeeds", + "status": "passed" + }, + { + "name": "profile page loads persisted structured cv and can re-parse it", + "status": "passed" + }, + { + "name": "saving the career profile PUTs the structured profile and cv text unchanged (Phase 1 refactor invariant)", + "status": "passed" + }, + { + "name": "editing a field in an extracted section updates parent state and flows into save (Phase 1 increment 2)", + "status": "passed" + }, + { + "name": "profile page can reprocess from stored artifact history", + "status": "passed" + }, + { + "name": "active extraction polling refreshes run status without resetting unsaved career fields", + "status": "passed" + }, + { + "name": "profile page shows durable CV operation state and retries a failed run", + "status": "passed" + }, + { + "name": "profile page keeps raw extraction collapsed until expanded", + "status": "passed" + }, + { + "name": "saving the master profile (career) persists the structured profile via /career/profile", + "status": "passed" + }, + { + "name": "/career shows the profile completeness overview", + "status": "passed" + }, + { + "name": "/career profile exposes extraction progress", + "status": "passed" + }, + { + "name": "/career profile can queue a CV upload without visiting import review first", + "status": "passed" + }, + { + "name": "/career localizes completeness and review confidence without changing CV data", + "status": "passed" + }, + { + "name": "/career separates overview, profile and import while preserving unsaved profile edits", + "status": "passed" + }, + { + "name": "pending CV extraction can be reviewed and applied", + "status": "passed" + }, + { + "name": "account email changes require a password and use the confirmation flow", + "status": "passed" + }, + { + "name": "Profile account provider cards [PRES-E02/E09] disabled Google has a named account section without starting OAuth", + "status": "passed" + }, + { + "name": "Profile account provider cards [PRES-E02/E09] disabled Microsoft has a named account section without starting OAuth", + "status": "passed" + }, + { + "name": "Profile account provider cards [PRES-E02/E09] Google linked metadata follows Bokmål while retaining the stored timestamp", + "status": "passed" + }, + { + "name": "Profile account provider cards [PRES-E03/E09] Microsoft linked metadata follows Bokmål while retaining the stored timestamp", + "status": "passed" + }, + { + "name": "Profile account provider cards [PRES-E03/E08] Microsoft local linking requires the current password and cancellation keeps it without exchanging credentials", + "status": "passed" + }, + { + "name": "Profile account provider cards [PRES-E03/E08] rejected Microsoft linking retains the password and local account for an explicit retry", + "status": "passed" + }, + { + "name": "Profile account provider cards [PRES-E03/E05] successful Microsoft local linking retains the established forced sign-in contract", + "status": "passed" + }, + { + "name": "Profile account provider cards [PRES-E03/E05/E08] Microsoft unlink is password-guarded and a failed attempt retains input until a deliberate retry succeeds", + "status": "passed" + }, + { + "name": "Profile account provider cards [PRES-E02/E03] a google-owned account cannot unlink its only authentication provider", + "status": "passed" + }, + { + "name": "Profile account provider cards [PRES-E02/E03] a microsoft-owned account cannot unlink its only authentication provider", + "status": "passed" + }, + { + "name": "Profile account provider cards [PRES-E02/E08] Google local linking reports failure and retries the link endpoint without an exchange", + "status": "passed" + }, + { + "name": "Profile account provider cards [PRES-E02/E08] failed Google unlink preserves the local account and linked state until explicit retry", + "status": "passed" + }, + { + "name": "content sections follow the editing-region heading without mutating CV settings", + "status": "passed" + }, + { + "name": "section navigation opens and focuses omitted/custom sections without changing their data", + "status": "passed" + }, + { + "name": "keyboard move controls retain focus and persist the same section order shown by navigation", + "status": "passed" + }, + { + "name": "secondary duplicate action saves the current draft before creating the copy", + "status": "passed" + }, + { + "name": "pane navigation focuses the existing preview without changing the pending draft", + "status": "passed" + }, + { + "name": "deep link loads the exact variant named in the route", + "status": "passed" + }, + { + "name": "a variant that does not exist shows a safe error instead of an empty editor", + "status": "passed" + }, + { + "name": "another user's variant is refused by the server and never rendered", + "status": "passed" + }, + { + "name": "invalid names are explained and are not silently autosaved", + "status": "passed" + }, + { + "name": "failed autosave is visible and can be retried with the latest data", + "status": "passed" + }, + { + "name": "session undo and redo restore content edits before autosave", + "status": "passed" + }, + { + "name": "queued saves use acknowledged server versions without replacing newer local editing state", + "status": "passed" + }, + { + "name": "revision conflict retains the draft and blocks later autosaves until explicit reload", + "status": "passed" + }, + { + "name": "edits made while a confirmed reload is in flight remain recoverable", + "status": "passed" + }, + { + "name": "restoring history uses the current version and never replaces edits made during the request", + "status": "passed" + }, + { + "name": "Content is the primary studio panel and all advanced tabs retain their controls", + "status": "passed" + }, + { + "name": "choosing a template restores its own structure instead of inheriting the previous template layout", + "status": "passed" + }, + { + "name": "editor sections, design controls and document settings render in Bokmål", + "status": "passed" + }, + { + "name": "internal navigation warns and can be cancelled before discarding a pending edit", + "status": "passed" + }, + { + "name": "custom entries can be added, edited, reordered and deleted with confirmation", + "status": "passed" + }, + { + "name": "[P5-10 PRES-C01 C07] source-backed entry edits identify their scope and save only this CV", + "status": "passed" + }, + { + "name": "[P5-10 PRES-C07 C10] resetting one entry field preserves omission, sibling override and order and supports undo", + "status": "passed" + }, + { + "name": "[P5-10 PRES-C07 C10 E08] omitted entries remain editable and retain their draft after a failed save", + "status": "passed" + }, + { + "name": "[P5-10 PRES-C01 C07] section content reset inherits source without changing omission or presentation", + "status": "passed" + }, + { + "name": "profile-backed sections expand, reorder, hide and persist variant-only overrides", + "status": "passed" + }, + { + "name": "preview failure is visible and retryable without leaving the editor", + "status": "passed" + }, + { + "name": "[P5-11 PRES-C06 E08] automatic fit attaches when the asynchronously loaded preview pane appears", + "status": "passed" + }, + { + "name": "[P5-11 PRES-C06 C12] page count is announced only after the renderer has settled", + "status": "passed" + }, + { + "name": "[P5-11 PRES-C06 E09] overlapping preview successes and failures cannot replace the newest document", + "status": "passed" + }, + { + "name": "[P5-11 PRES-C06 C10 E08] returning from preview focuses the existing editor without changing the pending draft", + "status": "passed" + }, + { + "name": "[PRES-C11 PRES-C07] whole-document AI Apply changes only an explicitly accepted CV override", + "status": "passed" + }, + { + "name": "[PRES-C11 PRES-C07] whole-document AI Reject changes only an explicitly accepted CV override", + "status": "passed" + }, + { + "name": "[PRES-C11] unsafe whole-document response (invalid JSON) cannot be applied", + "status": "passed" + }, + { + "name": "[PRES-C11] unsafe whole-document response (missing original ID) cannot be applied", + "status": "passed" + }, + { + "name": "[PRES-C11] unsafe whole-document response (duplicate original ID) cannot be applied", + "status": "passed" + }, + { + "name": "bound document generation saves the pending draft, reviews server Original, and accepted AI remains undoable", + "status": "passed" + }, + { + "name": "a newer draft is flushed before stale proposal acceptance fails without rebasing or changing content", + "status": "passed" + }, + { + "name": "late document proposals cannot return after tab unmount or a source undo", + "status": "passed" + }, + { + "name": "bound section:skills assistant sends explicit scope and edited wording to server acceptance", + "status": "passed" + }, + { + "name": "bound custom:sample assistant sends explicit scope and edited wording to server acceptance", + "status": "passed" + }, + { + "name": "bound entry:experience:job-1 assistant sends explicit scope and edited wording to server acceptance", + "status": "passed" + }, + { + "name": "user edits made during server acceptance are retained instead of overwritten by its late saved response", + "status": "passed" + }, + { + "name": "LoginPage provides a main landmark and level-one heading for login", + "status": "passed" + }, + { + "name": "LoginPage provides a main landmark and level-one heading for register", + "status": "passed" + }, + { + "name": "LoginPage renders a dedicated registration screen and explains when registration is disabled", + "status": "passed" + }, + { + "name": "LoginPage renders one conventional sign-in card with enabled provider alternatives", + "status": "passed" + }, + { + "name": "LoginPage accepts a username and reports invalid credentials without changing routes", + "status": "passed" + }, + { + "name": "LoginPage handles Microsoft cancellation without exchanging or navigating", + "status": "passed" + }, + { + "name": "LoginPage completes the Microsoft provider return through the hardened exchange endpoint", + "status": "passed" + }, + { + "name": "LoginPage completes a Google credential return without account-linking copy", + "status": "passed" + }, + { + "name": "LoginPage posts remember-me preference without storing an auth token in browser storage", + "status": "passed" + }, + { + "name": "LoginPage does not complete login when registration requires email verification", + "status": "passed" + }, + { + "name": "LoginPage rejects an unsafe post-login redirect path", + "status": "passed" + }, + { + "name": "LoginPage uses the safe fallback for the untrusted return target \"https://evil.example/analytics\"", + "status": "passed" + }, + { + "name": "LoginPage uses the safe fallback for the untrusted return target \"//evil.example/analytics\"", + "status": "passed" + }, + { + "name": "LoginPage uses the safe fallback for the untrusted return target \"/\\t/evil.example\"", + "status": "passed" + }, + { + "name": "LoginPage uses the safe fallback for the untrusted return target \"/%2f%2fevil.example\"", + "status": "passed" + }, + { + "name": "LoginPage uses the safe fallback for the untrusted return target \"/%252f%252fevil.example\"", + "status": "passed" + }, + { + "name": "LoginPage uses the safe fallback for the untrusted return target \"/%5cevil.example\"", + "status": "passed" + }, + { + "name": "LoginPage uses the safe fallback for the untrusted return target \"/analytics?months=%ZZ\"", + "status": "passed" + }, + { + "name": "LoginPage uses the safe fallback for the untrusted return target \"/jobs/../login\"", + "status": "passed" + }, + { + "name": "LoginPage uses the safe fallback for the untrusted return target \"/%6cogin\"", + "status": "passed" + }, + { + "name": "LoginPage uses the safe fallback for the untrusted return target \"/register\"", + "status": "passed" + }, + { + "name": "LoginPage uses the safe fallback for the untrusted return target \"/analytics%0a\"", + "status": "passed" + }, + { + "name": "LoginPage uses the safe fallback for the untrusted return target \"/analytics?months=6\\n\"", + "status": "passed" + }, + { + "name": "LoginPage uses the safe fallback for the untrusted return target \"/LOGIN\"", + "status": "passed" + }, + { + "name": "LoginPage uses the safe fallback for the untrusted return target \"/%4cOGIN\"", + "status": "passed" + }, + { + "name": "LoginPage preserves encoded internal query and fragment without decoding the navigation target", + "status": "passed" + }, + { + "name": "LoginPage restores valid owned-workspace navigation context separately from the sign-in destination", + "status": "passed" + }, + { + "name": "LoginPage does not replay an unsafe nested return context after authentication", + "status": "passed" + }, + { + "name": "LoginPage abandons prior return context on the established account/session disposal entry", + "status": "passed" + }, + { + "name": "LoginPage opens the separate forgot-password page with the typed email prefilled", + "status": "passed" + }, + { + "name": "LoginPage shows the 2FA code step when login requires two-factor, then proceeds like a normal login on success", + "status": "passed" + }, + { + "name": "LoginPage shows a clear message when the 2FA challenge is rate-limited", + "status": "passed" + }, + { + "name": "LoginPage offers a resend-verification action when login reports the account is not verified", + "status": "passed" + }, + { + "name": "each selected Settings section has a named panel linked to its tab and remains selectable on phones", + "status": "passed" + }, + { + "name": "settings view has no accent picker and uses one follow-up section, one notification section", + "status": "passed" + }, + { + "name": "AI privacy settings are server-backed and external processing is local-only by default", + "status": "passed" + }, + { + "name": "[PRES-S03] failed notification save preserves the draft for an explicit retry", + "status": "passed" + }, + { + "name": "[PRES-S03 PRES-W11] saving one follow-up rule preserves the other scenarios", + "status": "passed" + }, + { + "name": "[PRES-S03 PRES-J05] table and theme preferences keep their independent callbacks", + "status": "passed" + }, + { + "name": "[PRES-S03 W11] the Settings session retains independent pending Rules fields across tab unmounts", + "status": "passed" + }, + { + "name": "[PRES-S03] failed Rules save remains actionable after tab return and explicit retry clears dirty state", + "status": "passed" + }, + { + "name": "[PRES-S03] invalid and empty Rules input and associated validation survive internal navigation", + "status": "passed" + }, + { + "name": "[PRES-S03] explicit discard resets only the Rules draft to its saved baseline without writing", + "status": "passed" + }, + { + "name": "[PRES-S03] later edits stay dirty when an earlier Rules save completes after a tab switch", + "status": "passed" + }, + { + "name": "[PRES-S03 E09] leaving the Settings session disposes its draft and pending responses", + "status": "passed" + }, + { + "name": "[PRES-S03] switching locale while Rules is hidden does not reload or overwrite its pending draft", + "status": "passed" + }, + { + "name": "[PRES-S03] Rules draft updates do not refetch an unrelated notification preference", + "status": "passed" + }, + { + "name": "Table/Board switches preserve query, complete membership, page, density and history without hidden reads", + "status": "passed" + }, + { + "name": "direct Board and both switch directions retain exact result membership: ?q=Engineer&status=Custom+review", + "status": "passed" + }, + { + "name": "direct Board and both switch directions retain exact result membership: ?q=absent", + "status": "passed" + }, + { + "name": "Board uses the shared filter controls and saved query owner without fetching Table", + "status": "passed" + }, + { + "name": "Trash stays a Table even when supplied a Board view parameter", + "status": "passed" + }, + { + "name": "saved filters applied in Board reset the Table page and refresh only the active population", + "status": "passed" + }, + { + "name": "row status/edit/delete controls retain selection and do not open the workspace", + "status": "passed" + }, + { + "name": "table sort, page-size and optional columns keep their existing independent contracts", + "status": "passed" + }, + { + "name": "Trash select-all and bulk restore use the original IDs without workspace navigation", + "status": "passed" + }, + { + "name": "density preserves selected rows, query, page, custom status, columns and listing link without refetch", + "status": "passed" + }, + { + "name": "density is local and resets on unmount without writing settings or a saved view", + "status": "passed" + }, + { + "name": "density survives an empty result and a failed refresh with the existing retry action", + "status": "passed" + }, + { + "name": "Bokmål display localizes standard statuses without changing API values or custom status filters", + "status": "passed" + }, + { + "name": "partial bulk failure refreshes successful rows and retains only failed visible IDs", + "status": "passed" + }, + { + "name": "saved view create/apply/delete stays scoped to the signed-in account", + "status": "passed" + }, + { + "name": "changing the result set clears selection before an old row can be bulk-updated", + "status": "passed" + }, + { + "name": "CorrespondenceInboxPage renders correspondence inbox items and reloads with filters", + "status": "passed" + }, + { + "name": "CorrespondenceInboxPage keeps saved correspondence visible when provider status is unavailable", + "status": "passed" + }, + { + "name": "CorrespondenceInboxPage opens provider-backed plain-text message detail in the hub", + "status": "passed" + }, + { + "name": "CorrespondenceInboxPage navigates beyond the first correspondence page", + "status": "passed" + }, + { + "name": "CorrespondenceInboxPage unlinks a Gmail thread only after confirmation and returns it to review", + "status": "passed" + }, + { + "name": "CorrespondenceInboxPage falls back to the saved copy when provider detail is unavailable", + "status": "passed" + }, + { + "name": "CorrespondenceInboxPage keeps replies editable and requires final send confirmation", + "status": "passed" + }, + { + "name": "CorrespondenceInboxPage saves an incomplete reply with its server delivery identity", + "status": "passed" + }, + { + "name": "CorrespondenceInboxPage resumes a saved draft and surfaces revision conflicts without overwriting it", + "status": "passed" + }, + { + "name": "CorrespondenceInboxPage persists a new delivery identity only after a definitive failed attempt", + "status": "passed" + }, + { + "name": "CorrespondenceInboxPage starts a new-message draft only from an owned job and send-capable provider choice", + "status": "passed" + }, + { + "name": "CorrespondenceInboxPage searches the complete server-side job choice set while composing", + "status": "passed" + }, + { + "name": "CorrespondenceInboxPage does not offer retry when delivery is uncertain", + "status": "passed" + }, + { + "name": "CorrespondenceInboxPage uses one hub for linked messages and recruitment suggestions", + "status": "passed" + }, + { + "name": "CorrespondenceInboxPage localizes inbox controls while preserving correspondence content", + "status": "passed" + }, + { + "name": "CorrespondenceInboxPage legacy Gmail review route redirects into the consolidated hub filter", + "status": "passed" + }, + { + "name": "opens the dedicated workspace from the whole row and preserves list state on return", + "status": "passed" + }, + { + "name": "opens a direct workspace URL and returns to applications", + "status": "passed" + }, + { + "name": "row controls do not trigger navigation", + "status": "passed" + }, + { + "name": "handles a deleted or inaccessible job without rendering a broken workspace", + "status": "passed" + }, + { + "name": "warns before section navigation would discard application edits", + "status": "passed" + }, + { + "name": "renders shared workspace navigation and progress in Norwegian Bokmål", + "status": "passed" + }, + { + "name": "hydrates list filters, sort and page from a shareable URL", + "status": "passed" + }, + { + "name": "names the progress scroll region and identifies the current stage without making stages actions", + "status": "passed" + }, + { + "name": "keeps custom status literal without claiming a completed standard pipeline", + "status": "passed" + }, + { + "name": "retries a failed workspace read without creating or changing application content", + "status": "passed" + }, + { + "name": "keeps the server-selected next action and preparation entry on the existing CV section", + "status": "passed" + }, + { + "name": "uses recorded dates in the summary and keeps absent dates unknown", + "status": "passed" + }, + { + "name": "section changes retain follow-up query context and expose the selected labelled panel", + "status": "passed" + }, + { + "name": "accordion controls identify exactly one labelled content region", + "status": "passed" + }, + { + "name": "legacy document links reveal current attachments and frozen history in the named section", + "status": "passed" + }, + { + "name": "the missing-profile next task opens the existing canonical profile entry", + "status": "passed" + }, + { + "name": "application subsection navigation retains metadata and protects unsaved answers", + "status": "passed" + }, + { + "name": "legacy correspondence and notes reveal distinct domain content", + "status": "passed" + }, + { + "name": "[PRES-E08 PRES-S05] en confirmation traps/returns focus and preserves cancel/Escape/accept results", + "status": "passed" + }, + { + "name": "[PRES-E08 PRES-S05] nb confirmation traps/returns focus and preserves cancel/Escape/accept results", + "status": "passed" + }, + { + "name": "[PRES-E08 PRES-W08] en prompt preserves helper link, exact text, cancellation and reopen defaults", + "status": "passed" + }, + { + "name": "[PRES-E08 PRES-W08] nb prompt preserves helper link, exact text, cancellation and reopen defaults", + "status": "passed" + }, + { + "name": "[PRES-E07] command search opens Nordlys AS - Platform engineer at its existing destination", + "status": "passed" + }, + { + "name": "[PRES-E07] command search opens Nordlys AS at its existing destination", + "status": "passed" + }, + { + "name": "[PRES-E07 PRES-J04] the capture command opens review without creating a job, and closing clears search", + "status": "passed" + }, + { + "name": "[P5-00 PRES-E07] command and empty results use valid list semantics", + "status": "passed" + }, + { + "name": "[P5-02 PRES-E07] search failure is distinct from no matches and Retry recovers", + "status": "passed" + }, + { + "name": "[P5-02 PRES-E09] closed and superseded searches cannot display stale account results", + "status": "passed" + }, + { + "name": "[P5-02 PRES-E07] a slower old query cannot replace the newer matching results", + "status": "passed" + }, + { + "name": "[P5-02 PRES-E07] arrows, Home, End and Enter activate one existing command; composition does not", + "status": "passed" + }, + { + "name": "[P5-02 PRES-E07] arrowing from a focused result keeps focus and activation on the same item", + "status": "passed" + }, + { + "name": "[PRES-C08 C09 E08] common controls and advanced disclosure do not mutate document settings", + "status": "passed" + }, + { + "name": "[PRES-C09 C10] numeric edits reject invalid ranges, keep exact valid values and reset only one setting", + "status": "passed" + }, + { + "name": "[PRES-C07 C08 C09] capability filtering preserves dormant saved values and exposes inherited sidebar controls", + "status": "passed" + }, + { + "name": "[PRES-C08 C10 E09] locks prevent selection and confirmed template resets preserve content and document metadata", + "status": "passed" + }, + { + "name": "[PRES-C08 C09 E08] Bokmål controls expose their scope, values and non-colour selection state", + "status": "passed" + }, + { + "name": "[PRES-C08 C09] unsupported and legacy saved values survive unrelated edits without implicit normalization", + "status": "passed" + }, + { + "name": "document package history is read-only and separates captured notes from answers", + "status": "passed" + }, + { + "name": "cv section shows the attached variant and the ones available to attach", + "status": "passed" + }, + { + "name": "attaching a different variant only re-points the application", + "status": "passed" + }, + { + "name": "cv section points at the builder when there are no variants", + "status": "passed" + }, + { + "name": "tailoring renders suggestions grouped by kind", + "status": "passed" + }, + { + "name": "tailoring asks for a career profile when there is none", + "status": "passed" + }, + { + "name": "submitted package capture appends an immutable history entry", + "status": "passed" + }, + { + "name": "cover letter loads the current text and its history", + "status": "passed" + }, + { + "name": "editing marks the draft dirty and saving sends the new text", + "status": "passed" + }, + { + "name": "discarding returns to the saved text", + "status": "passed" + }, + { + "name": "restoring an old version calls the restore endpoint", + "status": "passed" + }, + { + "name": "an empty cover letter offers the template and an empty history", + "status": "passed" + }, + { + "name": "the empty cover-letter template and rich-text controls follow Bokmål UI language", + "status": "passed" + }, + { + "name": "AI cover letter suggestions use the linked CV and require explicit apply", + "status": "passed" + }, + { + "name": "[PRES-W06] rejecting an AI cover letter preserves the saved draft without a write", + "status": "passed" + }, + { + "name": "a failed load surfaces an error", + "status": "passed" + }, + { + "name": "application answer and recruiter drafts save from the dedicated workspace", + "status": "passed" + }, + { + "name": "application package drafts can be cleared without deleting ordinary notes", + "status": "passed" + }, + { + "name": "correspondence Gmail import shows ranked Gmail suggestions with reasons and refreshes correspondence after message import", + "status": "passed" + }, + { + "name": "correspondence Gmail import automatically refreshes already-linked Gmail threads without manual re-import", + "status": "passed" + }, + { + "name": "correspondence Gmail import lets the user unlink a linked Gmail thread", + "status": "passed" + }, + { + "name": "correspondence Gmail import lets the user move a linked Gmail thread to another job", + "status": "passed" + }, + { + "name": "correspondence Gmail import shows Gmail sync state diagnostics alongside linked thread continuity", + "status": "passed" + }, + { + "name": "correspondence Gmail import manual Gmail search override reloads job candidates with queryOverride", + "status": "passed" + }, + { + "name": "localizes interview categories while preserving user-authored preparation", + "status": "passed" + }, + { + "name": "prep renders grouped items with progress and marks AI-sourced ones", + "status": "passed" + }, + { + "name": "adding a prep item posts the chosen category and title", + "status": "passed" + }, + { + "name": "starts a structured interview debrief without inventing answers", + "status": "passed" + }, + { + "name": "marking an item ready patches it", + "status": "passed" + }, + { + "name": "an answer is only saved when the user asks", + "status": "passed" + }, + { + "name": "deleting a prep item calls delete", + "status": "passed" + }, + { + "name": "empty prep shows a useful empty state", + "status": "passed" + }, + { + "name": "AI preparation uses the linked CV and only saves after explicit acceptance", + "status": "passed" + }, + { + "name": "prep surfaces a load error", + "status": "passed" + }, + { + "name": "follow-up loads the existing date and open checklist tasks", + "status": "passed" + }, + { + "name": "saving a follow-up sends the date and next action", + "status": "passed" + }, + { + "name": "no follow-up shows an empty state", + "status": "passed" + }, + { + "name": "attachment Upload activates the same native input from Enter, Space and pointer", + "status": "passed" + }, + { + "name": "Import JSON activates its own native input from Enter, Space and pointer", + "status": "passed" + }, + { + "name": "Convert CSV to Import JSON activates its own native input from Enter, Space and pointer", + "status": "passed" + }, + { + "name": "Add Job attachment bucket 0 retains keyboard selection and distinct pending files", + "status": "passed" + }, + { + "name": "Add Job attachment bucket 1 retains keyboard selection and distinct pending files", + "status": "passed" + }, + { + "name": "Add Job attachment bucket 2 retains keyboard selection and distinct pending files", + "status": "passed" + }, + { + "name": "Add Job attachment bucket 3 retains keyboard selection and distinct pending files", + "status": "passed" + }, + { + "name": "attachment selection retains files and job ownership, disables activation while uploading and clears selection after completion", + "status": "passed" + }, + { + "name": "follow-up workflow signals route all overview surfaces to the same follow-up workspace", + "status": "passed" + }, + { + "name": "package-work workflow signals route all overview surfaces to the shared tailored-cv workspace", + "status": "passed" + }, + { + "name": "job table readiness filter follows workflow signals instead of raw notes or cv text heuristics", + "status": "passed" + }, + { + "name": "section AI uses the CV document language and requires explicit server acceptance", + "status": "passed" + }, + { + "name": "section AI controls render in Bokmål without changing the document language", + "status": "passed" + }, + { + "name": "[PRES-C11] Reject removes the suggestion without accepting content", + "status": "passed" + }, + { + "name": "[PRES-C11] Close removes the suggestion without accepting content", + "status": "passed" + }, + { + "name": "[PRES-C11] accepts the user's edited suggestion with the original proposal token", + "status": "passed" + }, + { + "name": "[PRES-C11] a failed suggestion can be retried without accepting content", + "status": "passed" + }, + { + "name": "closing and reopening ignores an old request even after a newer suggestion completes", + "status": "passed" + }, + { + "name": "a changed text invalidates a pending suggestion", + "status": "passed" + }, + { + "name": "a changed target invalidates a pending suggestion", + "status": "passed" + }, + { + "name": "a changed document language invalidates a pending suggestion", + "status": "passed" + }, + { + "name": "source changes remove an existing review before it can be accepted", + "status": "passed" + }, + { + "name": "regeneration replaces the proposal and its token before acceptance", + "status": "passed" + }, + { + "name": "Reject invalidates a pending regeneration and clears the previous review", + "status": "passed" + }, + { + "name": "failed server acceptance keeps the edited review and allows retry without duplicate submits", + "status": "passed" + }, + { + "name": "shows honest source metadata and offers the existing reviewed save flow", + "status": "passed" + }, + { + "name": "distinguishes the initial guidance from a completed empty search", + "status": "passed" + }, + { + "name": "retries the last submitted search after an error", + "status": "passed" + }, + { + "name": "sorts returned vacancies by title", + "status": "passed" + }, + { + "name": "renders the discovery workflow in Norwegian Bokmål", + "status": "passed" + }, + { + "name": "runs a saved search, marks unseen vacancies, and dismisses them", + "status": "passed" + }, + { + "name": "lets the user pause automatic alerts for a saved search", + "status": "passed" + }, + { + "name": "announces saved-search loading without presenting a successful empty list", + "status": "passed" + }, + { + "name": "reports saved-search failure and retries that read without replacing active discovery filters or results", + "status": "passed" + }, + { + "name": "rejects malformed saved-search payload null as a failed read rather than an empty list", + "status": "passed" + }, + { + "name": "rejects malformed saved-search payload {\"message\":\"not a list\"} as a failed read rather than an empty list", + "status": "passed" + }, + { + "name": "rejects malformed saved-search payload [{\"id\":7}] as a failed read rather than an empty list", + "status": "passed" + }, + { + "name": "the board collapses ten stages into the three agreed groups", + "status": "passed" + }, + { + "name": "a custom status gets an Other column that is not a drop target", + "status": "passed" + }, + { + "name": "loading and retryable error states replace the board", + "status": "passed" + }, + { + "name": "loading and retry controls follow the selected Bokmål locale", + "status": "passed" + }, + { + "name": "dropping onto a group applies that group entry stage", + "status": "passed" + }, + { + "name": "keyboard pickup and drop moves a card to a valid column", + "status": "passed" + }, + { + "name": "a failed move keeps the card and reports a retryable error", + "status": "passed" + }, + { + "name": "the card menu offers precise stages grouped by section", + "status": "passed" + }, + { + "name": "full-board population keeps jobs beyond the table page and offers a separate table link", + "status": "passed" + }, + { + "name": "keyboard Escape cancels movement without a write and successful movement restores card focus", + "status": "passed" + }, + { + "name": "opening a card preserves its board return route and does not mutate status", + "status": "passed" + }, + { + "name": "keyboard activation of the exact-status menu does not open the job", + "status": "passed" + }, + { + "name": "Board sends the shared logical URL query, excluding Table paging and client ownership fields", + "status": "passed" + }, + { + "name": "a successful filtered move reloads membership and focuses the board when the job leaves it", + "status": "passed" + }, + { + "name": "a committed move followed by a failed query hides stale membership until retry succeeds", + "status": "passed" + }, + { + "name": "EmailProviderConnections renders connection controls in Norwegian Bokmål", + "status": "passed" + }, + { + "name": "EmailProviderConnections renders connected state for Gmail and Outlook, disconnected form for IMAP", + "status": "passed" + }, + { + "name": "EmailProviderConnections submits IMAP connect form and reloads status on success", + "status": "passed" + }, + { + "name": "AdminSystemPage uses contextual system headings and keeps metric values out of heading navigation", + "status": "passed" + }, + { + "name": "AdminSystemPage renders AI service health, latency, and OCR readiness", + "status": "passed" + }, + { + "name": "AdminSystemPage loads and saves smtp settings from the settings tab", + "status": "passed" + }, + { + "name": "AdminSystemPage dispatches the allowlisted CV benchmark from the system panel", + "status": "passed" + }, + { + "name": "AdminSystemPage localizes benchmark diagnostics without changing runtime values", + "status": "passed" + }, + { + "name": "[PRES-S01] exact monthly values, sample, custom statuses and separate salary group remain readable", + "status": "passed" + }, + { + "name": "[PRES-S02] all five stored visibility flags hide their groups and survive remount", + "status": "passed" + }, + { + "name": "[PRES-S01] a trends failure retains summary and retries without showing a false zero response rate", + "status": "passed" + }, + { + "name": "[PRES-S01] a summary failure does not suppress independent trends", + "status": "passed" + }, + { + "name": "[PRES-J05 J09 S01] raw status and company destinations use supported exact query fields", + "status": "passed" + }, + { + "name": "[PRES-S01 S02] period changes only reload trends and retain current snapshot meaning", + "status": "passed" + }, + { + "name": "[PRES-S01] zero activity is an explicit window-empty state without a fabricated response percentage", + "status": "passed" + }, + { + "name": "[PRES-S01] activity chart uses one shared zero-based scale and a readable legend", + "status": "passed" + }, + { + "name": "cancelling setup sends no security write and clears the password before reopening", + "status": "passed" + }, + { + "name": "regenerated recovery codes require acknowledgement, survive Escape, export correctly, and clear after Done", + "status": "passed" + }, + { + "name": "Bokmål enabled status and trusted-device dates survive a read-only retry, then revoke the selected device with readback", + "status": "passed" + }, + { + "name": "a failed sessions read offers a read-only retry without inventing an empty session list", + "status": "passed" + }, + { + "name": "pending revoke-others disables confirmation and cancellation, blocks duplicate writes, and reads back the surviving session", + "status": "passed" + }, + { + "name": "opens one company relationship view with applications and recent contact", + "status": "passed" + }, + { + "name": "company contact dates follow the en UI locale", + "status": "passed" + }, + { + "name": "company contact dates follow the nb UI locale", + "status": "passed" + }, + { + "name": "[PRES-J08 E08] en company save failure remains inside the dialog with the draft available for retry", + "status": "passed" + }, + { + "name": "[PRES-J08 E08] nb company save failure remains inside the dialog with the draft available for retry", + "status": "passed" + }, + { + "name": "[PRES-C08 PRES-C09] en CV selects have visible names and retain exact override patches", + "status": "passed" + }, + { + "name": "[PRES-C08 PRES-C09] nb CV selects have visible names and retain exact override patches", + "status": "passed" + }, + { + "name": "[PRES-C07 PRES-W06] en rich text retains names, selected Markdown and editor focus", + "status": "passed" + }, + { + "name": "[PRES-C07 PRES-W06] nb rich text retains names, selected Markdown and editor focus", + "status": "passed" + }, + { + "name": "[PRES-C07 PRES-W06] disabled rich text cannot format or edit and repeated fields keep separate labels", + "status": "passed" + }, + { + "name": "requires explicit confirmation before removing your own admin role", + "status": "passed" + }, + { + "name": "warns before demoting another administrator", + "status": "passed" + }, + { + "name": "disables demotion and deletion for the final administrator", + "status": "passed" + }, + { + "name": "sends the exact account email after confirming deletion", + "status": "passed" + }, + { + "name": "dashboard attention card opens follow-up workspace", + "status": "passed" + }, + { + "name": "empty dashboard prioritizes onboarding and suppresses meaningless analytics", + "status": "passed" + }, + { + "name": "reminders open action routes tailored-cv gaps into the tailored cv workspace", + "status": "passed" + }, + { + "name": "job table urgency signals and next actions route into the shared workspace flow", + "status": "passed" + }, + { + "name": "public auth presentation names the password field for login intent and retains its autocomplete contract", + "status": "passed" + }, + { + "name": "public auth presentation names the password field for register intent and retains its autocomplete contract", + "status": "passed" + }, + { + "name": "public auth presentation provides one named main landmark and H1 for password request", + "status": "passed" + }, + { + "name": "public auth presentation provides one named main landmark and H1 for password reset", + "status": "passed" + }, + { + "name": "public auth presentation provides one named main landmark and H1 for email verification", + "status": "passed" + }, + { + "name": "public auth presentation provides one named main landmark and H1 for email change", + "status": "passed" + }, + { + "name": "public auth presentation provides one named main landmark and H1 for Microsoft recovery", + "status": "passed" + }, + { + "name": "public auth presentation announces the pending confirmation without an unnamed progressbar: false", + "status": "passed" + }, + { + "name": "public auth presentation announces the pending confirmation without an unnamed progressbar: true", + "status": "passed" + }, + { + "name": "public auth presentation retains the entered reset email and a durable error in the page when the command fails", + "status": "passed" + }, + { + "name": "public auth presentation retains both new-password fields and the decoded recovery token after a rejected reset", + "status": "passed" + }, + { + "name": "public auth presentation keeps a rejected local sign-in beside the retained fields without navigating", + "status": "passed" + }, + { + "name": "public auth presentation keeps the reset-request action name while announcing an in-flight command", + "status": "passed" + }, + { + "name": "public auth presentation announces an in-flight local sign-in while preserving its action name", + "status": "passed" + }, + { + "name": "public auth presentation announces a pending password save without changing its payload or successful destination", + "status": "passed" + }, + { + "name": "public auth presentation focuses the first invalid local-login field and associates its explanation", + "status": "passed" + }, + { + "name": "public auth presentation focuses the invalid new password without losing confirmation input", + "status": "passed" + }, + { + "name": "public auth presentation focuses and explains the invalid reset-request email without sending a command", + "status": "passed" + }, + { + "name": "public auth presentation replaces previous reset-request success when a later request fails", + "status": "passed" + }, + { + "name": "public auth presentation does not assert optional authentication while login configuration is unresolved", + "status": "passed" + }, + { + "name": "public auth presentation does not assert optional authentication while register configuration is unresolved", + "status": "passed" + }, + { + "name": "public auth presentation offers a read-only configuration retry while retaining entered sign-in fields", + "status": "passed" + }, + { + "name": "PDF status 503 shows an actionable message without retrying", + "status": "passed" + }, + { + "name": "PDF status 504 shows an actionable message without retrying", + "status": "passed" + }, + { + "name": "lists existing CVs from the variants API", + "status": "passed" + }, + { + "name": "shows the empty state and creates a CV then navigates to the editor", + "status": "passed" + }, + { + "name": "CV dashboard and create flow render in Bokmål", + "status": "passed" + }, + { + "name": "CV deletion requires confirmation and supports cancellation", + "status": "passed" + }, + { + "name": "[PRES-S01 S02] all existing reports have exact values independent of hidden Dashboard flags", + "status": "passed" + }, + { + "name": "[PRES-S01 E07] period is direct-loadable and browser Back restores the period without refetching snapshot", + "status": "passed" + }, + { + "name": "[PRES-S01] failed snapshot remains an error while trends and retry stay available", + "status": "passed" + }, + { + "name": "[PRES-S01] empty cohorts do not invent a denominator, and unsupported URL periods default safely", + "status": "passed" + }, + { + "name": "[PRES-J05 J09] drill-down retains supported raw status and company identities", + "status": "passed" + }, + { + "name": "auto-imports from initialUrl and prefills the form", + "status": "passed" + }, + { + "name": "does not auto-import when no initialUrl is given", + "status": "passed" + }, + { + "name": "localizes the job creation workflow in Norwegian Bokmål", + "status": "passed" + }, + { + "name": "can generate a tailored CV after creating a job", + "status": "passed" + }, + { + "name": "shows notifications and updates read and dismissed state in place", + "status": "passed" + }, + { + "name": "opens a notification destination instead of routing the bell itself", + "status": "passed" + }, + { + "name": "shows an honest empty state", + "status": "passed" + }, + { + "name": "[P5-02 PRES-E09] a closed notification request cannot populate a later account's panel", + "status": "passed" + }, + { + "name": "[P5-02 PRES-E09] a delayed read cannot navigate from a closed earlier-account panel", + "status": "passed" + }, + { + "name": "[P5-02 PRES-S02] failed notification load offers Retry without claiming an empty inbox", + "status": "passed" + }, + { + "name": "shows not-enabled status and walks through the full enable flow to recovery codes", + "status": "passed" + }, + { + "name": "shows wrong-password error on disable and lets the user retry", + "status": "passed" + }, + { + "name": "timeline renders grouped days, milestones and readable summaries", + "status": "passed" + }, + { + "name": "timeline filters by category", + "status": "passed" + }, + { + "name": "timeline shows an empty state when nothing has happened", + "status": "passed" + }, + { + "name": "timeline surfaces an error instead of rendering nothing", + "status": "passed" + }, + { + "name": "analysis renders the extracted structure", + "status": "passed" + }, + { + "name": "analysis prompts for the advert when there is none", + "status": "passed" + }, + { + "name": "analysis shows a loading state before the data arrives", + "status": "passed" + }, + { + "name": "match renders the score, evidence and suggestions", + "status": "passed" + }, + { + "name": "match asks for a career profile before showing a score", + "status": "passed" + }, + { + "name": "match warns when the advert is too short to score", + "status": "passed" + }, + { + "name": "match explains when a saved translation is used for a cross-language comparison", + "status": "passed" + }, + { + "name": "end-to-end trust loop overview entry opens the canonical communication workspace without sending mail", + "status": "passed" + }, + { + "name": "localizes system checklist content without changing user-authored tasks", + "status": "passed" + }, + { + "name": "renders the checklist grouped by category with progress", + "status": "passed" + }, + { + "name": "completing an item patches its status", + "status": "passed" + }, + { + "name": "un-ticking a completed item sends it back to pending", + "status": "passed" + }, + { + "name": "adding a custom task posts the title", + "status": "passed" + }, + { + "name": "reordering sends the new id order", + "status": "passed" + }, + { + "name": "removing an item calls delete", + "status": "passed" + }, + { + "name": "[P5-00 PRES-E07 E08 A03] en mobile names/current route ignore desktop collapse and retain metadata", + "status": "passed" + }, + { + "name": "[P5-00 PRES-E07 E08 A03] nb mobile names/current route ignore desktop collapse and retain metadata", + "status": "passed" + }, + { + "name": "[P5-00 PRES-E08 S05] toast retains its full existing display duration and dismiss action", + "status": "passed" + }, + { + "name": "[P5-00 PRES-E08] toast action is explicit and executes once", + "status": "passed" + }, + { + "name": "migrates the legacy Norwegian code and persists Bokmål using nb-NO", + "status": "passed" + }, + { + "name": "hydrates an authenticated server preference without writing it back", + "status": "passed" + }, + { + "name": "a failed account language save keeps the local choice and offers a localized retry: Norsk", + "status": "passed" + }, + { + "name": "a failed account language save keeps the local choice and offers a localized retry: English", + "status": "passed" + }, + { + "name": "a delayed language-save failure cannot offer a retry for a newer successful choice", + "status": "passed" + }, + { + "name": "a previously delivered language retry cannot reverse a newer choice", + "status": "passed" + }, + { + "name": "[PRES-E02/E09] Bokmål signed-in status and section semantics use the UI language", + "status": "passed" + }, + { + "name": "[PRES-E02/E08] a failed identity read is unknown and retries without a logout or login write", + "status": "passed" + }, + { + "name": "[PRES-E02] an authoritative unauthenticated identity read retains signed-out presentation", + "status": "passed" + }, + { + "name": "[PRES-E04] unknown 2FA status announces loading and cannot suggest setup", + "status": "passed" + }, + { + "name": "[PRES-E04/E08] failed 2FA status offers read-only retry before security actions", + "status": "passed" + }, + { + "name": "[PRES-E05/E08] session-revocation failure stays inside its named confirmation for retry", + "status": "passed" + }, + { + "name": "[PRES-E05/E09] sessions keep stored timestamps and render dates in Bokmål", + "status": "passed" + }, + { + "name": "downloads the readable account ZIP from the protected endpoint", + "status": "passed" + }, + { + "name": "explains the recent-sign-in requirement without weakening it", + "status": "passed" + }, + { + "name": "keeps account deletion hidden behind the server flag", + "status": "passed" + }, + { + "name": "requires the exact server-provided deletion phrase", + "status": "passed" + }, + { + "name": "[P5-01 PRES-E07 E08] grouped navigation exposes the current child without marking the parent as the page", + "status": "passed" + }, + { + "name": "[P5-01 PRES-E07 E08] collapsed secondary destinations remain keyboard-operable", + "status": "passed" + }, + { + "name": "[P5-01 PRES-E08] successful shell navigation focuses the new route heading and exposes a skip link", + "status": "passed" + }, + { + "name": "light loaded company workflow loading → populated remains understandable and usable", + "status": "passed" + }, + { + "name": "light loaded company workflow loading → empty remains understandable and usable", + "status": "passed" + }, + { + "name": "light loaded company workflow loading → error remains understandable and usable", + "status": "passed" + }, + { + "name": "dark loaded company workflow loading → populated remains understandable and usable", + "status": "passed" + }, + { + "name": "dark loaded company workflow loading → empty remains understandable and usable", + "status": "passed" + }, + { + "name": "dark loaded company workflow loading → error remains understandable and usable", + "status": "passed" + }, + { + "name": "first-run workspace presents concise profile, import, general CV and job-specific actions", + "status": "passed" + }, + { + "name": "career workspace overview and import state render in Bokmål", + "status": "passed" + }, + { + "name": "returning workspace shows missing profile information and recent general and job-specific CVs", + "status": "passed" + }, + { + "name": "workspace exposes the pending_review import state as a resumable action", + "status": "passed" + }, + { + "name": "workspace exposes the queued import state as a resumable action", + "status": "passed" + }, + { + "name": "workspace exposes the failed import state as a resumable action", + "status": "passed" + }, + { + "name": "workspace represents loading and profile-load errors without inventing status", + "status": "passed" + }, + { + "name": "saves factual evidence with provenance and tags", + "status": "passed" + }, + { + "name": "shows exactly Free and Pro without invented commercial terms", + "status": "passed" + }, + { + "name": "switches the public landing page to Norwegian Bokmål", + "status": "passed" + }, + { + "name": "GmailReviewPage renders Gmail review queue summary and candidate threads", + "status": "passed" + }, + { + "name": "GmailReviewPage persists a review decision for the top job", + "status": "passed" + }, + { + "name": "GmailReviewPage renders the review workflow in Norwegian Bokmål", + "status": "passed" + }, + { + "name": "attachments empty state renders drag and drop guidance", + "status": "passed" + }, + { + "name": "attachments metadata controls update purpose and ai usage", + "status": "passed" + }, + { + "name": "attachment failures remain explicit and retry restores the real empty state", + "status": "passed" + }, + { + "name": "notification bell exposes its unread count and keyboard-accessible action", + "status": "passed" + }, + { + "name": "shows build metadata only when the caller supplies the admin-only badge", + "status": "passed" + }, + { + "name": "shell icon controls expose accessible names", + "status": "passed" + }, + { + "name": "shows persistent operation and notification states with accessible actions", + "status": "passed" + }, + { + "name": "shows honest empty and failure states", + "status": "passed" + }, + { + "name": "localizes operation controls and empty states in Norwegian Bokmål", + "status": "passed" + }, + { + "name": "compares recorded offer facts without filling missing values", + "status": "passed" + }, + { + "name": "shows an honest empty state when there are no offers", + "status": "passed" + }, + { + "name": "account-scoped browser state keeps saved views private to the account that created them", + "status": "passed" + }, + { + "name": "account-scoped browser state does not show a previous account's cached companies after account switching", + "status": "passed" + }, + { + "name": "feedback does not reload company data; changing locale still reloads translated states", + "status": "passed" + }, + { + "name": "legacy Microsoft recovery requires the emailed proof and a fresh matching Microsoft token", + "status": "passed" + }, + { + "name": "pending Microsoft recovery keeps a named action and announces progress before the real confirmation boundary", + "status": "passed" + }, + { + "name": "LandingPage forwards deep-link intent to /login preserves location.state.from through the Sign in CTA", + "status": "passed" + }, + { + "name": "recruiter status suggestions require an explicit apply action", + "status": "passed" + }, + { + "name": "strategy snapshot shows saved output and queues regeneration only on request", + "status": "passed" + }, + { + "name": "[PRES-E08] page and nested section headings retain full names and independent actions", + "status": "passed" + }, + { + "name": "[PRES-E08] section composition retains loaded, error/retry and empty child semantics", + "status": "passed" + }, + { + "name": "adds the selected application date to a writable connected calendar", + "status": "passed" + }, + { + "name": "keeps portable calendar download available without a provider connection", + "status": "passed" + }, + { + "name": "explains when an existing connection needs renewed calendar consent", + "status": "passed" + }, + { + "name": "[PRES-E08] en loading/error/recovery updates one persistent polite region", + "status": "passed" + }, + { + "name": "[PRES-E08] nb loading/error/recovery updates one persistent polite region", + "status": "passed" + }, + { + "name": "[PRES-E08] idle/error-only notices do not invent a load completion; later loading still announces", + "status": "passed" + }, + { + "name": "distinguishes an unavailable audit log from an empty audit log and retries", + "status": "passed" + }, + { + "name": "localizes audit actions in Bokmål without translating stored event data", + "status": "passed" + }, + { + "name": "csv download preserves server bytes and a generated safe filename", + "status": "passed" + }, + { + "name": "json download preserves server bytes and a generated safe filename", + "status": "passed" + }, + { + "name": "VerifyEmailPage confirms the account and shows success when the link is valid", + "status": "passed" + }, + { + "name": "VerifyEmailPage shows an error when the link is invalid or expired", + "status": "passed" + }, + { + "name": "VerifyEmailPage shows an error without calling the API when the link is missing userId/token", + "status": "passed" + }, + { + "name": "VerifyEmailPage confirms a pending email change and requires a fresh sign-in", + "status": "passed" + }, + { + "name": "shows monthly AI call and token limits", + "status": "passed" + }, + { + "name": "does not offer a false upgrade action when billing is not configured", + "status": "passed" + }, + { + "name": "renders app-owned prompt dialog", + "status": "passed" + }, + { + "name": "shows prompt input helper length text", + "status": "passed" + }, + { + "name": "uses structured career data and connected inbox status for onboarding progress", + "status": "passed" + }, + { + "name": "builds contextual suggestions for the shared application correspondence view", + "status": "passed" + }, + { + "name": "does not invent weak context when the workspace has no company details", + "status": "passed" + }, + { + "name": "renders app-owned confirmation dialog", + "status": "passed" + }, + { + "name": "uses the selected language for shared dialog defaults", + "status": "passed" + }, + { + "name": "deterministic theme state uses one browser preference and falls back to System", + "status": "passed" + }, + { + "name": "deterministic theme state does not change theme on login or logout", + "status": "passed" + }, + { + "name": "deterministic theme state synchronizes only the canonical cross-tab preference", + "status": "passed" + }, + { + "name": "deterministic theme state migrates the previous account-scoped preference", + "status": "passed" + }, + { + "name": "deterministic theme state uses system preference only for System mode", + "status": "passed" + }, + { + "name": "deterministic theme state applies the saved preference before the client application renders", + "status": "passed" + }, + { + "name": "deterministic theme state changes color scheme without remounting application state", + "status": "passed" + }, + { + "name": "shows a respectful upgrade action and remembers dismissal for the session", + "status": "passed" + }, + { + "name": "destructive confirm dialog shows warning state", + "status": "passed" + }, + { + "name": "template thumbnails stay distinct and never replace themselves with sandboxed frames", + "status": "passed" + }, + { + "name": "renders the security check and provider widget in Bokmål", + "status": "passed" + }, + { + "name": "prompt dialog opens with default rename value", + "status": "passed" + }, + { + "name": "sidebar active nav ownership /career activates Career Workspace only", + "status": "passed" + }, + { + "name": "sidebar active nav ownership /career/builder activates CV Builder only, not Career Workspace", + "status": "passed" + }, + { + "name": "sidebar active nav ownership /career/builder/{id} activates CV Builder only (the reported bug)", + "status": "passed" + }, + { + "name": "sidebar active nav ownership a plain child of /career (not /builder) still belongs to Career Workspace", + "status": "passed" + }, + { + "name": "sidebar active nav ownership unrelated routes are unaffected and exact matches win", + "status": "passed" + }, + { + "name": "sidebar active nav ownership a path owned by no nav item activates nothing", + "status": "passed" + }, + { + "name": "sidebar active nav ownership exactly one item is ever active (no double highlight)", + "status": "passed" + }, + { + "name": "grouped sidebar ownership non-nested Jobs destinations select their actual secondary owner", + "status": "passed" + }, + { + "name": "grouped sidebar ownership legacy application details retain Jobs ownership", + "status": "passed" + }, + { + "name": "grouped sidebar ownership CV editor routes never select Career Profile", + "status": "passed" + }, + { + "name": "writes carry the page's server-issued context and preserve the same account", + "status": "passed" + }, + { + "name": "cookie switch rejects a stale callback before transport and never adopts B's context", + "status": "passed" + }, + { + "name": "cross-tab storage event authUserKey invalidates and aborts outstanding requests", + "status": "passed" + }, + { + "name": "cross-tab storage event authUserKey invalidates and aborts outstanding requests", + "status": "passed" + }, + { + "name": "cross-tab storage event authSessionContext invalidates and aborts outstanding requests", + "status": "passed" + }, + { + "name": "cross-tab storage event null invalidates and aborts outstanding requests", + "status": "passed" + }, + { + "name": "another session cannot be adopted even if the readable cookie signal is unchanged", + "status": "passed" + }, + { + "name": "same-account reauthentication also discards the old page", + "status": "passed" + }, + { + "name": "a delayed private response cannot repopulate caches after invalidation", + "status": "passed" + }, + { + "name": "a late bootstrap read/error cannot overwrite or sign out a newly bound session (error=false)", + "status": "passed" + }, + { + "name": "a late bootstrap read/error cannot overwrite or sign out a newly bound session (error=true)", + "status": "passed" + }, + { + "name": "server precondition failure clears the page without silently retrying a mutation", + "status": "passed" + }, + { + "name": "BFCache restoration invalidates old private state", + "status": "passed" + }, + { + "name": "anonymous public-page history does not require a new login", + "status": "passed" + }, + { + "name": "cookie fallback still works without a cross-tab storage event", + "status": "passed" + }, + { + "name": "light semantic contrast primary supports text and filled actions", + "status": "passed" + }, + { + "name": "light semantic contrast secondary supports text and filled actions", + "status": "passed" + }, + { + "name": "light semantic contrast success supports text and filled actions", + "status": "passed" + }, + { + "name": "light semantic contrast warning supports text and filled actions", + "status": "passed" + }, + { + "name": "light semantic contrast error supports text and filled actions", + "status": "passed" + }, + { + "name": "light semantic contrast info supports text and filled actions", + "status": "passed" + }, + { + "name": "light semantic contrast focus and essential control boundaries contrast with all application surfaces", + "status": "passed" + }, + { + "name": "dark semantic contrast primary supports text and filled actions", + "status": "passed" + }, + { + "name": "dark semantic contrast secondary supports text and filled actions", + "status": "passed" + }, + { + "name": "dark semantic contrast success supports text and filled actions", + "status": "passed" + }, + { + "name": "dark semantic contrast warning supports text and filled actions", + "status": "passed" + }, + { + "name": "dark semantic contrast error supports text and filled actions", + "status": "passed" + }, + { + "name": "dark semantic contrast info supports text and filled actions", + "status": "passed" + }, + { + "name": "dark semantic contrast focus and essential control boundaries contrast with all application surfaces", + "status": "passed" + }, + { + "name": "public CV exposes the rendered CV and PDF download", + "status": "passed" + }, + { + "name": "ignores an older request that completes after a newer dependency load", + "status": "passed" + }, + { + "name": "[P5-14 PRES-C07] nested item drag reorders only the owning item list", + "status": "passed" + }, + { + "name": "[P5-14 PRES-C07] unsupported cross-section item drop cannot reorder sections", + "status": "passed" + }, + { + "name": "[P5-14 PRES-C07] section drag over an inactive nested item reaches the section owner", + "status": "passed" + }, + { + "name": "[P5-14 PRES-C07] source dragend retains ownership after drop resets state and clears a cancelled gesture", + "status": "passed" + }, + { + "name": "pipeline normalizeStatus canonicalizes casing and synonyms", + "status": "passed" + }, + { + "name": "pipeline normalizeStatus preserves unknown as Other and empty as Applied", + "status": "passed" + }, + { + "name": "pipeline statusTone maps stages to palette keys", + "status": "passed" + }, + { + "name": "pipeline statusLabel localizes canonical and passes through custom", + "status": "passed" + }, + { + "name": "pipeline canonical stage list is stable and ordered", + "status": "passed" + }, + { + "name": "pipeline normalizeStatus canonicalizes the new stage synonyms", + "status": "passed" + }, + { + "name": "pipeline board groups match the agreed layout", + "status": "passed" + }, + { + "name": "pipeline every stage belongs to exactly one group, and groups agree with the backend", + "status": "passed" + }, + { + "name": "pipeline groupOf and isProspect classify stages", + "status": "passed" + }, + { + "name": "pipeline drag entry stages never infer Ghosted or Withdrawn", + "status": "passed" + }, + { + "name": "pipeline statusTone covers every stage", + "status": "passed" + }, + { + "name": "workspace navigation exposes seven purposes and preserves legacy links", + "status": "passed" + }, + { + "name": "legacy material and correspondence links reveal their original owner without rewriting URLs", + "status": "passed" + }, + { + "name": "canonical subsection URLs are durable and unknown inputs have safe defaults", + "status": "passed" + }, + { + "name": "transport failures use the caller's localized fallback: Failed to update company.", + "status": "passed" + }, + { + "name": "transport failures use the caller's localized fallback: Kunne ikke oppdatere selskap.", + "status": "passed" + }, + { + "name": "structured validation and sanitized server explanations remain available", + "status": "passed" + }, + { + "name": "explicit session cancellation explanations remain intact", + "status": "passed" + }, + { + "name": "ordinary application errors retain their explanation", + "status": "passed" + }, + { + "name": "canonical-host health reaches the backend instead of the SPA fallback", + "status": "passed" + }, + { + "name": "resolveCaptureUrl reads the bookmarklet add param", + "status": "passed" + }, + { + "name": "resolveCaptureUrl extracts a url embedded in shared text", + "status": "passed" + }, + { + "name": "resolveCaptureUrl prefers add over addtext", + "status": "passed" + }, + { + "name": "resolveCaptureUrl returns null when there is no url", + "status": "passed" + }, + { + "name": "application answer helpers keep human notes separate", + "status": "passed" + }, + { + "name": "clearing an answer retains human notes and removes all marker blocks", + "status": "passed" + }, + { + "name": "legacy application answer labels remain readable during migration", + "status": "passed" + }, + { + "name": "moveItem moves an item forward", + "status": "passed" + }, + { + "name": "moveItem moves an item backward", + "status": "passed" + }, + { + "name": "moveItem no-op for equal or out-of-range indices", + "status": "passed" + }, + { + "name": "moveItem does not mutate the input", + "status": "passed" + }, + { + "name": "wrapSelection wraps a selection and keeps the selection over the inner text", + "status": "passed" + }, + { + "name": "wrapSelection inserts a placeholder when nothing is selected", + "status": "passed" + }, + { + "name": "wrapSelection wraps a link with a url suffix", + "status": "passed" + }, + { + "name": "CV page measurement uses the selected physical page size", + "status": "passed" + }, + { + "name": "CV page measurement rounds layout pixels without hiding partial pages", + "status": "passed" + }, + { + "name": "CV language compatibility maps legacy Norwegian values to the supported Bokmål locale", + "status": "passed" + }, + { + "name": "CV language compatibility fits the physical page to the available preview width", + "status": "passed" + }, + { + "name": "CV template selection restores the selected template visual defaults without discarding user content", + "status": "passed" + }, + { + "name": "CV template selection shows ATS templates first and keeps an active non-ATS template visible", + "status": "passed" + }, + { + "name": "application panels use distinct timeline, interview board, and generated brief routes", + "status": "passed" + }, + { + "name": "test harness is configured", + "status": "passed" + }, + { + "name": "publishes only capability-based Free and Pro plans", + "status": "passed" + } + ] + }, + "nativeLanes": { + "account": { + "command": { + "name": "account", + "command": [ + "rtk", + "proxy", + "node", + "node_modules/@playwright/test/cli.js", + "test", + "e2e/phase7-account-security.spec.ts", + "--retries=0", + "--reporter=list,json", + "--output=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\ui\\account-results" + ], + "startedUtc": "2026-10-11T19:04:29.737861+00:00", + "finishedUtc": "2026-10-11T19:05:01.521044+00:00", + "exitCode": 0, + "stats": { + "startTime": "2026-10-11T19:04:30.267Z", + "duration": 31208.54, + "expected": 6, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + } + }, + "commandRaw": { + "path": ".artifacts/phase7/p7-01/account-final/runtime/account-command.json", + "sha256": "be6cffdbf96a61108f6a5a9e0da718cb58dac235509dd4e3a5195cb36b57bd71", + "bytes": 643, + "hashPolicy": "Exact measured raw bytes" + }, + "reportRaw": { + "path": ".artifacts/phase7/p7-01/account-final/runtime/account.json", + "sha256": "b326dfa880cf0741ee02b69692abe30dcc696b8d2194aa7bf37fd3e3417b7ee9", + "bytes": 1299789, + "hashPolicy": "Exact measured raw bytes" + }, + "logRaw": { + "path": ".artifacts/phase7/p7-01/account-final/runtime/account.log", + "sha256": "048cd9c87cefb6c21b98c3bdeb37acbf2dec8395ca5885af9ac0a62d7393efaa", + "bytes": 1651, + "hashPolicy": "Exact measured raw bytes" + }, + "stats": { + "startTime": "2026-10-11T19:04:30.267Z", + "duration": 31208.54, + "expected": 6, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "cases": [ + { + "title": "[P7-01 PRES-E06 E08 E09 C01 C07] nb/dark 320px account identity save reload preserves canonical and sibling CVs", + "file": "phase7-account-security.spec.ts", + "line": 138, + "expectedStatus": "passed", + "project": "chromium", + "resultStatuses": [ + "passed" + ], + "retries": [ + 0 + ], + "errors": [ + [] + ], + "durationMs": 4392 + }, + { + "title": "[P7-01 PRES-E06 E08 E09 C01 C07] en/light 390px account identity save reload preserves canonical and sibling CVs", + "file": "phase7-account-security.spec.ts", + "line": 138, + "expectedStatus": "passed", + "project": "chromium", + "resultStatuses": [ + "passed" + ], + "retries": [ + 0 + ], + "errors": [ + [] + ], + "durationMs": 3866 + }, + { + "title": "[P7-01 PRES-E06 E08 E09 C01 C07] nb/light 768px account identity save reload preserves canonical and sibling CVs", + "file": "phase7-account-security.spec.ts", + "line": 138, + "expectedStatus": "passed", + "project": "chromium", + "resultStatuses": [ + "passed" + ], + "retries": [ + 0 + ], + "errors": [ + [] + ], + "durationMs": 3819 + }, + { + "title": "[P7-01 PRES-E06 E08 E09 C01 C07] en/dark 1440px account identity save reload preserves canonical and sibling CVs", + "file": "phase7-account-security.spec.ts", + "line": 138, + "expectedStatus": "passed", + "project": "chromium", + "resultStatuses": [ + "passed" + ], + "retries": [ + 0 + ], + "errors": [ + [] + ], + "durationMs": 4250 + }, + { + "title": "[P7-01 PRES-E04 E08] failed 2FA status is read-only and deliberate retry restores the actual disabled state", + "file": "phase7-account-security.spec.ts", + "line": 202, + "expectedStatus": "passed", + "project": "chromium", + "resultStatuses": [ + "passed" + ], + "retries": [ + 0 + ], + "errors": [ + [] + ], + "durationMs": 3218 + }, + { + "title": "[P7-01 PRES-E05 E08 E09] native sessions cancel no-write, failed confirmed revoke stays in the dialog, retry revokes others and current logout disposes account", + "file": "phase7-account-security.spec.ts", + "line": 237, + "expectedStatus": "passed", + "project": "chromium", + "resultStatuses": [ + "passed" + ], + "retries": [ + 0 + ], + "errors": [ + [] + ], + "durationMs": 4342 + } + ], + "axeScans": [ + { + "test": "[P7-01 PRES-E06 E08 E09 C01 C07] nb/dark 320px account identity save reload preserves canonical and sibling CVs", + "attachment": "p7-account-nb-dark-320-identity-persisted-audit", + "raw": { + "storage": "Base64 attachment in raw report", + "sha256": "6697c5bb26c1963c30f531b83b8de28f8cd4ddbcef282ba9f63c71e5c0d6cc92", + "bytes": 13201 + }, + "context": { + "state": "identity-persisted", + "sample": { + "language": "nb", + "theme": "dark", + "width": 320, + "height": 844 + }, + "url": "http://localhost:3300/profile", + "ariaSnapshot": "- link \"Hopp til hovedinnhold\":\n - /url: \"#app-main-content\"\n- banner:\n - button \"Åpne navigasjon\"\n - img \"Jobbjakt\"\n - heading \"Jobbjakt\" [level=6]\n - group \"Språk i appen\":\n - button \"English\": EN\n - button \"Norsk\" [pressed]: \"NO\"\n - button \"Bruker\": PA\n - button \"Varsler\": \"0\"\n - button \"Innstillinger\"\n - button \"Hurtigsøk (Ctrl+K)\"\n- main:\n - navigation:\n - list:\n - listitem: Hjem\n - listitem: Konto\n - listitem: Profil\n - heading \"Profil\" [level=1]\n - region \"Profil\":\n - heading \"Profil\" [level=2]\n - text: ÅÅ\n - button \"Bytt bilde\"\n - paragraph: p7-account-1791745477455-akvdwnu5hum@example.test\n - paragraph: Canonical career headline\n - text: Lokal konto Google ikke koblet Ingen lagret CV-tekst\n - region \"Autentisering\":\n - heading \"Autentisering\" [level=2]\n - paragraph: Logget inn som p7-account-1791745477455-akvdwnu5hum@example.test. (Lokal konto)\n - button \"Logg ut\"\n - region \"Google-konto\":\n - heading \"Google-konto\" [level=2]\n - paragraph: Google-innlogging er ikke tilgjengelig i denne installasjonen.\n - region \"Microsoft-konto\":\n - heading \"Microsoft-konto\" [level=2]\n - paragraph: Microsoft-innlogging er ikke tilgjengelig i denne installasjonen.\n - region \"Konto\":\n - heading \"Konto\" [level=2]\n - text: Visningsnavn\n - textbox \"Visningsnavn\": Åse — documented account identity with a deliberately long personal display name\n - text: Brukernavn\n - textbox \"Brukernavn\": p7-account-1791745477455-akvdwnu5hum@example.test\n - text: Fornavn\n - textbox \"Fornavn\": Åse\n - text: Etternavn\n - textbox \"Etternavn\": Account identity\n - text: Ny e-post\n - textbox \"Ny e-post\": p7-account-1791745477455-akvdwnu5hum@example.test\n - paragraph: \"Nåværende e-post: p7-account-1791745477455-akvdwnu5hum@example.test\"\n - text: Profiloverskrift\n - textbox \"Profiloverskrift\": Canonical career headline\n - paragraph: Lagres bare i denne nettleseren for å gjøre arbeidsområdet mer personlig.\n - text: Passord for å bekrefte e-postendring\n - textbox \"Passord for å bekrefte e-postendring\"\n - button \"Send bekreftelse\" [disabled]\n - button \"Avbryt\" [disabled]\n - button \"Lagre endringer\"\n - region \"Bytt passord\":\n - heading \"Bytt passord\" [level=2]\n - text: Nåværende passord\n - textbox \"Nåværende passord\"\n - text: Nytt passord\n - textbox \"Nytt passord\"\n - button \"Oppdater passord\"\n - region \"Tofaktorautentisering\":\n - heading \"Tofaktorautentisering\" [level=2]\n - paragraph: Ikke aktivert\n - button \"Aktiver 2FA\"\n - region \"Økter\":\n - heading \"Økter\" [level=2]\n - list:\n - listitem:\n - text: Chrome on WindowsDenne enheten\n - paragraph: Sist aktiv 11.10.2026, 21:04:37 · Utløper 12.10.2026\n - button \"Logg ut\"\n- alert", + "limits": [ + "No human screen reader", + "No physical touch/software keyboard", + "No real provider or production qualification" + ] + }, + "geometry": { + "rootOverflow": 0, + "width": 320, + "height": 844, + "focus": "BODY", + "regions": [ + { + "name": "Profil", + "left": 16, + "right": 304, + "width": 288 + }, + { + "name": "Autentisering", + "left": 16, + "right": 304, + "width": 288 + }, + { + "name": "Google-konto", + "left": 16, + "right": 304, + "width": 288 + }, + { + "name": "Microsoft-konto", + "left": 16, + "right": 304, + "width": 288 + }, + { + "name": "Konto", + "left": 16, + "right": 304, + "width": 288 + }, + { + "name": "Bytt passord", + "left": 16, + "right": 304, + "width": 288 + }, + { + "name": "Tofaktorautentisering", + "left": 16, + "right": 304, + "width": 288 + }, + { + "name": "Økter", + "left": 16, + "right": 304, + "width": 288 + } + ] + }, + "engine": null, + "environment": null, + "toolOptions": null, + "violations": [], + "incomplete": [ + { + "id": "color-contrast", + "impact": "serious", + "tags": [ + "cat.color", + "wcag2aa", + "wcag143", + "TTv5", + "TT13.c", + "EN-301-549", + "EN-9.1.4.3", + "ACT", + "RGAAv4", + "RGAA-3.2.1" + ], + "description": "Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds", + "help": "Elements must meet minimum color contrast ratio thresholds", + "helpUrl": "https://dequeuniversity.com/rules/axe/4.13/color-contrast?application=playwright", + "nodes": [ + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "9.0pt (12px)", + "fontWeight": "normal", + "messageKey": "shortTextContent", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element content is too short to determine if it is actual text content" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "0", + "target": [ + ".MuiBadge-badge" + ], + "failureSummary": "Fix any of the following:\n Element content is too short to determine if it is actual text content" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_19_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1a_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1b_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1c_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1d_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1e_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1n_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1h_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1i_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + } + ] + } + ], + "violationNodes": 0, + "incompleteNodes": 10 + }, + { + "test": "[P7-01 PRES-E06 E08 E09 C01 C07] en/light 390px account identity save reload preserves canonical and sibling CVs", + "attachment": "p7-account-en-light-390-identity-persisted-audit", + "raw": { + "storage": "Base64 attachment in raw report", + "sha256": "09cbe130ca46f6abeaa9e1929463851a823b697d821738e1c098819c8df2a1e8", + "bytes": 13198 + }, + "context": { + "state": "identity-persisted", + "sample": { + "language": "en", + "theme": "light", + "width": 390, + "height": 844 + }, + "url": "http://localhost:3300/profile", + "ariaSnapshot": "- link \"Skip to main content\":\n - /url: \"#app-main-content\"\n- banner:\n - button \"Open navigation\"\n - img \"Jobbjakt\"\n - heading \"Jobbjakt\" [level=6]\n - group \"Application language\":\n - button \"English\" [pressed]: EN\n - button \"Norsk\": \"NO\"\n - button \"User\": PA\n - button \"Notifications\": \"0\"\n - button \"Settings\"\n - button \"Quick Search (Ctrl+K)\"\n- main:\n - navigation:\n - list:\n - listitem: Home\n - listitem: Account\n - listitem: Profile\n - heading \"Profile\" [level=1]\n - region \"Profile\":\n - heading \"Profile\" [level=2]\n - text: ÅÅ\n - button \"Change image\"\n - paragraph: p7-account-1791745481576-lwk9k1sozsj@example.test\n - paragraph: Canonical career headline\n - text: Local account Google not linked No stored CV text\n - region \"Authentication\":\n - heading \"Authentication\" [level=2]\n - paragraph: Signed in as p7-account-1791745481576-lwk9k1sozsj@example.test. (Local account)\n - button \"Sign out\"\n - region \"Google account\":\n - heading \"Google account\" [level=2]\n - paragraph: Google sign-in is not available in this deployment.\n - region \"Microsoft account\":\n - heading \"Microsoft account\" [level=2]\n - paragraph: Microsoft sign-in is not available in this deployment.\n - region \"Account\":\n - heading \"Account\" [level=2]\n - text: Display name\n - textbox \"Display name\": Åse — documented account identity with a deliberately long personal display name\n - text: Username\n - textbox \"Username\": p7-account-1791745481576-lwk9k1sozsj@example.test\n - text: First name\n - textbox \"First name\": Åse\n - text: Last name\n - textbox \"Last name\": Account identity\n - text: New email\n - textbox \"New email\": p7-account-1791745481576-lwk9k1sozsj@example.test\n - paragraph: \"Current email: p7-account-1791745481576-lwk9k1sozsj@example.test\"\n - text: Profile headline\n - textbox \"Profile headline\": Canonical career headline\n - paragraph: Stored only in this browser to personalize your workspace.\n - text: Password to confirm email change\n - textbox \"Password to confirm email change\"\n - button \"Send confirmation\" [disabled]\n - button \"Cancel\" [disabled]\n - button \"Save changes\"\n - region \"Change password\":\n - heading \"Change password\" [level=2]\n - text: Current password\n - textbox \"Current password\"\n - text: New password\n - textbox \"New password\"\n - button \"Update password\"\n - region \"Two-factor authentication\":\n - heading \"Two-factor authentication\" [level=2]\n - paragraph: Not enabled\n - button \"Enable 2FA\"\n - region \"Sessions\":\n - heading \"Sessions\" [level=2]\n - list:\n - listitem:\n - text: Chrome on WindowsThis device\n - paragraph: Last active 10/11/2026, 9:04:41 PM · Expires 10/12/2026\n - button \"Sign out\"\n- alert", + "limits": [ + "No human screen reader", + "No physical touch/software keyboard", + "No real provider or production qualification" + ] + }, + "geometry": { + "rootOverflow": 0, + "width": 390, + "height": 844, + "focus": "BODY", + "regions": [ + { + "name": "Profile", + "left": 16, + "right": 374, + "width": 358 + }, + { + "name": "Authentication", + "left": 16, + "right": 374, + "width": 358 + }, + { + "name": "Google account", + "left": 16, + "right": 374, + "width": 358 + }, + { + "name": "Microsoft account", + "left": 16, + "right": 374, + "width": 358 + }, + { + "name": "Account", + "left": 16, + "right": 374, + "width": 358 + }, + { + "name": "Change password", + "left": 16, + "right": 374, + "width": 358 + }, + { + "name": "Two-factor authentication", + "left": 16, + "right": 374, + "width": 358 + }, + { + "name": "Sessions", + "left": 16, + "right": 374, + "width": 358 + } + ] + }, + "engine": null, + "environment": null, + "toolOptions": null, + "violations": [], + "incomplete": [ + { + "id": "color-contrast", + "impact": "serious", + "tags": [ + "cat.color", + "wcag2aa", + "wcag143", + "TTv5", + "TT13.c", + "EN-301-549", + "EN-9.1.4.3", + "ACT", + "RGAAv4", + "RGAA-3.2.1" + ], + "description": "Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds", + "help": "Elements must meet minimum color contrast ratio thresholds", + "helpUrl": "https://dequeuniversity.com/rules/axe/4.13/color-contrast?application=playwright", + "nodes": [ + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "9.0pt (12px)", + "fontWeight": "normal", + "messageKey": "shortTextContent", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element content is too short to determine if it is actual text content" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "0", + "target": [ + ".MuiBadge-badge" + ], + "failureSummary": "Fix any of the following:\n Element content is too short to determine if it is actual text content" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_19_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1a_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1b_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1c_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1d_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1e_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1n_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1h_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1i_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + } + ] + } + ], + "violationNodes": 0, + "incompleteNodes": 10 + }, + { + "test": "[P7-01 PRES-E06 E08 E09 C01 C07] nb/light 768px account identity save reload preserves canonical and sibling CVs", + "attachment": "p7-account-nb-light-768-identity-persisted-audit", + "raw": { + "storage": "Base64 attachment in raw report", + "sha256": "681b7a91cc36935f8dae19fb543ce65ac475b719253cfb957f1b0eb9a268c09d", + "bytes": 13202 + }, + "context": { + "state": "identity-persisted", + "sample": { + "language": "nb", + "theme": "light", + "width": 768, + "height": 1024 + }, + "url": "http://localhost:3300/profile", + "ariaSnapshot": "- link \"Hopp til hovedinnhold\":\n - /url: \"#app-main-content\"\n- banner:\n - button \"Åpne navigasjon\"\n - img \"Jobbjakt\"\n - heading \"Jobbjakt\" [level=6]\n - group \"Språk i appen\":\n - button \"English\": EN\n - button \"Norsk\" [pressed]: \"NO\"\n - button \"Bruker\": PA\n - button \"Varsler\": \"0\"\n - button \"Innstillinger\"\n - button \"Hurtigsøk Ctrl+K\"\n- main:\n - navigation:\n - list:\n - listitem: Hjem\n - listitem: Konto\n - listitem: Profil\n - heading \"Profil\" [level=1]\n - region \"Profil\":\n - heading \"Profil\" [level=2]\n - text: ÅÅ\n - button \"Bytt bilde\"\n - paragraph: p7-account-1791745485451-aup10hn380q@example.test\n - paragraph: Canonical career headline\n - text: Lokal konto Google ikke koblet Ingen lagret CV-tekst\n - region \"Autentisering\":\n - heading \"Autentisering\" [level=2]\n - paragraph: Logget inn som p7-account-1791745485451-aup10hn380q@example.test. (Lokal konto)\n - button \"Logg ut\"\n - region \"Google-konto\":\n - heading \"Google-konto\" [level=2]\n - paragraph: Google-innlogging er ikke tilgjengelig i denne installasjonen.\n - region \"Microsoft-konto\":\n - heading \"Microsoft-konto\" [level=2]\n - paragraph: Microsoft-innlogging er ikke tilgjengelig i denne installasjonen.\n - region \"Konto\":\n - heading \"Konto\" [level=2]\n - text: Visningsnavn\n - textbox \"Visningsnavn\": Åse — documented account identity with a deliberately long personal display name\n - text: Brukernavn\n - textbox \"Brukernavn\": p7-account-1791745485451-aup10hn380q@example.test\n - text: Fornavn\n - textbox \"Fornavn\": Åse\n - text: Etternavn\n - textbox \"Etternavn\": Account identity\n - text: Ny e-post\n - textbox \"Ny e-post\": p7-account-1791745485451-aup10hn380q@example.test\n - paragraph: \"Nåværende e-post: p7-account-1791745485451-aup10hn380q@example.test\"\n - text: Profiloverskrift\n - textbox \"Profiloverskrift\": Canonical career headline\n - paragraph: Lagres bare i denne nettleseren for å gjøre arbeidsområdet mer personlig.\n - text: Passord for å bekrefte e-postendring\n - textbox \"Passord for å bekrefte e-postendring\"\n - button \"Send bekreftelse\" [disabled]\n - button \"Avbryt\" [disabled]\n - button \"Lagre endringer\"\n - region \"Bytt passord\":\n - heading \"Bytt passord\" [level=2]\n - text: Nåværende passord\n - textbox \"Nåværende passord\"\n - text: Nytt passord\n - textbox \"Nytt passord\"\n - button \"Oppdater passord\"\n - region \"Tofaktorautentisering\":\n - heading \"Tofaktorautentisering\" [level=2]\n - paragraph: Ikke aktivert\n - button \"Aktiver 2FA\"\n - region \"Økter\":\n - heading \"Økter\" [level=2]\n - list:\n - listitem:\n - text: Chrome on WindowsDenne enheten\n - paragraph: Sist aktiv 11.10.2026, 21:04:45 · Utløper 12.10.2026\n - button \"Logg ut\"\n- alert", + "limits": [ + "No human screen reader", + "No physical touch/software keyboard", + "No real provider or production qualification" + ] + }, + "geometry": { + "rootOverflow": 0, + "width": 768, + "height": 1024, + "focus": "BODY", + "regions": [ + { + "name": "Profil", + "left": 24, + "right": 744, + "width": 720 + }, + { + "name": "Autentisering", + "left": 24, + "right": 744, + "width": 720 + }, + { + "name": "Google-konto", + "left": 24, + "right": 744, + "width": 720 + }, + { + "name": "Microsoft-konto", + "left": 24, + "right": 744, + "width": 720 + }, + { + "name": "Konto", + "left": 24, + "right": 744, + "width": 720 + }, + { + "name": "Bytt passord", + "left": 24, + "right": 744, + "width": 720 + }, + { + "name": "Tofaktorautentisering", + "left": 24, + "right": 744, + "width": 720 + }, + { + "name": "Økter", + "left": 24, + "right": 744, + "width": 720 + } + ] + }, + "engine": null, + "environment": null, + "toolOptions": null, + "violations": [], + "incomplete": [ + { + "id": "color-contrast", + "impact": "serious", + "tags": [ + "cat.color", + "wcag2aa", + "wcag143", + "TTv5", + "TT13.c", + "EN-301-549", + "EN-9.1.4.3", + "ACT", + "RGAAv4", + "RGAA-3.2.1" + ], + "description": "Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds", + "help": "Elements must meet minimum color contrast ratio thresholds", + "helpUrl": "https://dequeuniversity.com/rules/axe/4.13/color-contrast?application=playwright", + "nodes": [ + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "9.0pt (12px)", + "fontWeight": "normal", + "messageKey": "shortTextContent", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element content is too short to determine if it is actual text content" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "0", + "target": [ + ".MuiBadge-badge" + ], + "failureSummary": "Fix any of the following:\n Element content is too short to determine if it is actual text content" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_19_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1a_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1b_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1c_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1d_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1e_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1n_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1h_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1i_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + } + ] + } + ], + "violationNodes": 0, + "incompleteNodes": 10 + }, + { + "test": "[P7-01 PRES-E06 E08 E09 C01 C07] en/dark 1440px account identity save reload preserves canonical and sibling CVs", + "attachment": "p7-account-en-dark-1440-identity-persisted-audit", + "raw": { + "storage": "Base64 attachment in raw report", + "sha256": "67a739699f29e488ff975b7fb3e6922e5100fe1e08acd5c672fb4476e9c88dca", + "bytes": 14753 + }, + "context": { + "state": "identity-persisted", + "sample": { + "language": "en", + "theme": "dark", + "width": 1440, + "height": 900 + }, + "url": "http://localhost:3300/profile", + "ariaSnapshot": "- link \"Skip to main content\":\n - /url: \"#app-main-content\"\n- banner:\n - button \"Collapse sidebar\"\n - group \"Application language\":\n - button \"English\" [pressed]: EN\n - button \"Norsk\": \"NO\"\n - button \"Notifications\": \"0\"\n - button \"Settings\"\n - button \"Quick Search Ctrl+K\"\n - button \"User\": PA\n - paragraph: p7-account-1791745489279-hzgp5c9wa2p@example.test\n - text: User\n- navigation \"Navigation\":\n - img \"Jobbjakt\"\n - heading \"Jobbjakt\" [level=6]\n - text: Track your hunt\n - separator\n - list \"Navigation\":\n - listitem:\n - button \"Dashboard\"\n - text: Job search\n - list \"Job search\":\n - listitem:\n - button \"Job Applications\"\n - list \"Job Applications\":\n - listitem:\n - button \"Discover jobs\":\n - paragraph: Discover jobs\n - listitem:\n - button \"Reminders\":\n - paragraph: Reminders\n - listitem:\n - button \"Companies\":\n - paragraph: Companies\n - listitem:\n - button \"Compare offers\":\n - paragraph: Compare offers\n - listitem:\n - button \"Trash\":\n - paragraph: Trash\n - listitem:\n - button \"Job email\"\n - text: Career\n - list \"Career\":\n - listitem:\n - button \"Career profile\"\n - listitem:\n - button \"CV Builder\"\n - text: Insights\n - list \"Insights\":\n - listitem:\n - button \"Analytics\"\n - separator\n - text: Account\n - list \"Account\":\n - listitem:\n - button \"Settings\"\n - list \"Settings\":\n - listitem:\n - button \"Profile\":\n - paragraph: Profile\n - listitem:\n - button \"Connected accounts\":\n - paragraph: Connected accounts\n- main:\n - navigation:\n - list:\n - listitem: Home\n - listitem: Account\n - listitem: Profile\n - heading \"Profile\" [level=1]\n - region \"Profile\":\n - heading \"Profile\" [level=2]\n - text: ÅÅ\n - button \"Change image\"\n - paragraph: p7-account-1791745489279-hzgp5c9wa2p@example.test\n - paragraph: Canonical career headline\n - text: Local account Google not linked No stored CV text\n - region \"Authentication\":\n - heading \"Authentication\" [level=2]\n - paragraph: Signed in as p7-account-1791745489279-hzgp5c9wa2p@example.test. (Local account)\n - button \"Sign out\"\n - region \"Google account\":\n - heading \"Google account\" [level=2]\n - paragraph: Google sign-in is not available in this deployment.\n - region \"Microsoft account\":\n - heading \"Microsoft account\" [level=2]\n - paragraph: Microsoft sign-in is not available in this deployment.\n - region \"Account\":\n - heading \"Account\" [level=2]\n - text: Display name\n - textbox \"Display name\": Åse — documented account identity with a deliberately long personal display name\n - text: Username\n - textbox \"Username\": p7-account-1791745489279-hzgp5c9wa2p@example.test\n - text: First name\n - textbox \"First name\": Åse\n - text: Last name\n - textbox \"Last name\": Account identity\n - text: New email\n - textbox \"New email\": p7-account-1791745489279-hzgp5c9wa2p@example.test\n - paragraph: \"Current email: p7-account-1791745489279-hzgp5c9wa2p@example.test\"\n - text: Profile headline\n - textbox \"Profile headline\": Canonical career headline\n - paragraph: Stored only in this browser to personalize your workspace.\n - text: Password to confirm email change\n - textbox \"Password to confirm email change\"\n - button \"Send confirmation\" [disabled]\n - button \"Cancel\" [disabled]\n - button \"Save changes\"\n - region \"Change password\":\n - heading \"Change password\" [level=2]\n - text: Current password\n - textbox \"Current password\"\n - text: New password\n - textbox \"New password\"\n - button \"Update password\"\n - region \"Two-factor authentication\":\n - heading \"Two-factor authentication\" [level=2]\n - paragraph: Not enabled\n - button \"Enable 2FA\"\n - region \"Sessions\":\n - heading \"Sessions\" [level=2]\n - list:\n - listitem:\n - text: Chrome on WindowsThis device\n - paragraph: Last active 10/11/2026, 9:04:49 PM · Expires 10/12/2026\n - button \"Sign out\"\n- alert", + "limits": [ + "No human screen reader", + "No physical touch/software keyboard", + "No real provider or production qualification" + ] + }, + "geometry": { + "rootOverflow": 0, + "width": 1440, + "height": 900, + "focus": "BODY", + "regions": [ + { + "name": "Profile", + "left": 286, + "right": 1408, + "width": 1122 + }, + { + "name": "Authentication", + "left": 286, + "right": 1408, + "width": 1122 + }, + { + "name": "Google account", + "left": 286, + "right": 1408, + "width": 1122 + }, + { + "name": "Microsoft account", + "left": 286, + "right": 1408, + "width": 1122 + }, + { + "name": "Account", + "left": 286, + "right": 1408, + "width": 1122 + }, + { + "name": "Change password", + "left": 286, + "right": 1408, + "width": 1122 + }, + { + "name": "Two-factor authentication", + "left": 286, + "right": 1408, + "width": 1122 + }, + { + "name": "Sessions", + "left": 286, + "right": 1408, + "width": 1122 + } + ] + }, + "engine": null, + "environment": null, + "toolOptions": null, + "violations": [], + "incomplete": [ + { + "id": "color-contrast", + "impact": "serious", + "tags": [ + "cat.color", + "wcag2aa", + "wcag143", + "TTv5", + "TT13.c", + "EN-301-549", + "EN-9.1.4.3", + "ACT", + "RGAAv4", + "RGAA-3.2.1" + ], + "description": "Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds", + "help": "Elements must meet minimum color contrast ratio thresholds", + "helpUrl": "https://dequeuniversity.com/rules/axe/4.13/color-contrast?application=playwright", + "nodes": [ + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "9.0pt (12px)", + "fontWeight": "normal", + "messageKey": "shortTextContent", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element content is too short to determine if it is actual text content" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "0", + "target": [ + ".MuiBadge-badge" + ], + "failureSummary": "Fix any of the following:\n Element content is too short to determine if it is actual text content" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_18_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_19_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1a_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1b_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1c_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1d_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1m_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1g_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1h_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + } + ] + } + ], + "violationNodes": 0, + "incompleteNodes": 10 + }, + { + "test": "[P7-01 PRES-E04 E08] failed 2FA status is read-only and deliberate retry restores the actual disabled state", + "attachment": "p7-account-nb-dark-320-status-failed-read-only-audit", + "raw": { + "storage": "Base64 attachment in raw report", + "sha256": "3adf6f2fdbd2adaed111fb0e26cf7fb8d5d873a87b8e06be86a413fb93f389d0", + "bytes": 13013 + }, + "context": { + "state": "status-failed-read-only", + "sample": { + "language": "nb", + "theme": "dark", + "width": 320, + "height": 844 + }, + "url": "http://localhost:3300/profile", + "ariaSnapshot": "- link \"Hopp til hovedinnhold\":\n - /url: \"#app-main-content\"\n- banner:\n - button \"Åpne navigasjon\"\n - img \"Jobbjakt\"\n - heading \"Jobbjakt\" [level=6]\n - group \"Språk i appen\":\n - button \"English\": EN\n - button \"Norsk\" [pressed]: \"NO\"\n - button \"Bruker\": PA\n - button \"Varsler\": \"0\"\n - button \"Innstillinger\"\n - button \"Hurtigsøk (Ctrl+K)\"\n- main:\n - navigation:\n - list:\n - listitem: Hjem\n - listitem: Konto\n - listitem: Profil\n - heading \"Profil\" [level=1]\n - region \"Profil\":\n - heading \"Profil\" [level=2]\n - text: PP\n - button \"Bytt bilde\"\n - paragraph: p7-account-1791745493576-2xa24b0co4m@example.test\n - paragraph: Legg til en kort overskrift for å gjøre kontovisningen mer personlig.\n - text: Lokal konto Google ikke koblet Ingen lagret CV-tekst\n - region \"Autentisering\":\n - heading \"Autentisering\" [level=2]\n - paragraph: Logget inn som p7-account-1791745493576-2xa24b0co4m@example.test. (Lokal konto)\n - button \"Logg ut\"\n - region \"Google-konto\":\n - heading \"Google-konto\" [level=2]\n - paragraph: Google-innlogging er ikke tilgjengelig i denne installasjonen.\n - region \"Microsoft-konto\":\n - heading \"Microsoft-konto\" [level=2]\n - paragraph: Microsoft-innlogging er ikke tilgjengelig i denne installasjonen.\n - region \"Konto\":\n - heading \"Konto\" [level=2]\n - text: Visningsnavn\n - textbox \"Visningsnavn\"\n - text: Brukernavn\n - textbox \"Brukernavn\": p7-account-1791745493576-2xa24b0co4m@example.test\n - text: Fornavn\n - textbox \"Fornavn\"\n - text: Etternavn\n - textbox \"Etternavn\"\n - text: Ny e-post\n - textbox \"Ny e-post\": p7-account-1791745493576-2xa24b0co4m@example.test\n - paragraph: \"Nåværende e-post: p7-account-1791745493576-2xa24b0co4m@example.test\"\n - text: Profiloverskrift\n - textbox \"Profiloverskrift\"\n - paragraph: Lagres bare i denne nettleseren for å gjøre arbeidsområdet mer personlig.\n - text: Passord for å bekrefte e-postendring\n - textbox \"Passord for å bekrefte e-postendring\"\n - button \"Send bekreftelse\" [disabled]\n - button \"Avbryt\" [disabled]\n - button \"Lagre endringer\"\n - region \"Bytt passord\":\n - heading \"Bytt passord\" [level=2]\n - text: Nåværende passord\n - textbox \"Nåværende passord\"\n - text: Nytt passord\n - textbox \"Nytt passord\"\n - button \"Oppdater passord\"\n - region \"Tofaktorautentisering\":\n - heading \"Tofaktorautentisering\" [level=2]\n - alert:\n - text: Noe gikk galt. Prøv igjen.\n - button \"Prøv igjen\"\n - region \"Økter\":\n - heading \"Økter\" [level=2]\n - list:\n - listitem:\n - text: Chrome on WindowsDenne enheten\n - paragraph: Sist aktiv 11.10.2026, 21:04:53 · Utløper 12.10.2026\n - button \"Logg ut\"\n- alert", + "limits": [ + "No human screen reader", + "No physical touch/software keyboard", + "No real provider or production qualification" + ] + }, + "geometry": { + "rootOverflow": 0, + "width": 320, + "height": 844, + "focus": "BODY", + "regions": [ + { + "name": "Profil", + "left": 16, + "right": 304, + "width": 288 + }, + { + "name": "Autentisering", + "left": 16, + "right": 304, + "width": 288 + }, + { + "name": "Google-konto", + "left": 16, + "right": 304, + "width": 288 + }, + { + "name": "Microsoft-konto", + "left": 16, + "right": 304, + "width": 288 + }, + { + "name": "Konto", + "left": 16, + "right": 304, + "width": 288 + }, + { + "name": "Bytt passord", + "left": 16, + "right": 304, + "width": 288 + }, + { + "name": "Tofaktorautentisering", + "left": 16, + "right": 304, + "width": 288 + }, + { + "name": "Økter", + "left": 16, + "right": 304, + "width": 288 + } + ] + }, + "engine": null, + "environment": null, + "toolOptions": null, + "violations": [], + "incomplete": [ + { + "id": "color-contrast", + "impact": "serious", + "tags": [ + "cat.color", + "wcag2aa", + "wcag143", + "TTv5", + "TT13.c", + "EN-301-549", + "EN-9.1.4.3", + "ACT", + "RGAAv4", + "RGAA-3.2.1" + ], + "description": "Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds", + "help": "Elements must meet minimum color contrast ratio thresholds", + "helpUrl": "https://dequeuniversity.com/rules/axe/4.13/color-contrast?application=playwright", + "nodes": [ + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "9.0pt (12px)", + "fontWeight": "normal", + "messageKey": "shortTextContent", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element content is too short to determine if it is actual text content" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "0", + "target": [ + ".MuiBadge-badge" + ], + "failureSummary": "Fix any of the following:\n Element content is too short to determine if it is actual text content" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_19_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1a_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1b_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1c_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1d_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1e_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1n_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1h_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1i_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + } + ] + } + ], + "violationNodes": 0, + "incompleteNodes": 10 + }, + { + "test": "[P7-01 PRES-E04 E08] failed 2FA status is read-only and deliberate retry restores the actual disabled state", + "attachment": "p7-account-nb-dark-320-status-read-recovered-audit", + "raw": { + "storage": "Base64 attachment in raw report", + "sha256": "647f1bf345969a1b5c6139a77b6f488f7c53cbdfa97fedd6a903ac0913552dba", + "bytes": 13084 + }, + "context": { + "state": "status-read-recovered", + "sample": { + "language": "nb", + "theme": "dark", + "width": 320, + "height": 844 + }, + "url": "http://localhost:3300/profile", + "ariaSnapshot": "- link \"Hopp til hovedinnhold\":\n - /url: \"#app-main-content\"\n- banner:\n - button \"Åpne navigasjon\"\n - img \"Jobbjakt\"\n - heading \"Jobbjakt\" [level=6]\n - group \"Språk i appen\":\n - button \"English\": EN\n - button \"Norsk\" [pressed]: \"NO\"\n - button \"Bruker\": PA\n - button \"Varsler\": \"0\"\n - button \"Innstillinger\"\n - button \"Hurtigsøk (Ctrl+K)\"\n- main:\n - navigation:\n - list:\n - listitem: Hjem\n - listitem: Konto\n - listitem: Profil\n - heading \"Profil\" [level=1]\n - region \"Profil\":\n - heading \"Profil\" [level=2]\n - text: PP\n - button \"Bytt bilde\"\n - paragraph: p7-account-1791745493576-2xa24b0co4m@example.test\n - paragraph: Legg til en kort overskrift for å gjøre kontovisningen mer personlig.\n - text: Lokal konto Google ikke koblet Ingen lagret CV-tekst\n - region \"Autentisering\":\n - heading \"Autentisering\" [level=2]\n - paragraph: Logget inn som p7-account-1791745493576-2xa24b0co4m@example.test. (Lokal konto)\n - button \"Logg ut\"\n - region \"Google-konto\":\n - heading \"Google-konto\" [level=2]\n - paragraph: Google-innlogging er ikke tilgjengelig i denne installasjonen.\n - region \"Microsoft-konto\":\n - heading \"Microsoft-konto\" [level=2]\n - paragraph: Microsoft-innlogging er ikke tilgjengelig i denne installasjonen.\n - region \"Konto\":\n - heading \"Konto\" [level=2]\n - text: Visningsnavn\n - textbox \"Visningsnavn\"\n - text: Brukernavn\n - textbox \"Brukernavn\": p7-account-1791745493576-2xa24b0co4m@example.test\n - text: Fornavn\n - textbox \"Fornavn\"\n - text: Etternavn\n - textbox \"Etternavn\"\n - text: Ny e-post\n - textbox \"Ny e-post\": p7-account-1791745493576-2xa24b0co4m@example.test\n - paragraph: \"Nåværende e-post: p7-account-1791745493576-2xa24b0co4m@example.test\"\n - text: Profiloverskrift\n - textbox \"Profiloverskrift\"\n - paragraph: Lagres bare i denne nettleseren for å gjøre arbeidsområdet mer personlig.\n - text: Passord for å bekrefte e-postendring\n - textbox \"Passord for å bekrefte e-postendring\"\n - button \"Send bekreftelse\" [disabled]\n - button \"Avbryt\" [disabled]\n - button \"Lagre endringer\"\n - region \"Bytt passord\":\n - heading \"Bytt passord\" [level=2]\n - text: Nåværende passord\n - textbox \"Nåværende passord\"\n - text: Nytt passord\n - textbox \"Nytt passord\"\n - button \"Oppdater passord\"\n - region \"Tofaktorautentisering\":\n - heading \"Tofaktorautentisering\" [level=2]\n - paragraph: Ikke aktivert\n - button \"Aktiver 2FA\"\n - region \"Økter\":\n - heading \"Økter\" [level=2]\n - list:\n - listitem:\n - text: Chrome on WindowsDenne enheten\n - paragraph: Sist aktiv 11.10.2026, 21:04:53 · Utløper 12.10.2026\n - button \"Logg ut\"\n- alert", + "limits": [ + "No human screen reader", + "No physical touch/software keyboard", + "No real provider or production qualification" + ] + }, + "geometry": { + "rootOverflow": 0, + "width": 320, + "height": 844, + "focus": "BODY", + "regions": [ + { + "name": "Profil", + "left": 16, + "right": 304, + "width": 288 + }, + { + "name": "Autentisering", + "left": 16, + "right": 304, + "width": 288 + }, + { + "name": "Google-konto", + "left": 16, + "right": 304, + "width": 288 + }, + { + "name": "Microsoft-konto", + "left": 16, + "right": 304, + "width": 288 + }, + { + "name": "Konto", + "left": 16, + "right": 304, + "width": 288 + }, + { + "name": "Bytt passord", + "left": 16, + "right": 304, + "width": 288 + }, + { + "name": "Tofaktorautentisering", + "left": 16, + "right": 304, + "width": 288 + }, + { + "name": "Økter", + "left": 16, + "right": 304, + "width": 288 + } + ] + }, + "engine": null, + "environment": null, + "toolOptions": null, + "violations": [], + "incomplete": [ + { + "id": "color-contrast", + "impact": "serious", + "tags": [ + "cat.color", + "wcag2aa", + "wcag143", + "TTv5", + "TT13.c", + "EN-301-549", + "EN-9.1.4.3", + "ACT", + "RGAAv4", + "RGAA-3.2.1" + ], + "description": "Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds", + "help": "Elements must meet minimum color contrast ratio thresholds", + "helpUrl": "https://dequeuniversity.com/rules/axe/4.13/color-contrast?application=playwright", + "nodes": [ + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "9.0pt (12px)", + "fontWeight": "normal", + "messageKey": "shortTextContent", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element content is too short to determine if it is actual text content" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "0", + "target": [ + ".MuiBadge-badge" + ], + "failureSummary": "Fix any of the following:\n Element content is too short to determine if it is actual text content" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_19_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1a_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1b_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1c_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1d_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1e_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1n_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1h_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + }, + { + "any": [ + { + "id": "color-contrast", + "data": { + "contrastRatio": 0, + "fontSize": "10.5pt (14px)", + "fontWeight": "normal", + "messageKey": "bgOverlap", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [], + "impact": "serious", + "message": "Element's background color could not be determined because it is overlapped by another element" + } + ], + "all": [], + "none": [], + "impact": "serious", + "html": "", + "target": [ + "#_r_1i_" + ], + "failureSummary": "Fix any of the following:\n Element's background color could not be determined because it is overlapped by another element" + } + ] + } + ], + "violationNodes": 0, + "incompleteNodes": 10 + }, + { + "test": "[P7-01 PRES-E05 E08 E09] native sessions cancel no-write, failed confirmed revoke stays in the dialog, retry revokes others and current logout disposes account", + "attachment": "p7-account-en-light-390-revoke-others-failed-modal-audit", + "raw": { + "storage": "Base64 attachment in raw report", + "sha256": "9bdf88d854b35ec17f30709f4c23412e4589b6450015252335cab5beba335dca", + "bytes": 18858 + }, + "context": { + "state": "revoke-others-failed-modal", + "sample": { + "language": "en", + "theme": "light", + "width": 390, + "height": 844 + }, + "url": "http://localhost:3300/profile", + "ariaSnapshot": "- dialog \"Sign out all other devices?\":\n - heading \"Sign out all other devices?\" [level=2]\n - alert: Something went wrong. Please try again.\n - paragraph: Every other session for your account will be signed out immediately. This device stays signed in.\n - button \"Cancel\"\n - button \"Sign out all other devices\"", + "limits": [ + "No human screen reader", + "No physical touch/software keyboard", + "No real provider or production qualification" + ] + }, + "geometry": { + "rootOverflow": 0, + "width": 390, + "height": 844, + "focus": "BUTTON", + "regions": [ + { + "name": "Profile", + "left": 16, + "right": 374, + "width": 358 + }, + { + "name": "Authentication", + "left": 16, + "right": 374, + "width": 358 + }, + { + "name": "Google account", + "left": 16, + "right": 374, + "width": 358 + }, + { + "name": "Microsoft account", + "left": 16, + "right": 374, + "width": 358 + }, + { + "name": "Account", + "left": 16, + "right": 374, + "width": 358 + }, + { + "name": "Change password", + "left": 16, + "right": 374, + "width": 358 + }, + { + "name": "Two-factor authentication", + "left": 16, + "right": 374, + "width": 358 + }, + { + "name": "Sessions", + "left": 16, + "right": 374, + "width": 358 + } + ] + }, + "engine": null, + "environment": null, + "toolOptions": null, + "violations": [], + "incomplete": [ + { + "id": "aria-hidden-focus", + "impact": "serious", + "tags": [ + "cat.name-role-value", + "wcag2a", + "wcag412", + "TTv5", + "TT6.a", + "EN-301-549", + "EN-9.4.1.2", + "RGAAv4", + "RGAA-10.8.1" + ], + "description": "Ensure aria-hidden elements are not focusable nor contain focusable elements", + "help": "ARIA hidden element must not be focusable or contain focusable elements", + "helpUrl": "https://dequeuniversity.com/rules/axe/4.13/aria-hidden-focus?application=playwright", + "nodes": [ + { + "any": [], + "all": [ + { + "id": "focusable-modal-open", + "data": null, + "relatedNodes": [ + { + "html": "Skip to main content", + "target": [ + "a" + ] + }, + { + "html": "", + "target": [ + ".css-1u962su" + ] + }, + { + "html": "", + "target": [ + ".css-1u962su" + ] + }, + { + "html": "", + "target": [ + ".css-1u962su" + ] + }, + { + "html": "}>{error} + ) : !me ? ( {t("authStatusNotSignedIn")} ) : ( - + - Signed in{label ? ` as ${label}` : ""}{me?.provider ? ` (${me.provider})` : ""}. + {label ? t("signedInAs", { name: label }) : t("signedIn")} + {me.provider ? ` (${me.provider === "local" ? t("profileLocalAccount") : me.provider === "google" ? t("profileGoogleSession") : t("profileExternalSession")})` : ""} {me?.roles && me.roles.length > 0 ? ( @@ -82,6 +89,6 @@ export default function AuthStatusCard() { )} - + ); } diff --git a/job-tracker-ui/src/components/GoogleAuthCard.tsx b/job-tracker-ui/src/components/GoogleAuthCard.tsx index 8a06596..9eae4f2 100644 --- a/job-tracker-ui/src/components/GoogleAuthCard.tsx +++ b/job-tracker-ui/src/components/GoogleAuthCard.tsx @@ -1,12 +1,13 @@ import React, { useEffect, useRef, useState } from "react"; -import { Box, Button, Chip, Paper, Typography } from "@mui/material"; +import { Box, Button, Chip, Typography } from "@mui/material"; import { api, getApiErrorMessage } from "../api"; import { clearAuthClientState, getAuthPersistencePreference } from "../auth"; import TwoFactorChallenge from "./TwoFactorChallenge"; import { useToast } from "../toast"; import { useI18n } from "../i18n/I18nProvider"; +import { PageSection } from "../layout/PageLayout"; declare global { interface Window { @@ -77,7 +78,7 @@ function loadGoogleScript(): Promise { export default function GoogleAuthCard({ onSignedIn, presentation = "account" }: { onSignedIn?: () => void; presentation?: "account" | "sign-in" }) { const { toast } = useToast(); - const { t } = useI18n(); + const { t, language } = useI18n(); const [me, setMe] = useState(null); const [working, setWorking] = useState(false); const [allowRegistration, setAllowRegistration] = useState(false); @@ -197,11 +198,7 @@ export default function GoogleAuthCard({ onSignedIn, presentation = "account" }: } return ( - - - {t("googleAccountTitle")} - - + {!clientId && ( {t("googleSetupHint")} @@ -225,7 +222,7 @@ export default function GoogleAuthCard({ onSignedIn, presentation = "account" }: - {me?.googleLink?.linkedAt ? : null} + {me?.googleLink?.linkedAt ? : null} {!signedIn ? ( @@ -295,6 +292,6 @@ export default function GoogleAuthCard({ onSignedIn, presentation = "account" }: ) : null} )} - + ); } diff --git a/job-tracker-ui/src/components/MicrosoftAuthCard.tsx b/job-tracker-ui/src/components/MicrosoftAuthCard.tsx index 4428e78..5917a82 100644 --- a/job-tracker-ui/src/components/MicrosoftAuthCard.tsx +++ b/job-tracker-ui/src/components/MicrosoftAuthCard.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; -import { Box, Button, Chip, Paper, TextField, Typography } from "@mui/material"; +import { Box, Button, Chip, TextField, Typography } from "@mui/material"; import { PublicClientApplication } from "@azure/msal-browser"; import { api, getApiErrorMessage } from "../api"; @@ -8,6 +8,7 @@ import { clearAuthClientState, getAuthPersistencePreference } from "../auth"; import TwoFactorChallenge from "./TwoFactorChallenge"; import { useToast } from "../toast"; import { useI18n } from "../i18n/I18nProvider"; +import { PageSection } from "../layout/PageLayout"; type MeResponse = { provider?: "local" | "google" | "microsoft" | "external"; @@ -34,7 +35,7 @@ export function getMicrosoftMsalInstance(clientId: string): PublicClientApplicat export default function MicrosoftAuthCard({ onSignedIn, presentation = "account" }: { onSignedIn?: () => void; presentation?: "account" | "sign-in" }) { const { toast } = useToast(); - const { t } = useI18n(); + const { t, language } = useI18n(); const [me, setMe] = useState(null); const [working, setWorking] = useState(false); const [pendingToken, setPendingToken] = useState(null); @@ -128,11 +129,7 @@ export default function MicrosoftAuthCard({ onSignedIn, presentation = "account" } return ( - - - {t("microsoftAccountTitle")} - - + {!clientId && ( {t("microsoftSetupHint")} @@ -156,7 +153,7 @@ export default function MicrosoftAuthCard({ onSignedIn, presentation = "account" - {me?.microsoftLink?.linkedAt ? : null} + {me?.microsoftLink?.linkedAt ? : null} {!signedIn ? ( @@ -241,6 +238,6 @@ export default function MicrosoftAuthCard({ onSignedIn, presentation = "account" ) : null} )} - + ); } diff --git a/job-tracker-ui/src/components/SessionsSettingsCard.tsx b/job-tracker-ui/src/components/SessionsSettingsCard.tsx index 836cdf1..7a44e98 100644 --- a/job-tracker-ui/src/components/SessionsSettingsCard.tsx +++ b/job-tracker-ui/src/components/SessionsSettingsCard.tsx @@ -12,7 +12,6 @@ import { ListItem, ListItemSecondaryAction, ListItemText, - Paper, Typography, } from "@mui/material"; import DeleteIcon from "@mui/icons-material/Delete"; @@ -21,6 +20,7 @@ import { api, getApiErrorMessage } from "../api"; import { useToast } from "../toast"; import { useI18n } from "../i18n/I18nProvider"; import { clearAuthClientState } from "../auth"; +import { PageSection } from "../layout/PageLayout"; type Session = { id: string; @@ -37,11 +37,14 @@ function apiErrorMessage(e: any, t: (k: any) => string) { export default function SessionsSettingsCard() { const { toast } = useToast(); - const { t } = useI18n(); + const { t, language } = useI18n(); + const locale = language === "nb" ? "nb-NO" : "en"; const [sessions, setSessions] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [confirmRevokeOthers, setConfirmRevokeOthers] = useState(false); + const [confirmError, setConfirmError] = useState(null); + const [revokingOthers, setRevokingOthers] = useState(false); const loadSessions = useCallback(() => { setLoading(true); @@ -73,23 +76,29 @@ export default function SessionsSettingsCard() { } async function revokeOthers() { + setRevokingOthers(true); + setConfirmError(null); try { await api.post("/auth/sessions/revoke-others"); toast(t("sessionsRevokedOthers"), "success"); setConfirmRevokeOthers(false); loadSessions(); } catch (e: any) { - setError(apiErrorMessage(e, t)); + setConfirmError(apiErrorMessage(e, t)); + } finally { + setRevokingOthers(false); } } - return ( - - - {t("sessionsSectionTitle")} - + function closeConfirmation() { + setConfirmRevokeOthers(false); + setConfirmError(null); + } - {error ? {error} : null} + return ( + + {loading ? {t("loading")} : null} + {error ? {t("retry")}}>{error} : null} {!loading && sessions.length === 0 && !error ? ( {t("sessionsEmpty")} ) : null} @@ -99,6 +108,7 @@ export default function SessionsSettingsCard() { {sessions.map((s) => ( {s.deviceLabel || t("sessionsUnknownDevice")} @@ -110,8 +120,8 @@ export default function SessionsSettingsCard() { } secondary={t("sessionsMeta", { - lastSeen: new Date(s.lastSeenAtUtc).toLocaleString(), - expires: new Date(s.expiresAtUtc).toLocaleDateString(), + lastSeen: new Date(s.lastSeenAtUtc).toLocaleString(locale), + expires: new Date(s.expiresAtUtc).toLocaleDateString(locale), })} /> @@ -125,23 +135,24 @@ export default function SessionsSettingsCard() { ) : null} {sessions.length > 1 ? ( - ) : null} - setConfirmRevokeOthers(false)} maxWidth="sm" fullWidth> + {t("sessionsRevokeOthersConfirmTitle")} + {confirmError ? {confirmError} : null} {t("sessionsRevokeOthersConfirmBody")} - - + - + ); } diff --git a/job-tracker-ui/src/components/TwoFactorSettingsCard.tsx b/job-tracker-ui/src/components/TwoFactorSettingsCard.tsx index ef1496c..ccb3259 100644 --- a/job-tracker-ui/src/components/TwoFactorSettingsCard.tsx +++ b/job-tracker-ui/src/components/TwoFactorSettingsCard.tsx @@ -16,7 +16,6 @@ import { ListItem, ListItemSecondaryAction, ListItemText, - Paper, TextField, Typography, } from "@mui/material"; @@ -25,6 +24,7 @@ import DeleteIcon from "@mui/icons-material/Delete"; import { api, getApiErrorMessage } from "../api"; import { useToast } from "../toast"; import { useI18n } from "../i18n/I18nProvider"; +import { PageSection } from "../layout/PageLayout"; type Status = { enabled: boolean; enabledAtUtc: string | null }; type SetupResponse = { manualEntryKey: string; qrCodeDataUrl: string }; @@ -55,8 +55,11 @@ function apiErrorMessage(e: any, t: (k: any) => string) { export default function TwoFactorSettingsCard() { const { toast } = useToast(); - const { t } = useI18n(); + const { t, language } = useI18n(); + const locale = language === "nb" ? "nb-NO" : "en"; const [status, setStatus] = useState(null); + const [statusLoading, setStatusLoading] = useState(true); + const [statusError, setStatusError] = useState(null); const [flow, setFlow] = useState("closed"); const [password, setPassword] = useState(""); const [code, setCode] = useState(""); @@ -70,8 +73,13 @@ export default function TwoFactorSettingsCard() { const [devicesError, setDevicesError] = useState(null); const loadStatus = useCallback(() => { - api.get("/auth/2fa/status").then((r) => setStatus(r.data)).catch(() => setStatus(null)); - }, []); + setStatusLoading(true); + setStatusError(null); + api.get("/auth/2fa/status") + .then((r) => setStatus(r.data)) + .catch((e) => { setStatus(null); setStatusError(apiErrorMessage(e, t)); }) + .finally(() => setStatusLoading(false)); + }, [t]); const loadDevices = useCallback(() => { setDevicesLoading(true); @@ -184,21 +192,19 @@ export default function TwoFactorSettingsCard() { const isRecoveryStep = flow === "enable-recovery" || flow === "regenerate-recovery"; return ( - - - {t("twoFactorSectionTitle")} - - - {status ? ( + + {statusLoading ? {t("loading")} : null} + {statusError ? {t("retry")}}>{statusError} : null} + {status && !statusLoading ? ( {status.enabled - ? t("twoFactorStatusEnabled", { date: status.enabledAtUtc ? new Date(status.enabledAtUtc).toLocaleDateString() : "" }) + ? t("twoFactorStatusEnabled", { date: status.enabledAtUtc ? new Date(status.enabledAtUtc).toLocaleDateString(locale) : "" }) : t("twoFactorStatusDisabled")} ) : null} - - {!status?.enabled ? ( + {status && !statusLoading ? + {!status.enabled ? ( @@ -212,15 +218,16 @@ export default function TwoFactorSettingsCard() { )} - + : null} {status?.enabled ? ( - + {t("twoFactorTrustedDevicesTitle")} - {devicesError ? {devicesError} : null} + {devicesLoading ? {t("loading")} : null} + {devicesError ? {t("retry")}}>{devicesError} : null} {!devicesLoading && devices.length === 0 && !devicesError ? ( {t("twoFactorTrustedDevicesEmpty")} ) : null} @@ -229,6 +236,7 @@ export default function TwoFactorSettingsCard() { {devices.map((d) => ( {d.deviceLabel || t("twoFactorTrustedDeviceUnknown")} @@ -240,8 +248,8 @@ export default function TwoFactorSettingsCard() { } secondary={t("twoFactorTrustedDeviceMeta", { - lastSeen: new Date(d.lastSeenAtUtc).toLocaleDateString(), - expires: new Date(d.expiresAtUtc).toLocaleDateString(), + lastSeen: new Date(d.lastSeenAtUtc).toLocaleDateString(locale), + expires: new Date(d.expiresAtUtc).toLocaleDateString(locale), })} /> @@ -385,6 +393,6 @@ export default function TwoFactorSettingsCard() { )} - + ); } diff --git a/job-tracker-ui/src/i18n/translations.ts b/job-tracker-ui/src/i18n/translations.ts index e8c4601..b81ec87 100644 --- a/job-tracker-ui/src/i18n/translations.ts +++ b/job-tracker-ui/src/i18n/translations.ts @@ -1189,6 +1189,8 @@ export const translations = { profileGoogleNotLinked: "Google not linked", profileCvReady: "CV ready · {count} words", profileCvMissing: "CV missing", + profileCvTextAvailable: "Stored CV text · {count} words", + profileCvTextMissing: "No stored CV text", profileChangeImage: "Change image", profileRemoveImage: "Remove", profileImageUpdated: "Profile image updated.", @@ -2057,7 +2059,7 @@ export const translations = { googleUnlinked: "Google account unlinked.", googleUnlinkFailed: "Failed to unlink Google account.", microsoftAccountTitle: "Microsoft account", - microsoftSetupHint: "Set `NEXT_PUBLIC_MICROSOFT_CLIENT_ID` in your UI environment to enable Microsoft sign-in and account linking.", + microsoftSetupHint: "Microsoft sign-in is not available in this deployment.", microsoftLinked: "Linked", microsoftAvailableToLink: "Available to link", microsoftLinkedDate: "Linked {date}", @@ -3773,6 +3775,8 @@ export const translations = { profileGoogleNotLinked: "Google ikke koblet", profileCvReady: "CV klart · {count} ord", profileCvMissing: "CV mangler", + profileCvTextAvailable: "Lagret CV-tekst · {count} ord", + profileCvTextMissing: "Ingen lagret CV-tekst", profileChangeImage: "Bytt bilde", profileRemoveImage: "Fjern", profileImageUpdated: "Profilbildet ble oppdatert.", @@ -3975,7 +3979,7 @@ export const translations = { twoFactorInvalidCode: "Ugyldig kode. Prøv igjen.", twoFactorRateLimited: "For mange forsøk. Vent noen minutter og prøv igjen.", twoFactorGenericError: "Noe gikk galt. Prøv igjen.", - twoFactorSectionTitle: "Topunkts autentisering", + twoFactorSectionTitle: "Tofaktorautentisering", twoFactorStatusEnabled: "Aktivert siden {date}", twoFactorStatusDisabled: "Ikke aktivert", twoFactorEnableButton: "Aktiver 2FA", @@ -4641,7 +4645,7 @@ export const translations = { googleUnlinked: "Google-konto koblet fra.", googleUnlinkFailed: "Kunne ikke koble fra Google-kontoen.", microsoftAccountTitle: "Microsoft-konto", - microsoftSetupHint: "Sett `NEXT_PUBLIC_MICROSOFT_CLIENT_ID` i UI-miljøet ditt for å aktivere Microsoft-innlogging og kontokobling.", + microsoftSetupHint: "Microsoft-innlogging er ikke tilgjengelig i denne installasjonen.", microsoftLinked: "Koblet", microsoftAvailableToLink: "Tilgjengelig for kobling", microsoftLinkedDate: "Koblet {date}", diff --git a/job-tracker-ui/src/profile-page.test.tsx b/job-tracker-ui/src/profile-page.test.tsx index 8014e55..3409afd 100644 --- a/job-tracker-ui/src/profile-page.test.tsx +++ b/job-tracker-ui/src/profile-page.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import '@testing-library/jest-dom'; -import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import { getContrastRatio } from '@mui/material/styles'; import { ToastProvider } from './toast'; import { I18nProvider } from './i18n/I18nProvider'; @@ -9,6 +9,7 @@ import CareerProfilePage from './views/CareerProfilePage'; import { api } from './api'; jest.mock('./api', () => ({ + getApiErrorMessage: jest.requireActual('./api').getApiErrorMessage, api: { get: jest.fn(), post: jest.fn(), @@ -219,8 +220,93 @@ test('account headings separate identity from security sections', async () => { renderWith(ProfilePage); await screen.findByText('DD'); expect(screen.getByRole('heading', { name: 'Profile', level: 2 })).toBeInTheDocument(); - expect(screen.getByRole('heading', { name: 'Account', level: 3 })).toBeInTheDocument(); - expect(screen.getByRole('heading', { name: 'Change password', level: 3 })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Account', level: 2 })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Change password', level: 2 })).toBeInTheDocument(); + expect(screen.queryByRole('heading', { level: 1 })).not.toBeInTheDocument(); + expect(screen.getByRole('region', { name: 'Account' })).toBeInTheDocument(); + expect(screen.getByRole('region', { name: 'Change password' })).toBeInTheDocument(); +}); + +test.each([ + ['en', 'No stored CV text', 'CV missing'], + ['nb', 'Ingen lagret CV-tekst', 'CV mangler'], +])('account CV text status in %s does not claim the CV library is empty', async (language, textLabel, misleadingLabel) => { + const originalRead = mockedApi.get.getMockImplementation()!; + mockedApi.get.mockImplementation(async (url: string) => { + const response = await originalRead(url) as { data: Record }; + return url === '/auth/me' ? { ...response, data: { ...response.data, profileCvText: '' } } : response; + }); + localStorage.setItem('uiLanguage', language); + renderWith(ProfilePage); + await screen.findByText('DD'); + expect(screen.getByText(textLabel)).toBeInTheDocument(); + expect(screen.queryByText(misleadingLabel)).not.toBeInTheDocument(); + expect(mockedApi.get.mock.calls.some(([url]) => String(url).startsWith('/cv/'))).toBe(false); + expect(mockedApi.put).not.toHaveBeenCalled(); +}); + +test('[PRES-E02/E09/C01] account save carries four identity fields and keeps career, email and CV writes separate', async () => { + renderWith(ProfilePage); + await waitFor(() => expect(screen.getByLabelText('Display name')).toHaveValue('Demo User')); + fireEvent.change(screen.getByLabelText('Display name'), { target: { value: 'Account Display' } }); + fireEvent.change(screen.getByLabelText('Username'), { target: { value: 'account-user' } }); + fireEvent.change(screen.getByLabelText('First name'), { target: { value: 'Account' } }); + fireEvent.change(screen.getByLabelText('Last name'), { target: { value: 'Identity' } }); + fireEvent.change(screen.getByLabelText('New email'), { target: { value: 'unconfirmed@example.com' } }); + fireEvent.change(screen.getByLabelText(/headline/i), { target: { value: ' Local account headline ' } }); + fireEvent.click(screen.getByRole('button', { name: 'Save changes' })); + + await waitFor(() => expect(mockedApi.put).toHaveBeenCalledTimes(1)); + expect(mockedApi.put).toHaveBeenCalledWith('/auth/profile', { + userName: 'account-user', firstName: 'Account', lastName: 'Identity', displayName: 'Account Display', + }); + expect(mockedApi.post).not.toHaveBeenCalled(); + expect(mockedApi.patch).not.toHaveBeenCalled(); + expect(mockedApi.delete).not.toHaveBeenCalled(); + expect(mockedApi.get.mock.calls.some(([url]) => String(url).startsWith('/career') || String(url).startsWith('/cv-variants'))).toBe(false); +}); + +test('[PRES-E02/E08/E09] failed account save keeps a scoped error and the identity draft for explicit retry', async () => { + mockedApi.put.mockRejectedValueOnce({ response: { status: 400, data: { detail: 'Username already belongs to another account.' } } }); + renderWith(ProfilePage); + await waitFor(() => expect(screen.getByLabelText('Display name')).toHaveValue('Demo User')); + fireEvent.change(screen.getByLabelText('Display name'), { target: { value: 'Unsaved identity' } }); + fireEvent.change(screen.getByLabelText('Username'), { target: { value: 'occupied-name' } }); + fireEvent.click(screen.getByRole('button', { name: 'Save changes' })); + + const account = screen.getByRole('region', { name: 'Account' }); + expect(await within(account).findByRole('alert')).toHaveTextContent('Username already belongs to another account.'); + expect(screen.getByLabelText('Display name')).toHaveValue('Unsaved identity'); + expect(screen.getByLabelText('Username')).toHaveValue('occupied-name'); + expect(within(account).getByRole('button', { name: 'Save changes' })).toBeEnabled(); + expect(mockedApi.put).toHaveBeenCalledTimes(1); + + fireEvent.click(within(account).getByRole('button', { name: 'Save changes' })); + await waitFor(() => expect(mockedApi.put).toHaveBeenCalledTimes(2)); + expect(mockedApi.put.mock.calls[1]).toEqual(mockedApi.put.mock.calls[0]); + await waitFor(() => expect(within(account).queryByRole('alert')).not.toBeInTheDocument()); +}); + +test('[PRES-E02/E08] failed password update keeps a scoped error and secret draft until explicit retry succeeds', async () => { + mockedApi.post.mockRejectedValueOnce({ response: { status: 400, data: { errors: { newPassword: ['The new password does not meet account requirements.'] } } } }); + renderWith(ProfilePage); + await waitFor(() => expect(screen.getByLabelText('Current password')).toBeEnabled()); + fireEvent.change(screen.getByLabelText('Current password'), { target: { value: 'current-secret' } }); + fireEvent.change(screen.getByLabelText('New password'), { target: { value: 'draft-secret' } }); + fireEvent.click(screen.getByRole('button', { name: 'Update password' })); + + const password = screen.getByRole('region', { name: 'Change password' }); + expect(await within(password).findByRole('alert')).toHaveTextContent('The new password does not meet account requirements.'); + expect(screen.getByLabelText('Current password')).toHaveValue('current-secret'); + expect(screen.getByLabelText('New password')).toHaveValue('draft-secret'); + expect(mockedApi.post).toHaveBeenCalledTimes(1); + expect(mockedApi.post).toHaveBeenCalledWith('/auth/change-password', { currentPassword: 'current-secret', newPassword: 'draft-secret' }); + + fireEvent.click(within(password).getByRole('button', { name: 'Update password' })); + await waitFor(() => expect(mockedApi.post).toHaveBeenCalledTimes(2)); + await waitFor(() => expect(screen.getByLabelText('Current password')).toHaveValue('')); + expect(screen.getByLabelText('New password')).toHaveValue(''); + expect(within(password).queryByRole('alert')).not.toBeInTheDocument(); }); test('profile page loads persisted structured cv and can re-parse it', async () => { diff --git a/job-tracker-ui/src/views/ProfilePage.tsx b/job-tracker-ui/src/views/ProfilePage.tsx index c6f75b7..8d3ba32 100644 --- a/job-tracker-ui/src/views/ProfilePage.tsx +++ b/job-tracker-ui/src/views/ProfilePage.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { Alert, Avatar, Box, Button, Chip, Divider, Paper, TextField, Typography } from "@mui/material"; +import { Alert, Avatar, Box, Button, Chip, TextField, Typography } from "@mui/material"; import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; import PhotoCameraOutlinedIcon from "@mui/icons-material/PhotoCameraOutlined"; @@ -16,6 +16,7 @@ import { useToast } from "../toast"; import { useI18n } from "../i18n/I18nProvider"; import { parseStructuredCvJson } from "../profileCv"; import { getUserScopedStorageKey } from "../auth"; +import { PageSection } from "../layout/PageLayout"; type MeResponse = { provider?: "local" | "google" | "external"; @@ -66,6 +67,8 @@ export default function ProfilePage() { const [me, setMe] = useState(null); const [loading, setLoading] = useState(false); const [loadError, setLoadError] = useState(null); + const [saveError, setSaveError] = useState(null); + const [passwordError, setPasswordError] = useState(null); const [uploadingAvatar, setUploadingAvatar] = useState(false); const [avatarFile, setAvatarFile] = useState(null); const [cropOpen, setCropOpen] = useState(false); @@ -108,7 +111,7 @@ export default function ProfilePage() { setLoadError(null); } catch (error: any) { setMe(null); - setLoadError(String(error?.response?.data || error?.message || t("profileLoadFailed"))); + setLoadError(getApiErrorMessage(error, t("profileLoadFailed"))); } finally { setLoading(false); } @@ -129,10 +132,10 @@ export default function ProfilePage() { ? t("profileGoogleLinkedWithEmail", { email: me.googleLink.email }) : t("profileGoogleLinked") : t("profileGoogleNotLinked"); - const cvLabel = profileCvText.trim() ? t("profileCvReady", { count: cvWordCount }) : t("profileCvMissing"); + const cvLabel = profileCvText.trim() ? t("profileCvTextAvailable", { count: cvWordCount }) : t("profileCvTextMissing"); return ( - + {loadError ? ( - void loadProfile()}>{t("retry")}}> + void loadProfile()}>{t("retry")}}> {t("profileLoadFailed")} {loadError} ) : null} - - - + {loading ? {t("loading")} : null} + + + + + {initials} - + (prev ? { ...prev, avatarImageDataUrl: undefined } : prev)); toast(t("profileImageRemoved"), "success"); } catch (e: any) { - toast(String(e?.response?.data || e?.message || t("profileImageRemoveFailed")), "error"); + toast(getApiErrorMessage(e, t("profileImageRemoveFailed")), "error"); } finally { setUploadingAvatar(false); } @@ -214,20 +220,18 @@ export default function ProfilePage() { ) : null} - - - {careerOnly ? "Master career profile" : t("profileTitle")} - + {me?.userName || me?.displayName || fullName || me?.email || "-"} {headline || t("profileHeadlinePlaceholder")} - + + {!careerOnly ? <> @@ -235,15 +239,11 @@ export default function ProfilePage() { : null} - - {!careerOnly ? - {t("profileAccountSection")} - {!isLocal ? ( - - {t("profileReadOnlyInfo")} - - ) : null} - : null} + + + {!careerOnly && !isLocal ? {t("profileReadOnlyInfo")} : null} + + {saveError ? {saveError} : null} {!careerOnly ? <> setDisplayName(e.target.value)} disabled={!isLocal} fullWidth /> @@ -260,10 +260,11 @@ export default function ProfilePage() { /> : null} - {!careerOnly && isLocal ? - {pendingEmail ? {t("profilePendingEmail", { email: pendingEmail })} : null} + {!careerOnly && isLocal ? + {pendingEmail ? {t("profilePendingEmail", { email: pendingEmail })} : null} setEmailChangePassword(e.target.value)} autoComplete="current-password" fullWidth /> + + - {!careerOnly ? - - {t("profileChangePassword")} - {!isLocal ? {t("profilePasswordLocalOnly")} : null} - : null} + {!careerOnly ? + + {!careerOnly && !isLocal ? {t("profilePasswordLocalOnly")} : null} + + {passwordError ? {passwordError} : null} {!careerOnly ? <> setCurrentPassword(e.target.value)} disabled={!isLocal} fullWidth /> @@ -342,18 +349,21 @@ export default function ProfilePage() { : null} - + + : null} {!careerOnly && isLocal ? : null} {!careerOnly && isLocal ? : null} - + ); }