diff --git a/admin/src/App.test.tsx b/admin/src/App.test.tsx index 7a38e6a..942a2fb 100644 --- a/admin/src/App.test.tsx +++ b/admin/src/App.test.tsx @@ -2,10 +2,29 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { cleanup, fireEvent, render, screen } from "@testing-library/react"; import App from "./App"; +function jsonResponse(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json" }, + }); +} + +let settingsResponse: () => Promise; + describe("NestQuest Admin shell", () => { beforeEach(() => { - // The Today tab fetches the admin snapshot; keep the shell tests offline. - vi.stubGlobal("fetch", vi.fn(() => new Promise(() => {}))); + settingsResponse = () => + Promise.resolve(jsonResponse({ timezone: "", effective_timezone: "Europe/London" })); + // The shell loads the stored application timezone; every tab's own + // requests stay pending so the shell tests remain offline. + vi.stubGlobal( + "fetch", + vi.fn((url: string) => + String(url).endsWith("/api/v1/admin/settings") + ? settingsResponse() + : new Promise(() => {}), + ), + ); }); afterEach(() => { @@ -22,10 +41,10 @@ describe("NestQuest Admin shell", () => { } }); - it("starts on the Today screen and switches on tab click", () => { + it("starts on the Today screen and switches on tab click", async () => { render(); - expect(screen.getByRole("heading", { name: "Today" })).toBeTruthy(); + expect(await screen.findByRole("heading", { name: "Today" })).toBeTruthy(); fireEvent.click(screen.getByRole("tab", { name: "History" })); @@ -36,12 +55,36 @@ describe("NestQuest Admin shell", () => { ); }); - it("the Tasks tab renders the definitions screen", () => { + it("the Tasks tab renders the definitions screen", async () => { render(); fireEvent.click(screen.getByRole("tab", { name: "Tasks" })); - expect(screen.getByRole("heading", { name: "Tasks" })).toBeTruthy(); + expect(await screen.findByRole("heading", { name: "Tasks" })).toBeTruthy(); expect(screen.getByRole("tabpanel").textContent).not.toContain("placeholder"); }); -}); \ No newline at end of file + + it("renders no screen until the stored application timezone is known", async () => { + settingsResponse = () => new Promise(() => {}); + render(); + + expect(screen.getByTestId("timezone-loading")).toBeTruthy(); + expect(screen.queryByRole("heading", { name: "Today" })).toBeNull(); + }); + + it("a failed timezone load says so and retries", async () => { + settingsResponse = () => Promise.resolve(jsonResponse({ detail: "boom" }, 500)); + render(); + + await screen.findByTestId("timezone-error"); + expect(screen.queryByRole("heading", { name: "Today" })).toBeNull(); + + settingsResponse = () => + Promise.resolve( + jsonResponse({ timezone: "Pacific/Auckland", effective_timezone: "Pacific/Auckland" }), + ); + fireEvent.click(screen.getByRole("button", { name: "Try again" })); + + expect(await screen.findByRole("heading", { name: "Today" })).toBeTruthy(); + }); +}); diff --git a/admin/src/App.tsx b/admin/src/App.tsx index 5f44fad..31ab78a 100644 --- a/admin/src/App.tsx +++ b/admin/src/App.tsx @@ -4,6 +4,7 @@ import { TransitionsProvider } from "./api/useTransitions"; import DefinitionsTab from "./definitions/DefinitionsTab"; import HistoryTab from "./history/HistoryTab"; import ScheduleTab from "./schedule/ScheduleTab"; +import { AppTimezoneProvider } from "./time/AppTimezone"; import TodayTab from "./today/TodayTab"; const TABS = [ @@ -32,15 +33,18 @@ export default function App() { role="tabpanel" aria-labelledby={`tab-${active}`} > - {active === "today" ? ( - - ) : active === "tasks" ? ( - - ) : active === "schedule" ? ( - - ) : ( - - )} + {/* Every screen's local time is the stored application timezone. */} + + {active === "today" ? ( + + ) : active === "tasks" ? ( + + ) : active === "schedule" ? ( + + ) : ( + + )} +