From fa6bd5d98ea121d4d91dbae214bcd826806377ed Mon Sep 17 00:00:00 2001 From: cesnimda Date: Wed, 4 Nov 2026 01:44:39 +0100 Subject: [PATCH] fix(i18n): preserve public auth commands and feedback across language changes --- .../visual-correction/PUBLIC_ASYNC_RED.json | 1 + .../visual-correction/PUBLIC_ERRORS_RED.json | 1 + .../visual-correction/PUBLIC_LOCALE.md | 11 +++ .../PUBLIC_LOCALE_FINAL_TESTS.json | 1 + .../visual-correction/PUBLIC_LOCALE_RED.json | 1 + .../PUBLIC_LOCALE_TESTS.json | 1 + .../PUBLIC_LOCAL_SAVE_RED.json | 1 + .../PUBLIC_SELECTOR_RED.json | 1 + .../scripts/verify-a3-public-entry.mjs | 82 ++++++++++++++++ .../src/components/TwoFactorChallenge.tsx | 10 +- job-tracker-ui/src/i18n-provider.test.tsx | 25 ++++- job-tracker-ui/src/i18n/I18nProvider.tsx | 12 ++- job-tracker-ui/src/layout/AuthPageFrame.tsx | 7 +- .../src/public-auth-presentation.test.tsx | 95 +++++++++++++++++++ job-tracker-ui/src/verify-email-page.test.tsx | 49 +++++++++- .../src/views/ForgotPasswordPage.tsx | 18 ++-- job-tracker-ui/src/views/LoginPage.tsx | 36 +++---- .../src/views/MicrosoftLegacyRelinkPage.tsx | 6 +- .../src/views/ResetPasswordPage.tsx | 22 +++-- job-tracker-ui/src/views/VerifyEmailPage.tsx | 21 ++-- 20 files changed, 343 insertions(+), 58 deletions(-) create mode 100644 docs/ui-modernisation/visual-correction/PUBLIC_ASYNC_RED.json create mode 100644 docs/ui-modernisation/visual-correction/PUBLIC_ERRORS_RED.json create mode 100644 docs/ui-modernisation/visual-correction/PUBLIC_LOCALE.md create mode 100644 docs/ui-modernisation/visual-correction/PUBLIC_LOCALE_FINAL_TESTS.json create mode 100644 docs/ui-modernisation/visual-correction/PUBLIC_LOCALE_RED.json create mode 100644 docs/ui-modernisation/visual-correction/PUBLIC_LOCALE_TESTS.json create mode 100644 docs/ui-modernisation/visual-correction/PUBLIC_LOCAL_SAVE_RED.json create mode 100644 docs/ui-modernisation/visual-correction/PUBLIC_SELECTOR_RED.json create mode 100644 job-tracker-ui/scripts/verify-a3-public-entry.mjs diff --git a/docs/ui-modernisation/visual-correction/PUBLIC_ASYNC_RED.json b/docs/ui-modernisation/visual-correction/PUBLIC_ASYNC_RED.json new file mode 100644 index 00000000..1e82de68 --- /dev/null +++ b/docs/ui-modernisation/visual-correction/PUBLIC_ASYNC_RED.json @@ -0,0 +1 @@ +{"numFailedTestSuites":1,"numFailedTests":3,"numPassedTestSuites":0,"numPassedTests":0,"numPendingTestSuites":0,"numPendingTests":31,"numRuntimeErrorTestSuites":0,"numTodoTests":0,"numTotalTestSuites":1,"numTotalTests":34,"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":1793751943631,"success":false,"testResults":[{"assertionResults":[{"ancestorTitles":["public auth presentation"],"duration":277,"failing":false,"failureDetails":[{"matcherResult":{"pass":false,"message":"expect(element).toHaveTextContent()\n\nExpected element to have text content:\n Logget inn.\nReceived:\n Signed in."}}],"failureMessages":["Error: expect(element).toHaveTextContent()\n\nExpected element to have text content:\n Logget inn.\nReceived:\n Signed in.\n at toHaveTextContent (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\public-auth-presentation.test.tsx:70:39)"],"fullName":"public auth presentation announces a pending login result in the current UI language without replay","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947018,"status":"failed","title":"announces a pending login result in the current UI language without replay"},{"ancestorTitles":["public auth presentation"],"duration":84,"failing":false,"failureDetails":[{"matcherResult":{"pass":false,"message":"expect(element).toHaveTextContent()\n\nExpected element to have text content:\n Hvis kontoen finnes, er en nullstillingslenke sendt.\nReceived:\n If that account exists, a reset link has been sent."}}],"failureMessages":["Error: expect(element).toHaveTextContent()\n\nExpected element to have text content:\n Hvis kontoen finnes, er en nullstillingslenke sendt.\nReceived:\n If that account exists, a reset link has been sent.\n at toHaveTextContent (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\public-auth-presentation.test.tsx:70:39)"],"fullName":"public auth presentation announces a pending request result in the current UI language without replay","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947304,"status":"failed","title":"announces a pending request result in the current UI language without replay"},{"ancestorTitles":["public auth presentation"],"duration":112,"failing":false,"failureDetails":[{"matcherResult":{"pass":false,"message":"expect(element).toHaveTextContent()\n\nExpected element to have text content:\n Passord tilbakestilt. Vennligst logg inn.\nReceived:\n Password reset. Please sign in."}}],"failureMessages":["Error: expect(element).toHaveTextContent()\n\nExpected element to have text content:\n Passord tilbakestilt. Vennligst logg inn.\nReceived:\n Password reset. Please sign in.\n at toHaveTextContent (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\public-auth-presentation.test.tsx:70:39)"],"fullName":"public auth presentation announces a pending reset result in the current UI language without replay","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947388,"status":"failed","title":"announces a pending reset result in the current UI language without replay"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation translates existing client validation on /login without submitting again","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"translates existing client validation on /login without submitting again"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation translates existing client validation on /register without submitting again","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"translates existing client validation on /register without submitting again"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation translates existing client validation on /forgot-password without submitting again","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"translates existing client validation on /forgot-password without submitting again"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation translates existing client validation on /reset-password?email=synthetic%40example.test&token=synthetic without submitting again","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"translates existing client validation on /reset-password?email=synthetic%40example.test&token=synthetic without submitting again"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation translates a failed login fallback while keeping its submitted draft and request count","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"translates a failed login fallback while keeping its submitted draft and request count"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation translates a failed request fallback while keeping its submitted draft and request count","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"translates a failed request fallback while keeping its submitted draft and request count"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation translates a failed reset fallback while keeping its submitted draft and request count","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"translates a failed reset fallback while keeping its submitted draft and request count"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation translates an MFA rate limit without another challenge or losing the entered code","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"translates an MFA rate limit without another challenge or losing the entered code"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation lets a signed-out user choose a persisted UI language without discarding a sign-in draft","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"lets a signed-out user choose a persisted UI language without discarding a sign-in draft"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation names the password field for login intent and retains its autocomplete contract","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"names the password field for login intent and retains its autocomplete contract"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation names the password field for register intent and retains its autocomplete contract","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"names the password field for register intent and retains its autocomplete contract"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation provides one named main landmark and H1 for password request","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"provides one named main landmark and H1 for password request"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation provides one named main landmark and H1 for password reset","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"provides one named main landmark and H1 for password reset"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation provides one named main landmark and H1 for email verification","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"provides one named main landmark and H1 for email verification"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation provides one named main landmark and H1 for email change","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"provides one named main landmark and H1 for email change"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation provides one named main landmark and H1 for Microsoft recovery","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"provides one named main landmark and H1 for Microsoft recovery"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation announces the pending confirmation without an unnamed progressbar: false","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"announces the pending confirmation without an unnamed progressbar: false"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation announces the pending confirmation without an unnamed progressbar: true","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"announces the pending confirmation without an unnamed progressbar: true"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation retains the entered reset email and a durable error in the page when the command fails","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"retains the entered reset email and a durable error in the page when the command fails"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation retains both new-password fields and the decoded recovery token after a rejected reset","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"retains both new-password fields and the decoded recovery token after a rejected reset"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation keeps a rejected local sign-in beside the retained fields without navigating","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"keeps a rejected local sign-in beside the retained fields without navigating"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation keeps the reset-request action name while announcing an in-flight command","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"keeps the reset-request action name while announcing an in-flight command"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation announces an in-flight local sign-in while preserving its action name","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"announces an in-flight local sign-in while preserving its action name"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation announces a pending password save without changing its payload or successful destination","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"announces a pending password save without changing its payload or successful destination"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation focuses the first invalid local-login field and associates its explanation","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"focuses the first invalid local-login field and associates its explanation"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation focuses the invalid new password without losing confirmation input","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"focuses the invalid new password without losing confirmation input"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation focuses and explains the invalid reset-request email without sending a command","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"focuses and explains the invalid reset-request email without sending a command"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation replaces previous reset-request success when a later request fails","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"replaces previous reset-request success when a later request fails"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation does not assert optional authentication while login configuration is unresolved","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"does not assert optional authentication while login configuration is unresolved"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation does not assert optional authentication while register configuration is unresolved","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"does not assert optional authentication while register configuration is unresolved"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation offers a read-only configuration retry while retaining entered sign-in fields","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751947501,"status":"pending","title":"offers a read-only configuration retry while retaining entered sign-in fields"}],"endTime":1793751947514,"message":" ● public auth presentation › announces a pending login result in the current UI language without replay\n\n expect(element).toHaveTextContent()\n\n Expected element to have text content:\n Logget inn.\n Received:\n Signed in.\n\n 68 | fireEvent.click(screen.getByRole('button', { name: 'Norsk bokmål' }));\n 69 | await act(async () => pending.resolve({ data: {} }));\n > 70 | expect(screen.getByRole('alert')).toHaveTextContent(translations.nb[messageKey]);\n | ^\n 71 | expect(mockedApi.post).toHaveBeenCalledTimes(1);\n 72 | });\n 73 |\n\n at toHaveTextContent (src/public-auth-presentation.test.tsx:70:39)\n\n ● public auth presentation › announces a pending request result in the current UI language without replay\n\n expect(element).toHaveTextContent()\n\n Expected element to have text content:\n Hvis kontoen finnes, er en nullstillingslenke sendt.\n Received:\n If that account exists, a reset link has been sent.\n\n 68 | fireEvent.click(screen.getByRole('button', { name: 'Norsk bokmål' }));\n 69 | await act(async () => pending.resolve({ data: {} }));\n > 70 | expect(screen.getByRole('alert')).toHaveTextContent(translations.nb[messageKey]);\n | ^\n 71 | expect(mockedApi.post).toHaveBeenCalledTimes(1);\n 72 | });\n 73 |\n\n at toHaveTextContent (src/public-auth-presentation.test.tsx:70:39)\n\n ● public auth presentation › announces a pending reset result in the current UI language without replay\n\n expect(element).toHaveTextContent()\n\n Expected element to have text content:\n Passord tilbakestilt. Vennligst logg inn.\n Received:\n Password reset. Please sign in.\n\n 68 | fireEvent.click(screen.getByRole('button', { name: 'Norsk bokmål' }));\n 69 | await act(async () => pending.resolve({ data: {} }));\n > 70 | expect(screen.getByRole('alert')).toHaveTextContent(translations.nb[messageKey]);\n | ^\n 71 | expect(mockedApi.post).toHaveBeenCalledTimes(1);\n 72 | });\n 73 |\n\n at toHaveTextContent (src/public-auth-presentation.test.tsx:70:39)\n","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\public-auth-presentation.test.tsx","startTime":1793751944526,"status":"failed","summary":""}],"wasInterrupted":false} diff --git a/docs/ui-modernisation/visual-correction/PUBLIC_ERRORS_RED.json b/docs/ui-modernisation/visual-correction/PUBLIC_ERRORS_RED.json new file mode 100644 index 00000000..e456fb6d --- /dev/null +++ b/docs/ui-modernisation/visual-correction/PUBLIC_ERRORS_RED.json @@ -0,0 +1 @@ +{"numFailedTestSuites":1,"numFailedTests":8,"numPassedTestSuites":0,"numPassedTests":0,"numPendingTestSuites":0,"numPendingTests":23,"numRuntimeErrorTestSuites":0,"numTodoTests":0,"numTotalTestSuites":1,"numTotalTests":31,"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":1793751864182,"success":false,"testResults":[{"assertionResults":[{"ancestorTitles":["public auth presentation"],"duration":204,"failing":false,"failureDetails":[{"name":"TestingLibraryElementError"}],"failureMessages":["TestingLibraryElementError: Unable to find an element with the text: Brukernavn eller e-post er påkrevd. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.\n\nIgnored nodes: comments, script, style\n\u001b[36m\u001b[39m\n \u001b[36m
\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mLogg inn\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mAutentisering er påkrevd for å bruke denne appen.\u001b[0m\n \u001b[36m

\u001b[39m\n \u001b[36m
\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mEN\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mNB\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mBrukernavn eller e-post\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mBrukernavn eller e-post\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mUsername or email is required\u001b[0m\n \u001b[36m

\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m
\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mOpprett konto\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mAutentisering er påkrevd for å bruke denne appen.\u001b[0m\n \u001b[36m

\u001b[39m\n \u001b[36m
\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mEN\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mNB\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mE-post\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mE-post\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mEnter your email address.\u001b[0m\n \u001b[36m

\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m
\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mBe om tilbakestilling av passord\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mSkriv inn e-postadressen din, så sender vi en nullstillingslenke hvis kontoen finnes.\u001b[0m\n \u001b[36m

\u001b[39m\n \u001b[36m
\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mEN\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mNB\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mE-post\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mE-post\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mEnter your email address.\u001b[0m\n \u001b[36m

\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mTilbake til innlogging\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mSend nullstillingslenke\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n\u001b[36m\u001b[39m\n at Object.getElementError (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\dom\\dist\\config.js:37:19)\n at C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\dom\\dist\\query-helpers.js:76:38\n at C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\dom\\dist\\query-helpers.js:52:17\n at C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\dom\\dist\\query-helpers.js:95:19\n at getByText (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\public-auth-presentation.test.tsx:62:19)"],"fullName":"public auth presentation translates existing client validation on /forgot-password without submitting again","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793751867861,"status":"failed","title":"translates existing client validation on /forgot-password without submitting again"},{"ancestorTitles":["public auth presentation"],"duration":77,"failing":false,"failureDetails":[{"name":"TestingLibraryElementError"}],"failureMessages":["TestingLibraryElementError: Unable to find an element with the text: Passordet må være minst 8 tegn, med et tall og en liten bokstav.. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.\n\nIgnored nodes: comments, script, style\n\u001b[36m\u001b[39m\n \u001b[36m
\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mTilbakestill passord\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mSett et nytt passord for kontoen din.\u001b[0m\n \u001b[36m

\u001b[39m\n \u001b[36m
\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mEN\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mNB\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mE-post\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mE-post\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m
\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mLogg inn\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mAutentisering er påkrevd for å bruke denne appen.\u001b[0m\n \u001b[36m

\u001b[39m\n \u001b[36m
\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mEN\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mNB\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mBrukernavn eller e-post\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mBrukernavn eller e-post\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mUsername or email is required\u001b[0m\n \u001b[36m

\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m 62 | expect(screen.getByText(translations.nb[errorKey])).toBeVisible();\n | ^\n 63 | expect(mockedApi.post).not.toHaveBeenCalled();\n 64 | });\n 65 |\n\n at Object.getElementError (node_modules/@testing-library/dom/dist/config.js:37:19)\n at node_modules/@testing-library/dom/dist/query-helpers.js:76:38\n at node_modules/@testing-library/dom/dist/query-helpers.js:52:17\n at node_modules/@testing-library/dom/dist/query-helpers.js:95:19\n at getByText (src/public-auth-presentation.test.tsx:62:19)\n\n ● public auth presentation › translates existing client validation on /register without submitting again\n\n TestingLibraryElementError: Unable to find an element with the text: Skriv inn e-postadressen din.. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.\n\n Ignored nodes: comments, script, style\n \u001b[36m\u001b[39m\n \u001b[36m
\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mOpprett konto\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mAutentisering er påkrevd for å bruke denne appen.\u001b[0m\n \u001b[36m

\u001b[39m\n \u001b[36m
\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mEN\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mNB\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mE-post\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mE-post\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mEnter your email address.\u001b[0m\n \u001b[36m

\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m 62 | expect(screen.getByText(translations.nb[errorKey])).toBeVisible();\n | ^\n 63 | expect(mockedApi.post).not.toHaveBeenCalled();\n 64 | });\n 65 |\n\n at Object.getElementError (node_modules/@testing-library/dom/dist/config.js:37:19)\n at node_modules/@testing-library/dom/dist/query-helpers.js:76:38\n at node_modules/@testing-library/dom/dist/query-helpers.js:52:17\n at node_modules/@testing-library/dom/dist/query-helpers.js:95:19\n at getByText (src/public-auth-presentation.test.tsx:62:19)\n\n ● public auth presentation › translates existing client validation on /forgot-password without submitting again\n\n TestingLibraryElementError: Unable to find an element with the text: Skriv inn e-postadressen din.. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.\n\n Ignored nodes: comments, script, style\n \u001b[36m\u001b[39m\n \u001b[36m
\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mBe om tilbakestilling av passord\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mSkriv inn e-postadressen din, så sender vi en nullstillingslenke hvis kontoen finnes.\u001b[0m\n \u001b[36m

\u001b[39m\n \u001b[36m
\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mEN\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mNB\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mE-post\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mE-post\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mEnter your email address.\u001b[0m\n \u001b[36m

\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mTilbake til innlogging\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mSend nullstillingslenke\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n\n 60 | expect(await screen.findByText(translations.en[errorKey])).toBeInTheDocument();\n 61 | fireEvent.click(screen.getByRole('button', { name: 'Norsk bokmål' }));\n > 62 | expect(screen.getByText(translations.nb[errorKey])).toBeVisible();\n | ^\n 63 | expect(mockedApi.post).not.toHaveBeenCalled();\n 64 | });\n 65 |\n\n at Object.getElementError (node_modules/@testing-library/dom/dist/config.js:37:19)\n at node_modules/@testing-library/dom/dist/query-helpers.js:76:38\n at node_modules/@testing-library/dom/dist/query-helpers.js:52:17\n at node_modules/@testing-library/dom/dist/query-helpers.js:95:19\n at getByText (src/public-auth-presentation.test.tsx:62:19)\n\n ● public auth presentation › translates existing client validation on /reset-password?email=synthetic%40example.test&token=synthetic without submitting again\n\n TestingLibraryElementError: Unable to find an element with the text: Passordet må være minst 8 tegn, med et tall og en liten bokstav.. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.\n\n Ignored nodes: comments, script, style\n \u001b[36m\u001b[39m\n \u001b[36m
\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mTilbakestill passord\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mSett et nytt passord for kontoen din.\u001b[0m\n \u001b[36m

\u001b[39m\n \u001b[36m
\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mEN\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mNB\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mE-post\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mE-post\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m 62 | expect(screen.getByText(translations.nb[errorKey])).toBeVisible();\n | ^\n 63 | expect(mockedApi.post).not.toHaveBeenCalled();\n 64 | });\n 65 |\n\n at Object.getElementError (node_modules/@testing-library/dom/dist/config.js:37:19)\n at node_modules/@testing-library/dom/dist/query-helpers.js:76:38\n at node_modules/@testing-library/dom/dist/query-helpers.js:52:17\n at node_modules/@testing-library/dom/dist/query-helpers.js:95:19\n at getByText (src/public-auth-presentation.test.tsx:62:19)\n\n ● public auth presentation › translates a failed login fallback while keeping its submitted draft and request count\n\n expect(element).toHaveTextContent()\n\n Expected element to have text content:\n Innlogging mislyktes.\n Received:\n Login failed.\n\n 83 | expect(await screen.findByRole('alert')).toHaveTextContent(translations.en[errorKey]);\n 84 | fireEvent.click(screen.getByRole('button', { name: 'Norsk bokmål' }));\n > 85 | expect(screen.getByRole('alert')).toHaveTextContent(translations.nb[errorKey]);\n | ^\n 86 | expect(mockedApi.post).toHaveBeenCalledTimes(1);\n 87 | expect(screen.getByLabelText(kind === 'login' ? translations.nb.usernameOrEmail : kind === 'request' ? translations.nb.profileEmail : translations.nb.profileNewPassword)).toHaveValue(kind === 'reset' ? 'Synthetic rejected example' : 'synthetic@example.test');\n 88 | });\n\n at toHaveTextContent (src/public-auth-presentation.test.tsx:85:39)\n\n ● public auth presentation › translates a failed request fallback while keeping its submitted draft and request count\n\n expect(element).toHaveTextContent()\n\n Expected element to have text content:\n Kunne ikke sende nullstillingslenken.\n Received:\n Could not send the reset link.\n\n 83 | expect(await screen.findByRole('alert')).toHaveTextContent(translations.en[errorKey]);\n 84 | fireEvent.click(screen.getByRole('button', { name: 'Norsk bokmål' }));\n > 85 | expect(screen.getByRole('alert')).toHaveTextContent(translations.nb[errorKey]);\n | ^\n 86 | expect(mockedApi.post).toHaveBeenCalledTimes(1);\n 87 | expect(screen.getByLabelText(kind === 'login' ? translations.nb.usernameOrEmail : kind === 'request' ? translations.nb.profileEmail : translations.nb.profileNewPassword)).toHaveValue(kind === 'reset' ? 'Synthetic rejected example' : 'synthetic@example.test');\n 88 | });\n\n at toHaveTextContent (src/public-auth-presentation.test.tsx:85:39)\n\n ● public auth presentation › translates a failed reset fallback while keeping its submitted draft and request count\n\n expect(element).toHaveTextContent()\n\n Expected element to have text content:\n Tilbakestilling mislyktes.\n Received:\n Reset failed.\n\n 83 | expect(await screen.findByRole('alert')).toHaveTextContent(translations.en[errorKey]);\n 84 | fireEvent.click(screen.getByRole('button', { name: 'Norsk bokmål' }));\n > 85 | expect(screen.getByRole('alert')).toHaveTextContent(translations.nb[errorKey]);\n | ^\n 86 | expect(mockedApi.post).toHaveBeenCalledTimes(1);\n 87 | expect(screen.getByLabelText(kind === 'login' ? translations.nb.usernameOrEmail : kind === 'request' ? translations.nb.profileEmail : translations.nb.profileNewPassword)).toHaveValue(kind === 'reset' ? 'Synthetic rejected example' : 'synthetic@example.test');\n 88 | });\n\n at toHaveTextContent (src/public-auth-presentation.test.tsx:85:39)\n\n ● public auth presentation › translates an MFA rate limit without another challenge or losing the entered code\n\n expect(element).toHaveTextContent()\n\n Expected element to have text content:\n For mange forsøk. Vent noen minutter og prøv igjen.\n Received:\n Too many attempts. Please wait a few minutes and try again.\n\n 100 | expect(await screen.findByRole('alert')).toHaveTextContent(translations.en.twoFactorRateLimited);\n 101 | fireEvent.click(screen.getByRole('button', { name: 'Norsk bokmål' }));\n > 102 | expect(screen.getByRole('alert')).toHaveTextContent(translations.nb.twoFactorRateLimited);\n | ^\n 103 | expect(screen.getByLabelText(translations.nb.twoFactorCodeLabel)).toHaveValue('000000');\n 104 | expect(mockedApi.post).toHaveBeenCalledTimes(2);\n 105 | expect(mockNavigate).not.toHaveBeenCalled();\n\n at Object.toHaveTextContent (src/public-auth-presentation.test.tsx:102:39)\n","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\public-auth-presentation.test.tsx","startTime":1793751865100,"status":"failed","summary":""}],"wasInterrupted":false} diff --git a/docs/ui-modernisation/visual-correction/PUBLIC_LOCALE.md b/docs/ui-modernisation/visual-correction/PUBLIC_LOCALE.md new file mode 100644 index 00000000..c032daf3 --- /dev/null +++ b/docs/ui-modernisation/visual-correction/PUBLIC_LOCALE.md @@ -0,0 +1,11 @@ +# Public sign-in and recovery language + +4 November 2026. Successor to account/editor receipt `368e90d57206df00204f16ced78d9379706a6878`. Existing AuthPageFrame now exposes the established EN/NB control across login, registration, password recovery/reset, verification, email-change confirmation and Microsoft legacy recovery. Public selection changes browser preference and document language without authenticated preference requests. Authenticated preference saves and server hydration retain their existing owners. No new dependency, server endpoint, token contract or provider is introduced. + +Locale changes preserve drafts. Client validation stores finite translation keys; fallback errors are formatted using the current locale while retaining the original server error. Pending success feedback uses the latest translator. Verification commands depend on their command flow rather than translated function identity, and ignore stale completion after a flow change. This prevents language-triggered duplicate POSTs; it does not promise deduplication across independent remounts or replace server-side one-time-token enforcement. + +PASSED:94 selected frontend cases/eight suites, TypeScript, full lint and static export build. Independent read-only source review found no actionable findings and performed no tests. Retained RED reports reproduce duplicate verification, stale validation/fallback/MFA errors, stale asynchronous feedback and missing local-only save behavior. The320px pilot passed14 accessibility scans/captures and one synthetic journey after correcting an ambiguous verifier alert locator: application alerts are scoped to public main, leaving the Next route announcer intact. Original locator-failure report is retained. Full nine-width EN/NB/light/dark matrix remains pending at this source checkpoint. + +New dynamic translation review (separate from188 historical reviews): Login render keys are assigned only by local validation to `usernameOrEmailRequired`, `emailRequired`, `invalidEmail`, `passwordRequired`, `passwordTooShort`, `passwordsDoNotMatch`; submit fallback is only `loginFailed` or `verifyEmailFailed`. Forgot-password email key is only `emailRequired` or `invalidEmail`; reset-password keys are only `passwordTooShort` or `passwordsDoNotMatch`. Verification error selection is the literal pair `missingEmailChangeLinkInfo` / `missingVerifyLinkInfo`; existing title/progress/success pairs remain finite. No API text or user input is used as a translation key. TranslationKey typing, current-locale regression cases and build validate catalogue membership. Generated-content language is unchanged. + +Limits: synthetic intercepted responses only; no real sign-in, reset, verification, email delivery, MFA enforcement, SSO, provider or model activation. Full route visual acceptance, human/native/device/accessibility, integrated security and release gates remain open. Public homepage claims and final launch appearance are separate. diff --git a/docs/ui-modernisation/visual-correction/PUBLIC_LOCALE_FINAL_TESTS.json b/docs/ui-modernisation/visual-correction/PUBLIC_LOCALE_FINAL_TESTS.json new file mode 100644 index 00000000..77e46573 --- /dev/null +++ b/docs/ui-modernisation/visual-correction/PUBLIC_LOCALE_FINAL_TESTS.json @@ -0,0 +1 @@ +{"numFailedTestSuites":0,"numFailedTests":0,"numPassedTestSuites":8,"numPassedTests":94,"numPendingTestSuites":0,"numPendingTests":0,"numRuntimeErrorTestSuites":0,"numTodoTests":0,"numTotalTestSuites":8,"numTotalTests":94,"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":1793752766816,"success":true,"testResults":[{"assertionResults":[{"ancestorTitles":["LoginPage"],"duration":133,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage provides a main landmark and level-one heading for login","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752770201,"status":"passed","title":"provides a main landmark and level-one heading for login"},{"ancestorTitles":["LoginPage"],"duration":49,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage provides a main landmark and level-one heading for register","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752770335,"status":"passed","title":"provides a main landmark and level-one heading for register"},{"ancestorTitles":["LoginPage"],"duration":73,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage renders a dedicated registration screen and explains when registration is disabled","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793752770384,"status":"passed","title":"renders a dedicated registration screen and explains when registration is disabled"},{"ancestorTitles":["LoginPage"],"duration":75,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage renders one conventional sign-in card with enabled provider alternatives","invocations":1,"location":null,"numPassingAsserts":9,"retryReasons":[],"startAt":1793752770457,"status":"passed","title":"renders one conventional sign-in card with enabled provider alternatives"},{"ancestorTitles":["LoginPage"],"duration":416,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage accepts a username and reports invalid credentials without changing routes","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752770533,"status":"passed","title":"accepts a username and reports invalid credentials without changing routes"},{"ancestorTitles":["LoginPage"],"duration":92,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage handles Microsoft cancellation without exchanging or navigating","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752770950,"status":"passed","title":"handles Microsoft cancellation without exchanging or navigating"},{"ancestorTitles":["LoginPage"],"duration":92,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage completes the Microsoft provider return through the hardened exchange endpoint","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793752771042,"status":"passed","title":"completes the Microsoft provider return through the hardened exchange endpoint"},{"ancestorTitles":["LoginPage"],"duration":94,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage completes a Google credential return without account-linking copy","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793752771134,"status":"passed","title":"completes a Google credential return without account-linking copy"},{"ancestorTitles":["LoginPage"],"duration":494,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage posts remember-me preference without storing an auth token in browser storage","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"startAt":1793752771228,"status":"passed","title":"posts remember-me preference without storing an auth token in browser storage"},{"ancestorTitles":["LoginPage"],"duration":373,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage does not complete login when registration requires email verification","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752771722,"status":"passed","title":"does not complete login when registration requires email verification"},{"ancestorTitles":["LoginPage"],"duration":153,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage rejects an unsafe post-login redirect path","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752772096,"status":"passed","title":"rejects an unsafe post-login redirect path"},{"ancestorTitles":["LoginPage"],"duration":557,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"https://evil.example/analytics\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752772249,"status":"passed","title":"uses the safe fallback for the untrusted return target \"https://evil.example/analytics\""},{"ancestorTitles":["LoginPage"],"duration":309,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"//evil.example/analytics\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752772806,"status":"passed","title":"uses the safe fallback for the untrusted return target \"//evil.example/analytics\""},{"ancestorTitles":["LoginPage"],"duration":185,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"/\\t/evil.example\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752773115,"status":"passed","title":"uses the safe fallback for the untrusted return target \"/\\t/evil.example\""},{"ancestorTitles":["LoginPage"],"duration":604,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"/%2f%2fevil.example\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752773300,"status":"passed","title":"uses the safe fallback for the untrusted return target \"/%2f%2fevil.example\""},{"ancestorTitles":["LoginPage"],"duration":278,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"/%252f%252fevil.example\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752773904,"status":"passed","title":"uses the safe fallback for the untrusted return target \"/%252f%252fevil.example\""},{"ancestorTitles":["LoginPage"],"duration":482,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"/%5cevil.example\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752774182,"status":"passed","title":"uses the safe fallback for the untrusted return target \"/%5cevil.example\""},{"ancestorTitles":["LoginPage"],"duration":479,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"/analytics?months=%ZZ\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752774664,"status":"passed","title":"uses the safe fallback for the untrusted return target \"/analytics?months=%ZZ\""},{"ancestorTitles":["LoginPage"],"duration":262,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"/jobs/../login\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752775143,"status":"passed","title":"uses the safe fallback for the untrusted return target \"/jobs/../login\""},{"ancestorTitles":["LoginPage"],"duration":614,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"/%6cogin\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752775406,"status":"passed","title":"uses the safe fallback for the untrusted return target \"/%6cogin\""},{"ancestorTitles":["LoginPage"],"duration":201,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"/register\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752776020,"status":"passed","title":"uses the safe fallback for the untrusted return target \"/register\""},{"ancestorTitles":["LoginPage"],"duration":559,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"/analytics%0a\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752776221,"status":"passed","title":"uses the safe fallback for the untrusted return target \"/analytics%0a\""},{"ancestorTitles":["LoginPage"],"duration":353,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"/analytics?months=6\\n\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752776780,"status":"passed","title":"uses the safe fallback for the untrusted return target \"/analytics?months=6\\n\""},{"ancestorTitles":["LoginPage"],"duration":215,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"/LOGIN\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752777134,"status":"passed","title":"uses the safe fallback for the untrusted return target \"/LOGIN\""},{"ancestorTitles":["LoginPage"],"duration":584,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"/%4cOGIN\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752777349,"status":"passed","title":"uses the safe fallback for the untrusted return target \"/%4cOGIN\""},{"ancestorTitles":["LoginPage"],"duration":264,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage preserves encoded internal query and fragment without decoding the navigation target","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752777933,"status":"passed","title":"preserves encoded internal query and fragment without decoding the navigation target"},{"ancestorTitles":["LoginPage"],"duration":431,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage restores valid owned-workspace navigation context separately from the sign-in destination","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752778197,"status":"passed","title":"restores valid owned-workspace navigation context separately from the sign-in destination"},{"ancestorTitles":["LoginPage"],"duration":570,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage does not replay an unsafe nested return context after authentication","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752778628,"status":"passed","title":"does not replay an unsafe nested return context after authentication"},{"ancestorTitles":["LoginPage"],"duration":496,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage abandons prior return context on the established account/session disposal entry","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752779198,"status":"passed","title":"abandons prior return context on the established account/session disposal entry"},{"ancestorTitles":["LoginPage"],"duration":509,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage opens the separate forgot-password page with the typed email prefilled","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752779694,"status":"passed","title":"opens the separate forgot-password page with the typed email prefilled"},{"ancestorTitles":["LoginPage"],"duration":710,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage shows the 2FA code step when login requires two-factor, then proceeds like a normal login on success","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"startAt":1793752780203,"status":"passed","title":"shows the 2FA code step when login requires two-factor, then proceeds like a normal login on success"},{"ancestorTitles":["LoginPage"],"duration":864,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage shows a clear message when the 2FA challenge is rate-limited","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752780913,"status":"passed","title":"shows a clear message when the 2FA challenge is rate-limited"},{"ancestorTitles":["LoginPage"],"duration":4604,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage offers a resend-verification action when login reports the account is not verified","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793752781777,"status":"passed","title":"offers a resend-verification action when login reports the account is not verified"}],"endTime":1793752786382,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\login-page.test.tsx","startTime":1793752767691,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":["public auth presentation"],"duration":191,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation announces a pending login result in the current UI language without replay","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793752787135,"status":"passed","title":"announces a pending login result in the current UI language without replay"},{"ancestorTitles":["public auth presentation"],"duration":79,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation announces a pending request result in the current UI language without replay","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793752787326,"status":"passed","title":"announces a pending request result in the current UI language without replay"},{"ancestorTitles":["public auth presentation"],"duration":125,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation announces a pending reset result in the current UI language without replay","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793752787405,"status":"passed","title":"announces a pending reset result in the current UI language without replay"},{"ancestorTitles":["public auth presentation"],"duration":100,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation translates existing client validation on /login without submitting again","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752787530,"status":"passed","title":"translates existing client validation on /login without submitting again"},{"ancestorTitles":["public auth presentation"],"duration":91,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation translates existing client validation on /register without submitting again","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752787630,"status":"passed","title":"translates existing client validation on /register without submitting again"},{"ancestorTitles":["public auth presentation"],"duration":42,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation translates existing client validation on /forgot-password without submitting again","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752787721,"status":"passed","title":"translates existing client validation on /forgot-password without submitting again"},{"ancestorTitles":["public auth presentation"],"duration":70,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation translates existing client validation on /reset-password?email=synthetic%40example.test&token=synthetic without submitting again","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752787763,"status":"passed","title":"translates existing client validation on /reset-password?email=synthetic%40example.test&token=synthetic without submitting again"},{"ancestorTitles":["public auth presentation"],"duration":157,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation translates a failed login fallback while keeping its submitted draft and request count","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793752787833,"status":"passed","title":"translates a failed login fallback while keeping its submitted draft and request count"},{"ancestorTitles":["public auth presentation"],"duration":81,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation translates a failed request fallback while keeping its submitted draft and request count","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793752787990,"status":"passed","title":"translates a failed request fallback while keeping its submitted draft and request count"},{"ancestorTitles":["public auth presentation"],"duration":120,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation translates a failed reset fallback while keeping its submitted draft and request count","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793752788071,"status":"passed","title":"translates a failed reset fallback while keeping its submitted draft and request count"},{"ancestorTitles":["public auth presentation"],"duration":92,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation translates an MFA rate limit without another challenge or losing the entered code","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"startAt":1793752788191,"status":"passed","title":"translates an MFA rate limit without another challenge or losing the entered code"},{"ancestorTitles":["public auth presentation"],"duration":64,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation lets a signed-out user choose a persisted UI language without discarding a sign-in draft","invocations":1,"location":null,"numPassingAsserts":7,"retryReasons":[],"startAt":1793752788283,"status":"passed","title":"lets a signed-out user choose a persisted UI language without discarding a sign-in draft"},{"ancestorTitles":["public auth presentation"],"duration":32,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation names the password field for login intent and retains its autocomplete contract","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752788347,"status":"passed","title":"names the password field for login intent and retains its autocomplete contract"},{"ancestorTitles":["public auth presentation"],"duration":30,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation names the password field for register intent and retains its autocomplete contract","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752788380,"status":"passed","title":"names the password field for register intent and retains its autocomplete contract"},{"ancestorTitles":["public auth presentation"],"duration":31,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation provides one named main landmark and H1 for password request","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752788410,"status":"passed","title":"provides one named main landmark and H1 for password request"},{"ancestorTitles":["public auth presentation"],"duration":31,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation provides one named main landmark and H1 for password reset","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752788441,"status":"passed","title":"provides one named main landmark and H1 for password reset"},{"ancestorTitles":["public auth presentation"],"duration":30,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation provides one named main landmark and H1 for email verification","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752788472,"status":"passed","title":"provides one named main landmark and H1 for email verification"},{"ancestorTitles":["public auth presentation"],"duration":31,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation provides one named main landmark and H1 for email change","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752788502,"status":"passed","title":"provides one named main landmark and H1 for email change"},{"ancestorTitles":["public auth presentation"],"duration":18,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation provides one named main landmark and H1 for Microsoft recovery","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752788534,"status":"passed","title":"provides one named main landmark and H1 for Microsoft recovery"},{"ancestorTitles":["public auth presentation"],"duration":26,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation announces the pending confirmation without an unnamed progressbar: false","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752788552,"status":"passed","title":"announces the pending confirmation without an unnamed progressbar: false"},{"ancestorTitles":["public auth presentation"],"duration":30,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation announces the pending confirmation without an unnamed progressbar: true","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752788578,"status":"passed","title":"announces the pending confirmation without an unnamed progressbar: true"},{"ancestorTitles":["public auth presentation"],"duration":68,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation retains the entered reset email and a durable error in the page when the command fails","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"startAt":1793752788608,"status":"passed","title":"retains the entered reset email and a durable error in the page when the command fails"},{"ancestorTitles":["public auth presentation"],"duration":105,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation retains both new-password fields and the decoded recovery token after a rejected reset","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"startAt":1793752788676,"status":"passed","title":"retains both new-password fields and the decoded recovery token after a rejected reset"},{"ancestorTitles":["public auth presentation"],"duration":106,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation keeps a rejected local sign-in beside the retained fields without navigating","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793752788781,"status":"passed","title":"keeps a rejected local sign-in beside the retained fields without navigating"},{"ancestorTitles":["public auth presentation"],"duration":76,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation keeps the reset-request action name while announcing an in-flight command","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752788887,"status":"passed","title":"keeps the reset-request action name while announcing an in-flight command"},{"ancestorTitles":["public auth presentation"],"duration":139,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation announces an in-flight local sign-in while preserving its action name","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752788963,"status":"passed","title":"announces an in-flight local sign-in while preserving its action name"},{"ancestorTitles":["public auth presentation"],"duration":138,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation announces a pending password save without changing its payload or successful destination","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793752789103,"status":"passed","title":"announces a pending password save without changing its payload or successful destination"},{"ancestorTitles":["public auth presentation"],"duration":32,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation focuses the first invalid local-login field and associates its explanation","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752789241,"status":"passed","title":"focuses the first invalid local-login field and associates its explanation"},{"ancestorTitles":["public auth presentation"],"duration":33,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation focuses the invalid new password without losing confirmation input","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"startAt":1793752789273,"status":"passed","title":"focuses the invalid new password without losing confirmation input"},{"ancestorTitles":["public auth presentation"],"duration":18,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation focuses and explains the invalid reset-request email without sending a command","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752789306,"status":"passed","title":"focuses and explains the invalid reset-request email without sending a command"},{"ancestorTitles":["public auth presentation"],"duration":45,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation replaces previous reset-request success when a later request fails","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793752789325,"status":"passed","title":"replaces previous reset-request success when a later request fails"},{"ancestorTitles":["public auth presentation"],"duration":25,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation does not assert optional authentication while login configuration is unresolved","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752789370,"status":"passed","title":"does not assert optional authentication while login configuration is unresolved"},{"ancestorTitles":["public auth presentation"],"duration":48,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation does not assert optional authentication while register configuration is unresolved","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752789395,"status":"passed","title":"does not assert optional authentication while register configuration is unresolved"},{"ancestorTitles":["public auth presentation"],"duration":134,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation offers a read-only configuration retry while retaining entered sign-in fields","invocations":1,"location":null,"numPassingAsserts":9,"retryReasons":[],"startAt":1793752789443,"status":"passed","title":"offers a read-only configuration retry while retaining entered sign-in fields"}],"endTime":1793752789578,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\public-auth-presentation.test.tsx","startTime":1793752786397,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":[],"duration":14,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"migrates the legacy Norwegian code and persists Bokmål using nb-NO","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"startAt":1793752790338,"status":"passed","title":"migrates the legacy Norwegian code and persists Bokmål using nb-NO"},{"ancestorTitles":[],"duration":7,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"hydrates an authenticated server preference without writing it back","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752790352,"status":"passed","title":"hydrates an authenticated server preference without writing it back"},{"ancestorTitles":[],"duration":6,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"a public local-only choice persists without authenticated requests","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"startAt":1793752790359,"status":"passed","title":"a public local-only choice persists without authenticated requests"},{"ancestorTitles":[],"duration":14,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"a local-only choice invalidates an older same-language account-save retry","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752790365,"status":"passed","title":"a local-only choice invalidates an older same-language account-save retry"},{"ancestorTitles":[],"duration":300,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"a failed account language save keeps the local choice and offers a localized retry: Norsk","invocations":1,"location":null,"numPassingAsserts":11,"retryReasons":[],"startAt":1793752790379,"status":"passed","title":"a failed account language save keeps the local choice and offers a localized retry: Norsk"},{"ancestorTitles":[],"duration":249,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"a failed account language save keeps the local choice and offers a localized retry: English","invocations":1,"location":null,"numPassingAsserts":11,"retryReasons":[],"startAt":1793752790679,"status":"passed","title":"a failed account language save keeps the local choice and offers a localized retry: English"},{"ancestorTitles":[],"duration":9,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"a delayed language-save failure cannot offer a retry for a newer successful choice","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752790928,"status":"passed","title":"a delayed language-save failure cannot offer a retry for a newer successful choice"},{"ancestorTitles":[],"duration":36,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"a previously delivered language retry cannot reverse a newer choice","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752790937,"status":"passed","title":"a previously delivered language retry cannot reverse a newer choice"}],"endTime":1793752790973,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\i18n-provider.test.tsx","startTime":1793752789587,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":[],"duration":373,"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":1793752791687,"status":"passed","title":"cancelling setup sends no security write and clears the password before reopening"},{"ancestorTitles":[],"duration":499,"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":1793752792060,"status":"passed","title":"regenerated recovery codes require acknowledgement, survive Escape, export correctly, and clear after Done"},{"ancestorTitles":[],"duration":136,"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":1793752792559,"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":64,"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":1793752792695,"status":"passed","title":"a failed sessions read offers a read-only retry without inventing an empty session list"},{"ancestorTitles":[],"duration":332,"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":1793752792759,"status":"passed","title":"pending revoke-others disables confirmation and cancellation, blocks duplicate writes, and reads back the surviving session"}],"endTime":1793752793091,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\account-security-flows.test.tsx","startTime":1793752790982,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":["VerifyEmailPage"],"duration":38,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"VerifyEmailPage confirms the account and shows success when the link is valid","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793752793841,"status":"passed","title":"confirms the account and shows success when the link is valid"},{"ancestorTitles":["VerifyEmailPage"],"duration":24,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"VerifyEmailPage shows an error when the link is invalid or expired","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752793879,"status":"passed","title":"shows an error when the link is invalid or expired"},{"ancestorTitles":["VerifyEmailPage"],"duration":21,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"VerifyEmailPage shows an error without calling the API when the link is missing userId/token","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793752793903,"status":"passed","title":"shows an error without calling the API when the link is missing userId/token"},{"ancestorTitles":["VerifyEmailPage"],"duration":31,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"VerifyEmailPage confirms a pending email change and requires a fresh sign-in","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793752793924,"status":"passed","title":"confirms a pending email change and requires a fresh sign-in"},{"ancestorTitles":["VerifyEmailPage"],"duration":43,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"VerifyEmailPage changing UI language never resubmits a pending confirmation: emailChange=false","invocations":1,"location":null,"numPassingAsserts":6,"retryReasons":[],"startAt":1793752793955,"status":"passed","title":"changing UI language never resubmits a pending confirmation: emailChange=false"},{"ancestorTitles":["VerifyEmailPage"],"duration":28,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"VerifyEmailPage changing UI language never resubmits a pending confirmation: emailChange=true","invocations":1,"location":null,"numPassingAsserts":6,"retryReasons":[],"startAt":1793752793998,"status":"passed","title":"changing UI language never resubmits a pending confirmation: emailChange=true"},{"ancestorTitles":["VerifyEmailPage"],"duration":29,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"VerifyEmailPage localizes missing-link errors without a confirmation request","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752794026,"status":"passed","title":"localizes missing-link errors without a confirmation request"},{"ancestorTitles":["VerifyEmailPage"],"duration":35,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"VerifyEmailPage localizes a failed confirmation fallback without retrying the one-time command","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752794055,"status":"passed","title":"localizes a failed confirmation fallback without retrying the one-time command"},{"ancestorTitles":["VerifyEmailPage"],"duration":37,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"VerifyEmailPage does not let an older confirmation replace a different flow missing-link error","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752794090,"status":"passed","title":"does not let an older confirmation replace a different flow missing-link error"}],"endTime":1793752794128,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\verify-email-page.test.tsx","startTime":1793752793099,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":[],"duration":129,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"feedback and locale changes translate company presentation without reloading company data","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793752794870,"status":"passed","title":"feedback and locale changes translate company presentation without reloading company data"}],"endTime":1793752795000,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\locale-fetch-stability.test.tsx","startTime":1793752794135,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":[],"duration":81,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"legacy Microsoft recovery requires the emailed proof and a fresh matching Microsoft token","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793752795664,"status":"passed","title":"legacy Microsoft recovery requires the emailed proof and a fresh matching Microsoft token"},{"ancestorTitles":[],"duration":63,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"pending Microsoft recovery keeps a named action and announces progress before the real confirmation boundary","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"startAt":1793752795745,"status":"passed","title":"pending Microsoft recovery keeps a named action and announces progress before the real confirmation boundary"}],"endTime":1793752795809,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\microsoft-legacy-relink-page.test.tsx","startTime":1793752795007,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":[],"duration":58,"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":1793752796531,"status":"passed","title":"[PRES-E08] page and nested section headings retain full names and independent actions"},{"ancestorTitles":[],"duration":46,"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":1793752796589,"status":"passed","title":"[PRES-E08] section composition retains loaded, error/retry and empty child semantics"}],"endTime":1793752796635,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\page-layout.test.tsx","startTime":1793752795816,"status":"passed","summary":""}],"wasInterrupted":false} diff --git a/docs/ui-modernisation/visual-correction/PUBLIC_LOCALE_RED.json b/docs/ui-modernisation/visual-correction/PUBLIC_LOCALE_RED.json new file mode 100644 index 00000000..50a6dd0c --- /dev/null +++ b/docs/ui-modernisation/visual-correction/PUBLIC_LOCALE_RED.json @@ -0,0 +1 @@ +{"numFailedTestSuites":1,"numFailedTests":4,"numPassedTestSuites":0,"numPassedTests":5,"numPendingTestSuites":0,"numPendingTests":0,"numRuntimeErrorTestSuites":0,"numTodoTests":0,"numTotalTestSuites":1,"numTotalTests":9,"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":1793751511232,"success":false,"testResults":[{"assertionResults":[{"ancestorTitles":["VerifyEmailPage"],"duration":67,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"VerifyEmailPage confirms the account and shows success when the link is valid","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793751514518,"status":"passed","title":"confirms the account and shows success when the link is valid"},{"ancestorTitles":["VerifyEmailPage"],"duration":28,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"VerifyEmailPage shows an error when the link is invalid or expired","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793751514586,"status":"passed","title":"shows an error when the link is invalid or expired"},{"ancestorTitles":["VerifyEmailPage"],"duration":14,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"VerifyEmailPage shows an error without calling the API when the link is missing userId/token","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793751514615,"status":"passed","title":"shows an error without calling the API when the link is missing userId/token"},{"ancestorTitles":["VerifyEmailPage"],"duration":31,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"VerifyEmailPage confirms a pending email change and requires a fresh sign-in","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793751514629,"status":"passed","title":"confirms a pending email change and requires a fresh sign-in"},{"ancestorTitles":["VerifyEmailPage"],"duration":16,"failing":false,"failureDetails":[{"matcherResult":{"actual":2,"expected":1,"message":"expect(received).toBe(expected) // Object.is equality\n\nExpected: 1\nReceived: 2","name":"toBe","pass":false}}],"failureMessages":["Error: expect(received).toBe(expected) // Object.is equality\n\nExpected: 1\nReceived: 2\n at toBe (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\verify-email-page.test.tsx:79:26)"],"fullName":"VerifyEmailPage changing UI language never resubmits a pending confirmation: emailChange=false","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793751514660,"status":"failed","title":"changing UI language never resubmits a pending confirmation: emailChange=false"},{"ancestorTitles":["VerifyEmailPage"],"duration":13,"failing":false,"failureDetails":[{"matcherResult":{"actual":2,"expected":1,"message":"expect(received).toBe(expected) // Object.is equality\n\nExpected: 1\nReceived: 2","name":"toBe","pass":false}}],"failureMessages":["Error: expect(received).toBe(expected) // Object.is equality\n\nExpected: 1\nReceived: 2\n at toBe (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\verify-email-page.test.tsx:79:26)"],"fullName":"VerifyEmailPage changing UI language never resubmits a pending confirmation: emailChange=true","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793751514681,"status":"failed","title":"changing UI language never resubmits a pending confirmation: emailChange=true"},{"ancestorTitles":["VerifyEmailPage"],"duration":47,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"VerifyEmailPage localizes missing-link errors without a confirmation request","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793751514695,"status":"passed","title":"localizes missing-link errors without a confirmation request"},{"ancestorTitles":["VerifyEmailPage"],"duration":36,"failing":false,"failureDetails":[{"matcherResult":{"message":"expect(jest.fn()).toHaveBeenCalledTimes(expected)\n\nExpected number of calls: 1\nReceived number of calls: 2","pass":false}}],"failureMessages":["Error: expect(jest.fn()).toHaveBeenCalledTimes(expected)\n\nExpected number of calls: 1\nReceived number of calls: 2\n at Object.toHaveBeenCalledTimes (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\verify-email-page.test.tsx:101:28)"],"fullName":"VerifyEmailPage localizes a failed confirmation fallback without retrying the one-time command","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793751514742,"status":"failed","title":"localizes a failed confirmation fallback without retrying the one-time command"},{"ancestorTitles":["VerifyEmailPage"],"duration":50,"failing":false,"failureDetails":[{"name":"TestingLibraryElementError"}],"failureMessages":["TestingLibraryElementError: Unable to find an accessible element with the role \"alert\"\n\nHere are the accessible roles:\n\n main:\n\n Name \"Confirm your new email\":\n \u001b[36m\u001b[39m\n\n --------------------------------------------------\n banner:\n\n Name \"\":\n \u001b[36m\u001b[39m\n\n --------------------------------------------------\n heading:\n\n Name \"Confirm your new email\":\n \u001b[36m\u001b[39m\n\n --------------------------------------------------\n status:\n\n Name \"\":\n \u001b[36m\u001b[39m\n\n --------------------------------------------------\n button:\n\n Name \"Back to login\":\n \u001b[36m\u001b[39m\n\n --------------------------------------------------\n\nIgnored nodes: comments, script, style\n\u001b[36m\u001b[39m\n \u001b[36m
\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mConfirm your new email\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m
\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mYour email has changed. Sign in again with the new address.\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mBack to login\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n\u001b[36m\u001b[39m\n at Object.getElementError (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\dom\\dist\\config.js:37:19)\n at C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\dom\\dist\\query-helpers.js:76:38\n at C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\dom\\dist\\query-helpers.js:52:17\n at C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\dom\\dist\\query-helpers.js:95:19\n at Object.getByRole (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\verify-email-page.test.tsx:112:19)"],"fullName":"VerifyEmailPage does not let an older confirmation replace a different flow missing-link error","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793751514778,"status":"failed","title":"does not let an older confirmation replace a different flow missing-link error"}],"endTime":1793751514845,"message":" ● VerifyEmailPage › changing UI language never resubmits a pending confirmation: emailChange=false\n\n expect(received).toBe(expected) // Object.is equality\n\n Expected: 1\n Received: 2\n\n 77 | const pendingCalls = mockedApi.post.mock.calls.length;\n 78 | await act(async () => finish({ data: {} }));\n > 79 | expect(pendingCalls).toBe(1);\n | ^\n 80 | expect(mockedApi.post).toHaveBeenCalledTimes(1);\n 81 | expect(screen.getByRole('status')).toHaveTextContent(translations.nb[emailChange ? 'confirmEmailChangeSuccess' : 'verifyEmailSuccess']);\n 82 | act(() => window.dispatchEvent(new StorageEvent('storage', { key: 'uiLanguage', newValue: 'en' })));\n\n at toBe (src/verify-email-page.test.tsx:79:26)\n\n ● VerifyEmailPage › changing UI language never resubmits a pending confirmation: emailChange=true\n\n expect(received).toBe(expected) // Object.is equality\n\n Expected: 1\n Received: 2\n\n 77 | const pendingCalls = mockedApi.post.mock.calls.length;\n 78 | await act(async () => finish({ data: {} }));\n > 79 | expect(pendingCalls).toBe(1);\n | ^\n 80 | expect(mockedApi.post).toHaveBeenCalledTimes(1);\n 81 | expect(screen.getByRole('status')).toHaveTextContent(translations.nb[emailChange ? 'confirmEmailChangeSuccess' : 'verifyEmailSuccess']);\n 82 | act(() => window.dispatchEvent(new StorageEvent('storage', { key: 'uiLanguage', newValue: 'en' })));\n\n at toBe (src/verify-email-page.test.tsx:79:26)\n\n ● VerifyEmailPage › localizes a failed confirmation fallback without retrying the one-time command\n\n expect(jest.fn()).toHaveBeenCalledTimes(expected)\n\n Expected number of calls: 1\n Received number of calls: 2\n\n 99 | await act(async () => window.dispatchEvent(new StorageEvent('storage', { key: 'uiLanguage', newValue: 'nb' })));\n 100 | expect(screen.getByRole('alert')).toHaveTextContent(translations.nb.verifyEmailFailed);\n > 101 | expect(mockedApi.post).toHaveBeenCalledTimes(1);\n | ^\n 102 | });\n 103 |\n 104 | it('does not let an older confirmation replace a different flow missing-link error', async () => {\n\n at Object.toHaveBeenCalledTimes (src/verify-email-page.test.tsx:101:28)\n\n ● VerifyEmailPage › does not let an older confirmation replace a different flow missing-link error\n\n TestingLibraryElementError: Unable to find an accessible element with the role \"alert\"\n\n Here are the accessible roles:\n\n main:\n\n Name \"Confirm your new email\":\n \u001b[36m\u001b[39m\n\n --------------------------------------------------\n banner:\n\n Name \"\":\n \u001b[36m\u001b[39m\n\n --------------------------------------------------\n heading:\n\n Name \"Confirm your new email\":\n \u001b[36m\u001b[39m\n\n --------------------------------------------------\n status:\n\n Name \"\":\n \u001b[36m\u001b[39m\n\n --------------------------------------------------\n button:\n\n Name \"Back to login\":\n \u001b[36m\u001b[39m\n\n --------------------------------------------------\n\n Ignored nodes: comments, script, style\n \u001b[36m\u001b[39m\n \u001b[36m
\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mConfirm your new email\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m
\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mYour email has changed. Sign in again with the new address.\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mBack to login\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n\n 110 | expect(await screen.findByText(translations.en.missingEmailChangeLinkInfo)).toBeInTheDocument();\n 111 | await act(async () => finish({ data: {} }));\n > 112 | expect(screen.getByRole('alert')).toHaveTextContent(translations.en.missingEmailChangeLinkInfo);\n | ^\n 113 | expect(mockedApi.post).toHaveBeenCalledTimes(1);\n 114 | });\n 115 | });\n\n at Object.getElementError (node_modules/@testing-library/dom/dist/config.js:37:19)\n at node_modules/@testing-library/dom/dist/query-helpers.js:76:38\n at node_modules/@testing-library/dom/dist/query-helpers.js:52:17\n at node_modules/@testing-library/dom/dist/query-helpers.js:95:19\n at Object.getByRole (src/verify-email-page.test.tsx:112:19)\n","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\verify-email-page.test.tsx","startTime":1793751512139,"status":"failed","summary":""}],"wasInterrupted":false} diff --git a/docs/ui-modernisation/visual-correction/PUBLIC_LOCALE_TESTS.json b/docs/ui-modernisation/visual-correction/PUBLIC_LOCALE_TESTS.json new file mode 100644 index 00000000..bdda0f93 --- /dev/null +++ b/docs/ui-modernisation/visual-correction/PUBLIC_LOCALE_TESTS.json @@ -0,0 +1 @@ +{"numFailedTestSuites":0,"numFailedTests":0,"numPassedTestSuites":5,"numPassedTests":80,"numPendingTestSuites":0,"numPendingTests":0,"numRuntimeErrorTestSuites":0,"numTodoTests":0,"numTotalTestSuites":5,"numTotalTests":80,"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":1793752013400,"success":true,"testResults":[{"assertionResults":[{"ancestorTitles":["public auth presentation"],"duration":279,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation announces a pending login result in the current UI language without replay","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793752016931,"status":"passed","title":"announces a pending login result in the current UI language without replay"},{"ancestorTitles":["public auth presentation"],"duration":83,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation announces a pending request result in the current UI language without replay","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793752017211,"status":"passed","title":"announces a pending request result in the current UI language without replay"},{"ancestorTitles":["public auth presentation"],"duration":112,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation announces a pending reset result in the current UI language without replay","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793752017294,"status":"passed","title":"announces a pending reset result in the current UI language without replay"},{"ancestorTitles":["public auth presentation"],"duration":85,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation translates existing client validation on /login without submitting again","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752017406,"status":"passed","title":"translates existing client validation on /login without submitting again"},{"ancestorTitles":["public auth presentation"],"duration":74,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation translates existing client validation on /register without submitting again","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752017491,"status":"passed","title":"translates existing client validation on /register without submitting again"},{"ancestorTitles":["public auth presentation"],"duration":38,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation translates existing client validation on /forgot-password without submitting again","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752017565,"status":"passed","title":"translates existing client validation on /forgot-password without submitting again"},{"ancestorTitles":["public auth presentation"],"duration":68,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation translates existing client validation on /reset-password?email=synthetic%40example.test&token=synthetic without submitting again","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752017604,"status":"passed","title":"translates existing client validation on /reset-password?email=synthetic%40example.test&token=synthetic without submitting again"},{"ancestorTitles":["public auth presentation"],"duration":106,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation translates a failed login fallback while keeping its submitted draft and request count","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793752017672,"status":"passed","title":"translates a failed login fallback while keeping its submitted draft and request count"},{"ancestorTitles":["public auth presentation"],"duration":38,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation translates a failed request fallback while keeping its submitted draft and request count","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793752017778,"status":"passed","title":"translates a failed request fallback while keeping its submitted draft and request count"},{"ancestorTitles":["public auth presentation"],"duration":64,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation translates a failed reset fallback while keeping its submitted draft and request count","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793752017816,"status":"passed","title":"translates a failed reset fallback while keeping its submitted draft and request count"},{"ancestorTitles":["public auth presentation"],"duration":127,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation translates an MFA rate limit without another challenge or losing the entered code","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"startAt":1793752017881,"status":"passed","title":"translates an MFA rate limit without another challenge or losing the entered code"},{"ancestorTitles":["public auth presentation"],"duration":96,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation lets a signed-out user choose a persisted UI language without discarding a sign-in draft","invocations":1,"location":null,"numPassingAsserts":6,"retryReasons":[],"startAt":1793752018008,"status":"passed","title":"lets a signed-out user choose a persisted UI language without discarding a sign-in draft"},{"ancestorTitles":["public auth presentation"],"duration":42,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation names the password field for login intent and retains its autocomplete contract","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752018104,"status":"passed","title":"names the password field for login intent and retains its autocomplete contract"},{"ancestorTitles":["public auth presentation"],"duration":45,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation names the password field for register intent and retains its autocomplete contract","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752018146,"status":"passed","title":"names the password field for register intent and retains its autocomplete contract"},{"ancestorTitles":["public auth presentation"],"duration":32,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation provides one named main landmark and H1 for password request","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752018191,"status":"passed","title":"provides one named main landmark and H1 for password request"},{"ancestorTitles":["public auth presentation"],"duration":32,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation provides one named main landmark and H1 for password reset","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752018223,"status":"passed","title":"provides one named main landmark and H1 for password reset"},{"ancestorTitles":["public auth presentation"],"duration":30,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation provides one named main landmark and H1 for email verification","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752018255,"status":"passed","title":"provides one named main landmark and H1 for email verification"},{"ancestorTitles":["public auth presentation"],"duration":31,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation provides one named main landmark and H1 for email change","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752018285,"status":"passed","title":"provides one named main landmark and H1 for email change"},{"ancestorTitles":["public auth presentation"],"duration":31,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation provides one named main landmark and H1 for Microsoft recovery","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752018316,"status":"passed","title":"provides one named main landmark and H1 for Microsoft recovery"},{"ancestorTitles":["public auth presentation"],"duration":28,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation announces the pending confirmation without an unnamed progressbar: false","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752018347,"status":"passed","title":"announces the pending confirmation without an unnamed progressbar: false"},{"ancestorTitles":["public auth presentation"],"duration":30,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation announces the pending confirmation without an unnamed progressbar: true","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752018375,"status":"passed","title":"announces the pending confirmation without an unnamed progressbar: true"},{"ancestorTitles":["public auth presentation"],"duration":68,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation retains the entered reset email and a durable error in the page when the command fails","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"startAt":1793752018406,"status":"passed","title":"retains the entered reset email and a durable error in the page when the command fails"},{"ancestorTitles":["public auth presentation"],"duration":104,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation retains both new-password fields and the decoded recovery token after a rejected reset","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"startAt":1793752018474,"status":"passed","title":"retains both new-password fields and the decoded recovery token after a rejected reset"},{"ancestorTitles":["public auth presentation"],"duration":91,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation keeps a rejected local sign-in beside the retained fields without navigating","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793752018578,"status":"passed","title":"keeps a rejected local sign-in beside the retained fields without navigating"},{"ancestorTitles":["public auth presentation"],"duration":76,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation keeps the reset-request action name while announcing an in-flight command","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752018669,"status":"passed","title":"keeps the reset-request action name while announcing an in-flight command"},{"ancestorTitles":["public auth presentation"],"duration":62,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation announces an in-flight local sign-in while preserving its action name","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752018745,"status":"passed","title":"announces an in-flight local sign-in while preserving its action name"},{"ancestorTitles":["public auth presentation"],"duration":46,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation announces a pending password save without changing its payload or successful destination","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793752018807,"status":"passed","title":"announces a pending password save without changing its payload or successful destination"},{"ancestorTitles":["public auth presentation"],"duration":31,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation focuses the first invalid local-login field and associates its explanation","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752018853,"status":"passed","title":"focuses the first invalid local-login field and associates its explanation"},{"ancestorTitles":["public auth presentation"],"duration":31,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation focuses the invalid new password without losing confirmation input","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"startAt":1793752018884,"status":"passed","title":"focuses the invalid new password without losing confirmation input"},{"ancestorTitles":["public auth presentation"],"duration":13,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation focuses and explains the invalid reset-request email without sending a command","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752018915,"status":"passed","title":"focuses and explains the invalid reset-request email without sending a command"},{"ancestorTitles":["public auth presentation"],"duration":59,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation replaces previous reset-request success when a later request fails","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793752018928,"status":"passed","title":"replaces previous reset-request success when a later request fails"},{"ancestorTitles":["public auth presentation"],"duration":39,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation does not assert optional authentication while login configuration is unresolved","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752018987,"status":"passed","title":"does not assert optional authentication while login configuration is unresolved"},{"ancestorTitles":["public auth presentation"],"duration":40,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation does not assert optional authentication while register configuration is unresolved","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752019026,"status":"passed","title":"does not assert optional authentication while register configuration is unresolved"},{"ancestorTitles":["public auth presentation"],"duration":103,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation offers a read-only configuration retry while retaining entered sign-in fields","invocations":1,"location":null,"numPassingAsserts":9,"retryReasons":[],"startAt":1793752019066,"status":"passed","title":"offers a read-only configuration retry while retaining entered sign-in fields"}],"endTime":1793752019170,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\public-auth-presentation.test.tsx","startTime":1793752014370,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":["LoginPage"],"duration":63,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage provides a main landmark and level-one heading for login","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752019926,"status":"passed","title":"provides a main landmark and level-one heading for login"},{"ancestorTitles":["LoginPage"],"duration":67,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage provides a main landmark and level-one heading for register","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752019989,"status":"passed","title":"provides a main landmark and level-one heading for register"},{"ancestorTitles":["LoginPage"],"duration":92,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage renders a dedicated registration screen and explains when registration is disabled","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793752020056,"status":"passed","title":"renders a dedicated registration screen and explains when registration is disabled"},{"ancestorTitles":["LoginPage"],"duration":83,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage renders one conventional sign-in card with enabled provider alternatives","invocations":1,"location":null,"numPassingAsserts":9,"retryReasons":[],"startAt":1793752020148,"status":"passed","title":"renders one conventional sign-in card with enabled provider alternatives"},{"ancestorTitles":["LoginPage"],"duration":416,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage accepts a username and reports invalid credentials without changing routes","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752020231,"status":"passed","title":"accepts a username and reports invalid credentials without changing routes"},{"ancestorTitles":["LoginPage"],"duration":94,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage handles Microsoft cancellation without exchanging or navigating","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752020648,"status":"passed","title":"handles Microsoft cancellation without exchanging or navigating"},{"ancestorTitles":["LoginPage"],"duration":140,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage completes the Microsoft provider return through the hardened exchange endpoint","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793752020742,"status":"passed","title":"completes the Microsoft provider return through the hardened exchange endpoint"},{"ancestorTitles":["LoginPage"],"duration":138,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage completes a Google credential return without account-linking copy","invocations":1,"location":null,"numPassingAsserts":4,"retryReasons":[],"startAt":1793752020882,"status":"passed","title":"completes a Google credential return without account-linking copy"},{"ancestorTitles":["LoginPage"],"duration":278,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage posts remember-me preference without storing an auth token in browser storage","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"startAt":1793752021020,"status":"passed","title":"posts remember-me preference without storing an auth token in browser storage"},{"ancestorTitles":["LoginPage"],"duration":695,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage does not complete login when registration requires email verification","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752021298,"status":"passed","title":"does not complete login when registration requires email verification"},{"ancestorTitles":["LoginPage"],"duration":170,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage rejects an unsafe post-login redirect path","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752021993,"status":"passed","title":"rejects an unsafe post-login redirect path"},{"ancestorTitles":["LoginPage"],"duration":542,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"https://evil.example/analytics\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752022163,"status":"passed","title":"uses the safe fallback for the untrusted return target \"https://evil.example/analytics\""},{"ancestorTitles":["LoginPage"],"duration":447,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"//evil.example/analytics\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752022705,"status":"passed","title":"uses the safe fallback for the untrusted return target \"//evil.example/analytics\""},{"ancestorTitles":["LoginPage"],"duration":493,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"/\\t/evil.example\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752023152,"status":"passed","title":"uses the safe fallback for the untrusted return target \"/\\t/evil.example\""},{"ancestorTitles":["LoginPage"],"duration":510,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"/%2f%2fevil.example\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752023645,"status":"passed","title":"uses the safe fallback for the untrusted return target \"/%2f%2fevil.example\""},{"ancestorTitles":["LoginPage"],"duration":494,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"/%252f%252fevil.example\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752024155,"status":"passed","title":"uses the safe fallback for the untrusted return target \"/%252f%252fevil.example\""},{"ancestorTitles":["LoginPage"],"duration":496,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"/%5cevil.example\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752024649,"status":"passed","title":"uses the safe fallback for the untrusted return target \"/%5cevil.example\""},{"ancestorTitles":["LoginPage"],"duration":465,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"/analytics?months=%ZZ\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752025145,"status":"passed","title":"uses the safe fallback for the untrusted return target \"/analytics?months=%ZZ\""},{"ancestorTitles":["LoginPage"],"duration":530,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"/jobs/../login\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752025610,"status":"passed","title":"uses the safe fallback for the untrusted return target \"/jobs/../login\""},{"ancestorTitles":["LoginPage"],"duration":418,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"/%6cogin\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752026140,"status":"passed","title":"uses the safe fallback for the untrusted return target \"/%6cogin\""},{"ancestorTitles":["LoginPage"],"duration":571,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"/register\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752026558,"status":"passed","title":"uses the safe fallback for the untrusted return target \"/register\""},{"ancestorTitles":["LoginPage"],"duration":403,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"/analytics%0a\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752027129,"status":"passed","title":"uses the safe fallback for the untrusted return target \"/analytics%0a\""},{"ancestorTitles":["LoginPage"],"duration":760,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"/analytics?months=6\\n\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752027532,"status":"passed","title":"uses the safe fallback for the untrusted return target \"/analytics?months=6\\n\""},{"ancestorTitles":["LoginPage"],"duration":725,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"/LOGIN\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752028292,"status":"passed","title":"uses the safe fallback for the untrusted return target \"/LOGIN\""},{"ancestorTitles":["LoginPage"],"duration":711,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage uses the safe fallback for the untrusted return target \"/%4cOGIN\"","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752029017,"status":"passed","title":"uses the safe fallback for the untrusted return target \"/%4cOGIN\""},{"ancestorTitles":["LoginPage"],"duration":693,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage preserves encoded internal query and fragment without decoding the navigation target","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752029728,"status":"passed","title":"preserves encoded internal query and fragment without decoding the navigation target"},{"ancestorTitles":["LoginPage"],"duration":679,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage restores valid owned-workspace navigation context separately from the sign-in destination","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752030421,"status":"passed","title":"restores valid owned-workspace navigation context separately from the sign-in destination"},{"ancestorTitles":["LoginPage"],"duration":399,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage does not replay an unsafe nested return context after authentication","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752031100,"status":"passed","title":"does not replay an unsafe nested return context after authentication"},{"ancestorTitles":["LoginPage"],"duration":785,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage abandons prior return context on the established account/session disposal entry","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752031499,"status":"passed","title":"abandons prior return context on the established account/session disposal entry"},{"ancestorTitles":["LoginPage"],"duration":527,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage opens the separate forgot-password page with the typed email prefilled","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752032284,"status":"passed","title":"opens the separate forgot-password page with the typed email prefilled"},{"ancestorTitles":["LoginPage"],"duration":907,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage shows the 2FA code step when login requires two-factor, then proceeds like a normal login on success","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"startAt":1793752032811,"status":"passed","title":"shows the 2FA code step when login requires two-factor, then proceeds like a normal login on success"},{"ancestorTitles":["LoginPage"],"duration":879,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage shows a clear message when the 2FA challenge is rate-limited","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752033718,"status":"passed","title":"shows a clear message when the 2FA challenge is rate-limited"},{"ancestorTitles":["LoginPage"],"duration":4753,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"LoginPage offers a resend-verification action when login reports the account is not verified","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793752034597,"status":"passed","title":"offers a resend-verification action when login reports the account is not verified"}],"endTime":1793752039352,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\login-page.test.tsx","startTime":1793752019181,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":[],"duration":60,"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":1793752040078,"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":1793752040138,"status":"passed","title":"[PRES-E08] section composition retains loaded, error/retry and empty child semantics"}],"endTime":1793752040200,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\page-layout.test.tsx","startTime":1793752039361,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":[],"duration":86,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"legacy Microsoft recovery requires the emailed proof and a fresh matching Microsoft token","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793752040918,"status":"passed","title":"legacy Microsoft recovery requires the emailed proof and a fresh matching Microsoft token"},{"ancestorTitles":[],"duration":46,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"pending Microsoft recovery keeps a named action and announces progress before the real confirmation boundary","invocations":1,"location":null,"numPassingAsserts":5,"retryReasons":[],"startAt":1793752041004,"status":"passed","title":"pending Microsoft recovery keeps a named action and announces progress before the real confirmation boundary"}],"endTime":1793752041050,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\microsoft-legacy-relink-page.test.tsx","startTime":1793752040208,"status":"passed","summary":""},{"assertionResults":[{"ancestorTitles":["VerifyEmailPage"],"duration":44,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"VerifyEmailPage confirms the account and shows success when the link is valid","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793752041754,"status":"passed","title":"confirms the account and shows success when the link is valid"},{"ancestorTitles":["VerifyEmailPage"],"duration":29,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"VerifyEmailPage shows an error when the link is invalid or expired","invocations":1,"location":null,"numPassingAsserts":1,"retryReasons":[],"startAt":1793752041798,"status":"passed","title":"shows an error when the link is invalid or expired"},{"ancestorTitles":["VerifyEmailPage"],"duration":31,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"VerifyEmailPage shows an error without calling the API when the link is missing userId/token","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793752041827,"status":"passed","title":"shows an error without calling the API when the link is missing userId/token"},{"ancestorTitles":["VerifyEmailPage"],"duration":32,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"VerifyEmailPage confirms a pending email change and requires a fresh sign-in","invocations":1,"location":null,"numPassingAsserts":2,"retryReasons":[],"startAt":1793752041858,"status":"passed","title":"confirms a pending email change and requires a fresh sign-in"},{"ancestorTitles":["VerifyEmailPage"],"duration":73,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"VerifyEmailPage changing UI language never resubmits a pending confirmation: emailChange=false","invocations":1,"location":null,"numPassingAsserts":6,"retryReasons":[],"startAt":1793752041890,"status":"passed","title":"changing UI language never resubmits a pending confirmation: emailChange=false"},{"ancestorTitles":["VerifyEmailPage"],"duration":32,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"VerifyEmailPage changing UI language never resubmits a pending confirmation: emailChange=true","invocations":1,"location":null,"numPassingAsserts":6,"retryReasons":[],"startAt":1793752041963,"status":"passed","title":"changing UI language never resubmits a pending confirmation: emailChange=true"},{"ancestorTitles":["VerifyEmailPage"],"duration":24,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"VerifyEmailPage localizes missing-link errors without a confirmation request","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752041995,"status":"passed","title":"localizes missing-link errors without a confirmation request"},{"ancestorTitles":["VerifyEmailPage"],"duration":31,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"VerifyEmailPage localizes a failed confirmation fallback without retrying the one-time command","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752042019,"status":"passed","title":"localizes a failed confirmation fallback without retrying the one-time command"},{"ancestorTitles":["VerifyEmailPage"],"duration":26,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"VerifyEmailPage does not let an older confirmation replace a different flow missing-link error","invocations":1,"location":null,"numPassingAsserts":3,"retryReasons":[],"startAt":1793752042050,"status":"passed","title":"does not let an older confirmation replace a different flow missing-link error"}],"endTime":1793752042077,"message":"","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\verify-email-page.test.tsx","startTime":1793752041058,"status":"passed","summary":""}],"wasInterrupted":false} diff --git a/docs/ui-modernisation/visual-correction/PUBLIC_LOCAL_SAVE_RED.json b/docs/ui-modernisation/visual-correction/PUBLIC_LOCAL_SAVE_RED.json new file mode 100644 index 00000000..3a545b9d --- /dev/null +++ b/docs/ui-modernisation/visual-correction/PUBLIC_LOCAL_SAVE_RED.json @@ -0,0 +1 @@ +{"numFailedTestSuites":1,"numFailedTests":2,"numPassedTestSuites":0,"numPassedTests":0,"numPendingTestSuites":0,"numPendingTests":6,"numRuntimeErrorTestSuites":0,"numTodoTests":0,"numTotalTestSuites":1,"numTotalTests":8,"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":1793752515641,"success":false,"testResults":[{"assertionResults":[{"ancestorTitles":[],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"migrates the legacy Norwegian code and persists Bokmål using nb-NO","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793752518886,"status":"pending","title":"migrates the legacy Norwegian code and persists Bokmål using nb-NO"},{"ancestorTitles":[],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"hydrates an authenticated server preference without writing it back","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793752518886,"status":"pending","title":"hydrates an authenticated server preference without writing it back"},{"ancestorTitles":[],"duration":57,"failing":false,"failureDetails":[{},{"matcherResult":{"pass":false,"message":"expect(element).toHaveTextContent()\n\nExpected element to have text content:\n nb\nReceived:\n en"}}],"failureMessages":["TypeError: setLocalLanguage is not a function\n at setLocalLanguage (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\i18n-provider.test.tsx:17:30)\n at executeDispatch (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\react-dom\\cjs\\react-dom-client.development.js:19116:9)\n at runWithFiberInDEV (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\react-dom\\cjs\\react-dom-client.development.js:874:13)\n at processDispatchQueue (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\react-dom\\cjs\\react-dom-client.development.js:19166:19)\n at C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\react-dom\\cjs\\react-dom-client.development.js:19767:9\n at batchedUpdates$1 (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\react-dom\\cjs\\react-dom-client.development.js:3255:40)\n at dispatchEventForPluginEventSystem (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\react-dom\\cjs\\react-dom-client.development.js:19320:7)\n at dispatchEvent (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\react-dom\\cjs\\react-dom-client.development.js:23585:11)\n at dispatchDiscreteEvent (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\react-dom\\cjs\\react-dom-client.development.js:23553:11)\n at HTMLDivElement.callTheUserObjectsOperation (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jsdom\\lib\\jsdom\\living\\generated\\EventListener.js:26:30)\n at innerInvokeEventListeners (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jsdom\\lib\\jsdom\\living\\events\\EventTarget-impl.js:350:25)\n at invokeEventListeners (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jsdom\\lib\\jsdom\\living\\events\\EventTarget-impl.js:286:3)\n at HTMLButtonElementImpl._dispatch (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jsdom\\lib\\jsdom\\living\\events\\EventTarget-impl.js:233:9)\n at HTMLButtonElementImpl.dispatchEvent (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jsdom\\lib\\jsdom\\living\\events\\EventTarget-impl.js:104:17)\n at HTMLButtonElement.dispatchEvent (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jsdom\\lib\\jsdom\\living\\generated\\EventTarget.js:241:34)\n at C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\dom\\dist\\events.js:19:20\n at C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\react\\dist\\pure.js:107:16\n at C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\react\\dist\\act-compat.js:47:24\n at process.env.NODE_ENV.exports.act (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\react\\cjs\\react.development.js:814:22)\n at C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\react\\dist\\act-compat.js:46:25\n at Object.eventWrapper (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\react\\dist\\pure.js:106:28)\n at fireEvent (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\dom\\dist\\events.js:12:35)\n at fireEvent. [as click] (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\dom\\dist\\events.js:110:36)\n at fireEvent. [as click] (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\react\\dist\\fire-event.js:15:52)\n at Object.click (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\i18n-provider.test.tsx:52:13)\n at Promise.finally.completed (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1561:28)\n at new Promise ()\n at callAsyncCircusFn (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1501:10)\n at _callCircusTest (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1011:40)\n at _runTest (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\jestAdapterInit.js:951:3)\n at C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\jestAdapterInit.js:853:7\n at _runTestsForDescribeBlock (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\jestAdapterInit.js:866:11)\n at run (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\jestAdapterInit.js:765:3)\n at runAndTransformResultsToJestFormat (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1993:21)\n at jestAdapter (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-runner\\build\\index.js:276:16)\n at runTest (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-runner\\build\\index.js:344:7)","Error: expect(element).toHaveTextContent()\n\nExpected element to have text content:\n nb\nReceived:\n en\n at Object.toHaveTextContent (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\i18n-provider.test.tsx:53:45)\n at Promise.finally.completed (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1561:28)\n at new Promise ()\n at callAsyncCircusFn (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1501:10)\n at _callCircusTest (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1011:40)\n at _runTest (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\jestAdapterInit.js:951:3)\n at C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\jestAdapterInit.js:853:7\n at _runTestsForDescribeBlock (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\jestAdapterInit.js:866:11)\n at run (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\jestAdapterInit.js:765:3)\n at runAndTransformResultsToJestFormat (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1993:21)\n at jestAdapter (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-runner\\build\\index.js:276:16)\n at runTest (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-runner\\build\\index.js:344:7)"],"fullName":"a public local-only choice persists without authenticated requests","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793752518886,"status":"failed","title":"a public local-only choice persists without authenticated requests"},{"ancestorTitles":[],"duration":68,"failing":false,"failureDetails":[{},{"matcherResult":{"pass":true,"message":"expect(element).not.toBeInTheDocument()\n\nexpected document not to contain element, found
Språket er endret på denne enheten, men kunne ikke lagres på kontoen din.
instead"}}],"failureMessages":["TypeError: setLocalLanguage is not a function\n at setLocalLanguage (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\i18n-provider.test.tsx:17:30)\n at executeDispatch (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\react-dom\\cjs\\react-dom-client.development.js:19116:9)\n at runWithFiberInDEV (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\react-dom\\cjs\\react-dom-client.development.js:874:13)\n at processDispatchQueue (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\react-dom\\cjs\\react-dom-client.development.js:19166:19)\n at C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\react-dom\\cjs\\react-dom-client.development.js:19767:9\n at batchedUpdates$1 (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\react-dom\\cjs\\react-dom-client.development.js:3255:40)\n at dispatchEventForPluginEventSystem (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\react-dom\\cjs\\react-dom-client.development.js:19320:7)\n at dispatchEvent (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\react-dom\\cjs\\react-dom-client.development.js:23585:11)\n at dispatchDiscreteEvent (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\react-dom\\cjs\\react-dom-client.development.js:23553:11)\n at HTMLDivElement.callTheUserObjectsOperation (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jsdom\\lib\\jsdom\\living\\generated\\EventListener.js:26:30)\n at innerInvokeEventListeners (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jsdom\\lib\\jsdom\\living\\events\\EventTarget-impl.js:350:25)\n at invokeEventListeners (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jsdom\\lib\\jsdom\\living\\events\\EventTarget-impl.js:286:3)\n at HTMLButtonElementImpl._dispatch (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jsdom\\lib\\jsdom\\living\\events\\EventTarget-impl.js:233:9)\n at HTMLButtonElementImpl.dispatchEvent (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jsdom\\lib\\jsdom\\living\\events\\EventTarget-impl.js:104:17)\n at HTMLButtonElement.dispatchEvent (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jsdom\\lib\\jsdom\\living\\generated\\EventTarget.js:241:34)\n at C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\dom\\dist\\events.js:19:20\n at C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\react\\dist\\pure.js:107:16\n at C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\react\\dist\\act-compat.js:47:24\n at process.env.NODE_ENV.exports.act (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\react\\cjs\\react.development.js:814:22)\n at C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\react\\dist\\act-compat.js:46:25\n at Object.eventWrapper (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\react\\dist\\pure.js:106:28)\n at fireEvent (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\dom\\dist\\events.js:12:35)\n at fireEvent. [as click] (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\dom\\dist\\events.js:110:36)\n at fireEvent. [as click] (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\react\\dist\\fire-event.js:15:52)\n at Object.click (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\i18n-provider.test.tsx:65:13)\n at Promise.finally.completed (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1561:28)\n at new Promise ()\n at callAsyncCircusFn (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1501:10)\n at _callCircusTest (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1011:40)\n at _runTest (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\jestAdapterInit.js:951:3)\n at C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\jestAdapterInit.js:853:7\n at _runTestsForDescribeBlock (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\jestAdapterInit.js:866:11)\n at run (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\jestAdapterInit.js:765:3)\n at runAndTransformResultsToJestFormat (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\jestAdapterInit.js:1993:21)\n at jestAdapter (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-circus\\build\\runner.js:111:19)\n at runTestInternal (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-runner\\build\\index.js:276:16)\n at runTest (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\jest-runner\\build\\index.js:344:7)","Error: expect(element).not.toBeInTheDocument()\n\nexpected document not to contain element, found
Språket er endret på denne enheten, men kunne ikke lagres på kontoen din.
instead\n at Object.toBeInTheDocument (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\i18n-provider.test.tsx:67:43)"],"fullName":"a local-only choice invalidates an older same-language account-save retry","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793752518964,"status":"failed","title":"a local-only choice invalidates an older same-language account-save retry"},{"ancestorTitles":[],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"a failed account language save keeps the local choice and offers a localized retry: Norsk","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793752519032,"status":"pending","title":"a failed account language save keeps the local choice and offers a localized retry: Norsk"},{"ancestorTitles":[],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"a failed account language save keeps the local choice and offers a localized retry: English","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793752519032,"status":"pending","title":"a failed account language save keeps the local choice and offers a localized retry: English"},{"ancestorTitles":[],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"a delayed language-save failure cannot offer a retry for a newer successful choice","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793752519032,"status":"pending","title":"a delayed language-save failure cannot offer a retry for a newer successful choice"},{"ancestorTitles":[],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"a previously delivered language retry cannot reverse a newer choice","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793752519032,"status":"pending","title":"a previously delivered language retry cannot reverse a newer choice"}],"endTime":1793752519050,"message":" ● a public local-only choice persists without authenticated requests\n\n TypeError: setLocalLanguage is not a function\n\n 15 | \n 16 | \n > 17 | \n | ^\n 18 | \n 19 | \n 20 | );\n\n at setLocalLanguage (src/i18n-provider.test.tsx:17:30)\n at executeDispatch (node_modules/react-dom/cjs/react-dom-client.development.js:19116:9)\n at runWithFiberInDEV (node_modules/react-dom/cjs/react-dom-client.development.js:874:13)\n at processDispatchQueue (node_modules/react-dom/cjs/react-dom-client.development.js:19166:19)\n at node_modules/react-dom/cjs/react-dom-client.development.js:19767:9\n at batchedUpdates$1 (node_modules/react-dom/cjs/react-dom-client.development.js:3255:40)\n at dispatchEventForPluginEventSystem (node_modules/react-dom/cjs/react-dom-client.development.js:19320:7)\n at dispatchEvent (node_modules/react-dom/cjs/react-dom-client.development.js:23585:11)\n at dispatchDiscreteEvent (node_modules/react-dom/cjs/react-dom-client.development.js:23553:11)\n at HTMLDivElement.callTheUserObjectsOperation (node_modules/jsdom/lib/jsdom/living/generated/EventListener.js:26:30)\n at innerInvokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:350:25)\n at invokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:286:3)\n at HTMLButtonElementImpl._dispatch (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:233:9)\n at HTMLButtonElementImpl.dispatchEvent (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:104:17)\n at HTMLButtonElement.dispatchEvent (node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:241:34)\n at node_modules/@testing-library/dom/dist/events.js:19:20\n at node_modules/@testing-library/react/dist/pure.js:107:16\n at node_modules/@testing-library/react/dist/act-compat.js:47:24\n at process.env.NODE_ENV.exports.act (node_modules/react/cjs/react.development.js:814:22)\n at node_modules/@testing-library/react/dist/act-compat.js:46:25\n at Object.eventWrapper (node_modules/@testing-library/react/dist/pure.js:106:28)\n at fireEvent (node_modules/@testing-library/dom/dist/events.js:12:35)\n at fireEvent. [as click] (node_modules/@testing-library/dom/dist/events.js:110:36)\n at fireEvent. [as click] (node_modules/@testing-library/react/dist/fire-event.js:15:52)\n at Object.click (src/i18n-provider.test.tsx:52:13)\n\n ● a public local-only choice persists without authenticated requests\n\n expect(element).toHaveTextContent()\n\n Expected element to have text content:\n nb\n Received:\n en\n\n 51 | render();\n 52 | fireEvent.click(screen.getByRole(\"button\", { name: \"Local Norsk\" }));\n > 53 | expect(screen.getByLabelText(\"language\")).toHaveTextContent(\"nb\");\n | ^\n 54 | expect(localStorage.getItem(\"uiLanguage\")).toBe(\"nb\");\n 55 | expect(document.documentElement.lang).toBe(\"nb-NO\");\n 56 | expect(api.put).not.toHaveBeenCalled();\n\n at Object.toHaveTextContent (src/i18n-provider.test.tsx:53:45)\n\n ● a local-only choice invalidates an older same-language account-save retry\n\n TypeError: setLocalLanguage is not a function\n\n 15 | \n 16 | \n > 17 | \n | ^\n 18 | \n 19 | \n 20 | );\n\n at setLocalLanguage (src/i18n-provider.test.tsx:17:30)\n at executeDispatch (node_modules/react-dom/cjs/react-dom-client.development.js:19116:9)\n at runWithFiberInDEV (node_modules/react-dom/cjs/react-dom-client.development.js:874:13)\n at processDispatchQueue (node_modules/react-dom/cjs/react-dom-client.development.js:19166:19)\n at node_modules/react-dom/cjs/react-dom-client.development.js:19767:9\n at batchedUpdates$1 (node_modules/react-dom/cjs/react-dom-client.development.js:3255:40)\n at dispatchEventForPluginEventSystem (node_modules/react-dom/cjs/react-dom-client.development.js:19320:7)\n at dispatchEvent (node_modules/react-dom/cjs/react-dom-client.development.js:23585:11)\n at dispatchDiscreteEvent (node_modules/react-dom/cjs/react-dom-client.development.js:23553:11)\n at HTMLDivElement.callTheUserObjectsOperation (node_modules/jsdom/lib/jsdom/living/generated/EventListener.js:26:30)\n at innerInvokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:350:25)\n at invokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:286:3)\n at HTMLButtonElementImpl._dispatch (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:233:9)\n at HTMLButtonElementImpl.dispatchEvent (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:104:17)\n at HTMLButtonElement.dispatchEvent (node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:241:34)\n at node_modules/@testing-library/dom/dist/events.js:19:20\n at node_modules/@testing-library/react/dist/pure.js:107:16\n at node_modules/@testing-library/react/dist/act-compat.js:47:24\n at process.env.NODE_ENV.exports.act (node_modules/react/cjs/react.development.js:814:22)\n at node_modules/@testing-library/react/dist/act-compat.js:46:25\n at Object.eventWrapper (node_modules/@testing-library/react/dist/pure.js:106:28)\n at fireEvent (node_modules/@testing-library/dom/dist/events.js:12:35)\n at fireEvent. [as click] (node_modules/@testing-library/dom/dist/events.js:110:36)\n at fireEvent. [as click] (node_modules/@testing-library/react/dist/fire-event.js:15:52)\n at Object.click (src/i18n-provider.test.tsx:65:13)\n\n ● a local-only choice invalidates an older same-language account-save retry\n\n expect(element).not.toBeInTheDocument()\n\n expected document not to contain element, found
Språket er endret på denne enheten, men kunne ikke lagres på kontoen din.
instead\n\n 65 | fireEvent.click(screen.getByRole(\"button\", { name: \"Local Norsk\" }));\n 66 | await act(async () => rejectOld(new Error(\"offline\")));\n > 67 | expect(screen.queryByRole(\"alert\")).not.toBeInTheDocument();\n | ^\n 68 | expect(api.put).toHaveBeenCalledTimes(1);\n 69 | expect(screen.getByLabelText(\"language\")).toHaveTextContent(\"nb\");\n 70 | });\n\n at Object.toBeInTheDocument (src/i18n-provider.test.tsx:67:43)\n","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\i18n-provider.test.tsx","startTime":1793752516582,"status":"failed","summary":""}],"wasInterrupted":false} diff --git a/docs/ui-modernisation/visual-correction/PUBLIC_SELECTOR_RED.json b/docs/ui-modernisation/visual-correction/PUBLIC_SELECTOR_RED.json new file mode 100644 index 00000000..ece3ae93 --- /dev/null +++ b/docs/ui-modernisation/visual-correction/PUBLIC_SELECTOR_RED.json @@ -0,0 +1 @@ +{"numFailedTestSuites":1,"numFailedTests":1,"numPassedTestSuites":0,"numPassedTests":0,"numPendingTestSuites":0,"numPendingTests":22,"numRuntimeErrorTestSuites":0,"numTodoTests":0,"numTotalTestSuites":1,"numTotalTests":23,"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":1793751565410,"success":false,"testResults":[{"assertionResults":[{"ancestorTitles":["public auth presentation"],"duration":279,"failing":false,"failureDetails":[{"name":"TestingLibraryElementError"}],"failureMessages":["TestingLibraryElementError: Unable to find an accessible element with the role \"button\" and name \"Norsk bokmål\"\n\nHere are the accessible roles:\n\n main:\n\n Name \"Sign in\":\n \u001b[36m\u001b[39m\n\n --------------------------------------------------\n banner:\n\n Name \"\":\n \u001b[36m\u001b[39m\n\n --------------------------------------------------\n heading:\n\n Name \"Sign in\":\n \u001b[36m\u001b[39m\n\n --------------------------------------------------\n paragraph:\n\n Name \"\":\n \u001b[36m\u001b[39m\n\n Name \"\":\n \u001b[36m\u001b[39m\n\n --------------------------------------------------\n textbox:\n\n Name \"Username or email\":\n \u001b[36m\u001b[39m\n\n --------------------------------------------------\n checkbox:\n\n Name \"Remember me\":\n \u001b[36m\u001b[39m\n\n --------------------------------------------------\n button:\n\n Name \"Forgot password?\":\n \u001b[36m\u001b[39m\n\n Name \"Create account\":\n \u001b[36m\u001b[39m\n\n Name \"Sign in\":\n \u001b[36m\u001b[39m\n\n --------------------------------------------------\n\nIgnored nodes: comments, script, style\n\u001b[36m\u001b[39m\n \u001b[36m
\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mSign in\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mAuthentication is required to use this app.\u001b[0m\n \u001b[36m

\u001b[39m\n \u001b[36m
\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mUsername or email\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mUsername or email\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mCurrent password\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mCurrent password\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mRemember me\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mForgot password?\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mKeeps you signed in on this device until you sign out.\u001b[0m\n \u001b[36m

\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mCreate account\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mSign in\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n\u001b[36m\u001b[39m\n at Object.getElementError (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\dom\\dist\\config.js:37:19)\n at C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\dom\\dist\\query-helpers.js:76:38\n at C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\dom\\dist\\query-helpers.js:52:17\n at C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\node_modules\\@testing-library\\dom\\dist\\query-helpers.js:95:19\n at Object.getByRole (C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\public-auth-presentation.test.tsx:56:28)"],"fullName":"public auth presentation lets a signed-out user choose a persisted UI language without discarding a sign-in draft","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751568751,"status":"failed","title":"lets a signed-out user choose a persisted UI language without discarding a sign-in draft"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation names the password field for login intent and retains its autocomplete contract","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751569041,"status":"pending","title":"names the password field for login intent and retains its autocomplete contract"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation names the password field for register intent and retains its autocomplete contract","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751569041,"status":"pending","title":"names the password field for register intent and retains its autocomplete contract"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation provides one named main landmark and H1 for password request","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751569041,"status":"pending","title":"provides one named main landmark and H1 for password request"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation provides one named main landmark and H1 for password reset","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751569041,"status":"pending","title":"provides one named main landmark and H1 for password reset"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation provides one named main landmark and H1 for email verification","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751569041,"status":"pending","title":"provides one named main landmark and H1 for email verification"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation provides one named main landmark and H1 for email change","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751569041,"status":"pending","title":"provides one named main landmark and H1 for email change"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation provides one named main landmark and H1 for Microsoft recovery","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751569041,"status":"pending","title":"provides one named main landmark and H1 for Microsoft recovery"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation announces the pending confirmation without an unnamed progressbar: false","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751569041,"status":"pending","title":"announces the pending confirmation without an unnamed progressbar: false"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation announces the pending confirmation without an unnamed progressbar: true","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751569041,"status":"pending","title":"announces the pending confirmation without an unnamed progressbar: true"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation retains the entered reset email and a durable error in the page when the command fails","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751569041,"status":"pending","title":"retains the entered reset email and a durable error in the page when the command fails"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation retains both new-password fields and the decoded recovery token after a rejected reset","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751569041,"status":"pending","title":"retains both new-password fields and the decoded recovery token after a rejected reset"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation keeps a rejected local sign-in beside the retained fields without navigating","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751569041,"status":"pending","title":"keeps a rejected local sign-in beside the retained fields without navigating"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation keeps the reset-request action name while announcing an in-flight command","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751569041,"status":"pending","title":"keeps the reset-request action name while announcing an in-flight command"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation announces an in-flight local sign-in while preserving its action name","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751569041,"status":"pending","title":"announces an in-flight local sign-in while preserving its action name"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation announces a pending password save without changing its payload or successful destination","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751569041,"status":"pending","title":"announces a pending password save without changing its payload or successful destination"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation focuses the first invalid local-login field and associates its explanation","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751569041,"status":"pending","title":"focuses the first invalid local-login field and associates its explanation"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation focuses the invalid new password without losing confirmation input","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751569041,"status":"pending","title":"focuses the invalid new password without losing confirmation input"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation focuses and explains the invalid reset-request email without sending a command","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751569041,"status":"pending","title":"focuses and explains the invalid reset-request email without sending a command"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation replaces previous reset-request success when a later request fails","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751569041,"status":"pending","title":"replaces previous reset-request success when a later request fails"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation does not assert optional authentication while login configuration is unresolved","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751569041,"status":"pending","title":"does not assert optional authentication while login configuration is unresolved"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation does not assert optional authentication while register configuration is unresolved","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751569041,"status":"pending","title":"does not assert optional authentication while register configuration is unresolved"},{"ancestorTitles":["public auth presentation"],"duration":null,"failing":false,"failureDetails":[],"failureMessages":[],"fullName":"public auth presentation offers a read-only configuration retry while retaining entered sign-in fields","invocations":1,"location":null,"numPassingAsserts":0,"retryReasons":[],"startAt":1793751569041,"status":"pending","title":"offers a read-only configuration retry while retaining entered sign-in fields"}],"endTime":1793751569054,"message":" ● public auth presentation › lets a signed-out user choose a persisted UI language without discarding a sign-in draft\n\n TestingLibraryElementError: Unable to find an accessible element with the role \"button\" and name \"Norsk bokmål\"\n\n Here are the accessible roles:\n\n main:\n\n Name \"Sign in\":\n \u001b[36m\u001b[39m\n\n --------------------------------------------------\n banner:\n\n Name \"\":\n \u001b[36m\u001b[39m\n\n --------------------------------------------------\n heading:\n\n Name \"Sign in\":\n \u001b[36m\u001b[39m\n\n --------------------------------------------------\n paragraph:\n\n Name \"\":\n \u001b[36m\u001b[39m\n\n Name \"\":\n \u001b[36m\u001b[39m\n\n --------------------------------------------------\n textbox:\n\n Name \"Username or email\":\n \u001b[36m\u001b[39m\n\n --------------------------------------------------\n checkbox:\n\n Name \"Remember me\":\n \u001b[36m\u001b[39m\n\n --------------------------------------------------\n button:\n\n Name \"Forgot password?\":\n \u001b[36m\u001b[39m\n\n Name \"Create account\":\n \u001b[36m\u001b[39m\n\n Name \"Sign in\":\n \u001b[36m\u001b[39m\n\n --------------------------------------------------\n\n Ignored nodes: comments, script, style\n \u001b[36m\u001b[39m\n \u001b[36m
\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mSign in\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mAuthentication is required to use this app.\u001b[0m\n \u001b[36m

\u001b[39m\n \u001b[36m
\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mUsername or email\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mUsername or email\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mCurrent password\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mCurrent password\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mRemember me\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mForgot password?\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mKeeps you signed in on this device until you sign out.\u001b[0m\n \u001b[36m

\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mCreate account\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[0mSign in\u001b[0m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n \u001b[36m\u001b[39m\n\n 54 | fireEvent.change(screen.getByLabelText('Username or email'), { target: { value: 'synthetic@example.test' } });\n 55 | fireEvent.change(screen.getByLabelText('Current password'), { target: { value: 'Unsubmitted synthetic example' } });\n > 56 | fireEvent.click(screen.getByRole('button', { name: 'Norsk bokmål' }));\n | ^\n 57 | expect(screen.getByRole('heading', { level: 1, name: translations.nb.signInTitle })).toBeVisible();\n 58 | expect(screen.getByLabelText(translations.nb.usernameOrEmail)).toHaveValue('synthetic@example.test');\n 59 | expect(screen.getByLabelText(translations.nb.profileCurrentPassword)).toHaveValue('Unsubmitted synthetic example');\n\n at Object.getElementError (node_modules/@testing-library/dom/dist/config.js:37:19)\n at node_modules/@testing-library/dom/dist/query-helpers.js:76:38\n at node_modules/@testing-library/dom/dist/query-helpers.js:52:17\n at node_modules/@testing-library/dom/dist/query-helpers.js:95:19\n at Object.getByRole (src/public-auth-presentation.test.tsx:56:28)\n","name":"C:\\Users\\Connor\\.codex\\worktrees\\direct-roadmap-integration\\Job tracker\\job-tracker-ui\\src\\public-auth-presentation.test.tsx","startTime":1793751566323,"status":"failed","summary":""}],"wasInterrupted":false} diff --git a/job-tracker-ui/scripts/verify-a3-public-entry.mjs b/job-tracker-ui/scripts/verify-a3-public-entry.mjs new file mode 100644 index 00000000..a2be9c0a --- /dev/null +++ b/job-tracker-ui/scripts/verify-a3-public-entry.mjs @@ -0,0 +1,82 @@ +// Actual public routes; synthetic confirmation/MFA responses, no real identity or provider access. +import { chromium, expect } from '@playwright/test'; +import AxeBuilder from '@axe-core/playwright'; +import ts from 'typescript'; +import assert from 'node:assert/strict'; +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/visual-correction/evidence/public-entry'); +fs.mkdirSync(out, { recursive: true }); +const report = { screens: [], journeys: [], confirmations: [], signIns: [], challenges: [], unexpectedWrites: [], unavailableReads: [], blockedExternal: [], errors: [] }; +const browser = await chromium.launch(); +try { + const configurations = process.env.A3_PILOT ? [[320, 'en', 'light']] : [320, 360, 390, 430, 768, 1024, 1280, 1440, 1920].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 other = language === 'en' ? 'nb' : 'en'; + const text = (locale, key) => { assert.ok(translations[locale][key], key); return translations[locale][key]; }; + const t = key => text(language, key); + const context = await browser.newContext({ viewport: { width, height: 1000 }, reducedMotion: 'reduce' }); + const page = await context.newPage(); page.on('pageerror', error => report.errors.push(error.message)); + await page.addInitScript(({ language, mode }) => { localStorage.setItem('uiLanguage', language); localStorage.setItem('jobtracker.themeMode', mode); }, { language, mode }); + let configReads = 0, confirmations = 0, signIns = 0, challenges = 0; + await page.route('**/*', route => { + const req = route.request(), url = new URL(req.url()), p = url.pathname; + 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() === 'GET' && p === '/api/auth/config') { configReads++; return answer({ requireAuth: true, localEnabled: true, allowRegistration: true, requireEmailVerification: true, googleEnabled: false, microsoftEnabled: false, turnstileEnabled: false }); } + if (req.method() === 'POST' && ['/api/auth/verify-email', '/api/auth/email-change/confirm'].includes(p)) { + const expected = { userId: 'synthetic-owner', ...(p.includes('email-change') ? { email: 'synthetic@example.test' } : {}), token: 'synthetic-confirmation' }; + assert.deepEqual(req.postDataJSON(), expected); confirmations++; report.confirmations.push({ width, language, mode, path: p }); return answer({}); + } + if (req.method() === 'POST' && p === '/api/auth/login') { + const body = req.postDataJSON(); assert.equal(body.email, 'synthetic@example.test'); assert.equal(body.password, 'Synthetic challenge example'); assert.equal(typeof body.rememberMe, 'boolean'); assert.deepEqual(Object.keys(body).sort(), ['email', 'password', 'rememberMe']); + signIns++; report.signIns.push({ width, language, mode }); return answer({ requiresTwoFactor: true, pendingToken: 'synthetic-pending' }); + } + if (req.method() === 'POST' && p === '/api/auth/2fa/challenge') { + assert.deepEqual(req.postDataJSON(), { pendingToken: 'synthetic-pending', code: '000000', trustDevice: false }); challenges++; report.challenges.push({ width, language, mode }); + return route.fulfill({ status: 429, contentType: 'application/problem+json', body: '{}' }); + } + if (req.method() !== 'GET') { report.unexpectedWrites.push(p); return route.abort(); } + report.unavailableReads.push(p); return route.fulfill({ status: 503, contentType: 'application/json', body: '{}' }); + }); + const scan = async state => { + 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); + await page.evaluate(() => window.scrollTo({ top: 0, behavior: 'instant' })); await page.screenshot({ path: path.join(out, `${state}-${language}-${mode}-${width}.png`), fullPage: true }); + }; + const selectLanguage = locale => page.getByRole('button', { name: locale === 'en' ? 'English' : 'Norsk bokmål', exact: true }).click(); + const ready = async (url, title, config = false) => { await page.goto(`http://localhost:4338${url}`); await expect(page.getByRole('heading', { level: 1, name: t(title), exact: true })).toBeVisible(); if (config) await expect(page.getByText(t('loading'), { exact: true })).toHaveCount(0); }; + await ready('/login', 'signInTitle', true); await scan('login'); + await page.getByLabel(t('usernameOrEmail'), { exact: true }).fill('synthetic@example.test'); await page.getByLabel(t('profileCurrentPassword'), { exact: true }).fill('Synthetic unsubmitted example'); + const reads = configReads; await selectLanguage(other); + await expect(page.getByLabel(text(other, 'usernameOrEmail'), { exact: true })).toHaveValue('synthetic@example.test'); await expect(page.getByLabel(text(other, 'profileCurrentPassword'), { exact: true })).toHaveValue('Synthetic unsubmitted example'); + assert.equal(await page.evaluate(() => localStorage.getItem('uiLanguage')), other); assert.equal(configReads, reads); assert.equal(signIns, 0); + await selectLanguage(language); await page.getByLabel(t('usernameOrEmail'), { exact: true }).fill(''); await page.getByLabel(t('profileCurrentPassword'), { exact: true }).fill(''); + await page.getByRole('button', { name: t('signInTitle'), exact: true }).click(); await expect(page.getByText(t('usernameOrEmailRequired'), { exact: true })).toBeVisible(); await scan('login-validation'); + await ready('/register', 'createAccount', true); await scan('register'); await page.getByRole('button', { name: t('createAccount'), exact: true }).click(); await expect(page.getByText(t('emailRequired'), { exact: true })).toBeVisible(); await scan('register-validation'); + await ready('/forgot-password', 'forgotPasswordTitle'); await scan('password-request'); await page.getByRole('button', { name: t('forgotPasswordSubmit'), exact: true }).click(); await expect(page.getByText(t('emailRequired'), { exact: true })).toBeVisible(); await scan('password-request-validation'); + await ready('/reset-password?email=synthetic%40example.test&token=synthetic', 'resetPasswordTitle'); await expect(page.getByRole('button', { name: t('updatePassword'), exact: true })).toBeEnabled(); await scan('password-reset'); + await page.getByRole('button', { name: t('updatePassword'), exact: true }).click(); await expect(page.getByLabel(t('profileNewPassword'), { exact: true })).toHaveAttribute('aria-invalid', 'true'); await scan('password-reset-validation'); + for (const [url, title, errorKey, state] of [['/verify-email', 'verifyEmailTitle', 'missingVerifyLinkInfo', 'verify-missing'], ['/confirm-email-change', 'confirmEmailChangeTitle', 'missingEmailChangeLinkInfo', 'email-change-missing'], ['/microsoft-legacy-relink', 'microsoftLegacyRelinkTitle', 'microsoftLegacyRelinkMissing', 'microsoft-recovery-missing']]) { + await ready(url, title); await expect(page.getByRole('main').getByRole('alert')).toContainText(t(errorKey)); if (state === 'microsoft-recovery-missing') await expect(page.getByRole('button', { name: t('continueWithMicrosoft'), exact: true })).toBeDisabled(); await scan(state); + } + assert.equal(confirmations, 0); assert.equal(signIns, 0); assert.equal(challenges, 0); + for (const [url, title, successKey, state] of [['/verify-email?userId=synthetic-owner&token=synthetic-confirmation', 'verifyEmailTitle', 'verifyEmailSuccess', 'verify-success'], ['/confirm-email-change?userId=synthetic-owner&email=synthetic%40example.test&token=synthetic-confirmation', 'confirmEmailChangeTitle', 'confirmEmailChangeSuccess', 'email-change-success']]) { + const expected = confirmations + 1; await ready(url, title); await expect(page.getByRole('status')).toContainText(t(successKey)); await selectLanguage(other); await expect(page.getByRole('status')).toContainText(text(other, successKey)); await selectLanguage(language); assert.equal(confirmations, expected); await scan(state); + } + await ready('/login', 'signInTitle', true); await page.getByLabel(t('usernameOrEmail'), { exact: true }).fill('synthetic@example.test'); await page.getByLabel(t('profileCurrentPassword'), { exact: true }).fill('Synthetic challenge example'); await page.getByRole('button', { name: t('signInTitle'), exact: true }).click(); + await page.getByLabel(t('twoFactorCodeLabel'), { exact: true }).fill('000000'); await page.getByRole('button', { name: t('twoFactorVerify'), exact: true }).click(); await expect(page.getByRole('main').getByRole('alert')).toContainText(t('twoFactorRateLimited')); + await selectLanguage(other); await expect(page.getByRole('main').getByRole('alert')).toContainText(text(other, 'twoFactorRateLimited')); await expect(page.getByLabel(text(other, 'twoFactorCodeLabel'), { exact: true })).toHaveValue('000000'); await selectLanguage(language); await scan('mfa-rate-limit'); + assert.equal(confirmations, 2); assert.equal(signIns, 1); assert.equal(challenges, 1); report.journeys.push({ width, language, mode, publicLanguageAndDraft: 'PASS', clientValidationWithoutSubmission: 'PASS', confirmationLocaleWithoutReplay: 'PASS', mfaRateLimitAndCodeRetained: 'PASS' }); await context.close(); + } + assert.equal(report.screens.length, configurations.length * 14); assert.equal(report.journeys.length, configurations.length); assert.equal(report.confirmations.length, configurations.length * 2); assert.equal(report.signIns.length, configurations.length); assert.equal(report.challenges.length, configurations.length); + assert.deepEqual(report.errors, []); assert.deepEqual(report.unexpectedWrites, []); assert.deepEqual(report.unavailableReads, []); assert.ok(report.blockedExternal.every(origin => origin === 'https://fonts.googleapis.com')); + console.log(`PASS:${report.journeys.length} public journeys,${report.screens.length} scans/captures; synthetic confirmations and MFA only.`); +} finally { + fs.writeFileSync(path.join(out, process.env.A3_PILOT ? 'PILOT.json' : 'BROWSER.json'), JSON.stringify({ ...report, limits: ['Every API response is intercepted; no real account, password, email delivery, verification, MFA or provider activation.', 'Synthetic confirmation tokens and challenge values cannot qualify server-side token, rate-limit or authentication enforcement.', 'No native/human, integrated security, release or production qualification.'] }, null, 2)); await browser.close(); +} diff --git a/job-tracker-ui/src/components/TwoFactorChallenge.tsx b/job-tracker-ui/src/components/TwoFactorChallenge.tsx index bdfe7b6c..cb2885ee 100644 --- a/job-tracker-ui/src/components/TwoFactorChallenge.tsx +++ b/job-tracker-ui/src/components/TwoFactorChallenge.tsx @@ -20,7 +20,7 @@ export default function TwoFactorChallenge({ const [code, setCode] = useState(""); const [trustDevice, setTrustDevice] = useState(false); const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); + const [error, setError] = useState<{ cause: unknown; rateLimited: boolean } | null>(null); async function submit() { setLoading(true); @@ -29,11 +29,7 @@ export default function TwoFactorChallenge({ const res = await api.post("/auth/2fa/challenge", { pendingToken, code, trustDevice }); onSuccess(res.data); } catch (e: any) { - if (e?.response?.status === 429) { - setError(t("twoFactorRateLimited")); - } else { - setError(getApiErrorMessage(e, t("twoFactorInvalidCode"))); - } + setError({ cause: e, rateLimited: e?.response?.status === 429 }); } finally { setLoading(false); } @@ -54,7 +50,7 @@ export default function TwoFactorChallenge({ {t("twoFactorHint")} - {error ? {error} : null} + {error ? {error.rateLimited ? t("twoFactorRateLimited") : getApiErrorMessage(error.cause, t("twoFactorInvalidCode"))} : null} {language} {t("settings")} + ); @@ -46,6 +47,28 @@ test("hydrates an authenticated server preference without writing it back", () = expect(api.put).not.toHaveBeenCalled(); }); +test("a public local-only choice persists without authenticated requests", () => { + render(); + fireEvent.click(screen.getByRole("button", { name: "Local Norsk" })); + expect(screen.getByLabelText("language")).toHaveTextContent("nb"); + expect(localStorage.getItem("uiLanguage")).toBe("nb"); + expect(document.documentElement.lang).toBe("nb-NO"); + expect(api.put).not.toHaveBeenCalled(); + expect(api.get).not.toHaveBeenCalled(); +}); + +test("a local-only choice invalidates an older same-language account-save retry", async () => { + let rejectOld!: (cause: Error) => void; + (api.put as jest.Mock).mockImplementationOnce(() => new Promise((_, reject) => { rejectOld = reject; })); + render(); + fireEvent.click(screen.getByRole("button", { name: "Norsk" })); + fireEvent.click(screen.getByRole("button", { name: "Local Norsk" })); + await act(async () => rejectOld(new Error("offline"))); + expect(screen.queryByRole("alert")).not.toBeInTheDocument(); + expect(api.put).toHaveBeenCalledTimes(1); + expect(screen.getByLabelText("language")).toHaveTextContent("nb"); +}); + test.each([ ["Norsk", "nb", "nb-NO", "Språket er endret på denne enheten, men kunne ikke lagres på kontoen din.", "Prøv igjen"], ["English", "en", "en", "The language changed on this device, but your account preference could not be saved.", "Retry"], diff --git a/job-tracker-ui/src/i18n/I18nProvider.tsx b/job-tracker-ui/src/i18n/I18nProvider.tsx index 13f8a258..6a291528 100644 --- a/job-tracker-ui/src/i18n/I18nProvider.tsx +++ b/job-tracker-ui/src/i18n/I18nProvider.tsx @@ -8,6 +8,7 @@ type TranslationParams = Record; type Ctx = { language: UiLanguage; setLanguage: (l: UiLanguage) => void; + setLocalLanguage: (l: UiLanguage) => void; hydrateLanguage: (language?: string | null) => void; t: (key: TranslationKey, params?: TranslationParams) => string; }; @@ -35,9 +36,14 @@ export function I18nProvider({ children }: { children: React.ReactNode }) { document.documentElement.lang = l === "nb" ? "nb-NO" : "en"; }, []); - const setLanguage = (l: UiLanguage) => { + const setLocalLanguage = (l: UiLanguage) => { + ++languageSaveSequence.current; applyLanguage(l); - const requestSequence = ++languageSaveSequence.current; + }; + + const setLanguage = (l: UiLanguage) => { + setLocalLanguage(l); + const requestSequence = languageSaveSequence.current; void api.put("/auth/preferences/language", { language: l === "nb" ? "nb-NO" : "en" }).catch(() => { if (requestSequence !== languageSaveSequence.current || currentLanguage.current !== l) return; feedback?.toast(translations[l].settingsLanguageSaveFailed, "error", { @@ -66,7 +72,7 @@ export function I18nProvider({ children }: { children: React.ReactNode }) { const t = (key: TranslationKey, params?: TranslationParams) => interpolate(translations[language][key] ?? translations.en[key], params); - return {children}; + return {children}; } export function useI18n() { diff --git a/job-tracker-ui/src/layout/AuthPageFrame.tsx b/job-tracker-ui/src/layout/AuthPageFrame.tsx index f90de1ad..b3eb6b3e 100644 --- a/job-tracker-ui/src/layout/AuthPageFrame.tsx +++ b/job-tracker-ui/src/layout/AuthPageFrame.tsx @@ -2,6 +2,8 @@ import React, { useId } from "react"; import { Box, Paper } from "@mui/material"; import { PageHeader } from "./PageLayout"; +import { StackLanguageToggle } from "./AppShell"; +import { useI18n } from "../i18n/I18nProvider"; // Public auth pages own their forms, commands and token lifecycles. export default function AuthPageFrame({ title, description, children }: { @@ -10,6 +12,7 @@ export default function AuthPageFrame({ title, description, children }: { children: React.ReactNode; }) { const titleId = useId(); + const { language, setLocalLanguage } = useI18n(); return ( header": { flexWrap: "nowrap", gap: 1 }, + "& > header > div:last-child": { flexShrink: 0 }, }}> - + } /> {children} diff --git a/job-tracker-ui/src/public-auth-presentation.test.tsx b/job-tracker-ui/src/public-auth-presentation.test.tsx index c8b0e9a9..cd579b4f 100644 --- a/job-tracker-ui/src/public-auth-presentation.test.tsx +++ b/job-tracker-ui/src/public-auth-presentation.test.tsx @@ -11,6 +11,7 @@ import LoginPage from './views/LoginPage'; import MicrosoftLegacyRelinkPage from './views/MicrosoftLegacyRelinkPage'; import ResetPasswordPage from './views/ResetPasswordPage'; import VerifyEmailPage from './views/VerifyEmailPage'; +import { translations } from './i18n/translations'; const mockNavigate = jest.fn(); jest.mock('react-router-dom', () => ({ @@ -47,6 +48,100 @@ beforeEach(() => { }); describe('public auth presentation', () => { + it.each([ + { view: , path: '/login', kind: 'login', messageKey: 'signedIn' as const }, + { view: , path: '/forgot-password', kind: 'request', messageKey: 'passwordResetRequestSent' as const }, + { view: , path: '/reset-password?email=synthetic%40example.test&token=synthetic', kind: 'reset', messageKey: 'passwordResetSuccess' as const }, + ])('announces a pending $kind result in the current UI language without replay', async ({ view, path, kind, messageKey }) => { + const pending = deferred(); mockedApi.post.mockReturnValue(pending.promise); + mount(view, path); + if (kind === 'login') { + await screen.findByText('Authentication is required to use this app.'); + fireEvent.change(screen.getByLabelText('Username or email'), { target: { value: 'synthetic@example.test' } }); + fireEvent.change(screen.getByLabelText('Current password'), { target: { value: 'Synthetic pending example' } }); + } else if (kind === 'request') fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'synthetic@example.test' } }); + else { + fireEvent.change(screen.getByLabelText('New password'), { target: { value: 'Synthetic pending example' } }); + fireEvent.change(screen.getByLabelText('Confirm password'), { target: { value: 'Synthetic pending example' } }); + } + submit(); + fireEvent.click(screen.getByRole('button', { name: 'Norsk bokmål' })); + await act(async () => pending.resolve({ data: {} })); + expect(screen.getByRole('alert')).toHaveTextContent(translations.nb[messageKey]); + expect(mockedApi.post).toHaveBeenCalledTimes(1); + }); + + it.each([ + { view: , path: '/login', errorKey: 'usernameOrEmailRequired' as const }, + { view: , path: '/register', errorKey: 'emailRequired' as const }, + { view: , path: '/forgot-password', errorKey: 'emailRequired' as const }, + { view: , path: '/reset-password?email=synthetic%40example.test&token=synthetic', errorKey: 'passwordTooShort' as const }, + ])('translates existing client validation on $path without submitting again', async ({ view, path, errorKey }) => { + mount(view, path); + if (path === '/login' || path === '/register') await screen.findByText('Authentication is required to use this app.'); + submit(); + expect(await screen.findByText(translations.en[errorKey])).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: 'Norsk bokmål' })); + expect(screen.getByText(translations.nb[errorKey])).toBeVisible(); + expect(mockedApi.post).not.toHaveBeenCalled(); + }); + + it.each([ + { view: , path: '/login', kind: 'login', errorKey: 'loginFailed' as const }, + { view: , path: '/forgot-password', kind: 'request', errorKey: 'passwordResetRequestFailed' as const }, + { view: , path: '/reset-password?email=synthetic%40example.test&token=synthetic', kind: 'reset', errorKey: 'resetFailed' as const }, + ])('translates a failed $kind fallback while keeping its submitted draft and request count', async ({ view, path, kind, errorKey }) => { + mockedApi.post.mockRejectedValue({ response: { status: 503, data: '' } }); + mount(view, path); + if (kind === 'login') { + await screen.findByText('Authentication is required to use this app.'); + fireEvent.change(screen.getByLabelText('Username or email'), { target: { value: 'synthetic@example.test' } }); + fireEvent.change(screen.getByLabelText('Current password'), { target: { value: 'Synthetic rejected example' } }); + } else if (kind === 'request') fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'synthetic@example.test' } }); + else { + fireEvent.change(screen.getByLabelText('New password'), { target: { value: 'Synthetic rejected example' } }); + fireEvent.change(screen.getByLabelText('Confirm password'), { target: { value: 'Synthetic rejected example' } }); + } + submit(); + expect(await screen.findByRole('alert')).toHaveTextContent(translations.en[errorKey]); + fireEvent.click(screen.getByRole('button', { name: 'Norsk bokmål' })); + expect(screen.getByRole('alert')).toHaveTextContent(translations.nb[errorKey]); + expect(mockedApi.post).toHaveBeenCalledTimes(1); + expect(screen.getByLabelText(kind === 'login' ? translations.nb.usernameOrEmail : kind === 'request' ? translations.nb.profileEmail : translations.nb.profileNewPassword)).toHaveValue(kind === 'reset' ? 'Synthetic rejected example' : 'synthetic@example.test'); + }); + + it('translates an MFA rate limit without another challenge or losing the entered code', async () => { + mockedApi.post.mockResolvedValueOnce({ data: { requiresTwoFactor: true, pendingToken: 'synthetic-pending' } } as any).mockRejectedValueOnce({ response: { status: 429 } }); + mount(, '/login'); + await screen.findByText('Authentication is required to use this app.'); + fireEvent.change(screen.getByLabelText('Username or email'), { target: { value: 'synthetic@example.test' } }); + fireEvent.change(screen.getByLabelText('Current password'), { target: { value: 'Synthetic challenge example' } }); + submit(); + const code = await screen.findByLabelText(translations.en.twoFactorCodeLabel); + fireEvent.change(code, { target: { value: '000000' } }); + submit(); + expect(await screen.findByRole('alert')).toHaveTextContent(translations.en.twoFactorRateLimited); + fireEvent.click(screen.getByRole('button', { name: 'Norsk bokmål' })); + expect(screen.getByRole('alert')).toHaveTextContent(translations.nb.twoFactorRateLimited); + expect(screen.getByLabelText(translations.nb.twoFactorCodeLabel)).toHaveValue('000000'); + expect(mockedApi.post).toHaveBeenCalledTimes(2); + expect(mockNavigate).not.toHaveBeenCalled(); + }); + + it('lets a signed-out user choose a persisted UI language without discarding a sign-in draft', async () => { + mount(, '/login'); + await screen.findByText('Authentication is required to use this app.'); + fireEvent.change(screen.getByLabelText('Username or email'), { target: { value: 'synthetic@example.test' } }); + fireEvent.change(screen.getByLabelText('Current password'), { target: { value: 'Unsubmitted synthetic example' } }); + fireEvent.click(screen.getByRole('button', { name: 'Norsk bokmål' })); + expect(screen.getByRole('heading', { level: 1, name: translations.nb.signInTitle })).toBeVisible(); + expect(screen.getByLabelText(translations.nb.usernameOrEmail)).toHaveValue('synthetic@example.test'); + expect(screen.getByLabelText(translations.nb.profileCurrentPassword)).toHaveValue('Unsubmitted synthetic example'); + expect(localStorage.getItem('uiLanguage')).toBe('nb'); + expect(mockedApi.get).toHaveBeenCalledTimes(1); + expect(mockedApi.post).not.toHaveBeenCalled(); + expect(mockedApi.put).not.toHaveBeenCalled(); + }); it.each([ { mode: 'login' as const, label: 'Current password', autocomplete: 'current-password', absent: 'New password' }, { mode: 'register' as const, label: 'New password', autocomplete: 'new-password', absent: 'Current password' }, diff --git a/job-tracker-ui/src/verify-email-page.test.tsx b/job-tracker-ui/src/verify-email-page.test.tsx index 0f996a92..11238cd4 100644 --- a/job-tracker-ui/src/verify-email-page.test.tsx +++ b/job-tracker-ui/src/verify-email-page.test.tsx @@ -1,11 +1,12 @@ import React from 'react'; import '@testing-library/jest-dom'; -import { render, screen } from '@testing-library/react'; +import { act, render, screen } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import VerifyEmailPage from './views/VerifyEmailPage'; import { I18nProvider } from './i18n/I18nProvider'; import { api, getApiErrorMessage } from './api'; +import { translations } from './i18n/translations'; const mockedApi = api as jest.Mocked; // CRA's jest config sets resetMocks: true, which wipes the initial implementation given to @@ -26,6 +27,7 @@ function renderVerifyEmailPage(search: string, emailChange = false) { describe('VerifyEmailPage', () => { beforeEach(() => { + localStorage.clear(); mockedApi.post.mockReset(); mockedGetApiErrorMessage.mockImplementation((e: any, fallback?: string) => { const data = e?.response?.data; @@ -65,4 +67,49 @@ describe('VerifyEmailPage', () => { expect(await screen.findByText('Your email has changed. Sign in again with the new address.')).toBeInTheDocument(); expect(mockedApi.post).toHaveBeenCalledWith('/auth/email-change/confirm', { userId: 'user-1', email: 'new@example.com', token: 'change-token' }); }); + + it.each([false, true])('changing UI language never resubmits a pending confirmation: emailChange=%s', async emailChange => { + let finish!: (value: any) => void; + mockedApi.post.mockReturnValue(new Promise(resolve => { finish = resolve; })); + renderVerifyEmailPage('?userId=user-1&email=new%40example.test&token=one-use', emailChange); + expect(mockedApi.post).toHaveBeenCalledTimes(1); + act(() => window.dispatchEvent(new StorageEvent('storage', { key: 'uiLanguage', newValue: 'nb' }))); + const pendingCalls = mockedApi.post.mock.calls.length; + await act(async () => finish({ data: {} })); + expect(pendingCalls).toBe(1); + expect(mockedApi.post).toHaveBeenCalledTimes(1); + expect(screen.getByRole('status')).toHaveTextContent(translations.nb[emailChange ? 'confirmEmailChangeSuccess' : 'verifyEmailSuccess']); + act(() => window.dispatchEvent(new StorageEvent('storage', { key: 'uiLanguage', newValue: 'en' }))); + expect(screen.getByRole('status')).toHaveTextContent(translations.en[emailChange ? 'confirmEmailChangeSuccess' : 'verifyEmailSuccess']); + expect(mockedApi.post).toHaveBeenCalledTimes(1); + }); + + it('localizes missing-link errors without a confirmation request', async () => { + renderVerifyEmailPage(''); + expect(await screen.findByText(translations.en.missingVerifyLinkInfo)).toBeInTheDocument(); + act(() => window.dispatchEvent(new StorageEvent('storage', { key: 'uiLanguage', newValue: 'nb' }))); + expect(screen.getByRole('alert')).toHaveTextContent(translations.nb.missingVerifyLinkInfo); + expect(mockedApi.post).not.toHaveBeenCalled(); + }); + + it('localizes a failed confirmation fallback without retrying the one-time command', async () => { + mockedApi.post.mockRejectedValue({ response: { status: 503, data: '' } }); + renderVerifyEmailPage('?userId=user-1&token=one-use'); + expect(await screen.findByText(translations.en.verifyEmailFailed)).toBeInTheDocument(); + await act(async () => window.dispatchEvent(new StorageEvent('storage', { key: 'uiLanguage', newValue: 'nb' }))); + expect(screen.getByRole('alert')).toHaveTextContent(translations.nb.verifyEmailFailed); + expect(mockedApi.post).toHaveBeenCalledTimes(1); + }); + + it('does not let an older confirmation replace a different flow missing-link error', async () => { + let finish!: (value: any) => void; + mockedApi.post.mockReturnValue(new Promise(resolve => { finish = resolve; })); + const view = renderVerifyEmailPage('?userId=user-1&token=one-use'); + window.history.replaceState({}, '', '/confirm-email-change?userId=user-1&token=one-use'); + view.rerender(); + expect(await screen.findByText(translations.en.missingEmailChangeLinkInfo)).toBeInTheDocument(); + await act(async () => finish({ data: {} })); + expect(screen.getByRole('alert')).toHaveTextContent(translations.en.missingEmailChangeLinkInfo); + expect(mockedApi.post).toHaveBeenCalledTimes(1); + }); }); diff --git a/job-tracker-ui/src/views/ForgotPasswordPage.tsx b/job-tracker-ui/src/views/ForgotPasswordPage.tsx index 19ff2fdc..b531639f 100644 --- a/job-tracker-ui/src/views/ForgotPasswordPage.tsx +++ b/job-tracker-ui/src/views/ForgotPasswordPage.tsx @@ -7,18 +7,20 @@ import { useNavigate } from "react-router-dom"; import { api, getApiErrorMessage } from "../api"; import { useToast } from "../toast"; import { useI18n } from "../i18n/I18nProvider"; +import type { TranslationKey } from "../i18n/translations"; import AuthPageFrame from "../layout/AuthPageFrame"; export default function ForgotPasswordPage() { const { toast } = useToast(); const { t } = useI18n(); + const currentTranslation = useRef(t); currentTranslation.current = t; const navigate = useNavigate(); const [email, setEmail] = useState(""); const [loading, setLoading] = useState(false); const [submitted, setSubmitted] = useState(false); - const [emailError, setEmailError] = useState(undefined); - const [submitError, setSubmitError] = useState(null); + const [emailError, setEmailError] = useState(undefined); + const [submitError, setSubmitError] = useState<{ cause: unknown } | null>(null); const emailRef = useRef(null); const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; @@ -35,12 +37,12 @@ export default function ForgotPasswordPage() { e.preventDefault(); const trimmed = email.trim(); if (!trimmed) { - setEmailError(t("emailRequired")); + setEmailError("emailRequired"); emailRef.current?.focus(); return; } if (!EMAIL_PATTERN.test(trimmed)) { - setEmailError(t("invalidEmail")); + setEmailError("invalidEmail"); emailRef.current?.focus(); return; } @@ -52,17 +54,17 @@ export default function ForgotPasswordPage() { .post("/auth/request-password-reset", { email: email.trim() }) .then(() => { setSubmitted(true); - toast(t("passwordResetRequestSent"), "success"); + toast(currentTranslation.current("passwordResetRequestSent"), "success"); }) .catch((error: any) => { - setSubmitError(getApiErrorMessage(error, t("passwordResetRequestFailed"))); + setSubmitError({ cause: error }); }) .finally(() => setLoading(false)); }} sx={{ display: "flex", flexDirection: "column", gap: 1.5 }} > {submitted ? {t("passwordResetRequestSent")} : null} - {submitError ? {submitError} : null} + {submitError ? {getApiErrorMessage(submitError.cause, t("passwordResetRequestFailed"))} : null} diff --git a/job-tracker-ui/src/views/LoginPage.tsx b/job-tracker-ui/src/views/LoginPage.tsx index b1c3b55e..9549a40e 100644 --- a/job-tracker-ui/src/views/LoginPage.tsx +++ b/job-tracker-ui/src/views/LoginPage.tsx @@ -12,6 +12,7 @@ import TwoFactorChallenge from "../components/TwoFactorChallenge"; import TurnstileWidget from "../components/TurnstileWidget"; import { useToast } from "../toast"; import { useI18n } from "../i18n/I18nProvider"; +import type { TranslationKey } from "../i18n/translations"; import AuthPageFrame from "../layout/AuthPageFrame"; type AuthConfig = { @@ -28,6 +29,7 @@ type AuthConfig = { export default function LoginPage({ initialMode = "login" }: { initialMode?: "login" | "register" }) { const { toast } = useToast(); const { t } = useI18n(); + const currentTranslation = useRef(t); currentTranslation.current = t; const navigate = useNavigate(); const location = useLocation() as any; @@ -44,10 +46,10 @@ export default function LoginPage({ initialMode = "login" }: { initialMode?: "lo const [emailNotVerified, setEmailNotVerified] = useState(false); const [resendingVerification, setResendingVerification] = useState(false); const [verificationResent, setVerificationResent] = useState(false); - const [fieldErrors, setFieldErrors] = useState<{ email?: string; password?: string; confirmPassword?: string }>({}); + const [fieldErrors, setFieldErrors] = useState<{ email?: TranslationKey; password?: TranslationKey; confirmPassword?: TranslationKey }>({}); const [registerMode, setRegisterMode] = useState(initialMode === "register"); const [turnstileToken, setTurnstileToken] = useState(""); - const [submitError, setSubmitError] = useState(null); + const [submitError, setSubmitError] = useState<{ cause: unknown; fallback: TranslationKey } | null>(null); const emailRef = useRef(null); const passwordRef = useRef(null); const confirmPasswordRef = useRef(null); @@ -78,20 +80,20 @@ export default function LoginPage({ initialMode = "login" }: { initialMode?: "lo async function completeLogin() { setAuthPersistencePreference(rememberMe ? "local" : "session"); await api.get("/auth/me"); - toast(t("signedIn"), "success"); + toast(currentTranslation.current("signedIn"), "success"); navigate(nextPath, nextOptions); } function validate(mode: "login" | "register") { - const errors: { email?: string; password?: string; confirmPassword?: string } = {}; - if (!email.trim()) errors.email = mode === "login" ? t("usernameOrEmailRequired") : t("emailRequired"); - else if (mode === "register" && !EMAIL_PATTERN.test(email.trim())) errors.email = t("invalidEmail"); + const errors: { email?: TranslationKey; password?: TranslationKey; confirmPassword?: TranslationKey } = {}; + if (!email.trim()) errors.email = mode === "login" ? "usernameOrEmailRequired" : "emailRequired"; + else if (mode === "register" && !EMAIL_PATTERN.test(email.trim())) errors.email = "invalidEmail"; - if (!password) errors.password = t("passwordRequired"); - else if (mode === "register" && password.length < 8) errors.password = t("passwordTooShort"); + if (!password) errors.password = "passwordRequired"; + else if (mode === "register" && password.length < 8) errors.password = "passwordTooShort"; if (mode === "register" && password && confirmPassword !== password) { - errors.confirmPassword = t("passwordsDoNotMatch"); + errors.confirmPassword = "passwordsDoNotMatch"; } setFieldErrors(errors); @@ -113,7 +115,7 @@ export default function LoginPage({ initialMode = "login" }: { initialMode?: "lo const res = await api.post<{ requiresTwoFactor?: boolean; pendingToken?: string; verificationRequired?: boolean }>(url, payload); if (res.data?.verificationRequired) { setEmailNotVerified(true); - toast(t("registerCheckEmailForVerification"), "info"); + toast(currentTranslation.current("registerCheckEmailForVerification"), "info"); return; } if (res.data?.requiresTwoFactor && res.data.pendingToken) { @@ -125,7 +127,7 @@ export default function LoginPage({ initialMode = "login" }: { initialMode?: "lo if (mode === "login" && e?.response?.data?.error === "email_not_verified") { setEmailNotVerified(true); } else { - setSubmitError(getApiErrorMessage(e, t("loginFailed"))); + setSubmitError({ cause: e, fallback: "loginFailed" }); } } finally { setLoading(false); @@ -138,9 +140,9 @@ export default function LoginPage({ initialMode = "login" }: { initialMode?: "lo try { await api.post("/auth/resend-verification-email", { email }); setVerificationResent(true); - toast(t("verificationEmailResent"), "success"); + toast(currentTranslation.current("verificationEmailResent"), "success"); } catch (e: any) { - setSubmitError(getApiErrorMessage(e, t("verifyEmailFailed"))); + setSubmitError({ cause: e, fallback: "verifyEmailFailed" }); } finally { setResendingVerification(false); } @@ -169,7 +171,7 @@ export default function LoginPage({ initialMode = "login" }: { initialMode?: "lo onSubmit={(e) => { e.preventDefault(); void submit(registerMode ? "register" : "login"); }} sx={{ display: "flex", flexDirection: "column", gap: 1.5 }} > - {submitError ? {submitError} : null} + {submitError ? {getApiErrorMessage(submitError.cause, t(submitError.fallback))} : null} {registerMode && cfg && !allowReg ? {t("registrationUnavailable")} : null} {cfg?.requireEmailVerification && emailNotVerified && ( {registerMode && ( @@ -215,7 +217,7 @@ export default function LoginPage({ initialMode = "login" }: { initialMode?: "lo inputRef={confirmPasswordRef} type="password" error={Boolean(fieldErrors.confirmPassword)} - helperText={fieldErrors.confirmPassword} + helperText={fieldErrors.confirmPassword ? t(fieldErrors.confirmPassword) : undefined} fullWidth /> )} diff --git a/job-tracker-ui/src/views/MicrosoftLegacyRelinkPage.tsx b/job-tracker-ui/src/views/MicrosoftLegacyRelinkPage.tsx index 49518de1..99a7f0dd 100644 --- a/job-tracker-ui/src/views/MicrosoftLegacyRelinkPage.tsx +++ b/job-tracker-ui/src/views/MicrosoftLegacyRelinkPage.tsx @@ -13,7 +13,7 @@ export default function MicrosoftLegacyRelinkPage() { const navigate = useNavigate(); const [working, setWorking] = useState(false); const [success, setSuccess] = useState(false); - const [error, setError] = useState(null); + const [error, setError] = useState<{ cause: unknown } | null>(null); const params = new URLSearchParams(window.location.search); const userId = params.get("userId") || ""; const tenantId = params.get("tenantId") || ""; @@ -39,7 +39,7 @@ export default function MicrosoftLegacyRelinkPage() { }); setSuccess(true); } catch (e: any) { - setError(getApiErrorMessage(e, t("microsoftLegacyRelinkFailed"))); + setError({ cause: e }); } finally { setWorking(false); } @@ -48,7 +48,7 @@ export default function MicrosoftLegacyRelinkPage() { return ( {missing ? {t("microsoftLegacyRelinkMissing")} : null} - {error ? {error} : null} + {error ? {getApiErrorMessage(error.cause, t("microsoftLegacyRelinkFailed"))} : null} {success ? {t("microsoftLegacyRelinkSuccess")} : null} button": { maxWidth: "100%", whiteSpace: "normal", overflowWrap: "anywhere" } }}> diff --git a/job-tracker-ui/src/views/ResetPasswordPage.tsx b/job-tracker-ui/src/views/ResetPasswordPage.tsx index f34f498b..96c16861 100644 --- a/job-tracker-ui/src/views/ResetPasswordPage.tsx +++ b/job-tracker-ui/src/views/ResetPasswordPage.tsx @@ -7,11 +7,13 @@ import { useNavigate } from "react-router-dom"; import { api, getApiErrorMessage } from "../api"; import { useToast } from "../toast"; import { useI18n } from "../i18n/I18nProvider"; +import type { TranslationKey } from "../i18n/translations"; import AuthPageFrame from "../layout/AuthPageFrame"; export default function ResetPasswordPage() { const { toast } = useToast(); const { t } = useI18n(); + const currentTranslation = useRef(t); currentTranslation.current = t; const navigate = useNavigate(); const [email, setEmail] = useState(""); @@ -19,8 +21,8 @@ export default function ResetPasswordPage() { const [newPassword, setNewPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [loading, setLoading] = useState(false); - const [fieldErrors, setFieldErrors] = useState<{ newPassword?: string; confirmPassword?: string }>({}); - const [submitError, setSubmitError] = useState(null); + const [fieldErrors, setFieldErrors] = useState<{ newPassword?: TranslationKey; confirmPassword?: TranslationKey }>({}); + const [submitError, setSubmitError] = useState<{ cause: unknown } | null>(null); const passwordRef = useRef(null); const confirmPasswordRef = useRef(null); @@ -42,9 +44,9 @@ export default function ResetPasswordPage() { toast(t("missingResetLinkInfo"), "error"); return; } - const errors: { newPassword?: string; confirmPassword?: string } = {}; - if (!newPassword || newPassword.length < 8) errors.newPassword = t("passwordTooShort"); - if (confirmPassword !== newPassword) errors.confirmPassword = t("passwordsDoNotMatch"); + const errors: { newPassword?: TranslationKey; confirmPassword?: TranslationKey } = {}; + if (!newPassword || newPassword.length < 8) errors.newPassword = "passwordTooShort"; + if (confirmPassword !== newPassword) errors.confirmPassword = "passwordsDoNotMatch"; setFieldErrors(errors); if (Object.keys(errors).length > 0) { (errors.newPassword ? passwordRef : confirmPasswordRef).current?.focus(); @@ -56,18 +58,18 @@ export default function ResetPasswordPage() { api .post("/auth/reset-password", { email, token, newPassword }) .then(() => { - toast(t("passwordResetSuccess"), "success"); + toast(currentTranslation.current("passwordResetSuccess"), "success"); navigate("/login", { replace: true }); }) .catch((e2: any) => { - setSubmitError(getApiErrorMessage(e2, t("resetFailed"))); + setSubmitError({ cause: e2 }); }) .finally(() => setLoading(false)); }} sx={{ display: "flex", flexDirection: "column", gap: 1.5 }} > {missingResetInfo ? {t("missingResetLinkInfo")} : null} - {submitError ? {submitError} : null} + {submitError ? {getApiErrorMessage(submitError.cause, t("resetFailed"))} : null} setEmail(e.target.value)} disabled={!missingResetInfo} fullWidth /> diff --git a/job-tracker-ui/src/views/VerifyEmailPage.tsx b/job-tracker-ui/src/views/VerifyEmailPage.tsx index 9e557305..3c4d3a5e 100644 --- a/job-tracker-ui/src/views/VerifyEmailPage.tsx +++ b/job-tracker-ui/src/views/VerifyEmailPage.tsx @@ -15,9 +15,13 @@ export default function VerifyEmailPage({ emailChange = false }: { emailChange?: const navigate = useNavigate(); const [status, setStatus] = useState("verifying"); - const [errorMessage, setErrorMessage] = useState(null); + const [verificationError, setVerificationError] = useState(null); + const [missingLink, setMissingLink] = useState(false); useEffect(() => { + setStatus("verifying"); + setMissingLink(false); + setVerificationError(null); const params = new URLSearchParams(window.location.search); const userId = params.get("userId") || ""; const email = params.get("email") || ""; @@ -25,18 +29,21 @@ export default function VerifyEmailPage({ emailChange = false }: { emailChange?: if (!userId || !token || (emailChange && !email)) { setStatus("error"); - setErrorMessage(t(emailChange ? "missingEmailChangeLinkInfo" : "missingVerifyLinkInfo")); + setMissingLink(true); return; } + let active = true; api .post(emailChange ? "/auth/email-change/confirm" : "/auth/verify-email", emailChange ? { userId, email, token } : { userId, token }) - .then(() => setStatus("success")) - .catch((e: any) => { + .then(() => { if (active) setStatus("success"); }) + .catch((cause: unknown) => { + if (!active) return; setStatus("error"); - setErrorMessage(getApiErrorMessage(e, t("verifyEmailFailed"))); + setVerificationError(cause); }); - }, [emailChange, t]); + return () => { active = false; }; + }, [emailChange]); return ( @@ -48,7 +55,7 @@ export default function VerifyEmailPage({ emailChange = false }: { emailChange?: )} {status === "success" && {t(emailChange ? "confirmEmailChangeSuccess" : "verifyEmailSuccess")}} - {status === "error" && {errorMessage}} + {status === "error" && {missingLink ? t(emailChange ? "missingEmailChangeLinkInfo" : "missingVerifyLinkInfo") : getApiErrorMessage(verificationError, t("verifyEmailFailed"))}} button": { maxWidth: "100%", whiteSpace: "normal", overflowWrap: "anywhere" } }}>