fix(ui): reconcile shared accessibility and read recovery
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## Continuation — 20 October 2026
|
||||
|
||||
P7-10 System **d824f59d2b3e9b680bee5952802aa3c0453aee03** normally pushed/directly verified clean13:40:25UTC. CI-only main repair merge **6db62095c6dc6f7e8275447827c62b09f9b2d2e3** pushed/directly verified clean13:40:41UTC. P7-10 is implemented CONDITIONAL GO and published. P7-11 [shared integration](p7-11/SHARED.md) has80/11 affected/consumer frontend, current7version/10command vectors and native5modal+1Strategy PASS; final hygiene/publication follows.59paired label additions are the shared foundation for subsequent responsible-owner commits. Whole candidate frontend898/101 and backend1057 pass; final ARIA continuations receive focused/native proof and fresh P7-12 regression.
|
||||
|
||||
Independent fixture/focus repair **bba52af4b8724f1a5c74013ce1476a20ca3972c2** normally pushed/directly verified on development18:05:08UTC, with all P7-11 WIP retained. Main's equivalent **3e1057d** plus Qwen adapter cherry-pick **f7dc22e5e7efe1fd1d64f6ace2c04cf2f95bf558** are pushed/directly verified18:05UTC. Release833 tests; no verified deployment/model activation or production GO claim. Run832's16failures are explained in its release report; one flaky Rules case remains debt.
|
||||
|
||||
P7-10 [System](p7-10/SYSTEM.md):20unit/46existingbackend/3native plus app/browser TypeScript/lint/build PASS. ExportEAHpn0GPj6rRy3WtHltbf;14axe0violations/100incompletes, two final images inspected after dirty-label correction. Publication follows final staged checks. Audit **f9e1d06bf7fc4081b70ebf461f1645ef219e0a93** pushed/directly verified13:26:54UTC. Separate Qwen adapter preparation **1d34406d1bacdb33d180f0e7e37bb0cb099ece5e** pushed/directly verified13:33:16UTC, full offline sidecar56/56; model activation remains pending. Main d1fcd23 CI832 is independently running; no verified deployment claim.
|
||||
|
||||
P7-10 [Audit](p7-10/AUDIT.md) final bounded gate:9 frontend,18 existing backend and3 native cases PASS; app/browser TypeScript, lint/build PASS. Export vZuD4f-46FJekdIR5rhRk; seven scans0violations/110incomplete nodes, two final images inspected. Ordinary REDs and pre-layout candidate remain retained. Users hygiene follow-up **4760fd102c21c684bd139206a12194c6fc8f6104** passed staged/full-phase whitespace and was normally pushed/directly verified13:10:57UTC. Audit publication follows after staged checks; System tests remain separate WIP.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Phase 7 — Retained debt and mandatory gates
|
||||
|
||||
P7-11 shared/CV continuation corrects the evidenced roleless Checklist/Interview/Undo/Redo tooltip spans, static version-badge name and dangling Quick Search title reference. Final modal26scans0violations/176incomplete observations retain172contrast and4hidden-focus menu-transition contexts; final Documents18scans0/105 are contrast-only. This is bounded source/DOM/browser remediation, not human reader/device or every-pixel conformance. Correspondence/gallery modal-background transitions, existing library wrappers, contrast/physical/output/reliability and production gates remain qualified. No old manual requirement or intermittent Rules incident is closed by rerun.
|
||||
|
||||
P7-09's [sender repair](p7-09/evidence/oauth-results.json) confirms/fixes uncorrelated OAuth messages and delayed startup-language feedback. Consumer regression retains89 axe incompletes, including46 roleless-span `aria-label` diagnostics (45 Notes/Tasks,1 Interview) for bounded P7-11 responsible-owner triage. No existing manual/provider/release gate is closed; the Settings presentation findings remain pending.
|
||||
|
||||
First-boundary [Preferences/Connections results](p7-09/evidence/preferences-results.json) repair notification draft/reset/recovery/snapshot/disposal and truthful provider loading/error/write-refresh/IMAP-port/focus states. New18 scans retain62 incompletes and existing36 retain222; indeterminate layered contrast, short badges and closing-menu settled focus/AT remain qualified. RD11/RD12/RD13 roots are not closed by successful or controlled recovery. Data lifecycle additionally requires the demonstrated app-export/import casing compatibility repair before its round-trip claim; no restore/transaction/replay capability is invented.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Phase 7 — Initial preservation matrix
|
||||
|
||||
P7-11 [shared integration](p7-11/SHARED.md) adds bounded PRES-E07/E08/W04/W09 evidence: valid badge/command names, retained item-specific checklist/interview actions, contextual form headings and Strategy GET-only error recovery with exact intentional-empty contract. Existing AI/Pro and operations semantics remain. All57 groups stay56 PARTIAL/C11 DEFERRED and33 routes; accessibility incompletes/manual/provider/output/production gates remain qualified. No Phase8 work.
|
||||
|
||||
P7-10 [System](p7-10/SYSTEM.md) adds bounded PRES-A03/E09 proof: four independent diagnostic reads, explicit failed-read availability, denied-refresh disposal, GET-only retry, unchanged SMTP password/clear payloads, retained newer drafts and synchronized/session-guarded operations.20unit/46backend/3native cases pass; administrator browser writes are controlled fixtures, not operational acceptance. All57 groups remain56 PARTIAL/C11 DEFERRED and33 routes. No schema/renderer/provider/AI-safety change. Human/device/provider/output/release qualifications remain.
|
||||
|
||||
P7-10 [Audit](p7-10/AUDIT.md) adds PRES-A02/E08/E09 proof: exact Undo allowlist, owned Restore versus administrator Undo, persisted event/readback and ordinary-user denials. GET-only recovery, synchronized writes, current-language errors and disposed-session guards pass. All57 groups retain56 PARTIAL/C11 DEFERRED,33 routes. No server authorization/data contract or PDF/AI safeguard change. System and shared/manual accessibility acceptance remain pending.
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
## Current continuation — 20 October 2026
|
||||
|
||||
P7-10 is implemented CONDITIONAL GO and published throughd824f59; release repair merge6db6209 is pushed. P7-11 shared integration and paired label foundation have scoped unit/native/type/lint/build proof, with normal publication following; generated-label and error-owner source follow as separate commits. Candidate full frontend898/101 and backend1057 pass, sidecar56/56; final ARIA continuations and P7-12 full browser/source acceptance remain. Main f7dc22e release833 is testing; live/model activation remain unverified. Earlier observations below are historical.
|
||||
|
||||
Latest: P7-09 published8af9168; P7-10 Users7ecc0d6/4760fd1 and Auditf9e1d06 are published; System implemented CONDITIONAL GO with20unit/46backend/3native/types/lint/build PASS, final hygiene/publication pending. Current derived inventory884/99frontend,348browser/57files,1057backend; separate full sidecar56/56. These do not replace P7-12's fresh integrated run. Main d1fcd23 is testing in run832 after a narrowly identified Playwright-runtime corruption repair; deployment remains unverified. Qwen3.5 adapter preparation1d34406 is pushed on development, with no live model activation. Earlier observations below remain historical.
|
||||
|
||||
P7-00–08 and P7-09 presentation are implemented CONDITIONAL GO. The [P7-09 import prerequisite](p7-09/IMPORT_COMPATIBILITY.md) passes full frontend 837/97, full backend 1057, affected native 25 and type/lint/build gates, published as `3028d81`. [Remaining data presentation](p7-09/DATA_PRIVACY_BACKUP.md) has affected47/6 plus22backendcontracts and final native/type/lint/build evidence, with final consumer/hygiene/publication pending. Derived inventory is855/99frontend,339browser,1057backend; this is not a fresh full integrated suite. P7-10 Administration, P7-11 residual UI and P7-12 acceptance remain. The historical sections below retain their as-observed baselines.
|
||||
|
||||
@@ -8,6 +8,6 @@ Follow the [approved handoff](../../phase-6/p6-05/PHASE7_HANDOFF.md): exactly th
|
||||
|---|---|
|
||||
| [Users](USERS.md) | Implemented CONDITIONAL GO; published7ecc0d6/4760fd1; follow-up passed staged/full-phase whitespace and remote verification13:10:57UTC |
|
||||
| [Audit](AUDIT.md) | Implemented CONDITIONAL GO; publishedf9e1d06, remote verified13:26:54UTC; exact Undo allowlist and owner-filtered Restore distinction verified |
|
||||
| [System](SYSTEM.md) | Implemented CONDITIONAL GO;20frontend/46backend/3native plus types/lint/build PASS, final hygiene/publication pending; SMTP drafts and operational scopes preserved |
|
||||
| [System](SYSTEM.md) | Implemented CONDITIONAL GO;20frontend/46backend/3native plus types/lint/build PASS; publishedd824f59, clean direct remote verification13:40:25UTC; SMTP drafts and operational scopes preserved |
|
||||
|
||||
All 57 preservation groups retain **56 PARTIAL / C11 DEFERRED**, with 33 routes. P5-13 is withheld; followMode remains deferred. No Phase 8 implementation. Production readiness remains NO-GO; the independently owner-authorized main release is still unverified after run831 failed with browser crashes/malformed Playwright JavaScript. Do not infer admin recent-auth enforcement: the current Users/Audit/System actions do not implement that separate guard. Preserve actual account-lifecycle/export recent-auth checks and record this gap rather than inventing a security protocol during presentation migration.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# P7-11 — Residual integration
|
||||
|
||||
Entry: P7-10 System `d824f59d2b3e9b680bee5952802aa3c0453aee03`, normally pushed/directly verified with a clean checkout20October13:40:25UTC. CI-only main continuation was merged and pushed as `6db62095c6dc6f7e8275447827c62b09f9b2d2e3`, clean13:40:41UTC. The approved handoff requires responsible-owner repairs, not another migration of completed pages.
|
||||
|
||||
## Recorded baseline and bounded owners
|
||||
|
||||
| Finding | Impact / responsible boundary |
|
||||
|---|---|
|
||||
| Roleless version badge and disabled-control tooltip spans carry unsupported names | Accessibility; shared shell, Checklist and Interview owners |
|
||||
| Add Job and Strategy headings skip semantic levels | Accessibility; existing form owners |
|
||||
| Strategy GET failures become an apparent absent plan | Usability; existing Match-tab Strategy read owner; exact `strategy_not_generated` remains intentional emptiness |
|
||||
| Known template, generated reason/tailoring and history metadata remain English in Bokmål | Localisation; existing gallery/reminder/application/history owners; user and unknown metadata remain verbatim |
|
||||
| Resumed-draft fallback freezes opening language; singular counts use plural English | Localisation; existing correspondence presentation without draft/save protocol changes |
|
||||
| CV private-source failures show raw `Not Found`, lack contextual heading and read retry | Usability; existing CV entry owner, private identifiers and server authorization unchanged |
|
||||
| Unknown route has generic shell title and h4 owner heading | Accessibility/context; existing wildcard route, Home and Back actions retained |
|
||||
| Documents action splits the last letter at1024px | Responsive usability; existing table and action sizing, metadata/query/download contracts unchanged |
|
||||
|
||||
These findings were carried from implemented checkpoints and verified in source, unit regressions and/or browser reproduction. Cosmetic preferences are not additional scope. Analytics already uses the approved DashboardView report owner; it is reused with unchanged periods, cohorts, source links and statistics. Shared loading/feedback and existing confirmations remain; no duplicate framework is introduced.
|
||||
|
||||
## Execution boundaries
|
||||
|
||||
1. [Shared accessibility and forms](SHARED.md), including the paired translation foundation used by the following owners.
|
||||
2. Generated-label localisation follows separately, preserving all stored metadata and user text.
|
||||
3. Documents and error navigation follows separately, including CV history display in its existing owner.
|
||||
|
||||
The independently authorized release repair is separate: development `bba52af4b8724f1a5c74013ce1476a20ca3972c2` was pushed/directly verified18:05:08UTC. Main contains equivalent fixture/focus repair `3e1057d` and Qwen compatibility `f7dc22e`, normally pushed and directly verified20October18:05UTC. Run833 is testing; deployment/model activation are unverified. [Release evidence](../../../infrastructure/2026-10-20-browser-fixture-and-focus-contracts.md) retains run832's314passing/16failed/oneflaky outcome and the two demonstrated causes. No failed assertion was hidden or timeout relaxed.
|
||||
|
||||
## Acceptance boundaries
|
||||
|
||||
P7-11 implementation and publication are in progress. Full integrated acceptance belongs to P7-12. All57 preservation groups retain56 PARTIAL/C11 DEFERRED and33 routes; no capability or route is removed. Manual reader/device/software-keyboard/zoom, production output/provider, reliability and release requirements remain. P5-13 stays withheld; followMode remains deferred. No Phase8 implementation or new dashboard customization. Production readiness remains NO-GO independently of the owner's authorization to release main.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Shared accessibility and forms
|
||||
|
||||
Final modal continuation **5/5 PASS**,18:23:09–18:24:16UTC; combined with the separate Strategy case,26all-enabled scans have0violations/176incomplete observations:172contrast and4hidden-focus MUI menu-transition contexts. The previous dangling-title and roleless Undo/Redo observations are absent in this fresh vector. No human/device acceptance follows.
|
||||
|
||||
**Implemented CONDITIONAL GO; final hygiene/publication pending.** Existing shell/form owners remain; no navigation or mutation protocol replacement.
|
||||
|
||||
The static administrator version badge now has a valid image role and localized version/commit accessible name. Checklist and Interview disabled-control tooltips describe their wrappers rather than placing names on roleless spans; item-specific inner-button names and disabled hover help remain. Add Job step headings are h3 under its dialog title, with progress text a paragraph. Strategy has a contextual h2 in the existing Match tab. Raw incomplete triage also proved Quick Search referenced a nonexistent MUI-generated title ID; its existing explicit name now stands without that dangling reference. Two existing command assertions failed before the correction; all10 command cases pass afterward.
|
||||
|
||||
Strategy's GET read distinguishes failures from exact404 `strategy_not_generated`. Retry only reads the same plan; denial has no retry, locale changes do not refetch, stale previous-job responses cannot replace the current owner, and plan-read completion cannot unlock controls while the independent initial operation read remains pending. Existing Pro admission, generation/operation endpoints and safeguards remain unchanged. No model calls or qualification run.
|
||||
|
||||
Meaningful initial regressions:34passed/11failed across six direct suites; expanded final affected/consumer result **80/80 across11 suites PASS**, TypeScript and targeted lint PASS. An additional English/Bokmål version-label case failed before localization and passed afterward; no net count change is claimed from overlapping runs. The later full run reconciles inventories.
|
||||
|
||||
Five native confirmation consumers pass with explicit named-body and both-button focus traversal. The separate Strategy native workflow passes in English desktop and Bokmål320px dark: controlled503 remains distinct from empty; one keyboard GET retry produces exact not-generated emptiness with preserved focus and zero browser mutations. The first native fixture mistakenly selected Overview; source inspection corrected it to the actual Match tab. That fixture failure is retained and is not a product regression. Automated scans and images are recorded separately from human acceptance.
|
||||
|
||||
Two Strategy final images were actually inspected:1440px English unavailable and320px Bokmål dark exact-empty. Full-page fixed-shell/skip-link capture artifacts retain their limits; source/advert prose remains document content, not translated UI labels. Other images and incomplete contexts are retained. The final modal rerun after Quick Search correction records updated scan totals in [portable results](evidence/shared-results.json); earlier incomplete vectors remain historical, including the CV-toolbar spans corrected by the error-owner boundary. No axe rule is hidden.
|
||||
|
||||
The accompanying59paired translation additions support all three residual boundaries. New labels are presentation-only; unused rows in this first commit do not activate downstream features. All consumers retain existing routes/IDs/actions. Full frontend898/101 and backend1057 pass before final small ARIA continuations; relevant continuation tests pass and P7-12 runs fresh against the committed final source. Shared feedback/confirm infrastructure, renderer geometry, API authorization and P5-13 withholding remain unchanged. Static checks and normal push follow; human/physical/provider/production conditions remain.
|
||||
@@ -0,0 +1,357 @@
|
||||
{
|
||||
"observedUtc": "2026-10-20T18:24:31.227604+00:00",
|
||||
"sourceScope": "Post-run source identities; no claim of pre-run capture or that later catalogue/comment changes were in earlier exports",
|
||||
"sources": {
|
||||
"src/layout/AppShell.tsx": "e75a58f2984bf47d7edc3c2c51439242aba2df32675a30b1f17088095ea1d634",
|
||||
"src/components/QuickCommandDialog.tsx": "27cbbb8b8596fdf4e7474eccfdb6d9a8a219f4baff6a9b8013b825ce5fc2c411",
|
||||
"src/components/AddJobModal.tsx": "a572ab197526a7b8263ba369b7230db7781176f3e820d255e89d7f71bab94c63",
|
||||
"src/components/ApplicationChecklist.tsx": "2c58d440669a76ec1882326e1039ca604f5bf7e38fd71ac6800d22e260a1fcc2",
|
||||
"src/components/InterviewPrep.tsx": "454c97a2cad3a4045fbc301c5da717db79fa41b53a231e9f3f3cc437eb5881d1",
|
||||
"src/components/ApplicationWorkflowAssist.tsx": "7a9cbb1f38879bb9e9156d19675e9db40c466d06008cc5495dfd99b6b4dc088b",
|
||||
"src/i18n/translations.ts": "15cc09b7dfcea607b6a597d1379aaad5d3e1c01adcc518846a9047b398ba5458"
|
||||
},
|
||||
"native": [
|
||||
{
|
||||
"name": "release-modal-contracts",
|
||||
"stats": {
|
||||
"startTime": "2026-10-20T18:23:09.847Z",
|
||||
"duration": 66822.658,
|
||||
"expected": 5,
|
||||
"skipped": 0,
|
||||
"unexpected": 0,
|
||||
"flaky": 0
|
||||
},
|
||||
"path": ".artifacts/phase7/current/ui/release-modal-contracts.json",
|
||||
"sha256": "f5352b3ab09d5f4e3c9c45cea369965efbad6d2211d0fad8b7a42502f4560f91",
|
||||
"command": {
|
||||
"name": "release-modal-contracts",
|
||||
"command": [
|
||||
"rtk",
|
||||
"proxy",
|
||||
"node",
|
||||
"node_modules/@playwright/test/cli.js",
|
||||
"test",
|
||||
"e2e/named-controls-modal-state.spec.ts",
|
||||
"e2e/phase7-discovery.spec.ts",
|
||||
"--grep",
|
||||
"named controls, modal keyboard|keyboard native create",
|
||||
"--retries=0",
|
||||
"--reporter=list,json",
|
||||
"--output=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\ui\\release-modal-contracts-results"
|
||||
],
|
||||
"startedUtc": "2026-10-20T18:23:09.321036+00:00",
|
||||
"finishedUtc": "2026-10-20T18:24:16.708620+00:00",
|
||||
"exitCode": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "strategy-residual",
|
||||
"stats": {
|
||||
"startTime": "2026-10-20T18:10:59.174Z",
|
||||
"duration": 16095.752,
|
||||
"expected": 1,
|
||||
"skipped": 0,
|
||||
"unexpected": 0,
|
||||
"flaky": 0
|
||||
},
|
||||
"path": ".artifacts/phase7/current/ui/strategy-residual.json",
|
||||
"sha256": "f58413b956da9464f07262d328c5f4712cccdc767e9164e4c7dc7fb801ba8106",
|
||||
"command": {
|
||||
"name": "strategy-residual",
|
||||
"command": [
|
||||
"rtk",
|
||||
"proxy",
|
||||
"node",
|
||||
"node_modules/@playwright/test/cli.js",
|
||||
"test",
|
||||
"e2e/phase7-application-forms.spec.ts",
|
||||
"--grep",
|
||||
"Strategy failed reads",
|
||||
"--retries=0",
|
||||
"--reporter=list,json",
|
||||
"--output=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\ui\\strategy-residual-results"
|
||||
],
|
||||
"startedUtc": "2026-10-20T18:10:58.660686+00:00",
|
||||
"finishedUtc": "2026-10-20T18:11:15.305746+00:00",
|
||||
"exitCode": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"images": [
|
||||
{
|
||||
"name": "p7-discovery-nb-light-360-native-create-held",
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/p7-discovery-nb-light-360-native-create-held-d66c78aa.png",
|
||||
"sha256": "d66c78aa7e307ba36246428ebe2275c6dc68d5c5069287f32bf5bdee4e7837d2"
|
||||
},
|
||||
{
|
||||
"name": "p7-discovery-nb-light-360-native-delete-held",
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/p7-discovery-nb-light-360-native-delete-held-ab570faa.png",
|
||||
"sha256": "ab570faa611f3074f8ae2ae2b088b40ff7d5b7572c25531384f57b892316775d"
|
||||
},
|
||||
{
|
||||
"name": "p706-forms-en-light-1440-strategy-read-unavailable",
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/p706-forms-en-light-1440-strategy-read-unavailable-bd863839.png",
|
||||
"sha256": "bd86383954334e34052a6ca5150a08fb5c03824721b0951276145c075da39a7a"
|
||||
},
|
||||
{
|
||||
"name": "p706-forms-en-light-1440-strategy-exact-not-generated",
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/p706-forms-en-light-1440-strategy-exact-not-generated-f2c5bfc1.png",
|
||||
"sha256": "f2c5bfc1b7d4abfba14cf43f9002cc34fec20262d08657d62f6f45ae6b1bd310"
|
||||
},
|
||||
{
|
||||
"name": "p706-forms-nb-dark-320-strategy-read-unavailable",
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/p706-forms-nb-dark-320-strategy-read-unavailable-bdac6183.png",
|
||||
"sha256": "bdac618366e6869d9f141b939e2b525dff88610fe1bf58d4d0279147efd885cb"
|
||||
},
|
||||
{
|
||||
"name": "p706-forms-nb-dark-320-strategy-exact-not-generated",
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/p706-forms-nb-dark-320-strategy-exact-not-generated-1ad739a1.png",
|
||||
"sha256": "1ad739a1fe0133e8228cf5fc2f629e82bda121bfc8d9960c58356afea866c0b3"
|
||||
}
|
||||
],
|
||||
"scans": [
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/p7-discovery-nb-light-360-native-create-held-audit-c82b8641.json",
|
||||
"sha256": "c82b864175ca44673005263b83b9763ed778629430fabb1bf6697aaf12fb36f1",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 1,
|
||||
"incompleteRules": [
|
||||
"color-contrast"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/p7-discovery-nb-light-360-native-delete-held-audit-2e8a5542.json",
|
||||
"sha256": "2e8a55425bca9ad5f5c23c865d940b6b108315372dd9002c0653d3dcaf42b834",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 2,
|
||||
"incompleteRules": [
|
||||
"color-contrast"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/command-axe-647699d7.json",
|
||||
"sha256": "647699d71b0d56b61f3af200452834d81210fa67b33696806f02abb7a8442c61",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 2,
|
||||
"incompleteRules": [
|
||||
"color-contrast"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/cv-controls-axe-c71e3b74.json",
|
||||
"sha256": "c71e3b74f32943b3a75b85b152e8f6a5c704ef0b65440cd0a93f225cf1b0c57e",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 25,
|
||||
"incompleteRules": [
|
||||
"aria-hidden-focus",
|
||||
"color-contrast"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/cover-controls-axe-52c71913.json",
|
||||
"sha256": "52c719132e9830a1736707443438408cf2843f22e508ca1098e567c32bf9f483",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 6,
|
||||
"incompleteRules": [
|
||||
"color-contrast"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/prompt-axe-5a4a000a.json",
|
||||
"sha256": "5a4a000ad4272cd7ac79c5f5f41c00ca6c6a62658912a2409dc17c134c1fe398",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 2,
|
||||
"incompleteRules": [
|
||||
"color-contrast"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/confirm-axe-15ee5431.json",
|
||||
"sha256": "15ee543140cd0d42b7254759ebd4cb045644eca882248c7567e80788bccfa001",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 2,
|
||||
"incompleteRules": [
|
||||
"color-contrast"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/command-axe-5895b5c3.json",
|
||||
"sha256": "5895b5c3a1046273c59c01c000ff183b0b01c34d1590ccd1814e61c5fc6dc87a",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 2,
|
||||
"incompleteRules": [
|
||||
"color-contrast"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/cv-controls-axe-db3f7506.json",
|
||||
"sha256": "db3f7506b91f58b816c29466e7939aa1a1af24e4f5f86a05ffa5782cd0fd02e8",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 30,
|
||||
"incompleteRules": [
|
||||
"aria-hidden-focus",
|
||||
"color-contrast"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/cover-controls-axe-900a7709.json",
|
||||
"sha256": "900a77096f5cef09372362420aaa6f9e52ffe94dc55fd6acb234767f2b70cfb6",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 6,
|
||||
"incompleteRules": [
|
||||
"color-contrast"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/prompt-axe-d2e174df.json",
|
||||
"sha256": "d2e174df033f399a942508acc5e7d046945a228ea022e0408b06945b8b719141",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 2,
|
||||
"incompleteRules": [
|
||||
"color-contrast"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/confirm-axe-15ee5431.json",
|
||||
"sha256": "15ee543140cd0d42b7254759ebd4cb045644eca882248c7567e80788bccfa001",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 2,
|
||||
"incompleteRules": [
|
||||
"color-contrast"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/command-axe-c9062adb.json",
|
||||
"sha256": "c9062adb6dd8206f866ab7af94f0ca38524b7f2478f9d3d0ade0b62405b121a5",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 0,
|
||||
"incompleteRules": []
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/cv-controls-axe-d2dea077.json",
|
||||
"sha256": "d2dea077614afeec933815d852a1ce72b5c63c41fcf3a3139d757d5519cd6087",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 24,
|
||||
"incompleteRules": [
|
||||
"aria-hidden-focus",
|
||||
"color-contrast"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/cover-controls-axe-c180f9b6.json",
|
||||
"sha256": "c180f9b684782328f0a33d32c6fb48705958c9370780bf71c7270f7ff83029b3",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 10,
|
||||
"incompleteRules": [
|
||||
"color-contrast"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/prompt-axe-5a4a000a.json",
|
||||
"sha256": "5a4a000ad4272cd7ac79c5f5f41c00ca6c6a62658912a2409dc17c134c1fe398",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 2,
|
||||
"incompleteRules": [
|
||||
"color-contrast"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/confirm-axe-15ee5431.json",
|
||||
"sha256": "15ee543140cd0d42b7254759ebd4cb045644eca882248c7567e80788bccfa001",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 2,
|
||||
"incompleteRules": [
|
||||
"color-contrast"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/command-axe-c9062adb.json",
|
||||
"sha256": "c9062adb6dd8206f866ab7af94f0ca38524b7f2478f9d3d0ade0b62405b121a5",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 0,
|
||||
"incompleteRules": []
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/cv-controls-axe-549ca659.json",
|
||||
"sha256": "549ca65975a2e785420651c37d0be7e9d3c2da3d83857a396c50a2db339bf591",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 24,
|
||||
"incompleteRules": [
|
||||
"aria-hidden-focus",
|
||||
"color-contrast"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/cover-controls-axe-61e54ade.json",
|
||||
"sha256": "61e54adebc11daf24dfa3d6c4620c65c61a7867dfe671a6b0d2b62e62088105f",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 10,
|
||||
"incompleteRules": [
|
||||
"color-contrast"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/prompt-axe-f58690b5.json",
|
||||
"sha256": "f58690b5f684193dbe77aa4227754fb3b6384abda77c1a977b5d8f5c97135b22",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 2,
|
||||
"incompleteRules": [
|
||||
"color-contrast"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/confirm-axe-15ee5431.json",
|
||||
"sha256": "15ee543140cd0d42b7254759ebd4cb045644eca882248c7567e80788bccfa001",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 2,
|
||||
"incompleteRules": [
|
||||
"color-contrast"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/p706-forms-en-light-1440-strategy-read-unavailable-audit-09114185.json",
|
||||
"sha256": "0911418580ebbad2ad3ea66b75195da55852288738efd81ae8000ccebdfb9a77",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 2,
|
||||
"incompleteRules": [
|
||||
"color-contrast"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/p706-forms-en-light-1440-strategy-exact-not-generated-audit-c68e7baa.json",
|
||||
"sha256": "c68e7baae8e9ebf228061b925af86d7932ee3dfe3f56eab0602a887d3ed8fa8f",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 2,
|
||||
"incompleteRules": [
|
||||
"color-contrast"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/p706-forms-nb-dark-320-strategy-read-unavailable-audit-af80696d.json",
|
||||
"sha256": "af80696d1ff9ff05489d77f869e4375b135ae89f13f5a9cd8ee029e324571968",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 7,
|
||||
"incompleteRules": [
|
||||
"color-contrast"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/phase7/p7-11/shared-visuals/p706-forms-nb-dark-320-strategy-exact-not-generated-audit-ad14073a.json",
|
||||
"sha256": "ad14073a4462a71d5aae93170f6d932bf42cec7241985c78905b181e3dc3e6ce",
|
||||
"violations": 0,
|
||||
"incompleteNodes": 7,
|
||||
"incompleteRules": [
|
||||
"color-contrast"
|
||||
]
|
||||
}
|
||||
],
|
||||
"focusedUnit": {
|
||||
"path": ".artifacts/phase7/p7-11/shared/green.json",
|
||||
"sha256": "4cdfac6b887315b6734b5eb8e6b9d15fadc0d7279dd31f98193db03d895f2b96",
|
||||
"numPassedTests": 80,
|
||||
"numFailedTests": 0,
|
||||
"numPendingTests": 0,
|
||||
"numPassedTestSuites": 11,
|
||||
"qualification": "Overlapping focused vectors; not a new full-suite total. Final integrated run covers subsequent documented corrections."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"checkpoint": "p7-11",
|
||||
"status": "PASS",
|
||||
"checkedUtc": "2026-10-20T18:26:12.103739+00:00",
|
||||
"commands": [
|
||||
{
|
||||
"name": "secret-scan",
|
||||
"command": [
|
||||
"rtk",
|
||||
"proxy",
|
||||
"C:\\Users\\Connor\\AppData\\Local\\Programs\\Python\\Python312\\python.exe",
|
||||
"-X",
|
||||
"utf8",
|
||||
"scripts/supply-chain.py",
|
||||
"scan-secrets"
|
||||
],
|
||||
"startedUtc": "2026-10-20T18:26:08.411065+00:00",
|
||||
"finishedUtc": "2026-10-20T18:26:11.161244+00:00",
|
||||
"exitCode": 0,
|
||||
"log": ".artifacts/phase7/current/static/20261020T182608Z/secret-scan.log",
|
||||
"logSha256Raw": "be5119e6057287d397e87759f855c91e634e3bfac61c8be7584d05592ba2dfcd"
|
||||
},
|
||||
{
|
||||
"name": "working-whitespace",
|
||||
"command": [
|
||||
"rtk",
|
||||
"proxy",
|
||||
"git",
|
||||
"diff",
|
||||
"--check"
|
||||
],
|
||||
"startedUtc": "2026-10-20T18:26:11.162273+00:00",
|
||||
"finishedUtc": "2026-10-20T18:26:11.270209+00:00",
|
||||
"exitCode": 0,
|
||||
"log": ".artifacts/phase7/current/static/20261020T182608Z/working-whitespace.log",
|
||||
"logSha256Raw": "85634f1014a6f42ddd6b5982ccec8a9cc597157a051cf7fd824992355be51c5c"
|
||||
},
|
||||
{
|
||||
"name": "staged-whitespace",
|
||||
"command": [
|
||||
"rtk",
|
||||
"proxy",
|
||||
"git",
|
||||
"diff",
|
||||
"--cached",
|
||||
"--check"
|
||||
],
|
||||
"startedUtc": "2026-10-20T18:26:11.270721+00:00",
|
||||
"finishedUtc": "2026-10-20T18:26:11.372430+00:00",
|
||||
"exitCode": 0,
|
||||
"log": ".artifacts/phase7/current/static/20261020T182608Z/staged-whitespace.log",
|
||||
"logSha256Raw": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"name": "phase7-whitespace",
|
||||
"command": [
|
||||
"rtk",
|
||||
"proxy",
|
||||
"git",
|
||||
"diff",
|
||||
"--check",
|
||||
"b5319981ad3d1bd635daff861453d23c0de5320b"
|
||||
],
|
||||
"startedUtc": "2026-10-20T18:26:11.372430+00:00",
|
||||
"finishedUtc": "2026-10-20T18:26:11.609452+00:00",
|
||||
"exitCode": 0,
|
||||
"log": ".artifacts/phase7/current/static/20261020T182608Z/phase7-whitespace.log",
|
||||
"logSha256Raw": "4d9b55a18a0e78345259dd2290d41d06f98d6ae3cec9e60a218744e90d41667a"
|
||||
}
|
||||
],
|
||||
"untrackedSecretScan": {
|
||||
"files": 7,
|
||||
"findings": 0
|
||||
},
|
||||
"documentation": {
|
||||
"files": 38,
|
||||
"relativeLinks": 642,
|
||||
"errors": []
|
||||
},
|
||||
"preservation": {
|
||||
"groups": 57,
|
||||
"statuses": {
|
||||
"PARTIAL": 56,
|
||||
"PASS": 0,
|
||||
"FAIL": 0,
|
||||
"DEFERRED": 1
|
||||
},
|
||||
"originalRoutesPreserved": true,
|
||||
"currentConcreteRoutes": 33
|
||||
},
|
||||
"production": {
|
||||
"main": "f7dc22e5e7efe1fd1d64f6ace2c04cf2f95bf558",
|
||||
"originMain": "f7dc22e5e7efe1fd1d64f6ace2c04cf2f95bf558",
|
||||
"status": "NO-GO",
|
||||
"qualification": "Local fetched refs; direct push observation recorded separately."
|
||||
},
|
||||
"hashPolicy": "Portable JSON UTF8 LF; raw log hashes measure exact saved bytes.",
|
||||
"limits": [
|
||||
"Pattern scanner skips binary and files over2MiB; not historical Git/production secrets.",
|
||||
"Local links check target existence, not URL/heading anchors.",
|
||||
"Static evidence is not runtime/manual/device/provider/production acceptance."
|
||||
]
|
||||
}
|
||||
@@ -93,7 +93,7 @@ Follow the approved [Phase 7 handoff](../redesign/phase-6/p6-05/PHASE7_HANDOFF.m
|
||||
- [x] P7-07 [owner-scoped Documents index](../redesign/phase-7/p7-07/README.md): implemented CONDITIONAL GO, final hygiene PASS; UI5a67ec3 and evidence84350b7 published through9dfea12, directly verified21:01:11UTC. Finite86/8+native13/types/lint/build/18images/15axe0/101qualified;33routes/57statuses retained. MariaDB/human/device/production qualifications remain.
|
||||
- [x] P7-08 [existing notifications and operations](../redesign/phase-7/p7-08/README.md): bothpresentationboundariesimplementedCONDITIONALGO. UTC9dfea/recent40cfe04 published; Operations77/7+native12+types/lint/build/14Ops+1P2actualimages PASS;12Opsaxe0/3+14notifaxe0/102qualified. FinalOpshygiene/normalpushpending.791/95frontend323browser1054backend derivedonly;57groups33routes/mainunchanged/productionNO-GO. P7-09next.
|
||||
- [x] P7-09 [Settings, Connections and data lifecycle](../redesign/phase-7/p7-09/README.md): implemented CONDITIONAL GO. Sender/Preferences/import/company published; remaining data presentation47/6 affected frontend,22backendcontracts, final native23/types/lint/build PASS. 37axe0violations/141incomplete nodes qualified; six final PNGs inspected. Final hygiene/publication recorded in checkpoint ledger. Inventory855/99frontend,339browser,1057backend; no fresh whole-suite claim. P5-13 withheld, manual/provider/output/release qualifications retained.
|
||||
- [x] P7-10 Users7ecc0d6/4760fd1 and Auditf9e1d06 published; System20frontend/46backend/3native plus types/lint/build PASS, final hygiene/publication pending. Implemented CONDITIONAL GO; shared/manual accessibility conditions remain.
|
||||
- [x] P7-10 Users7ecc0d6/4760fd1, Auditf9e1d06 and Systemd824f59 published/directly verified; implemented CONDITIONAL GO. Shared/manual accessibility conditions remain.
|
||||
- [ ] P7-11 residual Analytics/error/empty/dialog integration.
|
||||
- [ ] P7-12 integrated migration acceptance and Phase 8 handoff.
|
||||
|
||||
|
||||
@@ -220,6 +220,34 @@ async function capture(page: Page, info: TestInfo, sample: Sample, state: string
|
||||
expect(geometry.rootOverflow).toBe(0); expect(axe.violations).toEqual([]);
|
||||
}
|
||||
|
||||
test('[P7-11 E08] Strategy failed reads remain distinct from exact not-generated state and retry only GET in both locales', async ({ page, session }, info) => {
|
||||
const job = await createJob(page.request, session.owner, 'Recorded Strategy read recovery');
|
||||
let unavailable = true;
|
||||
await page.route(`**/api/jobapplications/${job.id}/focus-plan`, route => {
|
||||
expect(route.request().method()).toBe('GET');
|
||||
return route.fulfill(unavailable ? { status: 503, json: { title: 'Controlled unavailable Strategy read' } } : { status: 404, json: { code: 'strategy_not_generated' } });
|
||||
});
|
||||
for (const sample of [desktop, { language: 'nb', theme: 'dark', width: 320, height: 844 }] as Sample[]) {
|
||||
unavailable = true;
|
||||
await enter(page, session.owner, job, 'answers', sample);
|
||||
await keyboard(page, page.getByRole('tablist', { name: translations[sample.language].workspaceSections, exact: true }).getByRole('tab', { name: translations[sample.language].workspaceMatch, exact: true }));
|
||||
const t = translations[sample.language];
|
||||
const error = page.getByRole('alert').filter({ hasText: t.workspaceStrategyLoadFailed });
|
||||
await expect(error).toBeVisible();
|
||||
await expect(page.getByText(t.workspaceStrategyEmpty, { exact: true })).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: t.workspaceStrategyProRequired, exact: true })).toBeDisabled();
|
||||
await capture(page, info, sample, 'strategy-read-unavailable');
|
||||
const before = count(session, 'GET', `/api/jobapplications/${job.id}/focus-plan`).length;
|
||||
unavailable = false;
|
||||
await keyboard(page, error.getByRole('button', { name: t.retry, exact: true }));
|
||||
await expect(page.getByRole('heading', { name: t.workspaceStrategyTitle, level: 2, exact: true })).toBeFocused();
|
||||
await expect(page.getByText(t.workspaceStrategyEmpty, { exact: true })).toBeVisible();
|
||||
expect(count(session, 'GET', `/api/jobapplications/${job.id}/focus-plan`).length).toBe(before + 1);
|
||||
await capture(page, info, sample, 'strategy-exact-not-generated');
|
||||
}
|
||||
expect(mutations(session)).toEqual([]);
|
||||
});
|
||||
|
||||
test('[P7-06 forms] held CV read Retry preserves its request and actual attach/replace/detach remain owner scoped', async ({ page, session, playwright }, info) => {
|
||||
test.setTimeout(120_000); await source(page.request, session.owner);
|
||||
const job = await createJob(page.request, session.owner); const first = await variant(page.request, session.owner, 'First reusable CV'); const second = await variant(page.request, session.owner, 'Second reusable CV');
|
||||
|
||||
@@ -44,7 +44,8 @@ test("notification bell exposes its unread count and keyboard-accessible action"
|
||||
expect(open.mock.calls[0][0]).toBeInstanceOf(HTMLElement);
|
||||
});
|
||||
|
||||
test("shows build metadata only when the caller supplies the admin-only badge", () => {
|
||||
test.each([["en", "Application version 2.4.1, commit abc1234"], ["nb", "Applikasjonsversjon 2.4.1, revisjon abc1234"]])("shows localized %s build metadata only when the caller supplies the admin-only badge", (language, label) => {
|
||||
localStorage.setItem("uiLanguage", language);
|
||||
const commonProps = {
|
||||
pageTitle: "Dashboard",
|
||||
breadcrumbs: ["Home"],
|
||||
@@ -67,7 +68,8 @@ test("shows build metadata only when the caller supplies the admin-only badge",
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("admin-version-badge")).toHaveTextContent("v2.4.1");
|
||||
expect(screen.getByLabelText("Application version 2.4.1, commit abc1234")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(label)).toBeInTheDocument();
|
||||
expect(screen.getByRole("img", { name: label })).toBe(screen.getByTestId("admin-version-badge"));
|
||||
|
||||
rerender(
|
||||
<CssVarsProvider theme={getTheme("light") as any} defaultMode="light">
|
||||
|
||||
@@ -122,6 +122,20 @@ test("reordering sends the new id order", async () => {
|
||||
expect(mockedApi.put).toHaveBeenCalledWith("/jobapplications/7/checklist/order", [2, 1, 3]));
|
||||
});
|
||||
|
||||
test("move and remove tooltips describe their wrappers without replacing item-specific button names", async () => {
|
||||
mockedApi.patch.mockReturnValueOnce(new Promise(() => {}));
|
||||
renderChecklist();
|
||||
const down = await screen.findByRole("button", { name: "Move down: Prepare a CV for this role" });
|
||||
for (const label of ["Move up: Prepare a CV for this role", "Move down: Prepare a CV for this role", "Remove: Prepare a CV for this role"]) {
|
||||
expect(screen.getByRole("button", { name: label }).parentElement).not.toHaveAttribute("aria-label");
|
||||
}
|
||||
fireEvent.click(screen.getByRole("checkbox", { name: "Prepare a CV for this role" }));
|
||||
expect(down).toBeDisabled();
|
||||
fireEvent.mouseOver(down.parentElement!);
|
||||
expect(await screen.findByRole("tooltip")).toHaveTextContent("Move down");
|
||||
expect(mockedApi.put).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("removing an item calls delete", async () => {
|
||||
mockedApi.delete.mockResolvedValue({ data: undefined } as any);
|
||||
|
||||
|
||||
@@ -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 } from '@testing-library/react';
|
||||
|
||||
import { api } from './api';
|
||||
import { AccountPlanProvider } from './accountPlan';
|
||||
@@ -15,7 +15,7 @@ jest.mock('./api', () => ({
|
||||
|
||||
const mockedApi = api as jest.Mocked<typeof api>;
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
beforeEach(() => { jest.clearAllMocks(); localStorage.clear(); });
|
||||
|
||||
test('recruiter status suggestions require an explicit apply action', async () => {
|
||||
const applied = jest.fn();
|
||||
@@ -50,8 +50,56 @@ test('strategy snapshot shows saved output and queues regeneration only on reque
|
||||
|
||||
render(<I18nProvider><ToastProvider><AccountPlanProvider value={{ plan: 'pro', canUseAi: true, canUseProThemes: true }}><ApplicationStrategySnapshot jobId={42} /></AccountPlanProvider></ToastProvider></I18nProvider>);
|
||||
expect(await screen.findByText('Lead with delivery evidence.')).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: 'Strategy snapshot', level: 2 })).toBeVisible();
|
||||
expect(mockedApi.post).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Regenerate' }));
|
||||
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith('/jobapplications/42/focus-plan/operations', { attachmentIds: null }));
|
||||
expect(await screen.findByText('queued')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
function strategy(jobId = 42) {
|
||||
return <I18nProvider><ToastProvider><AccountPlanProvider value={{ plan: 'free', canUseAi: false, canUseProThemes: false }}><ApplicationStrategySnapshot jobId={jobId} /></AccountPlanProvider></ToastProvider></I18nProvider>;
|
||||
}
|
||||
|
||||
test('failed strategy reads stay distinct from empty, localize without refetching and retry only the read', async () => {
|
||||
let finishOperation!: (result: any) => void;
|
||||
mockedApi.get.mockImplementation((url: string) => url.endsWith('/operation') ? new Promise(resolve => { finishOperation = resolve; }) : Promise.reject({ response: { status: 503 } }));
|
||||
render(strategy());
|
||||
expect(await screen.findByText('Strategy snapshot could not be loaded.')).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: 'Retry' })).toBeDisabled();
|
||||
await act(async () => finishOperation({ data: null }));
|
||||
expect(screen.queryByText('No strategy snapshot yet. Generate one when you want AI-assisted planning.')).not.toBeInTheDocument();
|
||||
expect(mockedApi.post).not.toHaveBeenCalled();
|
||||
const calls = mockedApi.get.mock.calls.length;
|
||||
act(() => window.dispatchEvent(new StorageEvent('storage', { key: 'uiLanguage', newValue: 'nb' })));
|
||||
expect(await screen.findByText('Kunne ikke hente strategisammendraget.')).toBeVisible();
|
||||
expect(mockedApi.get).toHaveBeenCalledTimes(calls);
|
||||
mockedApi.get.mockResolvedValueOnce({ data: { strategicSummary: 'Saved plan', immediatePriorities: [], proofPointsToLeadWith: [], cvBulletIdeas: [], coverLetterAngles: [], followUpApproach: [] } } as any);
|
||||
const retry = screen.getByRole('button', { name: 'Prøv igjen' });
|
||||
act(() => retry.focus()); fireEvent.click(retry);
|
||||
expect(await screen.findByDisplayValue('Saved plan')).toBeVisible();
|
||||
expect(mockedApi.get.mock.calls.slice(calls).map(call => call[0])).toEqual(['/jobapplications/42/focus-plan']);
|
||||
expect(screen.getByRole('heading', { name: 'Strategioversikt', level: 2 })).toHaveFocus();
|
||||
expect(mockedApi.post).not.toHaveBeenCalled();
|
||||
expect(screen.getByRole('button', { name: 'Krever Pro' })).toBeDisabled();
|
||||
});
|
||||
|
||||
test('only the explicit not-generated response is an intentional empty strategy', async () => {
|
||||
mockedApi.get.mockRejectedValue({ response: { status: 404, data: { code: 'strategy_not_generated' } } });
|
||||
render(strategy());
|
||||
expect(await screen.findByText('No strategy snapshot yet. Generate one when you want AI-assisted planning.')).toBeVisible();
|
||||
expect(screen.queryByRole('button', { name: 'Retry' })).not.toBeInTheDocument();
|
||||
expect(mockedApi.post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('a late strategy response cannot replace another job and denial never offers read retry', async () => {
|
||||
let finish!: (result: any) => void;
|
||||
mockedApi.get.mockImplementation((url: string) => url === '/jobapplications/42/focus-plan' ? new Promise(resolve => { finish = resolve; }) : Promise.reject({ response: { status: 403 } }));
|
||||
const view = render(strategy());
|
||||
view.rerender(strategy(43));
|
||||
expect(await screen.findByText('Strategy snapshot could not be loaded.')).toBeVisible();
|
||||
await act(async () => finish({ data: { strategicSummary: 'Do not show old owner plan', immediatePriorities: [], proofPointsToLeadWith: [], cvBulletIdeas: [], coverLetterAngles: [], followUpApproach: [] } }));
|
||||
expect(screen.queryByDisplayValue('Do not show old owner plan')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Retry' })).not.toBeInTheDocument();
|
||||
expect(mockedApi.post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -445,13 +445,13 @@ export default function AddJobModal({ open, onClose, onCreated, initialUrl }: Pr
|
||||
<Stepper activeStep={activeStep} alternativeLabel sx={{ mb: 3, display: { xs: "none", sm: "flex" } }}>
|
||||
{steps.map((label) => <Step key={label}><StepLabel>{label}</StepLabel></Step>)}
|
||||
</Stepper>
|
||||
<Typography variant="subtitle2" sx={{ display: { xs: "block", sm: "none" }, mb: 2 }}>
|
||||
<Typography component="p" variant="subtitle2" sx={{ display: { xs: "block", sm: "none" }, mb: 2 }}>
|
||||
{t("addJobModalStepProgress", { current: activeStep + 1, total: steps.length, step: steps[activeStep] })}
|
||||
</Typography>
|
||||
|
||||
{activeStep === 0 ? <Box sx={{ display: "grid", gap: 2 }}>
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 900 }}>{t("addJobModalStartWithAdvert")}</Typography>
|
||||
<Typography component="h3" variant="h6" sx={{ fontWeight: 900 }}>{t("addJobModalStartWithAdvert")}</Typography>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", mt: 0.5 }}>
|
||||
{t("addJobModalStartWithAdvertBody")}
|
||||
</Typography>
|
||||
@@ -609,7 +609,7 @@ export default function AddJobModal({ open, onClose, onCreated, initialUrl }: Pr
|
||||
|
||||
{activeStep >= 2 ? <Box sx={{ display: "grid", gap: 2 }}>
|
||||
<Box>
|
||||
<Typography variant="h6" sx={{ fontWeight: 900 }}>{steps[activeStep]}</Typography>
|
||||
<Typography component="h3" variant="h6" sx={{ fontWeight: 900 }}>{steps[activeStep]}</Typography>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", mt: 0.5 }}>
|
||||
{activeStep === 2 ? t("addJobModalCvStepHelp") : null}
|
||||
{activeStep === 3 ? t("addJobModalCoverLetterStepHelp") : null}
|
||||
|
||||
@@ -150,21 +150,21 @@ export default function ApplicationChecklist({ jobId, onChanged }: { jobId: numb
|
||||
)}
|
||||
</Box>
|
||||
<Stack direction="row" className="checklist-actions" sx={{ opacity: { xs: 1, md: 0 }, transition: "opacity .15s" }}>
|
||||
<Tooltip title={t("checklistMoveUp")}>
|
||||
<Tooltip describeChild title={t("checklistMoveUp")}>
|
||||
<span>
|
||||
<IconButton size="small" disabled={busy} aria-label={t("checklistMoveUpItem", { title: display.title })} onClick={() => move(item, -1)}>
|
||||
<ArrowUpwardIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("checklistMoveDown")}>
|
||||
<Tooltip describeChild title={t("checklistMoveDown")}>
|
||||
<span>
|
||||
<IconButton size="small" disabled={busy} aria-label={t("checklistMoveDownItem", { title: display.title })} onClick={() => move(item, 1)}>
|
||||
<ArrowDownwardIcon fontSize="inherit" />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip title={item.isSystemGenerated ? t("checklistNotRelevant") : t("deleteAction")}>
|
||||
<Tooltip describeChild title={item.isSystemGenerated ? t("checklistNotRelevant") : t("deleteAction")}>
|
||||
<span>
|
||||
<IconButton size="small" disabled={busy} aria-label={t("checklistRemoveItem", { title: display.title })} onClick={() => remove(item)}>
|
||||
<DeleteOutlineIcon fontSize="inherit" />
|
||||
|
||||
@@ -62,19 +62,41 @@ export function ApplicationStrategySnapshot({ jobId }: { jobId: number }) {
|
||||
const [plan, setPlan] = useState<FocusPlanResponse | null>(null);
|
||||
const [operation, setOperation] = useState<UserOperation | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [readingPlan, setReadingPlan] = useState(false);
|
||||
const [readError, setReadError] = useState<{ cause: unknown; retryable: boolean } | null>(null);
|
||||
const announced = useRef<string | null>(null);
|
||||
const owner = useRef({ mounted: false, jobId, generation: 0 });
|
||||
const readSequence = useRef(0);
|
||||
const readPending = useRef(false);
|
||||
const headingRef = useRef<HTMLHeadingElement>(null);
|
||||
|
||||
const loadPlan = useCallback(async () => {
|
||||
if (!owner.current.mounted || owner.current.jobId !== jobId || readPending.current) return;
|
||||
const lifetime = owner.current.generation;
|
||||
const request = ++readSequence.current;
|
||||
const active = () => owner.current.mounted && owner.current.jobId === jobId && owner.current.generation === lifetime && request === readSequence.current;
|
||||
readPending.current = true;
|
||||
setReadingPlan(true);
|
||||
try {
|
||||
const { data } = await api.get<FocusPlanResponse>(`/jobapplications/${jobId}/focus-plan`);
|
||||
if (!active()) return;
|
||||
setPlan(data);
|
||||
} catch {
|
||||
setPlan(null);
|
||||
setReadError(null);
|
||||
} catch (cause) {
|
||||
if (!active()) return;
|
||||
const response = (cause as { response?: { status?: number; data?: { code?: string } } })?.response;
|
||||
if (response?.status === 404 && response.data?.code === "strategy_not_generated") { setPlan(null); setReadError(null); }
|
||||
else setReadError({ cause, retryable: !response?.status || response.status >= 500 });
|
||||
} finally {
|
||||
if (active()) { readPending.current = false; setReadingPlan(false); }
|
||||
}
|
||||
}, [jobId]);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
owner.current = { mounted: true, jobId, generation: owner.current.generation + 1 };
|
||||
readPending.current = false;
|
||||
setPlan(null); setReadError(null); setOperation(null);
|
||||
setLoading(true);
|
||||
Promise.all([
|
||||
loadPlan(),
|
||||
@@ -82,17 +104,18 @@ export function ApplicationStrategySnapshot({ jobId }: { jobId: number }) {
|
||||
.then(({ data }) => { if (active) setOperation(data); })
|
||||
.catch(() => { if (active) setOperation(null); }),
|
||||
]).finally(() => { if (active) setLoading(false); });
|
||||
return () => { active = false; };
|
||||
return () => { active = false; owner.current.mounted = false; owner.current.generation += 1; readSequence.current += 1; readPending.current = false; };
|
||||
}, [jobId, loadPlan]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!operation || terminal(operation.status)) return;
|
||||
let active = true;
|
||||
const timer = window.setTimeout(() => {
|
||||
api.get<UserOperation>(`/operations/${operation.id}`)
|
||||
.then(({ data }) => setOperation(data))
|
||||
.then(({ data }) => { if (active) setOperation(data); })
|
||||
.catch(() => undefined);
|
||||
}, 1000);
|
||||
return () => window.clearTimeout(timer);
|
||||
return () => { active = false; window.clearTimeout(timer); };
|
||||
}, [operation]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -133,17 +156,24 @@ export function ApplicationStrategySnapshot({ jobId }: { jobId: number }) {
|
||||
};
|
||||
|
||||
const working = !!operation && !terminal(operation.status);
|
||||
const reading = loading || readingPlan;
|
||||
const readErrorTitle = t("workspaceStrategyLoadFailed");
|
||||
const readErrorDetail = readError ? getApiErrorMessage(readError.cause, readErrorTitle) : "";
|
||||
return (
|
||||
<Paper sx={{ p: { xs: 2, sm: 2.5 }, borderRadius: 3 }}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" gap={1} sx={{ mb: 2 }}>
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 800 }}>{t("workspaceStrategyTitle")}</Typography>
|
||||
<Typography ref={headingRef} tabIndex={-1} component="h2" variant="subtitle2" sx={{ fontWeight: 800 }}>{t("workspaceStrategyTitle")}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">{t("workspaceStrategySubtitle")}</Typography>
|
||||
</Box>
|
||||
<Button variant="outlined" size="small" disabled={!canUseAi || loading || working} onClick={() => void generate()}>
|
||||
<Button variant="outlined" size="small" disabled={!canUseAi || reading || working} onClick={() => void generate()}>
|
||||
{!canUseAi ? t("workspaceStrategyProRequired") : plan ? t("workspaceStrategyRegenerate") : t("workspaceStrategyGenerate")}
|
||||
</Button>
|
||||
</Stack>
|
||||
{readError ? <Alert severity="error" sx={{ mb: 2, overflowWrap: "anywhere" }} action={readError.retryable ? <Button color="inherit" size="small" disabled={reading} onClick={() => { headingRef.current?.focus(); void loadPlan(); }}>{t("retry")}</Button> : undefined}>
|
||||
<Typography variant="body2">{readErrorTitle}</Typography>
|
||||
{readErrorDetail !== readErrorTitle ? <Typography variant="body2">{readErrorDetail}</Typography> : null}
|
||||
</Alert> : null}
|
||||
{operation && operation.status !== "succeeded" ? (
|
||||
<Alert severity={operation.status === "failed" ? "error" : operation.status === "cancelled" ? "warning" : "info"} sx={{ mb: 2 }} action={<>
|
||||
{operation.canCancel ? <Button size="small" color="inherit" onClick={() => void mutateOperation("cancel")}>{t("cancel")}</Button> : null}
|
||||
@@ -152,14 +182,14 @@ export function ApplicationStrategySnapshot({ jobId }: { jobId: number }) {
|
||||
{operation.progressStage || operation.status.replaceAll("_", " ")}{operation.progressPercent != null ? ` · ${operation.progressPercent}%` : ""}
|
||||
</Alert>
|
||||
) : null}
|
||||
{loading && !plan ? <Box sx={{ py: 4, display: "flex", justifyContent: "center" }}><CircularProgress size={28} /></Box> : plan ? (
|
||||
{reading && !plan ? <Box sx={{ py: 4, display: "flex", justifyContent: "center" }}><CircularProgress size={28} aria-label={t("loading")} /></Box> : plan ? (
|
||||
<Stack spacing={2}>
|
||||
<DraftCard title={t("jobDetailsFocusSummary")} content={plan.strategicSummary} />
|
||||
<TwoColumnSection leftTitle={t("jobDetailsImmediatePriorities")} leftItems={plan.immediatePriorities} rightTitle={t("jobDetailsProofPoints")} rightItems={plan.proofPointsToLeadWith} />
|
||||
<TwoColumnSection leftTitle={t("jobDetailsCvBulletIdeas")} leftItems={plan.cvBulletIdeas} rightTitle={t("jobDetailsCoverLetterAngles")} rightItems={plan.coverLetterAngles} />
|
||||
<ListCard title={t("jobDetailsFollowUpApproach")} items={plan.followUpApproach} />
|
||||
</Stack>
|
||||
) : <Typography color="text.secondary">{t("workspaceStrategyEmpty")}</Typography>}
|
||||
) : !readError ? <Typography color="text.secondary">{t("workspaceStrategyEmpty")}</Typography> : null}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -381,7 +381,7 @@ function PrepRow({ jobId, item, busy, onChanged, onError }: {
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
<Tooltip title={t("interviewPrepDelete")}>
|
||||
<Tooltip describeChild title={t("interviewPrepDelete")}>
|
||||
<span>
|
||||
<IconButton
|
||||
size="small"
|
||||
|
||||
@@ -147,7 +147,7 @@ export default function QuickCommandDialog({ open, onClose, onNavigate, onOpenAd
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} slotProps={{ paper: { "aria-label": t("quickSearch") }, transition: { onEntered: () => searchInputRef.current?.focus() } }} fullWidth maxWidth="sm">
|
||||
<Dialog open={open} onClose={onClose} slotProps={{ paper: { "aria-label": t("quickSearch"), "aria-labelledby": undefined }, transition: { onEntered: () => searchInputRef.current?.focus() } }} fullWidth maxWidth="sm">
|
||||
<DialogContent onKeyDown={onKeys} sx={{ p: 1.5, maxHeight: "min(70vh, 620px)" }}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1, px: 1, pt: 0.5 }}>
|
||||
<SearchIcon fontSize="small" />
|
||||
|
||||
@@ -61,6 +61,8 @@ test.each([0, 1, 2, 3])("Add Job attachment bucket %s retains keyboard selection
|
||||
await screen.findByDisplayValue("Supported role");
|
||||
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
|
||||
for (let step = 0; step < bucket; step += 1) fireEvent.click(screen.getByRole("button", { name: "Skip and continue" }));
|
||||
const headings = screen.getAllByRole("heading").map(heading => heading.tagName);
|
||||
expect(headings).toEqual(["H2", "H3"]);
|
||||
const input = document.querySelector<HTMLInputElement>('input[type="file"]')!;
|
||||
expect(input.multiple).toBe(true);
|
||||
expect(input.accept).toContain(".pdf,.doc,.docx,.txt,.md");
|
||||
|
||||
@@ -5,6 +5,7 @@ export const translations = {
|
||||
appTitle: "Jobbjakt",
|
||||
appTagline: "Track your hunt",
|
||||
applicationLanguage: "Application language",
|
||||
applicationVersionCommit: "Application version {version}, commit {commit}",
|
||||
dashboard: "Dashboard",
|
||||
jobApplications: "Job Applications",
|
||||
reminders: "Reminders",
|
||||
@@ -130,6 +131,61 @@ export const translations = {
|
||||
correspondenceInbox: "Job email",
|
||||
emailInboxSubtitle: "Review linked correspondence and suggested recruitment messages in one place.",
|
||||
emailInboxItems: "{count} items",
|
||||
emailInboxItem: "{count} item",
|
||||
emailInboxAttachmentCount: "{count} attachment",
|
||||
historySourceManual: "Manual",
|
||||
historySourceAi: "AI suggestion",
|
||||
historySourceTemplate: "Template",
|
||||
historySourceRestore: "Restored version",
|
||||
historySourceAutosave: "Autosave",
|
||||
historySourceCreate: "Created",
|
||||
cvTemplateModernName: "Modern",
|
||||
cvTemplateModernDescription: "Clean SaaS-style layout with a confident accent header.",
|
||||
cvTemplateMinimalName: "Minimal",
|
||||
cvTemplateMinimalDescription: "Quiet, spacious, typography-first. Nothing competes with the content.",
|
||||
cvTemplateExecutiveName: "Executive",
|
||||
cvTemplateExecutiveDescription: "High-contrast, serif, leadership-weighted. For senior and client-facing roles.",
|
||||
cvTemplateTechnicalName: "Technical",
|
||||
cvTemplateTechnicalDescription: "Dense two-column layout tuned for engineering CVs — skills and projects up front.",
|
||||
cvTemplateAtsClassicName: "ATS Classic",
|
||||
cvTemplateAtsClassicDescription: "Single column, no graphics, maximum parser compatibility. The safe default.",
|
||||
cvTemplateNordicName: "Nordic",
|
||||
cvTemplateNordicDescription: "Calm cool-blue sidebar, generous whitespace, Scandinavian restraint.",
|
||||
cvTemplateElegantName: "Elegant",
|
||||
cvTemplateElegantDescription: "Editorial serif headings over sans body, premium spacing and a plum accent.",
|
||||
cvTemplateCreativeName: "Creative",
|
||||
cvTemplateCreativeDescription: "Bold accent sidebar and photo-forward header for design and product roles.",
|
||||
cvTemplateCodeName: "Code",
|
||||
cvTemplateCodeDescription: "Developer-focused editorial layout with numbered sections and precise mono details.",
|
||||
cvTemplateCategoryModern: "Modern Professional",
|
||||
cvTemplateCategoryMinimal: "Minimal",
|
||||
cvTemplateCategoryExecutive: "Executive",
|
||||
cvTemplateCategoryTechnical: "Technical",
|
||||
cvTemplateCategoryAts: "ATS Professional",
|
||||
cvTemplateCategoryCreative: "Creative",
|
||||
assetsTailoringHighlightTitle: "Skills to highlight",
|
||||
assetsTailoringHighlightDetail: "The advert asks for these and your profile already has them — put them where they are seen first.",
|
||||
assetsTailoringExperienceTitle: "Experience to prioritise",
|
||||
assetsTailoringExperienceDetail: "Ordered by how much of the advert each role actually covers.",
|
||||
assetsTailoringProjectsTitle: "Projects to emphasise",
|
||||
assetsTailoringProjectsDetail: "These projects demonstrate what the advert is asking for.",
|
||||
assetsTailoringEvidenceTitle: "Verified evidence to reuse",
|
||||
assetsTailoringEvidenceDetail: "Facts you previously approved. Adapt the wording, but keep the claim accurate.",
|
||||
assetsTailoringKeywordsTitle: "Keywords to include",
|
||||
assetsTailoringKeywordsDetail: "Vocabulary from the advert. Use the ones that are honestly true of you — never pad.",
|
||||
assetsTailoringGapsTitle: "Gaps to address",
|
||||
assetsTailoringGapsDetail: "Asked for but not found in your profile. Add them if you have them; otherwise be ready to talk about them.",
|
||||
reminderReasonInterview: "Interview stage reached but prep notes are still missing.",
|
||||
reminderReasonCvAndAnswers: "Tailored CV and saved application answers still need work.",
|
||||
reminderReasonCv: "Tailored CV missing for this role.",
|
||||
reminderReasonAnswers: "Saved application answers still need work.",
|
||||
reminderReasonUnscheduled: "No response yet and no follow-up is scheduled.",
|
||||
reminderReasonDue: "Follow-up is due for this role.",
|
||||
reminderReasonNoGaps: "No urgent workflow gaps are blocking this job right now.",
|
||||
reminderReasonNoReplyDays: "No reply after {days}d",
|
||||
reminderReasonStalledDays: "Stalled after {days}d",
|
||||
reminderReasonFeedbackDays: "Feedback requested {days}d ago",
|
||||
reminderReasonWaitingDays: "Waiting {days}d",
|
||||
emailInboxLinked: "{count} linked",
|
||||
emailInboxLinkedOnPage: "{count} linked on this page",
|
||||
emailInboxInboundCount: "{count} inbound",
|
||||
@@ -869,6 +925,8 @@ export const translations = {
|
||||
cvEditorShowPhoto: "Show profile photo",
|
||||
cvEditorContactIcons: "Contact icons",
|
||||
cvEditorOpenFailed: "Could not open this CV.",
|
||||
cvEditorSourceUnavailable: "This CV is unavailable or you no longer have access to it.",
|
||||
cvEditorPermissionDenied: "You do not have permission to open this CV.",
|
||||
cvEditorDiscardMessage: "This CV has unsaved changes. Leave and discard them?",
|
||||
cvEditorDiscardTitle: "Discard unsaved CV changes?",
|
||||
cvEditorDiscardLeave: "Discard and leave",
|
||||
@@ -2759,6 +2817,7 @@ export const translations = {
|
||||
workspaceStatusSuggestionApplied: "Application status updated from the latest message.",
|
||||
workspaceStatusSuggestionFailed: "Could not apply the suggested status.",
|
||||
workspaceStrategyTitle: "Strategy snapshot",
|
||||
workspaceStrategyLoadFailed: "Strategy snapshot could not be loaded.",
|
||||
workspaceStrategySubtitle: "An on-demand plan grounded in this advert and your saved career data.",
|
||||
workspaceStrategyProRequired: "Pro required",
|
||||
workspaceStrategyGenerate: "Generate",
|
||||
@@ -2787,6 +2846,7 @@ export const translations = {
|
||||
appTitle: "Jobbjakt",
|
||||
appTagline: "Hold oversikt over jobbsøkingen",
|
||||
applicationLanguage: "Språk i appen",
|
||||
applicationVersionCommit: "Applikasjonsversjon {version}, revisjon {commit}",
|
||||
dashboard: "Dashboard",
|
||||
jobApplications: "Jobbsøknader",
|
||||
reminders: "Påminnelser",
|
||||
@@ -2912,6 +2972,61 @@ export const translations = {
|
||||
correspondenceInbox: "Jobb-e-post",
|
||||
emailInboxSubtitle: "Se tilknyttet korrespondanse og foreslåtte rekrutteringsmeldinger på ett sted.",
|
||||
emailInboxItems: "{count} elementer",
|
||||
emailInboxItem: "{count} element",
|
||||
emailInboxAttachmentCount: "{count} vedlegg",
|
||||
historySourceManual: "Manuell",
|
||||
historySourceAi: "KI-forslag",
|
||||
historySourceTemplate: "Mal",
|
||||
historySourceRestore: "Gjenopprettet versjon",
|
||||
historySourceAutosave: "Automatisk lagring",
|
||||
historySourceCreate: "Opprettet",
|
||||
cvTemplateModernName: "Moderne",
|
||||
cvTemplateModernDescription: "Ren SaaS-inspirert utforming med en tydelig topptekst i aksentfarge.",
|
||||
cvTemplateMinimalName: "Minimalistisk",
|
||||
cvTemplateMinimalDescription: "Rolig og luftig, med typografien i sentrum. Ingenting tar oppmerksomheten fra innholdet.",
|
||||
cvTemplateExecutiveName: "Leder",
|
||||
cvTemplateExecutiveDescription: "Høy kontrast og serifftype med vekt på ledelse. For seniorroller og kundevendte roller.",
|
||||
cvTemplateTechnicalName: "Teknisk",
|
||||
cvTemplateTechnicalDescription: "Kompakt tokolonneutforming for tekniske CV-er, med ferdigheter og prosjekter først.",
|
||||
cvTemplateAtsClassicName: "ATS klassisk",
|
||||
cvTemplateAtsClassicDescription: "Én kolonne uten grafikk, med best mulig støtte for CV-tolkere. Et trygt standardvalg.",
|
||||
cvTemplateNordicName: "Nordisk",
|
||||
cvTemplateNordicDescription: "Rolig sidekolonne i kjølig blått, god luft og skandinavisk enkelhet.",
|
||||
cvTemplateElegantName: "Elegant",
|
||||
cvTemplateElegantDescription: "Redaksjonelle serifoverskrifter over brødtekst uten seriffer, god luft og en plommefarget aksent.",
|
||||
cvTemplateCreativeName: "Kreativ",
|
||||
cvTemplateCreativeDescription: "Tydelig sidekolonne i aksentfarge og topptekst med vekt på bilde, for design- og produktroller.",
|
||||
cvTemplateCodeName: "Kode",
|
||||
cvTemplateCodeDescription: "Redaksjonell utforming for utviklere med nummererte seksjoner og presise detaljer i fastbreddeskrift.",
|
||||
cvTemplateCategoryModern: "Moderne og profesjonell",
|
||||
cvTemplateCategoryMinimal: "Minimalistisk",
|
||||
cvTemplateCategoryExecutive: "Leder",
|
||||
cvTemplateCategoryTechnical: "Teknisk",
|
||||
cvTemplateCategoryAts: "ATS-profesjonell",
|
||||
cvTemplateCategoryCreative: "Kreativ",
|
||||
assetsTailoringHighlightTitle: "Ferdigheter å fremheve",
|
||||
assetsTailoringHighlightDetail: "Annonsen etterspør disse, og de finnes allerede i profilen din. Plasser dem der de blir sett først.",
|
||||
assetsTailoringExperienceTitle: "Erfaring å prioritere",
|
||||
assetsTailoringExperienceDetail: "Sortert etter hvor mye av annonsen hver rolle faktisk dekker.",
|
||||
assetsTailoringProjectsTitle: "Prosjekter å fremheve",
|
||||
assetsTailoringProjectsDetail: "Disse prosjektene viser det annonsen etterspør.",
|
||||
assetsTailoringEvidenceTitle: "Bekreftet dokumentasjon å gjenbruke",
|
||||
assetsTailoringEvidenceDetail: "Fakta du tidligere har godkjent. Tilpass ordlyden, men behold en korrekt påstand.",
|
||||
assetsTailoringKeywordsTitle: "Nøkkelord å ta med",
|
||||
assetsTailoringKeywordsDetail: "Ord fra annonsen. Bruk dem som faktisk stemmer for deg, uten å pynte på innholdet.",
|
||||
assetsTailoringGapsTitle: "Mangler å ta tak i",
|
||||
assetsTailoringGapsDetail: "Etterspurt, men ikke funnet i profilen din. Legg dem til hvis du har dem, ellers bør du være forberedt på å snakke om dem.",
|
||||
reminderReasonInterview: "Du har kommet til intervju, men mangler fortsatt forberedelsesnotater.",
|
||||
reminderReasonCvAndAnswers: "Tilpasset CV og lagrede søknadssvar trenger fortsatt arbeid.",
|
||||
reminderReasonCv: "Tilpasset CV mangler for denne rollen.",
|
||||
reminderReasonAnswers: "Lagrede søknadssvar trenger fortsatt arbeid.",
|
||||
reminderReasonUnscheduled: "Ingen respons ennå, og ingen oppfølging er planlagt.",
|
||||
reminderReasonDue: "Det er tid for oppfølging av denne rollen.",
|
||||
reminderReasonNoGaps: "Ingen akutte mangler i arbeidsflyten blokkerer denne jobben nå.",
|
||||
reminderReasonNoReplyDays: "Ingen svar etter {days} dager",
|
||||
reminderReasonStalledDays: "Stoppet opp etter {days} dager",
|
||||
reminderReasonFeedbackDays: "Tilbakemelding ble forespurt for {days} dager siden",
|
||||
reminderReasonWaitingDays: "Ventet i {days} dager",
|
||||
emailInboxLinked: "{count} tilknyttet",
|
||||
emailInboxLinkedOnPage: "{count} tilknyttet på denne siden",
|
||||
emailInboxInboundCount: "{count} innkommende",
|
||||
@@ -3651,6 +3766,8 @@ export const translations = {
|
||||
cvEditorShowPhoto: "Vis profilbilde",
|
||||
cvEditorContactIcons: "Kontaktikoner",
|
||||
cvEditorOpenFailed: "Kunne ikke åpne denne CV-en.",
|
||||
cvEditorSourceUnavailable: "Denne CV-en er utilgjengelig, eller du har ikke lenger tilgang til den.",
|
||||
cvEditorPermissionDenied: "Du har ikke tilgang til å åpne denne CV-en.",
|
||||
cvEditorDiscardMessage: "Denne CV-en har ulagrede endringer. Vil du forlate siden og forkaste dem?",
|
||||
cvEditorDiscardTitle: "Forkaste ulagrede CV-endringer?",
|
||||
cvEditorDiscardLeave: "Forkast og forlat",
|
||||
@@ -5541,6 +5658,7 @@ export const translations = {
|
||||
workspaceStatusSuggestionApplied: "Søknadsstatusen ble oppdatert fra den siste meldingen.",
|
||||
workspaceStatusSuggestionFailed: "Kunne ikke bruke den foreslåtte statusen.",
|
||||
workspaceStrategyTitle: "Strategioversikt",
|
||||
workspaceStrategyLoadFailed: "Kunne ikke hente strategisammendraget.",
|
||||
workspaceStrategySubtitle: "En behovsstyrt plan basert på denne annonsen og dine lagrede karrieredata.",
|
||||
workspaceStrategyProRequired: "Krever Pro",
|
||||
workspaceStrategyGenerate: "Generer",
|
||||
|
||||
@@ -86,6 +86,19 @@ test("prep renders grouped items with progress and marks AI-sourced ones", async
|
||||
expect(screen.getByText("From AI")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("delete tooltip describes its wrapper while the disabled button keeps its item name", async () => {
|
||||
routeGet();
|
||||
mockedApi.patch.mockReturnValueOnce(new Promise(() => {}));
|
||||
render(<ApplicationInterviewPrep jobId={7} />);
|
||||
const remove = await screen.findByRole("button", { name: "Delete: Funding history" });
|
||||
expect(remove.parentElement).not.toHaveAttribute("aria-label");
|
||||
fireEvent.click(screen.getByRole("checkbox", { name: "Ready: Funding history" }));
|
||||
expect(remove).toBeDisabled();
|
||||
fireEvent.mouseOver(remove.parentElement!);
|
||||
expect(await screen.findByRole("tooltip")).toHaveTextContent("Delete");
|
||||
expect(mockedApi.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("adding a prep item posts the chosen category and title", async () => {
|
||||
routeGet();
|
||||
mockedApi.post.mockResolvedValue({ data: board.groups[0].items[0] } as any);
|
||||
|
||||
@@ -340,10 +340,11 @@ export default function AppShell({
|
||||
const buildVersion = buildMetadata?.version.trim();
|
||||
const buildCommit = buildMetadata?.commitSha?.trim();
|
||||
const buildBadge = buildVersion ? (
|
||||
<Tooltip title={buildCommit ? `Version ${buildVersion} • commit ${buildCommit}` : `Version ${buildVersion}`}>
|
||||
<Tooltip title={buildCommit ? t("applicationVersionCommit", { version: buildVersion, commit: buildCommit }) : t("applicationVersion", { version: buildVersion })}>
|
||||
<Chip
|
||||
data-testid="admin-version-badge"
|
||||
aria-label={`Application version ${buildVersion}${buildCommit ? `, commit ${buildCommit}` : ""}`}
|
||||
role="img"
|
||||
aria-label={buildCommit ? t("applicationVersionCommit", { version: buildVersion, commit: buildCommit }) : t("applicationVersion", { version: buildVersion })}
|
||||
label={`v${buildVersion}`}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
|
||||
@@ -74,6 +74,7 @@ test('localizes the job creation workflow in Norwegian Bokmål', async () => {
|
||||
renderModal();
|
||||
|
||||
expect(await screen.findByText('Start med jobbannonsen')).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: 'Start med jobbannonsen', level: 3 })).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: 'Skriv inn detaljer manuelt' })).toBeInTheDocument();
|
||||
expect(screen.getByText('Trinn 1 av 6: Legg til jobb')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -24,6 +24,8 @@ test.each([
|
||||
const onClose = jest.fn();
|
||||
render(<I18nProvider><QuickCommandDialog open onClose={onClose} onNavigate={onNavigate} onOpenAddJob={jest.fn()} /></I18nProvider>);
|
||||
expect(screen.getByRole("dialog", { name: "Quick Search" })).toBeVisible();
|
||||
const labelIds = screen.getByRole("dialog", { name: "Quick Search" }).getAttribute("aria-labelledby");
|
||||
expect(!labelIds || labelIds.split(/\s+/).every(id => document.getElementById(id))).toBe(true);
|
||||
fireEvent.change(screen.getByRole("textbox", { name: "Quick Search" }), { target: { value: "Nordlys" } });
|
||||
fireEvent.click(await screen.findByText(label, { exact: true }));
|
||||
expect(onNavigate).toHaveBeenCalledWith(destination);
|
||||
|
||||
Reference in New Issue
Block a user