diff --git a/docs/ui-modernisation/visual-correction/ACCOUNT_EDITOR.md b/docs/ui-modernisation/visual-correction/ACCOUNT_EDITOR.md new file mode 100644 index 00000000..fd5592db --- /dev/null +++ b/docs/ui-modernisation/visual-correction/ACCOUNT_EDITOR.md @@ -0,0 +1,13 @@ +# Account sections and editor hierarchy — active qualification + +4 November 2026. Continuation from published/independently verified `1c88cacb4d4d6137ba82e74e53fa27faadb86c1e`, which qualifies flat application source `c9138af01e6dd7cc407a9b71c921914d8c1299f0` with65 selected tests and36-configuration/216-state browser evidence. Those captures are bound to their original source, not automatically to this successor. + +Account changes: remove seven decorative PageSection surfaces from account navigation/identity/details/password, two-factor/session management and mail connection collection. Keep provider objects, recovery-code presentation, every form/control/ref, mounted draft, permission guard and confirmation dialog. The redundant inner Profile heading becomes the paired EN/NB **Identity / Identitet** label; shell Profile h1 remains. Canonical Career Profile and legacy careerOnly behaviour remain separate and unchanged. + +Editor hierarchy: only canonical numeric job-detail routes use the existing editor-width token (100%). Keep the shell h1/focus target but render its generic route label/description compactly; recorded role h2 uses the existing page-title type scale. This follows approved A3 role hierarchy and makes more useful desktop space without fetching/duplicating domain data or changing navigation. List/Settings/unrelated route headings retain their existing styles. + +PASSED:81 selected account/provider/security/export/catalogue tests/eight suites;16 shell/navigation/layout/theme tests/four suites; full lint and export build. Independent read-only reviews found no actionable findings in the seven surface removals, verifier branches, or scoped editor hierarchy. New Identity label is checked by the updated heading contract and paired translation catalogue; native-language acceptance remains a human qualification. + +Initial phone pilots passed seven account states (before final Identity copy) and six editor states including role-title visual hierarchy. Required final runs on the settled export: **36 configurations /252 account scans** (focused details/connections/security, summary, cancelled session confirmation, cancelled data confirmation with fixture403 export, mail connections), plus **36 /216 application scans** to qualify the changed role heading. No production account, actual deletion/revocation/export, provider activation or model is permitted by these fixtures. Skipped admin/processing checks explicitly remain NOT RUN in account-only mode. + +Current status: final browser matrices pending. Use A3_ACCOUNT_FLAT=1 with verify-a3-account-admin.mjs, and A3_VISUAL_MATRIX=1 / A3_FLAT_WORKSPACE=1 with verify-a3-applications.mjs. The existing sources/prototypes do not need another concept-selection decision. Keep source/export identities stable during runs; record actual reports, manually inspect lower focused account sections at phone/tablet/desktop, then seal/publish evidence. Do not declare full34-route/57-group, security/release or owner visual acceptance from these scoped checks. diff --git a/docs/ui-modernisation/visual-correction/ACCOUNT_FLAT_TESTS.json b/docs/ui-modernisation/visual-correction/ACCOUNT_FLAT_TESTS.json new file mode 100644 index 00000000..0fc81594 --- /dev/null +++ b/docs/ui-modernisation/visual-correction/ACCOUNT_FLAT_TESTS.json @@ -0,0 +1 @@ +{"numFailedTestSuites":0,"numFailedTests":0,"numPassedTestSuites":8,"numPassedTests":81,"numPendingTestSuites":0,"numPendingTests":0,"numRuntimeErrorTestSuites":0,"numTodoTests":0,"numTotalTestSuites":8,"numTotalTests":81,"openHandles":[],"snapshot":{"added":0,"didUpdate":false,"failure":false,"filesAdded":0,"filesRemoved":0,"filesRemovedList":[],"filesUnmatched":0,"filesUpdated":0,"matched":0,"total":0,"unchecked":0,"uncheckedKeysByFile":[],"unmatched":0,"updated":0},"startTime":1793751065719,"success":true,"testResults":[{"assertionResults":[{"ancestorTitles":[],"duration":355,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"account workspace navigation preserves identity drafts and focuses existing groups (en)","invocations":1,"location":null,"numPassingAsserts":10,"retryReasons":[],"startAt":1793751069443,"status":"passed","title":"account workspace navigation preserves identity drafts and focuses existing groups (en)"},{"ancestorTitles":[],"duration":209,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"account workspace navigation preserves identity drafts and focuses existing groups (nb)","invocations":1,"location":null,"numPassingAsserts":10,"retryReasons":[],"startAt":1793751069799,"status":"passed","title":"account workspace navigation preserves identity drafts and focuses existing groups (nb)"},{"ancestorTitles":[],"duration":423,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"meaningful identity initials use a readable foreground/background pair in [Function CareerProfilePage]","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793751070008,"status":"passed","title":"meaningful identity initials use a readable foreground/background pair in [Function CareerProfilePage]"},{"ancestorTitles":[],"duration":94,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"meaningful identity initials use a readable foreground/background pair in [Function ProfilePage]","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793751070431,"status":"passed","title":"meaningful identity initials use a readable foreground/background pair in [Function ProfilePage]"},{"ancestorTitles":[],"duration":568,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"Career Profile section headings describe the visible hierarchy without changing editing fields","invocations":1,"location":null,"numPassingAsserts":16,"retryReasons":[],"startAt":1793751070526,"status":"passed","title":"Career Profile section headings describe the visible hierarchy without changing editing fields"},{"ancestorTitles":[],"duration":158,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"account headings separate identity from security sections","invocations":1,"location":null,"numPassingAsserts":6,"retryReasons":[],"startAt":1793751071094,"status":"passed","title":"account headings separate identity from security sections"},{"ancestorTitles":[],"duration":55,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"account CV text status in en does not claim the CV library is empty","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793751071253,"status":"passed","title":"account CV text status in en does not claim the CV library is empty"},{"ancestorTitles":[],"duration":61,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"account CV text status in nb does not claim the CV library is empty","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793751071309,"status":"passed","title":"account CV text status in nb does not claim the CV library is empty"},{"ancestorTitles":[],"duration":366,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"[PRES-E02/E09/C01] account save carries four identity fields and keeps career, email and CV writes separate","invocations":1,"location":null,"numPassingAsserts":7,"retryReasons":[],"startAt":1793751071370,"status":"passed","title":"[PRES-E02/E09/C01] account save carries four identity fields and keeps career, email and CV writes separate"},{"ancestorTitles":[],"duration":372,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"[PRES-E02/E08/E09] failed account save keeps a scoped error and the identity draft for explicit retry","invocations":1,"location":null,"numPassingAsserts":9,"retryReasons":[],"startAt":1793751071737,"status":"passed","title":"[PRES-E02/E08/E09] failed account save keeps a scoped error and the identity draft for explicit retry"},{"ancestorTitles":[],"duration":250,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"[PRES-E02/E08] failed password update keeps a scoped error and secret draft until explicit retry succeeds","invocations":1,"location":null,"numPassingAsserts":10,"retryReasons":[],"startAt":1793751072109,"status":"passed","title":"[PRES-E02/E08] failed password update keeps a scoped error and secret draft until explicit retry succeeds"},{"ancestorTitles":[],"duration":1290,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"profile page loads persisted structured cv and can re-parse it","invocations":1,"location":null,"numPassingAsserts":18,"retryReasons":[],"startAt":1793751072359,"status":"passed","title":"profile page loads persisted structured cv and can re-parse it"},{"ancestorTitles":[],"duration":747,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"saving the career profile PUTs the structured profile and cv text unchanged (Phase 1 refactor invariant)","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793751073649,"status":"passed","title":"saving the career profile PUTs the structured profile and cv text unchanged (Phase 1 refactor invariant)"},{"ancestorTitles":[],"duration":982,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"editing a field in an extracted section updates parent state and flows into save (Phase 1 increment 2)","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793751074396,"status":"passed","title":"editing a field in an extracted section updates parent state and flows into save (Phase 1 increment 2)"},{"ancestorTitles":[],"duration":628,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"profile page can reprocess from stored artifact history","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793751075378,"status":"passed","title":"profile page can reprocess from stored artifact history"},{"ancestorTitles":[],"duration":1016,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"active extraction polling refreshes run status without resetting unsaved career fields","invocations":1,"location":null,"numPassingAsserts":6,"retryReasons":[],"startAt":1793751076006,"status":"passed","title":"active extraction polling refreshes run status without resetting unsaved career fields"},{"ancestorTitles":[],"duration":1075,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"profile page shows durable CV operation state and retries a failed run","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793751077022,"status":"passed","title":"profile page shows durable CV operation state and retries a failed run"},{"ancestorTitles":[],"duration":678,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"profile page keeps raw extraction collapsed until expanded","invocations":1,"location":null,"numPassingAsserts":6,"retryReasons":[],"startAt":1793751078097,"status":"passed","title":"profile page keeps raw extraction collapsed until expanded"},{"ancestorTitles":[],"duration":1061,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"saving the master profile (career) persists the structured profile via /career/profile","invocations":1,"location":null,"numPassingAsserts":8,"retryReasons":[],"startAt":1793751078775,"status":"passed","title":"saving the master profile (career) persists the structured profile via /career/profile"},{"ancestorTitles":[],"duration":120,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"/career shows the profile completeness overview","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793751079836,"status":"passed","title":"/career shows the profile completeness overview"},{"ancestorTitles":[],"duration":617,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"/career profile exposes extraction progress","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793751079957,"status":"passed","title":"/career profile exposes extraction progress"},{"ancestorTitles":[],"duration":898,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"/career profile can queue a legacy text CV upload without visiting import review first","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793751080574,"status":"passed","title":"/career profile can queue a legacy text CV upload without visiting import review first"},{"ancestorTitles":[],"duration":252,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"/career localizes completeness and review confidence without changing CV data","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793751081472,"status":"passed","title":"/career localizes completeness and review confidence without changing CV data"},{"ancestorTitles":[],"duration":1389,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"/career separates overview, profile and import while preserving unsaved profile edits","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"startAt":1793751081724,"status":"passed","title":"/career separates overview, profile and import while preserving unsaved profile edits"},{"ancestorTitles":[],"duration":1793,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"pending CV extraction can be reviewed and applied","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793751083113,"status":"passed","title":"pending CV extraction can be reviewed and applied"},{"ancestorTitles":[],"duration":228,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"account email changes require a password and use the confirmation flow","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793751084906,"status":"passed","title":"account email changes require a password and use the confirmation flow"}],"endTime":1793751085135,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\profile-page.test.tsx","startTime":1793751066603,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":["EmailProviderConnections"],"duration":112,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"EmailProviderConnections renders connection controls in Norwegian Bokmål","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793751086006,"status":"passed","title":"renders connection controls in Norwegian Bokmål"},{"ancestorTitles":["EmailProviderConnections"],"duration":57,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"EmailProviderConnections renders connected state for Gmail and Outlook, disconnected form for IMAP","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793751086118,"status":"passed","title":"renders connected state for Gmail and Outlook, disconnected form for IMAP"},{"ancestorTitles":["EmailProviderConnections"],"duration":816,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"EmailProviderConnections submits IMAP connect form and reloads status on success","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793751086175,"status":"passed","title":"submits IMAP connect form and reloads status on success"},{"ancestorTitles":["EmailProviderConnections"],"duration":32,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"EmailProviderConnections [P7-09 OAuth] unsolicited success and error messages cannot announce or refresh any connection","invocations":1,"location":null,"numPassingAsserts":6,"retryReasons":[],"startAt":1793751086992,"status":"passed","title":"[P7-09 OAuth] unsolicited success and error messages cannot announce or refresh any connection"},{"ancestorTitles":["EmailProviderConnections"],"duration":107,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"EmailProviderConnections [P7-09 OAuth] Gmail accepts only its captured popup and canonical callback origin once","invocations":1,"location":null,"numPassingAsserts":26,"retryReasons":[],"startAt":1793751087024,"status":"passed","title":"[P7-09 OAuth] Gmail accepts only its captured popup and canonical callback origin once"},{"ancestorTitles":["EmailProviderConnections"],"duration":125,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"EmailProviderConnections [P7-09 OAuth] Outlook / Microsoft 365 accepts only its captured popup and canonical callback origin once","invocations":1,"location":null,"numPassingAsserts":26,"retryReasons":[],"startAt":1793751087131,"status":"passed","title":"[P7-09 OAuth] Outlook / Microsoft 365 accepts only its captured popup and canonical callback origin once"},{"ancestorTitles":["EmailProviderConnections"],"duration":125,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"EmailProviderConnections [P7-09 OAuth] a pending popup survives a locale change and valid error uses current fallback without refreshing","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793751087256,"status":"passed","title":"[P7-09 OAuth] a pending popup survives a locale change and valid error uses current fallback without refreshing"},{"ancestorTitles":["EmailProviderConnections"],"duration":93,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"EmailProviderConnections [P7-09 OAuth] duplicate starts reserve one named popup until it is closed","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793751087381,"status":"passed","title":"[P7-09 OAuth] duplicate starts reserve one named popup until it is closed"},{"ancestorTitles":["EmailProviderConnections"],"duration":188,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"EmailProviderConnections [P7-09 OAuth] blocked popups and late disposed connect responses cannot establish a callback sender","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793751087474,"status":"passed","title":"[P7-09 OAuth] blocked popups and late disposed connect responses cannot establish a callback sender"},{"ancestorTitles":["EmailProviderConnections"],"duration":224,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"EmailProviderConnections [P7-09 OAuth] malformed or non-provider callback targets never launch a popup","invocations":1,"location":null,"numPassingAsserts":7,"retryReasons":[],"startAt":1793751087662,"status":"passed","title":"[P7-09 OAuth] malformed or non-provider callback targets never launch a popup"},{"ancestorTitles":["EmailProviderConnections"],"duration":67,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"EmailProviderConnections [P7-09 OAuth] delayed blocked startup feedback uses the language selected while its connect request was pending","invocations":1,"location":null,"numPassingAsserts":7,"retryReasons":[],"startAt":1793751087886,"status":"passed","title":"[P7-09 OAuth] delayed blocked startup feedback uses the language selected while its connect request was pending"},{"ancestorTitles":["EmailProviderConnections"],"duration":74,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"EmailProviderConnections [P7-09 OAuth] delayed rejected startup feedback uses the language selected while its connect request was pending","invocations":1,"location":null,"numPassingAsserts":7,"retryReasons":[],"startAt":1793751087953,"status":"passed","title":"[P7-09 OAuth] delayed rejected startup feedback uses the language selected while its connect request was pending"},{"ancestorTitles":["EmailProviderConnections"],"duration":65,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"EmailProviderConnections [P7-09 Connections] unknown and failed provider reads remain truthful and Retry reads only that provider once","invocations":1,"location":null,"numPassingAsserts":12,"retryReasons":[],"startAt":1793751088027,"status":"passed","title":"[P7-09 Connections] unknown and failed provider reads remain truthful and Retry reads only that provider once"},{"ancestorTitles":["EmailProviderConnections"],"duration":44,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"EmailProviderConnections [P7-09 Connections] a held status read follows the current language without a locale refetch","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793751088092,"status":"passed","title":"[P7-09 Connections] a held status read follows the current language without a locale refetch"},{"ancestorTitles":["EmailProviderConnections"],"duration":138,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"EmailProviderConnections [P7-09 Connections] a committed disconnect with failed refresh blocks writes until GET-only recovery","invocations":1,"location":null,"numPassingAsserts":11,"retryReasons":[],"startAt":1793751088136,"status":"passed","title":"[P7-09 Connections] a committed disconnect with failed refresh blocks writes until GET-only recovery"},{"ancestorTitles":["EmailProviderConnections"],"duration":169,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"EmailProviderConnections [P7-09 Connections] failed disconnect preserves its known status and uses current-language explicit retry","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"startAt":1793751088274,"status":"passed","title":"[P7-09 Connections] failed disconnect preserves its known status and uses current-language explicit retry"},{"ancestorTitles":["EmailProviderConnections"],"duration":218,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"EmailProviderConnections [P7-09 Connections] IMAP submits one captured snapshot and a late success cannot clear a newer password","invocations":1,"location":null,"numPassingAsserts":6,"retryReasons":[],"startAt":1793751088443,"status":"passed","title":"[P7-09 Connections] IMAP submits one captured snapshot and a late success cannot clear a newer password"},{"ancestorTitles":["EmailProviderConnections"],"duration":230,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"EmailProviderConnections [P7-09 Connections] failed IMAP save retains credentials for an explicit retry and current fallback","invocations":1,"location":null,"numPassingAsserts":8,"retryReasons":[],"startAt":1793751088661,"status":"passed","title":"[P7-09 Connections] failed IMAP save retains credentials for an explicit retry and current fallback"},{"ancestorTitles":["EmailProviderConnections"],"duration":131,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"EmailProviderConnections [P7-09 Connections] disposed reads and writes cannot refresh, toast or clear a new panel","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793751088891,"status":"passed","title":"[P7-09 Connections] disposed reads and writes cannot refresh, toast or clear a new panel"},{"ancestorTitles":["EmailProviderConnections"],"duration":152,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"EmailProviderConnections [P7-09 Connections] invalid IMAP ports remain visible and prevent sending a silently substituted port","invocations":1,"location":null,"numPassingAsserts":13,"retryReasons":[],"startAt":1793751089022,"status":"passed","title":"[P7-09 Connections] invalid IMAP ports remain visible and prevent sending a silently substituted port"},{"ancestorTitles":["EmailProviderConnections"],"duration":219,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"EmailProviderConnections [P7-09 Connections] removed Retry restores meaningful focus without stealing deliberate field focus","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793751089174,"status":"passed","title":"[P7-09 Connections] removed Retry restores meaningful focus without stealing deliberate field focus"},{"ancestorTitles":["EmailProviderConnections"],"duration":207,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"EmailProviderConnections [P7-09 Connections] an IMAP field removed after commit has a meaningful fallback while refresh is pending","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793751089393,"status":"passed","title":"[P7-09 Connections] an IMAP field removed after commit has a meaningful fallback while refresh is pending"}],"endTime":1793751089601,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\email-provider-connections.test.tsx","startTime":1793751085147,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":[],"duration":361,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"cancelling setup sends no security write and clears the password before reopening","invocations":1,"location":null,"numPassingAsserts":12,"retryReasons":[],"startAt":1793751090461,"status":"passed","title":"cancelling setup sends no security write and clears the password before reopening"},{"ancestorTitles":[],"duration":482,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"regenerated recovery codes require acknowledgement, survive Escape, export correctly, and clear after Done","invocations":1,"location":null,"numPassingAsserts":23,"retryReasons":[],"startAt":1793751090822,"status":"passed","title":"regenerated recovery codes require acknowledgement, survive Escape, export correctly, and clear after Done"},{"ancestorTitles":[],"duration":134,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"Bokmål enabled status and trusted-device dates survive a read-only retry, then revoke the selected device with readback","invocations":1,"location":null,"numPassingAsserts":21,"retryReasons":[],"startAt":1793751091304,"status":"passed","title":"Bokmål enabled status and trusted-device dates survive a read-only retry, then revoke the selected device with readback"},{"ancestorTitles":[],"duration":48,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"a failed sessions read offers a read-only retry without inventing an empty session list","invocations":1,"location":null,"numPassingAsserts":15,"retryReasons":[],"startAt":1793751091438,"status":"passed","title":"a failed sessions read offers a read-only retry without inventing an empty session list"},{"ancestorTitles":[],"duration":346,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"pending revoke-others disables confirmation and cancellation, blocks duplicate writes, and reads back the surviving session","invocations":1,"location":null,"numPassingAsserts":14,"retryReasons":[],"startAt":1793751091486,"status":"passed","title":"pending revoke-others disables confirmation and cancellation, blocks duplicate writes, and reads back the surviving session"}],"endTime":1793751091833,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\account-security-flows.test.tsx","startTime":1793751089610,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":["Profile account provider cards"],"duration":34,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"Profile account provider cards [PRES-E02/E09] disabled Google has a named account section without starting OAuth","invocations":1,"location":null,"numPassingAsserts":11,"retryReasons":[],"startAt":1793751092590,"status":"passed","title":"[PRES-E02/E09] disabled Google has a named account section without starting OAuth"},{"ancestorTitles":["Profile account provider cards"],"duration":29,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"Profile account provider cards [PRES-E02/E09] disabled Microsoft has a named account section without starting OAuth","invocations":1,"location":null,"numPassingAsserts":11,"retryReasons":[],"startAt":1793751092624,"status":"passed","title":"[PRES-E02/E09] disabled Microsoft has a named account section without starting OAuth"},{"ancestorTitles":["Profile account provider cards"],"duration":43,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"Profile account provider cards [PRES-E02/E09] Google linked metadata follows Bokmål while retaining the stored timestamp","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793751092653,"status":"passed","title":"[PRES-E02/E09] Google linked metadata follows Bokmål while retaining the stored timestamp"},{"ancestorTitles":["Profile account provider cards"],"duration":49,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"Profile account provider cards [PRES-E03/E09] Microsoft linked metadata follows Bokmål while retaining the stored timestamp","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793751092696,"status":"passed","title":"[PRES-E03/E09] Microsoft linked metadata follows Bokmål while retaining the stored timestamp"},{"ancestorTitles":["Profile account provider cards"],"duration":92,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"Profile account provider cards [PRES-E03/E08] Microsoft local linking requires the current password and cancellation keeps it without exchanging credentials","invocations":1,"location":null,"numPassingAsserts":11,"retryReasons":[],"startAt":1793751092745,"status":"passed","title":"[PRES-E03/E08] Microsoft local linking requires the current password and cancellation keeps it without exchanging credentials"},{"ancestorTitles":["Profile account provider cards"],"duration":77,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"Profile account provider cards [PRES-E03/E08] rejected Microsoft linking retains the password and local account for an explicit retry","invocations":1,"location":null,"numPassingAsserts":7,"retryReasons":[],"startAt":1793751092838,"status":"passed","title":"[PRES-E03/E08] rejected Microsoft linking retains the password and local account for an explicit retry"},{"ancestorTitles":["Profile account provider cards"],"duration":65,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"Profile account provider cards [PRES-E03/E05] successful Microsoft local linking retains the established forced sign-in contract","invocations":1,"location":null,"numPassingAsserts":8,"retryReasons":[],"startAt":1793751092915,"status":"passed","title":"[PRES-E03/E05] successful Microsoft local linking retains the established forced sign-in contract"},{"ancestorTitles":["Profile account provider cards"],"duration":74,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"Profile account provider cards [PRES-E03/E05/E08] Microsoft unlink is password-guarded and a failed attempt retains input until a deliberate retry succeeds","invocations":1,"location":null,"numPassingAsserts":13,"retryReasons":[],"startAt":1793751092980,"status":"passed","title":"[PRES-E03/E05/E08] Microsoft unlink is password-guarded and a failed attempt retains input until a deliberate retry succeeds"},{"ancestorTitles":["Profile account provider cards"],"duration":33,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"Profile account provider cards [PRES-E02/E03] a google-owned account cannot unlink its only authentication provider","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793751093054,"status":"passed","title":"[PRES-E02/E03] a google-owned account cannot unlink its only authentication provider"},{"ancestorTitles":["Profile account provider cards"],"duration":32,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"Profile account provider cards [PRES-E02/E03] a microsoft-owned account cannot unlink its only authentication provider","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793751093087,"status":"passed","title":"[PRES-E02/E03] a microsoft-owned account cannot unlink its only authentication provider"},{"ancestorTitles":["Profile account provider cards"],"duration":121,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"Profile account provider cards [PRES-E02/E08] Google local linking reports failure and retries the link endpoint without an exchange","invocations":1,"location":null,"numPassingAsserts":9,"retryReasons":[],"startAt":1793751093119,"status":"passed","title":"[PRES-E02/E08] Google local linking reports failure and retries the link endpoint without an exchange"},{"ancestorTitles":["Profile account provider cards"],"duration":63,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"Profile account provider cards [PRES-E02/E08] failed Google unlink preserves the local account and linked state until explicit retry","invocations":1,"location":null,"numPassingAsserts":10,"retryReasons":[],"startAt":1793751093240,"status":"passed","title":"[PRES-E02/E08] failed Google unlink preserves the local account and linked state until explicit retry"}],"endTime":1793751093304,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\account-providers.test.tsx","startTime":1793751091840,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":[],"duration":440,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"shows not-enabled status and walks through the full enable flow to recovery codes","invocations":1,"location":null,"numPassingAsserts":11,"retryReasons":[],"startAt":1793751094044,"status":"passed","title":"shows not-enabled status and walks through the full enable flow to recovery codes"},{"ancestorTitles":[],"duration":186,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"shows wrong-password error on disable and lets the user retry","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793751094484,"status":"passed","title":"shows wrong-password error on disable and lets the user retry"}],"endTime":1793751094670,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\two-factor-settings.test.tsx","startTime":1793751093311,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":[],"duration":65,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"downloads the readable account ZIP from the protected endpoint","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793751095319,"status":"passed","title":"downloads the readable account ZIP from the protected endpoint"},{"ancestorTitles":[],"duration":44,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"explains the recent-sign-in requirement without weakening it","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793751095385,"status":"passed","title":"explains the recent-sign-in requirement without weakening it"},{"ancestorTitles":[],"duration":34,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"keeps account deletion hidden behind the server flag","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793751095429,"status":"passed","title":"keeps account deletion hidden behind the server flag"},{"ancestorTitles":[],"duration":414,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"requires the exact server-provided deletion phrase","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793751095463,"status":"passed","title":"requires the exact server-provided deletion phrase"}],"endTime":1793751095877,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\account-data-export.test.tsx","startTime":1793751094677,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":[],"duration":38,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"[PRES-E02/E09] Bokmål signed-in status and section semantics use the UI language","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793751096575,"status":"passed","title":"[PRES-E02/E09] Bokmål signed-in status and section semantics use the UI language"},{"ancestorTitles":[],"duration":56,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"[PRES-E02/E08] a failed identity read is unknown and retries without a logout or login write","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793751096614,"status":"passed","title":"[PRES-E02/E08] a failed identity read is unknown and retries without a logout or login write"},{"ancestorTitles":[],"duration":15,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"[PRES-E02] an authoritative unauthenticated identity read retains signed-out presentation","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793751096670,"status":"passed","title":"[PRES-E02] an authoritative unauthenticated identity read retains signed-out presentation"},{"ancestorTitles":[],"duration":31,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"[PRES-E04] unknown 2FA status announces loading and cannot suggest setup","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793751096685,"status":"passed","title":"[PRES-E04] unknown 2FA status announces loading and cannot suggest setup"},{"ancestorTitles":[],"duration":62,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"[PRES-E04/E08] failed 2FA status offers read-only retry before security actions","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"startAt":1793751096716,"status":"passed","title":"[PRES-E04/E08] failed 2FA status offers read-only retry before security actions"},{"ancestorTitles":[],"duration":385,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"[PRES-E05/E08] session-revocation failure stays inside its named confirmation for retry","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793751096778,"status":"passed","title":"[PRES-E05/E08] session-revocation failure stays inside its named confirmation for retry"},{"ancestorTitles":[],"duration":33,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"[PRES-E05/E09] sessions keep stored timestamps and render dates in Bokmål","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793751097163,"status":"passed","title":"[PRES-E05/E09] sessions keep stored timestamps and render dates in Bokmål"}],"endTime":1793751097196,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\account-security-presentation.test.tsx","startTime":1793751095884,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":[],"duration":8,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"English and Bokmål expose the same UI message keys","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793751097400,"status":"passed","title":"English and Bokmål expose the same UI message keys"},{"ancestorTitles":[],"duration":5,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"Bokmål preserves every English substitution variable","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793751097408,"status":"passed","title":"Bokmål preserves every English substitution variable"},{"ancestorTitles":[],"duration":3,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"supported UI catalogues contain no blank messages","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793751097413,"status":"passed","title":"supported UI catalogues contain no blank messages"}],"endTime":1793751097416,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\translation-catalogue.test.ts","startTime":1793751097203,"status":"passed","summary":""}],"wasInterrupted":false} diff --git a/docs/ui-modernisation/visual-correction/EDITOR_HIERARCHY_TESTS.json b/docs/ui-modernisation/visual-correction/EDITOR_HIERARCHY_TESTS.json new file mode 100644 index 00000000..d37ce7be --- /dev/null +++ b/docs/ui-modernisation/visual-correction/EDITOR_HIERARCHY_TESTS.json @@ -0,0 +1 @@ +{"numFailedTestSuites":0,"numFailedTests":0,"numPassedTestSuites":4,"numPassedTests":16,"numPendingTestSuites":0,"numPendingTests":0,"numRuntimeErrorTestSuites":0,"numTodoTests":0,"numTotalTestSuites":4,"numTotalTests":16,"openHandles":[],"snapshot":{"added":0,"didUpdate":false,"failure":false,"filesAdded":0,"filesRemoved":0,"filesRemovedList":[],"filesUnmatched":0,"filesUpdated":0,"matched":0,"total":0,"unchecked":0,"uncheckedKeysByFile":[],"unmatched":0,"updated":0},"startTime":1793750826924,"success":true,"testResults":[{"assertionResults":[{"ancestorTitles":[],"duration":123,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"[PRES-E08] page and nested section headings retain full names and independent actions","invocations":1,"location":null,"numPassingAsserts":7,"retryReasons":[],"startAt":1793750830188,"status":"passed","title":"[PRES-E08] page and nested section headings retain full names and independent actions"},{"ancestorTitles":[],"duration":61,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"[PRES-E08] section composition retains loaded, error/retry and empty child semantics","invocations":1,"location":null,"numPassingAsserts":6,"retryReasons":[],"startAt":1793750830311,"status":"passed","title":"[PRES-E08] section composition retains loaded, error/retry and empty child semantics"}],"endTime":1793750830373,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\page-layout.test.tsx","startTime":1793750827783,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":[],"duration":385,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"en hybrid navigation has five labelled destinations and retains secondary tools","invocations":1,"location":null,"numPassingAsserts":9,"retryReasons":[],"startAt":1793750831378,"status":"passed","title":"en hybrid navigation has five labelled destinations and retains secondary tools"},{"ancestorTitles":[],"duration":291,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"nb hybrid navigation has five labelled destinations and retains secondary tools","invocations":1,"location":null,"numPassingAsserts":9,"retryReasons":[],"startAt":1793750831763,"status":"passed","title":"nb hybrid navigation has five labelled destinations and retains secondary tools"},{"ancestorTitles":[],"duration":174,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"/career/builder/42 selects its primary destination and keyboard route focus survives","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793750832054,"status":"passed","title":"/career/builder/42 selects its primary destination and keyboard route focus survives"},{"ancestorTitles":[],"duration":132,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"/applications/42 selects its primary destination and keyboard route focus survives","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793750832228,"status":"passed","title":"/applications/42 selects its primary destination and keyboard route focus survives"},{"ancestorTitles":[],"duration":106,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"/jobs/42 selects its primary destination and keyboard route focus survives","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793750832361,"status":"passed","title":"/jobs/42 selects its primary destination and keyboard route focus survives"},{"ancestorTitles":[],"duration":298,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"desktop retains labelled collapse, persistence, and all secondary destinations","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793750832468,"status":"passed","title":"desktop retains labelled collapse, persistence, and all secondary destinations"},{"ancestorTitles":[],"duration":303,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"choosing the current destination from More closes the drawer and focuses its heading","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793750832766,"status":"passed","title":"choosing the current destination from More closes the drawer and focuses its heading"}],"endTime":1793750833070,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\a3-navigation.test.tsx","startTime":1793750830386,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":[],"duration":338,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"[P5-00 PRES-E07 E08 A03] en mobile names/current route ignore desktop collapse and retain metadata","invocations":1,"location":null,"numPassingAsserts":7,"retryReasons":[],"startAt":1793750833924,"status":"passed","title":"[P5-00 PRES-E07 E08 A03] en mobile names/current route ignore desktop collapse and retain metadata"},{"ancestorTitles":[],"duration":278,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"[P5-00 PRES-E07 E08 A03] nb mobile names/current route ignore desktop collapse and retain metadata","invocations":1,"location":null,"numPassingAsserts":7,"retryReasons":[],"startAt":1793750834262,"status":"passed","title":"[P5-00 PRES-E07 E08 A03] nb mobile names/current route ignore desktop collapse and retain metadata"},{"ancestorTitles":[],"duration":211,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"compact mobile account menu preserves language selection and Settings access","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793750834541,"status":"passed","title":"compact mobile account menu preserves language selection and Settings access"},{"ancestorTitles":[],"duration":65,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"[P5-00 PRES-E08 S05] toast retains its full existing display duration and dismiss action","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793750834752,"status":"passed","title":"[P5-00 PRES-E08 S05] toast retains its full existing display duration and dismiss action"},{"ancestorTitles":[],"duration":25,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"[P5-00 PRES-E08] toast action is explicit and executes once","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793750834817,"status":"passed","title":"[P5-00 PRES-E08] toast action is explicit and executes once"}],"endTime":1793750834842,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\shell-readiness.test.tsx","startTime":1793750833082,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":[],"duration":6,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"light uses the approved A3 semantic roles","invocations":1,"location":null,"numPassingAsserts":7,"retryReasons":[],"startAt":1793750835296,"status":"passed","title":"light uses the approved A3 semantic roles"},{"ancestorTitles":[],"duration":4,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"dark uses the approved A3 semantic roles","invocations":1,"location":null,"numPassingAsserts":7,"retryReasons":[],"startAt":1793750835302,"status":"passed","title":"dark uses the approved A3 semantic roles"}],"endTime":1793750835307,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\a3-theme-contract.test.ts","startTime":1793750834853,"status":"passed","summary":""}],"wasInterrupted":false} diff --git a/job-tracker-ui/scripts/verify-a3-account-admin.mjs b/job-tracker-ui/scripts/verify-a3-account-admin.mjs index f38ce0c8..aa0f81f5 100644 --- a/job-tracker-ui/scripts/verify-a3-account-admin.mjs +++ b/job-tracker-ui/scripts/verify-a3-account-admin.mjs @@ -7,13 +7,15 @@ import fs from 'node:fs'; import path from 'node:path'; const compiled = ts.transpileModule(fs.readFileSync(new URL('../src/i18n/translations.ts', import.meta.url), 'utf8'), { compilerOptions: { module: ts.ModuleKind.ESNext } }).outputText; const { translations } = await import(`data:text/javascript;base64,${Buffer.from(compiled).toString('base64')}`); -const out = path.resolve(import.meta.dirname, '../../docs/ui-modernisation/evidence/account-admin/browser'); +const accountOnly = Boolean(process.env.A3_ACCOUNT_FLAT); +const out = path.resolve(import.meta.dirname, accountOnly ? '../../docs/ui-modernisation/visual-correction/evidence/account-flat' : '../../docs/ui-modernisation/evidence/account-admin/browser'); fs.mkdirSync(out, { recursive: true }); const report = { screens: [], journeys: [], errors: [], unexpectedWrites: [], exportDenials: [], unavailableReads: [], blockedExternal: [] }; const browser = await chromium.launch(); const processingOnly = Boolean(process.env.A3_PROCESSING_ONLY); +assert.ok(!(processingOnly && accountOnly), 'Account and processing qualification are distinct scopes.'); try { - const configurations = process.env.A3_PILOT ? [[320, 'en', 'light']] : [320, 390, 768, 1440].flatMap(width => ['en', 'nb'].flatMap(language => ['light', 'dark'].map(mode => [width, language, mode]))); + const configurations = process.env.A3_PILOT ? [[320, 'en', 'light']] : (accountOnly ? [320, 360, 390, 430, 768, 1024, 1280, 1440, 1920] : [320, 390, 768, 1440]).flatMap(width => ['en', 'nb'].flatMap(language => ['light', 'dark'].map(mode => [width, language, mode]))); for (const [width, language, mode] of configurations) { console.log(`Checking ${width}/${language}/${mode}`); const t = key => { assert.ok(translations[language][key], key); return translations[language][key]; }; @@ -25,7 +27,7 @@ try { const listing = { items: [operation], total: 1, counts: { failed: 1 }, workers: [{ name: 'CvDocumentImports', flagEnabled: false, individualOperationsRecorded: true }] }; await page.route('**/*', route => { const req = route.request(), url = new URL(req.url()), p = url.pathname; - if (!['localhost', '127.0.0.1'].includes(url.hostname)) { report.blockedExternal.push(url.origin); return route.abort(); } + if (url.origin !== 'http://localhost:4338' && !(url.origin === 'http://localhost:5202' && p.startsWith('/api/'))) { report.blockedExternal.push(url.origin); return route.abort(); } if (!p.startsWith('/api/')) return route.continue(); const answer = data => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(data) }); if (req.method() === 'POST' && p === '/api/export/account') { @@ -48,11 +50,11 @@ try { if (Object.hasOwn(fixtures, p)) return answer(fixtures[p]); report.unavailableReads.push(p); return route.fulfill({ status: 503, contentType: 'application/json', body: JSON.stringify({ detail: 'Controlled offline read unavailable.' }) }); }); - const scan = async state => { + const scan = async (state, keepScroll = false) => { assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth + 1), false, `overflow:${state}`); const result = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa']).analyze(); report.screens.push({ width, language, mode, state, violations: result.violations }); assert.deepEqual(result.violations, [], `${state} accessibility`); - await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'instant' })); await expect.poll(() => page.evaluate(() => window.scrollY)).toBe(0); + if (!keepScroll) { await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'instant' })); await expect.poll(() => page.evaluate(() => window.scrollY)).toBe(0); } await page.screenshot({ path: path.join(out, `${state}-${language}-${mode}-${width}.png`), fullPage: width >= 768 }); }; if (!processingOnly) { @@ -61,6 +63,7 @@ try { const nav = page.getByRole('navigation', { name: t('accountWorkspaceNavigation'), exact: true }); for (const [id, key] of [['account-details', 'profileAccountSection'], ['account-connections', 'accountWorkspaceConnections'], ['account-security', 'accountWorkspaceSecurity']]) { await nav.getByRole('button', { name: t(key), exact: true }).press('Enter'); await expect(page.locator(`#${id}`)).toBeFocused(); await expect(name).toHaveValue('Unsaved account name'); + if (accountOnly) await scan(id, true); } await scan('account'); await page.getByRole('button', { name: t('sessionsRevokeOthers'), exact: true }).click(); const confirmation = page.getByRole('dialog'); @@ -69,10 +72,13 @@ try { await page.getByRole('button', { name: t('accountExportDownload'), exact: true }).click(); await expect(page.getByText(t('accountExportFailed'), { exact: true })).toBeVisible(); await page.getByRole('button', { name: t('accountDeleteButton'), exact: true }).click(); const deletion = page.getByRole('dialog'); await expect(deletion).toContainText('DELETE alex@example.test'); await scan('data-confirmation'); await deletion.getByRole('button', { name: t('cancel'), exact: true }).click(); await page.goto('http://localhost:4338/settings/connected-accounts'); await expect(page.getByRole('heading', { name: t('connectedAccounts'), exact: true })).toBeVisible(); await scan('connections'); + if (!accountOnly) { await page.goto('http://localhost:4338/admin/users'); await expect(page.getByRole('button', { name: t('adminUsersRemoveAdmin'), exact: true })).toBeDisabled(); await expect(page.getByRole('button', { name: t('adminUsersDelete'), exact: true })).toBeDisabled(); await scan('last-admin'); await page.goto('http://localhost:4338/admin/audit'); await expect(page.getByText(t('adminAuditNoEvents'), { exact: true })).toBeVisible(); await scan('audit-empty'); await page.goto('http://localhost:4338/admin/system'); await expect(page.getByText(t('adminSystemLoadFailed'), { exact: true })).toBeVisible(); await scan('system-unavailable'); } + } + if (!accountOnly) { await page.goto('http://localhost:4338/admin/processing'); await expect(page.getByRole('button', { name: t('processingViewDetails').replace('{id}', operation.id), exact: true })).toBeVisible(); for (const key of ['processingStatus', 'processingTask', 'processingWorkerFilter', 'processingFailureCode']) { const control = page.getByRole('combobox', { name: t(key), exact: true }); @@ -84,9 +90,10 @@ try { await page.getByRole('button', { name: t('processingReturnFilters'), exact: true }).click(); await expect(page.getByRole('form', { name: t('processingFilters'), exact: true })).toBeFocused(); await page.getByRole('button', { name: t('processingViewDetails').replace('{id}', operation.id), exact: true }).click(); await expect(page.getByRole('heading', { name: t('processingDetails'), exact: true })).toBeFocused(); await expect(page.getByRole('link', { name: operation.job.title, exact: true })).toHaveCount(0); await scan('processing-detail'); - report.journeys.push({ width, language, mode, draftPreservedAcrossKeyboardSections: processingOnly ? 'NOT RUN' : 'PASS', revocationAndDeletionCancelled: processingOnly ? 'NOT RUN' : 'PASS', recentExportDenial: processingOnly ? 'NOT RUN' : 'PASS', lastAdminActionsDisabled: processingOnly ? 'NOT RUN' : 'PASS', processingKeyboardAndOwnerLink: 'PASS', processingFilterLabelsAndDurationGeometry: 'PASS' }); await context.close(); + } + report.journeys.push({ width, language, mode, draftPreservedAcrossKeyboardSections: processingOnly ? 'NOT RUN' : 'PASS', revocationAndDeletionCancelled: processingOnly ? 'NOT RUN' : 'PASS', recentExportDenial: processingOnly ? 'NOT RUN' : 'PASS', lastAdminActionsDisabled: processingOnly || accountOnly ? 'NOT RUN' : 'PASS', processingKeyboardAndOwnerLink: accountOnly ? 'NOT RUN' : 'PASS', processingFilterLabelsAndDurationGeometry: accountOnly ? 'NOT RUN' : 'PASS' }); await context.close(); } - assert.equal(report.screens.length, configurations.length * (processingOnly ? 1 : 8)); assert.equal(report.journeys.length, configurations.length); assert.equal(report.exportDenials.length, processingOnly ? 0 : configurations.length); + assert.equal(report.screens.length, configurations.length * (accountOnly ? 7 : processingOnly ? 1 : 8)); assert.equal(report.journeys.length, configurations.length); assert.equal(report.exportDenials.length, processingOnly ? 0 : configurations.length); assert.deepEqual(report.errors, []); assert.deepEqual(report.unexpectedWrites, []); assert.ok(report.blockedExternal.every(origin => origin === 'https://fonts.googleapis.com')); console.log(`PASS:${report.journeys.length} controlled journeys,${report.screens.length} axe scans/captures; no mutation requests.`); } finally { diff --git a/job-tracker-ui/scripts/verify-a3-applications.mjs b/job-tracker-ui/scripts/verify-a3-applications.mjs index 33eb6c1a..6a516351 100644 --- a/job-tracker-ui/scripts/verify-a3-applications.mjs +++ b/job-tracker-ui/scripts/verify-a3-applications.mjs @@ -99,6 +99,9 @@ try { assert.ok(width > 1000 ? side.x > main.x + main.width - 2 && Math.abs(side.y - main.y) < 2 : side.y >= main.y + main.height - 2); await scan('detail'); if (flatWorkspace) { + const pageTitleSize = await page.getByRole('heading', { level: 1 }).evaluate(element => parseFloat(getComputedStyle(element).fontSize)); + const roleTitleSize = await page.getByRole('heading', { name: 'Community nurse', level: 2, exact: true }).evaluate(element => parseFloat(getComputedStyle(element).fontSize)); + assert.ok(roleTitleSize > pageTitleSize, 'Recorded role title has stronger visual hierarchy than generic route label'); await page.getByRole('button', { name: language === 'en' ? 'Job details' : 'Stillingsdetaljer', exact: true }).click(); await expect(page.getByText('Recorded source advertisement.', { exact: true })).toBeVisible(); await scan('detail-expanded'); diff --git a/job-tracker-ui/src/App.tsx b/job-tracker-ui/src/App.tsx index 40be5810..3f26102f 100644 --- a/job-tracker-ui/src/App.tsx +++ b/job-tracker-ui/src/App.tsx @@ -428,7 +428,7 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo + + {loading ? {t("loading")} : null} {error ? {t("retry")}}>{error} : null} {!loading && sessions.length === 0 && !error ? ( diff --git a/job-tracker-ui/src/components/TwoFactorSettingsCard.tsx b/job-tracker-ui/src/components/TwoFactorSettingsCard.tsx index ccb32598..fe5cf1b2 100644 --- a/job-tracker-ui/src/components/TwoFactorSettingsCard.tsx +++ b/job-tracker-ui/src/components/TwoFactorSettingsCard.tsx @@ -192,7 +192,7 @@ export default function TwoFactorSettingsCard() { const isRecoveryStep = flow === "enable-recovery" || flow === "regenerate-recovery"; return ( - + {statusLoading ? {t("loading")} : null} {statusError ? {t("retry")}}>{statusError} : null} {status && !statusLoading ? ( diff --git a/job-tracker-ui/src/i18n/translations.ts b/job-tracker-ui/src/i18n/translations.ts index c9b0e3ed..5da15bc0 100644 --- a/job-tracker-ui/src/i18n/translations.ts +++ b/job-tracker-ui/src/i18n/translations.ts @@ -1651,6 +1651,7 @@ export const translations = { settingsNotificationsSaveFailed: "Notification settings could not be saved.", settingsCheckSystemStatus: "Check system status", profileTitle: "Profile", + profileIdentityTitle: "Identity", profileHeadlinePlaceholder: "Add a short headline to personalize your account view.", profileLocalAccount: "Local account", profileGoogleSession: "Google session", @@ -4866,6 +4867,7 @@ export const translations = { settingsNotificationsSaveFailed: "Varslingsinnstillingene kunne ikke lagres.", settingsCheckSystemStatus: "Sjekk systemstatus", profileTitle: "Profil", + profileIdentityTitle: "Identitet", profileHeadlinePlaceholder: "Legg til en kort overskrift for å gjøre kontovisningen mer personlig.", profileLocalAccount: "Lokal konto", profileGoogleSession: "Google-økt", diff --git a/job-tracker-ui/src/layout/AppShell.tsx b/job-tracker-ui/src/layout/AppShell.tsx index a183a6aa..ceeb1618 100644 --- a/job-tracker-ui/src/layout/AppShell.tsx +++ b/job-tracker-ui/src/layout/AppShell.tsx @@ -682,7 +682,9 @@ export default function AppShell({ > - + {breadcrumbs.map((c) => ( diff --git a/job-tracker-ui/src/profile-page.test.tsx b/job-tracker-ui/src/profile-page.test.tsx index 49c23320..e777be53 100644 --- a/job-tracker-ui/src/profile-page.test.tsx +++ b/job-tracker-ui/src/profile-page.test.tsx @@ -238,7 +238,7 @@ test('Career Profile section headings describe the visible hierarchy without cha test('account headings separate identity from security sections', async () => { renderWith(ProfilePage); await screen.findByText('DD'); - expect(screen.getByRole('heading', { name: 'Profile', level: 2 })).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Identity', level: 2 })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: 'Account', level: 2 })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: 'Change password', level: 2 })).toBeInTheDocument(); expect(screen.queryByRole('heading', { level: 1 })).not.toBeInTheDocument(); diff --git a/job-tracker-ui/src/views/ApplicationWorkspacePage.tsx b/job-tracker-ui/src/views/ApplicationWorkspacePage.tsx index a8dede7e..45689a92 100644 --- a/job-tracker-ui/src/views/ApplicationWorkspacePage.tsx +++ b/job-tracker-ui/src/views/ApplicationWorkspacePage.tsx @@ -358,7 +358,7 @@ function WorkspaceHeader({ overview, onEdit }: { overview: WorkspaceOverview | n const { language, t } = useI18n(); if (!overview) return ; return ( - + {overview.jobUrl ? : null} diff --git a/job-tracker-ui/src/views/ProfilePage.tsx b/job-tracker-ui/src/views/ProfilePage.tsx index 9294ab69..3760bc6c 100644 --- a/job-tracker-ui/src/views/ProfilePage.tsx +++ b/job-tracker-ui/src/views/ProfilePage.tsx @@ -139,7 +139,7 @@ export default function ProfilePage() { return ( - + {([ [accountDetails, t("profileAccountSection")], @@ -186,7 +186,7 @@ export default function ProfilePage() { {loading ? {t("loading")} : null} - + @@ -255,7 +255,7 @@ export default function ProfilePage() { - + {!careerOnly && !isLocal ? {t("profileReadOnlyInfo")} : null} @@ -355,7 +355,7 @@ export default function ProfilePage() { - {!careerOnly ? + {!careerOnly ? {!careerOnly && !isLocal ? {t("profilePasswordLocalOnly")} : null}