diff --git a/docs/i18n/SESSION_BOUNDARY_MESSAGE.md b/docs/i18n/SESSION_BOUNDARY_MESSAGE.md new file mode 100644 index 00000000..fc645efa --- /dev/null +++ b/docs/i18n/SESSION_BOUNDARY_MESSAGE.md @@ -0,0 +1,11 @@ +# Session-change message localisation — 2 November 2026 + +Implemented in the existing App invalidation branch. Reuses the existing `sessionChangedCleared` English/Bokmål catalogue message. The canonical document language (`nb-NO`, set by I18nProvider) selects Bokmål; other values safely select English. Reading it occurs only in the invalidated client branch. No provider, hook, storage access, request, dialog or private callback is retained to obtain the message. + +Callers: root App renders Application only while valid; invalidation replaces it with a main alert and preserves the existing passive redirect to /login?sessionChanged=1. I18nProvider already publishes the current UI language to the document; generated-content language is unrelated. All routes, auth/CSRF/context tokens, abort logic and provider teardown remain unchanged. No new translation key or dependency. + +RED: two new root-render tests failed against the previous literal English alert. PASSED after patch: 17 tests in session-boundary-message.test.tsx and session-boundary.test.ts, zero failures/pending, exit 0. New cases verify EN/NB feedback outside the private provider tree and zero API requests. Existing cases exercise cross-tab/cookie invalidation, abort/stale callbacks and session-context handling. These are local Jest/DOM tests; jsdom navigation is not browser redirect qualification. + +PASSED: full TypeScript typecheck; scoped ESLint of App.tsx and the new test, zero warnings. Local ignored native Jest result: job-tracker-ui/.artifacts/session-boundary-message.json. No fresh full build/browser or complete security acceptance claimed. Rollback: revert the two source/test files in this package; no data changes. The attachment-label fix remains independently preserved. + +The original literal-inventory App.tsx entry is now implemented; the inventory remains a historical snapshot. Final integrated security regression must run after relevant application changes are complete. diff --git a/job-tracker-ui/src/App.tsx b/job-tracker-ui/src/App.tsx index 90037ab0..309fdf0b 100644 --- a/job-tracker-ui/src/App.tsx +++ b/job-tracker-ui/src/App.tsx @@ -29,6 +29,7 @@ import JobTable from "./components/JobTable"; import NotificationsPopover from "./components/NotificationsPopover"; import type { JobTableColumns } from "./components/JobTable"; import { I18nProvider, useI18n } from "./i18n/I18nProvider"; +import { translations } from "./i18n/translations"; import LoginPage from "./views/LoginPage"; import LandingPage from "./views/LandingPage"; import ForgotPasswordPage from "./views/ForgotPasswordPage"; @@ -485,7 +486,7 @@ export default function App() { }, [invalidated]); // Unmount providers as well as routes: confirmations/toasts can retain private text and // callbacks. Passive effect navigation runs after the old forms' unload guards are removed. - return invalidated ?
Session changed. Clearing this page…
: ; + return invalidated ?
{translations[document.documentElement.lang === "nb-NO" ? "nb" : "en"].sessionChangedCleared}
: ; } function Application() { diff --git a/job-tracker-ui/src/session-boundary-message.test.tsx b/job-tracker-ui/src/session-boundary-message.test.tsx new file mode 100644 index 00000000..b55805ba --- /dev/null +++ b/job-tracker-ui/src/session-boundary-message.test.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import App from "./App"; +import { I18nProvider } from "./i18n/I18nProvider"; +import { api } from "./api"; + +jest.mock("./sessionBoundary", () => ({ + isSessionInvalidated: () => true, + SESSION_INVALIDATED_EVENT: "session-invalidated", +})); +jest.mock("./i18n/I18nProvider", () => ({ + ...jest.requireActual("./i18n/I18nProvider"), + I18nProvider: jest.fn(() => { throw new Error("Invalidated sessions must not mount providers"); }), +})); + +test.each([ + ["en", "Your session changed. Private data and unsaved edits were cleared from this page. Sign in again to continue."], + ["nb-NO", "Økten din ble endret. Private data og ulagrede endringer ble fjernet fra denne siden. Logg inn igjen for å fortsette."], +])("invalidated session shows %s feedback outside the private provider tree", (language, message) => { + const previousLanguage = document.documentElement.lang; + document.documentElement.lang = language; + try { + render(); + expect(screen.getByRole("alert")).toHaveTextContent(message); + expect(I18nProvider).not.toHaveBeenCalled(); + expect(api.get).not.toHaveBeenCalled(); + } finally { + document.documentElement.lang = previousLanguage; + } +});