feat(onboarding): migrate source-backed setup guidance
This commit is contained in:
@@ -8,7 +8,8 @@ Entry is the completed Phase6 successor receipt `b5319981ad3d1bd635daff861453d23
|
||||
|---|---|---|
|
||||
| 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 |
|
||||
| P7-01 account identity/security and final checkpoint outcome | `ad7b4196af638f3a8c79e261ffd96507bd002835` | [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 secret/whitespace/docs/matrix checks PASS (303 local links,57 groups/32 routes); normal push verified2026-10-11 19:16:15UTC: localHEAD=origin=live developmentad7b419, local/origin/live main27657f3 unchanged, worktree clean at observation. Actual receipt appended by successor; P7-02 entry satisfied. Source/export frozen independently from public8ad720d |
|
||||
| P7-02 onboarding guidance sub-slice | PENDING actual commit/push | [Report](p7-02/README.md), [frozen source/runtime](p7-02/evidence/onboarding-results.json): affected32/2 (+23 net over nine inherited subset cases), ordinary native13/regression4, app/browser types/lint/build and actual13-image review pass. BuildbwZtqlczGqVRJ0J01IxVK; thirteen all-enabled scans0/13 incompletes with M11/manual/provider conditions. Retry-focus unit/native RED and earlier12-pass history retained; latest full573/89 inherited,596 derived only. Implemented onboarding CONDITIONAL GO; slice-specific hygiene PASS (four commands0,9docs/329links/0errors,57/32 retained) and direct normal push receipt pending. Career overview/evidence and whole P7-02 pending;57/32 unchanged/main NO-GO |
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -79,10 +79,16 @@ RD28 public runtime provenance is retained in the immutable `.artifacts/phase7/p
|
||||
|
||||
[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.
|
||||
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; final staged finite hygiene passed and accountad7b419 is normally pushed with actual localHEAD=origin=live receipt2026-10-11 19:16:15UTC; main unchanged, worktree clean at observation. 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
|
||||
|
||||
### P7-02 onboarding conditional disposition
|
||||
|
||||
[Onboarding results](p7-02/evidence/onboarding-results.json) add fresh affected32/2, ordinary native13 and separate existing Dashboard/Analytics4 with types/lint/build passing. All13 final PNGs were actually inspected; thirteen all-enabled scans retain0 violation nodes/13 `color-contrast` incomplete occurrences on the invisible notification badge `0` (`shortTextContent`). Keep M09/M11 contextual/human qualification and all provider/zoom/physical-input gates open. The connected-mailbox response in the four-complete native case is an explicit stored-connection fixture, not a real grant. Controlled English failure text is server-fixture detail; translated product guidance/fallbacks remain separate.
|
||||
|
||||
Actual review demonstrated lost focus when Retry disappeared. Five strengthened existing unit cases and one focused native case reproduced it before the bounded Dismiss-ref/focus correction; final native13 proves retry focus and next action after dismissal/complete recovery. Existing Dashboard summary Retry recovery still captures focus on the shell Skip link with altered scroll; its owner focus restoration remains unqualified. Presentation retry does not diagnose historical Dashboard/Board500 causes or close RD08–RD14. Latest full573/89 is inherited;596/89 is derived573+23 only. Exact source/test/build/export/runner/raw/unit/history are frozen under `.artifacts/phase7/p7-02/onboarding-final/`,154 file-reference hashes verified; initial pre-focus source/export observations are historical identities rather than claimed snapshots. Implemented onboarding CONDITIONAL GO is final hygiene PASS/push pending; Career overview/evidence and whole P7-02 remain pending,57/32 statuses unchanged.
|
||||
|
||||
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.
|
||||
|
||||
API transferSize0 is unavailable cross-origin timing information, not zero network bytes. CDP document/node/heap snapshots can retain earlier navigation/CV iframe documents and do not prove leak/growth. The existing measured Board/Dashboard candidates require baseline/comparative evidence before a repair or capacity claim.
|
||||
|
||||
@@ -19,8 +19,8 @@ The migration-owner column assigns the approved slice while retaining completed
|
||||
| **PRES-E05** Sessions/current-device/revoke/logout | [Shell/private lifecycle](../phase-5/p5-02/README.md), Sessions/account-scoped tests | PARTIAL | Native session-card refresh after remote revocation and complete device/AT acceptance | P7-01; E01 retained product inventory |
|
||||
| **PRES-E06** Avatar/account identity/email/password | [Retained `/profile` and confirmation route](../phase-5/p5-15/ROUTE_ACCEPTANCE.md) | PARTIAL | Complete avatar/remove/password/email-change native flow; account identity remains distinct from career facts | P7-01; E01 retained product inventory |
|
||||
| **PRES-E07** Expanded/rail/mobile navigation, active route, commands | [Grouped shell](../phase-5/p5-01/README.md), [utilities](../phase-5/p5-02/README.md), [drawer repair](../phase-5/p5-15/p5-15-04/mobile-drawer/README.md) | PARTIAL | Spoken names/live state, settled drawer AX-name triage and historical command/dialog timing; no route silently hidden | Retain shell; P7-11 verified residual / P7-12 |
|
||||
| **PRES-E08** Themes, EN/NB hydration, shared toast/confirm/prompt/errors | [Semantic foundations](../phase-4/p4-06/README.md), [shared readiness](../phase-5/p5-00/README.md), [P6 corrections](../phase-6/p6-01/README.md) | PARTIAL | Full OS/locale/state cross-product, contrast/incomplete axe, reader/reflow; save feedback is distinct from server500 root-cause repair | Every affected slice / P7-12 |
|
||||
| **PRES-E09** Account switch/stale read-write/BFCache/revalidation | [Native owned-resource protection](../phase-5/p5-13-readiness/README.md), [auth expiry/logout proof](../phase-5/p5-15/p5-15-01/analytics-auth-repair/README.md) | PARTIAL | Broader BFCache/offline/old-bundle/deployed cases; presentation never becomes authorization | Every affected slice / P7-12 |
|
||||
| **PRES-E08** Themes, EN/NB hydration, shared toast/confirm/prompt/errors | [Semantic foundations](../phase-4/p4-06/README.md), [shared readiness](../phase-5/p5-00/README.md), [P6 corrections](../phase-6/p6-01/README.md), [P7-02 finite onboarding](p7-02/evidence/onboarding-results.json) | PARTIAL | Full OS/locale/state cross-product, contrast/incomplete axe, reader/reflow; save feedback is distinct from server500 root-cause repair | Every affected slice / P7-12 |
|
||||
| **PRES-E09** Account switch/stale read-write/BFCache/revalidation | [Native owned-resource protection](../phase-5/p5-13-readiness/README.md), [auth expiry/logout proof](../phase-5/p5-15/p5-15-01/analytics-auth-repair/README.md), [P7-02 owner-local dismissal](p7-02/evidence/onboarding-results.json) | PARTIAL | Broader BFCache/offline/old-bundle/deployed cases; presentation never becomes authorization | Every affected slice / P7-12 |
|
||||
| **PRES-J01** NAV search/filter/sort/source/freshness/details | [Retained Discover owner](../phase-5/p5-15/ROUTE_ACCEPTANCE.md) | PARTIAL | Live NAV availability/freshness and long-result keyboard/narrow traversal | P7-05 |
|
||||
| **PRES-J02** Named saved searches/run/delete/alerts/results/dismiss | [Native saved-search repair](../phase-5/p5-15/p5-15-01/saved-search-repair/README.md) | PARTIAL | Actual scheduler-to-owned-notification delivery, human/mobile delete acceptance and intended-provider qualification; deterministic runner is not live NAV | P7-05 |
|
||||
| **PRES-J03** Reviewed manual/URL/paste creation/company/tags/dates/duplicates | [Jobs/Add](../phase-5/p5-03/README.md), shared AddJobModal; transport boundary tests | PARTIAL | Duplicate/company/date/interrupted creation and distinct creation-intent idempotency debt; preview/open is no-write | Retain Jobs/capture; P7-11 verified residual / P7-12 |
|
||||
@@ -62,7 +62,7 @@ The migration-owner column assigns the approved slice while retaining completed
|
||||
| **PRES-M05** Gmail reviewed job/link/reject/keep/notes/sync/create | [Review alias/native route](../phase-5/p5-15/ROUTE_ACCEPTANCE.md), review controller/page tests | PARTIAL | Real provider and complete proposed-job create flow; route opening is no-write | P7-06 |
|
||||
| **PRES-M06** Gmail/Graph/IMAP connect/status/disconnect | [Retained Connections](../phase-5/p5-15/ROUTE_ACCEPTANCE.md), connection/controller tests | PARTIAL | Real grants/revoke/token expiry/reconnect/IMAP safety; keep sign-in identity separate | P7-06/09 |
|
||||
| **PRES-S01** Personal counts/activity/funnel/company/skills/stage/salary/next action | [Dashboard](../phase-5/p5-06/README.md), [implemented Analytics](../phase-5/p5-07/README.md), [target correction](../phase-5/p5-15/p5-15-04/report-targets/README.md) | PARTIAL | Exact cohort/timezone/sample/unsupported metrics and whole reader acceptance; no fabricated statistics or Analytics rebuild | Retain reports; P7-11 residual |
|
||||
| **PRES-S02** Five owner-local flags/onboarding/bell read-dismiss | [Dashboard flags](../phase-5/p5-06/README.md), [bell](../phase-5/p5-02/README.md), [12 concurrent rounds](../phase-5/p5-15/p5-15-06/README.md) | PARTIAL | Historical500 causes unproved; full onboarding/notification acceptance; order/resize/reset/cross-device design remains Phase8 | P7-02/08; Phase8 separate |
|
||||
| **PRES-S02** Five owner-local flags/onboarding/bell read-dismiss | [Dashboard flags](../phase-5/p5-06/README.md), [bell](../phase-5/p5-02/README.md), [12 concurrent rounds](../phase-5/p5-15/p5-15-06/README.md), [P7-02 truthful guidance/legacy dismissal](p7-02/evidence/onboarding-results.json) | PARTIAL | Historical500 causes unproved; full human/notification acceptance; order/resize/reset/cross-device design remains Phase8 | P7-02/08; Phase8 separate |
|
||||
| **PRES-S03** Rules/notification/table/theme/language/account preferences | [Settings primitives](../phase-4/p4-04/README.md), [Rules retention](../phase-5/p5-15/p5-15-01/rules-draft-repair/README.md), [P6 feedback](../phase-6/p6-01/README.md) | PARTIAL | Language failure visibility is separate from RD11 server cause; RD12 notification locking/RD13 Rules500 and human/provider acceptance retained | P7-09 |
|
||||
| **PRES-S04** Privacy/AI usage/limits/server entitlement/billing availability | [AI protocol/privacy/metering](../phase-5/p5-13-readiness/README.md), plan/usage tests | PARTIAL | Checkout/webhook/portal and provider delivery; temporary C11 withholding cannot disable unrelated AI or imply successful review | P7-09 |
|
||||
| **PRES-S05** Account ZIP/encrypted backup/lifecycle/deliberate delete | [Native package/ZIP relationships](../phase-5/p5-08b-readiness/README.md), [release recovery gate](../phase-5/p5-15/p5-15-05/README.md) | PARTIAL | Current complete backup/restore/keys/tombstones/rollback and deletion/provider policy; downloading backup is not restore proof | P7-09 |
|
||||
@@ -88,7 +88,7 @@ The22 public native cases and two isolated recovery cases are new separate packs
|
||||
|
||||
### 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.
|
||||
[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 staged finite hygiene passed and accountad7b419 was normally pushed, direct localHEAD=origin=live equality verified2026-10-11 19:16:15UTC with main unchanged and clean worktree at observation. All57 statuses and32 whole-route qualifications remain unchanged.
|
||||
|
||||
| Protected groups | Fresh finite account evidence | Remaining mandatory boundary |
|
||||
|---|---|---|
|
||||
|
||||
@@ -9,8 +9,8 @@ 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](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-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); pushedad7b419 with final static PASS/direct receipt19:16:15UTC; main unchanged |
|
||||
| [P7-02](p7-02/README.md) | Onboarding and Career overview/evidence | Entryad7b419 pushed/verified; bounded onboarding CONDITIONAL GO: affected32/native13/regression4, types/lint/build and13 actual PNG inspections pass; final hygiene PASS/push pending. Career overview/evidence and whole checkpoint remain pending |
|
||||
| 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 |
|
||||
| P7-05 | Discovery, Companies, Offers and Reminders | Existing domain owners and Jobs return context; planned |
|
||||
@@ -35,10 +35,12 @@ Keep the 52 all-enabled and 267 inherited-rule axe inventories separate: combine
|
||||
|
||||
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.
|
||||
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 staged finite hygiene passed and accountad7b419 was normally pushed, direct localHEAD=origin=live development equality verified2026-10-11 19:16:15UTC, main unchanged and worktree clean at observation. No whole-group/route upgrade follows from these finite results.
|
||||
|
||||
## Working constraints
|
||||
|
||||
The [P7-02 onboarding slice](p7-02/evidence/onboarding-results.json) adds actual fresh affected32/2 (+23 cases over its nine-case inherited subset), ordinary native13 and separate Dashboard/Analytics4, app/browser types/lint/build and13 inspected final PNGs. Its13 all-enabled scans retain0 violation nodes/13 invisible-zero notification-badge contrast incompletes; no manual conformance or real provider grant is inferred. The retry-focus defect was reproduced by five strengthened unit cases and a focused native failure before correction. Latest full frontend573/89 remains inherited;596/89 is only a derived expectation. Onboarding implemented scope is CONDITIONAL GO final hygiene PASS/push pending; Career overview/evidence remains pending,57 groups/32 routes unchanged and production NO-GO.
|
||||
|
||||
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.
|
||||
|
||||
Verify EN/Bokmål, light/dark, appropriate density, 320/360–390/768/1024/1280/1440/wide layouts, relevant breakpoint boundaries, keyboard/focus/errors and legitimate document-canvas scrolling. Human reader/device/provider/output evidence stays explicit when unavailable. A newly demonstrated severe shared defect blocks its dependent propagation; unaffected development can continue.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# P7-01 — Authentication and account-security presentation
|
||||
|
||||
**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.
|
||||
**Status: implemented P7-01 scope CONDITIONAL GO; public8ad720d and accountad7b419 normally pushed, final static PASS and actual direct receipts recorded.** 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 staged finite hygiene passed and normal account pushad7b419 has actual direct receipt2026-10-11 19:16:15UTC. The public sub-slice alone does not complete P7-01.
|
||||
|
||||
## Baseline and bounded commit scope
|
||||
|
||||
@@ -93,7 +93,7 @@ At the public gate, five account-presentation RED cases and12 real-provider case
|
||||
|
||||
## 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.
|
||||
**Implemented account scope: CONDITIONAL GO after fresh runtime and actual final visual/context review; final staged finite hygiene passed; accountad7b419 normally pushed with actual direct receipt19:16:15UTC.** 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.
|
||||
|
||||
@@ -131,6 +131,6 @@ The first native account attempt passed1/6. Four cases assumed Profile PUT retur
|
||||
|
||||
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.
|
||||
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 staged finite hygiene passed; accountad7b419 was normally committed/pushed, localHEAD=origin=live development directly verified2026-10-11 19:16:15UTC. Local/origin/live main27657f3 remains unchanged, worktree clean at immediate observation. Runtime/source/public evidence stays frozen and the actual successor receipt is appended separately. The latest account static record uses [static-checks.json](evidence/static-checks.json); its final2026-10-11 19:15:39UTC metadata and canonical-LF JSON hash are recorded under the actual push receipt.
|
||||
|
||||
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.
|
||||
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 pushad7b419 was directly verified2026-10-11 19:16:15UTC; this successor appends that actual receipt without changing frozen measurements.
|
||||
|
||||
@@ -136,6 +136,11 @@
|
||||
"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"
|
||||
},
|
||||
"historicalSourceApplicability": {
|
||||
"appliesAtCommit": "ad7b4196af638f3a8c79e261ffd96507bd002835",
|
||||
"mapping": "identity.accountSourceSnapshots maps each originalSourcePath to exact immutable raw bytes and its canonicalLfSha256",
|
||||
"qualification": "accountSourceFiles and accountSourceSha256Lf are historical measured source identities. Later legitimate changes, including P7-02 translations, must be verified against these immutable snapshots or the committed ad7b419 tree rather than the current workspace. Original hashes/digest remain unchanged."
|
||||
}
|
||||
},
|
||||
"environment": {
|
||||
@@ -34071,7 +34076,37 @@
|
||||
"serverErrorLocalisation": "Existing server-authored English errors in NB states are explicitly retained; caller fallback is translated. No new hard-coded product strings or invented server translation.",
|
||||
"productionAndDeferred": "Main27657f31131cd95d42f0e1290853ec9c9b3959a4 unchanged; production NO-GO, M01-M12 open, M13/P5-13 deferred do-not-execute, Phase8 outside scope",
|
||||
"pushReceipt": {
|
||||
"status": "PENDING_PARENT_ACCOUNT_COMMIT_AND_PUSH"
|
||||
"status": "NORMAL_PUSH_VERIFIED",
|
||||
"checkedUtc": "2026-10-11T19:16:15.819630+00:00",
|
||||
"head": "ad7b4196af638f3a8c79e261ffd96507bd002835",
|
||||
"originDevelopment": "ad7b4196af638f3a8c79e261ffd96507bd002835",
|
||||
"live": {
|
||||
"refs/heads/codex/ui-redesign-phase4": "ad7b4196af638f3a8c79e261ffd96507bd002835",
|
||||
"refs/heads/main": "27657f31131cd95d42f0e1290853ec9c9b3959a4"
|
||||
},
|
||||
"main": "27657f31131cd95d42f0e1290853ec9c9b3959a4",
|
||||
"originMain": "27657f31131cd95d42f0e1290853ec9c9b3959a4",
|
||||
"worktree": "",
|
||||
"raw": {
|
||||
"path": ".artifacts/phase7/p7-01/account-final/push-receipt.json",
|
||||
"sha256": "e27e026ae49478355af8f7ade07f825db3710ebf56907cf6f83263efcb7dcb96",
|
||||
"hashPolicy": "Exact measured raw bytes"
|
||||
},
|
||||
"staticGate": {
|
||||
"path": "docs/redesign/phase-7/p7-01/evidence/static-checks.json",
|
||||
"sha256": "a872440b8907f9303034ef652da7dbd03018ce9fc28584bea5d32f82354199f7",
|
||||
"hashPolicy": "Portable JSON UTF-8 text with CRLF normalized to LF",
|
||||
"checkedUtc": "2026-10-11T19:15:39.214576+00:00",
|
||||
"status": "PASS",
|
||||
"commands": 4,
|
||||
"documentation": {
|
||||
"files": 8,
|
||||
"relativeLinks": 303,
|
||||
"errors": []
|
||||
},
|
||||
"qualification": "Final staged checkpoint hygiene, distinct from successor receipt updates"
|
||||
},
|
||||
"qualification": "Actual successor receipt; original tested working-tree identity/build/runtime/public slice remains unchanged. Clean worktree recorded at immediate observation, not inferred for future work."
|
||||
},
|
||||
"rawRetention": {
|
||||
"root": ".artifacts/phase7/p7-01/account-final",
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# P7-02 — Onboarding and Career overview/evidence
|
||||
|
||||
**Status: implemented onboarding guidance CONDITIONAL GO; final hygiene PASS, normal push pending. Career overview/evidence and whole P7-02 remain pending.** Follow the exact [approved handoff](../../phase-6/p6-05/PHASE7_HANDOFF.md#migration-order-and-commit-boundaries). This checkpoint has two bounded commits: onboarding guidance first, then Career overview/evidence. Structured Career editing/import/history remains P7-03; no alternative checkpoint order or new route is introduced.
|
||||
|
||||
## Accepted entry baseline
|
||||
|
||||
P7-01 account `ad7b4196af638f3a8c79e261ffd96507bd002835` is normally pushed. Its actual immediate receipt2026-10-11 19:16:15UTC records localHEAD=origin=live development equality, local/origin/live main27657f3 unchanged and a clean worktree. See the [ledger](../COMMITS.md), [P7-01 report](../p7-01/README.md) and [frozen account evidence](../p7-01/evidence/account-results.json). P7-01 implemented scope is CONDITIONAL GO; production stays NO-GO.
|
||||
|
||||
The inherited latest frontend result is **573/89**, with account native6/public22/auth-boundaries10, final types/lint/build, eight actual account PNG inspections and staged secret/whitespace/docs/matrix checks passing. Account build `SbTVDn-CSb21uPKSzFqOv` is frozen separately from the earlier public export. Backend1,033, sidecar49, ordinary integrated159 and fidelity16/36+8/24 remain inherited Phase6 executions; the unchanged-backend auth subset87 is separate. These are entry evidence, **not freshly executed P7-02 results**.
|
||||
|
||||
All57 preservation groups remain56 PARTIAL + C11 DEFERRED and all32 routes remain retained with their qualifications. Account8 axe scans retain0 violation nodes/85 incomplete occurrences; account-build public22 retain0/31. Earlier public and Phase6 mixed-scope319/11/2,473 inventories remain distinct. Actual human reader/zoom/device/provider/output and production gates remain open, with M13/P5-13 deferred do-not-execute.
|
||||
|
||||
## Bounded onboarding-guidance commit
|
||||
|
||||
Owners are [OnboardingChecklist](../../../../job-tracker-ui/src/components/OnboardingChecklist.tsx), its [Dashboard consumer](../../../../job-tracker-ui/src/components/DashboardView.tsx), existing i18n/theme/state primitives and [checklist unit tests](../../../../job-tracker-ui/src/onboarding-checklist.test.tsx). The accepted entry checklist inferred two fixed completed steps for signup/email verification although its `MeResponse` has no verification fact. Authentication/verification itself remains the already verified P7-01 owner; this guidance change does not alter it.
|
||||
|
||||
Use **four observable guidance steps**: account identity, canonical Career content, optional mailbox connection and first tracked job. Keep the existing facts/endpoints, legacy per-user `onboardingDismissed:${getUserKeyFromToken()}` key and existing action destinations `/profile`, `/career`, `/settings/connected-accounts`, `/jobs`. Do not introduce forced onboarding, a preference migration or a fabricated completeness score. Canonical Career content is distinct from account identity, stored CV text and the CV library.
|
||||
|
||||
Known/incomplete/unknown are separate states. A pending/failed Dashboard Jobs summary passes nullable unknown rather than invented zero/no-jobs; failed source reads cannot become authoritative unmet criteria. Retry is deliberate and read-only. Preserve independent known facts, user dismissal/reload, account switching and stale response disposal. An optional mailbox is not a prerequisite for job tracking, and opening guidance never starts OAuth, a model or mail mutation.
|
||||
|
||||
The initial Dashboard caller reproducer at2026-10-11 19:17:24UTC passed8/9 and failed the new pending-summary assertion: the child receivedfalse instead ofnull. The bounded caller correction passed9/9 at19:17:51UTC with no skipped cases. Unique RED/GREEN command/report/log records are retained under `.artifacts/phase7/current/unit/onboarding-caller-{red,green}-...`. This one-file result does not establish acceptance of the new checklist or replace the forthcoming two-file affected run.
|
||||
|
||||
The first frozen onboarding source at23:37:03UTC passed affected **32/32 in two suites**, zero fail/pending/todo, executed23:38:08–23:38:14UTC. Checklist23 replaces inherited1 (+22); Dashboard caller9 replaces8 (+1). The affected baseline9 therefore becomes32 (+23). Exact added/removed-or-renamed titles remain in the finite manifest. Latest full frontend573/89 is inherited; no fresh full596 run is claimed. Source-worker RED passed1/23 and failed22. First GREEN passed22/23; the remaining failure selected an unscoped status also supplied by the fixture destination. The scoped guidance assertion then passed23/23. Unique command/report/log attempts are preserved.
|
||||
|
||||
The first native12 pack passed23:38:50–23:39:44UTC on build `pQI48DIBz4WVtCNmtoR5_`, with zero skip/flaky/unexpected/retry. That report is **historical**, not final acceptance: its retry workflow did not assert focus after the retry control disappeared. Actual review found focus was lost when Retry was removed during loading. Five strengthened existing unit cases reproduced it (18 pass/5 fail at23:40:32UTC); the focused native case failed0/1 at23:40:49–23:41:04UTC after actual Enter/GET success. The bounded seven-line existing Dismiss-button ref/focus correction does not change request or completion criteria. Corrected combined32/2 passed23:41:48UTC with zero failure/pending/todo. Fresh app/browser TypeScript, lint/build and corrected native12 passed, followed by separate existing Dashboard2/Analytics2 regression4. The initial manifest and raw attempts remain retained, without treating the earlier passing tests as sufficient focus proof.
|
||||
|
||||
The final **13 ordinary native cases passed23:45:40–23:46:34UTC**, with zero skip/flaky/unexpected/retry, on unchanged corrected build `bwZtqlczGqVRJ0J01IxVK`. The added thirteenth case changes only test coverage: four known-complete steps remove guidance and the next actual Tab reaches Customize Dashboard. Its mailbox-connected response is an explicit stored-connection fixture; account/Career/job data are real local API data, with no writes and no real provider grant qualification. Final browser TypeScript passed23:45:40UTC after this test-only addition; no rebuild was needed. The [finite manifest](evidence/onboarding-results.json) records exact source/build/export identities, commands, case deltas and all historical attempts. The separate regression4 passed23:43:50–23:44:49UTC and remains separate from onboarding axe scope.
|
||||
|
||||
The native pack includes eight finite EN/NB/theme width samples320/360/390/768/1024/1280 short landscape/1440/1920, ownerA/B dismissal/reload, failed mailbox read/read-only retry, four keyboard action destinations/Back, unknown Dashboard summary/retry and complete-guidance removal. Dismissal assertions wait for settled Dashboard state and verify no Career/mail-status GET after dismissed reload/returnA; early absent content while pending cannot stand in for dismissal proof. Actual next Tab after dismissal and persistent Dismiss focus through successful retry pass. The parent actually opened **all13 final raw PNGs** with `view_image`: labels/actions wrapped and the finite layouts were readable, with no accidental app overflow in the recorded metrics. The existing Dashboard summary Retry recovery image focuses the shell Skip link with altered scroll; this slice does not repair or accept that owner's focus restoration. Controlled English failure detail is fixture server-authored text, distinct from the translated product fallback. Onboarding has no global density setting/user control; density is not applicable. No physical input, zoom or human assistive evaluation is claimed.
|
||||
|
||||
All13 all-enabled axe scans retain **0 violation nodes and13 incomplete occurrences**, each `color-contrast`/`shortTextContent` on the shell notification badge `.MuiBadge-badge` with invisible `0` content. The tool cannot determine whether this short content is text; its reported ratio0 is not a measured contrast failure. Targets, HTML and check vectors remain intact for contextual M11/manual qualification. All13 recorded root-overflow measurements are0. These finite results do not replace human evaluation or combine with the separately scoped Dashboard/Analytics scans.
|
||||
|
||||
Final measured three-source LF digest is `dd98c47e6064a4f8d6abe6dbe4e2b61b86cf9dc45e5baa9e8c7fb51631ee3f70`; build `bwZtqlczGqVRJ0J01IxVK`;74-file exported digest `b6f528e55f7d12b357b0aea34fd61935aeb5c4e57550ea3d6fe6b54c66bc87f5`. Exact raw commands/reports/logs, whole UI/attempt tree, unit records, source/test snapshots, runner/config, BUILD_ID/export and accepted entry manifest are frozen under `.artifacts/phase7/p7-02/onboarding-final/`. All154 recorded path/hash references were verified at freeze; only file-reference paths were rebased, preserving measured bytes/hashes and original internal execution paths. Initial pre-focus source/export identities remain historical observations; this does not claim a pre-focus export/source snapshot. The frozen report keeps the earlier12-pass and focus RED evidence.
|
||||
|
||||
The fresh affected32 result replaces nine inherited cases with32: Dashboard8→9 adds one, checklist1→23 adds22 net. The original broad checklist title is replaced by its four-source-backed successor and22 additional cases; the manifest records all23 added titles and the single renamed title rather than hiding the change. Full596/89 is only the derived expectation573+23 with no new suite, **not a newly executed full frontend result**. The inherited latest full result remains573/89; final full regression belongs to P7-12. Native12→13 adds the complete-guidance recovery case with no product or unit-count change.
|
||||
|
||||
Slice-specific [final static checks](evidence/onboarding-static-checks.json) passed2026-10-11 23:53:23–23:53:26UTC: tracked/untracked secret scan and working/staged/Phase7-entry whitespace commands all exit0; nine Markdown documents/329 relative links checked with no errors at that observation;57 groups/32 concrete routes unchanged; main remains27657f3 and production NO-GO. Portable static JSON uses canonical UTF-8/LF hashes; raw log/runtime hashes retain exact saved bytes. Commit/push remains pending until its actual receipt.
|
||||
|
||||
| Required proof | Acceptance condition / current result |
|
||||
|---|---|
|
||||
| Truthful criteria and failures | Fresh affected32 and ordinary native13 pass four-step/unknown/failure/read-only retry assertions; provider qualification remains unverified |
|
||||
| Dismissal and owner boundary | Legacy-key/session dismissal, owner isolation/late responses/unavailable storage and actual dismiss/reload/settled no-read corroboration pass |
|
||||
| Existing navigation | Four existing destinations, actual keyboard/Back/no-write and dismissal/complete-recovery next-action focus proof pass |
|
||||
| Responsive/localised/semantic states | EN/NB/theme finite width samples, named H2/progress/control states, keyboard/focus and13 root-geometry/axe vectors pass their assertions; all13 raw PNGs actually inspected; M09/M11/human/zoom/device/provider qualifications retained |
|
||||
| Verification and bounded push | Fresh affected32/native13/regression4, app/browser types, lint/build, actual13-image inspection and slice-specific final hygiene pass; actual normal commit/push receipt **PENDING** |
|
||||
|
||||
## Career overview/evidence commit
|
||||
|
||||
Existing [CareerWorkspacePage](../../../../job-tracker-ui/src/views/CareerWorkspacePage.tsx) delegates to [CareerProfilePage](../../../../job-tracker-ui/src/views/CareerProfilePage.tsx). Preserve the actual Overview/Profile/Evidence/Import section owner and query/history behavior, including removal of the default overview query. Scope the residual presentation to [CareerWorkspaceOverview](../../../../job-tracker-ui/src/views/career/CareerWorkspaceOverview.tsx) and [CareerEvidenceBank](../../../../job-tracker-ui/src/views/career/CareerEvidenceBank.tsx), with existing [workspace](../../../../job-tracker-ui/src/career-workspace-page.test.tsx) and [evidence](../../../../job-tracker-ui/src/career-evidence.test.tsx) tests.
|
||||
|
||||
Keep canonical `/career/profile` data and existing server-provided completeness labels/provenance, source/raw-copy/history links, `/career/evidence` list/create and owned delete contracts. Evidence creation keeps existing category/title/statement/tags/source semantics and explicit manual review; presentation does not invent achievements or change persistence. Improve bounded headings/state/form/retry/reflow using existing owners after reproduction. No structured editor/save/import/history owner or CV renderer change belongs here.
|
||||
|
||||
Required native evidence covers populated/empty/loading/error/retry, raw-copy/source/history destinations, actual evidence create/delete confirmation/no-write cancellation/readback, long/many items and keyboard/EN/NB/theme/narrow-screen states. Canonical/sibling/frozen boundaries and foreign-owner denial remain applicable. Any concurrency/save-owner change requires the separate P7-03 disposition; unavailable human/device evidence is carried explicitly. **Implementation/results/commit/push for this second boundary are PENDING.**
|
||||
|
||||
## Preservation and exit
|
||||
|
||||
S02/C02/C04/E09 and shared E08/S03 are the finite scope in the [57 matrix](../PRESERVATION.md). Retain all [debt](../DEBT.md), established auth/private-tree behavior, original routes and semantic action boundaries. Scoped truthful loading/retry does not establish the root cause of historical Dashboard/Board500 observations.
|
||||
|
||||
Each completed logical boundary must include reproduction/baseline, actual changes, automated and visual evidence, exact count explanation, source/export identity, manual qualifications and normal commit/push with remote=HEAD. Record observed results here after execution; no skips, expected failures or relaxed assertions can substitute for passing ordinary cases. P7-03 starts only after the applicable P7-02 fixtures/entry are verified. Production main/deployment, P5-13 activation and Phase8 Dashboard customization remain outside this checkpoint.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"checkpoint": "p7-02",
|
||||
"status": "PASS",
|
||||
"checkedUtc": "2026-10-11T23:53:26.550463+00:00",
|
||||
"commands": [
|
||||
{
|
||||
"name": "secret-scan",
|
||||
"command": [
|
||||
"rtk",
|
||||
"proxy",
|
||||
"C:\\Users\\Connor\\AppData\\Local\\Programs\\Python\\Python312\\python.exe",
|
||||
"-X",
|
||||
"utf8",
|
||||
"scripts/supply-chain.py",
|
||||
"scan-secrets"
|
||||
],
|
||||
"startedUtc": "2026-10-11T23:53:23.232819+00:00",
|
||||
"finishedUtc": "2026-10-11T23:53:25.935101+00:00",
|
||||
"exitCode": 0,
|
||||
"log": ".artifacts/phase7/current/static/20261011T235323Z/secret-scan.log",
|
||||
"logSha256Raw": "c5a88d5ebdc042adba125c1eaacbe44f9f4a5c19898c0f195573c00d511aa28d"
|
||||
},
|
||||
{
|
||||
"name": "working-whitespace",
|
||||
"command": [
|
||||
"rtk",
|
||||
"proxy",
|
||||
"git",
|
||||
"diff",
|
||||
"--check"
|
||||
],
|
||||
"startedUtc": "2026-10-11T23:53:25.935603+00:00",
|
||||
"finishedUtc": "2026-10-11T23:53:26.019936+00:00",
|
||||
"exitCode": 0,
|
||||
"log": ".artifacts/phase7/current/static/20261011T235323Z/working-whitespace.log",
|
||||
"logSha256Raw": "d2993bc68ad9a63f711c8f5473155e2ba00fdcd461220cc3849185f2f9ad93e6"
|
||||
},
|
||||
{
|
||||
"name": "staged-whitespace",
|
||||
"command": [
|
||||
"rtk",
|
||||
"proxy",
|
||||
"git",
|
||||
"diff",
|
||||
"--cached",
|
||||
"--check"
|
||||
],
|
||||
"startedUtc": "2026-10-11T23:53:26.019936+00:00",
|
||||
"finishedUtc": "2026-10-11T23:53:26.117398+00:00",
|
||||
"exitCode": 0,
|
||||
"log": ".artifacts/phase7/current/static/20261011T235323Z/staged-whitespace.log",
|
||||
"logSha256Raw": "d2993bc68ad9a63f711c8f5473155e2ba00fdcd461220cc3849185f2f9ad93e6"
|
||||
},
|
||||
{
|
||||
"name": "phase7-whitespace",
|
||||
"command": [
|
||||
"rtk",
|
||||
"proxy",
|
||||
"git",
|
||||
"diff",
|
||||
"--check",
|
||||
"b5319981ad3d1bd635daff861453d23c0de5320b"
|
||||
],
|
||||
"startedUtc": "2026-10-11T23:53:26.117898+00:00",
|
||||
"finishedUtc": "2026-10-11T23:53:26.235183+00:00",
|
||||
"exitCode": 0,
|
||||
"log": ".artifacts/phase7/current/static/20261011T235323Z/phase7-whitespace.log",
|
||||
"logSha256Raw": "d2993bc68ad9a63f711c8f5473155e2ba00fdcd461220cc3849185f2f9ad93e6"
|
||||
}
|
||||
],
|
||||
"untrackedSecretScan": {
|
||||
"files": 0,
|
||||
"findings": 0
|
||||
},
|
||||
"documentation": {
|
||||
"files": 9,
|
||||
"relativeLinks": 329,
|
||||
"errors": []
|
||||
},
|
||||
"preservation": {
|
||||
"groups": 57,
|
||||
"statuses": {
|
||||
"PARTIAL": 56,
|
||||
"PASS": 0,
|
||||
"FAIL": 0,
|
||||
"DEFERRED": 1
|
||||
},
|
||||
"originalRoutesPreserved": true,
|
||||
"currentConcreteRoutes": 32
|
||||
},
|
||||
"production": {
|
||||
"main": "27657f31131cd95d42f0e1290853ec9c9b3959a4",
|
||||
"originMain": "27657f31131cd95d42f0e1290853ec9c9b3959a4",
|
||||
"status": "NO-GO",
|
||||
"qualification": "Local fetched refs; direct push observation recorded separately."
|
||||
},
|
||||
"hashPolicy": "Portable JSON UTF8 LF; raw log hashes measure exact saved bytes.",
|
||||
"limits": [
|
||||
"Pattern scanner skips binary and files over2MiB; not historical Git/production secrets.",
|
||||
"Local links check target existence, not URL/heading anchors.",
|
||||
"Static evidence is not runtime/manual/device/provider/production acceptance."
|
||||
]
|
||||
}
|
||||
@@ -83,8 +83,8 @@ Completed phase items are intentionally omitted here; their evidence remains in
|
||||
Follow the approved [Phase 7 handoff](../redesign/phase-6/p6-05/PHASE7_HANDOFF.md) and [current checkpoint ledger](../redesign/phase-7/README.md). Phase 8 implementation, P5-13 qualification/activation and production deployment remain outside this task.
|
||||
|
||||
- [x] P7-00 fresh scope inventory and unchanged-source fixture baseline; finite static checks pass. Commit and remote receipt in the Phase 7 ledger.
|
||||
- [x] P7-01 public authentication and separate account/security presentation (implemented scope CONDITIONAL GO; final hygiene/account push receipt pending): public sub-slice CONDITIONAL GO, pushed `8ad720d`, direct remote=HEAD verified2026-10-11 18:50:24UTC, main unchanged; affected61/native22/isolated SMTP2/helper6/auth subset87 pass. [Checkpoint record](../redesign/phase-7/p7-01/README.md) retains manual/provider/production gates. Fresh account full573/89, native6/public22/auth-boundaries10 and types/lint/build pass; all eight final post-copy PNGs actually inspected and source/context evidence frozen. Account85 and public31 current axe incompletes remain qualified; final finite hygiene/normal account push receipt pending, main unchanged.
|
||||
- [ ] P7-02 onboarding guidance and Career overview/evidence.
|
||||
- [x] P7-01 public authentication and separate account/security presentation (implemented scope CONDITIONAL GO; account pushedad7b419/direct receipt19:16:15UTC): public sub-slice CONDITIONAL GO, pushed `8ad720d`, direct remote=HEAD verified2026-10-11 18:50:24UTC, main unchanged; affected61/native22/isolated SMTP2/helper6/auth subset87 pass. [Checkpoint record](../redesign/phase-7/p7-01/README.md) retains manual/provider/production gates. Fresh account full573/89, native6/public22/auth-boundaries10 and types/lint/build pass; all eight final post-copy PNGs actually inspected and source/context evidence frozen. Account85 and public31 current axe incompletes remain qualified; final staged finite hygiene passed; normal account pushad7b419 directly verified2026-10-11 19:16:15UTC with localHEAD=origin=live equality and clean worktree at observation, main unchanged.
|
||||
- [ ] P7-02 [onboarding guidance and Career overview/evidence](../redesign/phase-7/p7-02/README.md): entryad7b419 pushed/verified; onboarding implemented-scope CONDITIONAL GO with fresh affected32/native13/regression4/types/lint/build and13 actual PNG inspections. Legacy dismissal/routes/data and unknown-summary state preserved; final onboarding hygiene PASS/push pending. Career overview/evidence second boundary and whole P7-02 remain pending;596 full frontend is derived only.
|
||||
- [ ] P7-03 Career structured editor and import/history presentation.
|
||||
- [ ] P7-04 CV library and public entry.
|
||||
- [ ] P7-05 Discovery, Companies, Offers and Reminders.
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
import AxeBuilder from '@axe-core/playwright';
|
||||
import { expect, test as base, type APIRequestContext, type BrowserContext, type Page, type TestInfo } from '@playwright/test';
|
||||
import { translations } from '../src/i18n/translations';
|
||||
|
||||
const apiUrl = 'http://localhost:5302/api';
|
||||
const password = 'E2ePassword123!';
|
||||
type Owner = { email: string; id: string; userName: string; headers: Record<string, string> };
|
||||
type Sample = { language: 'en' | 'nb'; theme: 'light' | 'dark'; width: number; height: number };
|
||||
|
||||
async function login(api: APIRequestContext, email: string): Promise<Owner> {
|
||||
expect((await api.post(`${apiUrl}/auth/login`, { data: { email, password } })).status()).toBe(200);
|
||||
const response = await api.get(`${apiUrl}/auth/me`);
|
||||
expect(response.status()).toBe(200);
|
||||
const me = await response.json();
|
||||
const csrf = (await api.storageState()).cookies.find(cookie => cookie.name === 'XSRF-TOKEN');
|
||||
expect(csrf).toBeDefined();
|
||||
expect(me.sessionContext).toEqual(expect.any(String));
|
||||
return { email, id: me.id, userName: me.userName, headers: {
|
||||
'X-CSRF-TOKEN': csrf!.value, 'X-Session-Context': me.sessionContext,
|
||||
} };
|
||||
}
|
||||
|
||||
async function createOwner(admin: APIRequestContext, adminHeaders: Record<string, string>, suffix: string) {
|
||||
const email = `p7-setup-${suffix}-${Date.now()}-${Math.random().toString(36).slice(2)}@example.test`;
|
||||
expect((await admin.post(`${apiUrl}/users`, { headers: adminHeaders, data: { email, password, roles: [] } })).status()).toBe(200);
|
||||
return email;
|
||||
}
|
||||
|
||||
async function localOnly(context: BrowserContext) {
|
||||
await context.route('**/*', route => {
|
||||
const url = new URL(route.request().url());
|
||||
return ['localhost', '127.0.0.1'].includes(url.hostname) || ['data:', 'blob:', 'about:'].includes(url.protocol)
|
||||
? route.continue() : route.abort();
|
||||
});
|
||||
}
|
||||
|
||||
const test = base.extend<{ owner: Owner }>({
|
||||
owner: async ({ page, context, playwright }, provide) => {
|
||||
const admin = await playwright.request.newContext();
|
||||
try {
|
||||
const adminOwner = await login(admin, 'e2e@example.test');
|
||||
const email = await createOwner(admin, adminOwner.headers, 'a');
|
||||
const owner = await login(page.request, email);
|
||||
await localOnly(context);
|
||||
await context.addInitScript(() => {
|
||||
if (!localStorage.getItem('uiLanguage')) localStorage.setItem('uiLanguage', 'en');
|
||||
if (!localStorage.getItem('jobtracker.themeMode')) localStorage.setItem('jobtracker.themeMode', 'light');
|
||||
});
|
||||
await provide(owner);
|
||||
} finally { await admin.dispose(); }
|
||||
},
|
||||
});
|
||||
|
||||
async function enter(page: Page, owner: Owner, sample: Sample) {
|
||||
expect((await page.request.put(`${apiUrl}/auth/preferences/language`, { headers: owner.headers,
|
||||
data: { language: sample.language === 'nb' ? 'nb-NO' : 'en' } } )).status()).toBe(204);
|
||||
await page.setViewportSize({ width: sample.width, height: sample.height });
|
||||
await page.goto('/dashboard');
|
||||
await expect(page.getByRole('main')).toBeVisible();
|
||||
await page.evaluate(({ language, theme }) => {
|
||||
localStorage.setItem('uiLanguage', language);
|
||||
localStorage.setItem('jobtracker.themeMode', theme);
|
||||
}, sample);
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
await page.reload();
|
||||
await expect(page.locator('html')).toHaveAttribute('lang', sample.language === 'nb' ? 'nb-NO' : 'en');
|
||||
await expect(page.locator('html')).toHaveAttribute('data-color-scheme', sample.theme);
|
||||
}
|
||||
|
||||
async function capture(page: Page, info: TestInfo, sample: Sample, state: string) {
|
||||
await page.evaluate(async () => { await document.fonts.ready; await new Promise<void>(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))); });
|
||||
const geometry = await page.evaluate(() => ({
|
||||
rootOverflow: Math.max(0, document.documentElement.scrollWidth - document.documentElement.clientWidth),
|
||||
width: innerWidth, height: innerHeight,
|
||||
}));
|
||||
const axe = await new AxeBuilder({ page }).options({ rules: { 'target-size': { enabled: true } } }).analyze();
|
||||
const name = `p7-setup-${sample.language}-${sample.theme}-${sample.width}-${state}`;
|
||||
await info.attach(`${name}-audit`, { body: JSON.stringify({ sample, state, geometry,
|
||||
ariaSnapshot: await page.getByRole('main').ariaSnapshot(),
|
||||
axe: { scope: 'All enabled rules with explicit target-size; every violation/incomplete retained',
|
||||
violations: axe.violations, incomplete: axe.incomplete,
|
||||
passes: axe.passes.map(rule => ({ id: rule.id, nodes: rule.nodes.length })) },
|
||||
limits: ['No human screen reader, physical touch or software keyboard', 'No real mailbox/provider or production qualification', 'Finite sample, no actual browser zoom'],
|
||||
}), contentType: 'application/json' });
|
||||
await info.attach(name, { body: await page.screenshot({ fullPage: true, animations: 'disabled' }), contentType: 'image/png' });
|
||||
expect(geometry.rootOverflow).toBe(0);
|
||||
}
|
||||
|
||||
async function assertDismissedAfterSettledNavigation(page: Page, navigate: () => Promise<void>) {
|
||||
const statusPaths = ['/api/career/profile', '/api/gmail/status', '/api/microsoft-graph/status', '/api/imap/status'];
|
||||
const reads: string[] = [];
|
||||
const observe = (request: import('@playwright/test').Request) => {
|
||||
const path = new URL(request.url()).pathname;
|
||||
if (request.method() === 'GET' && statusPaths.includes(path)) reads.push(path);
|
||||
};
|
||||
page.on('request', observe);
|
||||
try {
|
||||
await navigate();
|
||||
await expect(page.getByRole('heading', { name: translations.en.dashboardStartTitle, exact: true })).toBeVisible();
|
||||
await page.evaluate(() => new Promise<void>(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))));
|
||||
await expect(page.getByRole('region', { name: translations.en.onboardingTitle, exact: true })).toHaveCount(0);
|
||||
expect(reads, 'a persisted dismissal skips checklist status reads after its private Dashboard has settled').toEqual([]);
|
||||
} finally { page.off('request', observe); }
|
||||
}
|
||||
|
||||
const matrix: Sample[] = [
|
||||
{ language: 'nb', theme: 'dark', width: 320, height: 844 },
|
||||
{ language: 'en', theme: 'light', width: 360, height: 800 },
|
||||
{ language: 'nb', theme: 'light', width: 390, height: 844 },
|
||||
{ language: 'en', theme: 'dark', width: 768, height: 1024 },
|
||||
{ language: 'nb', theme: 'dark', width: 1024, height: 768 },
|
||||
{ language: 'en', theme: 'light', width: 1280, height: 320 },
|
||||
{ language: 'nb', theme: 'light', width: 1440, height: 900 },
|
||||
{ language: 'en', theme: 'dark', width: 1920, height: 1080 },
|
||||
];
|
||||
|
||||
for (const sample of matrix) {
|
||||
test(`[P7-02 PRES-S02 C01 C02 E09] ${sample.language}/${sample.theme} ${sample.width}px setup counts saved manual career information without claiming CV import or email verification`, async ({ page, owner }, info) => {
|
||||
test.setTimeout(120_000);
|
||||
expect((await page.request.put(`${apiUrl}/auth/profile`, { headers: owner.headers, data: {
|
||||
userName: owner.userName, firstName: 'Åse', lastName: 'Oppsett', displayName: 'Åse Oppsett',
|
||||
} })).status()).toBe(204);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers,
|
||||
data: { profile: { version: '1', contact: { fullName: 'Canonical career name' }, skills: ['Manually entered C#'] } } })).status()).toBe(200);
|
||||
const company = await page.request.post(`${apiUrl}/companies`, { headers: owner.headers, data: { name: 'Oppsett AS' } });
|
||||
expect(company.status()).toBe(201);
|
||||
expect((await page.request.post(`${apiUrl}/jobapplications`, { headers: owner.headers,
|
||||
data: { companyId: (await company.json()).id, jobTitle: 'Recorded first job', status: 'Saved' } })).status()).toBe(201);
|
||||
const beforeCareer = await (await page.request.get(`${apiUrl}/career/profile`)).json();
|
||||
expect(await (await page.request.get(`${apiUrl}/cv/variants`)).json()).toEqual([]);
|
||||
await enter(page, owner, sample);
|
||||
const t = translations[sample.language];
|
||||
const region = page.getByRole('region', { name: t.onboardingTitle, exact: true });
|
||||
await expect(region.getByRole('heading', { level: 2, name: t.onboardingTitle })).toBeVisible();
|
||||
await expect(region.getByRole('progressbar', { name: t.checklistProgress.replace('{completed}', '3').replace('{total}', '4') })).toHaveAttribute('aria-valuenow', '75');
|
||||
await expect(region.getByText(t.onboardingStepCv, { exact: true })).toBeVisible();
|
||||
await expect(region.getByText(t.onboardingStepVerify, { exact: true })).toHaveCount(0);
|
||||
await expect(region.getByText(t.onboardingStepSignup, { exact: true })).toHaveCount(0);
|
||||
await expect(region.getByRole('button', { name: t.onboardingStepCvAction, exact: true })).toHaveCount(0);
|
||||
await capture(page, info, sample, 'manual-data-3-of-4');
|
||||
await page.reload();
|
||||
await expect(region.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '75');
|
||||
expect(await (await page.request.get(`${apiUrl}/career/profile`)).json()).toEqual(beforeCareer);
|
||||
expect(await (await page.request.get(`${apiUrl}/cv/variants`)).json()).toEqual([]);
|
||||
});
|
||||
}
|
||||
|
||||
test('[P7-02 PRES-S02 E09] dismissal survives reload and remains isolated across actual A/B logout and login', async ({ page, owner, playwright }) => {
|
||||
test.setTimeout(120_000);
|
||||
const sample: Sample = { language: 'en', theme: 'light', width: 390, height: 844 };
|
||||
await enter(page, owner, sample);
|
||||
const region = page.getByRole('region', { name: translations.en.onboardingTitle, exact: true });
|
||||
await expect(region.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '0');
|
||||
await region.getByRole('button', { name: translations.en.onboardingDismiss }).focus();
|
||||
await page.keyboard.press('Enter');
|
||||
await expect(region).toHaveCount(0);
|
||||
await page.keyboard.press('Tab');
|
||||
await expect(page.getByRole('button', { name: translations.en.dashboardCustomize, exact: true })).toBeFocused();
|
||||
expect(await page.evaluate(id => localStorage.getItem(`onboardingDismissed:${id}`), owner.id)).toBe('true');
|
||||
await assertDismissedAfterSettledNavigation(page, async () => { await page.reload(); });
|
||||
const admin = await playwright.request.newContext();
|
||||
let bEmail: string;
|
||||
try {
|
||||
const adminOwner = await login(admin, 'e2e@example.test');
|
||||
bEmail = await createOwner(admin, adminOwner.headers, 'b');
|
||||
} finally { await admin.dispose(); }
|
||||
async function switchTo(email: string) {
|
||||
await page.getByRole('button', { name: 'User', exact: true }).click();
|
||||
await page.getByRole('menuitem', { name: 'Sign out', exact: true }).click();
|
||||
await expect(page).toHaveURL(/\/login\?sessionChanged=1$/);
|
||||
await page.getByLabel('Username or email', { exact: true }).fill(email);
|
||||
await page.getByLabel('Current password', { exact: true }).fill(password);
|
||||
await page.getByRole('button', { name: 'Sign in', exact: true }).click();
|
||||
await expect(page).toHaveURL(/\/dashboard$/);
|
||||
await expect(page.getByRole('main')).toBeVisible();
|
||||
const response = await page.request.get(`${apiUrl}/auth/me`);
|
||||
expect(response.status()).toBe(200);
|
||||
const me = await response.json();
|
||||
expect(me.email).toBe(email);
|
||||
await expect.poll(() => page.evaluate(() => localStorage.getItem('authUserKey'))).toBe(me.id);
|
||||
await expect(page.getByRole('heading', { name: translations.en.dashboardStartTitle, exact: true })).toBeVisible();
|
||||
}
|
||||
await switchTo(bEmail!);
|
||||
await expect(region.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '0');
|
||||
await page.reload();
|
||||
await expect(region.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '0');
|
||||
await assertDismissedAfterSettledNavigation(page, async () => { await switchTo(owner.email); });
|
||||
expect(await page.evaluate(id => localStorage.getItem(`onboardingDismissed:${id}`), owner.id)).toBe('true');
|
||||
});
|
||||
|
||||
test('[P7-02 PRES-S02 E08] a failed mailbox read cannot invent setup progress and retry only reads', async ({ page, owner }, info) => {
|
||||
const sample: Sample = { language: 'nb', theme: 'dark', width: 320, height: 844 };
|
||||
let failedReads = 0;
|
||||
await page.route(`${apiUrl}/gmail/status`, route => { failedReads++; return route.fulfill({ status: 500, json: { message: 'Setup connection read unavailable' } }); });
|
||||
await enter(page, owner, sample);
|
||||
const t = translations.nb;
|
||||
const region = page.getByRole('region', { name: t.onboardingTitle, exact: true });
|
||||
await expect(region.getByRole('alert')).toBeVisible();
|
||||
await expect(region.getByRole('progressbar')).toHaveCount(0);
|
||||
await expect(region.getByRole('button', { name: t.onboardingStepEmailAction, exact: true })).toHaveCount(0);
|
||||
expect(failedReads).toBeGreaterThan(0);
|
||||
await capture(page, info, sample, 'failed-mailbox-read-unknown');
|
||||
await page.unroute(`${apiUrl}/gmail/status`);
|
||||
const writes: string[] = [];
|
||||
page.on('request', request => { if (request.url().startsWith(apiUrl) && !['GET', 'HEAD', 'OPTIONS'].includes(request.method())) writes.push(request.method() + ' ' + new URL(request.url()).pathname); });
|
||||
await region.getByRole('button', { name: t.retry, exact: true }).focus();
|
||||
await page.keyboard.press('Enter');
|
||||
await expect(region.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '0');
|
||||
await expect(region.getByRole('alert')).toHaveCount(0);
|
||||
await expect(region.getByRole('button', { name: t.onboardingDismiss, exact: true })).toBeFocused();
|
||||
expect(writes).toEqual([]);
|
||||
await capture(page, info, sample, 'read-only-retry-known');
|
||||
});
|
||||
|
||||
test('[P7-02 PRES-S02 C02 E07] keyboard setup actions retain their four destinations and browser Back context', async ({ page, owner }) => {
|
||||
test.setTimeout(120_000);
|
||||
await enter(page, owner, { language: 'en', theme: 'light', width: 390, height: 844 });
|
||||
const region = page.getByRole('region', { name: translations.en.onboardingTitle, exact: true });
|
||||
for (const [name, destination] of [
|
||||
[translations.en.onboardingStepProfileAction, '/profile'],
|
||||
[translations.en.onboardingStepCvAction, '/career'],
|
||||
[translations.en.onboardingStepEmailAction, '/settings/connected-accounts'],
|
||||
[translations.en.onboardingStepJobAction, '/jobs'],
|
||||
]) {
|
||||
await expect(region.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '0');
|
||||
await region.getByRole('button', { name, exact: true }).focus();
|
||||
await page.keyboard.press('Enter');
|
||||
await expect(page).toHaveURL(new RegExp(destination.replaceAll('/', '\\/') + '$'));
|
||||
await expect(page.getByRole('main')).toBeVisible();
|
||||
await page.goBack();
|
||||
await expect(page).toHaveURL(/\/dashboard$/);
|
||||
}
|
||||
});
|
||||
|
||||
test('[P7-02 PRES-S01 S02 E08] failed Dashboard summary keeps first-job guidance unknown until its existing read owner retries', async ({ page, owner }, info) => {
|
||||
const sample: Sample = { language: 'en', theme: 'dark', width: 390, height: 844 };
|
||||
await page.route(`${apiUrl}/jobapplications/stats`, route => route.fulfill({ status: 500, json: { message: 'Summary read unavailable' } }));
|
||||
await enter(page, owner, sample);
|
||||
const t = translations.en;
|
||||
const region = page.getByRole('region', { name: t.onboardingTitle, exact: true });
|
||||
await expect(region.getByText(t.onboardingJobStatusUnknown, { exact: true })).toBeVisible();
|
||||
await expect(region.locator('[role="progressbar"][aria-valuenow]')).toHaveCount(0);
|
||||
await expect(region.getByRole('button', { name: t.onboardingStepJobAction, exact: true })).toHaveCount(0);
|
||||
await capture(page, info, sample, 'failed-summary-unknown');
|
||||
await page.unroute(`${apiUrl}/jobapplications/stats`);
|
||||
const summaryFailure = page.getByRole('main').getByRole('alert').filter({ hasText: t.dashboardSummaryUnavailableTitle });
|
||||
await summaryFailure.getByRole('button', { name: t.retry, exact: true }).focus();
|
||||
await page.keyboard.press('Enter');
|
||||
await expect(region.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '0');
|
||||
await expect(region.getByText(t.onboardingJobStatusUnknown, { exact: true })).toHaveCount(0);
|
||||
await capture(page, info, sample, 'summary-read-recovered');
|
||||
});
|
||||
|
||||
test('[P7-02 PRES-S02 E08] recovery to four complete setup steps removes guidance while retaining the next keyboard action', async ({ page, owner }, info) => {
|
||||
expect((await page.request.put(`${apiUrl}/auth/profile`, { headers: owner.headers, data: {
|
||||
userName: owner.userName, firstName: 'Åse', lastName: 'Oppsett', displayName: 'Åse Oppsett',
|
||||
} })).status()).toBe(204);
|
||||
expect((await page.request.put(`${apiUrl}/career/profile`, { headers: owner.headers,
|
||||
data: { profile: { version: '1', skills: ['Manually entered SQL'] } } })).status()).toBe(200);
|
||||
const company = await page.request.post(`${apiUrl}/companies`, { headers: owner.headers, data: { name: 'Complete setup AS' } });
|
||||
expect(company.status()).toBe(201);
|
||||
expect((await page.request.post(`${apiUrl}/jobapplications`, { headers: owner.headers,
|
||||
data: { companyId: (await company.json()).id, jobTitle: 'Recorded job', status: 'Saved' } })).status()).toBe(201);
|
||||
// Only the stored-connection response is controlled; no mailbox grant, connection or sync is attempted.
|
||||
let failed = true;
|
||||
await page.route(`${apiUrl}/gmail/status`, route => failed
|
||||
? route.fulfill({ status: 500, json: { message: 'Connection status unavailable' } })
|
||||
: route.fulfill({ json: { connected: true } }));
|
||||
const sample: Sample = { language: 'en', theme: 'light', width: 390, height: 844 };
|
||||
await enter(page, owner, sample);
|
||||
const region = page.getByRole('region', { name: translations.en.onboardingTitle, exact: true });
|
||||
await expect(region.getByRole('alert')).toBeVisible();
|
||||
failed = false;
|
||||
const writes: string[] = [];
|
||||
page.on('request', request => { if (request.url().startsWith(apiUrl) && !['GET', 'HEAD', 'OPTIONS'].includes(request.method())) writes.push(request.method()); });
|
||||
await region.getByRole('button', { name: translations.en.retry, exact: true }).focus();
|
||||
await page.keyboard.press('Enter');
|
||||
await expect(region).toHaveCount(0);
|
||||
await page.keyboard.press('Tab');
|
||||
await expect(page.getByRole('button', { name: translations.en.dashboardCustomize, exact: true })).toBeFocused();
|
||||
expect(writes).toEqual([]);
|
||||
await capture(page, info, sample, 'four-complete-recovery-next-keyboard-action');
|
||||
});
|
||||
@@ -221,7 +221,7 @@ export default function DashboardView({ mode = "dashboard" }: { mode?: "dashboar
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "grid", gap: 3, minWidth: 0, "& p, & h2, & h3": { overflowWrap: "anywhere" } }}>
|
||||
{!reporting ? <OnboardingChecklist hasJobs={hasJobs} /> : null}
|
||||
{!reporting ? <OnboardingChecklist hasJobs={summaryReady ? hasJobs : null} /> : null}
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, alignItems: "center", flexWrap: "wrap" }}>
|
||||
<Typography variant="body2" color="text.secondary">{t(reporting ? "analyticsScope" : "dashboardSnapshotScope")}</Typography>
|
||||
{reporting ? <Button component={RouterLink} to="/dashboard">{t("goToDashboard")}</Button> : prefsButton}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Box, Button, IconButton, LinearProgress, Paper, Stack, Typography } from "@mui/material";
|
||||
import { Alert, Box, Button, IconButton, LinearProgress, Typography } from "@mui/material";
|
||||
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import RadioButtonUncheckedIcon from "@mui/icons-material/RadioButtonUnchecked";
|
||||
import { alpha, useTheme } from "@mui/material/styles";
|
||||
|
||||
import { api } from "../api";
|
||||
import { api, getApiErrorMessage } from "../api";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
import { getUserKeyFromToken } from "../themePrefs";
|
||||
import { PageSection } from "../layout/PageLayout";
|
||||
|
||||
type MeResponse = {
|
||||
email?: string | null;
|
||||
@@ -33,21 +33,32 @@ type CareerProfileResponse = {
|
||||
};
|
||||
type ConnectionStatus = { connected?: boolean };
|
||||
|
||||
export default function OnboardingChecklist({ hasJobs }: { hasJobs: boolean }) {
|
||||
const theme = useTheme();
|
||||
export default function OnboardingChecklist({ hasJobs }: { hasJobs: boolean | null }) {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useI18n();
|
||||
const dismissalKey = `onboardingDismissed:${getUserKeyFromToken()}`;
|
||||
const [dismissed, setDismissed] = useState(() => window.localStorage.getItem(dismissalKey) === "true");
|
||||
// The session boundary disposes this owner before another account is adopted.
|
||||
const [dismissalKey] = useState(() => `onboardingDismissed:${getUserKeyFromToken()}`);
|
||||
const [dismissed, setDismissed] = useState(() => {
|
||||
try { return window.localStorage.getItem(dismissalKey) === "true"; }
|
||||
catch { return false; }
|
||||
});
|
||||
const [status, setStatus] = useState<{ profile: boolean; cv: boolean; email: boolean } | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadError, setLoadError] = useState<{ cause: unknown } | null>(null);
|
||||
const [readAttempt, setReadAttempt] = useState(0);
|
||||
const dismissRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (dismissed) return;
|
||||
let active = true;
|
||||
setLoading(true);
|
||||
setLoadError(null);
|
||||
setStatus(null);
|
||||
Promise.all([
|
||||
api.get<MeResponse>("/auth/me"),
|
||||
api.get<CareerProfileResponse>("/career/profile"),
|
||||
Promise.all(["/gmail/status", "/microsoft-graph/status", "/imap/status"].map((url) =>
|
||||
api.get<ConnectionStatus>(url).then((r) => Boolean(r.data?.connected)).catch(() => false),
|
||||
api.get<ConnectionStatus>(url).then((r) => Boolean(r.data?.connected)),
|
||||
)),
|
||||
]).then(([me, career, connections]) => {
|
||||
if (!active) return;
|
||||
@@ -68,59 +79,77 @@ export default function OnboardingChecklist({ hasJobs }: { hasJobs: boolean }) {
|
||||
cv: structuredCount > 0 || Boolean(career.data?.cvText?.trim()),
|
||||
email: connections.some(Boolean),
|
||||
});
|
||||
}).catch(() => { if (active) setStatus({ profile: false, cv: false, email: false }); });
|
||||
}).catch((cause: unknown) => {
|
||||
if (active) setLoadError({ cause });
|
||||
}).finally(() => {
|
||||
if (active) setLoading(false);
|
||||
});
|
||||
return () => { active = false; };
|
||||
}, []);
|
||||
}, [dismissed, readAttempt]);
|
||||
|
||||
if (!status || dismissed) return null;
|
||||
if (dismissed) return null;
|
||||
|
||||
// These four recorded steps are guidance, not a completeness score or proof of
|
||||
// verified email, an imported CV, a rendered PDF, or a live mailbox grant.
|
||||
const steps = [
|
||||
{ done: true, label: t("onboardingStepSignup"), actionLabel: "" },
|
||||
{ done: true, label: t("onboardingStepVerify"), actionLabel: "" },
|
||||
{ done: status.profile, label: t("onboardingStepProfile"), action: () => navigate("/profile"), actionLabel: t("onboardingStepProfileAction") },
|
||||
{ done: status.cv, label: t("onboardingStepCv"), action: () => navigate("/career"), actionLabel: t("onboardingStepCvAction") },
|
||||
{ done: status.email, label: t("onboardingStepEmail"), action: () => navigate("/settings/connected-accounts"), actionLabel: t("onboardingStepEmailAction") },
|
||||
{ done: hasJobs, label: t("onboardingStepJob"), action: () => navigate("/jobs"), actionLabel: t("onboardingStepJobAction") },
|
||||
{ done: status?.profile ?? null, label: t("onboardingStepProfile"), path: "/profile", actionLabel: t("onboardingStepProfileAction") },
|
||||
{ done: status?.cv ?? null, label: t("onboardingStepCv"), path: "/career", actionLabel: t("onboardingStepCvAction") },
|
||||
{ done: status?.email ?? null, label: t("onboardingStepEmail"), path: "/settings/connected-accounts", actionLabel: t("onboardingStepEmailAction") },
|
||||
{ done: hasJobs, label: t("onboardingStepJob"), path: "/jobs", actionLabel: t("onboardingStepJobAction") },
|
||||
];
|
||||
const completed = steps.filter((step) => step.done).length;
|
||||
if (completed === steps.length) return null;
|
||||
const completed = steps.filter((step) => step.done === true).length;
|
||||
if (status && hasJobs !== null && completed === steps.length) return null;
|
||||
|
||||
return (
|
||||
<Paper sx={{ p: 2.25, mb: 2, borderRadius: 4, border: "1px solid", borderColor: alpha(theme.palette.primary.main, 0.25), background: alpha(theme.palette.primary.main, 0.04) }}>
|
||||
<Box sx={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 1 }}>
|
||||
<Typography sx={{ fontWeight: 900, mb: 0.25 }}>{t("onboardingTitle")}</Typography>
|
||||
<PageSection surface title={t("onboardingTitle")} description={t("onboardingBody")} actions={
|
||||
<IconButton
|
||||
ref={dismissRef}
|
||||
size="small"
|
||||
aria-label={t("onboardingDismiss")}
|
||||
onClick={() => {
|
||||
window.localStorage.setItem(dismissalKey, "true");
|
||||
try { window.localStorage.setItem(dismissalKey, "true"); }
|
||||
catch { /* This dismissal still applies while the page remains open. */ }
|
||||
setDismissed(true);
|
||||
}}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary" }}>{t("onboardingBody")}</Typography>
|
||||
<Typography variant="caption" sx={{ color: "text.secondary" }}>{completed} / {steps.length}</Typography>
|
||||
}>
|
||||
{loading ? <>
|
||||
<Typography role="status" variant="body2" color="text.secondary">{t("loading")}</Typography>
|
||||
<LinearProgress aria-label={t("loading")} sx={{ mt: 1.5 }} />
|
||||
</> : loadError ? (
|
||||
<Alert severity="error" sx={{ overflowWrap: "anywhere" }} action={
|
||||
<Button color="inherit" onClick={() => {
|
||||
// Retry is replaced by loading feedback; keep focus inside this setup section.
|
||||
dismissRef.current?.focus();
|
||||
setReadAttempt((attempt) => attempt + 1);
|
||||
}}>{t("retry")}</Button>
|
||||
}>{getApiErrorMessage(loadError.cause, t("onboardingLoadFailed"))}</Alert>
|
||||
) : status ? <>
|
||||
{hasJobs === null ? (
|
||||
<Typography role="status" variant="body2" color="text.secondary">{t("onboardingJobStatusUnknown")}</Typography>
|
||||
) : <Typography variant="caption" color="text.secondary">{completed} / {steps.length}</Typography>}
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={(completed / steps.length) * 100}
|
||||
aria-label={t("checklistProgress", { completed, total: steps.length })}
|
||||
sx={{ my: 1.5, borderRadius: 2 }}
|
||||
variant={hasJobs === null ? "indeterminate" : "determinate"}
|
||||
value={hasJobs === null ? undefined : (completed / steps.length) * 100}
|
||||
aria-label={hasJobs === null ? t("onboardingJobStatusUnknown") : t("checklistProgress", { completed, total: steps.length })}
|
||||
sx={{ my: 1.5 }}
|
||||
/>
|
||||
<Stack spacing={1}>
|
||||
<Box component="ul" sx={{ display: "grid", gap: 1, listStyle: "none", m: 0, p: 0, minWidth: 0 }}>
|
||||
{steps.map((step) => (
|
||||
<Box key={step.label} sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 1.5, flexWrap: "wrap" }}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
{step.done ? <CheckCircleIcon fontSize="small" color="success" /> : <RadioButtonUncheckedIcon fontSize="small" sx={{ color: "text.secondary" }} />}
|
||||
<Typography variant="body2" sx={{ fontWeight: step.done ? 400 : 700, color: step.done ? "text.secondary" : "text.primary", textDecoration: step.done ? "line-through" : "none" }}>
|
||||
<Box component="li" key={step.path} sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 1.5, flexWrap: "wrap", minWidth: 0 }}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1, minWidth: 0, flex: "1 1 12rem" }}>
|
||||
{step.done === null ? null : step.done ? <CheckCircleIcon fontSize="small" color="success" titleAccess={t("checklistProgress", { completed: 1, total: 1 })} sx={{ flexShrink: 0 }} /> : <RadioButtonUncheckedIcon fontSize="small" titleAccess={t("checklistProgress", { completed: 0, total: 1 })} sx={{ color: "text.secondary", flexShrink: 0 }} />}
|
||||
<Typography variant="body2" sx={{ fontWeight: step.done ? 400 : 700, color: step.done ? "text.secondary" : "text.primary", textDecoration: step.done ? "line-through" : "none", overflowWrap: "anywhere" }}>
|
||||
{step.label}
|
||||
</Typography>
|
||||
</Box>
|
||||
{!step.done && step.action ? <Button size="small" variant="text" onClick={step.action}>{step.actionLabel}</Button> : null}
|
||||
{step.done === false ? <Button size="small" variant="text" sx={{ maxWidth: "100%", whiteSpace: "normal", overflowWrap: "anywhere" }} onClick={() => navigate(step.path)}>{step.actionLabel}</Button> : null}
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Box>
|
||||
</> : null}
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
||||
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
||||
import { MemoryRouter, useLocation } from 'react-router-dom';
|
||||
import DashboardView from './components/DashboardView';
|
||||
import { I18nProvider } from './i18n/I18nProvider';
|
||||
import { api } from './api';
|
||||
|
||||
jest.mock('./components/OnboardingChecklist', () => () => <div>Setup retained</div>);
|
||||
jest.mock('./components/OnboardingChecklist', () => ({ hasJobs }: { hasJobs: boolean | null }) => <><div>Setup retained</div><output aria-label="Setup job status">{hasJobs === null ? 'unknown' : String(hasJobs)}</output></>);
|
||||
const mockedApi = api as jest.Mocked<typeof api>;
|
||||
const stats = { total: 9, active: 8, deleted: 1, byStatus: { Applied: 3, Interview: 2, Offer: 1, 'Faglig vurdering': 2 }, appliedLast30Days: 4 };
|
||||
const overview = { funnel: [{ label: 'Applied', count: 3 }, { label: 'Interview', count: 2 }, { label: 'Offer', count: 1 }], totalActive: 8, totalResponses: 3, responseRateBySource: [{ label: 'NAV', total: 8, responses: 3, rate: 37.5 }], topCompanies: [{ companyId: 7, company: 'Nordlys AS', count: 8, responses: 3, responseRate: 37.5 }], timeInStage: [{ stage: 'Applied', medianDays: 4, count: 3 }], salaryInsights: [{ currency: 'NOK', period: 'year', count: 2, minimum: 600000, maximum: 800000, averageMidpoint: 700000 }] };
|
||||
@@ -99,3 +99,26 @@ test('[PRES-S01] activity chart uses one shared zero-based scale and a readable
|
||||
const firstY = (path: Element) => Number(path.getAttribute('d')!.split(' ')[2]);
|
||||
expect(firstY(paths[0])).toBeLessThan(firstY(paths[1]));
|
||||
});
|
||||
|
||||
test('[P7-02 PRES-S01 S02] setup guidance treats pending and failed summaries as unknown until their owner retries', async () => {
|
||||
let resolveStats!: (response: { data: typeof stats }) => void;
|
||||
let fail = false;
|
||||
const pending = new Promise<{ data: typeof stats }>(resolve => { resolveStats = resolve; });
|
||||
mockedApi.get.mockImplementation((url: string) => url.endsWith('/stats')
|
||||
? fail ? Promise.reject(new Error('summary unavailable')) : pending
|
||||
: Promise.resolve({ data: data(url) }) as any);
|
||||
const view = open();
|
||||
expect(screen.getByLabelText('Setup job status')).toHaveTextContent('unknown');
|
||||
await screen.findByRole('region', { name: 'Application activity' });
|
||||
await act(async () => { resolveStats({ data: { ...stats, total: 0 } }); });
|
||||
await waitFor(() => expect(screen.getByLabelText('Setup job status')).toHaveTextContent('false'));
|
||||
view.unmount();
|
||||
fail = true;
|
||||
open();
|
||||
await screen.findByText('Unable to load dashboard summary');
|
||||
expect(screen.getByLabelText('Setup job status')).toHaveTextContent('unknown');
|
||||
mockedApi.get.mockImplementation((url: string) => Promise.resolve({ data: data(url) }) as any);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Retry' }));
|
||||
await waitFor(() => expect(screen.getByLabelText('Setup job status')).toHaveTextContent('true'));
|
||||
expect(mockedApi.get).toHaveBeenCalledWith('/jobapplications/stats');
|
||||
});
|
||||
|
||||
@@ -1488,17 +1488,19 @@ export const translations = {
|
||||
dashboardOverviewTitle: "Dashboard overview",
|
||||
dashboardHeroLabel: "Job search overview",
|
||||
onboardingTitle: "Get set up",
|
||||
onboardingBody: "A few steps to get the most out of Jobbjakt.",
|
||||
onboardingBody: "Setup guidance based on your saved account details, Career information, mailbox connections and tracked jobs.",
|
||||
onboardingLoadFailed: "Could not load setup status.",
|
||||
onboardingJobStatusUnknown: "Tracked-job status is unavailable. Retry the dashboard summary to check your jobs.",
|
||||
onboardingDismiss: "Dismiss setup checklist",
|
||||
onboardingStepSignup: "Create your account",
|
||||
onboardingStepVerify: "Verify your email",
|
||||
onboardingStepProfile: "Complete your profile",
|
||||
onboardingStepProfile: "Add account details",
|
||||
onboardingStepProfileAction: "Open profile",
|
||||
onboardingStepCv: "Import your CV",
|
||||
onboardingStepCvAction: "Add CV",
|
||||
onboardingStepJob: "Import your first job",
|
||||
onboardingStepCv: "Add Career information",
|
||||
onboardingStepCvAction: "Open Career Profile",
|
||||
onboardingStepJob: "Track your first job",
|
||||
onboardingStepJobAction: "Add job",
|
||||
onboardingStepEmail: "Connect your email",
|
||||
onboardingStepEmail: "Connect a mailbox",
|
||||
onboardingStepEmailAction: "Connect email",
|
||||
dashboardResponseRate: "{rate}% response rate",
|
||||
dashboardTodayTitle: "Today",
|
||||
@@ -4074,17 +4076,19 @@ export const translations = {
|
||||
dashboardOverviewTitle: "Dashboard-oversikt",
|
||||
dashboardHeroLabel: "Oversikt over jobbsøket",
|
||||
onboardingTitle: "Kom i gang",
|
||||
onboardingBody: "Noen få steg for å få mest mulig ut av Jobbjakt.",
|
||||
onboardingBody: "Veiledning basert på lagrede kontoopplysninger, karriereinformasjon, tilkoblede postkasser og stillinger du følger.",
|
||||
onboardingLoadFailed: "Kunne ikke laste oppsettstatus.",
|
||||
onboardingJobStatusUnknown: "Status for stillinger er ikke tilgjengelig. Last oversikten på dashbordet på nytt for å sjekke stillingene dine.",
|
||||
onboardingDismiss: "Skjul oppstartslisten",
|
||||
onboardingStepSignup: "Opprett kontoen din",
|
||||
onboardingStepVerify: "Bekreft e-postadressen din",
|
||||
onboardingStepProfile: "Fullfør profilen din",
|
||||
onboardingStepProfile: "Legg til kontoopplysninger",
|
||||
onboardingStepProfileAction: "Åpne profil",
|
||||
onboardingStepCv: "Importer CV-en din",
|
||||
onboardingStepCvAction: "Legg til CV",
|
||||
onboardingStepJob: "Importer din første jobb",
|
||||
onboardingStepCv: "Legg til karriereinformasjon",
|
||||
onboardingStepCvAction: "Åpne karriereprofil",
|
||||
onboardingStepJob: "Følg din første stilling",
|
||||
onboardingStepJobAction: "Legg til jobb",
|
||||
onboardingStepEmail: "Koble til e-post",
|
||||
onboardingStepEmail: "Koble til en postkasse",
|
||||
onboardingStepEmailAction: "Koble til e-post",
|
||||
dashboardResponseRate: "{rate}% svarrate",
|
||||
dashboardTodayTitle: "I dag",
|
||||
|
||||
@@ -1,36 +1,261 @@
|
||||
import React from "react";
|
||||
import "@testing-library/jest-dom";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { act, fireEvent, render, screen, within } from "@testing-library/react";
|
||||
import { MemoryRouter, useLocation } from "react-router-dom";
|
||||
|
||||
import OnboardingChecklist from "./components/OnboardingChecklist";
|
||||
import { I18nProvider } from "./i18n/I18nProvider";
|
||||
import { translations } from "./i18n/translations";
|
||||
import { api } from "./api";
|
||||
|
||||
jest.mock("./api", () => ({
|
||||
api: { get: jest.fn(), interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } } },
|
||||
getApiErrorMessage: jest.requireActual("./api").getApiErrorMessage,
|
||||
api: {
|
||||
get: jest.fn(), post: jest.fn(), put: jest.fn(), patch: jest.fn(), delete: jest.fn(),
|
||||
interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } },
|
||||
},
|
||||
}));
|
||||
|
||||
const mockedApi = api as jest.Mocked<typeof api>;
|
||||
const sources = ["/auth/me", "/career/profile", "/gmail/status", "/microsoft-graph/status", "/imap/status"];
|
||||
let account: any;
|
||||
let career: any;
|
||||
let connectedProvider: string | null;
|
||||
|
||||
function renderChecklist() {
|
||||
return render(<MemoryRouter><I18nProvider><OnboardingChecklist hasJobs={false} /></I18nProvider></MemoryRouter>);
|
||||
function sourceResponse(url: string) {
|
||||
if (url === "/auth/me") return { data: account };
|
||||
if (url === "/career/profile") return { data: career };
|
||||
if (sources.includes(url)) return { data: { connected: url === connectedProvider } };
|
||||
throw new Error(`Unexpected read: ${url}`);
|
||||
}
|
||||
|
||||
test("uses structured career data and connected inbox status for onboarding progress", async () => {
|
||||
mockedApi.get.mockImplementation((url: string) => {
|
||||
if (url === "/auth/me") return Promise.resolve({ data: { firstName: "Ada" } } as any);
|
||||
if (url === "/career/profile") return Promise.resolve({ data: { cvText: "", profile: { jobs: [{ title: "Engineer" }] } } } as any);
|
||||
return Promise.resolve({ data: { connected: url === "/gmail/status" } } as any);
|
||||
});
|
||||
function Destination() {
|
||||
return <output aria-label="Destination">{useLocation().pathname}</output>;
|
||||
}
|
||||
|
||||
renderChecklist();
|
||||
function Checklist({ hasJobs = false }: { hasJobs?: boolean | null }) {
|
||||
return <MemoryRouter><I18nProvider><OnboardingChecklist hasJobs={hasJobs} /><Destination /></I18nProvider></MemoryRouter>;
|
||||
}
|
||||
|
||||
expect(await screen.findByText("5 / 6")).toBeInTheDocument();
|
||||
expect(screen.getByRole("progressbar", { name: "5 of 6 done" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Add CV" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Open profile" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Connect email" })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Add job" })).toBeInTheDocument();
|
||||
await waitFor(() => expect(mockedApi.get).toHaveBeenCalledWith("/career/profile"));
|
||||
function expectNoWrites() {
|
||||
for (const method of [mockedApi.post, mockedApi.put, mockedApi.patch, mockedApi.delete]) expect(method).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
window.localStorage.clear();
|
||||
window.localStorage.setItem("authUserKey", "owner-a");
|
||||
account = { firstName: "Ada" };
|
||||
career = { cvText: "", profile: { jobs: [{ title: "Engineer" }] } };
|
||||
connectedProvider = "/gmail/status";
|
||||
mockedApi.get.mockImplementation((url: string) => Promise.resolve(sourceResponse(url) as any));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
window.localStorage.clear();
|
||||
});
|
||||
|
||||
test("uses structured career data and connected inbox status for four source-backed onboarding steps", async () => {
|
||||
render(<Checklist />);
|
||||
|
||||
expect(await screen.findByText("3 / 4")).toBeInTheDocument();
|
||||
expect(screen.getByRole("progressbar", { name: "3 of 4 done" })).toHaveAttribute("aria-valuenow", "75");
|
||||
const region = screen.getByRole("region", { name: translations.en.onboardingTitle });
|
||||
expect(within(region).getByRole("heading", { name: translations.en.onboardingTitle, level: 2 })).toBeInTheDocument();
|
||||
expect(within(region).getAllByRole("listitem")).toHaveLength(4);
|
||||
expect(screen.queryByText(translations.en.onboardingStepSignup)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(translations.en.onboardingStepVerify)).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: translations.en.onboardingStepCvAction })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: translations.en.onboardingStepProfileAction })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: translations.en.onboardingStepEmailAction })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: translations.en.onboardingStepJobAction })).toBeInTheDocument();
|
||||
expect(mockedApi.get.mock.calls.map(([url]) => url)).toEqual(sources);
|
||||
expectNoWrites();
|
||||
});
|
||||
|
||||
test.each(["/microsoft-graph/status", "/imap/status"])("a stored %s connection counts without connecting or syncing", async (provider) => {
|
||||
connectedProvider = provider;
|
||||
render(<Checklist />);
|
||||
expect(await screen.findByText("3 / 4")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: translations.en.onboardingStepEmailAction })).not.toBeInTheDocument();
|
||||
expect(mockedApi.get).toHaveBeenCalledTimes(5);
|
||||
expectNoWrites();
|
||||
});
|
||||
|
||||
test.each([
|
||||
["manual skills", { cvText: "", profile: { skills: ["SQL"] } }, true],
|
||||
["raw career text", { cvText: " Existing resume text ", profile: {} }, true],
|
||||
["contact only", { cvText: " ", profile: { contact: { fullName: "Career Name" } } }, false],
|
||||
])("%s preserves the existing career-content criterion without claiming a CV import", async (_name, data, done) => {
|
||||
career = data;
|
||||
render(<Checklist />);
|
||||
expect(await screen.findByText(done ? "3 / 4" : "2 / 4")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: translations.en.onboardingStepCvAction }) !== null).toBe(!done);
|
||||
expect(mockedApi.get.mock.calls.some(([url]) => String(url).includes("cv-variant") || String(url).includes("profile-cv"))).toBe(false);
|
||||
expectNoWrites();
|
||||
});
|
||||
|
||||
test("career names and account email alone do not count as account identity details", async () => {
|
||||
account = { email: "owner@example.test", firstName: " ", lastName: "", displayName: " " };
|
||||
career = { cvText: "", profile: { contact: { fullName: "Career Name" }, skills: ["SQL"] } };
|
||||
render(<Checklist />);
|
||||
expect(await screen.findByText("2 / 4")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: translations.en.onboardingStepProfileAction })).toBeInTheDocument();
|
||||
expectNoWrites();
|
||||
});
|
||||
|
||||
test("unknown job summary has indeterminate progress, then responds to known jobs without refetching sources", async () => {
|
||||
const view = render(<Checklist hasJobs={null} />);
|
||||
const unknown = await screen.findByText(translations.en.onboardingJobStatusUnknown);
|
||||
expect(unknown).toHaveAttribute("role", "status");
|
||||
expect(screen.getByRole("progressbar")).not.toHaveAttribute("aria-valuenow");
|
||||
expect(screen.queryByText(/\d \/ 4/)).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: translations.en.onboardingStepJobAction })).not.toBeInTheDocument();
|
||||
|
||||
view.rerender(<Checklist hasJobs={false} />);
|
||||
expect(await screen.findByText("3 / 4")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: translations.en.onboardingStepJobAction })).toBeInTheDocument();
|
||||
view.rerender(<Checklist hasJobs />);
|
||||
expect(screen.queryByRole("region", { name: translations.en.onboardingTitle })).not.toBeInTheDocument();
|
||||
expect(mockedApi.get).toHaveBeenCalledTimes(5);
|
||||
expectNoWrites();
|
||||
});
|
||||
|
||||
test("all four recorded steps hide the optional checklist after the sources settle", async () => {
|
||||
render(<Checklist hasJobs />);
|
||||
await act(async () => { await Promise.resolve(); await Promise.resolve(); });
|
||||
expect(screen.queryByRole("region", { name: translations.en.onboardingTitle })).not.toBeInTheDocument();
|
||||
expect(mockedApi.get).toHaveBeenCalledTimes(5);
|
||||
expectNoWrites();
|
||||
});
|
||||
|
||||
test.each(sources)("failed %s status stays unknown and retries only the same source reads", async (failedSource) => {
|
||||
let rejectFirst = true;
|
||||
mockedApi.get.mockImplementation((url: string) => url === failedSource && rejectFirst
|
||||
? Promise.reject({ response: { status: 503, data: { detail: "Setup source unavailable." } } })
|
||||
: Promise.resolve(sourceResponse(url) as any));
|
||||
render(<Checklist />);
|
||||
const alert = await screen.findByRole("alert");
|
||||
expect(alert).toHaveTextContent("Setup source unavailable.");
|
||||
expect(screen.queryByText(/\d \/ 4/)).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("progressbar")).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: translations.en.onboardingStepEmailAction })).not.toBeInTheDocument();
|
||||
expectNoWrites();
|
||||
|
||||
rejectFirst = false;
|
||||
const retry = within(alert).getByRole("button", { name: translations.en.retry });
|
||||
retry.focus();
|
||||
fireEvent.click(retry);
|
||||
expect(await screen.findByText("3 / 4")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: translations.en.onboardingDismiss })).toHaveFocus();
|
||||
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
||||
expect(mockedApi.get.mock.calls.map(([url]) => url)).toEqual([...sources, ...sources]);
|
||||
expectNoWrites();
|
||||
});
|
||||
|
||||
test.each([
|
||||
["onboardingStepProfileAction", "/profile"],
|
||||
["onboardingStepCvAction", "/career"],
|
||||
["onboardingStepEmailAction", "/settings/connected-accounts"],
|
||||
["onboardingStepJobAction", "/jobs"],
|
||||
] as const)("%s keeps its source-owned destination", async (key, destination) => {
|
||||
account = {};
|
||||
career = { cvText: "", profile: {} };
|
||||
connectedProvider = null;
|
||||
render(<Checklist />);
|
||||
await screen.findByText("0 / 4");
|
||||
fireEvent.click(screen.getByRole("button", { name: translations.en[key] }));
|
||||
expect(screen.getByLabelText("Destination")).toHaveTextContent(destination);
|
||||
expectNoWrites();
|
||||
});
|
||||
|
||||
test("a legacy owner dismissal skips all reads on mount", async () => {
|
||||
localStorage.setItem("onboardingDismissed:owner-a", "true");
|
||||
render(<Checklist />);
|
||||
await act(async () => { await Promise.resolve(); });
|
||||
expect(screen.queryByRole("region", { name: translations.en.onboardingTitle })).not.toBeInTheDocument();
|
||||
expect(mockedApi.get).not.toHaveBeenCalled();
|
||||
expectNoWrites();
|
||||
});
|
||||
|
||||
test("dismissal survives reload for its owner and stays separate from another owner's checklist and Dashboard flags", async () => {
|
||||
const flags = JSON.stringify({ cards: false, activity: true, funnel: true, companies: true, skills: true });
|
||||
localStorage.setItem("dashboardPrefs:owner-a", flags);
|
||||
const first = render(<Checklist />);
|
||||
await screen.findByText("3 / 4");
|
||||
fireEvent.click(screen.getByRole("button", { name: translations.en.onboardingDismiss }));
|
||||
expect(screen.queryByRole("region", { name: translations.en.onboardingTitle })).not.toBeInTheDocument();
|
||||
expect(localStorage.getItem("onboardingDismissed:owner-a")).toBe("true");
|
||||
expect(localStorage.getItem("dashboardPrefs:owner-a")).toBe(flags);
|
||||
first.unmount();
|
||||
|
||||
mockedApi.get.mockClear();
|
||||
const reloaded = render(<Checklist />);
|
||||
await act(async () => { await Promise.resolve(); });
|
||||
expect(mockedApi.get).not.toHaveBeenCalled();
|
||||
reloaded.unmount();
|
||||
localStorage.setItem("authUserKey", "owner-b");
|
||||
const second = render(<Checklist />);
|
||||
expect(await screen.findByText("3 / 4")).toBeInTheDocument();
|
||||
expect(localStorage.getItem("onboardingDismissed:owner-b")).toBeNull();
|
||||
second.unmount();
|
||||
|
||||
mockedApi.get.mockClear();
|
||||
localStorage.setItem("authUserKey", "owner-a");
|
||||
render(<Checklist />);
|
||||
await act(async () => { await Promise.resolve(); });
|
||||
expect(mockedApi.get).not.toHaveBeenCalled();
|
||||
expect(screen.queryByRole("region", { name: translations.en.onboardingTitle })).not.toBeInTheDocument();
|
||||
expectNoWrites();
|
||||
});
|
||||
|
||||
test("unavailable dismissal storage still permits setup and an in-memory dismissal", async () => {
|
||||
const getItem = Storage.prototype.getItem;
|
||||
const setItem = Storage.prototype.setItem;
|
||||
jest.spyOn(Storage.prototype, "getItem").mockImplementation(function (this: Storage, key: string) {
|
||||
if (key.startsWith("onboardingDismissed:")) throw new Error("Storage unavailable");
|
||||
return getItem.call(this, key);
|
||||
});
|
||||
jest.spyOn(Storage.prototype, "setItem").mockImplementation(function (this: Storage, key: string, value: string) {
|
||||
if (key.startsWith("onboardingDismissed:")) throw new Error("Storage unavailable");
|
||||
return setItem.call(this, key, value);
|
||||
});
|
||||
render(<Checklist />);
|
||||
expect(await screen.findByText("3 / 4")).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: translations.en.onboardingDismiss }));
|
||||
expect(screen.queryByRole("region", { name: translations.en.onboardingTitle })).not.toBeInTheDocument();
|
||||
expectNoWrites();
|
||||
});
|
||||
|
||||
test("loading is announced without guessed progress and dismissal prevents late failure from reopening the checklist", async () => {
|
||||
let rejectCareer!: (reason: unknown) => void;
|
||||
mockedApi.get.mockImplementation((url: string) => url === "/career/profile"
|
||||
? new Promise((_resolve, reject) => { rejectCareer = reject; })
|
||||
: Promise.resolve(sourceResponse(url) as any));
|
||||
render(<Checklist />);
|
||||
const region = screen.getByRole("region", { name: translations.en.onboardingTitle });
|
||||
expect(within(region).getByRole("status")).toHaveTextContent(translations.en.loading);
|
||||
expect(screen.getByRole("progressbar")).not.toHaveAttribute("aria-valuenow");
|
||||
expect(screen.queryByText(/\d \/ 4/)).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: translations.en.onboardingDismiss }));
|
||||
await act(async () => rejectCareer(new Error("Late failure")));
|
||||
expect(screen.queryByRole("region", { name: translations.en.onboardingTitle })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
||||
expect(mockedApi.get).toHaveBeenCalledTimes(5);
|
||||
expectNoWrites();
|
||||
});
|
||||
|
||||
test("Bokmål guidance keeps semantic list and progress names without additional source reads", async () => {
|
||||
localStorage.setItem("uiLanguage", "nb");
|
||||
render(<Checklist />);
|
||||
expect(await screen.findByText("3 / 4")).toBeInTheDocument();
|
||||
const region = screen.getByRole("region", { name: translations.nb.onboardingTitle });
|
||||
expect(within(region).getByRole("heading", { name: translations.nb.onboardingTitle, level: 2 })).toBeInTheDocument();
|
||||
expect(within(region).getAllByRole("listitem")).toHaveLength(4);
|
||||
expect(screen.getByRole("progressbar", { name: translations.nb.checklistProgress.replace("{completed}", "3").replace("{total}", "4") })).toHaveAttribute("aria-valuenow", "75");
|
||||
expect(within(region).getByText(translations.nb.onboardingStepProfile)).toBeInTheDocument();
|
||||
expect(within(region).getByText(translations.nb.onboardingStepCv)).toBeInTheDocument();
|
||||
expect(mockedApi.get).toHaveBeenCalledTimes(5);
|
||||
expectNoWrites();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user