From fcacae7384b4a38c2675e6527a33cc1d09f4b505 Mon Sep 17 00:00:00 2001 From: cesnimda Date: Mon, 12 Oct 2026 20:03:38 +0200 Subject: [PATCH] feat(ui): migrate CV library actions and recovery --- docs/redesign/phase-7/COMMITS.md | 3 + docs/redesign/phase-7/DEBT.md | 4 + docs/redesign/phase-7/PRESERVATION.md | 4 + docs/redesign/phase-7/README.md | 4 + docs/redesign/phase-7/p7-04/README.md | 14 + .../p7-04/evidence/library-results.json | 3277 +++++++++++++++++ .../p7-04/evidence/library-static-checks.json | 104 + docs/roadmap/todo.md | 2 +- job-tracker-ui/e2e/phase7-cv-library.spec.ts | 480 +++ .../src/components/CvTemplateGallery.tsx | 16 +- job-tracker-ui/src/cv-builder-page.test.tsx | 292 +- job-tracker-ui/src/i18n/translations.ts | 4 + job-tracker-ui/src/views/CvBuilderPage.tsx | 170 +- 13 files changed, 4289 insertions(+), 85 deletions(-) create mode 100644 docs/redesign/phase-7/p7-04/evidence/library-results.json create mode 100644 docs/redesign/phase-7/p7-04/evidence/library-static-checks.json create mode 100644 job-tracker-ui/e2e/phase7-cv-library.spec.ts diff --git a/docs/redesign/phase-7/COMMITS.md b/docs/redesign/phase-7/COMMITS.md index 01558ff..2ab05e1 100644 --- a/docs/redesign/phase-7/COMMITS.md +++ b/docs/redesign/phase-7/COMMITS.md @@ -35,3 +35,6 @@ Execute the exact approved P7-00…P7-12 plus optional P7-06F from [the handoff] Implemented P7-03 scope is **CONDITIONAL GO**. The normal import/history push `5d6e32358c79cc8cfe505013fc79995940079767` was directly verified at 2026-10-12 17:28:54.912933UTC: local HEAD=origin=live development, clean worktree, local/origin/live main unchanged `27657f31131cd95d42f0e1290853ec9c9b3959a4`. Fresh affected76/6, ordinary native35, backend98/seeder24, types/lint/build and staged hygiene pass; 33/54 retained final images were inspected. Import17 scans0/302, editor13 scans0/377 and Career22 scans0/99 remain qualified. RD21 concurrency/partial-write recovery, old orphan records and human/device/provider/output requirements remain open. All57 groups remain56 PARTIAL/C11 DEFERRED,32 routes retained. 648/93 frontend and233 browser are derived only; latest whole573/89 remains inherited. Production NO-GO, P5-13 withholding and Phase8 separation remain unchanged. Exact direct receipt is retained at `.artifacts/phase7/p7-03/import-history/final/push-receipt.json`; the portable results include its raw hash and observation. No runtime bytes were replaced by this metadata successor. + +| P7-03 observed push receipt / P7-04 entry | `5680c0031f377b0caaae79c0cec900f58e409886` | Records the actual preceding 5d6e3235 clean normal push, closes implemented P7-03 conditionally and establishes exact library/public entry baselines. Entry static PASS (four commands0,11docs/400 local links0errors/57groups/32routes); normal push directly verified2026-10-12 17:35:00.882169UTC: localHEAD=origin=live5680c00, clean worktree, local/origin/live main27657f3 unchanged. No product/runtime/count change | +| P7-04 CV library | Pending normal commit/push after final staged gate | [Library report](p7-04/README.md#cv-library-implemented-result), [finite results](p7-04/evidence/library-results.json): affected38/4/native8/editor4/backend72/types/lint/build PASS;19 actually reviewed PNGs;19 all-enabled scans0/64 plus12 retained editor scans0/122 with manual conditions. Current10 sources/75-file export verified against exact frozen final; +12/0 gives660/93 and +8 gives241 derived only. Public entry remains pending; no main/renderer/backend/AI activation | diff --git a/docs/redesign/phase-7/DEBT.md b/docs/redesign/phase-7/DEBT.md index b4a971c..992bcae 100644 --- a/docs/redesign/phase-7/DEBT.md +++ b/docs/redesign/phase-7/DEBT.md @@ -126,3 +126,7 @@ Production remains NO-GO until RD01–05/RD27 plus effective schema/configuratio ### Observed import/history push receipt Implemented P7-03 scope is **CONDITIONAL GO**. The normal import/history push `5d6e32358c79cc8cfe505013fc79995940079767` was directly verified at 2026-10-12 17:28:54.912933UTC: local HEAD=origin=live development, clean worktree, local/origin/live main unchanged `27657f31131cd95d42f0e1290853ec9c9b3959a4`. Fresh affected76/6, ordinary native35, backend98/seeder24, types/lint/build and staged hygiene pass; 33/54 retained final images were inspected. Import17 scans0/302, editor13 scans0/377 and Career22 scans0/99 remain qualified. RD21 concurrency/partial-write recovery, old orphan records and human/device/provider/output requirements remain open. All57 groups remain56 PARTIAL/C11 DEFERRED,32 routes retained. 648/93 frontend and233 browser are derived only; latest whole573/89 remains inherited. Production NO-GO, P5-13 withholding and Phase8 separation remain unchanged. + +### P7-04 library finite disposition + +[Library results](p7-04/evidence/library-results.json) record fresh affected38/4 (+12/0), ordinary native8, existing editor/gallery consumer4 with actual PDF200, unchanged-binary backend72 and types/lint/build PASS on _fYf-IvM_ULd3ZPdUW-8h. All19 final PNGs actually reviewed; finite root overflow0. Library19 all-enabled scans0/64 and editor12 retained-scope scans0/122 remain contextually qualified: badge/overlap/modal portals, disabled Undo/Redo wrappers and obscured print headings retain M09/M11/manual requirements. Server template names/categories remain English metadata in NB and are P7-11 localisation residual. Meaningful unit/native RED and peer fixes preserve draft/error/current-locale/managed-focus behavior; no assertion/rule waiver. Implemented library CONDITIONAL GO with final hygiene/push pending; public entry/full P7-04 pending. All57 statuses/32 routes unchanged; frontend660/93/browser241/backend1,034 derived only, full573/89/backend1,033/sidecar49 inherited until P7-12. RD21/canonical concurrency, old orphan/frozen/output/provider/human/release debt remains; P5-13 withheld/main NO-GO/Phase8 separate. diff --git a/docs/redesign/phase-7/PRESERVATION.md b/docs/redesign/phase-7/PRESERVATION.md index 4cb3cb8..20e8358 100644 --- a/docs/redesign/phase-7/PRESERVATION.md +++ b/docs/redesign/phase-7/PRESERVATION.md @@ -144,3 +144,7 @@ Complete affected migration acceptance requires applicable evidence or an explic ### Observed import/history push receipt Implemented P7-03 scope is **CONDITIONAL GO**. The normal import/history push `5d6e32358c79cc8cfe505013fc79995940079767` was directly verified at 2026-10-12 17:28:54.912933UTC: local HEAD=origin=live development, clean worktree, local/origin/live main unchanged `27657f31131cd95d42f0e1290853ec9c9b3959a4`. Fresh affected76/6, ordinary native35, backend98/seeder24, types/lint/build and staged hygiene pass; 33/54 retained final images were inspected. Import17 scans0/302, editor13 scans0/377 and Career22 scans0/99 remain qualified. RD21 concurrency/partial-write recovery, old orphan records and human/device/provider/output requirements remain open. All57 groups remain56 PARTIAL/C11 DEFERRED,32 routes retained. 648/93 frontend and233 browser are derived only; latest whole573/89 remains inherited. Production NO-GO, P5-13 withholding and Phase8 separation remain unchanged. + +### P7-04 library finite disposition + +[Library results](p7-04/evidence/library-results.json) record fresh affected38/4 (+12/0), ordinary native8, existing editor/gallery consumer4 with actual PDF200, unchanged-binary backend72 and types/lint/build PASS on _fYf-IvM_ULd3ZPdUW-8h. All19 final PNGs actually reviewed; finite root overflow0. Library19 all-enabled scans0/64 and editor12 retained-scope scans0/122 remain contextually qualified: badge/overlap/modal portals, disabled Undo/Redo wrappers and obscured print headings retain M09/M11/manual requirements. Server template names/categories remain English metadata in NB and are P7-11 localisation residual. Meaningful unit/native RED and peer fixes preserve draft/error/current-locale/managed-focus behavior; no assertion/rule waiver. Implemented library CONDITIONAL GO with final hygiene/push pending; public entry/full P7-04 pending. All57 statuses/32 routes unchanged; frontend660/93/browser241/backend1,034 derived only, full573/89/backend1,033/sidecar49 inherited until P7-12. RD21/canonical concurrency, old orphan/frozen/output/provider/human/release debt remains; P5-13 withheld/main NO-GO/Phase8 separate. diff --git a/docs/redesign/phase-7/README.md b/docs/redesign/phase-7/README.md index 1f15651..54b738a 100644 --- a/docs/redesign/phase-7/README.md +++ b/docs/redesign/phase-7/README.md @@ -52,3 +52,7 @@ Verify EN/Bokmål, light/dark, appropriate density, 320/360–390/768/1024/1280/ ### Observed import/history push receipt Implemented P7-03 scope is **CONDITIONAL GO**. The normal import/history push `5d6e32358c79cc8cfe505013fc79995940079767` was directly verified at 2026-10-12 17:28:54.912933UTC: local HEAD=origin=live development, clean worktree, local/origin/live main unchanged `27657f31131cd95d42f0e1290853ec9c9b3959a4`. Fresh affected76/6, ordinary native35, backend98/seeder24, types/lint/build and staged hygiene pass; 33/54 retained final images were inspected. Import17 scans0/302, editor13 scans0/377 and Career22 scans0/99 remain qualified. RD21 concurrency/partial-write recovery, old orphan records and human/device/provider/output requirements remain open. All57 groups remain56 PARTIAL/C11 DEFERRED,32 routes retained. 648/93 frontend and233 browser are derived only; latest whole573/89 remains inherited. Production NO-GO, P5-13 withholding and Phase8 separation remain unchanged. + +### P7-04 library finite disposition + +[Library results](p7-04/evidence/library-results.json) record fresh affected38/4 (+12/0), ordinary native8, existing editor/gallery consumer4 with actual PDF200, unchanged-binary backend72 and types/lint/build PASS on _fYf-IvM_ULd3ZPdUW-8h. All19 final PNGs actually reviewed; finite root overflow0. Library19 all-enabled scans0/64 and editor12 retained-scope scans0/122 remain contextually qualified: badge/overlap/modal portals, disabled Undo/Redo wrappers and obscured print headings retain M09/M11/manual requirements. Server template names/categories remain English metadata in NB and are P7-11 localisation residual. Meaningful unit/native RED and peer fixes preserve draft/error/current-locale/managed-focus behavior; no assertion/rule waiver. Implemented library CONDITIONAL GO with final hygiene/push pending; public entry/full P7-04 pending. All57 statuses/32 routes unchanged; frontend660/93/browser241/backend1,034 derived only, full573/89/backend1,033/sidecar49 inherited until P7-12. RD21/canonical concurrency, old orphan/frozen/output/provider/human/release debt remains; P5-13 withheld/main NO-GO/Phase8 separate. diff --git a/docs/redesign/phase-7/p7-04/README.md b/docs/redesign/phase-7/p7-04/README.md index 41b877c..67438d0 100644 --- a/docs/redesign/phase-7/p7-04/README.md +++ b/docs/redesign/phase-7/p7-04/README.md @@ -10,3 +10,17 @@ | Public entry | Previous slug HTML/error remain during the next request; hard-coded English/theme styles and no semantic page heading. Retain late-completion guard, white print canvas and existing sandbox | Slug isolation/error reset/late request/Retry; missing/revoked links; anonymous public HTML/PDF versus private numeric IDs; narrow responsive chrome, unchanged geometry and prescribed fidelity when output/preview is touched | PRES-C05/C12/E09 and affected revision/source/export contracts remain binding; all57 whole-group statuses stay56 PARTIAL/C11 DEFERRED at entry. Rename retains latest settings and expectedVersion; publication/revocation stays with the editor/server owner. No second history/renderer/editor, new lifecycle/schema, automatic model/provider action or Phase8 work. Human screen-reader/device/zoom/production font/photo/print evidence remains pending. Production remains NO-GO; main is unchanged. + +### CV library implemented result + +**Library implemented scope CONDITIONAL GO; normal push pending. Public-entry boundary remains pending.** [Final staged hygiene](evidence/library-static-checks.json) PASS at2026-10-12 18:03:12UTC: four commands0,11docs/406 local links0errors,57groups/32routes retained. [Portable finite evidence](evidence/library-results.json) freezes current source and the 75-file `_fYf-IvM_ULd3ZPdUW-8h` export separately from earlier failures;289 exact file references and current10 sources/export match. Fresh affected **38/4**, new ordinary native **8**, unchanged-binary backend **72**, and existing editor/gallery-consumer **4** pass; app/browser types, lint and production build pass. No test is skipped or expected to fail. The existing editor journeys retain actual private PDF200 and gallery/template/settings behavior; the two new 503/504 cases qualify controlled failure presentation only. + +Shared PageHeader H2 sits below the existing shell H1; cards use H3, density tokens and quiet surfaces. Shared resource feedback adds visible loading, localized error and explicit Retry without locale refetch. Pending snapshots prevent repeat mutations and protect draft/template choice; existing destructive confirmations and latest-settings/expectedVersion rename remain. Durable errors survive Cancel/reopen and locale changes; pending completion returns focus only while it remains in the managed region. The shared gallery's optional disabled prop defaults false for other consumers; unavailable Pro templates remain visible and guarded. + +Meaningful RED coverage and peer review reproduced draft/error loss, stale language and late focus theft before correction. Native candidate6/2 also found a duplicate shell/local H1, helper contrast4.48:1 and a pending scroller without a keyboard target. The final local H2, readable disabled helper and named focusable pending DialogContent correct those demonstrated barriers. Raw failed outcomes/source qualifications remain; an initial Node collection error ran0 cases and invalid MUI-exit fixtures are identified rather than called product defects. + +All **19 final PNGs were displayed and reviewed** across a finite EN/Bokmål/light/dark/320/360/390/768/1024/1280-landscape/1440/1920 sample, not the full Cartesian matrix. Long names wrap and root overflow is0. Full-page capture fixed/scroll/focus context, including off-viewport modal backdrop, is qualified. White CV thumbnails remain white in dark mode. Server-authored template names/categories remain English metadata in Bokmål samples; P7-11 must review that residual without changing stored theme IDs or print content. + +New library **19 all-enabled scans0 violations/64 incomplete occurrences** retain19 invisible-badge short-text,17 modal/menu hidden-focus and28 background overlap. Existing editor **12 retained-scope scans0/122** retain24 disabled Undo/Redo wrapper aria labels,12 invisible-badge short-text,78 overlap and8 obscured iframe-heading occurrences. Full vectors remain; neither scope establishes human screen-reader/modal/contrast acceptance. M09/M11 and all M01–M12 device/zoom/output/provider gates remain. RD21 canonical concurrency/partial-write risk and historical frozen/orphan/output debt are unchanged. + +Twelve new cases in the existing suite give **660/93 frontend derived only**; eight new ordinary browser cases give **241 derived only**. Latest whole frontend573/89, backend1,033 and sidecar49 are inherited; backend1,034 is derived. P7-12 must run fresh integrated regression. All57 statuses remain56 PARTIAL/C11 DEFERRED,32 routes retained; P5-13 stays withheld. No backend/schema/renderer/PDF geometry or Phase8 work. Production NO-GO/main unchanged. diff --git a/docs/redesign/phase-7/p7-04/evidence/library-results.json b/docs/redesign/phase-7/p7-04/evidence/library-results.json new file mode 100644 index 0000000..376f865 --- /dev/null +++ b/docs/redesign/phase-7/p7-04/evidence/library-results.json @@ -0,0 +1,3277 @@ +{ + "schemaVersion": 1, + "checkpoint": "P7-04", + "slice": "CV library", + "recordedUtc": "2026-10-12T18:03:01.494389+00:00", + "acceptance": "IMPLEMENTED_SCOPE_CONDITIONAL_GO_STATIC_AND_NORMAL_PUSH_PENDING", + "entryReceipt": { + "observedUtc": "2026-10-12T17:35:00.882169+00:00", + "branch": "codex/ui-redesign-phase4", + "head": "5680c0031f377b0caaae79c0cec900f58e409886", + "originDevelopment": "5680c0031f377b0caaae79c0cec900f58e409886", + "liveDevelopment": "5680c0031f377b0caaae79c0cec900f58e409886", + "main": "27657f31131cd95d42f0e1290853ec9c9b3959a4", + "originMain": "27657f31131cd95d42f0e1290853ec9c9b3959a4", + "liveMain": "27657f31131cd95d42f0e1290853ec9c9b3959a4", + "workingTreeClean": true, + "pendingPaths": [], + "commands": [ + { + "command": [ + "rtk", + "proxy", + "git", + "branch", + "--show-current" + ], + "exitCode": 0, + "stdout": "codex/ui-redesign-phase4\n", + "stderr": "[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n" + }, + { + "command": [ + "rtk", + "proxy", + "git", + "rev-parse", + "HEAD" + ], + "exitCode": 0, + "stdout": "5680c0031f377b0caaae79c0cec900f58e409886\n", + "stderr": "[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n" + }, + { + "command": [ + "rtk", + "proxy", + "git", + "rev-parse", + "origin/codex/ui-redesign-phase4" + ], + "exitCode": 0, + "stdout": "5680c0031f377b0caaae79c0cec900f58e409886\n", + "stderr": "[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n" + }, + { + "command": [ + "rtk", + "proxy", + "git", + "rev-parse", + "main" + ], + "exitCode": 0, + "stdout": "27657f31131cd95d42f0e1290853ec9c9b3959a4\n", + "stderr": "[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n" + }, + { + "command": [ + "rtk", + "proxy", + "git", + "rev-parse", + "origin/main" + ], + "exitCode": 0, + "stdout": "27657f31131cd95d42f0e1290853ec9c9b3959a4\n", + "stderr": "[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n" + }, + { + "command": [ + "rtk", + "proxy", + "git", + "ls-remote", + "origin", + "refs/heads/codex/ui-redesign-phase4", + "refs/heads/main" + ], + "exitCode": 0, + "stdout": "5680c0031f377b0caaae79c0cec900f58e409886\trefs/heads/codex/ui-redesign-phase4\n27657f31131cd95d42f0e1290853ec9c9b3959a4\trefs/heads/main\n", + "stderr": "[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n" + }, + { + "command": [ + "rtk", + "proxy", + "git", + "status", + "--porcelain=v1" + ], + "exitCode": 0, + "stdout": "", + "stderr": "[rtk] /!\\ No hook installed — run `rtk init -g` for automatic token savings\n" + } + ] + }, + "immutableFinalCapsule": { + "path": ".artifacts/phase7/p7-04/library/final/results.json", + "sha256": "23b7cbb0b173201315735881833fc16b096fb65f0d7ff263057f83e28422b7fb", + "bytes": 112185, + "hashPolicy": "Exact raw bytes" + }, + "immutableRegressionCapsule": { + "path": ".artifacts/phase7/p7-04/library/final-regression/results.json", + "sha256": "1589af3a3a855394302965fe0d90212417e2aa07e1becac3ea3c8509ddada3b1", + "bytes": 49801, + "hashPolicy": "Exact raw bytes" + }, + "verifiedFileReferences": 289, + "currentSourceMatchesFinalLfSnapshots": true, + "currentExportMatchesMeasuredFinal": true, + "units": [ + { + "raw": { + "path": ".artifacts/phase7/p7-04/library/final/unit/p704-library-peer-green-20261012T175127Z.json", + "sha256": "97b7f6aba13d80ea080b1ca5409231fed7d4d925d7512d149866a552a717427f", + "bytes": 16167, + "hashPolicy": "Exact measured raw bytes" + }, + "startedUnixMs": 1791827488678, + "passed": 38, + "failed": 0, + "pending": 0, + "total": 38, + "suites": 4, + "cases": [ + { + "name": "the CV library has a section heading and announces its held initial read without showing an empty result", + "status": "passed" + }, + { + "name": "a failed CV list read has an explicit retry that clears the stale failure after recovery", + "status": "passed" + }, + { + "name": "a live language change retains an open CV name and template draft without refetching library data", + "status": "passed" + }, + { + "name": "held CV creation locks the draft and gallery, then keeps a failed draft for an explicit retry", + "status": "passed" + }, + { + "name": "a locked CV template remains identifiable and cannot change the creation request", + "status": "passed" + }, + { + "name": "a held duplicate blocks another action for the same CV and shows the persisted refreshed copy", + "status": "passed" + }, + { + "name": "rename reads the current revision and preserves its settings when saving the new name", + "status": "passed" + }, + { + "name": "a rename conflict keeps the original library entry and never retries the write automatically", + "status": "passed" + }, + { + "name": "cancelling deletion returns focus to the surviving CV action control without removing the CV", + "status": "passed" + }, + { + "name": "a failed creation keeps its draft and translates its failure after closing and reopening in another language", + "status": "passed" + }, + { + "name": "generic PDF failure follows the current language both after failure and when a held request fails", + "status": "passed" + }, + { + "name": "a held deletion preserves focus moved to another surviving control before the response arrives", + "status": "passed" + }, + { + "name": "PDF status 503 shows an actionable message without retrying", + "status": "passed" + }, + { + "name": "PDF status 504 shows an actionable message without retrying", + "status": "passed" + }, + { + "name": "lists existing CVs from the variants API", + "status": "passed" + }, + { + "name": "shows the empty state and creates a CV then navigates to the editor", + "status": "passed" + }, + { + "name": "CV dashboard and create flow render in Bokmål", + "status": "passed" + }, + { + "name": "CV deletion requires confirmation and supports cancellation", + "status": "passed" + }, + { + "name": "[PRES-C08 C09 E08] common controls and advanced disclosure do not mutate document settings", + "status": "passed" + }, + { + "name": "[PRES-C09 C10] numeric edits reject invalid ranges, keep exact valid values and reset only one setting", + "status": "passed" + }, + { + "name": "[PRES-C07 C08 C09] capability filtering preserves dormant saved values and exposes inherited sidebar controls", + "status": "passed" + }, + { + "name": "[PRES-C08 C10 E09] locks prevent selection and confirmed template resets preserve content and document metadata", + "status": "passed" + }, + { + "name": "[PRES-C08 C09 E08] Bokmål controls expose their scope, values and non-colour selection state", + "status": "passed" + }, + { + "name": "[PRES-C08 C09] unsupported and legacy saved values survive unrelated edits without implicit normalization", + "status": "passed" + }, + { + "name": "template thumbnails stay distinct and never replace themselves with sandboxed frames", + "status": "passed" + }, + { + "name": "moveItem moves an item forward", + "status": "passed" + }, + { + "name": "moveItem moves an item backward", + "status": "passed" + }, + { + "name": "moveItem no-op for equal or out-of-range indices", + "status": "passed" + }, + { + "name": "moveItem does not mutate the input", + "status": "passed" + }, + { + "name": "wrapSelection wraps a selection and keeps the selection over the inner text", + "status": "passed" + }, + { + "name": "wrapSelection inserts a placeholder when nothing is selected", + "status": "passed" + }, + { + "name": "wrapSelection wraps a link with a url suffix", + "status": "passed" + }, + { + "name": "CV page measurement uses the selected physical page size", + "status": "passed" + }, + { + "name": "CV page measurement rounds layout pixels without hiding partial pages", + "status": "passed" + }, + { + "name": "CV language compatibility maps legacy Norwegian values to the supported Bokmål locale", + "status": "passed" + }, + { + "name": "CV language compatibility fits the physical page to the available preview width", + "status": "passed" + }, + { + "name": "CV template selection restores the selected template visual defaults without discarding user content", + "status": "passed" + }, + { + "name": "CV template selection shows ATS templates first and keeps an active non-ATS template visible", + "status": "passed" + } + ], + "commandRaw": { + "path": ".artifacts/phase7/p7-04/library/final/unit/p704-library-peer-green-20261012T175127Z-command.json", + "sha256": "81cfd6c83e843d2ae689fbb19daa2409306980004221264a4e92c9d9dc052d13", + "bytes": 761, + "hashPolicy": "Exact measured raw bytes" + }, + "logRaw": { + "path": ".artifacts/phase7/p7-04/library/final/unit/p704-library-peer-green-20261012T175127Z.log", + "sha256": "4fcc7a7c80a3ff900ee11cd2f307b5d491787b9636ab1b0bf029c84d0e0970cd", + "bytes": 763, + "hashPolicy": "Exact measured raw bytes" + }, + "qualification": "Raw executed outcome; source bytes are not inferred retroactively. Passing subset is not slice acceptance." + } + ], + "unitHistory": [ + { + "name": "p704-inherited-library-20261012T173624Z", + "passed": 26, + "failed": 0, + "total": 26, + "raw": [ + { + "path": ".artifacts/phase7/p7-04/library/unit-history/p704-inherited-library-20261012T173624Z.json", + "sha256": "6de07bcfb58ac2f2322da1d1d1e609d1df6614c33f07545fde78e73738b59c0f", + "bytes": 11122, + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/p7-04/library/unit-history/p704-inherited-library-20261012T173624Z.log", + "sha256": "c53a4db8794b2e52f828822ecaf86ee5f790bc4e08e1544969edab07baedb6ff", + "bytes": 760, + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/p7-04/library/unit-history/p704-inherited-library-20261012T173624Z-command.json", + "sha256": "1c6fb27ee2c00cf4d2654446200c67a7b51dd0dd209c0f4ed1bde0571089369e", + "bytes": 760, + "hashPolicy": "Exact raw bytes" + } + ] + }, + { + "name": "p704-library-candidate-20261012T174038Z", + "passed": 32, + "failed": 3, + "total": 35, + "raw": [ + { + "path": ".artifacts/phase7/p7-04/library/unit-history/p704-library-candidate-20261012T174038Z.json", + "sha256": "13a3c129b964f067bc3e9a6d372e1a8b58f56a1af078be1aa450af764d886436", + "bytes": 84682, + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/p7-04/library/unit-history/p704-library-candidate-20261012T174038Z.log", + "sha256": "e274b964146e3bc7ec9f6a99499d1dfed77cadcd5e88e22e9becc64ae5980133", + "bytes": 74489, + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/p7-04/library/unit-history/p704-library-candidate-20261012T174038Z-command.json", + "sha256": "606019f19be861fdb65985dec14cf5c4a795419c35fe1cff74bc2ce553e40784", + "bytes": 760, + "hashPolicy": "Exact raw bytes" + } + ] + }, + { + "name": "p704-library-focus-candidate-20261012T174320Z", + "passed": 35, + "failed": 0, + "total": 35, + "raw": [ + { + "path": ".artifacts/phase7/p7-04/library/unit-history/p704-library-focus-candidate-20261012T174320Z.json", + "sha256": "ace46d936f115abd7247b0240d006b6efde1cb198eec397206ed30dc6c7834a7", + "bytes": 14863, + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/p7-04/library/unit-history/p704-library-focus-candidate-20261012T174320Z.log", + "sha256": "e15743d60103125f7137a5ced715e575c3c4e23b8b55e8c874838ab7c101dbd8", + "bytes": 788, + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/p7-04/library/unit-history/p704-library-focus-candidate-20261012T174320Z-command.json", + "sha256": "031edf1224fd83fff5ba597eb48e8f1b61630aedec1d609e5a59b5a64ef9065a", + "bytes": 766, + "hashPolicy": "Exact raw bytes" + } + ] + }, + { + "name": "p704-library-peer-green-20261012T175127Z", + "passed": 38, + "failed": 0, + "total": 38, + "raw": [ + { + "path": ".artifacts/phase7/p7-04/library/unit-history/p704-library-peer-green-20261012T175127Z.json", + "sha256": "97b7f6aba13d80ea080b1ca5409231fed7d4d925d7512d149866a552a717427f", + "bytes": 16167, + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/p7-04/library/unit-history/p704-library-peer-green-20261012T175127Z.log", + "sha256": "4fcc7a7c80a3ff900ee11cd2f307b5d491787b9636ab1b0bf029c84d0e0970cd", + "bytes": 763, + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/p7-04/library/unit-history/p704-library-peer-green-20261012T175127Z-command.json", + "sha256": "81cfd6c83e843d2ae689fbb19daa2409306980004221264a4e92c9d9dc052d13", + "bytes": 761, + "hashPolicy": "Exact raw bytes" + } + ] + }, + { + "name": "p704-library-peer-red-20261012-20261012T175004Z", + "passed": 15, + "failed": 3, + "total": 18, + "raw": [ + { + "path": ".artifacts/phase7/p7-04/library/unit-history/p704-library-peer-red-20261012-20261012T175004Z.json", + "sha256": "aec38ff3bda49a176ef9ad70632d7e22bf59054e21b14d1e7e0fc5f71eb08695", + "bytes": 52955, + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/p7-04/library/unit-history/p704-library-peer-red-20261012-20261012T175004Z.log", + "sha256": "c3f096e0ab4b0b59cf5587e5dbec4ba0892e91300b27825e103879925f6ce405", + "bytes": 89942, + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/p7-04/library/unit-history/p704-library-peer-red-20261012-20261012T175004Z-command.json", + "sha256": "49db50ffaaf6e3a0deedd23c4e4cac0c449ecc81616feea3408a99cdb3a00a07", + "bytes": 653, + "hashPolicy": "Exact raw bytes" + } + ] + }, + { + "name": "p704-library-red-20261012-20261012T173809Z", + "passed": 8, + "failed": 7, + "total": 15, + "raw": [ + { + "path": ".artifacts/phase7/p7-04/library/unit-history/p704-library-red-20261012-20261012T173809Z.json", + "sha256": "d32e86cf422c0739fba1de7f520c5c42486fbda4be6ebb9c0063eaab2b9875fe", + "bytes": 146547, + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/p7-04/library/unit-history/p704-library-red-20261012-20261012T173809Z.log", + "sha256": "b16839a1f76e5dac81328289bd66623bb15052038d054b3c3c5dc8d1c8207544", + "bytes": 113441, + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/p7-04/library/unit-history/p704-library-red-20261012-20261012T173809Z-command.json", + "sha256": "201dfb0cf6745a194124713c740383148068c739aa0f7fa789e0d292b0c37fd9", + "bytes": 647, + "hashPolicy": "Exact raw bytes" + } + ] + }, + { + "name": "p704-library-red-corrected-20261012-20261012T173844Z", + "passed": 9, + "failed": 6, + "total": 15, + "raw": [ + { + "path": ".artifacts/phase7/p7-04/library/unit-history/p704-library-red-corrected-20261012-20261012T173844Z.json", + "sha256": "f681da40d80d37b7a03ee2d9676b0632474a51ca69a4884664bcf5fb2dfded6a", + "bytes": 74036, + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/p7-04/library/unit-history/p704-library-red-corrected-20261012-20261012T173844Z.log", + "sha256": "5363480f00bbccf16f7687a434b2af663da412df07643c4fb63f1c75c15ebc55", + "bytes": 83915, + "hashPolicy": "Exact raw bytes" + }, + { + "path": ".artifacts/phase7/p7-04/library/unit-history/p704-library-red-corrected-20261012-20261012T173844Z-command.json", + "sha256": "bdf1a95c8ea2bf9efdb3c94829683412fd5836f95aba0ebd479f5af3a2cadf20", + "bytes": 657, + "hashPolicy": "Exact raw bytes" + } + ] + } + ], + "native": { + "cv-library": { + "command": { + "name": "cv-library", + "command": [ + "rtk", + "proxy", + "node", + "node_modules/@playwright/test/cli.js", + "test", + "e2e/phase7-cv-library.spec.ts", + "--retries=0", + "--reporter=list,json", + "--output=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\ui\\cv-library-results" + ], + "startedUtc": "2026-10-12T17:52:19.949542+00:00", + "finishedUtc": "2026-10-12T17:53:08.315122+00:00", + "exitCode": 0, + "stats": { + "startTime": "2026-10-12T17:52:20.473Z", + "duration": 47783.311, + "expected": 8, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + } + }, + "commandRaw": { + "path": ".artifacts/phase7/p7-04/library/final/cv-library-command.json", + "sha256": "8249dd67050896e3429fa34eb6636b3097cfd9f85dca4bf4dbd1d9d6e30c5287", + "bytes": 644, + "hashPolicy": "Exact measured raw bytes" + }, + "reportRaw": { + "path": ".artifacts/phase7/p7-04/library/final/cv-library.json", + "sha256": "39352bcabcd3789197da4efc59e4aa03e73cb31ee7d6549d58f39497c8e760b5", + "bytes": 2233932, + "hashPolicy": "Exact measured raw bytes" + }, + "logRaw": { + "path": ".artifacts/phase7/p7-04/library/final/cv-library.log", + "sha256": "75fa3dac2cae9509e7a2fca85027907a28c81562b25d1c7f0bf15e4ca0cbd3b9", + "bytes": 1898, + "hashPolicy": "Exact measured raw bytes" + }, + "stats": { + "startTime": "2026-10-12T17:52:20.473Z", + "duration": 47783.311, + "expected": 8, + "skipped": 0, + "unexpected": 0, + "flaky": 0 + }, + "cases": [ + { + "title": "[P7-04 PRES-C05 E08] failed list has an explicit keyboard retry with held progress, stable focus and actual recovery", + "file": "phase7-cv-library.spec.ts", + "line": 146, + "expectedStatus": "passed", + "project": "chromium", + "resultStatuses": [ + "passed" + ], + "retries": [ + 0 + ], + "errors": [ + [] + ], + "durationMs": 2713 + }, + { + "title": "[P7-04 PRES-C05 C08 E08] locale-retained create draft locks during held failure and retries one identical snapshot", + "file": "phase7-cv-library.spec.ts", + "line": 182, + "expectedStatus": "passed", + "project": "chromium", + "resultStatuses": [ + "passed" + ], + "retries": [ + 0 + ], + "errors": [ + [] + ], + "durationMs": 2990 + }, + { + "title": "[P7-04 PRES-C05 C07 C10 W07] keyboard rename duplicate and delete persist without changing source, settings or frozen packages", + "file": "phase7-cv-library.spec.ts", + "line": 260, + "expectedStatus": "passed", + "project": "chromium", + "resultStatuses": [ + "passed" + ], + "retries": [ + 0 + ], + "errors": [ + [] + ], + "durationMs": 4574 + }, + { + "title": "[P7-04 PRES-C05 C10] concurrent rename receives ordinary409 and requires owner review before a new save", + "file": "phase7-cv-library.spec.ts", + "line": 326, + "expectedStatus": "passed", + "project": "chromium", + "resultStatuses": [ + "passed" + ], + "retries": [ + 0 + ], + "errors": [ + [] + ], + "durationMs": 2720 + }, + { + "title": "[P7-04 PRES-C05 C08 C12 A02] free gallery, private ownership and revoked public links retain server admission boundaries", + "file": "phase7-cv-library.spec.ts", + "line": 367, + "expectedStatus": "passed", + "project": "chromium", + "resultStatuses": [ + "passed" + ], + "retries": [ + 0 + ], + "errors": [ + [] + ], + "durationMs": 2685 + }, + { + "title": "[P7-04 PRES-C05 C12 E08] private PDF503 feedback does not retry or change the CV", + "file": "phase7-cv-library.spec.ts", + "line": 424, + "expectedStatus": "passed", + "project": "chromium", + "resultStatuses": [ + "passed" + ], + "retries": [ + 0 + ], + "errors": [ + [] + ], + "durationMs": 1239 + }, + { + "title": "[P7-04 PRES-C05 C12 E08] private PDF504 feedback does not retry or change the CV", + "file": "phase7-cv-library.spec.ts", + "line": 424, + "expectedStatus": "passed", + "project": "chromium", + "resultStatuses": [ + "passed" + ], + "retries": [ + 0 + ], + "errors": [ + [] + ], + "durationMs": 1226 + }, + { + "title": "[P7-04 PRES-C05 C08 E08] finite library and gallery width locale theme matrix keeps actions and real data reachable", + "file": "phase7-cv-library.spec.ts", + "line": 443, + "expectedStatus": "passed", + "project": "chromium", + "resultStatuses": [ + "passed" + ], + "retries": [ + 0 + ], + "errors": [ + [] + ], + "durationMs": 22239 + } + ], + "reportErrors": [], + "configuration": { + "version": "1.62.1", + "workers": 1, + "projects": [ + { + "name": "chromium", + "retries": 0 + } + ] + }, + "audit": { + "scans": [ + { + "test": "[P7-04 PRES-C05 E08] failed list has an explicit keyboard retry with held progress, stable focus and actual recovery", + "attachment": "p7-cv-library-en-light-1440-list-recovered-audit", + "geometry": { + "width": 1440, + "height": 900, + "rootOverflow": 0, + "overflowElements": [] + }, + "engine": null, + "toolOptions": null, + "violationNodes": 0, + "incompleteNodes": 1, + "raw": { + "path": ".artifacts/phase7/p7-04/library/final/embedded/cv-library/001-p7-cv-library-en-light-1440-list-recovered-audit.json", + "sha256": "0a8a25fe99373e3391347a288bfba03ea2be2a2d031dbdc67f301918537df959", + "bytes": 5700, + "hashPolicy": "Exact measured raw bytes" + }, + "originalEmbeddedReport": { + "path": ".artifacts/phase7/p7-04/library/final/cv-library.json", + "sha256": "39352bcabcd3789197da4efc59e4aa03e73cb31ee7d6549d58f39497c8e760b5", + "bytes": 2233932, + "hashPolicy": "Exact measured raw bytes" + }, + "context": { + "sample": { + "language": "en", + "theme": "light", + "width": 1440, + "height": 900 + }, + "state": "list-recovered", + "qualifications": [ + "Automated Chromium only; incomplete axe findings require triage and do not establish human screen-reader acceptance.", + "No physical touch/software keyboard, actual browser zoom, production font/photo, print/PDF fidelity, model/provider or production acceptance." + ] + }, + "rules": [ + { + "kind": "incomplete", + "id": "color-contrast", + "nodes": 1 + } + ] + }, + { + "test": "[P7-04 PRES-C05 C08 E08] locale-retained create draft locks during held failure and retries one identical snapshot", + "attachment": "p7-cv-library-nb-light-360-held-create-audit", + "geometry": { + "width": 360, + "height": 844, + "rootOverflow": 0, + "overflowElements": [] + }, + "engine": null, + "toolOptions": null, + "violationNodes": 0, + "incompleteNodes": 4, + "raw": { + "path": ".artifacts/phase7/p7-04/library/final/embedded/cv-library/005-p7-cv-library-nb-light-360-held-create-audit.json", + "sha256": "73423deec44767a89ebf7bb23e44903775bc0fb33b8d2d8a45b4a3faedd63ded", + "bytes": 8519, + "hashPolicy": "Exact measured raw bytes" + }, + "originalEmbeddedReport": { + "path": ".artifacts/phase7/p7-04/library/final/cv-library.json", + "sha256": "39352bcabcd3789197da4efc59e4aa03e73cb31ee7d6549d58f39497c8e760b5", + "bytes": 2233932, + "hashPolicy": "Exact measured raw bytes" + }, + "context": { + "sample": { + "language": "nb", + "theme": "light", + "width": 360, + "height": 844 + }, + "state": "held-create", + "qualifications": [ + "Automated Chromium only; incomplete axe findings require triage and do not establish human screen-reader acceptance.", + "No physical touch/software keyboard, actual browser zoom, production font/photo, print/PDF fidelity, model/provider or production acceptance." + ] + }, + "rules": [ + { + "kind": "incomplete", + "id": "aria-hidden-focus", + "nodes": 1 + }, + { + "kind": "incomplete", + "id": "color-contrast", + "nodes": 3 + } + ] + }, + { + "test": "[P7-04 PRES-C05 C07 C10 W07] keyboard rename duplicate and delete persist without changing source, settings or frozen packages", + "attachment": "p7-cv-library-en-light-1440-crud-reloaded-audit", + "geometry": { + "width": 1440, + "height": 900, + "rootOverflow": 0, + "overflowElements": [] + }, + "engine": null, + "toolOptions": null, + "violationNodes": 0, + "incompleteNodes": 1, + "raw": { + "path": ".artifacts/phase7/p7-04/library/final/embedded/cv-library/008-p7-cv-library-en-light-1440-crud-reloaded-audit.json", + "sha256": "9f7d5e98dadacbec12d83de00fd1fd538f7458d55b3719ac01c88a5d3b8b3cbe", + "bytes": 5838, + "hashPolicy": "Exact measured raw bytes" + }, + "originalEmbeddedReport": { + "path": ".artifacts/phase7/p7-04/library/final/cv-library.json", + "sha256": "39352bcabcd3789197da4efc59e4aa03e73cb31ee7d6549d58f39497c8e760b5", + "bytes": 2233932, + "hashPolicy": "Exact measured raw bytes" + }, + "context": { + "sample": { + "language": "en", + "theme": "light", + "width": 1440, + "height": 900 + }, + "state": "crud-reloaded", + "qualifications": [ + "Automated Chromium only; incomplete axe findings require triage and do not establish human screen-reader acceptance.", + "No physical touch/software keyboard, actual browser zoom, production font/photo, print/PDF fidelity, model/provider or production acceptance." + ] + }, + "rules": [ + { + "kind": "incomplete", + "id": "color-contrast", + "nodes": 1 + } + ] + }, + { + "test": "[P7-04 PRES-C05 C08 E08] finite library and gallery width locale theme matrix keeps actions and real data reachable", + "attachment": "p7-cv-library-nb-light-320-library-audit", + "geometry": { + "width": 320, + "height": 844, + "rootOverflow": 0, + "overflowElements": [] + }, + "engine": null, + "toolOptions": null, + "violationNodes": 0, + "incompleteNodes": 2, + "raw": { + "path": ".artifacts/phase7/p7-04/library/final/embedded/cv-library/015-p7-cv-library-nb-light-320-library-audit.json", + "sha256": "6429043f2d291a864ab915277173ff87711de6a4eec1315aa2d8e104c56f1968", + "bytes": 6442, + "hashPolicy": "Exact measured raw bytes" + }, + "originalEmbeddedReport": { + "path": ".artifacts/phase7/p7-04/library/final/cv-library.json", + "sha256": "39352bcabcd3789197da4efc59e4aa03e73cb31ee7d6549d58f39497c8e760b5", + "bytes": 2233932, + "hashPolicy": "Exact measured raw bytes" + }, + "context": { + "sample": { + "language": "nb", + "theme": "light", + "width": 320, + "height": 844 + }, + "state": "library", + "qualifications": [ + "Automated Chromium only; incomplete axe findings require triage and do not establish human screen-reader acceptance.", + "No physical touch/software keyboard, actual browser zoom, production font/photo, print/PDF fidelity, model/provider or production acceptance." + ] + }, + "rules": [ + { + "kind": "incomplete", + "id": "aria-hidden-focus", + "nodes": 1 + }, + { + "kind": "incomplete", + "id": "color-contrast", + "nodes": 1 + } + ] + }, + { + "test": "[P7-04 PRES-C05 C08 E08] finite library and gallery width locale theme matrix keeps actions and real data reachable", + "attachment": "p7-cv-library-nb-light-320-gallery-audit", + "geometry": { + "width": 320, + "height": 844, + "rootOverflow": 0, + "overflowElements": [] + }, + "engine": null, + "toolOptions": null, + "violationNodes": 0, + "incompleteNodes": 6, + "raw": { + "path": ".artifacts/phase7/p7-04/library/final/embedded/cv-library/017-p7-cv-library-nb-light-320-gallery-audit.json", + "sha256": "12968f0e66802978c917add6cb6241ec246a6e5e303b0aae7b3e320d5c153b2f", + "bytes": 11455, + "hashPolicy": "Exact measured raw bytes" + }, + "originalEmbeddedReport": { + "path": ".artifacts/phase7/p7-04/library/final/cv-library.json", + "sha256": "39352bcabcd3789197da4efc59e4aa03e73cb31ee7d6549d58f39497c8e760b5", + "bytes": 2233932, + "hashPolicy": "Exact measured raw bytes" + }, + "context": { + "sample": { + "language": "nb", + "theme": "light", + "width": 320, + "height": 844 + }, + "state": "gallery", + "qualifications": [ + "Automated Chromium only; incomplete axe findings require triage and do not establish human screen-reader acceptance.", + "No physical touch/software keyboard, actual browser zoom, production font/photo, print/PDF fidelity, model/provider or production acceptance." + ] + }, + "rules": [ + { + "kind": "incomplete", + "id": "aria-hidden-focus", + "nodes": 1 + }, + { + "kind": "incomplete", + "id": "color-contrast", + "nodes": 5 + } + ] + }, + { + "test": "[P7-04 PRES-C05 C08 E08] finite library and gallery width locale theme matrix keeps actions and real data reachable", + "attachment": "p7-cv-library-en-dark-360-library-audit", + "geometry": { + "width": 360, + "height": 844, + "rootOverflow": 0, + "overflowElements": [] + }, + "engine": null, + "toolOptions": null, + "violationNodes": 0, + "incompleteNodes": 2, + "raw": { + "path": ".artifacts/phase7/p7-04/library/final/embedded/cv-library/019-p7-cv-library-en-dark-360-library-audit.json", + "sha256": "e4e2bbf2e0f1b798653ce029f8f582b2d96518b35b5db49a95672d7e5f07d6e6", + "bytes": 6426, + "hashPolicy": "Exact measured raw bytes" + }, + "originalEmbeddedReport": { + "path": ".artifacts/phase7/p7-04/library/final/cv-library.json", + "sha256": "39352bcabcd3789197da4efc59e4aa03e73cb31ee7d6549d58f39497c8e760b5", + "bytes": 2233932, + "hashPolicy": "Exact measured raw bytes" + }, + "context": { + "sample": { + "language": "en", + "theme": "dark", + "width": 360, + "height": 844 + }, + "state": "library", + "qualifications": [ + "Automated Chromium only; incomplete axe findings require triage and do not establish human screen-reader acceptance.", + "No physical touch/software keyboard, actual browser zoom, production font/photo, print/PDF fidelity, model/provider or production acceptance." + ] + }, + "rules": [ + { + "kind": "incomplete", + "id": "aria-hidden-focus", + "nodes": 1 + }, + { + "kind": "incomplete", + "id": "color-contrast", + "nodes": 1 + } + ] + }, + { + "test": "[P7-04 PRES-C05 C08 E08] finite library and gallery width locale theme matrix keeps actions and real data reachable", + "attachment": "p7-cv-library-en-dark-360-gallery-audit", + "geometry": { + "width": 360, + "height": 844, + "rootOverflow": 0, + "overflowElements": [] + }, + "engine": null, + "toolOptions": null, + "violationNodes": 0, + "incompleteNodes": 5, + "raw": { + "path": ".artifacts/phase7/p7-04/library/final/embedded/cv-library/021-p7-cv-library-en-dark-360-gallery-audit.json", + "sha256": "e4eb7541d75b16deee440026d5f2dabea2827413f74a4ebb75caf8733c294279", + "bytes": 10681, + "hashPolicy": "Exact measured raw bytes" + }, + "originalEmbeddedReport": { + "path": ".artifacts/phase7/p7-04/library/final/cv-library.json", + "sha256": "39352bcabcd3789197da4efc59e4aa03e73cb31ee7d6549d58f39497c8e760b5", + "bytes": 2233932, + "hashPolicy": "Exact measured raw bytes" + }, + "context": { + "sample": { + "language": "en", + "theme": "dark", + "width": 360, + "height": 844 + }, + "state": "gallery", + "qualifications": [ + "Automated Chromium only; incomplete axe findings require triage and do not establish human screen-reader acceptance.", + "No physical touch/software keyboard, actual browser zoom, production font/photo, print/PDF fidelity, model/provider or production acceptance." + ] + }, + "rules": [ + { + "kind": "incomplete", + "id": "aria-hidden-focus", + "nodes": 1 + }, + { + "kind": "incomplete", + "id": "color-contrast", + "nodes": 4 + } + ] + }, + { + "test": "[P7-04 PRES-C05 C08 E08] finite library and gallery width locale theme matrix keeps actions and real data reachable", + "attachment": "p7-cv-library-nb-dark-390-library-audit", + "geometry": { + "width": 390, + "height": 844, + "rootOverflow": 0, + "overflowElements": [] + }, + "engine": null, + "toolOptions": null, + "violationNodes": 0, + "incompleteNodes": 2, + "raw": { + "path": ".artifacts/phase7/p7-04/library/final/embedded/cv-library/023-p7-cv-library-nb-dark-390-library-audit.json", + "sha256": "4e0e57b98ad9e28b0b4b059973b679c84904660eff3437ab2e42e25ecaa3a9b7", + "bytes": 6441, + "hashPolicy": "Exact measured raw bytes" + }, + "originalEmbeddedReport": { + "path": ".artifacts/phase7/p7-04/library/final/cv-library.json", + "sha256": "39352bcabcd3789197da4efc59e4aa03e73cb31ee7d6549d58f39497c8e760b5", + "bytes": 2233932, + "hashPolicy": "Exact measured raw bytes" + }, + "context": { + "sample": { + "language": "nb", + "theme": "dark", + "width": 390, + "height": 844 + }, + "state": "library", + "qualifications": [ + "Automated Chromium only; incomplete axe findings require triage and do not establish human screen-reader acceptance.", + "No physical touch/software keyboard, actual browser zoom, production font/photo, print/PDF fidelity, model/provider or production acceptance." + ] + }, + "rules": [ + { + "kind": "incomplete", + "id": "aria-hidden-focus", + "nodes": 1 + }, + { + "kind": "incomplete", + "id": "color-contrast", + "nodes": 1 + } + ] + }, + { + "test": "[P7-04 PRES-C05 C08 E08] finite library and gallery width locale theme matrix keeps actions and real data reachable", + "attachment": "p7-cv-library-nb-dark-390-gallery-audit", + "geometry": { + "width": 390, + "height": 844, + "rootOverflow": 0, + "overflowElements": [] + }, + "engine": null, + "toolOptions": null, + "violationNodes": 0, + "incompleteNodes": 6, + "raw": { + "path": ".artifacts/phase7/p7-04/library/final/embedded/cv-library/025-p7-cv-library-nb-dark-390-gallery-audit.json", + "sha256": "40ca360db3eb09a2f35b6f9e7c5872410ecff22f8de6f4ee188352e3bd1b0022", + "bytes": 11454, + "hashPolicy": "Exact measured raw bytes" + }, + "originalEmbeddedReport": { + "path": ".artifacts/phase7/p7-04/library/final/cv-library.json", + "sha256": "39352bcabcd3789197da4efc59e4aa03e73cb31ee7d6549d58f39497c8e760b5", + "bytes": 2233932, + "hashPolicy": "Exact measured raw bytes" + }, + "context": { + "sample": { + "language": "nb", + "theme": "dark", + "width": 390, + "height": 844 + }, + "state": "gallery", + "qualifications": [ + "Automated Chromium only; incomplete axe findings require triage and do not establish human screen-reader acceptance.", + "No physical touch/software keyboard, actual browser zoom, production font/photo, print/PDF fidelity, model/provider or production acceptance." + ] + }, + "rules": [ + { + "kind": "incomplete", + "id": "aria-hidden-focus", + "nodes": 1 + }, + { + "kind": "incomplete", + "id": "color-contrast", + "nodes": 5 + } + ] + }, + { + "test": "[P7-04 PRES-C05 C08 E08] finite library and gallery width locale theme matrix keeps actions and real data reachable", + "attachment": "p7-cv-library-en-light-768-library-audit", + "geometry": { + "width": 768, + "height": 1024, + "rootOverflow": 0, + "overflowElements": [] + }, + "engine": null, + "toolOptions": null, + "violationNodes": 0, + "incompleteNodes": 2, + "raw": { + "path": ".artifacts/phase7/p7-04/library/final/embedded/cv-library/027-p7-cv-library-en-light-768-library-audit.json", + "sha256": "7969fda3f423f504d88f1b4ff00ba7c03d2b4f0c57445730ff089e83c6f68963", + "bytes": 6427, + "hashPolicy": "Exact measured raw bytes" + }, + "originalEmbeddedReport": { + "path": ".artifacts/phase7/p7-04/library/final/cv-library.json", + "sha256": "39352bcabcd3789197da4efc59e4aa03e73cb31ee7d6549d58f39497c8e760b5", + "bytes": 2233932, + "hashPolicy": "Exact measured raw bytes" + }, + "context": { + "sample": { + "language": "en", + "theme": "light", + "width": 768, + "height": 1024 + }, + "state": "library", + "qualifications": [ + "Automated Chromium only; incomplete axe findings require triage and do not establish human screen-reader acceptance.", + "No physical touch/software keyboard, actual browser zoom, production font/photo, print/PDF fidelity, model/provider or production acceptance." + ] + }, + "rules": [ + { + "kind": "incomplete", + "id": "aria-hidden-focus", + "nodes": 1 + }, + { + "kind": "incomplete", + "id": "color-contrast", + "nodes": 1 + } + ] + }, + { + "test": "[P7-04 PRES-C05 C08 E08] finite library and gallery width locale theme matrix keeps actions and real data reachable", + "attachment": "p7-cv-library-en-light-768-gallery-audit", + "geometry": { + "width": 768, + "height": 1024, + "rootOverflow": 0, + "overflowElements": [] + }, + "engine": null, + "toolOptions": null, + "violationNodes": 0, + "incompleteNodes": 5, + "raw": { + "path": ".artifacts/phase7/p7-04/library/final/embedded/cv-library/029-p7-cv-library-en-light-768-gallery-audit.json", + "sha256": "9c85e2e0f6615c5139c6f4679acade4c78231f3e372d4e9f04d7507b5dd2875b", + "bytes": 10553, + "hashPolicy": "Exact measured raw bytes" + }, + "originalEmbeddedReport": { + "path": ".artifacts/phase7/p7-04/library/final/cv-library.json", + "sha256": "39352bcabcd3789197da4efc59e4aa03e73cb31ee7d6549d58f39497c8e760b5", + "bytes": 2233932, + "hashPolicy": "Exact measured raw bytes" + }, + "context": { + "sample": { + "language": "en", + "theme": "light", + "width": 768, + "height": 1024 + }, + "state": "gallery", + "qualifications": [ + "Automated Chromium only; incomplete axe findings require triage and do not establish human screen-reader acceptance.", + "No physical touch/software keyboard, actual browser zoom, production font/photo, print/PDF fidelity, model/provider or production acceptance." + ] + }, + "rules": [ + { + "kind": "incomplete", + "id": "aria-hidden-focus", + "nodes": 1 + }, + { + "kind": "incomplete", + "id": "color-contrast", + "nodes": 4 + } + ] + }, + { + "test": "[P7-04 PRES-C05 C08 E08] finite library and gallery width locale theme matrix keeps actions and real data reachable", + "attachment": "p7-cv-library-nb-light-1024-library-audit", + "geometry": { + "width": 1024, + "height": 768, + "rootOverflow": 0, + "overflowElements": [] + }, + "engine": null, + "toolOptions": null, + "violationNodes": 0, + "incompleteNodes": 2, + "raw": { + "path": ".artifacts/phase7/p7-04/library/final/embedded/cv-library/031-p7-cv-library-nb-light-1024-library-audit.json", + "sha256": "6ad850e4e8b39eb23dda6b9d04129649762237cec67ec172a3f4f09518aa3d7d", + "bytes": 7971, + "hashPolicy": "Exact measured raw bytes" + }, + "originalEmbeddedReport": { + "path": ".artifacts/phase7/p7-04/library/final/cv-library.json", + "sha256": "39352bcabcd3789197da4efc59e4aa03e73cb31ee7d6549d58f39497c8e760b5", + "bytes": 2233932, + "hashPolicy": "Exact measured raw bytes" + }, + "context": { + "sample": { + "language": "nb", + "theme": "light", + "width": 1024, + "height": 768 + }, + "state": "library", + "qualifications": [ + "Automated Chromium only; incomplete axe findings require triage and do not establish human screen-reader acceptance.", + "No physical touch/software keyboard, actual browser zoom, production font/photo, print/PDF fidelity, model/provider or production acceptance." + ] + }, + "rules": [ + { + "kind": "incomplete", + "id": "aria-hidden-focus", + "nodes": 1 + }, + { + "kind": "incomplete", + "id": "color-contrast", + "nodes": 1 + } + ] + }, + { + "test": "[P7-04 PRES-C05 C08 E08] finite library and gallery width locale theme matrix keeps actions and real data reachable", + "attachment": "p7-cv-library-nb-light-1024-gallery-audit", + "geometry": { + "width": 1024, + "height": 768, + "rootOverflow": 0, + "overflowElements": [] + }, + "engine": null, + "toolOptions": null, + "violationNodes": 0, + "incompleteNodes": 5, + "raw": { + "path": ".artifacts/phase7/p7-04/library/final/embedded/cv-library/033-p7-cv-library-nb-light-1024-gallery-audit.json", + "sha256": "2a9d31bcd7a0a18bdd197fb230fd598c2e80d93ca71eb43d25f33d4d33a32b2e", + "bytes": 18627, + "hashPolicy": "Exact measured raw bytes" + }, + "originalEmbeddedReport": { + "path": ".artifacts/phase7/p7-04/library/final/cv-library.json", + "sha256": "39352bcabcd3789197da4efc59e4aa03e73cb31ee7d6549d58f39497c8e760b5", + "bytes": 2233932, + "hashPolicy": "Exact measured raw bytes" + }, + "context": { + "sample": { + "language": "nb", + "theme": "light", + "width": 1024, + "height": 768 + }, + "state": "gallery", + "qualifications": [ + "Automated Chromium only; incomplete axe findings require triage and do not establish human screen-reader acceptance.", + "No physical touch/software keyboard, actual browser zoom, production font/photo, print/PDF fidelity, model/provider or production acceptance." + ] + }, + "rules": [ + { + "kind": "incomplete", + "id": "aria-hidden-focus", + "nodes": 1 + }, + { + "kind": "incomplete", + "id": "color-contrast", + "nodes": 4 + } + ] + }, + { + "test": "[P7-04 PRES-C05 C08 E08] finite library and gallery width locale theme matrix keeps actions and real data reachable", + "attachment": "p7-cv-library-en-dark-1280-library-audit", + "geometry": { + "width": 1280, + "height": 320, + "rootOverflow": 0, + "overflowElements": [] + }, + "engine": null, + "toolOptions": null, + "violationNodes": 0, + "incompleteNodes": 2, + "raw": { + "path": ".artifacts/phase7/p7-04/library/final/embedded/cv-library/035-p7-cv-library-en-dark-1280-library-audit.json", + "sha256": "edf599ac539fc13e7544e2d4a4c28b00ba2036195fdc1b86adf80d919dc5f622", + "bytes": 7926, + "hashPolicy": "Exact measured raw bytes" + }, + "originalEmbeddedReport": { + "path": ".artifacts/phase7/p7-04/library/final/cv-library.json", + "sha256": "39352bcabcd3789197da4efc59e4aa03e73cb31ee7d6549d58f39497c8e760b5", + "bytes": 2233932, + "hashPolicy": "Exact measured raw bytes" + }, + "context": { + "sample": { + "language": "en", + "theme": "dark", + "width": 1280, + "height": 320 + }, + "state": "library", + "qualifications": [ + "Automated Chromium only; incomplete axe findings require triage and do not establish human screen-reader acceptance.", + "No physical touch/software keyboard, actual browser zoom, production font/photo, print/PDF fidelity, model/provider or production acceptance." + ] + }, + "rules": [ + { + "kind": "incomplete", + "id": "aria-hidden-focus", + "nodes": 1 + }, + { + "kind": "incomplete", + "id": "color-contrast", + "nodes": 1 + } + ] + }, + { + "test": "[P7-04 PRES-C05 C08 E08] finite library and gallery width locale theme matrix keeps actions and real data reachable", + "attachment": "p7-cv-library-en-dark-1280-gallery-audit", + "geometry": { + "width": 1280, + "height": 320, + "rootOverflow": 0, + "overflowElements": [] + }, + "engine": null, + "toolOptions": null, + "violationNodes": 0, + "incompleteNodes": 5, + "raw": { + "path": ".artifacts/phase7/p7-04/library/final/embedded/cv-library/037-p7-cv-library-en-dark-1280-gallery-audit.json", + "sha256": "46f733316a5061dd62dbb72b861e0d0ffc0cae8932761cc906a09d34621a2084", + "bytes": 18640, + "hashPolicy": "Exact measured raw bytes" + }, + "originalEmbeddedReport": { + "path": ".artifacts/phase7/p7-04/library/final/cv-library.json", + "sha256": "39352bcabcd3789197da4efc59e4aa03e73cb31ee7d6549d58f39497c8e760b5", + "bytes": 2233932, + "hashPolicy": "Exact measured raw bytes" + }, + "context": { + "sample": { + "language": "en", + "theme": "dark", + "width": 1280, + "height": 320 + }, + "state": "gallery", + "qualifications": [ + "Automated Chromium only; incomplete axe findings require triage and do not establish human screen-reader acceptance.", + "No physical touch/software keyboard, actual browser zoom, production font/photo, print/PDF fidelity, model/provider or production acceptance." + ] + }, + "rules": [ + { + "kind": "incomplete", + "id": "aria-hidden-focus", + "nodes": 1 + }, + { + "kind": "incomplete", + "id": "color-contrast", + "nodes": 4 + } + ] + }, + { + "test": "[P7-04 PRES-C05 C08 E08] finite library and gallery width locale theme matrix keeps actions and real data reachable", + "attachment": "p7-cv-library-nb-dark-1440-library-audit", + "geometry": { + "width": 1440, + "height": 900, + "rootOverflow": 0, + "overflowElements": [] + }, + "engine": null, + "toolOptions": null, + "violationNodes": 0, + "incompleteNodes": 2, + "raw": { + "path": ".artifacts/phase7/p7-04/library/final/embedded/cv-library/039-p7-cv-library-nb-dark-1440-library-audit.json", + "sha256": "7d97a4425c42e2d9a2374b86e87290a5929a35d54e224c1f330c4e5fbdd4fbce", + "bytes": 7970, + "hashPolicy": "Exact measured raw bytes" + }, + "originalEmbeddedReport": { + "path": ".artifacts/phase7/p7-04/library/final/cv-library.json", + "sha256": "39352bcabcd3789197da4efc59e4aa03e73cb31ee7d6549d58f39497c8e760b5", + "bytes": 2233932, + "hashPolicy": "Exact measured raw bytes" + }, + "context": { + "sample": { + "language": "nb", + "theme": "dark", + "width": 1440, + "height": 900 + }, + "state": "library", + "qualifications": [ + "Automated Chromium only; incomplete axe findings require triage and do not establish human screen-reader acceptance.", + "No physical touch/software keyboard, actual browser zoom, production font/photo, print/PDF fidelity, model/provider or production acceptance." + ] + }, + "rules": [ + { + "kind": "incomplete", + "id": "aria-hidden-focus", + "nodes": 1 + }, + { + "kind": "incomplete", + "id": "color-contrast", + "nodes": 1 + } + ] + }, + { + "test": "[P7-04 PRES-C05 C08 E08] finite library and gallery width locale theme matrix keeps actions and real data reachable", + "attachment": "p7-cv-library-nb-dark-1440-gallery-audit", + "geometry": { + "width": 1440, + "height": 900, + "rootOverflow": 0, + "overflowElements": [] + }, + "engine": null, + "toolOptions": null, + "violationNodes": 0, + "incompleteNodes": 5, + "raw": { + "path": ".artifacts/phase7/p7-04/library/final/embedded/cv-library/041-p7-cv-library-nb-dark-1440-gallery-audit.json", + "sha256": "c55440de87471837527a1c938b28a15b4e622a201ce5c2eb9e37c6d685482fb8", + "bytes": 18576, + "hashPolicy": "Exact measured raw bytes" + }, + "originalEmbeddedReport": { + "path": ".artifacts/phase7/p7-04/library/final/cv-library.json", + "sha256": "39352bcabcd3789197da4efc59e4aa03e73cb31ee7d6549d58f39497c8e760b5", + "bytes": 2233932, + "hashPolicy": "Exact measured raw bytes" + }, + "context": { + "sample": { + "language": "nb", + "theme": "dark", + "width": 1440, + "height": 900 + }, + "state": "gallery", + "qualifications": [ + "Automated Chromium only; incomplete axe findings require triage and do not establish human screen-reader acceptance.", + "No physical touch/software keyboard, actual browser zoom, production font/photo, print/PDF fidelity, model/provider or production acceptance." + ] + }, + "rules": [ + { + "kind": "incomplete", + "id": "aria-hidden-focus", + "nodes": 1 + }, + { + "kind": "incomplete", + "id": "color-contrast", + "nodes": 4 + } + ] + }, + { + "test": "[P7-04 PRES-C05 C08 E08] finite library and gallery width locale theme matrix keeps actions and real data reachable", + "attachment": "p7-cv-library-en-light-1920-library-audit", + "geometry": { + "width": 1920, + "height": 1080, + "rootOverflow": 0, + "overflowElements": [] + }, + "engine": null, + "toolOptions": null, + "violationNodes": 0, + "incompleteNodes": 2, + "raw": { + "path": ".artifacts/phase7/p7-04/library/final/embedded/cv-library/043-p7-cv-library-en-light-1920-library-audit.json", + "sha256": "708fdcc2d0cc3dcc8013d92d73b225ad98a976e2e0b1efb9f2f452b16aab5edc", + "bytes": 7882, + "hashPolicy": "Exact measured raw bytes" + }, + "originalEmbeddedReport": { + "path": ".artifacts/phase7/p7-04/library/final/cv-library.json", + "sha256": "39352bcabcd3789197da4efc59e4aa03e73cb31ee7d6549d58f39497c8e760b5", + "bytes": 2233932, + "hashPolicy": "Exact measured raw bytes" + }, + "context": { + "sample": { + "language": "en", + "theme": "light", + "width": 1920, + "height": 1080 + }, + "state": "library", + "qualifications": [ + "Automated Chromium only; incomplete axe findings require triage and do not establish human screen-reader acceptance.", + "No physical touch/software keyboard, actual browser zoom, production font/photo, print/PDF fidelity, model/provider or production acceptance." + ] + }, + "rules": [ + { + "kind": "incomplete", + "id": "aria-hidden-focus", + "nodes": 1 + }, + { + "kind": "incomplete", + "id": "color-contrast", + "nodes": 1 + } + ] + }, + { + "test": "[P7-04 PRES-C05 C08 E08] finite library and gallery width locale theme matrix keeps actions and real data reachable", + "attachment": "p7-cv-library-en-light-1920-gallery-audit", + "geometry": { + "width": 1920, + "height": 1080, + "rootOverflow": 0, + "overflowElements": [] + }, + "engine": null, + "toolOptions": null, + "violationNodes": 0, + "incompleteNodes": 5, + "raw": { + "path": ".artifacts/phase7/p7-04/library/final/embedded/cv-library/045-p7-cv-library-en-light-1920-gallery-audit.json", + "sha256": "cc7a3e3227823038c21310869f75b5018bdbd073a3f875964585582b121367d1", + "bytes": 18593, + "hashPolicy": "Exact measured raw bytes" + }, + "originalEmbeddedReport": { + "path": ".artifacts/phase7/p7-04/library/final/cv-library.json", + "sha256": "39352bcabcd3789197da4efc59e4aa03e73cb31ee7d6549d58f39497c8e760b5", + "bytes": 2233932, + "hashPolicy": "Exact measured raw bytes" + }, + "context": { + "sample": { + "language": "en", + "theme": "light", + "width": 1920, + "height": 1080 + }, + "state": "gallery", + "qualifications": [ + "Automated Chromium only; incomplete axe findings require triage and do not establish human screen-reader acceptance.", + "No physical touch/software keyboard, actual browser zoom, production font/photo, print/PDF fidelity, model/provider or production acceptance." + ] + }, + "rules": [ + { + "kind": "incomplete", + "id": "aria-hidden-focus", + "nodes": 1 + }, + { + "kind": "incomplete", + "id": "color-contrast", + "nodes": 4 + } + ] + } + ], + "violationNodes": 0, + "incompleteNodes": 64, + "checkContexts": [ + { + "kind": "incomplete", + "rule": "color-contrast", + "check": "color-contrast", + "message": "Element content is too short to determine if it is actual text content", + "occurrences": 19, + "representativeContexts": [ + { + "attachment": "p7-cv-library-en-light-1440-list-recovered-audit", + "target": [ + ".MuiBadge-badge" + ], + "data": { + "contrastRatio": 0, + "fontSize": "9.0pt (12px)", + "fontWeight": "normal", + "messageKey": "shortTextContent", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [] + }, + { + "attachment": "p7-cv-library-nb-light-360-held-create-audit", + "target": [ + ".MuiBadge-badge" + ], + "data": { + "contrastRatio": 0, + "fontSize": "9.0pt (12px)", + "fontWeight": "normal", + "messageKey": "shortTextContent", + "expectedContrastRatio": "4.5:1" + }, + "relatedNodes": [] + } + ] + }, + { + "kind": "incomplete", + "rule": "aria-hidden-focus", + "check": "focusable-modal-open", + "message": "Check that focusable elements are not tabbable in the current state", + "occurrences": 17, + "representativeContexts": [ + { + "attachment": "p7-cv-library-nb-light-360-held-create-audit", + "target": [ + "#root" + ], + "data": null, + "relatedNodes": [ + { + "html": "Hopp til hovedinnhold", + "target": [ + "a" + ] + }, + { + "html": "", + "target": [ + ".css-1u962su" + ] + }, + { + "html": " - + + {atsOnly && {t("cvEditorAtsTemplateHelp")}} {visibleThemes.map((theme) => { const active = theme.id === selectedId; const locked = theme.available === false; + const unavailable = locked || disabled; return ( - { if (!locked) onSelect(theme); }} - onKeyDown={(event) => { if (!locked && (event.key === "Enter" || event.key === " ")) { event.preventDefault(); onSelect(theme); } }} - sx={{ p: 1, cursor: locked ? "not-allowed" : "pointer", position: "relative", borderColor: active ? "primary.main" : "divider", borderWidth: active ? 2 : 1, "&:hover": locked ? undefined : { borderColor: "primary.main" } }}> + { if (!unavailable) onSelect(theme); }} + onKeyDown={(event) => { if (!unavailable && (event.key === "Enter" || event.key === " ")) { event.preventDefault(); onSelect(theme); } }} + sx={{ p: 1, cursor: unavailable ? "not-allowed" : "pointer", position: "relative", minWidth: 0, overflowWrap: "anywhere", borderColor: active ? "primary.main" : "divider", borderWidth: active ? 2 : 1, "&:hover": unavailable ? undefined : { borderColor: "primary.main" } }}> {active && } {theme.name} diff --git a/job-tracker-ui/src/cv-builder-page.test.tsx b/job-tracker-ui/src/cv-builder-page.test.tsx index 3cb88ad..641d9d7 100644 --- a/job-tracker-ui/src/cv-builder-page.test.tsx +++ b/job-tracker-ui/src/cv-builder-page.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import '@testing-library/jest-dom'; -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import CvBuilderPage from './views/CvBuilderPage'; @@ -9,6 +9,7 @@ import { ToastProvider } from './toast'; import { api } from './api'; import { ConfirmProvider } from './confirm'; import { PromptProvider } from './prompt'; +import { emptyCvVariantSettings } from './cvBuilder'; const mockNavigate = jest.fn(); jest.mock('react-router-dom', () => ({ @@ -30,13 +31,14 @@ jest.mock('./api', () => ({ const mockedApi = api as jest.Mocked; -function renderPage() { +function renderPage(extra?: React.ReactNode) { return render( + {extra} @@ -48,11 +50,293 @@ function renderPage() { beforeEach(() => { window.localStorage.removeItem('uiLanguage'); - jest.clearAllMocks(); + jest.resetAllMocks(); + mockedApi.get.mockImplementation(async (url) => ({ data: url === '/cv/themes' ? themes : [] }) as any); }); afterEach(() => window.localStorage.removeItem('uiLanguage')); +const themes = [ + { id: 'modern', name: 'Modern', category: 'Professional', description: '', layout: 'header-band', accent: '#3157d5', photoShape: 'none', supportsIcons: false, atsFriendly: true, requiresPro: false, available: true, swatches: ['#3157d5'] }, + { id: 'clean', name: 'Clean', category: 'Professional', description: '', layout: 'single-column', accent: '#444444', photoShape: 'none', supportsIcons: false, atsFriendly: true, requiresPro: false, available: true, swatches: ['#444444'] }, + { id: 'premium', name: 'Premium', category: 'Professional', description: '', layout: 'sidebar', accent: '#333333', photoShape: 'none', supportsIcons: true, atsFriendly: true, requiresPro: true, available: false, swatches: ['#333333'] }, +]; + +const existingCv = { + id: 1, name: 'Frontend CV', themeId: 'modern', language: 'en', version: 2, + jobApplicationId: 17, jobTitle: 'Frontend Engineer', companyName: 'Northstar', + publicSlug: 'original-slug', isPublic: true, updatedAtUtc: '2026-10-12T10:00:00Z', +}; + +function deferred() { + let resolve!: (value: T) => void; + let reject!: (reason: unknown) => void; + const promise = new Promise((yes, no) => { resolve = yes; reject = no; }); + return { promise, resolve, reject }; +} + +async function selectAction(name: string) { + fireEvent.click(screen.getByRole('button', { name: 'Actions for Frontend CV' })); + const item = await screen.findByRole('menuitem', { name }); + item.focus(); + fireEvent.click(item); +} + +test('the CV library has a section heading and announces its held initial read without showing an empty result', async () => { + const read = deferred(); + mockedApi.get.mockImplementation((url) => url === '/cv/variants' ? read.promise : Promise.resolve({ data: themes }) as any); + renderPage(); + + expect(screen.getByRole('heading', { level: 2, name: 'Your CVs' })).toBeInTheDocument(); + expect(screen.getByRole('status')).toHaveTextContent(/Loading/i); + expect(screen.queryByText('No CVs yet')).not.toBeInTheDocument(); + + await act(async () => { read.resolve({ data: [existingCv] }); }); + expect(await screen.findByRole('link', { name: 'Open Frontend CV' })).toBeInTheDocument(); + expect(screen.getByRole('status')).toHaveTextContent('Content loaded'); + expect(screen.getByRole('status')).not.toHaveTextContent(/Loading/i); +}); + +test('a failed CV list read has an explicit retry that clears the stale failure after recovery', async () => { + const retry = deferred(); + let listReads = 0; + mockedApi.get.mockImplementation((url) => url === '/cv/variants' + ? ++listReads === 1 ? Promise.reject(new Error('synthetic unavailable')) : retry.promise + : Promise.resolve({ data: themes }) as any); + renderPage(); + expect(await screen.findByRole('alert')).toHaveTextContent('Could not load your CVs.'); + expect(screen.queryByText('No CVs yet')).not.toBeInTheDocument(); + const retryButton = screen.getByRole('button', { name: 'Retry' }); + retryButton.focus(); + fireEvent.click(retryButton); + await waitFor(() => expect(mockedApi.get.mock.calls.filter(([url]) => url === '/cv/variants')).toHaveLength(2)); + expect(screen.getByRole('status')).toHaveTextContent(/Loading/i); + expect(mockedApi.post).not.toHaveBeenCalled(); + + await act(async () => { retry.resolve({ data: [existingCv] }); }); + expect(await screen.findByRole('link', { name: 'Open Frontend CV' })).toBeInTheDocument(); + expect(screen.queryByText('Could not load your CVs.')).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Retry' })).not.toBeInTheDocument(); +}); + +test('a live language change retains an open CV name and template draft without refetching library data', async () => { + renderPage(); + await screen.findByText('No CVs yet'); + fireEvent.click(screen.getAllByRole('button', { name: 'New CV' })[0]); + await screen.findByRole('dialog', { name: 'Create a CV' }); + fireEvent.change(screen.getByRole('textbox', { name: 'CV name' }), { target: { value: 'My unsaved application CV' } }); + fireEvent.click(screen.getByRole('button', { name: 'Clean' })); + const requests = mockedApi.get.mock.calls.length; + + act(() => { window.dispatchEvent(new StorageEvent('storage', { key: 'uiLanguage', newValue: 'nb' })); }); + expect(await screen.findByRole('dialog', { name: 'Opprett en CV' })).toBeInTheDocument(); + expect(screen.getByRole('textbox', { name: 'CV-navn' })).toHaveValue('My unsaved application CV'); + expect(screen.getByRole('button', { name: 'Clean' })).toHaveAttribute('aria-pressed', 'true'); + expect(mockedApi.get).toHaveBeenCalledTimes(requests); + expect(mockedApi.post).not.toHaveBeenCalled(); +}); + +test('held CV creation locks the draft and gallery, then keeps a failed draft for an explicit retry', async () => { + const write = deferred(); + mockedApi.post.mockImplementationOnce(() => write.promise).mockResolvedValueOnce({ data: { id: 42 } } as any); + renderPage(); + await screen.findByText('No CVs yet'); + fireEvent.click(screen.getAllByRole('button', { name: 'New CV' })[0]); + const dialog = await screen.findByRole('dialog', { name: 'Create a CV' }); + const name = within(dialog).getByRole('textbox', { name: 'CV name' }); + fireEvent.change(name, { target: { value: 'Application-specific CV' } }); + fireEvent.click(within(dialog).getByRole('button', { name: 'Clean' })); + fireEvent.click(within(dialog).getByRole('button', { name: 'Create CV' })); + await waitFor(() => expect(mockedApi.post).toHaveBeenCalledTimes(1)); + expect(name).toBeDisabled(); + expect(within(dialog).getByRole('button', { name: 'ATS templates' })).toBeDisabled(); + expect(within(dialog).getByRole('button', { name: 'All templates' })).toBeDisabled(); + expect(within(dialog).getByRole('button', { name: 'Modern' })).toHaveAttribute('aria-disabled', 'true'); + expect(within(dialog).getByRole('button', { name: 'Clean' })).toHaveAttribute('aria-disabled', 'true'); + expect(within(dialog).getByRole('button', { name: 'Cancel' })).toBeDisabled(); + fireEvent.click(within(dialog).getByRole('button', { name: /Creating/ })); + fireEvent.click(within(dialog).getByRole('button', { name: 'Modern' })); + expect(mockedApi.post).toHaveBeenCalledTimes(1); + expect(within(dialog).getByRole('button', { name: 'Clean' })).toHaveAttribute('aria-pressed', 'true'); + + await act(async () => { write.reject(new Error('synthetic failed create')); }); + expect(await within(dialog).findByRole('alert')).toHaveTextContent('Could not create a CV.'); + expect(name).toBeEnabled(); + expect(name).toHaveValue('Application-specific CV'); + expect(within(dialog).getByRole('button', { name: 'Clean' })).toHaveAttribute('aria-pressed', 'true'); + expect(mockedApi.post).toHaveBeenCalledTimes(1); + expect(mockNavigate).not.toHaveBeenCalled(); + + fireEvent.click(within(dialog).getByRole('button', { name: 'Create CV' })); + await waitFor(() => expect(mockedApi.post).toHaveBeenLastCalledWith('/cv/variants', { name: 'Application-specific CV', settings: emptyCvVariantSettings('clean') })); + await waitFor(() => expect(mockNavigate).toHaveBeenCalledWith('/career/builder/42')); +}); + +test('a locked CV template remains identifiable and cannot change the creation request', async () => { + mockedApi.post.mockResolvedValueOnce({ data: { id: 42 } } as any); + renderPage(); + await screen.findByText('No CVs yet'); + fireEvent.click(screen.getAllByRole('button', { name: 'New CV' })[0]); + const dialog = await screen.findByRole('dialog', { name: 'Create a CV' }); + const premium = within(dialog).getByRole('button', { name: 'Premium' }); + expect(premium).toHaveAttribute('aria-disabled', 'true'); + expect(premium).toHaveAttribute('tabindex', '-1'); + fireEvent.click(premium); + fireEvent.keyDown(premium, { key: 'Enter' }); + expect(within(dialog).getByRole('button', { name: 'Modern' })).toHaveAttribute('aria-pressed', 'true'); + fireEvent.click(within(dialog).getByRole('button', { name: 'Create CV' })); + await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/cv/variants', expect.objectContaining({ settings: emptyCvVariantSettings('modern') }))); +}); + +test('a held duplicate blocks another action for the same CV and shows the persisted refreshed copy', async () => { + const write = deferred(); + let listReads = 0; + mockedApi.get.mockImplementation(async (url) => ({ data: url === '/cv/variants' + ? ++listReads === 1 ? [existingCv] : [existingCv, { ...existingCv, id: 2, name: 'Frontend CV (copy)', publicSlug: 'copy-slug', isPublic: false }] + : themes }) as any); + mockedApi.post.mockImplementationOnce(() => write.promise); + renderPage(); + await screen.findByText('Frontend CV'); + const actions = screen.getByRole('button', { name: 'Actions for Frontend CV' }); + await selectAction('Duplicate'); + await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/cv/variants/1/duplicate', { name: undefined })); + expect(actions).toBeDisabled(); + fireEvent.click(actions); + const duplicate = screen.queryByRole('menuitem', { name: 'Duplicate' }); + if (duplicate) fireEvent.click(duplicate); + expect(mockedApi.post).toHaveBeenCalledTimes(1); + expect(listReads).toBe(1); + + await act(async () => { write.resolve({ data: { id: 2 } }); }); + expect(await screen.findByRole('link', { name: 'Open Frontend CV (copy)' })).toBeInTheDocument(); + await waitFor(() => expect(actions).toBeEnabled()); + expect(mockedApi.post).toHaveBeenCalledTimes(1); + expect(listReads).toBe(2); +}); + +test('rename reads the current revision and preserves its settings when saving the new name', async () => { + const settings = { ...emptyCvVariantSettings('clean'), overrides: { summary: { text: 'Owned override' } }, showPageNumbers: true }; + mockedApi.get.mockImplementation(async (url) => ({ data: url === '/cv/variants' ? [existingCv] + : url === '/cv/variants/1' ? { ...existingCv, settings, version: 7 } : themes }) as any); + mockedApi.put.mockResolvedValueOnce({ data: { ...existingCv, name: 'Renamed application CV', version: 8, updatedAtUtc: '2026-10-12T11:00:00Z' } } as any); + renderPage(); + await screen.findByText('Frontend CV'); + await selectAction('Rename'); + const input = await screen.findByRole('textbox', { name: 'Give this CV a clear name.' }); + fireEvent.change(input, { target: { value: ' Renamed application CV ' } }); + fireEvent.click(screen.getByRole('button', { name: 'Rename' })); + await waitFor(() => expect(mockedApi.put).toHaveBeenCalledWith('/cv/variants/1', { name: 'Renamed application CV', settings, source: 'manual', expectedVersion: 7 })); + expect(await screen.findByRole('link', { name: 'Open Renamed application CV' })).toBeInTheDocument(); + expect(screen.getByText(/Frontend Engineer at Northstar/)).toBeInTheDocument(); + expect(screen.getByText('Public')).toBeInTheDocument(); + expect(mockedApi.put).toHaveBeenCalledTimes(1); +}); + +test('a rename conflict keeps the original library entry and never retries the write automatically', async () => { + const settings = { ...emptyCvVariantSettings('modern'), overrides: { summary: { text: 'Owned override' } } }; + mockedApi.get.mockImplementation(async (url) => ({ data: url === '/cv/variants' ? [existingCv] + : url === '/cv/variants/1' ? { ...existingCv, settings, version: 7 } : themes }) as any); + mockedApi.put.mockRejectedValueOnce({ response: { status: 409 } }); + renderPage(); + await screen.findByText('Frontend CV'); + await selectAction('Rename'); + fireEvent.change(await screen.findByRole('textbox', { name: 'Give this CV a clear name.' }), { target: { value: 'Conflicting CV name' } }); + fireEvent.click(screen.getByRole('button', { name: 'Rename' })); + expect((await screen.findAllByText('Rename failed.')).length).toBeGreaterThan(0); + await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()); + expect(screen.getByRole('link', { name: 'Open Frontend CV' })).toBeInTheDocument(); + expect(screen.queryByRole('link', { name: 'Open Conflicting CV name' })).not.toBeInTheDocument(); + expect(mockedApi.put).toHaveBeenCalledTimes(1); + expect(mockedApi.delete).not.toHaveBeenCalled(); +}); + +test('cancelling deletion returns focus to the surviving CV action control without removing the CV', async () => { + mockedApi.get.mockImplementation(async (url) => ({ data: url === '/cv/variants' ? [existingCv] : themes }) as any); + renderPage(); + await screen.findByText('Frontend CV'); + const actions = screen.getByRole('button', { name: 'Actions for Frontend CV' }); + actions.focus(); + await selectAction('Delete'); + await screen.findByRole('heading', { name: 'Delete CV' }); + fireEvent.click(screen.getByRole('button', { name: 'Cancel' })); + await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()); + await waitFor(() => expect(actions).toHaveFocus()); + expect(screen.getByRole('link', { name: 'Open Frontend CV' })).toBeInTheDocument(); + expect(mockedApi.delete).not.toHaveBeenCalled(); +}); + +test('a failed creation keeps its draft and translates its failure after closing and reopening in another language', async () => { + mockedApi.post.mockRejectedValueOnce(new Error('synthetic unavailable')); + renderPage(); + await screen.findByText('No CVs yet'); + fireEvent.click(screen.getByRole('button', { name: 'New CV' })); + const dialog = await screen.findByRole('dialog', { name: 'Create a CV' }); + fireEvent.change(within(dialog).getByRole('textbox', { name: 'CV name' }), { target: { value: 'Keep this failed CV draft' } }); + fireEvent.click(within(dialog).getByRole('button', { name: 'Clean' })); + fireEvent.click(within(dialog).getByRole('button', { name: 'Create CV' })); + expect(await within(dialog).findByRole('alert')).toHaveTextContent('Could not create a CV.'); + fireEvent.click(within(dialog).getByRole('button', { name: 'Cancel' })); + await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()); + const reads = mockedApi.get.mock.calls.length; + act(() => { window.dispatchEvent(new StorageEvent('storage', { key: 'uiLanguage', newValue: 'nb' })); }); + fireEvent.click(screen.getByRole('button', { name: 'Ny CV' })); + const reopened = await screen.findByRole('dialog', { name: 'Opprett en CV' }); + expect(within(reopened).getByRole('textbox', { name: 'CV-navn' })).toHaveValue('Keep this failed CV draft'); + expect(within(reopened).getByRole('button', { name: 'Clean' })).toHaveAttribute('aria-pressed', 'true'); + expect(within(reopened).getByRole('alert')).toHaveTextContent('Kunne ikke opprette en CV.'); + expect(within(reopened).queryByText('Could not create a CV.')).not.toBeInTheDocument(); + expect(mockedApi.get).toHaveBeenCalledTimes(reads); + expect(mockedApi.post).toHaveBeenCalledTimes(1); + expect(mockNavigate).not.toHaveBeenCalled(); +}); + +test('generic PDF failure follows the current language both after failure and when a held request fails', async () => { + const secondDownload = deferred(); + mockedApi.get.mockImplementation(async (url) => ({ data: url === '/cv/variants' ? [existingCv] : themes }) as any); + mockedApi.post.mockRejectedValueOnce(new Error('synthetic unavailable')).mockImplementationOnce(() => secondDownload.promise); + renderPage(); + await screen.findByText('Frontend CV'); + await selectAction('Download PDF'); + expect(await screen.findByRole('alert')).toHaveTextContent('PDF download failed.'); + const reads = mockedApi.get.mock.calls.length; + act(() => { window.dispatchEvent(new StorageEvent('storage', { key: 'uiLanguage', newValue: 'nb' })); }); + expect(screen.getByRole('alert')).toHaveTextContent('Nedlasting av PDF mislyktes.'); + expect(screen.queryByText('PDF download failed.')).not.toBeInTheDocument(); + expect(mockedApi.get).toHaveBeenCalledTimes(reads); + expect(mockedApi.post).toHaveBeenCalledTimes(1); + + fireEvent.click(screen.getByRole('button', { name: 'Handlinger for Frontend CV' })); + fireEvent.click(await screen.findByRole('menuitem', { name: 'Last ned PDF' })); + await waitFor(() => expect(mockedApi.post).toHaveBeenCalledTimes(2)); + act(() => { window.dispatchEvent(new StorageEvent('storage', { key: 'uiLanguage', newValue: 'en' })); }); + await act(async () => { secondDownload.reject(new Error('synthetic unavailable')); }); + expect(await screen.findByRole('alert')).toHaveTextContent('PDF download failed.'); + expect(screen.queryByText('Nedlasting av PDF mislyktes.')).not.toBeInTheDocument(); + expect(mockedApi.get).toHaveBeenCalledTimes(reads); + expect(mockedApi.post).toHaveBeenCalledTimes(2); + expect(mockedApi.put).not.toHaveBeenCalled(); +}); + +test('a held deletion preserves focus moved to another surviving control before the response arrives', async () => { + const write = deferred(); + mockedApi.get.mockImplementation(async (url) => ({ data: url === '/cv/variants' ? [existingCv] : themes }) as any); + mockedApi.delete.mockImplementationOnce(() => write.promise); + renderPage(); + await screen.findByText('Frontend CV'); + await selectAction('Delete'); + fireEvent.click(await screen.findByRole('button', { name: 'Delete CV' })); + await waitFor(() => expect(mockedApi.delete).toHaveBeenCalledWith('/cv/variants/1')); + await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument()); + const otherControl = screen.getByRole('button', { name: 'Separate workflow control' }); + otherControl.focus(); + expect(otherControl).toHaveFocus(); + await act(async () => { write.resolve({ data: null }); }); + await waitFor(() => expect(screen.queryByRole('link', { name: 'Open Frontend CV' })).not.toBeInTheDocument()); + expect(otherControl).toHaveFocus(); + expect(mockedApi.delete).toHaveBeenCalledTimes(1); + expect(mockedApi.post).not.toHaveBeenCalled(); +}); + test.each([ [503, 'PDF generation is busy. Please try again shortly.'], [504, 'PDF generation took too long. Please try again.'], @@ -113,7 +397,7 @@ test('CV dashboard and create flow render in Bokmål', async () => { expect(await screen.findByText('Ingen CV-er ennå')).toBeInTheDocument(); fireEvent.click(screen.getAllByRole('button', { name: 'Ny CV' })[0]); expect(await screen.findByRole('dialog', { name: 'Opprett en CV' })).toBeInTheDocument(); - expect(screen.getByLabelText('CV-navn')).toHaveValue('CV uten tittel'); + expect(screen.getByLabelText(/^CV-navn/)).toHaveValue('CV uten tittel'); expect(screen.getByText('Velg en mal')).toBeInTheDocument(); }); diff --git a/job-tracker-ui/src/i18n/translations.ts b/job-tracker-ui/src/i18n/translations.ts index 43749d5..f1cec1b 100644 --- a/job-tracker-ui/src/i18n/translations.ts +++ b/job-tracker-ui/src/i18n/translations.ts @@ -784,6 +784,7 @@ export const translations = { cvEditorRestoredVersion: "Restored version {version}.", cvEditorRestoreFailed: "Restore failed.", cvDashboardTitle: "CV Builder", + cvDashboardLibraryTitle: "Your CVs", cvDashboardSubtitle: "Build polished, job-specific resumes from one trusted career profile. Every version keeps its own template, content choices and history.", cvDashboardUntitled: "Untitled CV", cvDashboardLoadFailed: "Could not load your CVs.", @@ -816,6 +817,7 @@ export const translations = { cvDashboardEnterName: "Enter a name.", cvDashboardNameExample: "For example: Backend Engineer — Acme", cvDashboardChooseTemplate: "Choose a template", + cvDashboardDefaultTemplate: "Modern", cvDashboardCreating: "Creating…", cvDashboardCreateCv: "Create CV", cvAiFailed: "AI suggestion failed.", @@ -3416,6 +3418,7 @@ export const translations = { cvEditorRestoredVersion: "Versjon {version} er gjenopprettet.", cvEditorRestoreFailed: "Gjenoppretting mislyktes.", cvDashboardTitle: "CV-bygger", + cvDashboardLibraryTitle: "Dine CV-er", cvDashboardSubtitle: "Lag profesjonelle, jobbtilpassede CV-er fra én pålitelig karriereprofil. Hver versjon beholder sin egen mal, sine innholdsvalg og sin historikk.", cvDashboardUntitled: "CV uten tittel", cvDashboardLoadFailed: "Kunne ikke laste CV-ene dine.", @@ -3448,6 +3451,7 @@ export const translations = { cvDashboardEnterName: "Skriv inn et navn.", cvDashboardNameExample: "For eksempel: Backendutvikler — Acme", cvDashboardChooseTemplate: "Velg en mal", + cvDashboardDefaultTemplate: "Moderne", cvDashboardCreating: "Oppretter…", cvDashboardCreateCv: "Opprett CV", cvAiFailed: "AI-forslaget mislyktes.", diff --git a/job-tracker-ui/src/views/CvBuilderPage.tsx b/job-tracker-ui/src/views/CvBuilderPage.tsx index 5d0f478..a1302db 100644 --- a/job-tracker-ui/src/views/CvBuilderPage.tsx +++ b/job-tracker-ui/src/views/CvBuilderPage.tsx @@ -1,11 +1,10 @@ -import React, { useCallback, useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; import { Alert, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Menu, MenuItem, Paper, Stack, TextField, Typography, } from "@mui/material"; import AddIcon from "@mui/icons-material/Add"; -import DescriptionOutlinedIcon from "@mui/icons-material/DescriptionOutlined"; import MoreVertIcon from "@mui/icons-material/MoreVert"; import PublicIcon from "@mui/icons-material/Public"; @@ -16,98 +15,126 @@ import { useDialogActions } from "../dialogs"; import CvTemplateGallery from "../components/CvTemplateGallery"; import CvTemplateThumbnail from "../components/CvTemplateThumbnail"; import { useI18n } from "../i18n/I18nProvider"; +import type { TranslationKey } from "../i18n/translations"; +import { PageHeader } from "../layout/PageLayout"; +import ViewStateNotice from "../components/ViewStateNotice"; +import { useViewResource } from "../hooks/useViewResource"; export default function CvBuilderPage() { const navigate = useNavigate(); const { toast } = useToast(); const { language, t } = useI18n(); + const localeRef = useRef(t); + localeRef.current = t; const { confirmAction, promptForValue } = useDialogActions(); - const [variants, setVariants] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const resource = useViewResource(async () => ({ + variants: await cvBuilderApi.list(), + themes: await cvBuilderApi.themes().catch(() => []), + }), { initialData: { variants: [] as CvVariantSummary[], themes: [] as CvTheme[] }, errorMessage: "" }); + const { variants, themes } = resource.data; + const loading = resource.loading || resource.refreshing; + const contentRef = useRef(null); + const createTitleRef = useRef(null); + const actionBusy = useRef(false); + const actionAnchor = useRef(null); + const [pending, setPending] = useState(false); + const [actionError, setActionError] = useState<{ error?: unknown; key: TranslationKey } | null>(null); + const [createError, setCreateError] = useState<{ error: unknown } | null>(null); const [menu, setMenu] = useState<{ anchor: HTMLElement; id: number } | null>(null); - const [themes, setThemes] = useState([]); const [createOpen, setCreateOpen] = useState(false); const [newName, setNewName] = useState(t("cvDashboardUntitled")); const [newTheme, setNewTheme] = useState("modern"); const [creating, setCreating] = useState(false); - const load = useCallback(async () => { - try { - setVariants(await cvBuilderApi.list()); - try { setThemes(await cvBuilderApi.themes()); } catch { setThemes([]); } - } catch (err) { - setError(getApiErrorMessage(err, t("cvDashboardLoadFailed"))); - } finally { - setLoading(false); - } - }, [t]); useEffect(() => { - void load(); - }, [load]); + if (pending) contentRef.current?.focus(); + else if (document.activeElement === contentRef.current && actionAnchor.current?.isConnected) actionAnchor.current.focus(); + }, [pending]); - const createNew = async () => { + const startAction = (showPending = true) => { + if (actionBusy.current) return false; + actionBusy.current = true; + actionAnchor.current = menu?.anchor ?? null; + setMenu(null); + setActionError(null); + if (showPending) setPending(true); + return true; + }; + const finishAction = () => { actionBusy.current = false; setPending(false); }; + const setVariants = (update: (items: CvVariantSummary[]) => CvVariantSummary[]) => { + resource.setData((data) => ({ ...data, variants: update(data.variants) })); + }; + + const createNew = () => { + if (actionBusy.current) return; setCreateOpen(true); }; const confirmCreate = async () => { - if (!newName.trim()) return; + if (!newName.trim() || actionBusy.current) return; + actionBusy.current = true; + createTitleRef.current?.focus(); + setCreateError(null); setCreating(true); try { const variant = await cvBuilderApi.create({ name: newName.trim(), settings: emptyCvVariantSettings(newTheme) }); setCreateOpen(false); navigate(`/career/builder/${variant.id}`); } catch (err) { - toast(getApiErrorMessage(err, t("cvDashboardCreateFailed")), "error"); - } finally { setCreating(false); } + setCreateError({ error: err }); + } finally { actionBusy.current = false; setCreating(false); } }; const duplicate = async (id: number) => { + if (!startAction()) return; try { await cvBuilderApi.duplicate(id); - await load(); - toast(t("cvDashboardDuplicated"), "success"); + await resource.reload(); + toast(localeRef.current("cvDashboardDuplicated"), "success"); } catch (err) { - toast(getApiErrorMessage(err, t("cvDashboardDuplicateFailed")), "error"); + setActionError({ error: err, key: "cvDashboardDuplicateFailed" }); } finally { - setMenu(null); + finishAction(); } }; const remove = async (id: number) => { + if (!startAction(false)) return; const variant = variants.find((item) => item.id === id); - setMenu(null); - if (!(await confirmAction(t("cvDashboardDeleteMessage", { name: variant?.name ?? t("cvDashboardThisCv") }), { - title: t("cvDashboardDeleteCv"), - confirmLabel: t("cvDashboardDeleteCv"), - destructive: true, - }))) return; try { + if (!(await confirmAction(t("cvDashboardDeleteMessage", { name: variant?.name ?? t("cvDashboardThisCv") }), { + title: t("cvDashboardDeleteCv"), + confirmLabel: t("cvDashboardDeleteCv"), + destructive: true, + }))) return; + setPending(true); await cvBuilderApi.remove(id); setVariants((v) => v.filter((x) => x.id !== id)); - toast(t("cvDashboardDeleted"), "success"); + toast(localeRef.current("cvDashboardDeleted"), "success"); } catch (err) { - toast(getApiErrorMessage(err, t("cvDashboardDeleteFailed")), "error"); + setActionError({ error: err, key: "cvDashboardDeleteFailed" }); } finally { - setMenu(null); + finishAction(); } }; const rename = async (id: number) => { + if (!startAction(false)) return; const current = variants.find((item) => item.id === id); - setMenu(null); - const nextName = await promptForValue(t("cvDashboardRenameMessage"), current?.name ?? "", { title: t("cvDashboardRenameCv"), confirmLabel: t("cvDashboardRename") }); - if (!nextName?.trim() || nextName.trim() === current?.name) return; try { + const nextName = await promptForValue(t("cvDashboardRenameMessage"), current?.name ?? "", { title: t("cvDashboardRenameCv"), confirmLabel: t("cvDashboardRename") }); + if (!nextName?.trim() || nextName.trim() === current?.name) return; + setPending(true); const variant = await cvBuilderApi.get(id); const updated = await cvBuilderApi.save(id, { name: nextName.trim(), settings: variant.settings, source: "manual", expectedVersion: variant.version }); setVariants((items) => items.map((item) => item.id === id ? { ...item, name: updated.name, updatedAtUtc: updated.updatedAtUtc, version: updated.version } : item)); - toast(t("cvDashboardRenamed"), "success"); - } catch (err) { toast(getApiErrorMessage(err, t("cvDashboardRenameFailed")), "error"); } + toast(localeRef.current("cvDashboardRenamed"), "success"); + } catch (err) { setActionError({ error: err, key: "cvDashboardRenameFailed" }); } + finally { finishAction(); } }; const download = async (id: number, cvName: string) => { - setMenu(null); + if (!startAction()) return; try { const response = await api.post(cvBuilderApi.exportPdfUrl(id), {}, { responseType: "blob" }); const url = URL.createObjectURL(response.data as Blob); @@ -118,42 +145,38 @@ export default function CvBuilderPage() { URL.revokeObjectURL(url); } catch (err) { const status = (err as { response?: { status?: number } })?.response?.status; - toast(status === 503 ? t("cvPdfBusy") : status === 504 ? t("cvPdfTimedOut") : getApiErrorMessage(err, t("cvDashboardDownloadFailed")), "error"); - } + setActionError({ error: status === 503 || status === 504 ? undefined : err, key: status === 503 ? "cvPdfBusy" : status === 504 ? "cvPdfTimedOut" : "cvDashboardDownloadFailed" }); + } finally { finishAction(); } }; return ( - - - - {t("cvDashboardTitle")} - {t("cvDashboardSubtitle")} - - - - - {error && {error}} - - {!loading && variants.length === 0 && !error && ( - - - {t("cvDashboardEmptyTitle")} + + } onClick={createNew}>{t("cvDashboardNewCv")}} /> + { if (actionBusy.current || loading) return; contentRef.current?.focus(); void resource.reload(); }} compact /> + {actionError && {getApiErrorMessage(actionError.error, t(actionError.key))}} + {pending && {t("loading")}} + {!loading && variants.length === 0 && !resource.error && ( + + {t("cvDashboardEmptyTitle")} {t("cvDashboardEmptyBody")} - )} - + {variants.map((v) => ( navigate(`/career/builder/${v.id}`)} + sx={{ p: "var(--app-card-padding)", minWidth: 0, borderRadius: 1, cursor: pending ? "default" : "pointer", border: "1px solid", borderColor: "divider", "&:hover": { borderColor: "primary.main" }, "&:focus-visible": { outline: "3px solid", outlineColor: "primary.main", outlineOffset: 2 } }} + onClick={() => { if (!actionBusy.current) navigate(`/career/builder/${v.id}`); }} onKeyDown={(event) => { - if (event.target === event.currentTarget && (event.key === "Enter" || event.key === " ")) { + if (!actionBusy.current && event.target === event.currentTarget && (event.key === "Enter" || event.key === " ")) { event.preventDefault(); navigate(`/career/builder/${v.id}`); } @@ -161,12 +184,12 @@ export default function CvBuilderPage() { > theme.id === v.themeId)} /> - {v.name} - { e.stopPropagation(); setMenu({ anchor: e.currentTarget, id: v.id }); }}> + {v.name} + { e.stopPropagation(); if (!actionBusy.current) setMenu({ anchor: e.currentTarget, id: v.id }); }}> - + {v.isPublic && } label={t("cvEditorPublic")} />} @@ -187,13 +210,14 @@ export default function CvBuilderPage() { menu && remove(menu.id)} sx={{ color: "error.main" }}>{t("cvDashboardDelete")} - { if (!creating) setCreateOpen(false); }} fullWidth maxWidth="md" aria-labelledby="create-cv-title"> - {t("cvDashboardCreateTitle")} - - {t("cvDashboardCreateHelp")} - setNewName(event.target.value)} error={!newName.trim()} helperText={!newName.trim() ? t("cvDashboardEnterName") : t("cvDashboardNameExample")} sx={{ mb: 2 }} /> - {t("cvDashboardChooseTemplate")} - theme.available) : [{ id: "modern", name: "Modern", category: "Professional", description: "", layout: "header-band", accent: "#3157d5", photoShape: "none", supportsIcons: false, atsFriendly: true, requiresPro: false, available: true, swatches: ["#3157d5", "#eef1f4", "#fff"] }]} selectedId={newTheme} onSelect={(theme) => setNewTheme(theme.id)} previewHeight={125} showDescriptions={false} /> + { if (!actionBusy.current) setCreateOpen(false); }} fullWidth maxWidth="md" aria-labelledby="create-cv-title" aria-describedby={`create-cv-help${createError ? " create-cv-error" : ""}`}> + {t("cvDashboardCreateTitle")} + + {t("cvDashboardCreateHelp")} + {createError && {getApiErrorMessage(createError.error, t("cvDashboardCreateFailed"))}} + setNewName(event.target.value)} error={!newName.trim()} helperText={!newName.trim() ? t("cvDashboardEnterName") : t("cvDashboardNameExample")} sx={{ mb: 2, "& .MuiFormHelperText-root.Mui-disabled": { color: "text.secondary" } }} /> + {t("cvDashboardChooseTemplate")} + { if (!actionBusy.current) setNewTheme(theme.id); }} previewHeight={125} showDescriptions={false} />