import { ReminderSettings } from "./components/ReminderSettings"; import { Modal } from "./components/design-system/Modal"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, spyOn, test, } from "bun:test"; import { Window } from "happy-dom"; import { act, StrictMode, useState } from "react"; import { MemoryRouter, useLocation, useNavigate } from "react-router"; import type { Root } from "react-dom/client"; import { App } from "./App"; import { CalendarHeatmap } from "./components/design-system/CalendarHeatmap"; import { fixture } from "./habits/test-fixture"; // Keep the design system independent of authentication and the local database. const dom = new Window({ url: "http://localhost:3000/" }); const originalGlobals = new Map(); let createRoot: typeof import("react-dom/client").createRoot; let root: Root; let container: HTMLDivElement; let fetchMock: ReturnType>; let apiFixture: ReturnType | undefined; beforeAll(async () => { for (const key of [ "window", "document", "navigator", "HTMLElement", "HTMLInputElement", "Element", "Node", "Event", "MouseEvent", "IS_REACT_ACT_ENVIRONMENT", ]) { originalGlobals.set(key, Object.getOwnPropertyDescriptor(globalThis, key)); Object.defineProperty(globalThis, key, { configurable: true, writable: true, value: key === "window" ? dom : key === "IS_REACT_ACT_ENVIRONMENT" ? true : (dom as unknown as Record)[key], }); } ({ createRoot } = await import("react-dom/client")); }); beforeEach(() => { container = document.createElement("div"); document.body.append(container); root = createRoot(container); fetchMock = spyOn(globalThis, "fetch").mockResolvedValue( new Response(null, { status: 401 }), ); }); afterEach(async () => { await act(async () => root.unmount()); container.remove(); fetchMock.mockRestore(); apiFixture?.close(); apiFixture = undefined; }); afterAll(() => { dom.happyDOM.abort(); for (const [key, descriptor] of originalGlobals) { if (descriptor) Object.defineProperty(globalThis, key, descriptor); else Reflect.deleteProperty(globalThis, key); } }); function LocationProbe() { const location = useLocation(); const navigate = useNavigate(); return <> {location.pathname} {location.hash} ; } async function render(path = "/design-system") { await act(async () => root.render( , ), ); } function connectAccount() { apiFixture = fixture(); const f = apiFixture; fetchMock.mockImplementation((async (input, init) => { const headers = new Headers(init?.headers); headers.set("Cookie", `minabot_session=${"a".repeat(43)}`); headers.set("Origin", f.origin); return f.app.request(new Request(new URL(String(input), f.origin), { ...init, headers })); }) as typeof fetch); return f; } const buttonNamed = (name: string) => [...container.querySelectorAll("button")] .find(button => button.textContent === name || button.getAttribute("aria-label") === name)!; describe("homepage", () => { test("development Strict Mode does not restore background focus while a modal is open", async () => { await act(async () => root.render( {}} initialFocus="input">)); await new Promise(resolve => dom.requestAnimationFrame(() => dom.requestAnimationFrame(resolve))); expect(document.activeElement).toBe(container.querySelector("input")); expect(container.querySelector("dialog")?.open).toBe(true); expect(document.documentElement.style.overflow).toBe("hidden"); }); test("settings open as a modal and Escape restores the opener and scrolling", async () => { connectAccount(); await render("/"); const opener = buttonNamed("Settings"); opener.focus(); await act(async () => opener.click()); const dialog = container.querySelector("dialog#account-settings")!; expect(dialog.open).toBe(true); expect(dialog.contains(document.activeElement)).toBe(true); expect(document.documentElement.style.overflow).toBe("hidden"); await act(async () => dialog.dispatchEvent(new dom.Event("cancel", { cancelable: true }) as unknown as Event)); await new Promise(resolve => dom.requestAnimationFrame(resolve)); expect(container.querySelector("dialog")).toBeNull(); expect(document.activeElement).toBe(opener); expect(document.documentElement.style.overflow).toBe(""); }); test("modal dismissal waits for saves and nested dialogs retain the scroll lock", async () => { function Harness() { const [nested, setNested] = useState(true); const [busy, setBusy] = useState(true); return {}}> {nested && setNested(false)}> } ; } await act(async () => root.render()); const inner = container.querySelector("#inner")!; await act(async () => inner.dispatchEvent(new dom.Event("cancel", { cancelable: true }) as unknown as Event)); expect(inner.open).toBe(true); expect(inner.querySelector('[aria-label="Close dialog"]')?.disabled).toBe(true); await act(async () => buttonNamed("Finish save").click()); await act(async () => inner.dispatchEvent(new dom.Event("cancel", { cancelable: true }) as unknown as Event)); expect(container.querySelector("#inner")).toBeNull(); expect(container.querySelector("#outer")?.open).toBe(true); expect(document.documentElement.style.overflow).toBe("hidden"); await act(async () => root.render(null)); expect(document.documentElement.style.overflow).toBe(""); }); test("archived habits expose history and restore to the dashboard", async () => { const f = connectAccount(); f.setTime("2026-09-03T12:00:00Z"); const h = await f.json("/habits", "POST", { name: "Reading", method: "manual" }, 201); await f.json(`/habits/${h.id}/days/2026-09-03/progress`, "PUT", { done: true }); f.setTime("2026-09-04T12:00:00Z"); await f.request(`/habits/${h.id}`, "DELETE"); await render("/"); await act(async () => buttonNamed("Archived habits").click()); expect(buttonNamed("Restore Reading")).toBeDefined(); await act(async () => buttonNamed("View history").click()); expect(container.querySelector('[aria-label="2026-09-03: 1 of 1 completion · Complete"]')).not.toBeNull(); await act(async () => buttonNamed("Restore Reading").click()); expect(container.textContent).toContain("No archived habits."); expect((await f.json("/today")).habits[0].name).toBe("Reading"); expect((await f.json(`/habits/${h.id}/days/2026-09-03`)).complete).toBe(true); }); test("historical check-ins use the selected date's tracking method and preserve today", async () => { const f = connectAccount(); f.setTime("2026-09-03T12:00:00Z"); const habit = await f.json("/habits", "POST", { name: "Reading", method: "manual" }, 201); f.setTime("2026-09-04T12:00:00Z"); await f.json(`/habits/${habit.id}`, "PATCH", { method: "count", target: 10, unit: "pages" }); await render("/"); expect(container.querySelector(".ds-calendar")).toBeNull(); expect(buttonNamed("View history for Reading").getAttribute("aria-expanded")).toBe("false"); await act(async () => buttonNamed("View history for Reading").click()); expect(buttonNamed("Hide history for Reading").getAttribute("aria-expanded")).toBe("true"); await act(async () => container.querySelector('[aria-label^="2026-09-03:"]')!.click()); expect(buttonNamed("Edit selected check-in").closest(".ds-date-inspector")?.textContent).toContain("September 3, 2026"); await act(async () => buttonNamed("Edit selected check-in").click()); const past = [...container.querySelectorAll('input[type="checkbox"]')].find(input => input.closest("label")?.textContent === "Completed on this date")!; expect(past).toBeDefined(); await act(async () => past.click()); expect((await f.json(`/habits/${habit.id}/days/2026-09-03`)).complete).toBe(true); expect((await f.json(`/habits/${habit.id}/days/2026-09-04`)).value).toBe(0); expect(container.textContent).toContain("Saved check-in for 2026-09-03"); }); test("signed-out visitors can try progress without calling private habit APIs", async () => { await render("/"); expect(container.querySelector("h1")?.textContent).toBe("Small steps.Lasting rhythm."); expect(fetchMock.mock.calls.map(call => call[0])).toEqual(["/api/me"]); await act(async () => buttonNamed("Increase glasses of water").click()); expect(container.querySelector('[aria-label="Total glasses of water"]')?.value).toBe("4"); expect(fetchMock).toHaveBeenCalledTimes(1); }); test("OAuth errors are retained at the homepage and unknown routes go home", async () => { await render("/?auth_error=denied"); expect(container.querySelector('[role="alert"]')?.textContent).toContain("cancelled"); // A new router is needed when changing initialEntries after mount. await act(async () => root.unmount()); root = createRoot(container); await render("/missing"); expect(container.querySelector('[data-testid="pathname"]')?.textContent).toBe("/"); expect(container.querySelector("#welcome-title")).not.toBeNull(); }); test("does not flash the signed-out page while the account is loading", async () => { let resolve!: (response: Response) => void; fetchMock.mockImplementation(Object.assign(() => new Promise(done => { resolve = done; }), { preconnect: fetch.preconnect })); await render("/"); expect(container.textContent).toContain("Getting things ready"); expect(container.querySelector("#welcome-title")).toBeNull(); await act(async () => resolve(new Response(null, { status: 401 }))); expect(container.querySelector("#welcome-title")).not.toBeNull(); }); test("account failures offer retry without pretending the user is signed out", async () => { fetchMock.mockResolvedValueOnce(new Response(null, { status: 500 })); await render("/"); expect(container.textContent).toContain("Account unavailable"); expect(container.querySelector("#welcome-title")).toBeNull(); await act(async () => buttonNamed("Try again").click()); expect(container.querySelector("#welcome-title")).not.toBeNull(); }); test("signed-in users create a habit from the empty state and sign out", async () => { const f = connectAccount(); await render("/"); expect(container.textContent).toContain("Welcome back, alice."); expect(container.textContent).toContain("Start with one habit."); await act(async () => buttonNamed("Add a habit +").click()); const input = container.querySelector("form input")!; expect(document.activeElement).toBe(input); await act(async () => { Object.getOwnPropertyDescriptor(dom.HTMLInputElement.prototype, "value")!.set!.call(input, "Read a little"); input.dispatchEvent(new dom.Event("input", { bubbles: true }) as unknown as Event); }); await act(async () => container.querySelector("form")!.dispatchEvent(new dom.Event("submit", { bubbles: true, cancelable: true }) as unknown as Event)); expect((await f.json("/habits")).habits[0].name).toBe("Read a little"); expect(container.querySelector("form")).toBeNull(); expect(container.textContent).toContain("Read a little created."); expect(container.querySelector(".ds-habit-chart")).not.toBeNull(); await act(async () => buttonNamed("Sign out").click()); expect(container.querySelector("#welcome-title")).not.toBeNull(); expect((await f.request("/me")).status).toBe(401); }); test("count, manual, and task progress persist and update today's completion", async () => { const f = connectAccount(); const water = await f.json("/habits", "POST", { name: "Water", method: "count", target: 8, unit: "glasses" }, 201); await f.json(`/habits/${water.id}/days/2026-09-04/progress`, "PUT", { count: 7 }); await f.json("/habits", "POST", { name: "Reading", method: "manual" }, 201); await f.json("/habits", "POST", { name: "Evening", method: "tasks", tasks: [{ name: "Stretch" }] }, 201); await f.json("/habits", "POST", { name: "Sunday walk", method: "manual", schedule: { type: "weekdays", days: [0] } }, 201); await render("/"); expect(container.querySelector(".ds-welcome-progress")?.textContent).toContain("0 / 3"); await act(async () => buttonNamed("Increase Water").click()); expect(container.querySelector(".ds-welcome-progress")?.textContent).toContain("1 / 3"); expect(container.querySelector(`#history-${water.id} .ds-date-inspector`)).toBeNull(); await act(async () => buttonNamed("View history for Water").click()); expect(container.querySelector(`#history-${water.id} .ds-date-inspector`)?.textContent).toContain("8 of 8 glasses"); expect(container.textContent).not.toContain("Sunday walk"); await act(async () => buttonNamed("All habits · 4").click()); const checkbox = (name: string) => [...container.querySelectorAll('input[type="checkbox"]')].find(input => (input.getAttribute("aria-label") || input.closest("label")?.textContent) === name)!; expect(checkbox("Sunday walk done").disabled).toBe(true); await act(async () => checkbox("Reading done").click()); await act(async () => checkbox("Stretch").click()); expect(container.querySelector(".ds-welcome-progress")?.textContent).toContain("3 / 3"); expect((await f.json("/today")).completed).toBe(3); await act(async () => buttonNamed("Decrease Water").click()); expect((await f.json("/today")).completed).toBe(2); }); test("inline habit edits persist configuration and delete archives without losing earlier history", async () => { const f = connectAccount(); f.setTime("2026-09-03T12:00:00Z"); const habit = await f.json("/habits", "POST", { name: "Water", method: "count", target: 8, unit: "glasses" }, 201); await f.json(`/habits/${habit.id}/days/2026-09-03/progress`, "PUT", { count: 5 }); f.setTime("2026-09-04T12:00:00Z"); await render("/"); await act(async () => buttonNamed("Edit habit Water").click()); const form = container.querySelector('form[aria-label="Edit habit Water"]')!; expect(document.activeElement).toBe(form.querySelector("input")); await act(async () => { const name = form.querySelector("input")!; Object.getOwnPropertyDescriptor(dom.HTMLInputElement.prototype, "value")!.set!.call(name, "Daily water"); name.dispatchEvent(new dom.Event("input", { bubbles: true }) as unknown as Event); const target = form.querySelector('input[type="number"]')!; Object.getOwnPropertyDescriptor(dom.HTMLInputElement.prototype, "value")!.set!.call(target, "10"); target.dispatchEvent(new dom.Event("input", { bubbles: true }) as unknown as Event); }); await act(async () => form.dispatchEvent(new dom.Event("submit", { bubbles: true, cancelable: true }) as unknown as Event)); const saved = await f.json(`/habits/${habit.id}`); expect(saved.name).toBe("Daily water"); expect(saved.target).toBe(10); expect(container.querySelector("form")).toBeNull(); expect(container.querySelector("h3")?.textContent).toContain("Daily water"); await act(async () => buttonNamed("Archive habit Daily water").click()); expect((await f.json("/habits")).habits).toHaveLength(1); await act(async () => buttonNamed("Cancel").click()); expect((await f.json("/habits")).habits).toHaveLength(1); await act(async () => buttonNamed("Archive habit Daily water").click()); await act(async () => buttonNamed("Archive habit").click()); expect((await f.json("/habits")).habits).toHaveLength(0); expect(container.querySelector(".ds-habit-chart")).toBeNull(); const previous = await f.json(`/habits/${habit.id}/days/2026-09-03`); expect(previous.name).toBe("Water"); expect(previous.target).toBe(8); expect(previous.value).toBe(5); }); test("inline task rename keeps its completion and schedule; deletion updates totals and preserves yesterday", async () => { const f = connectAccount(); f.setTime("2026-09-03T12:00:00Z"); const habit = await f.json("/habits", "POST", { name: "Evening", method: "tasks", tasks: [{ name: "Stretch", schedule: { type: "weekdays", days: [4, 5] } }, { name: "Clear desk" }] }, 201); const task = habit.tasks[0]; await f.json(`/habits/${habit.id}/days/2026-09-03/tasks/${task.id}`, "PUT", { done: true }); f.setTime("2026-09-04T12:00:00Z"); await f.json(`/habits/${habit.id}/days/2026-09-04/tasks/${task.id}`, "PUT", { done: true }); await render("/"); await act(async () => buttonNamed("Edit task Stretch").click()); const form = container.querySelector('form[aria-label="Edit task Stretch"]')!; await act(async () => { const input = form.querySelector("input")!; Object.getOwnPropertyDescriptor(dom.HTMLInputElement.prototype, "value")!.set!.call(input, "Stretch gently"); input.dispatchEvent(new dom.Event("input", { bubbles: true }) as unknown as Event); }); await act(async () => form.dispatchEvent(new dom.Event("submit", { bubbles: true, cancelable: true }) as unknown as Event)); const saved = await f.json(`/habits/${habit.id}/tasks/${task.id}`); expect(saved.name).toBe("Stretch gently"); expect(saved.schedule).toEqual({ type: "weekdays", days: [4, 5] }); expect((await f.json("/today")).habits[0].value).toBe(1); await act(async () => buttonNamed("Delete task Stretch gently").click()); await act(async () => buttonNamed("Delete task").click()); const today = (await f.json("/today")).habits[0]; expect(today.target).toBe(1); expect(today.value).toBe(0); expect(container.textContent).not.toContain("Stretch gently"); const previous = await f.json(`/habits/${habit.id}/days/2026-09-03`); expect(previous.tasks.find((item: { taskId: string }) => item.taskId === task.id).done).toBe(true); expect(previous.tasks.find((item: { taskId: string }) => item.taskId === task.id).name).toBe("Stretch"); }); test("task edits retain failed drafts for retry and remain available on days off", async () => { const f = connectAccount(); const habit = await f.json("/habits", "POST", { name: "Sunday", method: "tasks", schedule: { type: "weekdays", days: [0] }, tasks: [{ name: "Walk" }] }, 201); await render("/"); expect(container.querySelector(".ds-habit-chart")).toBeNull(); await act(async () => buttonNamed("All habits · 1").click()); expect(container.textContent).toContain("Not scheduled today"); expect(container.querySelector('.ds-editable-task input[type="checkbox"]')?.disabled).toBe(true); await act(async () => buttonNamed("Edit task Walk").click()); const form = container.querySelector('form[aria-label="Edit task Walk"]')!; await act(async () => { const input = form.querySelector("input")!; Object.getOwnPropertyDescriptor(dom.HTMLInputElement.prototype, "value")!.set!.call(input, "Long walk"); input.dispatchEvent(new dom.Event("input", { bubbles: true }) as unknown as Event); }); fetchMock.mockResolvedValueOnce(new Response(JSON.stringify({ error: "Please try again" }), { status: 500 })); await act(async () => form.dispatchEvent(new dom.Event("submit", { bubbles: true, cancelable: true }) as unknown as Event)); expect(form.querySelector("input")?.value).toBe("Long walk"); expect(form.querySelector('[role="alert"]')?.textContent).toBe("Please try again"); expect((await f.json(`/habits/${habit.id}/tasks`)).tasks[0].name).toBe("Walk"); await act(async () => form.dispatchEvent(new dom.Event("submit", { bubbles: true, cancelable: true }) as unknown as Event)); expect((await f.json(`/habits/${habit.id}/tasks`)).tasks[0].name).toBe("Long walk"); expect(container.querySelector("form")).toBeNull(); }); test("task recurrence edits round-trip every schedule type and preserve earlier check-ins", async () => { const f = connectAccount(); f.setTime("2026-09-03T12:00:00Z"); const habit = await f.json("/habits", "POST", { name: "Routine", method: "tasks", tasks: [{ name: "Walk" }] }, 201); const id = habit.tasks[0].id; await f.json(`/habits/${habit.id}/days/2026-09-03/tasks/${id}`, "PUT", { done: true }); f.setTime("2026-09-04T12:00:00Z"); await render("/"); const inputValue = async (input: HTMLInputElement, value: string) => act(async () => { Object.getOwnPropertyDescriptor(dom.HTMLInputElement.prototype, "value")!.set!.call(input, value); input.dispatchEvent(new dom.Event("input", { bubbles: true }) as unknown as Event); }); const selectValue = async (select: HTMLSelectElement, value: string) => act(async () => { select.value = value; select.dispatchEvent(new dom.Event("change", { bubbles: true }) as unknown as Event); }); for (const schedule of [ { type: "interval", every: 3, anchor: "2026-09-05" }, { type: "weekly", every: 2, anchor: "2026-09-04", weekday: 5 }, { type: "weekdays", days: [1, 2, 3, 4, 5] }, { type: "daily" }, ] as const) { await act(async () => buttonNamed("Edit task Walk").click()); const form = container.querySelector('form[aria-label="Edit task Walk"]')!; await selectValue(form.querySelector("select")!, schedule.type); if (schedule.type === "interval" || schedule.type === "weekly") { await inputValue(form.querySelector('input[type="number"]')!, String(schedule.every)); await inputValue(form.querySelector('input[type="date"]')!, schedule.anchor); } if (schedule.type === "weekly") await selectValue(form.querySelectorAll("select")[1]!, String(schedule.weekday)); await act(async () => form.dispatchEvent(new dom.Event("submit", { bubbles: true, cancelable: true }) as unknown as Event)); expect((await f.json(`/habits/${habit.id}/tasks/${id}`)).schedule).toEqual(schedule); if (schedule.type === "interval") expect(buttonNamed("All habits · 1").getAttribute("aria-pressed")).toBe("true"); expect(container.querySelector("form")).toBeNull(); await act(async () => buttonNamed("Edit task Walk").click()); const reopened = container.querySelector('form[aria-label="Edit task Walk"]')!; expect(reopened.querySelector("select")?.value).toBe(schedule.type); if (schedule.type === "interval" || schedule.type === "weekly") { expect(reopened.querySelector('input[type="number"]')?.value).toBe(String(schedule.every)); expect(reopened.querySelector('input[type="date"]')?.value).toBe(schedule.anchor); } await act(async () => buttonNamed("Cancel").click()); } const previous = await f.json(`/habits/${habit.id}/days/2026-09-03`); expect(previous.tasks[0].done).toBe(true); expect(previous.requirements.tasks[0].schedule).toEqual({ type: "daily" }); }); test("habit method and carryover controls save, and task conversion supports adding recurring tasks", async () => { const f = connectAccount(); const habit = await f.json("/habits", "POST", { name: "Reading", method: "manual" }, 201); await render("/"); const selectMethod = async (method: string) => { const select = container.querySelector('form select')!; await act(async () => { select.value = method; select.dispatchEvent(new dom.Event("change", { bubbles: true }) as unknown as Event); }); }; const submit = async () => act(async () => container.querySelector("form")!.dispatchEvent(new dom.Event("submit", { bubbles: true, cancelable: true }) as unknown as Event)); await act(async () => buttonNamed("Edit habit Reading").click()); await selectMethod("count"); await act(async () => container.querySelector('form input[type="checkbox"]')!.click()); await submit(); expect((await f.json(`/habits/${habit.id}`)).method).toBe("count"); expect((await f.json(`/habits/${habit.id}`)).carryPartialProgress).toBe(true); await act(async () => buttonNamed("Edit habit Reading").click()); expect(container.querySelector('form input[type="checkbox"]')?.checked).toBe(true); await selectMethod("tasks"); await submit(); expect((await f.json(`/habits/${habit.id}`)).method).toBe("tasks"); await act(async () => buttonNamed("Add task +").click()); const form = container.querySelector('form[aria-label="Add task"]')!; await act(async () => { const input = form.querySelector("input")!; Object.getOwnPropertyDescriptor(dom.HTMLInputElement.prototype, "value")!.set!.call(input, "Read chapter"); input.dispatchEvent(new dom.Event("input", { bubbles: true }) as unknown as Event); }); await submit(); const task = (await f.json(`/habits/${habit.id}/tasks`)).tasks[0]; expect(task.name).toBe("Read chapter"); expect(task.schedule).toEqual({ type: "daily" }); expect(buttonNamed("Edit task Read chapter")).toBeDefined(); expect((await f.json("/today")).habits[0].target).toBe(1); }); test("a successful delete is not retried when the subsequent refresh fails", async () => { const f = connectAccount(); const habit = await f.json("/habits", "POST", { name: "Reading", method: "manual" }, 201); await render("/"); await act(async () => buttonNamed("Archive habit Reading").click()); fetchMock.mockImplementationOnce((async (input, init) => { return f.request(String(input).replace("/api", ""), init?.method); }) as typeof fetch); fetchMock.mockResolvedValueOnce(new Response(null, { status: 500 })); await act(async () => buttonNamed("Archive habit").click()); expect(container.querySelector("form")).toBeNull(); expect(container.textContent).toContain("Your change was saved, but the dashboard could not refresh"); expect(buttonNamed("Archive habit Reading").disabled).toBe(true); expect((await f.json(`/habits/${habit.id}`)).archived).toBe(true); await act(async () => buttonNamed("Try again").click()); expect(container.querySelector(".ds-habit-chart")).toBeNull(); expect(fetchMock.mock.calls.filter(call => call[1]?.method === "DELETE")).toHaveLength(1); }); test("failed saves retain recorded progress and allow another attempt", async () => { const f = connectAccount(); await f.json("/habits", "POST", { name: "Water", method: "count", target: 8 }, 201); await render("/"); fetchMock.mockResolvedValueOnce(Response.json({ error: "Save unavailable" }, { status: 500 })); await act(async () => buttonNamed("Increase Water").click()); expect(container.querySelector('[role="alert"]')?.textContent).toContain("Save unavailable"); expect(container.querySelector('[aria-label="Total Water"]')?.value).toBe("0"); expect(buttonNamed("Increase Water").disabled).toBe(false); await act(async () => buttonNamed("Increase Water").click()); expect(container.querySelector('[aria-label="Total Water"]')?.value).toBe("1"); expect((await f.json("/today")).habits[0].value).toBe(1); }); }); test("unscheduled dates stay inspectable without a progress-square fill", async () => { await act(async () => root.render( , ), ); const restDay = container.querySelector(".ds-day--not-due")!; expect(restDay.style.backgroundColor).toBe("transparent"); expect(restDay.getAttribute("aria-label")).toContain( "Not included in the score", ); expect( container.querySelector(".ds-day--due")!.style.backgroundColor, ).toBe("#ebedf0"); expect( container.querySelector(".ds-day--future")!.style .backgroundColor, ).toBe("#ebedf0"); expect(container.querySelector(".ds-legend-swatch--not-due")).not.toBeNull(); await act(async () => restDay.click()); expect(restDay.getAttribute("aria-pressed")).toBe("true"); expect(restDay.tabIndex).toBe(0); expect(container.querySelector(".ds-date-inspector")?.textContent).toContain( "Nothing scheduled", ); await act(async () => restDay.dispatchEvent( new dom.KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, }) as unknown as KeyboardEvent, ), ); expect( container.querySelector(".ds-day--due")?.getAttribute("aria-pressed"), ).toBe("true"); }); describe("independent design-system routing", () => { for (const path of ["/design-system", "/design-system/"]) { test(`renders only the public design system at ${path}`, async () => { await render(path); expect(container.querySelector("#ds-title")).not.toBeNull(); expect(container.querySelectorAll("main")).toHaveLength(1); expect(["/design-system", "/design-system/"]).toContain(container.querySelector('[data-testid="pathname"]')!.textContent!); expect(container.querySelector(".landing-root")).toBeNull(); expect(container.querySelector(".home-root")).toBeNull(); expect(fetchMock).not.toHaveBeenCalled(); expect(container.querySelectorAll('[role="tablist"]')).toHaveLength(1); expect(container.querySelectorAll('[role="tab"]')).toHaveLength(4); expect(container.querySelectorAll('[role="tabpanel"]:not([hidden])')).toHaveLength(1); for (const tab of container.querySelectorAll('[role="tab"]')) { const panel = document.getElementById(tab.getAttribute("aria-controls")!); expect(panel?.getAttribute("aria-labelledby")).toBe(tab.id); } }); } test("calendar examples update individual and combined progress without API requests", async () => { await render("/design-system#calendar-states"); const calendars = container.querySelectorAll("#ds-panel-calendar-states .ds-calendar"); expect(calendars).toHaveLength(3); const inspected = (index: number) => calendars[index]!.querySelector(".ds-date-inspector")!.textContent!; expect(inspected(0)).toContain("7 of 8 glasses"); expect(inspected(2)).toContain("1 of 2 habits complete"); await act(async () => container.querySelector('[aria-label="Increase glasses of water"]')!.click()); expect(inspected(0)).toContain("8 of 8 glasses"); expect(inspected(2)).toContain("2 of 2 habits complete"); const reading = container.querySelector('#ds-panel-calendar-states input[type="checkbox"]')!; await act(async () => reading.click()); expect(inspected(1)).toContain("0 of 1 reading session"); expect(inspected(2)).toContain("1 of 2 habits complete"); const reset = [...container.querySelectorAll('#ds-panel-calendar-states button')].find(button => button.textContent === "Reset examples ↺")!; await act(async () => reset.click()); expect(inspected(0)).toContain("7 of 8 glasses"); expect(reading.checked).toBe(true); expect(fetchMock).not.toHaveBeenCalled(); }); }); describe("design system tabs", () => { const tab = (id: string) => container.querySelector(`#ds-tab-${id}`)!; const panel = () => container.querySelector('[role="tabpanel"]:not([hidden])')!; const clickTab = async (id: string) => { await act(async () => tab(id).click()); }; const press = async (id: string, key: string) => { await act(async () => tab(id).dispatchEvent( new dom.KeyboardEvent("keydown", { key, bubbles: true }) as unknown as KeyboardEvent, )); }; test("defaults to foundations and each tab exposes only its own panel", async () => { await render(); expect(panel().id).toBe("ds-panel-foundations"); for (const id of ["components", "containers", "calendar-states", "foundations"]) { await clickTab(id); expect(panel().id).toBe(`ds-panel-${id}`); expect(container.querySelectorAll('[role="tabpanel"]:not([hidden])')).toHaveLength(1); expect(tab(id).getAttribute("aria-selected")).toBe("true"); expect(container.querySelectorAll('[role="tab"][tabindex="0"]')).toHaveLength(1); expect(container.querySelector('[data-testid="hash"]')?.textContent).toBe(`#${id}`); } expect(fetchMock).not.toHaveBeenCalled(); }); test("supports arrow keys, Home, End, and wraparound with focus", async () => { await render(); await press("foundations", "ArrowLeft"); expect(document.activeElement).toBe(tab("calendar-states")); expect(panel().id).toBe("ds-panel-calendar-states"); await press("calendar-states", "ArrowRight"); expect(document.activeElement).toBe(tab("foundations")); await press("foundations", "End"); expect(document.activeElement).toBe(tab("calendar-states")); await press("calendar-states", "Home"); expect(document.activeElement).toBe(tab("foundations")); expect(panel().id).toBe("ds-panel-foundations"); }); test("honors deep links and browser history", async () => { await render("/design-system#calendar-states"); expect(panel().id).toBe("ds-panel-calendar-states"); await clickTab("containers"); const historyButton = (label: string) => Array.from(container.querySelectorAll("button")).find(button => button.textContent === label)!; await act(async () => historyButton("History back").click()); expect(panel().id).toBe("ds-panel-calendar-states"); await act(async () => historyButton("History forward").click()); expect(panel().id).toBe("ds-panel-containers"); }); test("keeps example state and opens Calendars from View habit", async () => { await render("/design-system#components"); await act(async () => panel().querySelector('[aria-label="Increase example count"]')!.click()); await clickTab("calendar-states"); await clickTab("components"); expect(panel().querySelector('[aria-label="Total example count"]')?.value).toBe("4"); const viewHabit = Array.from(panel().querySelectorAll("button")).find(button => button.textContent?.startsWith("View habit"))!; await act(async () => viewHabit.click()); expect(panel().id).toBe("ds-panel-calendar-states"); expect(panel().querySelectorAll(".ds-calendar")).toHaveLength(3); expect(document.activeElement).toBe(tab("calendar-states")); }); test("retains child component state and skip link does not switch tabs", async () => { await render("/design-system#containers"); const reading = panel().querySelector('input[type="checkbox"]')!; await act(async () => reading.click()); expect(reading.checked).toBe(true); await clickTab("foundations"); await clickTab("containers"); expect(reading.checked).toBe(true); await act(async () => container.querySelector(".ds-skip-link")!.click()); expect(panel().id).toBe("ds-panel-containers"); expect(document.activeElement?.id).toBe("ds-main"); }); for (const [retired, destination] of [["playground", "calendar-states"], ["editing", "foundations"], ["views", "foundations"]]) { test(`retired #${retired} links resolve to #${destination}`, async () => { await render(`/design-system#${retired}`); expect(panel().id).toBe(`ds-panel-${destination}`); expect(container.querySelector('[data-testid="hash"]')?.textContent).toBe(`#${destination}`); expect([...container.querySelectorAll('[role="tab"]')].map(item => item.textContent)).toEqual(["Foundations", "Components", "Layout", "Calendars"]); for (const id of ["playground", "editing", "views"]) { expect(container.querySelector(`#${id}`)).toBeNull(); expect(container.querySelector(`#ds-panel-${id}`)).toBeNull(); } }); } test("an unknown section safely falls back to foundations", async () => { await render("/design-system#unknown"); expect(panel().id).toBe("ds-panel-foundations"); }); test("mounted panels keep unique IDs and valid accessible references", async () => { await render(); const ids = Array.from(container.querySelectorAll("[id]"), element => element.id); expect(new Set(ids).size).toBe(ids.length); for (const element of container.querySelectorAll("[aria-labelledby], [aria-describedby], [aria-controls], label[for]")) { for (const attribute of ["aria-labelledby", "aria-describedby", "aria-controls", "for"]) { for (const id of element.getAttribute(attribute)?.split(/\s+/).filter(Boolean) ?? []) { expect(document.getElementById(id)).not.toBeNull(); } } } expect(fetchMock).not.toHaveBeenCalled(); }); }); test("reminder form submits native time values and explicit opt-out", async () => { const saved: unknown[] = []; fetchMock.mockImplementation((async (_input, init) => { if (init?.method === "PUT") { saved.push(JSON.parse(String(init.body))); return Response.json({}); } return Response.json({ enabled: false, time: "20:00", quietStart: "22:00", quietEnd: "08:00", available: true, lastDelivery: null }); }) as typeof fetch); await act(async () => root.render()); const time = container.querySelector('input[name="time"]')!; await act(async () => { time.value = "14:30"; container.querySelector('input[type="checkbox"]')!.click(); }); await act(async () => container.querySelector("form")!.dispatchEvent(new dom.Event("submit", { bubbles: true, cancelable: true }) as unknown as Event)); expect(saved[0]).toEqual({ enabled: true, time: "14:30", quietStart: "22:00", quietEnd: "08:00" }); await act(async () => container.querySelector('input[type="checkbox"]')!.click()); await act(async () => container.querySelector("form")!.dispatchEvent(new dom.Event("submit", { bubbles: true, cancelable: true }) as unknown as Event)); expect(saved[1]).toEqual({ enabled: false, time: "14:30", quietStart: "22:00", quietEnd: "08:00" }); });