+
{bandShown ? null : (
withinPath(path, root));
}
+/**
+ * Whether a clinical page shows the bell on a tablet or desktop (11 Oct 2026
+ * usefulness and layout audit, Josh's go-ahead): the big-screen Clinical header
+ * matches Work's, so the bell sits there too. Phones keep the clinical header
+ * as it is. First Nations stays without it (its own governed header), and the
+ * staff work modes use `staffWorkBellVisible`.
+ */
+export function clinicalWideBellVisible(modeId: AppModeId, pathname: string | null | undefined): boolean {
+ if (!pathname || STAFF_WORK_BELL_MODES.has(modeId) || modeId === "first-nations") return false;
+ return true;
+}
+
/** True on the Notifications page or anything below it. */
export function isNotificationsPath(pathname: string | null | undefined): boolean {
if (!pathname) return false;
diff --git a/tests/cme-applications-cv-page.dom.test.tsx b/tests/cme-applications-cv-page.dom.test.tsx
index e034aa7ecc..a3fbbe48df 100644
--- a/tests/cme-applications-cv-page.dom.test.tsx
+++ b/tests/cme-applications-cv-page.dom.test.tsx
@@ -10,6 +10,14 @@ import { CPD_APPLICATIONS_STORAGE_KEY, TEACHING_TERM_TRACKER_STORAGE_KEY } from
import type { CmeEntry } from "@/lib/cme/types";
import * as clipboard from "@/lib/copy-to-clipboard";
+// The page draws its Back in the top bar's left slot (one way back), so the test provides that slot.
+function addHeaderBackSlot() {
+ const slot = document.createElement("div");
+ slot.id = "universal-header-leading";
+ document.body.appendChild(slot);
+ return () => slot.remove();
+}
+
const refresh = vi.fn();
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn(), refresh }),
@@ -269,10 +277,12 @@ describe("CV that fills itself", () => {
expect(stored.hiddenCvLines).toEqual(["talk:o1"]);
});
- it("goes back to Job applications, its parent", () => {
+ it("goes back to Job applications, its parent, from the top bar", () => {
+ const removeSlot = addHeaderBackSlot();
renderCv();
expect(screen.getByTestId("cpd-feature-back").getAttribute("href")).toBe("/admin/applications");
- expect(screen.getByTestId("cpd-feature-back").textContent).toContain("Job applications");
+ expect(screen.getByTestId("cpd-feature-back").getAttribute("aria-label")).toBe("Back to Job applications");
+ removeSlot();
});
it("narrows to this year", () => {
diff --git a/tests/cme-cpd-home-send-page.dom.test.tsx b/tests/cme-cpd-home-send-page.dom.test.tsx
index a0a74217f0..9ee50b58f1 100644
--- a/tests/cme-cpd-home-send-page.dom.test.tsx
+++ b/tests/cme-cpd-home-send-page.dom.test.tsx
@@ -10,6 +10,14 @@ import { CPD_HOME_SEND_STORAGE_KEY, clearAccountScopedBrowserStorage } from "@/l
import type { CmeEntry, CmeRequirementSet } from "@/lib/cme/types";
import * as clipboard from "@/lib/copy-to-clipboard";
+// The page draws its Back in the top bar's left slot (one way back), so the test provides that slot.
+function addHeaderBackSlot() {
+ const slot = document.createElement("div");
+ slot.id = "universal-header-leading";
+ document.body.appendChild(slot);
+ return () => slot.remove();
+}
+
const announcer = vi.hoisted(() => ({ announce: vi.fn() }));
vi.mock("@/components/ui/live-announcer", async (importOriginal) => ({
...(await importOriginal()),
@@ -301,11 +309,13 @@ describe("Send to AMA CPD Home", () => {
});
it("goes back to the CPD summary, where its link lives, even with nothing to send", () => {
+ const removeSlot = addHeaderBackSlot();
const { unmount } = renderPage();
expect(screen.getByTestId("cpd-feature-back").getAttribute("href")).toBe("/cme/summary");
unmount();
renderPage({ entries: [] });
expect(screen.getByTestId("cpd-feature-back").getAttribute("href")).toBe("/cme/summary");
+ removeSlot();
});
it("says the import format is not confirmed, with its source pending", () => {
diff --git a/tests/mode-pages-sheet-classes.test.ts b/tests/mode-pages-sheet-classes.test.ts
index c70d4dedad..42f6c2ecfd 100644
--- a/tests/mode-pages-sheet-classes.test.ts
+++ b/tests/mode-pages-sheet-classes.test.ts
@@ -56,8 +56,8 @@ describe("mode pages sheet recipes", () => {
});
it("keeps the pill's page and mode lines at 600", () => {
- // All five text lines of the pill: page, mode, a work area's name alone
- // (pill 4b), the "Mode" eyebrow and the mode name. The owner's rule is
+ // All four text lines of the pill: page, mode, a work area's name alone
+ // (pill 4b) and the mode name (the "Mode" eyebrow went on 11 Oct 2026). The owner's rule is
// nothing heavier than 600 in the shared pill.
const source = readFileSync(
path.join(process.cwd(), "src/components/clinical-dashboard/master-search-header.tsx"),
@@ -67,6 +67,6 @@ describe("mode pages sheet recipes", () => {
const pill = source.slice(start, source.indexOf(" {
expect(screen.queryByTestId("needs-you-bell")).toBeNull();
});
+ it("arms the clinical bell's count on idle only for a signed-in reader", () => {
+ vi.useFakeTimers();
+ const matchMedia = window.matchMedia;
+ window.matchMedia = ((query: string) => ({
+ matches: true,
+ media: query,
+ addEventListener: () => undefined,
+ removeEventListener: () => undefined,
+ })) as unknown as typeof window.matchMedia;
+ try {
+ centre.summary = { count: 2, overdue: 0 };
+ nav.pathname = "/psychiatry";
+ auth.status = "unconfigured";
+ const demo = render();
+ expect(screen.getByTestId("needs-you-bell")).toBeTruthy();
+ act(() => {
+ vi.advanceTimersByTime(5000);
+ });
+ expect(screen.queryByTestId("needs-you-badge")).toBeNull();
+ demo.unmount();
+ auth.status = "authenticated";
+ render();
+ act(() => {
+ vi.advanceTimersByTime(5000);
+ });
+ expect(screen.getByTestId("needs-you-badge").textContent).toBe("2");
+ } finally {
+ window.matchMedia = matchMedia;
+ vi.useRealTimers();
+ }
+ });
+
it("shows on On Call home and Who's on", () => {
nav.pathname = "/on-call";
const { unmount } = render();
diff --git a/tests/needs-you-homes.test.ts b/tests/needs-you-homes.test.ts
index 4aa3228ab3..36fa850b4f 100644
--- a/tests/needs-you-homes.test.ts
+++ b/tests/needs-you-homes.test.ts
@@ -4,6 +4,7 @@ import {
isNotificationsPath,
isStaffWorkHomePath,
needsYouBellVisibleForAuth,
+ clinicalWideBellVisible,
staffWorkBellVisible,
} from "@/lib/needs-you/homes";
import { groupNeedsYouItems, needsYouWaitingCopy } from "@/lib/needs-you/groups";
@@ -70,6 +71,16 @@ describe("where the header bell shows", () => {
expect(staffWorkBellVisible("my-day", undefined)).toBe(false);
});
+ it("shows on clinical pages for the big-screen header, never First Nations or the work modes", () => {
+ // 11 Oct 2026 audit: the tablet and desktop Clinical header matches Work's.
+ expect(clinicalWideBellVisible("answer", "/")).toBe(true);
+ expect(clinicalWideBellVisible("dsm", "/dsm/search")).toBe(true);
+ expect(clinicalWideBellVisible("psychiatry", "/psychiatry")).toBe(true);
+ expect(clinicalWideBellVisible("first-nations", "/first-nations")).toBe(false);
+ expect(clinicalWideBellVisible("roster", "/roster")).toBe(false);
+ expect(clinicalWideBellVisible("answer", null)).toBe(false);
+ });
+
it("knows the Notifications page and the pages below it", () => {
expect(isNotificationsPath("/my-day/notifications")).toBe(true);
expect(isNotificationsPath("/my-day/notifications/")).toBe(true);
diff --git a/tests/roster-sick-page.dom.test.tsx b/tests/roster-sick-page.dom.test.tsx
index ef96fcb949..440a7c1f6f 100644
--- a/tests/roster-sick-page.dom.test.tsx
+++ b/tests/roster-sick-page.dom.test.tsx
@@ -32,6 +32,14 @@ vi.mock("@/components/ui/live-announcer", () => ({ announce: mocks.announce }));
import { RosterSickPage } from "@/components/roster/sick/roster-sick-page";
+// The page draws its Back in the top bar's left slot (one way back), so the test provides that slot.
+function addHeaderBackSlot() {
+ const slot = document.createElement("div");
+ slot.id = "universal-header-leading";
+ document.body.appendChild(slot);
+ return () => slot.remove();
+}
+
// Tue 6 Oct 2026, 19:30 in Perth.
const NOW = new Date("2026-10-06T11:30:00Z");
const ME = "5e000000-0000-4000-8000-000000000001";
@@ -335,11 +343,14 @@ it("the example team refuses to send and says to phone", async () => {
expect(mocks.post).not.toHaveBeenCalled();
});
-it("has a visible way back to Roster", async () => {
+it("has one way back to Roster, in the top bar", async () => {
+ const removeSlot = addHeaderBackSlot();
await renderPage();
const back = screen.getByTestId("roster-sick-back");
expect(back.getAttribute("href")).toBe("/roster");
- expect(back.textContent).toBe("Roster");
+ expect(back.getAttribute("aria-label")).toBe("Back to Roster");
+ expect(back.closest("#universal-header-leading")).not.toBeNull();
+ removeSlot();
});
it("the personal leave link sits on its own line under the text", async () => {
diff --git a/tests/roster-staffing-page.dom.test.tsx b/tests/roster-staffing-page.dom.test.tsx
index fac3e9149d..935f704888 100644
--- a/tests/roster-staffing-page.dom.test.tsx
+++ b/tests/roster-staffing-page.dom.test.tsx
@@ -58,6 +58,14 @@ vi.mock("@/lib/copy-to-clipboard", () => ({ copyTextToClipboard: (text: string)
import { RosterStaffingPage } from "@/components/roster/staffing/roster-staffing-page";
+// The page draws its Back in the top bar's left slot (one way back), so the test provides that slot.
+function addHeaderBackSlot() {
+ const slot = document.createElement("div");
+ slot.id = "universal-header-leading";
+ document.body.appendChild(slot);
+ return () => slot.remove();
+}
+
const NOW = new Date("2026-10-06T11:30:00Z"); // Tue 6 Oct, 19:30 Perth
const ME = "5e000000-0000-4000-8000-000000000001";
const SERVICE = "5e000000-0000-4000-8000-000000000003";
@@ -129,15 +137,18 @@ it("shows the next three weeks, each day read out in words, and says no safe num
expect(screen.queryByTestId("staffing-plan")).toBeNull();
});
-it("has a visible way back to Roster, and the span sits on its own line under the head", () => {
+it("has one way back to Roster in the top bar, and the span sits on its own line under the head", () => {
+ const removeSlot = addHeaderBackSlot();
render();
const back = screen.getByTestId("roster-staffing-back");
expect(back.getAttribute("href")).toBe("/roster");
- expect(back.textContent).toBe("Roster");
+ expect(back.getAttribute("aria-label")).toBe("Back to Roster");
+ expect(back.closest("#universal-header-leading")).not.toBeNull();
const meta = screen.getByTestId("staffing-meta");
expect(meta.textContent).toContain("Next 3 weeks");
expect(meta.textContent).toContain("Rechecked");
expect(meta.closest("h2")).toBeNull();
+ removeSlot();
});
it("typed dates show your leave, the fewest on, other dates and a Plan this leave hand-off", async () => {
diff --git a/tests/teaching-term-folder.dom.test.tsx b/tests/teaching-term-folder.dom.test.tsx
index cec2dddcd7..bdc044d536 100644
--- a/tests/teaching-term-folder.dom.test.tsx
+++ b/tests/teaching-term-folder.dom.test.tsx
@@ -19,6 +19,14 @@ import { sampleTermTracker } from "@/lib/teaching/term-tracker";
import { authState } from "./helpers/teaching-auth";
import { NOW, json, serveFetch, useTeachingTestClock } from "./helpers/teaching-fixtures";
+// The page draws its Back in the top bar's left slot (one way back), so the test provides that slot.
+function addHeaderBackSlot() {
+ const slot = document.createElement("div");
+ slot.id = "universal-header-leading";
+ document.body.appendChild(slot);
+ return () => slot.remove();
+}
+
// eslint-disable-next-line react-hooks/rules-of-hooks
useTeachingTestClock(NOW);
@@ -180,10 +188,12 @@ describe("term evidence folder page", () => {
);
});
- it("has a visible way back to Term", async () => {
+ it("has one way back to Term, in the top bar", async () => {
+ const removeSlot = addHeaderBackSlot();
render();
expect(await screen.findByTestId("term-folder-back")).toHaveAttribute("href", "/teaching/term");
- expect(screen.getByRole("link", { name: "Term" })).toBeVisible();
+ expect(screen.getByRole("link", { name: "Back to Term" })).toBeVisible();
+ removeSlot();
});
it("says in the copied demo summary that it is made up", async () => {
diff --git a/tests/work-search-header.test.ts b/tests/work-search-header.test.ts
index 9fa4e1f135..02e303b5d3 100644
--- a/tests/work-search-header.test.ts
+++ b/tests/work-search-header.test.ts
@@ -53,6 +53,7 @@ describe("AI Search header icon", () => {
const header = readFileSync("src/components/clinical-dashboard/master-search-header.tsx", "utf8");
expect(header.match(/@\/components\/(?:work-search|needs-you)\/[\w-]+/g)).toEqual([
"@/components/needs-you/staff-work-header-controls",
+ "@/components/needs-you/needs-you-button",
]);
const controls = readFileSync("src/components/needs-you/staff-work-header-controls.tsx", "utf8");