",
+ "target": [
+ ".MuiPaper-outlined"
+ ]
+ }
+ ]
+ },
+ {
+ "attachment": "p706-forms-en-light-320-long-answers-audit",
+ "target": [
+ ".css-1vcc3k.MuiBox-root:nth-child(4) > .css-b7tg9p.MuiStack-root > .css-1etkf64.MuiTypography-caption"
+ ],
+ "data": {
+ "contrastRatio": 0,
+ "fontSize": "9.0pt (12px)",
+ "fontWeight": "normal",
+ "messageKey": "elmPartiallyObscured",
+ "expectedContrastRatio": "4.5:1"
+ },
+ "relatedNodes": [
+ {
+ "html": "
",
+ "target": [
+ ".MuiPaper-outlined"
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "kind": "incomplete",
+ "rule": "color-contrast",
+ "check": "color-contrast",
+ "message": "Element's background color could not be determined because it partially overlaps other elements",
+ "occurrences": 1,
+ "representativeContexts": [
+ {
+ "attachment": "p706-forms-en-light-1440-controlled-language503-dirty-answer-drafts-audit",
+ "target": [
+ ".MuiAlert-message"
+ ],
+ "data": {
+ "contrastRatio": 0,
+ "fontSize": "10.5pt (14px)",
+ "fontWeight": "normal",
+ "messageKey": "elmPartiallyObscuring",
+ "expectedContrastRatio": "4.5:1"
+ },
+ "relatedNodes": []
+ }
+ ]
+ }
+ ],
+ "qualification": "Every violation/incomplete category counted; complete node/check vectors remain exact raw report and extracted JSON. Parent contextual triage pending."
+ },
+ "asRunQualification": "Final parent product/export; native test-only company reuse and Home correction passed browsertypes after build, strict locale failure retained."
+ },
+ "application-forms-regression": {
+ "command": {
+ "name": "application-forms-regression",
+ "command": [
+ "rtk",
+ "proxy",
+ "node",
+ "node_modules/@playwright/test/cli.js",
+ "test",
+ "e2e/job-sections-presentation.spec.ts",
+ "--retries=0",
+ "--reporter=list,json",
+ "--output=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\ui\\application-forms-regression-results"
+ ],
+ "startedUtc": "2026-10-13T21:21:45.248272+00:00",
+ "finishedUtc": "2026-10-13T21:23:27.806820+00:00",
+ "exitCode": 0
+ },
+ "commandRaw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms-regression-command.json",
+ "sha256": "1e2f27caa587ed05e626c6b16173dcf539d8f242a8ccb5996ce26cdcf69db07d",
+ "bytes": 517,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "reportRaw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms-regression.json",
+ "sha256": "f03fd7c502e9c0d04b5b3685bb227795b577acf69d70b4b1ce29d9159712f305",
+ "bytes": 137701,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "logRaw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms-regression.log",
+ "sha256": "692e11fd61f071b2fbf39b2659d4f03d8519f1ce6601c9f108916a346141d898",
+ "bytes": 1400,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "stats": {
+ "startTime": "2026-10-13T21:21:45.760Z",
+ "duration": 102005.338,
+ "expected": 5,
+ "skipped": 0,
+ "unexpected": 0,
+ "flaky": 0
+ },
+ "cases": [
+ {
+ "title": "[PRES-W01 W08 M01 E08] Job section loading and partial read failures stay explicit and recover locally",
+ "file": "job-sections-presentation.spec.ts",
+ "line": 13,
+ "expectedStatus": "passed",
+ "project": "chromium",
+ "resultStatuses": [
+ "passed"
+ ],
+ "retries": [
+ 0
+ ],
+ "errors": [
+ []
+ ],
+ "durationMs": 14746
+ },
+ {
+ "title": "[PRES-W01–W11 E08 E09] native Job sections retain drafts, frozen documents, tasks and history (en, light)",
+ "file": "job-sections-presentation.spec.ts",
+ "line": 62,
+ "expectedStatus": "passed",
+ "project": "chromium",
+ "resultStatuses": [
+ "passed"
+ ],
+ "retries": [
+ 0
+ ],
+ "errors": [
+ []
+ ],
+ "durationMs": 22935
+ },
+ {
+ "title": "[PRES-W01–W11 E08 E09] native Job sections retain drafts, frozen documents, tasks and history (en, dark)",
+ "file": "job-sections-presentation.spec.ts",
+ "line": 62,
+ "expectedStatus": "passed",
+ "project": "chromium",
+ "resultStatuses": [
+ "passed"
+ ],
+ "retries": [
+ 0
+ ],
+ "errors": [
+ []
+ ],
+ "durationMs": 18824
+ },
+ {
+ "title": "[PRES-W01–W11 E08 E09] native Job sections retain drafts, frozen documents, tasks and history (nb, light)",
+ "file": "job-sections-presentation.spec.ts",
+ "line": 62,
+ "expectedStatus": "passed",
+ "project": "chromium",
+ "resultStatuses": [
+ "passed"
+ ],
+ "retries": [
+ 0
+ ],
+ "errors": [
+ []
+ ],
+ "durationMs": 18839
+ },
+ {
+ "title": "[PRES-W01–W11 E08 E09] native Job sections retain drafts, frozen documents, tasks and history (nb, dark)",
+ "file": "job-sections-presentation.spec.ts",
+ "line": 62,
+ "expectedStatus": "passed",
+ "project": "chromium",
+ "resultStatuses": [
+ "passed"
+ ],
+ "retries": [
+ 0
+ ],
+ "errors": [
+ []
+ ],
+ "durationMs": 19405
+ }
+ ],
+ "reportErrors": [],
+ "configuration": {
+ "version": "1.62.1",
+ "workers": 1,
+ "projects": [
+ {
+ "name": "chromium",
+ "retries": 0
+ }
+ ]
+ },
+ "audit": {
+ "violationNodes": 0,
+ "incompleteNodes": 89,
+ "checkContexts": [
+ {
+ "kind": "incomplete",
+ "rule": "color-contrast",
+ "check": "color-contrast",
+ "message": "Element content is too short to determine if it is actual text content",
+ "occurrences": 8,
+ "representativeContexts": [
+ {
+ "attachment": "sections-documents-en-light-axe",
+ "target": [
+ ".MuiBadge-badge"
+ ],
+ "data": {
+ "contrastRatio": 0,
+ "fontSize": "9.0pt (12px)",
+ "fontWeight": "normal",
+ "messageKey": "shortTextContent",
+ "expectedContrastRatio": "4.5:1"
+ },
+ "relatedNodes": []
+ },
+ {
+ "attachment": "sections-correspondence-en-light-axe",
+ "target": [
+ ".MuiBadge-badge"
+ ],
+ "data": {
+ "contrastRatio": 0,
+ "fontSize": "9.0pt (12px)",
+ "fontWeight": "normal",
+ "messageKey": "shortTextContent",
+ "expectedContrastRatio": "4.5:1"
+ },
+ "relatedNodes": []
+ }
+ ]
+ },
+ {
+ "kind": "incomplete",
+ "rule": "color-contrast",
+ "check": "color-contrast",
+ "message": "Element's background color could not be determined because it is overlapped by another element",
+ "occurrences": 15,
+ "representativeContexts": [
+ {
+ "attachment": "sections-documents-en-light-axe",
+ "target": [
+ ".MuiSelect-select"
+ ],
+ "data": {
+ "contrastRatio": 0,
+ "fontSize": "10.5pt (14px)",
+ "fontWeight": "normal",
+ "messageKey": "bgOverlap",
+ "expectedContrastRatio": "4.5:1"
+ },
+ "relatedNodes": []
+ },
+ {
+ "attachment": "sections-correspondence-en-light-axe",
+ "target": [
+ "#_r_11_"
+ ],
+ "data": {
+ "contrastRatio": 0,
+ "fontSize": "10.5pt (14px)",
+ "fontWeight": "normal",
+ "messageKey": "bgOverlap",
+ "expectedContrastRatio": "4.5:1"
+ },
+ "relatedNodes": []
+ }
+ ]
+ },
+ {
+ "kind": "incomplete",
+ "rule": "aria-prohibited-attr",
+ "check": "aria-prohibited-attr",
+ "message": "aria-label attribute is not well supported on a span with no valid role attribute.",
+ "occurrences": 46,
+ "representativeContexts": [
+ {
+ "attachment": "sections-notes-tasks-en-light-axe",
+ "target": [
+ "section[aria-labelledby=\"_r_1e_\"] > .css-31l7gp.MuiBox-root > .css-qfnqh.MuiStack-root > .css-1c9ky8i.MuiStack-root:nth-child(1) > .checklist-actions.css-1iqpi3n.MuiStack-root > span[aria-label=\"Move up\"]"
+ ],
+ "data": {
+ "role": null,
+ "nodeName": "span",
+ "messageKey": "noRoleSingular",
+ "prohibited": [
+ "aria-label"
+ ]
+ },
+ "relatedNodes": []
+ },
+ {
+ "attachment": "sections-notes-tasks-en-light-axe",
+ "target": [
+ "section[aria-labelledby=\"_r_1e_\"] > .css-31l7gp.MuiBox-root > .css-qfnqh.MuiStack-root > .css-1c9ky8i.MuiStack-root:nth-child(1) > .checklist-actions.css-1iqpi3n.MuiStack-root > span[aria-label=\"Move down\"]"
+ ],
+ "data": {
+ "role": null,
+ "nodeName": "span",
+ "messageKey": "noRoleSingular",
+ "prohibited": [
+ "aria-label"
+ ]
+ },
+ "relatedNodes": []
+ }
+ ]
+ },
+ {
+ "kind": "incomplete",
+ "rule": "color-contrast",
+ "check": "color-contrast",
+ "message": "Element's background color could not be determined because it's partially obscured by another element",
+ "occurrences": 20,
+ "representativeContexts": [
+ {
+ "attachment": "sections-axe-en-light",
+ "target": [
+ ".css-1vcc3k.MuiBox-root:nth-child(3) > .css-b7tg9p.MuiStack-root > .css-1etkf64.MuiTypography-caption"
+ ],
+ "data": {
+ "contrastRatio": 0,
+ "fontSize": "9.0pt (12px)",
+ "fontWeight": "normal",
+ "messageKey": "elmPartiallyObscured",
+ "expectedContrastRatio": "4.5:1"
+ },
+ "relatedNodes": [
+ {
+ "html": "
",
+ "target": [
+ ".css-qkj143"
+ ]
+ }
+ ]
+ },
+ {
+ "attachment": "sections-axe-en-light",
+ "target": [
+ ".css-1vcc3k.MuiBox-root:nth-child(4) > .css-b7tg9p.MuiStack-root > .css-1etkf64.MuiTypography-caption"
+ ],
+ "data": {
+ "contrastRatio": 0,
+ "fontSize": "9.0pt (12px)",
+ "fontWeight": "normal",
+ "messageKey": "elmPartiallyObscured",
+ "expectedContrastRatio": "4.5:1"
+ },
+ "relatedNodes": [
+ {
+ "html": "
",
+ "target": [
+ ".css-qkj143"
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "qualification": "Every violation/incomplete category counted; complete node/check vectors remain exact raw report and extracted JSON. Parent contextual triage pending."
+ },
+ "asRunQualification": "Fresh final parent product/export; ordinary existing test."
+ },
+ "application-p2d01": {
+ "command": {
+ "name": "application-p2d01",
+ "command": [
+ "rtk",
+ "proxy",
+ "node",
+ "node_modules/@playwright/test/cli.js",
+ "test",
+ "e2e/redesign-preservation.spec.ts",
+ "--grep",
+ "legacy workspace preserves frozen documents",
+ "--retries=0",
+ "--reporter=list,json",
+ "--output=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\ui\\application-p2d01-results"
+ ],
+ "startedUtc": "2026-10-13T21:23:27.812337+00:00",
+ "finishedUtc": "2026-10-13T21:23:39.684013+00:00",
+ "exitCode": 0
+ },
+ "commandRaw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-p2d01-command.json",
+ "sha256": "600c7c150960b8b9aac319da3ba549dfbe119c96a461607c178dbf067520ccc7",
+ "bytes": 558,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "reportRaw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-p2d01.json",
+ "sha256": "54d529745d4221464505f6ade506c73de5687a998b60f60ccaebdb6373aefc86",
+ "bytes": 153080,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "logRaw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-p2d01.log",
+ "sha256": "c67916207cdb6ddc60f82aa42de2992412f2c5902b05bf1fae728afac224265c",
+ "bytes": 674,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "stats": {
+ "startTime": "2026-10-13T21:23:28.353Z",
+ "duration": 11298.247,
+ "expected": 1,
+ "skipped": 0,
+ "unexpected": 0,
+ "flaky": 0
+ },
+ "cases": [
+ {
+ "title": "[PRES-W01 PRES-W05 PRES-W06 PRES-W07 PRES-W08] legacy workspace preserves frozen documents after current sources change",
+ "file": "redesign-preservation.spec.ts",
+ "line": 73,
+ "expectedStatus": "passed",
+ "project": "chromium",
+ "resultStatuses": [
+ "passed"
+ ],
+ "retries": [
+ 0
+ ],
+ "errors": [
+ []
+ ],
+ "durationMs": 4346
+ }
+ ],
+ "reportErrors": [],
+ "configuration": {
+ "version": "1.62.1",
+ "workers": 1,
+ "projects": [
+ {
+ "name": "chromium",
+ "retries": 0
+ }
+ ]
+ },
+ "audit": {
+ "violationNodes": 0,
+ "incompleteNodes": 0,
+ "checkContexts": [],
+ "qualification": "Every violation/incomplete category counted; complete node/check vectors remain exact raw report and extracted JSON. Parent contextual triage pending."
+ },
+ "asRunQualification": "Fresh final parent product/export; ordinary existing test."
+ },
+ "application-rich-cv": {
+ "command": {
+ "name": "application-rich-cv",
+ "command": [
+ "rtk",
+ "proxy",
+ "node",
+ "node_modules/@playwright/test/cli.js",
+ "test",
+ "e2e/cv-editor-frame.spec.ts",
+ "--grep",
+ "CV frame",
+ "--retries=0",
+ "--reporter=list,json",
+ "--output=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\ui\\application-rich-cv-results"
+ ],
+ "startedUtc": "2026-10-13T16:43:40.252898+00:00",
+ "finishedUtc": "2026-10-13T16:45:01.237686+00:00",
+ "exitCode": 0
+ },
+ "commandRaw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-rich-cv-command.json",
+ "sha256": "832f07a9ac1454050c562f4a8910ecfc759958d0e9437de17fccfba4515072bc",
+ "bytes": 521,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "reportRaw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-rich-cv.json",
+ "sha256": "c58f08ba2a88c939b9d6b815adf5885b5c0325afead49553042d174ffe26bbca",
+ "bytes": 136481,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "logRaw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-rich-cv.log",
+ "sha256": "24da6708301e9070765f42c7a2d80c46d7e8effaaa2b614a262c1b4e12dc377a",
+ "bytes": 1104,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "stats": {
+ "startTime": "2026-10-13T16:43:40.794Z",
+ "duration": 80399.18,
+ "expected": 4,
+ "skipped": 0,
+ "unexpected": 0,
+ "flaky": 0
+ },
+ "cases": [
+ {
+ "title": "[PRES-C05 C06 C07 C10 W05 E08] light/en CV frame navigation, actions and pending editing",
+ "file": "cv-editor-frame.spec.ts",
+ "line": 23,
+ "expectedStatus": "passed",
+ "project": "chromium",
+ "resultStatuses": [
+ "passed"
+ ],
+ "retries": [
+ 0
+ ],
+ "errors": [
+ []
+ ],
+ "durationMs": 18720
+ },
+ {
+ "title": "[PRES-C05 C06 C07 C10 W05 E08] dark/nb CV frame navigation, actions and pending editing",
+ "file": "cv-editor-frame.spec.ts",
+ "line": 23,
+ "expectedStatus": "passed",
+ "project": "chromium",
+ "resultStatuses": [
+ "passed"
+ ],
+ "retries": [
+ 0
+ ],
+ "errors": [
+ []
+ ],
+ "durationMs": 17684
+ },
+ {
+ "title": "[PRES-C05 C06 C07 C10 W05 E08] light/nb CV frame navigation, actions and pending editing",
+ "file": "cv-editor-frame.spec.ts",
+ "line": 23,
+ "expectedStatus": "passed",
+ "project": "chromium",
+ "resultStatuses": [
+ "passed"
+ ],
+ "retries": [
+ 0
+ ],
+ "errors": [
+ []
+ ],
+ "durationMs": 18306
+ },
+ {
+ "title": "[PRES-C05 C06 C07 C10 W05 E08] dark/en CV frame navigation, actions and pending editing",
+ "file": "cv-editor-frame.spec.ts",
+ "line": 23,
+ "expectedStatus": "passed",
+ "project": "chromium",
+ "resultStatuses": [
+ "passed"
+ ],
+ "retries": [
+ 0
+ ],
+ "errors": [
+ []
+ ],
+ "durationMs": 18321
+ }
+ ],
+ "reportErrors": [],
+ "configuration": {
+ "version": "1.62.1",
+ "workers": 1,
+ "projects": [
+ {
+ "name": "chromium",
+ "retries": 0
+ }
+ ]
+ },
+ "audit": {
+ "violationNodes": 0,
+ "incompleteNodes": 121,
+ "checkContexts": [
+ {
+ "kind": "incomplete",
+ "rule": "aria-prohibited-attr",
+ "check": "aria-prohibited-attr",
+ "message": "aria-label attribute is not well supported on a span with no valid role attribute.",
+ "occurrences": 24,
+ "representativeContexts": [
+ {
+ "attachment": "desktop-content-axe",
+ "target": [
+ "span[aria-label=\"Undo\"]"
+ ],
+ "data": {
+ "role": null,
+ "nodeName": "span",
+ "messageKey": "noRoleSingular",
+ "prohibited": [
+ "aria-label"
+ ]
+ },
+ "relatedNodes": []
+ },
+ {
+ "attachment": "desktop-content-axe",
+ "target": [
+ "span[aria-label=\"Redo\"]"
+ ],
+ "data": {
+ "role": null,
+ "nodeName": "span",
+ "messageKey": "noRoleSingular",
+ "prohibited": [
+ "aria-label"
+ ]
+ },
+ "relatedNodes": []
+ }
+ ]
+ },
+ {
+ "kind": "incomplete",
+ "rule": "color-contrast",
+ "check": "color-contrast",
+ "message": "Element content is too short to determine if it is actual text content",
+ "occurrences": 12,
+ "representativeContexts": [
+ {
+ "attachment": "desktop-content-axe",
+ "target": [
+ ".MuiBadge-badge"
+ ],
+ "data": {
+ "contrastRatio": 0,
+ "fontSize": "9.0pt (12px)",
+ "fontWeight": "normal",
+ "messageKey": "shortTextContent",
+ "expectedContrastRatio": "4.5:1"
+ },
+ "relatedNodes": []
+ },
+ {
+ "attachment": "history-axe",
+ "target": [
+ ".MuiBadge-badge"
+ ],
+ "data": {
+ "contrastRatio": 0,
+ "fontSize": "9.0pt (12px)",
+ "fontWeight": "normal",
+ "messageKey": "shortTextContent",
+ "expectedContrastRatio": "4.5:1"
+ },
+ "relatedNodes": []
+ }
+ ]
+ },
+ {
+ "kind": "incomplete",
+ "rule": "color-contrast",
+ "check": "color-contrast",
+ "message": "Element's background color could not be determined because it is overlapped by another element",
+ "occurrences": 77,
+ "representativeContexts": [
+ {
+ "attachment": "desktop-content-axe",
+ "target": [
+ "#_r_10_"
+ ],
+ "data": {
+ "contrastRatio": 0,
+ "fontSize": "10.5pt (14px)",
+ "fontWeight": "normal",
+ "messageKey": "bgOverlap",
+ "expectedContrastRatio": "4.5:1"
+ },
+ "relatedNodes": []
+ },
+ {
+ "attachment": "history-axe",
+ "target": [
+ ".css-15ja76x"
+ ],
+ "data": {
+ "contrastRatio": 0,
+ "fontSize": "9.8pt (13px)",
+ "fontWeight": "normal",
+ "messageKey": "bgOverlap",
+ "expectedContrastRatio": "4.5:1"
+ },
+ "relatedNodes": []
+ }
+ ]
+ },
+ {
+ "kind": "incomplete",
+ "rule": "color-contrast",
+ "check": "color-contrast",
+ "message": "Element's background color could not be determined because it's partially obscured by another element",
+ "occurrences": 8,
+ "representativeContexts": [
+ {
+ "attachment": "desktop-content-axe",
+ "target": [
+ "iframe[title=\"CV preview page 1\"]",
+ "h1"
+ ],
+ "data": {
+ "contrastRatio": 0,
+ "fontSize": "24.0pt (32px)",
+ "fontWeight": "bold",
+ "messageKey": "elmPartiallyObscured",
+ "expectedContrastRatio": "3:1"
+ },
+ "relatedNodes": [
+ {
+ "html": "
",
+ "target": [
+ "iframe[title=\"CV preview page 1\"]",
+ "main"
+ ]
+ }
+ ]
+ },
+ {
+ "attachment": "history-axe",
+ "target": [
+ "iframe[title=\"CV preview page 1\"]",
+ "h1"
+ ],
+ "data": {
+ "contrastRatio": 0,
+ "fontSize": "24.0pt (32px)",
+ "fontWeight": "bold",
+ "messageKey": "elmPartiallyObscured",
+ "expectedContrastRatio": "3:1"
+ },
+ "relatedNodes": [
+ {
+ "html": "",
+ "target": [
+ "iframe[title=\"CV preview page 1\"]",
+ "main"
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "qualification": "Every violation/incomplete category counted; complete node/check vectors remain exact raw report and extracted JSON. Parent contextual triage pending."
+ },
+ "asRunQualification": "Earlier _Hza-JrlN1-7iFYcv7mKU export; current shared RichText and CV/Career bytes unchanged; later parent/ApplicationAssets-only changes do not enter these consumers. Not final-build reruns."
+ },
+ "application-rich-career": {
+ "command": {
+ "name": "application-rich-career",
+ "command": [
+ "rtk",
+ "proxy",
+ "node",
+ "node_modules/@playwright/test/cli.js",
+ "test",
+ "e2e/phase7-career-editor.spec.ts",
+ "--retries=0",
+ "--reporter=list,json",
+ "--output=E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\ui\\application-rich-career-results"
+ ],
+ "startedUtc": "2026-10-13T16:45:01.238686+00:00",
+ "finishedUtc": "2026-10-13T16:45:50.312791+00:00",
+ "exitCode": 0
+ },
+ "commandRaw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-rich-career-command.json",
+ "sha256": "7bf52ca8ff578d10a7003306ec87c6b91f449e68e7d671ec33f64962351a0f07",
+ "bytes": 502,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "reportRaw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-rich-career.json",
+ "sha256": "fb503724efff57129907ef5e54b383bfb039d3db53d9447d84557efe418ddbbf",
+ "bytes": 4601190,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "logRaw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-rich-career.log",
+ "sha256": "6b01588aebaeb2762e7e0a76fe401ef2444f04700529889e00c566e6f3271d71",
+ "bytes": 2464,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "stats": {
+ "startTime": "2026-10-13T16:45:01.771Z",
+ "duration": 48484.881,
+ "expected": 11,
+ "skipped": 0,
+ "unexpected": 0,
+ "flaky": 0
+ },
+ "cases": [
+ {
+ "title": "[P7-03 PRES-C01 C02 E08] structured and raw drafts survive English to Bokmål without canonical refetch or write",
+ "file": "phase7-career-editor.spec.ts",
+ "line": 161,
+ "expectedStatus": "passed",
+ "project": "chromium",
+ "resultStatuses": [
+ "passed"
+ ],
+ "retries": [
+ 0
+ ],
+ "errors": [
+ []
+ ],
+ "durationMs": 4428
+ },
+ {
+ "title": "[P7-03 PRES-C01 C02 E08] held save locks edits and duplicate writes; failed save retains its snapshot for explicit identical-body retry",
+ "file": "phase7-career-editor.spec.ts",
+ "line": 211,
+ "expectedStatus": "passed",
+ "project": "chromium",
+ "resultStatuses": [
+ "passed"
+ ],
+ "retries": [
+ 0
+ ],
+ "errors": [
+ []
+ ],
+ "durationMs": 7919
+ },
+ {
+ "title": "[P7-03 PRES-C01 C02 E08] actual validation400 retains draft and source; corrected explicit save persists once",
+ "file": "phase7-career-editor.spec.ts",
+ "line": 321,
+ "expectedStatus": "passed",
+ "project": "chromium",
+ "resultStatuses": [
+ "passed"
+ ],
+ "retries": [
+ 0
+ ],
+ "errors": [
+ []
+ ],
+ "durationMs": 4120
+ },
+ {
+ "title": "[P7-03 PRES-C01 C02 E08] structured editor headings, contextual actions and geometry nb/dark/320x844",
+ "file": "phase7-career-editor.spec.ts",
+ "line": 381,
+ "expectedStatus": "passed",
+ "project": "chromium",
+ "resultStatuses": [
+ "passed"
+ ],
+ "retries": [
+ 0
+ ],
+ "errors": [
+ []
+ ],
+ "durationMs": 2866
+ },
+ {
+ "title": "[P7-03 PRES-C01 C02 E08] structured editor headings, contextual actions and geometry en/light/360x800",
+ "file": "phase7-career-editor.spec.ts",
+ "line": 381,
+ "expectedStatus": "passed",
+ "project": "chromium",
+ "resultStatuses": [
+ "passed"
+ ],
+ "retries": [
+ 0
+ ],
+ "errors": [
+ []
+ ],
+ "durationMs": 3004
+ },
+ {
+ "title": "[P7-03 PRES-C01 C02 E08] structured editor headings, contextual actions and geometry nb/light/390x844",
+ "file": "phase7-career-editor.spec.ts",
+ "line": 381,
+ "expectedStatus": "passed",
+ "project": "chromium",
+ "resultStatuses": [
+ "passed"
+ ],
+ "retries": [
+ 0
+ ],
+ "errors": [
+ []
+ ],
+ "durationMs": 2863
+ },
+ {
+ "title": "[P7-03 PRES-C01 C02 E08] structured editor headings, contextual actions and geometry en/dark/768x1024",
+ "file": "phase7-career-editor.spec.ts",
+ "line": 381,
+ "expectedStatus": "passed",
+ "project": "chromium",
+ "resultStatuses": [
+ "passed"
+ ],
+ "retries": [
+ 0
+ ],
+ "errors": [
+ []
+ ],
+ "durationMs": 3097
+ },
+ {
+ "title": "[P7-03 PRES-C01 C02 E08] structured editor headings, contextual actions and geometry nb/dark/1024x768",
+ "file": "phase7-career-editor.spec.ts",
+ "line": 381,
+ "expectedStatus": "passed",
+ "project": "chromium",
+ "resultStatuses": [
+ "passed"
+ ],
+ "retries": [
+ 0
+ ],
+ "errors": [
+ []
+ ],
+ "durationMs": 3163
+ },
+ {
+ "title": "[P7-03 PRES-C01 C02 E08] structured editor headings, contextual actions and geometry en/light/1280x320",
+ "file": "phase7-career-editor.spec.ts",
+ "line": 381,
+ "expectedStatus": "passed",
+ "project": "chromium",
+ "resultStatuses": [
+ "passed"
+ ],
+ "retries": [
+ 0
+ ],
+ "errors": [
+ []
+ ],
+ "durationMs": 3280
+ },
+ {
+ "title": "[P7-03 PRES-C01 C02 E08] structured editor headings, contextual actions and geometry nb/light/1440x900",
+ "file": "phase7-career-editor.spec.ts",
+ "line": 381,
+ "expectedStatus": "passed",
+ "project": "chromium",
+ "resultStatuses": [
+ "passed"
+ ],
+ "retries": [
+ 0
+ ],
+ "errors": [
+ []
+ ],
+ "durationMs": 3142
+ },
+ {
+ "title": "[P7-03 PRES-C01 C02 E08] structured editor headings, contextual actions and geometry en/dark/1920x1080",
+ "file": "phase7-career-editor.spec.ts",
+ "line": 381,
+ "expectedStatus": "passed",
+ "project": "chromium",
+ "resultStatuses": [
+ "passed"
+ ],
+ "retries": [
+ 0
+ ],
+ "errors": [
+ []
+ ],
+ "durationMs": 3389
+ }
+ ],
+ "reportErrors": [],
+ "configuration": {
+ "version": "1.62.1",
+ "workers": 1,
+ "projects": [
+ {
+ "name": "chromium",
+ "retries": 0
+ }
+ ]
+ },
+ "audit": {
+ "violationNodes": 0,
+ "incompleteNodes": 377,
+ "checkContexts": [
+ {
+ "kind": "incomplete",
+ "rule": "color-contrast",
+ "check": "color-contrast",
+ "message": "Element's background color could not be determined because it is overlapped by another element",
+ "occurrences": 362,
+ "representativeContexts": [
+ {
+ "attachment": "p7-career-editor-nb-light-1440-locale-retained-raw-draft-audit",
+ "target": [
+ ".MuiButtonGroup-firstButton"
+ ],
+ "data": {
+ "contrastRatio": 0,
+ "fontSize": "9.8pt (13px)",
+ "fontWeight": "bold",
+ "messageKey": "bgOverlap",
+ "expectedContrastRatio": "4.5:1"
+ },
+ "relatedNodes": []
+ },
+ {
+ "attachment": "p7-career-editor-nb-light-1440-locale-retained-raw-draft-audit",
+ "target": [
+ ".MuiButton-containedSizeSmall"
+ ],
+ "data": {
+ "contrastRatio": 0,
+ "fontSize": "9.8pt (13px)",
+ "fontWeight": "bold",
+ "messageKey": "bgOverlap",
+ "expectedContrastRatio": "4.5:1"
+ },
+ "relatedNodes": []
+ }
+ ]
+ },
+ {
+ "kind": "incomplete",
+ "rule": "color-contrast",
+ "check": "color-contrast",
+ "message": "Element content is too short to determine if it is actual text content",
+ "occurrences": 13,
+ "representativeContexts": [
+ {
+ "attachment": "p7-career-editor-nb-light-1440-locale-retained-raw-draft-audit",
+ "target": [
+ ".MuiBadge-badge"
+ ],
+ "data": {
+ "contrastRatio": 0,
+ "fontSize": "9.0pt (12px)",
+ "fontWeight": "normal",
+ "messageKey": "shortTextContent",
+ "expectedContrastRatio": "4.5:1"
+ },
+ "relatedNodes": []
+ },
+ {
+ "attachment": "p7-career-editor-en-light-390-held-save-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": 1,
+ "representativeContexts": [
+ {
+ "attachment": "p7-career-editor-en-light-390-held-save-raw-import-audit",
+ "target": [
+ "#menu-"
+ ],
+ "data": null,
+ "relatedNodes": [
+ {
+ "html": "",
+ "target": [
+ "li[data-value=\"import\"]"
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "kind": "incomplete",
+ "rule": "color-contrast",
+ "check": "color-contrast",
+ "message": "Element's background color could not be determined because it partially overlaps other elements",
+ "occurrences": 1,
+ "representativeContexts": [
+ {
+ "attachment": "p7-career-editor-nb-dark-390-actual-validation400-retained-draft-audit",
+ "target": [
+ ".MuiAlert-filledError > .MuiAlert-message.css-127h8j3"
+ ],
+ "data": {
+ "contrastRatio": 0,
+ "fontSize": "10.5pt (14px)",
+ "fontWeight": "normal",
+ "messageKey": "elmPartiallyObscuring",
+ "expectedContrastRatio": "4.5:1"
+ },
+ "relatedNodes": []
+ }
+ ]
+ }
+ ],
+ "qualification": "Every violation/incomplete category counted; complete node/check vectors remain exact raw report and extracted JSON. Parent contextual triage pending."
+ },
+ "asRunQualification": "Earlier _Hza-JrlN1-7iFYcv7mKU export; current shared RichText and CV/Career bytes unchanged; later parent/ApplicationAssets-only changes do not enter these consumers. Not final-build reruns."
+ }
+ },
+ "gates": [
+ {
+ "command": {
+ "name": "typecheck",
+ "command": [
+ "rtk",
+ "proxy",
+ "npm.cmd",
+ "run",
+ "typecheck"
+ ],
+ "startedUtc": "2026-10-13T21:16:52.403174+00:00",
+ "finishedUtc": "2026-10-13T21:17:02.191480+00:00",
+ "exitCode": 0
+ },
+ "commandRaw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/gates/typecheck-command.json",
+ "sha256": "0c5530b6f5ef5888bb0b3c8ff453735e84edd46317125652e79e4dad589992ab",
+ "bytes": 247,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "logRaw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/gates/typecheck.log",
+ "sha256": "89b0ec97b537eca6560c13ca2dfea9e3a97dd1bb35580c4ff39d2462db25e080",
+ "bytes": 148,
+ "hashPolicy": "Exact measured raw bytes"
+ }
+ },
+ {
+ "command": {
+ "name": "lint",
+ "command": [
+ "rtk",
+ "proxy",
+ "npm.cmd",
+ "run",
+ "lint"
+ ],
+ "startedUtc": "2026-10-13T21:17:02.195004+00:00",
+ "finishedUtc": "2026-10-13T21:17:22.024833+00:00",
+ "exitCode": 0
+ },
+ "commandRaw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/gates/lint-command.json",
+ "sha256": "059423a13c2ab2c66684fa602e0e6cdf84bff9d503410ef404f2d6a78e125a69",
+ "bytes": 237,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "logRaw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/gates/lint.log",
+ "sha256": "b040dbeca9b4adb3f6f4c25ac49de0d37e921375b2d9b57e87b542a10f599913",
+ "bytes": 136,
+ "hashPolicy": "Exact measured raw bytes"
+ }
+ },
+ {
+ "command": {
+ "name": "browser-typecheck",
+ "command": [
+ "rtk",
+ "proxy",
+ "npm.cmd",
+ "exec",
+ "tsc",
+ "--",
+ "--project",
+ "../.artifacts/phase7/e2e-tsconfig.json",
+ "--noEmit"
+ ],
+ "startedUtc": "2026-10-13T21:19:30.118147+00:00",
+ "finishedUtc": "2026-10-13T21:19:34.110133+00:00",
+ "exitCode": 0
+ },
+ "commandRaw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/gates/browser-typecheck-command.json",
+ "sha256": "f0646e21a72a4b0542bd3cd9155a0c94f9cf5f3e14750a62b0158ca9398f6a3b",
+ "bytes": 343,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "logRaw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/gates/browser-typecheck.log",
+ "sha256": "d2993bc68ad9a63f711c8f5473155e2ba00fdcd461220cc3849185f2f9ad93e6",
+ "bytes": 78,
+ "hashPolicy": "Exact measured raw bytes"
+ }
+ },
+ {
+ "command": {
+ "name": "build",
+ "command": [
+ "rtk",
+ "proxy",
+ "npm.cmd",
+ "run",
+ "build"
+ ],
+ "startedUtc": "2026-10-13T21:17:26.396619+00:00",
+ "finishedUtc": "2026-10-13T21:17:34.144417+00:00",
+ "exitCode": 0
+ },
+ "commandRaw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/gates/build-command.json",
+ "sha256": "ba78439333cf35c5e21ba4bddc68f7853bb746c2213824a02761f8361e5a110f",
+ "bytes": 239,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "logRaw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/gates/build.log",
+ "sha256": "93c7a399fd901200af634d7332de533d697ee6822b9f7eb361997dc1378184ee",
+ "bytes": 670,
+ "hashPolicy": "Exact measured raw bytes"
+ }
+ }
+ ],
+ "sourceSnapshots": [
+ {
+ "originalSourcePath": "job-tracker-ui/src/components/ApplicationAssets.tsx",
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/src/components/ApplicationAssets.tsx",
+ "sha256": "d3be34c85fd4ab8984356f5fdc8a5b741590120e4253f97246bf870486d027c1",
+ "bytes": 39070,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "canonicalLf": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/src/components/ApplicationAssets.tsx",
+ "sha256": "d3be34c85fd4ab8984356f5fdc8a5b741590120e4253f97246bf870486d027c1",
+ "bytes": 39070,
+ "hashPolicy": "UTF-8 text with CRLF normalized to LF"
+ },
+ "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance."
+ },
+ {
+ "originalSourcePath": "job-tracker-ui/src/components/RichTextField.tsx",
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/src/components/RichTextField.tsx",
+ "sha256": "dcd2ae783722f870c2d2170015e0a2e35ac6d05421bab5dda7b59aab12c6f85a",
+ "bytes": 3046,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "canonicalLf": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/src/components/RichTextField.tsx",
+ "sha256": "dcd2ae783722f870c2d2170015e0a2e35ac6d05421bab5dda7b59aab12c6f85a",
+ "bytes": 3046,
+ "hashPolicy": "UTF-8 text with CRLF normalized to LF"
+ },
+ "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance."
+ },
+ {
+ "originalSourcePath": "job-tracker-ui/src/views/ApplicationWorkspacePage.tsx",
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/src/views/ApplicationWorkspacePage.tsx",
+ "sha256": "7a0fb891f2632b35a8267ca0143a68522334f20bd2b2eda57b3ed2661e357934",
+ "bytes": 31009,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "canonicalLf": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/src/views/ApplicationWorkspacePage.tsx",
+ "sha256": "7a0fb891f2632b35a8267ca0143a68522334f20bd2b2eda57b3ed2661e357934",
+ "bytes": 31009,
+ "hashPolicy": "UTF-8 text with CRLF normalized to LF"
+ },
+ "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance."
+ },
+ {
+ "originalSourcePath": "job-tracker-ui/src/application-workspace-overlay.test.tsx",
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/src/application-workspace-overlay.test.tsx",
+ "sha256": "d51a6fa0f1884048b5a13e4935184edc36ce50527bec42303fd7f265a9a8fdf5",
+ "bytes": 20982,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "canonicalLf": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/src/application-workspace-overlay.test.tsx",
+ "sha256": "c7e9baf3d02c45c4d6766f51b2b1d51d50af73f58b56ea617d376d7800f68672",
+ "bytes": 20569,
+ "hashPolicy": "UTF-8 text with CRLF normalized to LF"
+ },
+ "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance."
+ },
+ {
+ "originalSourcePath": "job-tracker-ui/src/i18n/translations.ts",
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/src/i18n/translations.ts",
+ "sha256": "df8fe7be192c02411e7a1a5a4d9ad27b14f877c207dec5991bfe1f4baf4daca3",
+ "bytes": 316747,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "canonicalLf": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/src/i18n/translations.ts",
+ "sha256": "df8fe7be192c02411e7a1a5a4d9ad27b14f877c207dec5991bfe1f4baf4daca3",
+ "bytes": 316747,
+ "hashPolicy": "UTF-8 text with CRLF normalized to LF"
+ },
+ "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance."
+ },
+ {
+ "originalSourcePath": "job-tracker-ui/src/application-assets.test.tsx",
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/src/application-assets.test.tsx",
+ "sha256": "964a5f8352d451a7f8c5355aeaa79d54f4c687bb03c7ebc7c58324df7355a398",
+ "bytes": 42930,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "canonicalLf": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/src/application-assets.test.tsx",
+ "sha256": "964a5f8352d451a7f8c5355aeaa79d54f4c687bb03c7ebc7c58324df7355a398",
+ "bytes": 42930,
+ "hashPolicy": "UTF-8 text with CRLF normalized to LF"
+ },
+ "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance."
+ },
+ {
+ "originalSourcePath": "job-tracker-ui/e2e/phase7-application-forms.spec.ts",
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/e2e/phase7-application-forms.spec.ts",
+ "sha256": "b349b5190226b0d4b5e4cfd9a56d0bd5507c7ad5da328f11d5b38829cb271914",
+ "bytes": 57869,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "canonicalLf": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/e2e/phase7-application-forms.spec.ts",
+ "sha256": "b349b5190226b0d4b5e4cfd9a56d0bd5507c7ad5da328f11d5b38829cb271914",
+ "bytes": 57869,
+ "hashPolicy": "UTF-8 text with CRLF normalized to LF"
+ },
+ "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance."
+ },
+ {
+ "originalSourcePath": "job-tracker-ui/src/views/career/CareerProfileSections.tsx",
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/src/views/career/CareerProfileSections.tsx",
+ "sha256": "0c772feb6bde8324676b0884aaf343283465c5cd3b3318128363063df4ea22ab",
+ "bytes": 20375,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "canonicalLf": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/src/views/career/CareerProfileSections.tsx",
+ "sha256": "aa3acbe472d34074a517a566c77983e7d95f8ec1df9ee37e47537ad96beffd0e",
+ "bytes": 20131,
+ "hashPolicy": "UTF-8 text with CRLF normalized to LF"
+ },
+ "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance."
+ },
+ {
+ "originalSourcePath": "job-tracker-ui/src/views/CvBuilderEditor.tsx",
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/src/views/CvBuilderEditor.tsx",
+ "sha256": "c45bda4288a9e109c889990f3695077cc8cbafbfb7c55533e0e432d07b236b23",
+ "bytes": 79941,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "canonicalLf": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/src/views/CvBuilderEditor.tsx",
+ "sha256": "4ee49213f01dfbec545c4cd721e57773c7c2409969f83a81c2688167ee925a4f",
+ "bytes": 78572,
+ "hashPolicy": "UTF-8 text with CRLF normalized to LF"
+ },
+ "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance."
+ },
+ {
+ "originalSourcePath": "job-tracker-ui/e2e/job-sections-presentation.spec.ts",
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/e2e/job-sections-presentation.spec.ts",
+ "sha256": "bd3e08daecc48d4fb99e9b3d46bfc3ce0e2f9ada19c2fc521c5a1cadbea33a54",
+ "bytes": 16838,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "canonicalLf": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/e2e/job-sections-presentation.spec.ts",
+ "sha256": "bd3e08daecc48d4fb99e9b3d46bfc3ce0e2f9ada19c2fc521c5a1cadbea33a54",
+ "bytes": 16838,
+ "hashPolicy": "UTF-8 text with CRLF normalized to LF"
+ },
+ "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance."
+ },
+ {
+ "originalSourcePath": "job-tracker-ui/e2e/redesign-preservation.spec.ts",
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/e2e/redesign-preservation.spec.ts",
+ "sha256": "997b01c7e2a313e5f590820bad6b630af7eec67e782df7a786ccce6ebd569da0",
+ "bytes": 23275,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "canonicalLf": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/e2e/redesign-preservation.spec.ts",
+ "sha256": "fd96f798f33badd676c5850f65b306ad171af69bfc5644e35760482518c25105",
+ "bytes": 22991,
+ "hashPolicy": "UTF-8 text with CRLF normalized to LF"
+ },
+ "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance."
+ },
+ {
+ "originalSourcePath": "job-tracker-ui/e2e/cv-editor-frame.spec.ts",
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/e2e/cv-editor-frame.spec.ts",
+ "sha256": "560044042c97f71f1e8d6dc2f0fc2c8106f29d3fd3557324c9cc7c5569c58ecd",
+ "bytes": 14261,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "canonicalLf": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/e2e/cv-editor-frame.spec.ts",
+ "sha256": "560044042c97f71f1e8d6dc2f0fc2c8106f29d3fd3557324c9cc7c5569c58ecd",
+ "bytes": 14261,
+ "hashPolicy": "UTF-8 text with CRLF normalized to LF"
+ },
+ "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance."
+ },
+ {
+ "originalSourcePath": "job-tracker-ui/e2e/phase7-career-editor.spec.ts",
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/e2e/phase7-career-editor.spec.ts",
+ "sha256": "dfc696107e8ad9adf265a5bae885407afd09f1be3b507de8ff03739b39a7f0b1",
+ "bytes": 29805,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "canonicalLf": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/source/job-tracker-ui/e2e/phase7-career-editor.spec.ts",
+ "sha256": "dfc696107e8ad9adf265a5bae885407afd09f1be3b507de8ff03739b39a7f0b1",
+ "bytes": 29805,
+ "hashPolicy": "UTF-8 text with CRLF normalized to LF"
+ },
+ "qualification": "Current candidate snapshot at collection time; parent must qualify unchanged as-run source before acceptance."
+ }
+ ],
+ "buildId": "9sM6acEkOGxgN5fTRZXdE",
+ "export": {
+ "count": 74,
+ "sha256": "e91f356cb09a8e828ad10e6497eb2927d5e7159b48907ddcd7132458486f9a1f",
+ "policy": "Windows Path sort; original output path + TAB + exact raw file SHA + LF"
+ },
+ "actualScopeCounts": {
+ "frontendPassed": 55,
+ "frontendFailed": 0,
+ "frontendSuites": 4,
+ "backendPassed": 30,
+ "backendFailed": 0,
+ "finalBuildBrowserPassed": 15,
+ "finalBuildBrowserFailed": 1,
+ "retainedApplicableBrowserPassed": 30,
+ "retainedApplicableBrowserFailed": 1
+ },
+ "baselineAndCountProvenance": "Original assets18+10 and parent18+1, helper3, named5=55/4 actual. Newordinaryparent props/router test is18pass/1fail RED, then55/4 GREEN. Intermediate52/3 used nonexistent helper filename and did not execute helper3; retained raw observation. +11frontend/0suite derives717/94; +10native derives283 browser only. Latest whole573/89 frontend,1033 backend,49sidecar inherited;1034backendderivedonly. P7-12 fresh integrated regression remains mandatory.",
+ "visualReview": {
+ "reviewed": 23,
+ "retained": 111,
+ "images": [
+ {
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/embedded/application-forms/002-p706-forms-en-light-1440-held-second-job-aggregate-no-foreign-drafts.png",
+ "sha256": "6a72ef9c075174870845cfd3cd2cfa7a850012e49ed12c67bddd5c64fa8dd519",
+ "bytes": 64597,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "originalEmbeddedReport": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms.json",
+ "sha256": "7c73a7b6cc05acaed360b2bb820034fa00735445750e29c2e56525da850aaa1e",
+ "bytes": 4633426,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "lane": "application-forms",
+ "test": "[P7-06 PRES-W01 W04 W10] same-route job navigation waits for owned answers and preserves both jobs on save",
+ "name": "p706-forms-en-light-1440-held-second-job-aggregate-no-foreign-drafts",
+ "manualReview": "ACTUALLY_DISPLAYED_AND_REVIEWED_BY_PARENT"
+ },
+ {
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/embedded/application-forms/004-p706-forms-en-light-1440-second-job-answer-saved-first-job-preserved.png",
+ "sha256": "3efe9f0f3977538ffd529e29233cf4d5eedd8073197f816e9cf754119c878b47",
+ "bytes": 104414,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "originalEmbeddedReport": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms.json",
+ "sha256": "7c73a7b6cc05acaed360b2bb820034fa00735445750e29c2e56525da850aaa1e",
+ "bytes": 4633426,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "lane": "application-forms",
+ "test": "[P7-06 PRES-W01 W04 W10] same-route job navigation waits for owned answers and preserves both jobs on save",
+ "name": "p706-forms-en-light-1440-second-job-answer-saved-first-job-preserved",
+ "manualReview": "ACTUALLY_DISPLAYED_AND_REVIEWED_BY_PARENT"
+ },
+ {
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/embedded/application-forms/007-p706-forms-en-light-1440-held-cv-read.png",
+ "sha256": "776b6783ec80d48d5a1c627c4e4620a5030067ed597566868a6a9190bec5d1c5",
+ "bytes": 100051,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "originalEmbeddedReport": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms.json",
+ "sha256": "7c73a7b6cc05acaed360b2bb820034fa00735445750e29c2e56525da850aaa1e",
+ "bytes": 4633426,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "lane": "application-forms",
+ "test": "[P7-06 forms] held CV read Retry preserves its request and actual attach/replace/detach remain owner scoped",
+ "name": "p706-forms-en-light-1440-held-cv-read",
+ "manualReview": "ACTUALLY_DISPLAYED_AND_REVIEWED_BY_PARENT"
+ },
+ {
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/embedded/application-forms/009-p706-forms-en-light-1440-cv-detached-preserved.png",
+ "sha256": "ed3cb26a4d5d7a118619f06162492670f5144b5c337b4fcec38b576a433a6647",
+ "bytes": 107324,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "originalEmbeddedReport": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms.json",
+ "sha256": "7c73a7b6cc05acaed360b2bb820034fa00735445750e29c2e56525da850aaa1e",
+ "bytes": 4633426,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "lane": "application-forms",
+ "test": "[P7-06 forms] held CV read Retry preserves its request and actual attach/replace/detach remain owner scoped",
+ "name": "p706-forms-en-light-1440-cv-detached-preserved",
+ "manualReview": "ACTUALLY_DISPLAYED_AND_REVIEWED_BY_PARENT"
+ },
+ {
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/embedded/application-forms/012-p706-forms-en-light-1440-held-letter-save.png",
+ "sha256": "12ad0bb0d316106a16bcfe7091f7fa7dea7df3558c12663c300f0fd5c3df77c4",
+ "bytes": 362482,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "originalEmbeddedReport": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms.json",
+ "sha256": "7c73a7b6cc05acaed360b2bb820034fa00735445750e29c2e56525da850aaa1e",
+ "bytes": 4633426,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "lane": "application-forms",
+ "test": "[P7-06 forms] long letter failed held Save retains snapshot and dirty restore Cancel writes nothing before additive explicit restore",
+ "name": "p706-forms-en-light-1440-held-letter-save",
+ "manualReview": "ACTUALLY_DISPLAYED_AND_REVIEWED_BY_PARENT"
+ },
+ {
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/embedded/application-forms/014-p706-forms-en-light-1440-restored-additive-letter-history.png",
+ "sha256": "7ad29c8dbf8765162cb12f41530b2ed286dd33e22ce68dfbd351e46454994811",
+ "bytes": 135584,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "originalEmbeddedReport": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms.json",
+ "sha256": "7c73a7b6cc05acaed360b2bb820034fa00735445750e29c2e56525da850aaa1e",
+ "bytes": 4633426,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "lane": "application-forms",
+ "test": "[P7-06 forms] long letter failed held Save retains snapshot and dirty restore Cancel writes nothing before additive explicit restore",
+ "name": "p706-forms-en-light-1440-restored-additive-letter-history",
+ "manualReview": "ACTUALLY_DISPLAYED_AND_REVIEWED_BY_PARENT"
+ },
+ {
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/embedded/application-forms/017-p706-forms-en-light-1440-held-two-draft-save.png",
+ "sha256": "b5e4ea4c86d87e39638c0fa970d9a2f43ffe72e804fec7d414578ef88cb3fd2a",
+ "bytes": 108744,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "originalEmbeddedReport": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms.json",
+ "sha256": "7c73a7b6cc05acaed360b2bb820034fa00735445750e29c2e56525da850aaa1e",
+ "bytes": 4633426,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "lane": "application-forms",
+ "test": "[P7-06 forms] failed held Answers save preserves both drafts and ordinary Notes through explicit recovery and reload",
+ "name": "p706-forms-en-light-1440-held-two-draft-save",
+ "manualReview": "ACTUALLY_DISPLAYED_AND_REVIEWED_BY_PARENT"
+ },
+ {
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/embedded/application-forms/019-p706-forms-en-light-1440-drafts-saved-personal-notes-preserved.png",
+ "sha256": "f668c2b0c5e3d438beaf215f4b6fc3a92bc2507159eabd887195fa818efd86e5",
+ "bytes": 179241,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "originalEmbeddedReport": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms.json",
+ "sha256": "7c73a7b6cc05acaed360b2bb820034fa00735445750e29c2e56525da850aaa1e",
+ "bytes": 4633426,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "lane": "application-forms",
+ "test": "[P7-06 forms] failed held Answers save preserves both drafts and ordinary Notes through explicit recovery and reload",
+ "name": "p706-forms-en-light-1440-drafts-saved-personal-notes-preserved",
+ "manualReview": "ACTUALLY_DISPLAYED_AND_REVIEWED_BY_PARENT"
+ },
+ {
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/embedded/application-forms/022-p706-forms-en-light-1440-frozen-original-white-print-preview.png",
+ "sha256": "12484613865489660830c0f4f496784f251f871eb5be39863982c3eec6282196",
+ "bytes": 59672,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "originalEmbeddedReport": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms.json",
+ "sha256": "7c73a7b6cc05acaed360b2bb820034fa00735445750e29c2e56525da850aaa1e",
+ "bytes": 4633426,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "lane": "application-forms",
+ "test": "[P7-06 forms] actual native capture freezes CV and exact archive bytes after current sources change without renderer mutation",
+ "name": "p706-forms-en-light-1440-frozen-original-white-print-preview",
+ "manualReview": "ACTUALLY_DISPLAYED_AND_REVIEWED_BY_PARENT"
+ },
+ {
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/embedded/application-forms/026-p706-forms-en-light-1440-held-original-package-detail-retry.png",
+ "sha256": "306381c93851f86373660f9277141aa2cbfac4f1b0cb0027176106a354a0b7e6",
+ "bytes": 103391,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "originalEmbeddedReport": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms.json",
+ "sha256": "7c73a7b6cc05acaed360b2bb820034fa00735445750e29c2e56525da850aaa1e",
+ "bytes": 4633426,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "lane": "application-forms",
+ "test": "[P7-06 forms] failed preserved detail and download retain history and Retry only the original owned read",
+ "name": "p706-forms-en-light-1440-held-original-package-detail-retry",
+ "manualReview": "ACTUALLY_DISPLAYED_AND_REVIEWED_BY_PARENT"
+ },
+ {
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/embedded/application-forms/028-p706-forms-en-light-1440-original-archive-retry-history-retained.png",
+ "sha256": "8f132a3e747ed1889c8316a7f3cc6a6e2a9afd052f5ed608898e2d875cf0589e",
+ "bytes": 101884,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "originalEmbeddedReport": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms.json",
+ "sha256": "7c73a7b6cc05acaed360b2bb820034fa00735445750e29c2e56525da850aaa1e",
+ "bytes": 4633426,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "lane": "application-forms",
+ "test": "[P7-06 forms] failed preserved detail and download retain history and Retry only the original owned read",
+ "name": "p706-forms-en-light-1440-original-archive-retry-history-retained",
+ "manualReview": "ACTUALLY_DISPLAYED_AND_REVIEWED_BY_PARENT"
+ },
+ {
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms-results/phase7-application-forms---65823-without-asset-GET-or-writes-chromium/test-failed-1.png",
+ "sha256": "1e0e5fe0518c19a585521f5dd773b4a6335bb62cac4eb18bfe46862afb9104e1",
+ "bytes": 97516,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "originalEmbeddedReport": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms.json",
+ "sha256": "7c73a7b6cc05acaed360b2bb820034fa00735445750e29c2e56525da850aaa1e",
+ "bytes": 4633426,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "lane": "application-forms",
+ "test": "[P7-06 forms] live EN/Bokmal retains dirty letter and both answer drafts without asset GET or writes",
+ "name": "screenshot",
+ "manualReview": "ACTUALLY_DISPLAYED_AND_REVIEWED_BY_PARENT"
+ },
+ {
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/embedded/application-forms/033-p706-forms-en-light-1440-created-copy-failed-attach-preserved.png",
+ "sha256": "0bd54df2544d28ec243a5140471eaed1a0edc83beae52f262cc79786cddaa6b6",
+ "bytes": 135595,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "originalEmbeddedReport": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms.json",
+ "sha256": "7c73a7b6cc05acaed360b2bb820034fa00735445750e29c2e56525da850aaa1e",
+ "bytes": 4633426,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "lane": "application-forms",
+ "test": "[P7-06 forms] real duplicate then failed attach preserves created copy and original without automatic duplicate retry",
+ "name": "p706-forms-en-light-1440-created-copy-failed-attach-preserved",
+ "manualReview": "ACTUALLY_DISPLAYED_AND_REVIEWED_BY_PARENT"
+ },
+ {
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/embedded/application-forms/037-p706-forms-en-light-320-long-answers.png",
+ "sha256": "e5e5021d7f12b66931d500eb34a0ce4f1183226166425ca7bfbe67fa0279244a",
+ "bytes": 135809,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "originalEmbeddedReport": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms.json",
+ "sha256": "7c73a7b6cc05acaed360b2bb820034fa00735445750e29c2e56525da850aaa1e",
+ "bytes": 4633426,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "lane": "application-forms",
+ "test": "[P7-06 forms] eight finite bilingual theme widths keep long CV, letter, drafts and frozen preview usable with keyboard",
+ "name": "p706-forms-en-light-320-long-answers",
+ "manualReview": "ACTUALLY_DISPLAYED_AND_REVIEWED_BY_PARENT"
+ },
+ {
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/embedded/application-forms/040-p706-forms-nb-dark-360-long-letter.png",
+ "sha256": "193c15211abd16bdef184115e4cb19fda687de0b57ede2d0a34c436777014649",
+ "bytes": 274032,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "originalEmbeddedReport": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms.json",
+ "sha256": "7c73a7b6cc05acaed360b2bb820034fa00735445750e29c2e56525da850aaa1e",
+ "bytes": 4633426,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "lane": "application-forms",
+ "test": "[P7-06 forms] eight finite bilingual theme widths keep long CV, letter, drafts and frozen preview usable with keyboard",
+ "name": "p706-forms-nb-dark-360-long-letter",
+ "manualReview": "ACTUALLY_DISPLAYED_AND_REVIEWED_BY_PARENT"
+ },
+ {
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/embedded/application-forms/042-p706-forms-en-dark-390-long-submission.png",
+ "sha256": "623427191ac60aecc7635bde365c879318263a829e8e668151c0e6dba49bc173",
+ "bytes": 67201,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "originalEmbeddedReport": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms.json",
+ "sha256": "7c73a7b6cc05acaed360b2bb820034fa00735445750e29c2e56525da850aaa1e",
+ "bytes": 4633426,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "lane": "application-forms",
+ "test": "[P7-06 forms] eight finite bilingual theme widths keep long CV, letter, drafts and frozen preview usable with keyboard",
+ "name": "p706-forms-en-dark-390-long-submission",
+ "manualReview": "ACTUALLY_DISPLAYED_AND_REVIEWED_BY_PARENT"
+ },
+ {
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/embedded/application-forms/044-p706-forms-nb-light-768-long-cv.png",
+ "sha256": "62e008992eb8cc7eeb184502cbbc2dd325b32ff899011e83a8849354e500c9ed",
+ "bytes": 102705,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "originalEmbeddedReport": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms.json",
+ "sha256": "7c73a7b6cc05acaed360b2bb820034fa00735445750e29c2e56525da850aaa1e",
+ "bytes": 4633426,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "lane": "application-forms",
+ "test": "[P7-06 forms] eight finite bilingual theme widths keep long CV, letter, drafts and frozen preview usable with keyboard",
+ "name": "p706-forms-nb-light-768-long-cv",
+ "manualReview": "ACTUALLY_DISPLAYED_AND_REVIEWED_BY_PARENT"
+ },
+ {
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/embedded/application-forms/046-p706-forms-en-dark-1024-long-answers.png",
+ "sha256": "48970bff97b4a07fe8d6fb570b0ded26c7f34648f7c63607249c0320df958dbc",
+ "bytes": 180883,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "originalEmbeddedReport": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms.json",
+ "sha256": "7c73a7b6cc05acaed360b2bb820034fa00735445750e29c2e56525da850aaa1e",
+ "bytes": 4633426,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "lane": "application-forms",
+ "test": "[P7-06 forms] eight finite bilingual theme widths keep long CV, letter, drafts and frozen preview usable with keyboard",
+ "name": "p706-forms-en-dark-1024-long-answers",
+ "manualReview": "ACTUALLY_DISPLAYED_AND_REVIEWED_BY_PARENT"
+ },
+ {
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/embedded/application-forms/049-p706-forms-nb-light-1280-long-letter.png",
+ "sha256": "c095249586fbb7ff0a81ee6f1a45ae149057ce603039d713ec6a44fba4c460f0",
+ "bytes": 294119,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "originalEmbeddedReport": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms.json",
+ "sha256": "7c73a7b6cc05acaed360b2bb820034fa00735445750e29c2e56525da850aaa1e",
+ "bytes": 4633426,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "lane": "application-forms",
+ "test": "[P7-06 forms] eight finite bilingual theme widths keep long CV, letter, drafts and frozen preview usable with keyboard",
+ "name": "p706-forms-nb-light-1280-long-letter",
+ "manualReview": "ACTUALLY_DISPLAYED_AND_REVIEWED_BY_PARENT"
+ },
+ {
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/embedded/application-forms/051-p706-forms-en-light-1440-long-submission.png",
+ "sha256": "e282d6752a5441c3d804056fb96a7a4b58c6e103eb476e601e06970ccc7d970f",
+ "bytes": 66317,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "originalEmbeddedReport": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms.json",
+ "sha256": "7c73a7b6cc05acaed360b2bb820034fa00735445750e29c2e56525da850aaa1e",
+ "bytes": 4633426,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "lane": "application-forms",
+ "test": "[P7-06 forms] eight finite bilingual theme widths keep long CV, letter, drafts and frozen preview usable with keyboard",
+ "name": "p706-forms-en-light-1440-long-submission",
+ "manualReview": "ACTUALLY_DISPLAYED_AND_REVIEWED_BY_PARENT"
+ },
+ {
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/embedded/application-forms/053-p706-forms-nb-dark-1920-long-cv.png",
+ "sha256": "d4919f188bee2b4a358f8efd220c883aca7332c3ad46805f4883a019375b7f73",
+ "bytes": 137982,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "originalEmbeddedReport": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms.json",
+ "sha256": "7c73a7b6cc05acaed360b2bb820034fa00735445750e29c2e56525da850aaa1e",
+ "bytes": 4633426,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "lane": "application-forms",
+ "test": "[P7-06 forms] eight finite bilingual theme widths keep long CV, letter, drafts and frozen preview usable with keyboard",
+ "name": "p706-forms-nb-dark-1920-long-cv",
+ "manualReview": "ACTUALLY_DISPLAYED_AND_REVIEWED_BY_PARENT"
+ },
+ {
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/embedded/application-forms/056-p706-forms-nb-light-1440-controlled-language503-dirty-letter.png",
+ "sha256": "dc64ab4baeed881a1edbeca1fbc9ab531789d6ed4a1f885da201fe8449f63aad",
+ "bytes": 136448,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "originalEmbeddedReport": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms.json",
+ "sha256": "7c73a7b6cc05acaed360b2bb820034fa00735445750e29c2e56525da850aaa1e",
+ "bytes": 4633426,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "lane": "application-forms",
+ "test": "[P7-06 forms] controlled language HTTP503 preserves local dirty drafts and Retry only repeats the language request",
+ "name": "p706-forms-nb-light-1440-controlled-language503-dirty-letter",
+ "manualReview": "ACTUALLY_DISPLAYED_AND_REVIEWED_BY_PARENT"
+ },
+ {
+ "raw": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/embedded/application-forms/058-p706-forms-en-light-1440-controlled-language503-dirty-answer-drafts.png",
+ "sha256": "6deadc2a766dfe2d8faab6e0e6906f9793eaa2e7a5e18f1c8fc162543b40f61c",
+ "bytes": 112420,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "originalEmbeddedReport": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-final/application-forms.json",
+ "sha256": "7c73a7b6cc05acaed360b2bb820034fa00735445750e29c2e56525da850aaa1e",
+ "bytes": 4633426,
+ "hashPolicy": "Exact measured raw bytes"
+ },
+ "lane": "application-forms",
+ "test": "[P7-06 forms] controlled language HTTP503 preserves local dirty drafts and Retry only repeats the language request",
+ "name": "p706-forms-en-light-1440-controlled-language503-dirty-answer-drafts",
+ "manualReview": "ACTUALLY_DISPLAYED_AND_REVIEWED_BY_PARENT"
+ }
+ ],
+ "qualification": "All23 latest parent-source forms PNGs displayed/reviewed, including heldB absent foreignfields, savedB and strictNB500 screenshot. Other88 retained consumer images not visually inspected in this continuation. Earlier22/110 observations stay separate; no duplicate cases. Long320/360/1280/1440 captures display-resized; sticky composite geometry, white 2D CVcanvas and rootOverflow0 qualified. No everypixel/viewportocclusion/physicalkeyboard/zoom/humanAT acceptance."
+ },
+ "backend": {
+ "command": {
+ "command": [
+ "rtk",
+ "proxy",
+ "C:/Users/Connor/.dotnet/dotnet.exe",
+ "test",
+ "JobTrackerApi.Tests/JobTrackerApi.Tests.csproj",
+ "--configuration",
+ "Release",
+ "--no-build",
+ "--filter",
+ "FullyQualifiedName~ApplicationAssetsTests|FullyQualifiedName~JobApplicationsApplicationPackageTests|FullyQualifiedName~SubmittedApplicationPackageTests",
+ "--logger",
+ "trx;LogFileName=career.trx",
+ "--results-directory",
+ "E:\\Storage\\Projects\\Active\\Job tracker\\.artifacts\\phase7\\current\\backend-career\\p706-forms-20261013T161800Z"
+ ],
+ "startedUtc": "2026-10-13T16:18:00.184066+00:00",
+ "finishedUtc": "2026-10-13T16:18:03.191603+00:00",
+ "exitCode": 0,
+ "logSha256": "8354b4552bcda70d71eac4cc7438e454db7f0161bc48120cefd191ad0387ca3d",
+ "counts": {
+ "total": 30,
+ "passed": 30,
+ "failed": 0,
+ "notExecuted": 0
+ },
+ "resultSha256": "987b38f189ff75876a9b12563160a0a4f5c24b2414ca09c6f4494abffaafbf1d"
+ },
+ "files": [
+ {
+ "path": ".artifacts/phase7/p7-06/forms/backend/career.log",
+ "sha256": "8354b4552bcda70d71eac4cc7438e454db7f0161bc48120cefd191ad0387ca3d",
+ "bytes": 607,
+ "hashPolicy": "Exact raw bytes"
+ },
+ {
+ "path": ".artifacts/phase7/p7-06/forms/backend/career.trx",
+ "sha256": "987b38f189ff75876a9b12563160a0a4f5c24b2414ca09c6f4494abffaafbf1d",
+ "bytes": 44086,
+ "hashPolicy": "Exact raw bytes"
+ },
+ {
+ "path": ".artifacts/phase7/p7-06/forms/backend/command.json",
+ "sha256": "3dc0b9a36f7a7aaef07a3ac043dc76e876e9910866ae66ba572dc989e3226dbd",
+ "bytes": 998,
+ "hashPolicy": "Exact raw bytes"
+ }
+ ],
+ "qualification": "30 passing assets/package ownership/immutable contracts, same unchanged Release DLL/no-build; no new backend full suite/build."
+ },
+ "accessibility": {
+ "completeContextTriage": {
+ "path": ".artifacts/phase7/p7-06/forms/axe-triage-parent.json",
+ "sha256": "51f6adcb2b205f0a983c73646864a53ba2b724a453217c3f5390d0520e35da67",
+ "bytes": 1669708,
+ "hashPolicy": "Exact raw bytes"
+ },
+ "lanes": {
+ "application-forms": {
+ "scans": 22,
+ "violations": 0,
+ "incomplete": 154
+ },
+ "application-forms-regression": {
+ "scans": 8,
+ "violations": 0,
+ "incomplete": 89
+ },
+ "application-p2d01": {
+ "scans": 0,
+ "violations": 0,
+ "incomplete": 0
+ },
+ "application-rich-cv": {
+ "scans": 12,
+ "violations": 0,
+ "incomplete": 121
+ },
+ "application-rich-career": {
+ "scans": 13,
+ "violations": 0,
+ "incomplete": 377
+ }
+ },
+ "qualification": "Full raw incomplete vectors retained; current/defaulttarget and WCAG consumer scopes distinct. Missing strictlocale capture reduces observed nodes; not contrast improvement. Reader/device/zoom/modal/frame contexts remain pending."
+ },
+ "failedHistory": [
+ {
+ "path": ".artifacts/phase7/p7-06/forms/attempt-1-F7dRaRLWo_pjz6zoPoAPk/results.json",
+ "sha256": "e56e1b9a3a4e7663a12cc07b76e7dab21033fc9cb3ed4ff42678cde18aa100bc",
+ "bytes": 135385,
+ "hashPolicy": "Exact raw bytes"
+ },
+ {
+ "path": ".artifacts/phase7/p7-06/forms/attempt-2-_Hza-JrlN1-7iFYcv7mKU/results.json",
+ "sha256": "f27368dfa41d454508e8ec58ae0af36b639464f2877c162c2e16499029d5d9ce",
+ "bytes": 129508,
+ "hashPolicy": "Exact raw bytes"
+ },
+ {
+ "path": ".artifacts/phase7/p7-06/forms/final/results.json",
+ "sha256": "4da6523e909f0a69cfead47e73cd094bb391b37d181e7efd05b5421919f6e322",
+ "bytes": 430012,
+ "hashPolicy": "Exact raw bytes"
+ },
+ {
+ "path": ".artifacts/phase7/p7-06/forms/attempt-4-7rHcRydumjgHBrGRn7NZA/results.json",
+ "sha256": "5268cfd446ba5b91917c2ccc30c786584eb8d497278373f397a2e6d2c0501de1",
+ "bytes": 175411,
+ "hashPolicy": "Exact raw bytes"
+ },
+ {
+ "path": ".artifacts/phase7/p7-06/forms/final-verified/results.json",
+ "sha256": "28da994d574e819208d65edc756fac13eca0597a9d622bcd47a3eb488db7eea4",
+ "bytes": 430829,
+ "hashPolicy": "Exact raw bytes"
+ },
+ {
+ "path": ".artifacts/phase7/p7-06/forms/attempt-6-parent-9sM6acEkOGxgN5fTRZXdE/results.json",
+ "sha256": "27517dce65e6b68e1d9c1268776df9ddc458b063701a076437ce20da2bb9d1f0",
+ "bytes": 177252,
+ "hashPolicy": "Exact raw bytes"
+ }
+ ],
+ "contracts": "57groups56PARTIAL/C11DEFERRED;32 routes preserved. Owned asset GET-only Retry, exact package detail/file identity, original frozen bytes/whiteCV renderer, canonical/override separation, bodyless restore/additive history/noExpectedVersion RD21, existing Notesanswerencoding/clear. CV duplicate retains original/copy partial association before explicit attach; exactcreatedID recovery normalizes without secondduplicate. Existing AI explicit Apply/requestaction/currentowner guards only;P513WITHHELD/no model calls.",
+ "mandatoryConditions": [
+ "RD11 strict real account-language save repeatedly returns HTTP500; later P7-09 diagnosis and P7-12 verification required, no retry/skip/assertion relaxation hides failure.",
+ "M01-M12 human screenreader/physicaltouch/softwarekeyboard/zoom/provider/fontphoto/nativeprint and release evidence remain unverified.",
+ "P7-11 server-English Tailoring/raw source/action metadata localisation residual; user-authored language intentionally retained.",
+ "RD21 last-writer/partialwrite and duplicate two-write server limit retained.",
+ "P7-06 inbox/read and compose/send boundaries remain;06FDEFERRED;P513WITHHELD;Phase8 not started."
+ ],
+ "production": {
+ "status": "NO-GO",
+ "main": "27657f31131cd95d42f0e1290853ec9c9b3959a4",
+ "deployment": false
+ },
+ "staticChecks": {
+ "path": "docs/redesign/phase-7/p7-06/evidence/forms-static-checks.json",
+ "sha256": "700bd7b96e0daf6eac11cc21f7c0c0fab78468c854eba823d59d60c389d2e11d",
+ "bytes": 3166,
+ "hashPolicy": "Exact raw bytes"
+ },
+ "previousPortableObservation": {
+ "path": ".artifacts/phase7/p7-06/forms/pre-parent-portable-results.json",
+ "sha256": "ffaad594f560157d4a50a00de30174005d0fdcf75a1c3c6e5424595115839496",
+ "bytes": 143116,
+ "hashPolicy": "Exact raw bytes"
+ },
+ "preNativeSourceQualification": "Six non-native-test source files match current; test source changed afterwards only for fixture company reuse and real keyboard Home assertion. Captured final native source matches executed final source. Receipt collected during gates before native.",
+ "parentIdentityRepair": {
+ "redCommand": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-identity-red/p706-parent-identity-red-20261013-20261013T211454Z-command.json",
+ "sha256": "227727128b7948d85e132ca0aca359d94efbf95e640e6e12c8ea35a60d34d9a6",
+ "bytes": 670,
+ "hashPolicy": "Exact raw bytes"
+ },
+ "redReport": {
+ "path": ".artifacts/phase7/p7-06/forms/parent-identity-red/p706-parent-identity-red-20261013-20261013T211454Z.json",
+ "sha256": "05f8ea1df075114f448ab1fc6b0cfb31e7db5888e665c3cd5bc3b0cb521b0af1",
+ "bytes": 9713,
+ "hashPolicy": "Exact raw bytes"
+ },
+ "reproduced": "New B editor received A aggregate props while B GET held;18oldPASS/1newFAIL. Gate matchingID and generationguard preserve current owner; actualSPA native timeOrigin stable, nofields/no Bwrite held, exact B payload and both A drafts unchanged.",
+ "review": "Read-only follow-up review found no remaining actionable gap in narrowrepair.",
+ "limits": "Original locale-dependent aggregate reload/error presentation and RD21 noExpectedVersion remain; no new backend locking contract."
+ }
+}
diff --git a/docs/redesign/phase-7/p7-06/evidence/forms-static-checks.json b/docs/redesign/phase-7/p7-06/evidence/forms-static-checks.json
new file mode 100644
index 0000000..6423f85
--- /dev/null
+++ b/docs/redesign/phase-7/p7-06/evidence/forms-static-checks.json
@@ -0,0 +1,104 @@
+{
+ "schemaVersion": 1,
+ "checkpoint": "p7-06",
+ "status": "PASS",
+ "checkedUtc": "2026-10-13T21:26:35.976163+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-13T21:26:32.660030+00:00",
+ "finishedUtc": "2026-10-13T21:26:35.270044+00:00",
+ "exitCode": 0,
+ "log": ".artifacts/phase7/current/static/20261013T212632Z/secret-scan.log",
+ "logSha256Raw": "c5a88d5ebdc042adba125c1eaacbe44f9f4a5c19898c0f195573c00d511aa28d"
+ },
+ {
+ "name": "working-whitespace",
+ "command": [
+ "rtk",
+ "proxy",
+ "git",
+ "diff",
+ "--check"
+ ],
+ "startedUtc": "2026-10-13T21:26:35.270544+00:00",
+ "finishedUtc": "2026-10-13T21:26:35.365625+00:00",
+ "exitCode": 0,
+ "log": ".artifacts/phase7/current/static/20261013T212632Z/working-whitespace.log",
+ "logSha256Raw": "d2993bc68ad9a63f711c8f5473155e2ba00fdcd461220cc3849185f2f9ad93e6"
+ },
+ {
+ "name": "staged-whitespace",
+ "command": [
+ "rtk",
+ "proxy",
+ "git",
+ "diff",
+ "--cached",
+ "--check"
+ ],
+ "startedUtc": "2026-10-13T21:26:35.366126+00:00",
+ "finishedUtc": "2026-10-13T21:26:35.464414+00:00",
+ "exitCode": 0,
+ "log": ".artifacts/phase7/current/static/20261013T212632Z/staged-whitespace.log",
+ "logSha256Raw": "d2993bc68ad9a63f711c8f5473155e2ba00fdcd461220cc3849185f2f9ad93e6"
+ },
+ {
+ "name": "phase7-whitespace",
+ "command": [
+ "rtk",
+ "proxy",
+ "git",
+ "diff",
+ "--check",
+ "b5319981ad3d1bd635daff861453d23c0de5320b"
+ ],
+ "startedUtc": "2026-10-13T21:26:35.464414+00:00",
+ "finishedUtc": "2026-10-13T21:26:35.607563+00:00",
+ "exitCode": 0,
+ "log": ".artifacts/phase7/current/static/20261013T212632Z/phase7-whitespace.log",
+ "logSha256Raw": "d2993bc68ad9a63f711c8f5473155e2ba00fdcd461220cc3849185f2f9ad93e6"
+ }
+ ],
+ "untrackedSecretScan": {
+ "files": 0,
+ "findings": 0
+ },
+ "documentation": {
+ "files": 14,
+ "relativeLinks": 465,
+ "errors": []
+ },
+ "preservation": {
+ "groups": 57,
+ "statuses": {
+ "PARTIAL": 56,
+ "PASS": 0,
+ "FAIL": 0,
+ "DEFERRED": 1
+ },
+ "originalRoutesPreserved": true,
+ "currentConcreteRoutes": 32
+ },
+ "production": {
+ "main": "27657f31131cd95d42f0e1290853ec9c9b3959a4",
+ "originMain": "27657f31131cd95d42f0e1290853ec9c9b3959a4",
+ "status": "NO-GO",
+ "qualification": "Local fetched refs; direct push observation recorded separately."
+ },
+ "hashPolicy": "Portable JSON UTF8 LF; raw log hashes measure exact saved bytes.",
+ "limits": [
+ "Pattern scanner skips binary and files over2MiB; not historical Git/production secrets.",
+ "Local links check target existence, not URL/heading anchors.",
+ "Static evidence is not runtime/manual/device/provider/production acceptance."
+ ]
+}
diff --git a/docs/roadmap/todo.md b/docs/roadmap/todo.md
index 7d8aee0..f0a7cd6 100644
--- a/docs/roadmap/todo.md
+++ b/docs/roadmap/todo.md
@@ -87,8 +87,8 @@ Follow the approved [Phase 7 handoff](../redesign/phase-6/p6-05/PHASE7_HANDOFF.m
- [x] P7-02 [onboarding guidance and Career overview/evidence](../redesign/phase-7/p7-02/README.md): entryad7b419 pushed/verified; onboarding implemented-scope CONDITIONAL GO32/13/4+types/lint/build/13 actual PNG inspections/hygiene, pushedd21501aa/direct receipt23:55:12UTC. Separate actual SQLite evidence-list repair91 guards/HTTP1/Release passed and pushed01281f52/direct receipt00:06:47UTC; unfinished Career work preserved/main unchanged. Career affected46/3/native14/fresh preservation2/types/lint/build and22 actual PNG inspections pass; implemented P7-02 CONDITIONAL GO, final Career hygiene PASS (four commands0,9docs347links0errors) and normal push8b4d5c2 directly verified05:51:04UTC, localHEAD=origin=live/clean worktree/main unchanged. Career0/99 axe incompletes/manual gates retained; fullfrontend610/backend1,034 are derived only, latest fresh full573/89 unchanged.
- [x] P7-03 Career structured editor and import/history: 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.
- [x] P7-04 CV library and public entry implemented scope CONDITIONAL GO: library fcacae7 pushed/verified; public38/3 affected/native8/fidelity16/36+8/24/types/lint/build PASS,18 actual public images reviewed. Public final hygiene PASS and bfa0ab6 normal push verified; sandbox frame-tested12 and human/device/production qualifications retained.
-- [x] P7-05 Discovery, Companies, Offers and Reminders: Discovery21/1,new8+existing4native/backend14/types/lint/build pass;18 actual new PNG reviews,81+52 axe incompletes retained. Discovery hygiene PASS/normal push34a4d6d verified. Companies30/4,new6+old2+named4native/backend2/types/lint/build pass;20actual PNG reviews,91+163incompletes qualified. Companies hygiene PASS/normal pusha8aba85 verified; Offers9/1,new4native/backend6/types/lint/build pass;14actual PNG reviews,15incompletes qualified. Offers hygiene PASS/normal pushc723a4e verified; Reminders17/3,new6native/backend6/types/lint/build pass;17actual PNG reviews/114incompletes qualified. P7-05 implemented CONDITIONAL GO, hygiene PASS/Reminders normal push pending; P7-06 next.
-- [ ] P7-06 remaining Application forms and global correspondence.
+- [x] P7-05 Discovery, Companies, Offers and Reminders: Discovery21/1,new8+existing4native/backend14/types/lint/build pass;18 actual new PNG reviews,81+52 axe incompletes retained. Discovery hygiene PASS/normal push34a4d6d verified. Companies30/4,new6+old2+named4native/backend2/types/lint/build pass;20actual PNG reviews,91+163incompletes qualified. Companies hygiene PASS/normal pusha8aba85 verified; Offers9/1,new4native/backend6/types/lint/build pass;14actual PNG reviews,15incompletes qualified. Offers hygiene PASS/normal pushc723a4e verified; Reminders17/3,new6native/backend6/types/lint/build pass;17actual PNG reviews/114incompletes qualified. P7-05 implemented CONDITIONAL GO, hygiene PASS/normal627955a push directly verified2026-10-13 11:49:32.473200UTC, clean localHEAD=origin=live/main unchanged.
+- [ ] P7-06 [remaining Application forms and global correspondence](../redesign/phase-7/p7-06/README.md) ongoing: [embedded forms](../redesign/phase-7/p7-06/FORMS.md) CONDITIONAL GO pending final static/push. Latest54/4 units/backend30 no-build; current native8/1 only strict language-save204→HTTP500 EN (RD11), current Jobs5/P2-D01ordinary1 give14pass/1fail of15. Previous-build CV4/Career11 give29pass/1fail relevant30 with qualifiers. PageDown passes; failure retained without rerun/waiver. Derived716/94 frontend/282browser cases only. Preserve oneCV/letter-history/Notes/frozen packages; next separate inbox/read review and compose/send commits,06F deferred.
- [ ] P7-06F optional reviewed followMode composer: retain deferral unless its required action/fallback decision is established.
- [ ] P7-07 owner-scoped metadata projection, then Documents interface.
- [ ] P7-08 existing notifications and operations.
@@ -96,3 +96,7 @@ Follow the approved [Phase 7 handoff](../redesign/phase-6/p6-05/PHASE7_HANDOFF.m
- [ ] P7-10 Users, Audit and System administration presentation.
- [ ] P7-11 residual Analytics/error/empty/dialog integration.
- [ ] P7-12 integrated migration acceptance and Phase 8 handoff.
+
+### Final P7-06 embedded-forms observation
+
+Final embedded-forms implemented scope is **CONDITIONAL GO**, normal push pending. The matching aggregate/generation repair resolves the reviewed cross-job draft leak: ordinary parent RED18/1, latest affected **55/4 PASS** and actual SPA/heldB/exact two-job readback PASS. Final parent build9sM6acEkOGxgN5fTRZXdE has **9PASS/1FAIL of10** (unchanged strict actual NB language204→500/RD11); fresh Jobs5/P2-D01ordinary1 give **15PASS/1FAIL of16**, qualified earlier CV4/Career11 give **30PASS/1FAIL relevant31**. Backend30 sameDLL/no-build; app/browser types/lint/build PASS. All23/111 current forms PNGs actually reviewed, 55 recorded axe scans0/741 (current22/154+Jobs8/89; earlierCV12/121+Career13/377), full incomplete contexts pending human qualification. See [portable forms result](../redesign/phase-7/p7-06/evidence/forms-results.json). +11frontend/0suites derives **717/94**; +10browser derives **283**, no integrated run. All57 groups56PARTIAL/C11DEFERRED and32routes, P513withheld/06Fdeferred/main unchanged/productionNO-GO. Inbox/read and compose/send remain; Phase8 not started.
diff --git a/job-tracker-ui/e2e/phase7-application-forms.spec.ts b/job-tracker-ui/e2e/phase7-application-forms.spec.ts
new file mode 100644
index 0000000..841c56c
--- /dev/null
+++ b/job-tracker-ui/e2e/phase7-application-forms.spec.ts
@@ -0,0 +1,559 @@
+import AxeBuilder from '@axe-core/playwright';
+import { createHash } from 'node:crypto';
+import { readFile } from 'node:fs/promises';
+import { expect, test as base, type APIRequestContext, type APIResponse, type Locator, type Page, type Response, type TestInfo } from '@playwright/test';
+import { translations } from '../src/i18n/translations';
+import { emptyStructuredCv } from '../src/profileCv';
+import type { CvVariant, CvVariantSummary } from '../src/cvBuilder';
+import type { ApplicationCv, CoverLetter, SubmittedPackage, SubmittedPackageDetail, WorkspaceOverview } from '../src/applicationWorkspace';
+import { removeApplicationAnswerDraft } from '../src/applicationDrafts';
+
+const apiUrl = 'http://localhost:5302/api';
+const password = 'E2ePassword123!';
+type Owner = { id: string; headers: Record };
+type Sample = { language: 'en' | 'nb'; theme: 'light' | 'dark'; width: number; height: number };
+type Subsection = 'cv' | 'letter' | 'answers' | 'submission';
+type Job = { id: number; jobTitle: string; notes: string | null };
+type RequestRecord = { method: string; path: string; body: unknown };
+type Session = { owner: Owner; admin: APIRequestContext; administrator: Owner; requests: RequestRecord[] };
+const desktop: Sample = { language: 'en', theme: 'light', width: 1440, height: 900 };
+const boundary = 'Actual owner HTTP fixtures use disposable isolated SQLite and existing application/CV/attachment APIs. Free accounts disable AI; the serial runtime disables model, delivery and alert workers. No provider/model/generation/send endpoint, raw SQL, schema or production write. Controlled precommit HTTP failures and held requests qualify scoped presentation/recovery, not network-uncertain idempotency or production reliability. Request metadata includes only fixture application/CV/file paths and bodies; no auth/CSRF/session headers. Existing seven workspace sections, CV renderer, immutable packages, one-job CV association and append-only cover history are preserved. Existing P2-D01 remains a separately executed ordinary preservation case; this file supplements that corpus. Duplicate-for-job remains two writes; created-copy/failed-attach is explicitly qualified, with no automatic second duplicate. Current AttachmentsTable and its separate ID0-download debt are outside this slice.';
+function jobUrl(id: number) { return `${apiUrl}/jobapplications/${id}`; }
+function interpolate(template: string, params: Record) { return template.replace(/\{(\w+)\}/g, (_, key: string) => String(params[key] ?? `{${key}}`)); }
+function hold() { let release!: () => void; const pending = new Promise(resolve => { release = resolve; }); return { pending, release }; }
+async function json(response: APIResponse | Response, status = 200): Promise { expect(response.status(), response.url()).toBe(status); return response.json(); }
+function response(page: Page, method: string, url: string) { return page.waitForResponse(value => value.url() === url && value.request().method() === method); }
+async function keyboard(page: Page, control: Locator) { await control.focus(); await expect(control).toBeFocused(); await page.keyboard.press('Enter'); }
+function count(session: Session, method: string, path: string) { return session.requests.filter(value => value.method === method && value.path === path); }
+function mutations(session: Session) { return session.requests.filter(value => ['POST', 'PUT', 'PATCH', 'DELETE'].includes(value.method)); }
+function assetReads(session: Session, id: number) {
+ const start = `/api/jobapplications/${id}/`;
+ return session.requests.filter(value => value.method === 'GET' && value.path.startsWith(start) &&
+ ['cv', 'tailoring', 'cover-letter', 'submitted-packages'].includes(value.path.slice(start.length).split('/')[0]));
+}
+function section(page: Page, title: string) { return page.getByRole('region', { name: title, exact: true }); }
+function childFocus(page: Page, title: string) { return page.getByRole('group', { name: title, exact: true }); }
+function letter(page: Page, language: Sample['language'] = 'en') { return page.getByRole('textbox', { name: translations[language].jobDetailsCoverLetter, exact: true }); }
+function attachedCv(page: Page, language: Sample['language'] = 'en') {
+ const label = translations[language].assetsAttachedCv.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ // MUI names its Select from the field label and the current selected value.
+ return page.getByRole('combobox', { name: new RegExp(`^${label}(?:\\s|$)`) });
+}
+function drafts(page: Page, language: Sample['language'] = 'en') { return {
+ answer: page.getByRole('textbox', { name: translations[language].assetsApplicationAnswer, exact: true }),
+ message: page.getByRole('textbox', { name: translations[language].assetsRecruiterMessage, exact: true }),
+}; }
+async function login(api: APIRequestContext, email: string): Promise {
+ expect((await api.post(`${apiUrl}/auth/login`, { data: { email, password } })).status()).toBe(200);
+ const me = await json<{ id: string; sessionContext: string }>(await api.get(`${apiUrl}/auth/me`));
+ const csrf = (await api.storageState()).cookies.find(cookie => cookie.name === 'XSRF-TOKEN'); expect(csrf).toBeDefined();
+ return { id: me.id, headers: { 'X-CSRF-TOKEN': csrf!.value, 'X-Session-Context': me.sessionContext } };
+}
+async function account(admin: APIRequestContext, owner: Owner) {
+ const email = `p706-forms-${Date.now()}-${Math.random().toString(36).slice(2)}@example.test`;
+ expect((await admin.post(`${apiUrl}/users`, { headers: owner.headers, data: { email, password, roles: [] } })).status()).toBe(200);
+ return email;
+}
+const test = base.extend<{ session: Session }>({
+ session: async ({ page, context, playwright }, provide, info) => {
+ expect(process.env.PLAYWRIGHT_REUSE_SERVERS).not.toBe('true');
+ expect(info.config.metadata.isolatedDataRoot).toEqual(expect.any(String));
+ const admin = await playwright.request.newContext(); const requests: RequestRecord[] = []; const blocked: string[] = [];
+ page.on('request', request => {
+ const url = new URL(request.url());
+ if (!/^\/api\/(?:jobapplications(?:\/|$)|cv\/variants(?:\/|$)|attachments(?:\/|$))/.test(url.pathname)) return;
+ requests.push({ method: request.method(), path: url.pathname, body: request.postData() ? request.postDataJSON() : null });
+ });
+ try {
+ const administrator = await login(admin, 'e2e@example.test');
+ const owner = await login(page.request, await account(admin, administrator));
+ expect((await json<{ entitlements: { ai: boolean } }>(await page.request.get(`${apiUrl}/auth/me`))).entitlements.ai).toBe(false);
+ await context.route('**/*', route => {
+ const url = new URL(route.request().url());
+ if (['localhost', '127.0.0.1'].includes(url.hostname) || ['blob:', 'data:', 'about:'].includes(url.protocol)) return route.continue();
+ blocked.push(url.origin + url.pathname); return route.abort();
+ });
+ await provide({ owner, admin, administrator, requests });
+ } finally {
+ await info.attach('application-forms-boundary', { body: JSON.stringify({ boundary, requests, blockedExternalRequests: blocked }), contentType: 'application/json' });
+ await admin.dispose();
+ }
+ },
+});
+function profile(headline = 'Original reviewed career evidence') {
+ const empty = emptyStructuredCv();
+ return { ...empty, contact: { ...empty.contact, fullName: 'Åse Recorded Application Identity', headline },
+ summary: ['Canonical owner-reviewed evidence — æøå'], skills: ['C#', 'SQL'] };
+}
+async function source(api: APIRequestContext, owner: Owner, headline?: string) {
+ return json(await api.put(`${apiUrl}/career/profile`, { headers: owner.headers, data: { profile: profile(headline), cvText: 'Exact owner-reviewed source — æøå' } }));
+}
+async function createJob(api: APIRequestContext, owner: Owner, title = 'Recorded application material — æøå') {
+ const company = await json<{ id: number }>(await api.post(`${apiUrl}/companies`, { headers: owner.headers, data: { name: 'Recorded application company — æøå', source: 'User' } }), 201);
+ return json(await api.post(`${apiUrl}/jobapplications`, { headers: owner.headers, data: {
+ companyId: company.id, jobTitle: title, status: 'Saved', notes: 'Human notes remain separate — æøå', description: 'Recorded C# and SQL role; fixture facts only.',
+ } }), 201);
+}
+async function variant(api: APIRequestContext, owner: Owner, name: string) {
+ return json(await api.post(`${apiUrl}/cv/variants`, { headers: owner.headers, data: {
+ name, settings: { themeId: 'minimal', language: 'nb-NO', showPhoto: false, showPageNumbers: false, showIcons: true, sections: [], overrides: {}, customSections: [] },
+ } }));
+}
+async function saveLetter(api: APIRequestContext, owner: Owner, id: number, text: string) {
+ return json(await api.put(`${jobUrl(id)}/cover-letter`, { headers: owner.headers, data: { text, source: 'manual' } }));
+}
+async function saveDrafts(api: APIRequestContext, owner: Owner, id: number, answer: string, message: string) {
+ expect((await api.put(`${jobUrl(id)}/application-drafts`, { headers: owner.headers, data: { applicationAnswerDraft: answer, recruiterMessageDraft: message } })).status()).toBe(204);
+}
+async function upload(api: APIRequestContext, owner: Owner, id: number) {
+ const bytes = Buffer.from('Exact archived application evidence — æøå\nOriginal bytes retained.\n', 'utf8');
+ const name = 'Recorded application evidence — æøå.txt';
+ expect((await api.post(`${apiUrl}/attachments`, { headers: owner.headers, multipart: { jobId: String(id), files: { name, mimeType: 'text/plain', buffer: bytes } } })).ok()).toBe(true);
+ const [attachment] = await json<{ id: number; fileName: string }[]>(await api.get(`${apiUrl}/attachments/${id}`));
+ expect(attachment.id).toBeGreaterThan(0); return { attachment, bytes, name };
+}
+async function fixture(page: Page, owner: Owner, long = false) {
+ await source(page.request, owner);
+ const job = await createJob(page.request, owner, long ? 'Langsiktig søknad om samfunnskritisk plattformutvikling med mange samarbeidspartnere og særnorske bokstaver Æ Ø Å' : undefined);
+ const cv = await variant(page.request, owner, long ? 'CV med lang dokumenttittel for samfunnskritisk plattformutvikling og inkluderende arbeidsprosesser Æ Ø Å' : 'Recorded attached application CV');
+ await json(await page.request.put(`${jobUrl(job.id)}/cv`, { headers: owner.headers, data: { variantId: cv.id } }));
+ const originalLetter = long ? ('Reviewed long cover letter — æøå. Existing storage remains plain markdown.\n\n').repeat(55).trim() : 'Original reviewed cover letter — æøå';
+ const answer = long ? ('Recorded motivation and application answer — æøå.\n').repeat(24).trim() : 'Original application answer — æøå';
+ const message = long ? ('Reviewed recruiter message draft; never sent — æøå.\n').repeat(14).trim() : 'Original recruiter message; never sent';
+ await saveLetter(page.request, owner, job.id, originalLetter);
+ await saveDrafts(page.request, owner, job.id, answer, message);
+ return { job, cv, originalLetter, answer, message };
+}
+async function frozenFixture(page: Page, owner: Owner, long = false) {
+ const seeded = await fixture(page, owner, long); const file = await upload(page.request, owner, seeded.job.id);
+ const preserved = await json(await page.request.post(`${jobUrl(seeded.job.id)}/submitted-packages`, { headers: owner.headers }));
+ const detail = await json(await page.request.get(`${jobUrl(seeded.job.id)}/submitted-packages/${preserved.id}`));
+ return { ...seeded, ...file, preserved, detail };
+}
+async function enter(page: Page, owner: Owner, job: Job, sub: Subsection, sample: Sample = desktop) {
+ expect((await page.request.put(`${apiUrl}/auth/preferences/language`, { headers: owner.headers, data: { language: sample.language === 'nb' ? 'nb-NO' : 'en' } })).status()).toBe(204);
+ await page.addInitScript(({ language, theme }) => {
+ if (localStorage.getItem('uiLanguage') === null) localStorage.setItem('uiLanguage', language);
+ if (localStorage.getItem('jobtracker.themeMode') === null) localStorage.setItem('jobtracker.themeMode', theme);
+ }, sample);
+ await page.setViewportSize({ width: sample.width, height: sample.height }); await page.emulateMedia({ reducedMotion: 'reduce' });
+ await page.goto(`/jobs/${job.id}?section=application&sub=${sub}&source=phase7-forms&followMode=waiting-update#materials`);
+ const stored = await page.evaluate(() => ({ language: localStorage.getItem('uiLanguage'), theme: localStorage.getItem('jobtracker.themeMode') }));
+ if (stored.language !== sample.language || stored.theme !== sample.theme) {
+ await page.evaluate(({ language, theme }) => { localStorage.setItem('uiLanguage', language); localStorage.setItem('jobtracker.themeMode', theme); }, sample); await page.reload();
+ }
+ const t = translations[sample.language];
+ await expect(page.locator('html')).toHaveAttribute('lang', sample.language === 'nb' ? 'nb-NO' : 'en');
+ await expect(page.locator('html')).toHaveAttribute('data-color-scheme', sample.theme);
+ await expect(page.getByRole('heading', { level: 1 })).toHaveCount(1);
+ await expect(page.getByRole('heading', { level: 2, name: job.jobTitle, exact: true })).toBeVisible();
+ await expect(page.getByRole('tablist', { name: t.workspaceSections, exact: true }).getByRole('tab')).toHaveCount(7);
+ await expect(page.getByRole('tablist', { name: t.workspaceApplicationSections, exact: true }).getByRole('tab')).toHaveCount(4);
+ expect(new URL(page.url()).searchParams.get('followMode')).toBe('waiting-update');
+}
+async function selectCv(page: Page, value: CvVariant | null, language: Sample['language'] = 'en') {
+ await keyboard(page, attachedCv(page, language));
+ const option = value ? page.getByRole('option').filter({ hasText: value.name }) : page.getByRole('option', { name: translations[language].assetsNone, exact: true });
+ await keyboard(page, option);
+}
+
+test('[P7-06 PRES-W01 W04 W10] same-route job navigation waits for owned answers and preserves both jobs on save', async ({ page, session }, info) => {
+ const first = await fixture(page, session.owner);
+ const original = await json<{ companyId: number }>(await page.request.get(jobUrl(first.job.id)));
+ const second = await json(await page.request.post(`${apiUrl}/jobapplications`, { headers: session.owner.headers, data: {
+ companyId: original.companyId, jobTitle: 'Second independent application draft owner', status: 'Saved', notes: 'Independent second-job notes',
+ } }), 201);
+ const secondAnswer = 'Owned second-job answer — æøå';
+ const secondMessage = 'Owned second-job recruiter message; never sent';
+ await saveDrafts(page.request, session.owner, second.id, secondAnswer, secondMessage);
+ await enter(page, session.owner, first.job, 'answers');
+ await expect(drafts(page).answer).toHaveValue(first.answer);
+ const origin = await page.evaluate(() => performance.timeOrigin);
+ const held = hold(); const reached = hold();
+ const workspaceUrl = `${jobUrl(second.id)}/workspace`;
+ await page.route(workspaceUrl, async route => { reached.release(); await held.pending; await route.continue(); });
+ try {
+ await keyboard(page, page.getByRole('button', { name: new RegExp(translations.en.quickSearch) }));
+ const commands = page.getByRole('dialog', { name: translations.en.quickSearch });
+ await commands.getByRole('textbox', { name: translations.en.quickSearch }).fill(second.jobTitle);
+ await keyboard(page, commands.getByRole('button').filter({ hasText: second.jobTitle }));
+ await reached.pending;
+ await expect(page).toHaveURL(new RegExp(`/jobs/${second.id}$`));
+ expect(await page.evaluate(() => performance.timeOrigin)).toBe(origin);
+ await keyboard(page, page.getByRole('tab', { name: translations.en.workspaceApplication, exact: true }));
+ await keyboard(page, page.getByRole('tab', { name: translations.en.workspaceAnswers, exact: true }));
+ await expect(drafts(page).answer).toHaveCount(0);
+ await expect(drafts(page).message).toHaveCount(0);
+ expect(count(session, 'PUT', `/api/jobapplications/${second.id}/application-drafts`)).toHaveLength(0);
+ await capture(page, info, desktop, 'held-second-job-aggregate-no-foreign-drafts');
+ const loaded = response(page, 'GET', workspaceUrl); held.release(); await loaded;
+ await expect(drafts(page).answer).toHaveValue(secondAnswer);
+ await expect(drafts(page).message).toHaveValue(secondMessage);
+ const reviewed = 'Edited second-job answer only — æøå';
+ await drafts(page).answer.fill(reviewed);
+ const saved = response(page, 'PUT', `${jobUrl(second.id)}/application-drafts`);
+ await keyboard(page, page.getByRole('button', { name: translations.en.assetsSaveDrafts, exact: true }));
+ expect((await saved).status()).toBe(204);
+ await expect(drafts(page).message).toHaveValue(secondMessage);
+ const persistedSecond = await json(await page.request.get(workspaceUrl));
+ expect(persistedSecond.applicationAnswerDraft).toBe(reviewed);
+ expect(persistedSecond.recruiterMessageDraft).toBe(secondMessage);
+ const persistedFirst = await json(await page.request.get(`${jobUrl(first.job.id)}/workspace`));
+ expect(persistedFirst.applicationAnswerDraft).toBe(first.answer);
+ expect(persistedFirst.recruiterMessageDraft).toBe(first.message);
+ expect(count(session, 'PUT', `/api/jobapplications/${second.id}/application-drafts`)).toEqual([{ method: 'PUT', path: `/api/jobapplications/${second.id}/application-drafts`, body: { applicationAnswerDraft: reviewed, recruiterMessageDraft: secondMessage } }]);
+ await capture(page, info, desktop, 'second-job-answer-saved-first-job-preserved');
+ } finally { held.release(); await page.unroute(workspaceUrl); }
+});
+async function capture(page: Page, info: TestInfo, sample: Sample, state: string) {
+ await page.evaluate(async () => { await document.fonts.ready; await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve()))); });
+ const geometry = await page.evaluate(() => ({ width: innerWidth, height: innerHeight, rootOverflow: Math.max(0, document.documentElement.scrollWidth - document.documentElement.clientWidth),
+ focused: { tag: document.activeElement?.tagName, role: document.activeElement?.getAttribute('role'), label: document.activeElement?.getAttribute('aria-label') } }));
+ const axe = await new AxeBuilder({ page }).options({ rules: { 'target-size': { enabled: true } } }).analyze();
+ const name = `p706-forms-${sample.language}-${sample.theme}-${sample.width}-${state}`;
+ await info.attach(`${name}-audit`, { body: JSON.stringify({ sample, state, geometry, boundary, ariaSnapshot: await page.locator('body').ariaSnapshot(),
+ axe: { scope: 'All default-enabled rules plus target-size, no exclusions', violations: axe.violations, incomplete: axe.incomplete, passes: axe.passes.map(rule => ({ id: rule.id, nodes: rule.nodes.length })) },
+ qualifications: ['Finite Chromium states, not all combinations or physical devices.', 'All incomplete vectors retained for contextual triage; not human screen-reader, touch/software keyboard or real browser zoom acceptance.',
+ 'Full-page screenshots do not establish real viewport focus occlusion or every-pixel contrast.', 'Print iframe/canvas may scroll internally; outer app root reflow measured separately. Frozen HTML unchanged, not a substitute for existing full fidelity corpus.'],
+ }), contentType: 'application/json' });
+ await info.attach(name, { body: await page.screenshot({ fullPage: true, animations: 'disabled' }), contentType: 'image/png' });
+ expect(geometry.rootOverflow).toBe(0); expect(axe.violations).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');
+ const gate = hold(); let reads = 0;
+ await page.route(`${jobUrl(job.id)}/cv`, async route => {
+ if (route.request().method() !== 'GET') return route.continue();
+ reads += 1; if (reads === 1) { await gate.pending; await route.fulfill({ status: 503, json: {} }); } else return route.continue();
+ });
+ try {
+ await enter(page, session.owner, job, 'cv'); await expect(section(page, translations.en.workspaceCv).getByRole('status')).toContainText(translations.en.loading);
+ await capture(page, info, desktop, 'held-cv-read'); gate.release();
+ await expect(section(page, translations.en.workspaceCv).getByRole('alert')).toContainText(translations.en.assetsLoadFailed);
+ await keyboard(page, section(page, translations.en.workspaceCv).getByRole('button', { name: translations.en.retry, exact: true }));
+ await expect(childFocus(page, translations.en.workspaceCv)).toBeFocused(); await expect.poll(() => reads).toBe(2);
+ await expect(attachedCv(page)).toBeVisible();
+ for (const current of [first, second, null]) {
+ const written = response(page, 'PUT', `${jobUrl(job.id)}/cv`); await selectCv(page, current); expect((await written).status()).toBe(200);
+ await expect(attachedCv(page)).toBeEnabled();
+ expect((await json(await page.request.get(`${jobUrl(job.id)}/cv`))).attachedVariantId).toBe(current?.id ?? null);
+ }
+ expect(count(session, 'PUT', `/api/jobapplications/${job.id}/cv`).map(value => value.body)).toEqual([{ variantId: first.id }, { variantId: second.id }, { variantId: null }]);
+ const available = await json(await page.request.get(`${apiUrl}/cv/variants`));
+ expect(available.map(value => value.id).sort((a, b) => a - b)).toEqual([first.id, second.id].sort((a, b) => a - b));
+ expect(available.every(value => value.jobApplicationId === null)).toBe(true);
+ const foreign = await playwright.request.newContext();
+ try {
+ const other = await login(foreign, await account(session.admin, session.administrator));
+ const foreignVariant = await variant(foreign, other, 'Foreign private variant');
+ expect((await page.request.put(`${jobUrl(job.id)}/cv`, { headers: session.owner.headers, data: { variantId: foreignVariant.id } })).status()).toBe(404);
+ expect((await page.request.put(`${jobUrl(job.id)}/cv`, { headers: session.owner.headers, data: { variantId: 2147483647 } })).status()).toBe(404);
+ for (const suffix of ['cv', 'cover-letter', 'submitted-packages']) expect((await foreign.get(`${jobUrl(job.id)}/${suffix}`)).status()).toBe(404);
+ expect((await foreign.put(`${jobUrl(job.id)}/cv`, { headers: other.headers, data: { variantId: null } })).status()).toBe(404);
+ expect((await json(await page.request.get(`${jobUrl(job.id)}/cv`))).availableVariants.some(value => value.id === foreignVariant.id)).toBe(false);
+ } finally { await foreign.dispose(); }
+ await capture(page, info, desktop, 'cv-detached-preserved');
+ } finally { gate.release(); }
+});
+
+test('[P7-06 forms] long letter failed held Save retains snapshot and dirty restore Cancel writes nothing before additive explicit restore', async ({ page, session }, info) => {
+ test.setTimeout(120_000); const seeded = await fixture(page, session.owner); const url = `${jobUrl(seeded.job.id)}/cover-letter`;
+ await saveLetter(page.request, session.owner, seeded.job.id, 'Second reviewed letter — æøå');
+ const before = await json(await page.request.get(url));
+ await enter(page, session.owner, seeded.job, 'letter');
+ const draft = ' ' + ('Long manually reviewed application letter — æøå. Preserve every sentence and markdown **evidence**.\n\n').repeat(60) + ' ';
+ await letter(page).fill(draft); const gate = hold(); let saves = 0;
+ await page.route(url, async route => {
+ if (route.request().method() !== 'PUT') return route.continue();
+ saves += 1; if (saves === 1) { await gate.pending; await route.fulfill({ status: 503, json: {} }); } else return route.continue();
+ });
+ try {
+ const body = section(page, translations.en.workspaceCoverLetter);
+ await keyboard(page, body.getByRole('button', { name: translations.en.save, exact: true })); await expect.poll(() => saves).toBe(1);
+ await expect(letter(page)).toBeDisabled(); await expect(childFocus(page, translations.en.workspaceCoverLetter)).toBeFocused();
+ await expect(body.getByRole('button', { name: translations.en.assetsDiscardChanges, exact: true })).toBeDisabled();
+ expect(count(session, 'PUT', `/api/jobapplications/${seeded.job.id}/cover-letter`).map(value => value.body)).toEqual([{ text: draft, source: 'manual' }]);
+ await capture(page, info, desktop, 'held-letter-save'); gate.release();
+ await expect(body.getByRole('alert')).toContainText(translations.en.assetsCoverSaveFailed); await expect(letter(page)).toHaveValue(draft);
+ expect(await json(await page.request.get(url))).toEqual(before); expect(saves).toBe(1);
+ const saved = response(page, 'PUT', url); await keyboard(page, body.getByRole('button', { name: translations.en.save, exact: true }));
+ const current = await json(await saved); expect(current.text).toBe(draft.trim()); expect(current.currentVersion).toBe(3);
+ expect(current.versions.map(value => [value.version, value.source, value.aiAction])).toEqual([[3, 'manual', null], [2, 'manual', null], [1, 'manual', null]]);
+ await expect(letter(page)).toHaveValue(draft.trim()); await letter(page).fill('Unsaved changes must survive Cancel — æøå');
+ const restore = page.getByRole('button', { name: interpolate(translations.en.assetsRestoreVersionNumber, { version: 1 }), exact: true });
+ await keyboard(page, restore);
+ const dialog = page.getByRole('dialog', { name: translations.en.assetsRestoreDiscardTitle, exact: true });
+ await expect(dialog).toContainText(translations.en.assetsRestoreDiscardMessage);
+ await keyboard(page, dialog.getByRole('button', { name: translations.en.cancel, exact: true }));
+ await expect(dialog).toHaveCount(0); await expect(restore).toBeFocused(); await expect(letter(page)).toHaveValue('Unsaved changes must survive Cancel — æøå');
+ expect(count(session, 'POST', `/api/jobapplications/${seeded.job.id}/cover-letter/versions/1/restore`)).toHaveLength(0);
+ expect(await json(await page.request.get(url))).toEqual(current);
+ await keyboard(page, restore); const restored = response(page, 'POST', `${url}/versions/1/restore`);
+ await keyboard(page, dialog.getByRole('button', { name: translations.en.assetsRestoreConfirm, exact: true }));
+ const readback = await json(await restored); expect(readback.text).toBe(seeded.originalLetter); expect(readback.currentVersion).toBe(4);
+ expect(readback.versions.map(value => [value.version, value.source, value.aiAction])).toEqual([[4, 'restore', null], [3, 'manual', null], [2, 'manual', null], [1, 'manual', null]]);
+ await expect(letter(page)).toHaveValue(seeded.originalLetter); await capture(page, info, desktop, 'restored-additive-letter-history');
+ } finally { gate.release(); }
+});
+
+test('[P7-06 forms] failed held Answers save preserves both drafts and ordinary Notes through explicit recovery and reload', async ({ page, session }, info) => {
+ test.setTimeout(90_000); const seeded = await fixture(page, session.owner); const url = `${jobUrl(seeded.job.id)}/application-drafts`;
+ const original = await json(await page.request.get(jobUrl(seeded.job.id)));
+ await enter(page, session.owner, seeded.job, 'answers'); const fields = drafts(page); const answer = 'Reviewed answer with **specific evidence** — æøå\nSecond line.'; const message = 'Reviewed recruiter note — æøå; draft only, never sent.';
+ await fields.answer.fill(answer); await fields.message.fill(message);
+ const gate = hold(); let writes = 0;
+ await page.route(url, async route => { writes += 1; if (writes === 1) { await gate.pending; await route.fulfill({ status: 503, json: {} }); } else return route.continue(); });
+ try {
+ const body = section(page, translations.en.assetsApplicationDrafts);
+ await keyboard(page, body.getByRole('button', { name: translations.en.assetsSaveDrafts, exact: true }));
+ await expect.poll(() => writes).toBe(1); await expect(fields.answer).toBeDisabled(); await expect(fields.message).toBeDisabled(); await expect(childFocus(page, translations.en.assetsApplicationDrafts)).toBeFocused();
+ expect(count(session, 'PUT', `/api/jobapplications/${seeded.job.id}/application-drafts`).map(value => value.body)).toEqual([{ applicationAnswerDraft: answer, recruiterMessageDraft: message }]);
+ await capture(page, info, desktop, 'held-two-draft-save'); gate.release();
+ await expect(body.getByRole('alert')).toContainText(translations.en.assetsDraftsSaveFailed); await expect(fields.answer).toHaveValue(answer); await expect(fields.message).toHaveValue(message);
+ expect((await json(await page.request.get(`${jobUrl(seeded.job.id)}/workspace`))).applicationAnswerDraft).toBe(seeded.answer); expect(writes).toBe(1);
+ const saved = response(page, 'PUT', url); await keyboard(page, body.getByRole('button', { name: translations.en.assetsSaveDrafts, exact: true })); expect((await saved).status()).toBe(204);
+ await expect(body.getByRole('button', { name: translations.en.assetsSaveDrafts, exact: true })).toBeDisabled(); await page.reload();
+ await expect(drafts(page).answer).toHaveValue(answer); await expect(drafts(page).message).toHaveValue(message);
+ const readback = await json(await page.request.get(`${jobUrl(seeded.job.id)}/workspace`));
+ expect(readback.applicationAnswerDraft).toBe(answer); expect(readback.recruiterMessageDraft).toBe(message);
+ const stored = await json(await page.request.get(jobUrl(seeded.job.id))); expect(removeApplicationAnswerDraft(stored.notes)).toBe(removeApplicationAnswerDraft(original.notes));
+ await keyboard(page, page.getByRole('tablist', { name: translations.en.workspaceSections, exact: true }).getByRole('tab', { name: translations.en.workspaceTasks, exact: true }));
+ await expect(section(page, translations.en.workspaceNotes)).toContainText('Human notes remain separate — æøå'); await expect(section(page, translations.en.workspaceNotes)).not.toContainText(answer);
+ expect(count(session, 'PUT', `/api/jobapplications/${seeded.job.id}/application-drafts`)).toHaveLength(2);
+ await capture(page, info, desktop, 'drafts-saved-personal-notes-preserved');
+ } finally { gate.release(); }
+});
+
+test('[P7-06 forms] actual native capture freezes CV and exact archive bytes after current sources change without renderer mutation', async ({ page, session }, info) => {
+ test.setTimeout(120_000); const seeded = await fixture(page, session.owner); const file = await upload(page.request, session.owner, seeded.job.id);
+ await enter(page, session.owner, seeded.job, 'submission'); const captured = response(page, 'POST', `${jobUrl(seeded.job.id)}/submitted-packages`);
+ await keyboard(page, section(page, translations.en.assetsSubmittedPackages).getByRole('button', { name: translations.en.assetsPackageCapture, exact: true }));
+ const summary = await json(await captured); expect(summary.version).toBe(1); expect(summary.attachments).toHaveLength(1);
+ const preservedUrl = `${jobUrl(seeded.job.id)}/submitted-packages/${summary.id}`; const before = await json(await page.request.get(preservedUrl));
+ expect(before.renderedCvHtml).toContain('Original reviewed career evidence'); expect(before.coverLetterText).toBe(seeded.originalLetter);
+ expect(summary.cvVariantId).toBe(seeded.cv.id); expect(summary.cvVariantVersion).toBe(seeded.cv.version);
+ expect(summary.attachments[0].sha256).toBe(createHash('sha256').update(file.bytes).digest('hex'));
+ await saveLetter(page.request, session.owner, seeded.job.id, 'Later current letter — æøå');
+ await saveDrafts(page.request, session.owner, seeded.job.id, 'Later answer', 'Later recruiter draft');
+ const cv = await json(await page.request.get(`${apiUrl}/cv/variants/${seeded.cv.id}`));
+ const changedCv = await json(await page.request.put(`${apiUrl}/cv/variants/${cv.id}`, { headers: session.owner.headers, data: { name: 'Later current CV', settings: { ...cv.settings, headline: 'Later CV-only headline' }, expectedVersion: cv.version, source: 'manual' } }));
+ await source(page.request, session.owner, 'Later canonical headline');
+ expect((await page.request.delete(`${apiUrl}/attachments/${file.attachment.id}`, { headers: session.owner.headers })).status()).toBe(204);
+ expect(await json(await page.request.get(preservedUrl))).toEqual(before);
+ const archiveUrl = `${preservedUrl}/attachments/${summary.attachments[0].id}`; const archived = await page.request.get(archiveUrl); expect(archived.status()).toBe(200); expect(await archived.body()).toEqual(file.bytes);
+ await page.reload(); const view = section(page, translations.en.assetsSubmittedPackages).getByRole('button', { name: translations.en.assetsPackageView, exact: true }); await keyboard(page, view);
+ const dialog = page.getByRole('dialog', { name: interpolate(translations.en.assetsSubmittedPackageVersion, { version: 1 }), exact: true });
+ await expect(dialog).toContainText(seeded.originalLetter); await expect(dialog).toContainText(seeded.answer); await expect(dialog).not.toContainText('Later current letter');
+ const scrollRegion = dialog.getByRole('region', { name: interpolate(translations.en.assetsSubmittedPackageVersion, { version: 1 }), exact: true });
+ await scrollRegion.focus(); await expect(scrollRegion).toBeFocused();
+ const beforeScroll = await scrollRegion.evaluate(node => node.scrollTop); await page.keyboard.press('PageDown');
+ await expect.poll(() => scrollRegion.evaluate(node => node.scrollTop)).toBeGreaterThan(beforeScroll);
+ await page.keyboard.press('Home'); await expect.poll(() => scrollRegion.evaluate(node => node.scrollTop)).toBe(0);
+ const iframe = dialog.getByTitle(translations.en.assetsPackageCvPreview, { exact: true });
+ await expect(iframe).toHaveAttribute('sandbox', ''); await expect(iframe).toHaveAttribute('srcdoc', before.renderedCvHtml!);
+ expect(await iframe.evaluate(node => getComputedStyle(node).backgroundColor)).toBe('rgb(255, 255, 255)');
+ await capture(page, info, desktop, 'frozen-original-white-print-preview');
+ await page.keyboard.press('Escape'); await expect(view).toBeFocused();
+ const download = page.waitForEvent('download'); await keyboard(page, section(page, translations.en.assetsSubmittedPackages).getByRole('button').filter({ hasText: file.name }));
+ const result = await download; expect(result.suggestedFilename()).toBe(file.name); const path = await result.path(); expect(path).not.toBeNull(); expect(await readFile(path!)).toEqual(file.bytes);
+ expect(count(session, 'POST', `/api/jobapplications/${seeded.job.id}/submitted-packages`)).toHaveLength(1);
+ expect(await json(await page.request.get(preservedUrl))).toEqual(before);
+ await info.attach('application-frozen-bytes', { body: JSON.stringify({ boundary, packageId: summary.id, recordedFileSha256: summary.attachments[0].sha256, downloadSha256: createHash('sha256').update(file.bytes).digest('hex'),
+ preservedCvVersion: summary.cvVariantVersion, currentCvVersion: changedCv.version, qualification: 'Exact scoped real-HTTP current-source changes and archive-byte checks; existing ordinary P2-D01/full renderer fidelity runs remain required separately.' }), contentType: 'application/json' });
+});
+
+test('[P7-06 forms] failed preserved detail and download retain history and Retry only the original owned read', async ({ page, session }, info) => {
+ test.setTimeout(120_000); const seeded = await frozenFixture(page, session.owner); const detailUrl = `${jobUrl(seeded.job.id)}/submitted-packages/${seeded.preserved.id}`;
+ const downloadUrl = `${detailUrl}/attachments/${seeded.preserved.attachments[0].id}`;
+ await enter(page, session.owner, seeded.job, 'submission'); const body = section(page, translations.en.assetsSubmittedPackages); await expect(body.getByRole('button', { name: translations.en.assetsPackageView, exact: true })).toBeVisible();
+ const gate = hold(); let details = 0; let downloads = 0;
+ await page.route(detailUrl, async route => { details += 1; if (details === 1) await route.fulfill({ status: 503, json: {} }); else { await gate.pending; await route.continue(); } });
+ await page.route(downloadUrl, async route => { downloads += 1; if (downloads === 1) await route.fulfill({ status: 503, json: {} }); else return route.continue(); });
+ try {
+ await keyboard(page, body.getByRole('button', { name: translations.en.assetsPackageView, exact: true }));
+ await expect(body.getByRole('alert')).toContainText(translations.en.assetsPackageLoadFailed); await expect(body.getByText(interpolate(translations.en.assetsSubmittedPackageVersion, { version: 1 }), { exact: true })).toBeVisible();
+ await expect(body.getByRole('button', { name: translations.en.assetsPackageView, exact: true })).toBeVisible(); expect(mutations(session)).toEqual([]);
+ const baseline = assetReads(session, seeded.job.id).length;
+ await keyboard(page, body.getByRole('button', { name: translations.en.retry, exact: true })); await expect.poll(() => details).toBe(2);
+ await expect(childFocus(page, translations.en.assetsSubmittedPackages)).toBeFocused(); await capture(page, info, desktop, 'held-original-package-detail-retry'); gate.release();
+ const dialog = page.getByRole('dialog'); await expect(dialog).toContainText(seeded.originalLetter); await page.keyboard.press('Escape');
+ await expect(body.getByRole('button', { name: translations.en.assetsPackageView, exact: true })).toBeFocused();
+ expect(assetReads(session, seeded.job.id)).toHaveLength(baseline + 1);
+ await keyboard(page, body.getByRole('button').filter({ hasText: seeded.name }));
+ await expect(body.getByRole('alert')).toContainText(translations.en.assetsPackageDownloadFailed); await expect(body.getByText(interpolate(translations.en.assetsSubmittedPackageVersion, { version: 1 }), { exact: true })).toBeVisible();
+ const download = page.waitForEvent('download'); await keyboard(page, body.getByRole('button', { name: translations.en.retry, exact: true }));
+ const result = await download; const path = await result.path(); expect(path).not.toBeNull(); expect(await readFile(path!)).toEqual(seeded.bytes);
+ expect(details).toBe(2); expect(downloads).toBe(2); expect(mutations(session)).toEqual([]);
+ expect(await json(await page.request.get(detailUrl))).toEqual(seeded.detail);
+ await capture(page, info, desktop, 'original-archive-retry-history-retained');
+ } finally { gate.release(); }
+});
+
+test('[P7-06 forms] live EN/Bokmal retains dirty letter and both answer drafts without asset GET or writes', async ({ page, session }, info) => {
+ test.setTimeout(90_000); const seeded = await fixture(page, session.owner); await enter(page, session.owner, seeded.job, 'letter');
+ await expect(letter(page)).toHaveValue(seeded.originalLetter); await letter(page).fill('Dirty owner text remains the same across UI locale — æøå');
+ let reads = assetReads(session, seeded.job.id).length;
+ const firstLocale = response(page, 'PUT', `${apiUrl}/auth/preferences/language`); await keyboard(page, page.locator('header').first().getByRole('button', { name: 'Norsk', exact: true }));
+ const firstLocaleResponse = await firstLocale;
+ await info.attach('application-locale-first-http', { body: JSON.stringify({ status: firstLocaleResponse.status(), body: await firstLocaleResponse.text() }), contentType: 'application/json' });
+ expect(firstLocaleResponse.status()).toBe(204);
+ await expect(page.locator('html')).toHaveAttribute('lang', 'nb-NO'); await expect(letter(page, 'nb')).toHaveValue('Dirty owner text remains the same across UI locale — æøå');
+ expect(assetReads(session, seeded.job.id)).toHaveLength(reads); expect(mutations(session)).toEqual([]);
+ await capture(page, info, { ...desktop, language: 'nb' }, 'dirty-letter-live-locale');
+ await keyboard(page, section(page, translations.nb.workspaceCoverLetter).getByRole('button', { name: translations.nb.assetsDiscardChanges, exact: true }));
+ await keyboard(page, page.getByRole('tablist', { name: translations.nb.workspaceApplicationSections, exact: true }).getByRole('tab', { name: translations.nb.workspaceAnswers, exact: true }));
+ const fields = drafts(page, 'nb'); await expect(fields.answer).toHaveValue(seeded.answer);
+ await fields.answer.fill('Ulagret søknadssvar — æøå'); await fields.message.fill('Ulagret melding; ikke sendt — æøå'); reads = assetReads(session, seeded.job.id).length;
+ const secondLocale = response(page, 'PUT', `${apiUrl}/auth/preferences/language`); await keyboard(page, page.locator('header').first().getByRole('button', { name: 'English', exact: true }));
+ const secondLocaleResponse = await secondLocale;
+ await info.attach('application-locale-second-http', { body: JSON.stringify({ status: secondLocaleResponse.status(), body: await secondLocaleResponse.text() }), contentType: 'application/json' });
+ expect(secondLocaleResponse.status()).toBe(204);
+ await expect(page.locator('html')).toHaveAttribute('lang', 'en'); await expect(drafts(page).answer).toHaveValue('Ulagret søknadssvar — æøå'); await expect(drafts(page).message).toHaveValue('Ulagret melding; ikke sendt — æøå');
+ expect(assetReads(session, seeded.job.id)).toHaveLength(reads); expect(mutations(session)).toEqual([]);
+ await capture(page, info, desktop, 'dirty-two-drafts-live-locale');
+ await info.attach('application-locale-contract', { body: JSON.stringify({ boundary, assetGetDelta: 0, writes: 0, qualification: 'Assets only: existing workspace aggregate may independently reload on locale. Persisted document/draft language is not rewritten by UI language; no model calls.' }), contentType: 'application/json' });
+});
+
+test('[P7-06 forms] real duplicate then failed attach preserves created copy and original without automatic duplicate retry', async ({ page, session }, info) => {
+ test.setTimeout(90_000); const seeded = await fixture(page, session.owner);
+ const original = await json(await page.request.get(`${apiUrl}/cv/variants/${seeded.cv.id}`));
+ await enter(page, session.owner, seeded.job, 'cv'); const attachUrl = `${jobUrl(seeded.job.id)}/cv`; let attaches = 0;
+ await page.route(attachUrl, async route => { if (route.request().method() !== 'PUT') return route.continue(); attaches += 1; return attaches === 1 ? route.fulfill({ status: 503, json: {} }) : route.continue(); });
+ const copyResponse = response(page, 'POST', `${apiUrl}/cv/variants/${seeded.cv.id}/duplicate`);
+ await keyboard(page, section(page, translations.en.workspaceCv).getByRole('button', { name: translations.en.assetsDuplicateForJob, exact: true }));
+ const copy = await json(await copyResponse);
+ await expect(section(page, translations.en.workspaceCv).getByRole('alert').filter({ hasText: translations.en.assetsAttachCvFailed })).toBeVisible();
+ await expect(section(page, translations.en.workspaceCv).getByText(translations.en.assetsCopyCreatedHelp, { exact: true })).toBeVisible();
+ await expect(attachedCv(page)).toContainText(seeded.cv.name);
+ // DuplicateAsync preserves JobApplicationId before the second attach write. The
+ // existing read projects the newest associated variant, even while this UI
+ // still holds its pre-write selection. Explicit attach normalises cardinality.
+ expect((await json(await page.request.get(attachUrl))).attachedVariantId).toBe(copy.id);
+ expect(await json(await page.request.get(`${apiUrl}/cv/variants/${seeded.cv.id}`))).toEqual(original);
+ const partial = await json(await page.request.get(`${apiUrl}/cv/variants`));
+ expect(partial.map(value => value.id).sort((a, b) => a - b)).toEqual([seeded.cv.id, copy.id].sort((a, b) => a - b));
+ expect(partial.filter(value => value.jobApplicationId === seeded.job.id).map(value => value.id).sort((a, b) => a - b)).toEqual([seeded.cv.id, copy.id].sort((a, b) => a - b));
+ await expect(section(page, translations.en.workspaceCv).getByRole('button', { name: translations.en.retry, exact: true })).toHaveCount(0);
+ await capture(page, info, desktop, 'created-copy-failed-attach-preserved');
+ expect(count(session, 'POST', `/api/cv/variants/${seeded.cv.id}/duplicate`)).toHaveLength(1); expect(attaches).toBe(1);
+ const recovery = response(page, 'PUT', attachUrl);
+ const builderRead = response(page, 'GET', `${apiUrl}/cv/variants/${copy.id}`);
+ await keyboard(page, section(page, translations.en.workspaceCv).getByRole('button', { name: translations.en.assetsAttachCreatedCopy, exact: true }));
+ expect((await recovery).status()).toBe(200);
+ await expect(page).toHaveURL(new RegExp(`/career/builder/${copy.id}$`));
+ expect(await json(await builderRead)).toMatchObject({ id: copy.id, name: copy.name });
+ expect((await json(await page.request.get(attachUrl))).attachedVariantId).toBe(copy.id);
+ expect((await json(await page.request.get(`${apiUrl}/cv/variants`))).filter(value => value.jobApplicationId === seeded.job.id).map(value => value.id)).toEqual([copy.id]);
+ expect(count(session, 'POST', `/api/cv/variants/${seeded.cv.id}/duplicate`)).toHaveLength(1); expect(attaches).toBe(2);
+ await info.attach('application-duplicate-partial-write', { body: JSON.stringify({ boundary, originalId: seeded.cv.id, createdCopyId: copy.id, duplicatePostCount: 1, attachAttempts: 2,
+ recovery: 'Explicit Attach created copy reuses its exact ID, normalises association cardinality and opens its existing builder; no second duplicate.',
+ qualification: 'Existing duplicate retains JobApplicationId on both original and copy before explicit attach; newest-copy read projection can differ from stale UI. Two nontransactional HTTP writes remain, without rollback/idempotency/version guarantee.' }), contentType: 'application/json' });
+});
+
+test('[P7-06 forms] eight finite bilingual theme widths keep long CV, letter, drafts and frozen preview usable with keyboard', async ({ page, session }, info) => {
+ test.setTimeout(240_000); const seeded = await frozenFixture(page, session.owner, true);
+ const samples: (Sample & { sub: Subsection })[] = [
+ { language: 'en', theme: 'light', width: 320, height: 740, sub: 'answers' }, { language: 'nb', theme: 'dark', width: 360, height: 844, sub: 'letter' },
+ { language: 'en', theme: 'dark', width: 390, height: 844, sub: 'submission' }, { language: 'nb', theme: 'light', width: 768, height: 1024, sub: 'cv' },
+ { language: 'en', theme: 'dark', width: 1024, height: 768, sub: 'answers' }, { language: 'nb', theme: 'light', width: 1280, height: 320, sub: 'letter' },
+ { language: 'en', theme: 'light', width: 1440, height: 900, sub: 'submission' }, { language: 'nb', theme: 'dark', width: 1920, height: 1080, sub: 'cv' },
+ ];
+ for (const sample of samples) {
+ await enter(page, session.owner, seeded.job, sample.sub, sample); const t = translations[sample.language];
+ let letterHint: { anchor: Locator; hint: Locator; id: string } | null = null;
+ const selected = page.getByRole('tablist', { name: t.workspaceApplicationSections, exact: true }).locator('[aria-selected=true]'); await selected.focus(); await expect(selected).toBeFocused();
+ if (sample.sub === 'letter') {
+ await expect(letter(page, sample.language)).toHaveValue(seeded.originalLetter); await letter(page, sample.language).focus(); await expect(letter(page, sample.language)).toBeFocused();
+ for (const title of [t.richTextBold, t.richTextItalic, t.richTextUnderline, t.richTextLink]) { const button = section(page, t.workspaceCoverLetter).getByRole('button', { name: title, exact: true }); await button.focus(); await expect(button).toBeFocused(); }
+ const anchor = section(page, t.workspaceCoverLetter).getByRole('button', { name: t.richTextLink, exact: true });
+ await expect(anchor).toHaveAttribute('aria-label', t.richTextLink);
+ const visibleHint = page.getByRole('tooltip', { name: t.richTextLink, exact: true }); await expect(visibleHint).toBeVisible();
+ // Existing MUI Tooltip names the child; describeChild is false, so there
+ // is no aria-describedby contract to invent for this toolbar button.
+ const id = await visibleHint.getAttribute('id'); expect(id).not.toBeNull();
+ const hint = page.locator(`[id="${id}"]`); await expect(hint).toHaveAttribute('role', 'tooltip'); await expect(hint).toBeVisible();
+ letterHint = { anchor, hint, id: id! };
+ } else if (sample.sub === 'answers') {
+ const fields = drafts(page, sample.language); await expect(fields.answer).toHaveValue(seeded.answer); await expect(fields.message).toHaveValue(seeded.message);
+ await fields.answer.focus(); await expect(fields.answer).toBeFocused(); await fields.message.focus(); await expect(fields.message).toBeFocused();
+ } else if (sample.sub === 'cv') {
+ const control = attachedCv(page, sample.language); await expect(control).toContainText(seeded.cv.name); await control.focus(); await expect(control).toBeFocused();
+ await expect(page.getByRole('link', { name: t.assetsEditPreviewExport, exact: true })).toHaveAttribute('href', `/career/builder/${seeded.cv.id}`);
+ } else {
+ const view = section(page, t.assetsSubmittedPackages).getByRole('button', { name: t.assetsPackageView, exact: true }); await keyboard(page, view);
+ const dialog = page.getByRole('dialog', { name: interpolate(t.assetsSubmittedPackageVersion, { version: 1 }), exact: true });
+ await expect(dialog).toContainText(seeded.originalLetter); const iframe = dialog.getByTitle(t.assetsPackageCvPreview, { exact: true });
+ await expect(iframe).toHaveAttribute('srcdoc', seeded.detail.renderedCvHtml!); await expect(iframe).toHaveAttribute('sandbox', '');
+ expect(await iframe.evaluate(node => getComputedStyle(node).backgroundColor)).toBe('rgb(255, 255, 255)');
+ const close = dialog.getByRole('button', { name: t.close, exact: true }); await close.focus(); await expect(close).toBeFocused();
+ }
+ await page.evaluate(() => scrollTo(0, 0));
+ if (letterHint) {
+ await expect(letterHint.anchor).toBeFocused();
+ const geometry = await letterHint.anchor.evaluate(node => {
+ const rect = node.getBoundingClientRect();
+ return { top: rect.top, bottom: rect.bottom, left: rect.left, right: rect.right, viewportWidth: innerWidth, viewportHeight: innerHeight,
+ whollyOutsideViewport: rect.bottom <= 0 || rect.top >= innerHeight || rect.right <= 0 || rect.left >= innerWidth };
+ });
+ await expect(letterHint.hint).toBeAttached();
+ if (geometry.whollyOutsideViewport) await expect(letterHint.hint).toBeHidden();
+ else await expect(letterHint.hint).toBeVisible();
+ await info.attach(`application-letter-hint-${sample.language}-${sample.width}`, { body: JSON.stringify({ matchingTooltipId: letterHint.id, anchorAccessibleName: t.richTextLink, geometry,
+ expectation: geometry.whollyOutsideViewport ? 'Focused anchor outside viewport: matching hint hidden' : 'Focused anchor intersects viewport: matching hint visible' }), contentType: 'application/json' });
+ }
+ await capture(page, info, sample, `long-${sample.sub}`);
+ if (sample.sub === 'submission') { await page.keyboard.press('Escape'); await expect(section(page, t.assetsSubmittedPackages).getByRole('button', { name: t.assetsPackageView, exact: true })).toBeFocused(); }
+ }
+ expect(mutations(session)).toEqual([]);
+});
+
+test('[P7-06 forms] controlled language HTTP503 preserves local dirty drafts and Retry only repeats the language request', async ({ page, session }, info) => {
+ test.setTimeout(90_000);
+ const seeded = await fixture(page, session.owner); await enter(page, session.owner, seeded.job, 'letter');
+ await expect(letter(page)).toHaveValue(seeded.originalLetter);
+ const persistedBefore = await json<{ uiLanguage: string }>(await page.request.get(`${apiUrl}/auth/me`)); expect(persistedBefore.uiLanguage).toBe('en');
+ const preferenceUrl = `${apiUrl}/auth/preferences/language`;
+ const preferenceAttempts: { method: string; body: unknown }[] = [];
+ await page.route(preferenceUrl, async route => {
+ if (route.request().method() !== 'PUT') return route.continue();
+ preferenceAttempts.push({ method: route.request().method(), body: route.request().postDataJSON() });
+ return route.fulfill({ status: 503, json: {} });
+ });
+ const dirtyLetter = 'Controlled-failure draft; retain my exact unsaved letter — æøå';
+ await letter(page).fill(dirtyLetter); let reads = assetReads(session, seeded.job.id).length;
+ const firstResponse = response(page, 'PUT', preferenceUrl);
+ await keyboard(page, page.locator('header').first().getByRole('button', { name: 'Norsk', exact: true }));
+ expect((await firstResponse).status()).toBe(503); await expect(page.locator('html')).toHaveAttribute('lang', 'nb-NO');
+ await expect(letter(page, 'nb')).toHaveValue(dirtyLetter);
+ const nbFeedback = page.getByRole('alert').filter({ hasText: translations.nb.settingsLanguageSaveFailed }); await expect(nbFeedback).toBeVisible();
+ const firstRetry = response(page, 'PUT', preferenceUrl);
+ await keyboard(page, nbFeedback.getByRole('button', { name: translations.nb.retry, exact: true })); expect((await firstRetry).status()).toBe(503);
+ await expect(nbFeedback).toBeVisible(); await expect(letter(page, 'nb')).toHaveValue(dirtyLetter);
+ expect(preferenceAttempts).toEqual([{ method: 'PUT', body: { language: 'nb-NO' } }, { method: 'PUT', body: { language: 'nb-NO' } }]);
+ expect(assetReads(session, seeded.job.id)).toHaveLength(reads); expect(mutations(session)).toEqual([]);
+ await capture(page, info, { ...desktop, language: 'nb' }, 'controlled-language503-dirty-letter');
+
+ await keyboard(page, section(page, translations.nb.workspaceCoverLetter).getByRole('button', { name: translations.nb.assetsDiscardChanges, exact: true }));
+ await keyboard(page, page.getByRole('tablist', { name: translations.nb.workspaceApplicationSections, exact: true }).getByRole('tab', { name: translations.nb.workspaceAnswers, exact: true }));
+ await expect(drafts(page, 'nb').answer).toHaveValue(seeded.answer);
+ const dirtyAnswer = 'Ulagret søknadssvar under kontrollert språkfeil — æøå'; const dirtyMessage = 'Ulagret melding; ikke sendt under språkfeilen — æøå';
+ await drafts(page, 'nb').answer.fill(dirtyAnswer); await drafts(page, 'nb').message.fill(dirtyMessage); reads = assetReads(session, seeded.job.id).length;
+ const secondResponse = response(page, 'PUT', preferenceUrl);
+ await keyboard(page, page.locator('header').first().getByRole('button', { name: 'English', exact: true }));
+ expect((await secondResponse).status()).toBe(503); await expect(page.locator('html')).toHaveAttribute('lang', 'en');
+ await expect(drafts(page).answer).toHaveValue(dirtyAnswer); await expect(drafts(page).message).toHaveValue(dirtyMessage);
+ const enFeedback = page.getByRole('alert').filter({ hasText: translations.en.settingsLanguageSaveFailed }); await expect(enFeedback).toBeVisible();
+ const secondRetry = response(page, 'PUT', preferenceUrl);
+ await keyboard(page, enFeedback.getByRole('button', { name: translations.en.retry, exact: true })); expect((await secondRetry).status()).toBe(503);
+ await expect(enFeedback).toBeVisible(); await expect(drafts(page).answer).toHaveValue(dirtyAnswer); await expect(drafts(page).message).toHaveValue(dirtyMessage);
+ expect(preferenceAttempts).toEqual([{ method: 'PUT', body: { language: 'nb-NO' } }, { method: 'PUT', body: { language: 'nb-NO' } }, { method: 'PUT', body: { language: 'en' } }, { method: 'PUT', body: { language: 'en' } }]);
+ expect(assetReads(session, seeded.job.id)).toHaveLength(reads); expect(mutations(session)).toEqual([]);
+ const persistedAfter = await json<{ uiLanguage: string }>(await page.request.get(`${apiUrl}/auth/me`)); expect(persistedAfter.uiLanguage).toBe(persistedBefore.uiLanguage);
+ expect((await json(await page.request.get(`${jobUrl(seeded.job.id)}/cover-letter`))).text).toBe(seeded.originalLetter);
+ const workspace = await json(await page.request.get(`${jobUrl(seeded.job.id)}/workspace`)); expect(workspace.applicationAnswerDraft).toBe(seeded.answer); expect(workspace.recruiterMessageDraft).toBe(seeded.message);
+ await capture(page, info, desktop, 'controlled-language503-dirty-answer-drafts');
+ await info.attach('application-controlled-language503-boundary', { body: JSON.stringify({ boundary, controlledResponseStatus: 503, preferenceAttempts, persistedBefore: persistedBefore.uiLanguage, persistedAfter: persistedAfter.uiLanguage,
+ qualification: 'Page-only language PUT is deliberately fulfilled503 before server execution. Proves local presentation, retained drafts and exact Retry request without asset GET/job writes; it does not prove account preference persistence or close RD11. The separate ordinary real-server locale case keeps its strict204 assertions and observed500 failures.' }), contentType: 'application/json' });
+});
diff --git a/job-tracker-ui/src/application-assets.test.tsx b/job-tracker-ui/src/application-assets.test.tsx
index b65e8bd..ef1b3b1 100644
--- a/job-tracker-ui/src/application-assets.test.tsx
+++ b/job-tracker-ui/src/application-assets.test.tsx
@@ -1,14 +1,17 @@
import React from "react";
import "@testing-library/jest-dom";
-import { fireEvent, render as rtlRender, screen, waitFor } from "@testing-library/react";
+import { act, fireEvent, render as rtlRender, screen, waitFor, within } from "@testing-library/react";
import {
ApplicationCoverLetterSection, ApplicationCvSection, ApplicationPackageDraftsSection, SubmittedPackagesSection,
} from "./components/ApplicationAssets";
import { api } from "./api";
import { I18nProvider } from "./i18n/I18nProvider";
+import { ConfirmProvider } from "./confirm";
+import { translations, type UiLanguage } from "./i18n/translations";
-const render = (ui: React.ReactElement) => rtlRender({ui});
+const withProviders = (ui: React.ReactElement) => {ui};
+const render = (ui: React.ReactElement) => rtlRender(withProviders(ui));
jest.mock("./api", () => ({
api: {
@@ -17,7 +20,7 @@ jest.mock("./api", () => ({
post: jest.fn(),
interceptors: { request: { use: jest.fn() }, response: { use: jest.fn() } },
},
- getApiErrorMessage: (_e: any, fallback?: string) => fallback || "Request failed.",
+ getApiErrorMessage: (_e: unknown, fallback = "Request failed.") => fallback,
}));
const mockedApi = api as jest.Mocked;
@@ -69,10 +72,37 @@ function routeGet(overrides: Record = {}) {
beforeEach(() => {
window.localStorage.removeItem("uiLanguage");
+ document.documentElement.lang = "en";
jest.clearAllMocks();
+ mockedApi.get.mockReset();
+ mockedApi.put.mockReset();
+ mockedApi.post.mockReset();
});
-afterEach(() => window.localStorage.removeItem("uiLanguage"));
+afterEach(() => {
+ window.localStorage.removeItem("uiLanguage");
+ jest.restoreAllMocks();
+});
+
+function copy(key: string, params: Record = {}, language: UiLanguage = document.documentElement.lang === "nb-NO" ? "nb" : "en") {
+ const template = (translations[language] as Record)[key] ?? key;
+ return template.replace(/\{(\w+)\}/g, (_, name) => String(params[name] ?? `{${name}}`));
+}
+
+function section(key: string) {
+ return within(screen.getByRole("region", { name: copy(key) }));
+}
+
+function changeLanguage(language: UiLanguage) {
+ act(() => { window.dispatchEvent(new StorageEvent("storage", { key: "uiLanguage", newValue: language })); });
+}
+
+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 };
+}
test("document package history is read-only and separates captured notes from answers", async () => {
const summary = { id: 91, version: 1, cvVariantId: 3, cvVariantName: "Original CV", cvVariantVersion: 2, cvThemeId: "nordic", hasCv: false, hasCoverLetter: true, createdAtUtc: "2026-07-19T10:00:00Z", attachments: [] };
@@ -302,7 +332,7 @@ test("a failed load surfaces an error", async () => {
render();
- expect(await screen.findByText(/Could not load this section/i)).toBeInTheDocument();
+ expect(await section("workspaceCoverLetter").findByText(/Could not load this section/i)).toBeInTheDocument();
});
// ---------- Application package drafts ----------
@@ -355,3 +385,350 @@ test("application package drafts can be cleared without deleting ordinary notes"
{ applicationAnswerDraft: "", recruiterMessageDraft: "" },
));
});
+
+// ---------- Phase 7 retained-owner recovery ----------
+
+test("failed cover-letter saving retains editable dirty text and original Save recovery across a live locale change", async () => {
+ routeGet();
+ const text = "Retained letter draft — æøå";
+ mockedApi.put.mockRejectedValueOnce({ response: { status: 503 } })
+ .mockResolvedValueOnce({ data: { ...coverLetter, text } } as any);
+ const onDirtyChange = jest.fn();
+ render();
+ fireEvent.change(await screen.findByRole("textbox", { name: copy("jobDetailsCoverLetter") }), { target: { value: text } });
+ fireEvent.click(section("workspaceCoverLetter").getByRole("button", { name: copy("save") }));
+ expect(await section("workspaceCoverLetter").findByRole("alert")).toHaveTextContent(copy("assetsCoverSaveFailed"));
+ expect(screen.getByRole("textbox", { name: copy("jobDetailsCoverLetter") })).toHaveValue(text);
+ expect(screen.getByRole("textbox", { name: copy("jobDetailsCoverLetter") })).toBeEnabled();
+ expect(section("workspaceCoverLetter").getByRole("button", { name: copy("save") })).toBeEnabled();
+ expect(section("workspaceCoverLetter").getByRole("button", { name: copy("assetsDiscardChanges") })).toBeEnabled();
+ expect(section("workspaceCoverLetter").queryByRole("button", { name: copy("retry") })).not.toBeInTheDocument();
+ expect(onDirtyChange).toHaveBeenLastCalledWith(true);
+ const reads = mockedApi.get.mock.calls.length;
+ changeLanguage("nb");
+ expect(section("workspaceCoverLetter").getByRole("alert")).toHaveTextContent(copy("assetsCoverSaveFailed"));
+ expect(screen.getByRole("textbox", { name: copy("jobDetailsCoverLetter") })).toHaveValue(text);
+ expect(mockedApi.get).toHaveBeenCalledTimes(reads);
+ fireEvent.click(section("workspaceCoverLetter").getByRole("button", { name: copy("save") }));
+ await waitFor(() => expect(mockedApi.put).toHaveBeenCalledTimes(2));
+ const payload = { text, source: "manual", aiAction: undefined };
+ expect(mockedApi.put).toHaveBeenNthCalledWith(1, "/jobapplications/7/cover-letter", payload);
+ expect(mockedApi.put).toHaveBeenNthCalledWith(2, "/jobapplications/7/cover-letter", payload);
+ await waitFor(() => expect(onDirtyChange).toHaveBeenLastCalledWith(false));
+ expect(screen.getByRole("textbox", { name: copy("jobDetailsCoverLetter") })).toHaveValue(text);
+ expect(mockedApi.post).not.toHaveBeenCalled();
+});
+
+test("failed application-answer saving keeps both dirty drafts editable and retries only the original save payload", async () => {
+ mockedApi.put.mockRejectedValueOnce({ response: { status: 503 } }).mockResolvedValueOnce({ status: 204 } as any);
+ const onDirtyChange = jest.fn();
+ const onSaved = jest.fn();
+ render();
+ fireEvent.change(screen.getByLabelText(copy("assetsApplicationAnswer")), { target: { value: "Retained answer" } });
+ fireEvent.change(screen.getByLabelText(copy("assetsRecruiterMessage")), { target: { value: "Retained recruiter draft" } });
+ fireEvent.click(screen.getByRole("button", { name: copy("assetsSaveDrafts") }));
+ expect(await section("assetsApplicationDrafts").findByRole("alert")).toHaveTextContent(copy("assetsDraftsSaveFailed"));
+ expect(screen.getByLabelText(copy("assetsApplicationAnswer"))).toHaveValue("Retained answer");
+ expect(screen.getByLabelText(copy("assetsRecruiterMessage"))).toHaveValue("Retained recruiter draft");
+ expect(screen.getByLabelText(copy("assetsApplicationAnswer"))).toBeEnabled();
+ expect(screen.getByLabelText(copy("assetsRecruiterMessage"))).toBeEnabled();
+ expect(onDirtyChange).toHaveBeenLastCalledWith(true);
+ expect(onSaved).not.toHaveBeenCalled();
+ changeLanguage("nb");
+ expect(section("assetsApplicationDrafts").getByRole("alert")).toHaveTextContent(copy("assetsDraftsSaveFailed"));
+ expect(mockedApi.get).not.toHaveBeenCalled();
+ fireEvent.click(screen.getByRole("button", { name: copy("assetsSaveDrafts") }));
+ await waitFor(() => expect(onSaved).toHaveBeenCalledTimes(1));
+ const payload = { applicationAnswerDraft: "Retained answer", recruiterMessageDraft: "Retained recruiter draft" };
+ expect(mockedApi.put).toHaveBeenNthCalledWith(1, "/jobapplications/7/application-drafts", payload);
+ expect(mockedApi.put).toHaveBeenNthCalledWith(2, "/jobapplications/7/application-drafts", payload);
+ expect(onDirtyChange).toHaveBeenLastCalledWith(false);
+ expect(mockedApi.post).not.toHaveBeenCalled();
+});
+
+test("failed CV attachment leaves the current association and selector available for the original explicit action", async () => {
+ routeGet();
+ mockedApi.put.mockRejectedValueOnce({ response: { status: 503 } })
+ .mockResolvedValueOnce({ data: { ...cv, attachedVariantId: 5, attachedVariantName: "Generalist CV" } } as any);
+ render();
+ const select = () => screen.getByRole("combobox", { name: copy("assetsAttachedCv") });
+ fireEvent.mouseDown(await screen.findByRole("combobox", { name: /Attached CV variant/i }));
+ fireEvent.click(await screen.findByRole("option", { name: /Generalist CV/ }));
+ expect(await section("workspaceCv").findByRole("alert")).toHaveTextContent(copy("assetsAttachCvFailed"));
+ expect(select()).toHaveTextContent("Backend CV");
+ expect(select()).not.toHaveAttribute("aria-disabled", "true");
+ expect(section("workspaceCv").getByRole("link", { name: /Edit, preview and export/i })).toHaveAttribute("href", "/career/builder/3");
+ expect(section("workspaceCv").queryByRole("button", { name: copy("retry") })).not.toBeInTheDocument();
+ fireEvent.mouseDown(select());
+ fireEvent.click(await screen.findByRole("option", { name: /Generalist CV/ }));
+ await waitFor(() => expect(mockedApi.put).toHaveBeenCalledTimes(2));
+ expect(mockedApi.put).toHaveBeenNthCalledWith(1, "/jobapplications/7/cv", { variantId: 5 });
+ expect(mockedApi.put).toHaveBeenNthCalledWith(2, "/jobapplications/7/cv", { variantId: 5 });
+ await waitFor(() => expect(section("workspaceCv").getByRole("link", { name: /Edit, preview and export/i })).toHaveAttribute("href", "/career/builder/5"));
+ expect(mockedApi.post).not.toHaveBeenCalled();
+});
+
+test("dirty version restore cancels without a write and confirmed held restore appends history exactly once", async () => {
+ routeGet();
+ const write = deferred();
+ mockedApi.post.mockImplementation(() => write.promise);
+ const onDirtyChange = jest.fn();
+ render();
+ fireEvent.change(await screen.findByRole("textbox", { name: "Cover letter" }), { target: { value: "Unsaved text before restore" } });
+ fireEvent.click(screen.getByRole("button", { name: /Restore version 1/i }));
+ expect(mockedApi.post).not.toHaveBeenCalled();
+ const dialog = await screen.findByRole("dialog", { name: copy("assetsRestoreDiscardTitle") });
+ fireEvent.click(within(dialog).getByRole("button", { name: copy("cancel") }));
+ await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
+ expect(screen.getByRole("textbox", { name: "Cover letter" })).toHaveValue("Unsaved text before restore");
+ expect(onDirtyChange).toHaveBeenLastCalledWith(true);
+ expect(mockedApi.post).not.toHaveBeenCalled();
+ fireEvent.click(screen.getByRole("button", { name: /Restore version 1/i }));
+ const confirmed = await screen.findByRole("dialog", { name: copy("assetsRestoreDiscardTitle") });
+ const confirm = within(confirmed).getByRole("button", { name: copy("assetsRestoreConfirm") });
+ act(() => { fireEvent.click(confirm); fireEvent.click(confirm); });
+ await waitFor(() => expect(mockedApi.post).toHaveBeenCalledTimes(1));
+ expect(mockedApi.post).toHaveBeenCalledWith("/jobapplications/7/cover-letter/versions/1/restore");
+ await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
+ expect(screen.getByRole("button", { name: /Restore version 1/i })).toBeDisabled();
+ await act(async () => { write.resolve({ data: {
+ ...coverLetter, text: "Restored old text", currentVersion: 3,
+ versions: [{ version: 3, source: "restore", aiAction: null, length: 17, createdAtUtc: "2026-07-20T10:00:00Z", isCurrent: true }, ...coverLetter.versions.map((version) => ({ ...version, isCurrent: false }))],
+ } }); });
+ expect(await screen.findByDisplayValue("Restored old text")).toBeInTheDocument();
+ expect(screen.getByText("v3")).toBeInTheDocument();
+ expect(screen.getByText("v2")).toBeInTheDocument();
+ expect(screen.getByText("v1")).toBeInTheDocument();
+ expect(onDirtyChange).toHaveBeenLastCalledWith(false);
+ expect(mockedApi.post).toHaveBeenCalledTimes(1);
+ expect(mockedApi.put).not.toHaveBeenCalled();
+});
+
+test("held letter Save submits one snapshot and prevents an AI Apply from replacing its pending draft", async () => {
+ routeGet();
+ mockedApi.post.mockResolvedValue({ data: { result: { text: "Unaccepted AI suggestion" } } } as any);
+ const write = deferred();
+ mockedApi.put.mockImplementation(() => write.promise);
+ render();
+ fireEvent.click(await screen.findByRole("button", { name: "Generate" }));
+ const apply = await screen.findByRole("button", { name: /Apply to editor/i });
+ fireEvent.change(screen.getByRole("textbox", { name: "Cover letter" }), { target: { value: "Explicit manual save snapshot" } });
+ const save = section("workspaceCoverLetter").getByRole("button", { name: "Save" });
+ act(() => { fireEvent.click(save); fireEvent.click(save); });
+ expect(mockedApi.put).toHaveBeenCalledTimes(1);
+ expect(mockedApi.put).toHaveBeenCalledWith("/jobapplications/7/cover-letter", { text: "Explicit manual save snapshot", source: "manual", aiAction: undefined });
+ expect(apply).toBeDisabled();
+ fireEvent.click(apply);
+ expect(screen.getByRole("textbox", { name: "Cover letter" })).toHaveValue("Explicit manual save snapshot");
+ await act(async () => { write.resolve({ data: { ...coverLetter, text: "Explicit manual save snapshot" } }); });
+ expect(screen.getByRole("textbox", { name: "Cover letter" })).toHaveValue("Explicit manual save snapshot");
+ expect(mockedApi.put).toHaveBeenCalledTimes(1);
+});
+
+test("held AI generation locks its controls and preserves captured action attribution after a later action selection", async () => {
+ routeGet();
+ const generation = deferred();
+ mockedApi.post.mockImplementation(() => generation.promise);
+ render();
+ await screen.findByText(/application's full job advert and analysis/i);
+ fireEvent.mouseDown(screen.getByRole("combobox", { name: "Document language" }));
+ fireEvent.click(await screen.findByRole("option", { name: /Norsk bokmål/i }));
+ fireEvent.change(screen.getByRole("textbox", { name: "Additional instructions" }), { target: { value: "Use only recorded context" } });
+ const generate = screen.getByRole("button", { name: "Generate" });
+ act(() => { fireEvent.click(generate); fireEvent.click(generate); });
+ expect(mockedApi.post).toHaveBeenCalledTimes(1);
+ expect(mockedApi.post).toHaveBeenCalledWith("/jobapplications/7/ai/generate", expect.objectContaining({ module: "cover-letter", action: "generate", mode: "professional", targetLanguage: "nb-NO", currentText: "Dear team", extraContext: "Use only recorded context" }));
+ for (const name of ["Action", "Tone", "Document language"]) expect(screen.getByRole("combobox", { name })).toHaveAttribute("aria-disabled", "true");
+ expect(screen.getByRole("textbox", { name: "Additional instructions" })).toBeDisabled();
+ await act(async () => { generation.resolve({ data: { result: { text: "Captured generated suggestion" } } }); });
+ fireEvent.mouseDown(screen.getByRole("combobox", { name: "Action" }));
+ fireEvent.click(await screen.findByRole("option", { name: "Improve" }));
+ fireEvent.click(screen.getByRole("button", { name: /Apply to editor/i }));
+ mockedApi.put.mockResolvedValue({ data: { ...coverLetter, text: "Captured generated suggestion" } } as any);
+ fireEvent.click(section("workspaceCoverLetter").getByRole("button", { name: "Save" }));
+ await waitFor(() => expect(mockedApi.put).toHaveBeenCalledWith("/jobapplications/7/cover-letter", { text: "Captured generated suggestion", source: "ai", aiAction: "generate" }));
+ expect(mockedApi.post).toHaveBeenCalledTimes(1);
+});
+
+test("capture, detail and download failures keep owned frozen history and recover only through their original explicit actions", async () => {
+ const attachment = { id: 17, fileName: "frozen-letter.txt", fileSize: 1024, contentType: "text/plain", sha256: "fixture-hash" };
+ const summary = { id: 91, version: 1, cvVariantId: 3, cvVariantName: "Original CV", cvVariantVersion: 2, cvThemeId: "nordic", hasCv: true, hasCoverLetter: true, createdAtUtc: "2026-07-19T10:00:00Z", attachments: [attachment] };
+ const created = { ...summary, id: 92, version: 2, cvVariantName: "Backend CV", cvVariantVersion: 4, attachments: [] };
+ const detail = { summary, renderedCvHtml: "Frozen CV fixture", coverLetterText: "Frozen original letter", applicationMaterialJson: "{}" };
+ const blob = new Blob(["Frozen file fixture"], { type: "text/plain" });
+ let detailReads = 0;
+ let downloads = 0;
+ mockedApi.get.mockImplementation((url: string) => {
+ if (url === "/jobapplications/7/submitted-packages/91") return ++detailReads === 1 ? Promise.reject({ response: { status: 503 } }) : Promise.resolve({ data: detail } as any);
+ if (url === "/jobapplications/7/submitted-packages/91/attachments/17") return ++downloads === 1 ? Promise.reject({ response: { status: 503 } }) : Promise.resolve({ data: blob } as any);
+ if (url.endsWith("/submitted-packages")) return Promise.resolve({ data: [summary] } as any);
+ if (url.endsWith("/tailoring")) return Promise.resolve({ data: tailoring } as any);
+ return Promise.resolve({ data: cv } as any);
+ });
+ mockedApi.post.mockRejectedValueOnce({ response: { status: 503 } }).mockResolvedValueOnce({ data: created } as any);
+ const originalCreate = Object.getOwnPropertyDescriptor(URL, "createObjectURL");
+ const originalRevoke = Object.getOwnPropertyDescriptor(URL, "revokeObjectURL");
+ const createUrl = jest.fn(() => "blob:owned-frozen-fixture");
+ const revokeUrl = jest.fn();
+ Object.defineProperty(URL, "createObjectURL", { configurable: true, value: createUrl });
+ Object.defineProperty(URL, "revokeObjectURL", { configurable: true, value: revokeUrl });
+ const downloadedNames: string[] = [];
+ jest.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(function (this: HTMLAnchorElement) { downloadedNames.push(this.download); });
+ try {
+ render();
+ fireEvent.click(await screen.findByRole("button", { name: /Freeze current package/i }));
+ expect(await section("assetsSubmittedPackages").findByRole("alert")).toHaveTextContent(copy("assetsPackageCaptureFailed"));
+ expect(screen.getByText("Submission package v1")).toBeInTheDocument();
+ expect(section("workspaceCv").getByRole("link", { name: /Edit, preview and export/i })).toHaveAttribute("href", "/career/builder/3");
+ expect(section("assetsSubmittedPackages").queryByRole("button", { name: copy("retry") })).not.toBeInTheDocument();
+ expect(mockedApi.post).toHaveBeenCalledTimes(1);
+ fireEvent.click(section("assetsSubmittedPackages").getByRole("button", { name: /Freeze current package/i }));
+ await screen.findByText("Submission package v2");
+ const packageRow = () => screen.getByText("Submission package v1").closest(".MuiPaper-root")!;
+ fireEvent.click(within(packageRow()).getByRole("button", { name: "View preserved package" }));
+ expect(await section("assetsSubmittedPackages").findByRole("alert")).toHaveTextContent(copy("assetsPackageLoadFailed"));
+ expect(screen.getByText("Submission package v1")).toBeInTheDocument();
+ expect(screen.getByText("Submission package v2")).toBeInTheDocument();
+ expect(detailReads).toBe(1);
+ expect(mockedApi.post).toHaveBeenCalledTimes(2);
+ fireEvent.click(within(packageRow()).getByRole("button", { name: "View preserved package" }));
+ const preserved = await screen.findByRole("dialog");
+ expect(preserved).toHaveTextContent("Frozen original letter");
+ expect(detailReads).toBe(2);
+ fireEvent.click(within(preserved).getByRole("button", { name: "Close" }));
+ await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
+ fireEvent.click(within(packageRow()).getByRole("button", { name: /frozen-letter\.txt/ }));
+ expect(await section("assetsSubmittedPackages").findByRole("alert")).toHaveTextContent(copy("assetsPackageDownloadFailed"));
+ expect(screen.getByText("Original CV · v2")).toBeInTheDocument();
+ expect(section("workspaceCv").getByRole("link", { name: /Edit, preview and export/i })).toHaveAttribute("href", "/career/builder/3");
+ expect(downloads).toBe(1);
+ fireEvent.click(within(packageRow()).getByRole("button", { name: /frozen-letter\.txt/ }));
+ await waitFor(() => expect(createUrl).toHaveBeenCalledWith(blob));
+ expect(downloads).toBe(2);
+ expect(downloadedNames).toEqual(["frozen-letter.txt"]);
+ expect(revokeUrl).toHaveBeenCalledWith("blob:owned-frozen-fixture");
+ expect(mockedApi.post.mock.calls).toEqual([["/jobapplications/7/submitted-packages"], ["/jobapplications/7/submitted-packages"]]);
+ expect(mockedApi.put).not.toHaveBeenCalled();
+ } finally {
+ if (originalCreate) Object.defineProperty(URL, "createObjectURL", originalCreate); else Reflect.deleteProperty(URL, "createObjectURL");
+ if (originalRevoke) Object.defineProperty(URL, "revokeObjectURL", originalRevoke); else Reflect.deleteProperty(URL, "revokeObjectURL");
+ }
+});
+
+test("all five asset read owners expose GET-only recovery without false empty history or locale refetch", async () => {
+ mockedApi.get.mockRejectedValue({ response: { status: 503 } });
+ render(<>>);
+ const owners = ["workspaceCv", "assetsTailoring", "assetsSubmittedPackages", "workspaceCoverLetter", "coverAiTitle"];
+ await waitFor(() => expect(mockedApi.get).toHaveBeenCalledTimes(5));
+ for (const key of owners) expect(await section(key).findByRole("alert")).toHaveTextContent(copy("assetsLoadFailed"));
+ expect(screen.queryByText(copy("assetsNoVersions"))).not.toBeInTheDocument();
+ expect(screen.queryByText(copy("assetsNoSubmittedPackages"))).not.toBeInTheDocument();
+ expect(screen.queryByText(copy("coverAiSelectCv"))).not.toBeInTheDocument();
+ changeLanguage("nb");
+ for (const key of owners) expect(section(key).getByRole("alert")).toHaveTextContent(copy("assetsLoadFailed"));
+ expect(mockedApi.get).toHaveBeenCalledTimes(5);
+ routeGet();
+ const endpoints = ["cv", "tailoring", "submitted-packages", "cover-letter", "cv"];
+ for (let index = 0; index < owners.length; index++) {
+ fireEvent.click(section(owners[index]).getByRole("button", { name: copy("retry") }));
+ await waitFor(() => expect(mockedApi.get).toHaveBeenCalledTimes(6 + index));
+ expect(mockedApi.get).toHaveBeenNthCalledWith(6 + index, `/jobapplications/7/${endpoints[index]}`);
+ await waitFor(() => expect(section(owners[index]).queryByRole("button", { name: copy("retry") })).not.toBeInTheDocument());
+ }
+ expect(screen.getByRole("textbox", { name: copy("jobDetailsCoverLetter") })).toHaveValue("Dear team");
+ expect(screen.getByText("v2")).toBeInTheDocument();
+ expect(mockedApi.put).not.toHaveBeenCalled();
+ expect(mockedApi.post).not.toHaveBeenCalled();
+});
+
+test("keyed job changes stop a late CV copy from attaching and never reuse another job's saved-copy recovery", async () => {
+ const jobBCv = {
+ ...cv,
+ attachedVariantId: 9,
+ attachedVariantName: "Job B CV",
+ attachedVersion: 6,
+ availableVariants: [{ ...cv.availableVariants[0], id: 9, name: "Job B CV", version: 6, jobApplicationId: 8 }],
+ };
+ mockedApi.get.mockImplementation((url: string) => Promise.resolve({
+ data: url.endsWith("/tailoring") ? tailoring : url === "/jobapplications/8/cv" ? jobBCv : cv,
+ } as any));
+ const oldCopy = deferred();
+ mockedApi.post.mockReturnValueOnce(oldCopy.promise);
+ const mounted = render();
+ const initialLocation = window.location.href;
+ fireEvent.click(await screen.findByRole("button", { name: copy("assetsDuplicateForJob") }));
+ expect(mockedApi.post).toHaveBeenCalledWith("/cv/variants/3/duplicate", {
+ name: copy("assetsTailoredCopyName", { name: "Backend CV" }),
+ });
+
+ mounted.rerender(withProviders());
+ await waitFor(() => expect(screen.getByRole("combobox", { name: copy("assetsAttachedCv") })).toHaveTextContent("Job B CV"));
+ await act(async () => { oldCopy.resolve({ data: { id: 83 } }); await oldCopy.promise; });
+ expect(mockedApi.put).not.toHaveBeenCalled();
+ expect(window.location.href).toBe(initialLocation);
+ expect(section("workspaceCv").getByRole("link", { name: /Edit, preview and export/i })).toHaveAttribute("href", "/career/builder/9");
+ expect(screen.getByRole("button", { name: copy("assetsDuplicateForJob") })).toBeEnabled();
+ expect(screen.queryByRole("button", { name: copy("assetsAttachCreatedCopy") })).not.toBeInTheDocument();
+
+ mounted.rerender(withProviders());
+ mockedApi.post.mockResolvedValueOnce({ data: { id: 84 } } as any);
+ mockedApi.put.mockRejectedValueOnce({ response: { status: 503 } });
+ fireEvent.click(await screen.findByRole("button", { name: copy("assetsDuplicateForJob") }));
+ await waitFor(() => expect(screen.getByRole("button", { name: copy("assetsAttachCreatedCopy") })).toBeEnabled());
+ expect(await section("workspaceCv").findByText(copy("assetsAttachCvFailed"))).toBeInTheDocument();
+ expect(mockedApi.put).toHaveBeenCalledTimes(1);
+ expect(mockedApi.put).toHaveBeenCalledWith("/jobapplications/7/cv", { variantId: 84 });
+
+ mounted.rerender(withProviders());
+ await waitFor(() => expect(screen.getByRole("combobox", { name: copy("assetsAttachedCv") })).toHaveTextContent("Job B CV"));
+ expect(screen.queryByRole("button", { name: copy("assetsAttachCreatedCopy") })).not.toBeInTheDocument();
+ expect(screen.queryByText(copy("assetsCopyCreatedHelp"))).not.toBeInTheDocument();
+ const jobBCopy = deferred();
+ mockedApi.post.mockReturnValueOnce(jobBCopy.promise);
+ fireEvent.click(screen.getByRole("button", { name: copy("assetsDuplicateForJob") }));
+ expect(mockedApi.post).toHaveBeenCalledTimes(3);
+ expect(mockedApi.post).toHaveBeenLastCalledWith("/cv/variants/9/duplicate", {
+ name: copy("assetsTailoredCopyName", { name: "Job B CV" }),
+ });
+ expect(mockedApi.put).toHaveBeenCalledTimes(1);
+ expect(section("workspaceCv").getByRole("link", { name: /Edit, preview and export/i })).toHaveAttribute("href", "/career/builder/9");
+ expect(window.location.href).toBe(initialLocation);
+ mounted.unmount();
+});
+
+test("keyed job changes keep job B's answer drafts and callbacks independent of job A's late save", async () => {
+ const saveA = deferred();
+ mockedApi.put.mockReturnValueOnce(saveA.promise);
+ const onSavedA = jest.fn();
+ const onSavedB = jest.fn();
+ const onDirtyB = jest.fn();
+ const mounted = render();
+ fireEvent.change(screen.getByRole("textbox", { name: copy("assetsApplicationAnswer") }), { target: { value: "Pending A answer" } });
+ fireEvent.change(screen.getByRole("textbox", { name: copy("assetsRecruiterMessage") }), { target: { value: "Pending A message" } });
+ fireEvent.click(screen.getByRole("button", { name: copy("assetsSaveDrafts") }));
+ expect(mockedApi.put).toHaveBeenCalledWith("/jobapplications/7/application-drafts", {
+ applicationAnswerDraft: "Pending A answer", recruiterMessageDraft: "Pending A message",
+ });
+
+ mounted.rerender(withProviders());
+ fireEvent.change(screen.getByRole("textbox", { name: copy("assetsApplicationAnswer") }), { target: { value: "Unsaved B answer" } });
+ fireEvent.change(screen.getByRole("textbox", { name: copy("assetsRecruiterMessage") }), { target: { value: "Unsaved B message" } });
+ await act(async () => { saveA.resolve({ data: undefined }); await saveA.promise; });
+ expect(screen.getByRole("textbox", { name: copy("assetsApplicationAnswer") })).toHaveValue("Unsaved B answer");
+ expect(screen.getByRole("textbox", { name: copy("assetsRecruiterMessage") })).toHaveValue("Unsaved B message");
+ expect(screen.getByRole("button", { name: copy("assetsSaveDrafts") })).toBeEnabled();
+ expect(onDirtyB).toHaveBeenLastCalledWith(true);
+ expect(onSavedA).not.toHaveBeenCalled();
+ expect(onSavedB).not.toHaveBeenCalled();
+ expect(mockedApi.put).toHaveBeenCalledTimes(1);
+
+ mockedApi.put.mockResolvedValueOnce({ data: undefined } as any);
+ fireEvent.click(screen.getByRole("button", { name: copy("assetsSaveDrafts") }));
+ await waitFor(() => expect(onSavedB).toHaveBeenCalledTimes(1));
+ expect(mockedApi.put).toHaveBeenLastCalledWith("/jobapplications/8/application-drafts", {
+ applicationAnswerDraft: "Unsaved B answer", recruiterMessageDraft: "Unsaved B message",
+ });
+ expect(onSavedA).not.toHaveBeenCalled();
+});
diff --git a/job-tracker-ui/src/application-workspace-overlay.test.tsx b/job-tracker-ui/src/application-workspace-overlay.test.tsx
index 04097fb..e9d2d9a 100644
--- a/job-tracker-ui/src/application-workspace-overlay.test.tsx
+++ b/job-tracker-ui/src/application-workspace-overlay.test.tsx
@@ -1,6 +1,6 @@
import React from "react";
import "@testing-library/jest-dom";
-import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
+import { act, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { createMemoryRouter, RouterProvider, Route, Routes, useLocation } from "react-router-dom";
import { api } from "./api";
@@ -27,7 +27,7 @@ jest.mock("./components/ApplicationAssets", () => ({
),
ApplicationCvSection: () =>
CV section
,
- ApplicationPackageDraftsSection: ({ onDirtyChange }: { onDirtyChange?: (dirty: boolean) => void }) =>
Application drafts section
,
+ ApplicationPackageDraftsSection: ({ jobId, initialApplicationAnswer, initialRecruiterMessage, onDirtyChange }: { jobId: number; initialApplicationAnswer: string; initialRecruiterMessage: string; onDirtyChange?: (dirty: boolean) => void }) =>
Application drafts section
,
SubmittedPackagesSection: () =>
Frozen packages section
,
}));
jest.mock("./components/InterviewPrep", () => ({ ApplicationInterviewPrep: () =>
Interview section
}));
@@ -136,7 +136,7 @@ function renderTable(path = "/jobs") {
},
], { initialEntries: [path] });
- return render(
+ const mounted = render(
@@ -147,6 +147,7 @@ function renderTable(path = "/jobs") {
,
);
+ return { ...mounted, router };
}
beforeEach(() => {
@@ -383,3 +384,30 @@ test("legacy correspondence and notes reveal distinct domain content", async ()
expect(screen.queryByText("Application drafts section")).not.toBeInTheDocument();
expect(screen.getByTestId("location")).toHaveTextContent("followMode=waiting-update");
});
+
+test("holds answers until their aggregate matches the route and ignores a late previous-job aggregate", async () => {
+ let resolveB!: (value: any) => void;
+ let resolveC!: (value: any) => void;
+ const heldB = new Promise
((resolve) => { resolveB = resolve; });
+ const heldC = new Promise((resolve) => { resolveC = resolve; });
+ const originalGet = mockedApi.get.getMockImplementation()!;
+ mockedApi.get.mockImplementation((url: string, ...rest: any[]) => {
+ if (url === "/jobapplications/43/workspace") return heldB;
+ if (url === "/jobapplications/44/workspace") return heldC;
+ return originalGet(url, ...rest);
+ });
+ const { router } = renderTable("/jobs/42?section=application&sub=answers");
+ expect(await screen.findByTestId("answer-props")).toHaveTextContent("42|Saved answer|Saved recruiter message");
+ await act(async () => { await router.navigate("/jobs/43?section=application&sub=answers"); });
+ await waitFor(() => expect(mockedApi.get).toHaveBeenCalledWith("/jobapplications/43/workspace"));
+ expect(screen.queryByTestId("answer-props")).not.toBeInTheDocument();
+ await act(async () => { await router.navigate("/jobs/44?section=application&sub=answers"); });
+ expect(screen.queryByTestId("answer-props")).not.toBeInTheDocument();
+ await act(async () => { resolveC({ data: { ...overview, id: 44, jobTitle: "Job C", applicationAnswerDraft: "C answer", recruiterMessageDraft: "C message" } }); await heldC; });
+ expect(await screen.findByTestId("answer-props")).toHaveTextContent("44|C answer|C message");
+ await act(async () => { resolveB({ data: { ...overview, id: 43, jobTitle: "Late Job B", applicationAnswerDraft: "B answer", recruiterMessageDraft: "B message" } }); await heldB; });
+ expect(screen.getByTestId("answer-props")).toHaveTextContent("44|C answer|C message");
+ expect(screen.getByRole("heading", { name: "Job C" })).toBeInTheDocument();
+ expect(mockedApi.post).not.toHaveBeenCalled();
+ expect(mockedApi.put).not.toHaveBeenCalled();
+});
diff --git a/job-tracker-ui/src/components/ApplicationAssets.tsx b/job-tracker-ui/src/components/ApplicationAssets.tsx
index e4e8c67..afa0103 100644
--- a/job-tracker-ui/src/components/ApplicationAssets.tsx
+++ b/job-tracker-ui/src/components/ApplicationAssets.tsx
@@ -19,6 +19,9 @@ import { cvBuilderApi } from "../cvBuilder";
import { aiWorkspaceApi } from "../aiWorkspace";
import { useAccountPlan } from "../accountPlan";
import { useI18n } from "../i18n/I18nProvider";
+import type { TranslationKey } from "../i18n/translations";
+import type { ViewResourceError } from "../hooks/useViewResource";
+import { useConfirm } from "../confirm";
import { PageSection } from "../layout/PageLayout";
import ViewStateNotice from "./ViewStateNotice";
import { extractApplicationAnswerDraft, removeApplicationAnswerDraft } from "../applicationDrafts";
@@ -30,52 +33,75 @@ import { extractApplicationAnswerDraft, removeApplicationAnswerDraft } from "../
// application uses and links out. The cover letter is the one thing genuinely owned here, because it
// is application-specific by nature. docs/architecture/overview.md.
+type AssetError = ViewResourceError & { messageKey: TranslationKey; retry?: () => Promise };
+
+function assetError(error: unknown, messageKey: TranslationKey, retryable = false): AssetError {
+ const status = (error as { response?: { status?: number } })?.response?.status;
+ const unauthorized = status === 401 || status === 403;
+ return { kind: unauthorized ? "unauthorized" : "error", message: getApiErrorMessage(error, ""), messageKey, retryable: retryable && !unauthorized, status };
+}
+
function useAsset(load: () => Promise, deps: React.DependencyList) {
- const { t } = useI18n();
const [data, setData] = useState(null);
- const [error, setError] = useState(null);
+ const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
+ const request = useRef(0);
+ const pending = useRef(false);
+ const active = useRef(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
const run = useCallback(load, deps);
- const reload = useCallback(() => {
- let cancelled = false;
+ const reload = useCallback(async () => {
+ if (pending.current) return;
+ pending.current = true;
+ const current = ++request.current;
setLoading(true);
- run()
- .then((r) => {
- if (!cancelled) {
- setData(r);
- setError(null);
- }
- })
- .catch((err) => {
- if (!cancelled) setError(getApiErrorMessage(err, t("assetsLoadFailed")));
- })
- .finally(() => {
- if (!cancelled) setLoading(false);
- });
- return () => {
- cancelled = true;
- };
- }, [run, t]);
+ try {
+ const next = await run();
+ if (current !== request.current) return;
+ setData(next);
+ setError(null);
+ } catch (err) {
+ if (current === request.current) setError(assetError(err, "assetsLoadFailed", true));
+ } finally {
+ if (current === request.current) { pending.current = false; setLoading(false); }
+ }
+ }, [run]);
- useEffect(() => reload(), [reload]);
+ useEffect(() => {
+ active.current = true;
+ setData(null);
+ setError(null);
+ pending.current = false;
+ void reload();
+ return () => { active.current = false; request.current += 1; pending.current = false; };
+ }, [reload]);
- return { data, error, loading, setData, setError, reload };
+ return { data, error, loading, setData, setError, reload, isCurrent: () => active.current };
}
-function Shell({ title, subtitle, loading, error, children }: {
+function Shell({ title, subtitle, loading, error, available = true, onRetry, contentRef, children }: {
title: string;
subtitle?: string;
loading: boolean;
- error: string | null;
+ error: AssetError | null;
+ available?: boolean;
+ onRetry?: () => void | Promise;
+ contentRef?: React.RefObject;
children: React.ReactNode;
}) {
+ const { t } = useI18n();
+ const localRef = useRef(null);
+ const bodyRef = contentRef ?? localRef;
+ const retry = error?.retry ?? onRetry;
return (
-
- {!loading && !error && children}
+
+ { bodyRef.current?.focus(); return retry(); } : undefined} compact />
+ {available && error?.kind !== "unauthorized" && children}
+
);
}
@@ -84,20 +110,29 @@ function Shell({ title, subtitle, loading, error, children }: {
export function ApplicationCvSection({ jobId, includePackages = true }: { jobId: number; includePackages?: boolean }) {
const { t } = useI18n();
- const { data, error, loading, setData, setError } = useAsset(
+ const { data, error, loading, setData, setError, reload, isCurrent } = useAsset(
() => applicationAssetsApi.cv(jobId),
[jobId],
);
const [busy, setBusy] = useState(false);
+ const busyRef = useRef(false);
+ const contentRef = useRef(null);
+ const [createdCopy, setCreatedCopy] = useState(null);
const attach = async (variantId: number | null) => {
+ if (busyRef.current || loading || !data) return;
+ busyRef.current = true;
+ contentRef.current?.focus();
setBusy(true);
try {
- setData(await applicationAssetsApi.attachVariant(jobId, variantId));
+ const next = await applicationAssetsApi.attachVariant(jobId, variantId);
+ if (!isCurrent()) return;
+ setData(next);
setError(null);
} catch (err) {
- setError(getApiErrorMessage(err, t("assetsAttachCvFailed")));
+ if (isCurrent()) setError(assetError(err, "assetsAttachCvFailed"));
} finally {
+ busyRef.current = false;
setBusy(false);
}
};
@@ -105,15 +140,24 @@ export function ApplicationCvSection({ jobId, includePackages = true }: { jobId:
const attached = data?.attachedVariantId ?? "";
const duplicateForJob = async () => {
- if (!data?.attachedVariantId) return;
+ if (busyRef.current || loading || !data?.attachedVariantId) return;
+ busyRef.current = true;
+ contentRef.current?.focus();
setBusy(true);
+ let copyId = createdCopy;
try {
- const copy = await cvBuilderApi.duplicate(data.attachedVariantId, t("assetsTailoredCopyName", { name: data.attachedVariantName || "CV" }));
- setData(await applicationAssetsApi.attachVariant(jobId, copy.id));
- window.location.assign(`/career/builder/${copy.id}`);
+ copyId ??= (await cvBuilderApi.duplicate(data.attachedVariantId, t("assetsTailoredCopyName", { name: data.attachedVariantName || "CV" }))).id;
+ if (!isCurrent()) return;
+ setCreatedCopy(copyId);
+ const next = await applicationAssetsApi.attachVariant(jobId, copyId);
+ if (!isCurrent()) return;
+ setData(next);
+ setCreatedCopy(null);
+ window.location.assign(`/career/builder/${copyId}`);
} catch (err) {
- setError(getApiErrorMessage(err, t("assetsDuplicateCvFailed")));
+ if (isCurrent()) setError(assetError(err, copyId ? "assetsAttachCvFailed" : "assetsDuplicateCvFailed"));
} finally {
+ busyRef.current = false;
setBusy(false);
}
};
@@ -123,8 +167,11 @@ export function ApplicationCvSection({ jobId, includePackages = true }: { jobId:
{(data?.availableVariants.length ?? 0) === 0 ? (
@@ -138,7 +185,7 @@ export function ApplicationCvSection({ jobId, includePackages = true }: { jobId:
fullWidth
label={t("assetsAttachedCv")}
value={attached}
- disabled={busy}
+ disabled={busy || loading}
onChange={(e) => attach(e.target.value === "" ? null : Number(e.target.value))}
helperText={t("assetsAttachedCvHelp")}
>
@@ -172,8 +219,8 @@ export function ApplicationCvSection({ jobId, includePackages = true }: { jobId:
>
{t("assetsEditPreviewExport")}
-
@@ -189,6 +236,7 @@ export function ApplicationCvSection({ jobId, includePackages = true }: { jobId:
{t("assetsLegacyCvText")}
)}
+ {createdCopy && {t("assetsCopyCreatedHelp")}}
@@ -202,49 +250,73 @@ export function SubmittedPackagesSection({ jobId, allowCapture = true }: { jobId
const titleId = useId();
const viewTrigger = useRef(null);
const { language, t } = useI18n();
- const { data, error, loading, setData, setError } = useAsset(
+ const { data, error, loading, setData, setError, reload, isCurrent } = useAsset(
() => applicationAssetsApi.submittedPackages(jobId),
[jobId],
);
const [busy, setBusy] = useState(false);
const [detail, setDetail] = useState(null);
const [detailBusy, setDetailBusy] = useState(false);
+ const [downloadBusy, setDownloadBusy] = useState(false);
+ const operation = useRef(false);
+ const contentRef = useRef(null);
+ const pending = busy || detailBusy || downloadBusy || loading;
const capture = async () => {
+ if (operation.current || loading || !data) return;
+ operation.current = true;
+ contentRef.current?.focus();
setBusy(true);
try {
const created = await applicationAssetsApi.captureSubmittedPackage(jobId);
+ if (!isCurrent()) return;
setData([created, ...(data ?? [])]);
setError(null);
} catch (err) {
- setError(getApiErrorMessage(err, t("assetsPackageCaptureFailed")));
+ if (isCurrent()) setError(assetError(err, "assetsPackageCaptureFailed"));
} finally {
+ operation.current = false;
setBusy(false);
}
};
const download = async (item: SubmittedPackage, attachment: SubmittedPackage["attachments"][number]) => {
+ if (operation.current || loading || !data) return;
+ operation.current = true;
+ contentRef.current?.focus();
+ setDownloadBusy(true);
try {
const blob = await applicationAssetsApi.downloadSubmittedAttachment(jobId, item.id, attachment.id);
+ if (!isCurrent()) return;
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = attachment.fileName;
anchor.click();
URL.revokeObjectURL(url);
+ setError(null);
} catch (err) {
- setError(getApiErrorMessage(err, t("assetsPackageDownloadFailed")));
+ if (isCurrent()) setError({ ...assetError(err, "assetsPackageDownloadFailed", true), retry: () => download(item, attachment) });
+ } finally {
+ operation.current = false;
+ setDownloadBusy(false);
}
};
const view = async (item: SubmittedPackage) => {
+ if (operation.current || loading || !data) return;
+ operation.current = true;
+ contentRef.current?.focus();
setDetailBusy(true);
try {
- setDetail(await applicationAssetsApi.submittedPackage(jobId, item.id));
+ const next = await applicationAssetsApi.submittedPackage(jobId, item.id);
+ if (!isCurrent()) return;
+ setDetail(next);
setError(null);
} catch (err) {
- setError(getApiErrorMessage(err, t("assetsPackageLoadFailed")));
+ if (isCurrent()) setError({ ...assetError(err, "assetsPackageLoadFailed", true), retry: () => view(item) });
} finally {
+ operation.current = false;
setDetailBusy(false);
}
};
@@ -255,9 +327,9 @@ export function SubmittedPackagesSection({ jobId, allowCapture = true }: { jobId
})();
return (
-
+
- {allowCapture && } disabled={busy} onClick={() => void capture()} sx={{ alignSelf: "flex-start" }}>
+ {allowCapture && } disabled={pending} onClick={() => void capture()} sx={{ alignSelf: "flex-start" }}>
{busy ? t("assetsPackageCapturing") : t("assetsPackageCapture")}
}
{(data?.length ?? 0) === 0 ? (
@@ -277,11 +349,11 @@ export function SubmittedPackagesSection({ jobId, allowCapture = true }: { jobId
- { viewTrigger.current = event.currentTarget; void view(item); }} sx={{ alignSelf: "flex-start" }}>
+ { viewTrigger.current = event.currentTarget; void view(item); }} sx={{ alignSelf: "flex-start" }}>
{t("assetsPackageView")}
{item.attachments.map((attachment) => (
- } onClick={() => void download(item, attachment)} sx={{ alignSelf: "flex-start", maxWidth: "100%", overflowWrap: "anywhere", textAlign: "left" }}>
+ } onClick={() => void download(item, attachment)} sx={{ alignSelf: "flex-start", maxWidth: "100%", overflowWrap: "anywhere", textAlign: "left" }}>
{attachment.fileName} · {(attachment.fileSize / 1024).toFixed(1)} KB
))}
@@ -293,25 +365,25 @@ export function SubmittedPackagesSection({ jobId, allowCapture = true }: { jobId
{detail ? t("assetsSubmittedPackageVersion", { version: detail.summary.version }) : t("assetsSubmittedPackages")}
-
+
{t("workspaceFrozenHelp")}
{detail?.renderedCvHtml ? (
- {t("workspaceCv")}
+ {t("workspaceCv")}
{detail.summary.cvVariantName} · {t("assetsThemeVersion", { theme: detail.summary.cvThemeId ?? "—", version: detail.summary.cvVariantVersion ?? "—" })}
) : {t("assetsPackageNoCv")}}
- {t("workspaceCoverLetter")}
+ {t("workspaceCoverLetter")}
{detail?.coverLetterText || t("assetsPackageNoCoverLetter")}
{material && ["TailoredCvText", "RecruiterMessageDraft"].map((key) => typeof material[key] === "string" && material[key] ? (
- {t(`assetsPackage${key}` as "assetsPackageTailoredCvText")}
+ {t(`assetsPackage${key}` as "assetsPackageTailoredCvText")}
{String(material[key])}
) : null)}
@@ -319,7 +391,7 @@ export function SubmittedPackagesSection({ jobId, allowCapture = true }: { jobId
[t("workspaceNotes"), removeApplicationAnswerDraft(material.Notes)],
[t("assetsApplicationAnswer"), extractApplicationAnswerDraft(material.Notes)],
].map(([label, content]) => content ?
- {label}
+ {label}
{content}
: null)}
@@ -334,7 +406,7 @@ export function SubmittedPackagesSection({ jobId, allowCapture = true }: { jobId
export function ApplicationTailoringSection({ jobId }: { jobId: number }) {
const { t } = useI18n();
- const { data, error, loading } = useAsset(
+ const { data, error, loading, reload } = useAsset(
() => applicationAssetsApi.tailoring(jobId),
[jobId],
);
@@ -345,6 +417,8 @@ export function ApplicationTailoringSection({ jobId }: { jobId: number }) {
subtitle={t("assetsTailoringSubtitle")}
loading={loading}
error={error}
+ available={data !== null}
+ onRetry={reload}
>
{data && !data.hasCareerProfile && (
@@ -413,13 +487,17 @@ Med vennlig hilsen
export function ApplicationCoverLetterSection({ jobId, onDirtyChange }: { jobId: number; onDirtyChange?: (dirty: boolean) => void }) {
const { language, t } = useI18n();
- const { data, error, loading, setData, setError } = useAsset(
+ const { confirm } = useConfirm();
+ const { data, error, loading, setData, setError, reload, isCurrent } = useAsset(
() => applicationAssetsApi.coverLetter(jobId),
[jobId],
);
const [draft, setDraft] = useState(null);
const [draftAiAction, setDraftAiAction] = useState(null);
const [busy, setBusy] = useState(false);
+ const [confirming, setConfirming] = useState(false);
+ const busyRef = useRef(false);
+ const contentRef = useRef(null);
// The textarea is only seeded from the server until the user starts typing, so a reload never
// clobbers unsaved edits.
@@ -432,30 +510,53 @@ export function ApplicationCoverLetterSection({ jobId, onDirtyChange }: { jobId:
}, [dirty, onDirtyChange]);
const save = async (value: string, source = "manual", aiAction?: string) => {
+ if (busyRef.current || loading || !data) return;
+ busyRef.current = true;
+ contentRef.current?.focus();
setBusy(true);
try {
- setData(await applicationAssetsApi.saveCoverLetter(jobId, value, source, aiAction));
+ const next = await applicationAssetsApi.saveCoverLetter(jobId, value, source, aiAction);
+ if (!isCurrent()) return;
+ setData(next);
setDraft(null);
setDraftAiAction(null);
setError(null);
} catch (err) {
- setError(getApiErrorMessage(err, t("assetsCoverSaveFailed")));
+ if (isCurrent()) setError(assetError(err, "assetsCoverSaveFailed"));
} finally {
+ busyRef.current = false;
setBusy(false);
}
};
const restore = async (version: number) => {
- setBusy(true);
+ if (busyRef.current || loading || !data) return;
+ busyRef.current = true;
try {
- setData(await applicationAssetsApi.restoreCoverLetter(jobId, version));
+ if (dirty) {
+ setConfirming(true);
+ const accepted = await confirm({
+ title: t("assetsRestoreDiscardTitle"), message: t("assetsRestoreDiscardMessage"),
+ confirmLabel: t("assetsRestoreConfirm"), cancelLabel: t("cancel"), destructive: true,
+ });
+ setConfirming(false);
+ if (!accepted) return;
+ }
+ if (!isCurrent()) return;
+ contentRef.current?.focus();
+ setBusy(true);
+ const next = await applicationAssetsApi.restoreCoverLetter(jobId, version);
+ if (!isCurrent()) return;
+ setData(next);
setDraft(null);
setDraftAiAction(null);
setError(null);
} catch (err) {
- setError(getApiErrorMessage(err, t("assetsCoverRestoreFailed")));
+ if (isCurrent()) setError(assetError(err, "assetsCoverRestoreFailed"));
} finally {
+ busyRef.current = false;
setBusy(false);
+ setConfirming(false);
}
};
@@ -464,28 +565,31 @@ export function ApplicationCoverLetterSection({ jobId, onDirtyChange }: { jobId:
- save(text, draftAiAction ? "ai" : "manual", draftAiAction ?? undefined)}>
+ save(text, draftAiAction ? "ai" : "manual", draftAiAction ?? undefined)}>
{t("save")}
- { setDraft(null); setDraftAiAction(null); }}>
+ { setDraft(null); setDraftAiAction(null); }}>
{t("assetsDiscardChanges")}
0}
+ disabled={busy || confirming || loading || text.trim().length > 0}
onClick={() => setDraft(COVER_LETTER_TEMPLATES[language])}
>
{t("assetsStartTemplate")}
@@ -500,10 +604,11 @@ export function ApplicationCoverLetterSection({ jobId, onDirtyChange }: { jobId:
{ setDraft(value); setDraftAiAction(aiAction); }}
+ disabled={busy || confirming || loading || !data}
+ onApply={(value, aiAction) => { if (!busyRef.current && !loading && data) { setDraft(value); setDraftAiAction(aiAction); } }}
/>
-
+
{(data?.versions.length ?? 0) === 0 ? (
{t("assetsNoVersions")}
@@ -529,11 +634,11 @@ export function ApplicationCoverLetterSection({ jobId, onDirtyChange }: { jobId:
{!v.isCurrent && (
-
+
restore(v.version)}
>
@@ -563,20 +668,25 @@ const COVER_LETTER_ACTIONS = [
{ key: "tailor", labelKey: "coverActionTailor" },
] as const;
-function CoverLetterAiAssistant({ jobId, currentText, onApply }: { jobId: number; currentText: string; onApply: (text: string, action: string) => void }) {
+function CoverLetterAiAssistant({ jobId, currentText, disabled, onApply }: { jobId: number; currentText: string; disabled: boolean; onApply: (text: string, action: string) => void }) {
const labelId = useId();
const { canUseAi } = useAccountPlan();
const { language: uiLanguage, t } = useI18n();
- const { data: cv, loading: loadingCv } = useAsset(() => applicationAssetsApi.cv(jobId), [jobId]);
+ const { data: cv, error: cvError, loading: loadingCv, reload, isCurrent } = useAsset(() => applicationAssetsApi.cv(jobId), [jobId]);
const [action, setAction] = useState("generate");
const [mode, setMode] = useState("professional");
const [language, setLanguage] = useState<"en" | "nb-NO">(() => uiLanguage === "nb" ? "nb-NO" : "en");
const [instructions, setInstructions] = useState("");
- const [suggestion, setSuggestion] = useState("");
+ const [suggestion, setSuggestion] = useState<{ text: string; action: string } | null>(null);
const [busy, setBusy] = useState(false);
- const [error, setError] = useState(null);
+ const [error, setError] = useState(null);
+ const busyRef = useRef(false);
+ const contentRef = useRef(null);
const generate = async () => {
+ if (busyRef.current || disabled || loadingCv || !canUseAi || !cv?.attachedVariantId) return;
+ busyRef.current = true;
+ contentRef.current?.focus();
setBusy(true);
setError(null);
try {
@@ -588,10 +698,13 @@ function CoverLetterAiAssistant({ jobId, currentText, onApply }: { jobId: number
currentText: currentText.trim() || undefined,
extraContext: instructions.trim() || undefined,
});
- setSuggestion(result.result.text?.trim() ?? "");
+ if (!isCurrent()) return;
+ const text = result.result.text?.trim() ?? "";
+ setSuggestion(text ? { text, action } : null);
} catch (err) {
- setError(getApiErrorMessage(err, t("coverAiGenerateFailed")));
+ if (isCurrent()) setError(assetError(err, "coverAiGenerateFailed"));
} finally {
+ busyRef.current = false;
setBusy(false);
}
};
@@ -601,8 +714,11 @@ function CoverLetterAiAssistant({ jobId, currentText, onApply }: { jobId: number
{!hasCv ? (
@@ -615,13 +731,13 @@ function CoverLetterAiAssistant({ jobId, currentText, onApply }: { jobId: number
{t("coverAiAction")}
-
{t("coverAiTone")}
- setMode(event.target.value)}>
+ setMode(event.target.value)}>
{[
["professional", "coverToneProfessional"], ["friendly", "coverToneFriendly"], ["short", "coverToneShort"],
["detailed", "coverToneDetailed"], ["modern", "coverToneModern"], ["traditional", "coverToneTraditional"],
@@ -630,7 +746,7 @@ function CoverLetterAiAssistant({ jobId, currentText, onApply }: { jobId: number
{t("coverAiDocumentLanguage")}
- setLanguage(event.target.value as "en" | "nb-NO")}>
+ setLanguage(event.target.value as "en" | "nb-NO")}>
@@ -640,16 +756,17 @@ function CoverLetterAiAssistant({ jobId, currentText, onApply }: { jobId: number
label={t("coverAiAdditionalInstructions")}
placeholder={t("coverAiInstructionsPlaceholder")}
value={instructions}
+ disabled={busy || disabled || loadingCv}
onChange={(event) => setInstructions(event.target.value)}
multiline
minRows={2}
fullWidth
/>
- {error && void generate()}>{t("retry")}}>{error}}
+ {error && void generate()}>{t("retry")}}>{error.message || t(error.messageKey)}}
}
- disabled={!canUseAi || !hasCv || busy}
+ disabled={!canUseAi || !hasCv || busy || disabled || loadingCv}
onClick={() => void generate()}
sx={{ alignSelf: "flex-start" }}
>
@@ -666,10 +783,10 @@ function CoverLetterAiAssistant({ jobId, currentText, onApply }: { jobId: number
{t("coverAiSuggestion")}
- {suggestion}
+ {suggestion.text}
- { onApply(suggestion, action); setSuggestion(""); }}>{t("coverAiApply")}
- setSuggestion("")}>{t("coverAiReject")}
+ { if (!busyRef.current && !disabled && !loadingCv) { onApply(suggestion.text, suggestion.action); setSuggestion(null); } }}>{t("coverAiApply")}
+ setSuggestion(null)}>{t("coverAiReject")}
@@ -701,7 +818,11 @@ export function ApplicationPackageDraftsSection({
const [saved, setSaved] = useState(initial);
const [draft, setDraft] = useState(null);
const [busy, setBusy] = useState(false);
- const [error, setError] = useState(null);
+ const [error, setError] = useState(null);
+ const busyRef = useRef(false);
+ const contentRef = useRef(null);
+ const active = useRef(true);
+ useEffect(() => { active.current = true; return () => { active.current = false; }; }, []);
useEffect(() => {
if (draft === null) setSaved(initial);
@@ -722,16 +843,21 @@ export function ApplicationPackageDraftsSection({
const update = (patch: Partial) => setDraft({ ...value, ...patch });
const save = async () => {
+ if (busyRef.current) return;
+ busyRef.current = true;
+ contentRef.current?.focus();
setBusy(true);
try {
await applicationAssetsApi.saveApplicationDrafts(jobId, value.applicationAnswer, value.recruiterMessage);
+ if (!active.current) return;
setSaved(value);
setDraft(null);
setError(null);
onSaved?.();
} catch (err) {
- setError(getApiErrorMessage(err, t("assetsDraftsSaveFailed")));
+ if (active.current) setError(assetError(err, "assetsDraftsSaveFailed"));
} finally {
+ busyRef.current = false;
setBusy(false);
}
};
@@ -740,8 +866,9 @@ export function ApplicationPackageDraftsSection({
(
-
+
e.preventDefault()} onClick={() => apply(before, after, ph)}>
{icon}
diff --git a/job-tracker-ui/src/i18n/translations.ts b/job-tracker-ui/src/i18n/translations.ts
index 60cf352..04ad14f 100644
--- a/job-tracker-ui/src/i18n/translations.ts
+++ b/job-tracker-ui/src/i18n/translations.ts
@@ -508,6 +508,8 @@ export const translations = {
assetsPublic: "public",
assetsEditPreviewExport: "Edit, preview and export",
assetsDuplicateForJob: "Duplicate for this job",
+ assetsAttachCreatedCopy: "Attach created copy",
+ assetsCopyCreatedHelp: "The copy is saved in your CV library. Attach it here to continue editing.",
assetsNoCvAttached: "No CV is attached to this application yet.",
assetsLegacyCvText: "This application also contains legacy tailored CV text. An attached CV variant takes precedence.",
assetsTailoring: "Tailoring",
@@ -958,6 +960,9 @@ export const translations = {
assetsCharacterCount: "{count} characters",
assetsRestoreVersion: "Restore this version",
assetsRestoreVersionNumber: "Restore version {version}",
+ assetsRestoreDiscardTitle: "Restore saved version?",
+ assetsRestoreDiscardMessage: "Your unsaved edits will be discarded and replaced with the selected saved version.",
+ assetsRestoreConfirm: "Discard and restore",
coverAiAction: "Action",
coverAiTone: "Tone",
coverActionGenerate: "Generate",
@@ -3157,6 +3162,8 @@ export const translations = {
assetsPublic: "offentlig",
assetsEditPreviewExport: "Rediger, forhåndsvis og eksporter",
assetsDuplicateForJob: "Dupliser for denne jobben",
+ assetsAttachCreatedCopy: "Koble til opprettet kopi",
+ assetsCopyCreatedHelp: "Kopien er lagret i CV-biblioteket. Koble den til her for å fortsette redigeringen.",
assetsNoCvAttached: "Ingen CV er tilknyttet søknaden ennå.",
assetsLegacyCvText: "Søknaden inneholder også eldre tilpasset CV-tekst. En tilknyttet CV-variant har prioritet.",
assetsTailoring: "Tilpasning",
@@ -3607,6 +3614,9 @@ export const translations = {
assetsCharacterCount: "{count} tegn",
assetsRestoreVersion: "Gjenopprett denne versjonen",
assetsRestoreVersionNumber: "Gjenopprett versjon {version}",
+ assetsRestoreDiscardTitle: "Gjenopprette lagret versjon?",
+ assetsRestoreDiscardMessage: "Endringene du ikke har lagret, blir forkastet og erstattet med den valgte versjonen.",
+ assetsRestoreConfirm: "Forkast og gjenopprett",
coverAiAction: "Handling",
coverAiTone: "Tone",
coverActionGenerate: "Generer",
diff --git a/job-tracker-ui/src/views/ApplicationWorkspacePage.tsx b/job-tracker-ui/src/views/ApplicationWorkspacePage.tsx
index fb03a35..3ed15fd 100644
--- a/job-tracker-ui/src/views/ApplicationWorkspacePage.tsx
+++ b/job-tracker-ui/src/views/ApplicationWorkspacePage.tsx
@@ -82,7 +82,9 @@ export function ApplicationWorkspace({
const secondaryId = useId();
const panelRef = useRef(null);
- const [overview, setOverview] = useState(null);
+ const [loadedOverview, setOverview] = useState(null);
+ const overview = loadedOverview?.id === jobId ? loadedOverview : null;
+ const overviewRequest = useRef(0);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
const [editOpen, setEditOpen] = useState(false);
@@ -123,6 +125,7 @@ export function ApplicationWorkspace({
}, [blocker, confirm, t]);
const load = useCallback(async () => {
+ const request = ++overviewRequest.current;
if (!Number.isInteger(jobId) || jobId <= 0) {
setOverview(null);
setError({ kind: "error", message: t("workspaceInvalidLink"), retryable: false });
@@ -132,17 +135,20 @@ export function ApplicationWorkspace({
try {
setError(null);
setLoading(true);
- setOverview(await applicationWorkspaceApi.overview(jobId));
+ const next = await applicationWorkspaceApi.overview(jobId);
+ if (request === overviewRequest.current) setOverview(next);
} catch (err) {
+ if (request !== overviewRequest.current) return;
const status = (err as { response?: { status?: number } })?.response?.status;
setError({ kind: "error", message: getApiErrorMessage(err, t("workspaceLoadFailed")), retryable: !status || status >= 500, status });
} finally {
- setLoading(false);
+ if (request === overviewRequest.current) setLoading(false);
}
}, [jobId, t]);
useEffect(() => {
- load();
+ void load();
+ return () => { overviewRequest.current += 1; };
}, [load]);
useEffect(() => {
@@ -253,16 +259,17 @@ export function ApplicationWorkspace({
{subsections.map((sub) => )}
- {section === "application" && subsection === "cv" && jobId > 0 && }
- {section === "application" && subsection === "letter" && jobId > 0 && }
- {section === "application" && subsection === "answers" && jobId > 0 && 0 && }
+ {section === "application" && subsection === "letter" && jobId > 0 && }
+ {section === "application" && subsection === "answers" && overview && jobId > 0 && }
- {section === "application" && subsection === "submission" && jobId > 0 && }
+ {section === "application" && subsection === "submission" && jobId > 0 && }
{section === "timeline" && subsection === "activity" && jobId > 0 && }
{section === "timeline" && subsection === "correspondence" && jobId > 0 && }
@@ -272,7 +279,7 @@ export function ApplicationWorkspace({
go("application", "cv")}>{t("workspaceCv")}
go("application", "letter")}>{t("workspaceCoverLetter")}
>}>
-
+
>}
{section === "tasks" && jobId > 0 && <>
} onClick={() => setEditOpen(true)}>{t("workspaceEdit")}}>