Split optional routes and editing dialogs from initial browser code

This commit is contained in:
syntaxbullet
2026-09-08 16:20:04 +02:00
parent 9a0fc108a9
commit b22c933fc4
6 changed files with 68 additions and 8 deletions

View File

@@ -10,6 +10,7 @@ const result = await Bun.build({
outdir, outdir,
plugins: [tailwind], plugins: [tailwind],
minify: true, minify: true,
splitting: true,
target: "bun", target: "bun",
external: ["@napi-rs/canvas"], external: ["@napi-rs/canvas"],
sourcemap: "linked", sourcemap: "linked",

View File

@@ -91,6 +91,12 @@ function LocationProbe() {
</>; </>;
} }
async function settleDeferred() {
for (let attempt = 0; attempt < 100 && [...container.querySelectorAll('[role="status"]')].some(node => node.textContent === "Loading…"); attempt++) {
await act(async () => { await new Promise(resolve => setTimeout(resolve, 1)); });
}
}
async function render(path = "/design-system") { async function render(path = "/design-system") {
await act(async () => await act(async () =>
root.render( root.render(
@@ -100,6 +106,7 @@ async function render(path = "/design-system") {
</MemoryRouter>, </MemoryRouter>,
), ),
); );
await settleDeferred();
} }
function connectAccount() { function connectAccount() {
@@ -131,6 +138,7 @@ describe("homepage", () => {
const opener = buttonNamed("Settings"); const opener = buttonNamed("Settings");
opener.focus(); opener.focus();
await act(async () => opener.click()); await act(async () => opener.click());
await settleDeferred();
const dialog = container.querySelector<HTMLDialogElement>("dialog#account-settings")!; const dialog = container.querySelector<HTMLDialogElement>("dialog#account-settings")!;
expect(dialog.open).toBe(true); expect(dialog.open).toBe(true);
expect(dialog.contains(document.activeElement)).toBe(true); expect(dialog.contains(document.activeElement)).toBe(true);
@@ -158,6 +166,7 @@ describe("homepage", () => {
expect(inner.open).toBe(true); expect(inner.open).toBe(true);
expect(inner.querySelector<HTMLButtonElement>('[aria-label="Close dialog"]')?.disabled).toBe(true); expect(inner.querySelector<HTMLButtonElement>('[aria-label="Close dialog"]')?.disabled).toBe(true);
await act(async () => buttonNamed("Finish save").click()); await act(async () => buttonNamed("Finish save").click());
await settleDeferred();
await act(async () => inner.dispatchEvent(new dom.Event("cancel", { cancelable: true }) as unknown as Event)); await act(async () => inner.dispatchEvent(new dom.Event("cancel", { cancelable: true }) as unknown as Event));
expect(container.querySelector("#inner")).toBeNull(); expect(container.querySelector("#inner")).toBeNull();
expect(container.querySelector<HTMLDialogElement>("#outer")?.open).toBe(true); expect(container.querySelector<HTMLDialogElement>("#outer")?.open).toBe(true);
@@ -175,10 +184,13 @@ describe("homepage", () => {
await f.request(`/habits/${h.id}`, "DELETE"); await f.request(`/habits/${h.id}`, "DELETE");
await render("/"); await render("/");
await act(async () => buttonNamed("Archived habits").click()); await act(async () => buttonNamed("Archived habits").click());
await settleDeferred();
expect(buttonNamed("Restore Reading")).toBeDefined(); expect(buttonNamed("Restore Reading")).toBeDefined();
await act(async () => buttonNamed("View history").click()); await act(async () => buttonNamed("View history").click());
await settleDeferred();
expect(container.querySelector('[aria-label="2026-09-03: 1 of 1 completion · Complete"]')).not.toBeNull(); expect(container.querySelector('[aria-label="2026-09-03: 1 of 1 completion · Complete"]')).not.toBeNull();
await act(async () => buttonNamed("Restore Reading").click()); await act(async () => buttonNamed("Restore Reading").click());
await settleDeferred();
expect(container.textContent).toContain("No archived habits."); expect(container.textContent).toContain("No archived habits.");
expect((await f.json("/today")).habits[0].name).toBe("Reading"); expect((await f.json("/today")).habits[0].name).toBe("Reading");
expect((await f.json(`/habits/${h.id}/days/2026-09-03`)).complete).toBe(true); expect((await f.json(`/habits/${h.id}/days/2026-09-03`)).complete).toBe(true);
@@ -190,12 +202,16 @@ describe("homepage", () => {
await f.request(`/habits/${habit.id}`, "DELETE"); await f.request(`/habits/${habit.id}`, "DELETE");
await render("/"); await render("/");
await act(async () => buttonNamed("Archived habits").click()); await act(async () => buttonNamed("Archived habits").click());
await settleDeferred();
await act(async () => buttonNamed("Delete permanently Discard me").click()); await act(async () => buttonNamed("Delete permanently Discard me").click());
await settleDeferred();
expect(container.querySelector("#delete-archived-habit")?.textContent).toContain("This cannot be undone."); expect(container.querySelector("#delete-archived-habit")?.textContent).toContain("This cannot be undone.");
expect(document.activeElement).toBe(buttonNamed("Keep habit")); expect(document.activeElement).toBe(buttonNamed("Keep habit"));
await act(async () => buttonNamed("Keep habit").click()); await act(async () => buttonNamed("Keep habit").click());
await settleDeferred();
expect((await f.request(`/habits/${habit.id}`)).status).toBe(200); expect((await f.request(`/habits/${habit.id}`)).status).toBe(200);
await act(async () => buttonNamed("Delete permanently Discard me").click()); await act(async () => buttonNamed("Delete permanently Discard me").click());
await settleDeferred();
const confirm = () => container.querySelector<HTMLButtonElement>("#delete-archived-habit .ds-action-row button:last-child")!; const confirm = () => container.querySelector<HTMLButtonElement>("#delete-archived-habit .ds-action-row button:last-child")!;
fetchMock.mockResolvedValueOnce(Response.json({ error: "Temporary failure" }, { status: 500 })); fetchMock.mockResolvedValueOnce(Response.json({ error: "Temporary failure" }, { status: 500 }));
await act(async () => confirm().click()); await act(async () => confirm().click());
@@ -220,6 +236,7 @@ describe("homepage", () => {
await act(async () => container.querySelector<HTMLButtonElement>('[aria-label^="2026-09-03:"]')!.click()); await act(async () => container.querySelector<HTMLButtonElement>('[aria-label^="2026-09-03:"]')!.click());
expect(buttonNamed("Edit selected check-in").closest(".ds-date-inspector")?.textContent).toContain("September 3, 2026"); expect(buttonNamed("Edit selected check-in").closest(".ds-date-inspector")?.textContent).toContain("September 3, 2026");
await act(async () => buttonNamed("Edit selected check-in").click()); await act(async () => buttonNamed("Edit selected check-in").click());
await settleDeferred();
const past = [...container.querySelectorAll<HTMLInputElement>('input[type="checkbox"]')].find(input => input.closest("label")?.textContent === "Completed on this date")!; const past = [...container.querySelectorAll<HTMLInputElement>('input[type="checkbox"]')].find(input => input.closest("label")?.textContent === "Completed on this date")!;
expect(past).toBeDefined(); expect(past).toBeDefined();
await act(async () => past.click()); await act(async () => past.click());
@@ -233,6 +250,7 @@ describe("homepage", () => {
expect(container.querySelector("h1")?.textContent).toBe("Small steps.Lasting rhythm."); expect(container.querySelector("h1")?.textContent).toBe("Small steps.Lasting rhythm.");
expect(fetchMock.mock.calls.map(call => call[0])).toEqual(["/api/me"]); expect(fetchMock.mock.calls.map(call => call[0])).toEqual(["/api/me"]);
await act(async () => buttonNamed("Increase glasses of water").click()); await act(async () => buttonNamed("Increase glasses of water").click());
await settleDeferred();
expect(container.querySelector<HTMLInputElement>('[aria-label="Total glasses of water"]')?.value).toBe("4"); expect(container.querySelector<HTMLInputElement>('[aria-label="Total glasses of water"]')?.value).toBe("4");
expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock).toHaveBeenCalledTimes(1);
}); });
@@ -264,6 +282,7 @@ describe("homepage", () => {
expect(container.textContent).toContain("Account unavailable"); expect(container.textContent).toContain("Account unavailable");
expect(container.querySelector("#welcome-title")).toBeNull(); expect(container.querySelector("#welcome-title")).toBeNull();
await act(async () => buttonNamed("Try again").click()); await act(async () => buttonNamed("Try again").click());
await settleDeferred();
expect(container.querySelector("#welcome-title")).not.toBeNull(); expect(container.querySelector("#welcome-title")).not.toBeNull();
}); });
@@ -273,6 +292,7 @@ describe("homepage", () => {
expect(container.textContent).toContain("Welcome back, alice."); expect(container.textContent).toContain("Welcome back, alice.");
expect(container.textContent).toContain("Start with one habit."); expect(container.textContent).toContain("Start with one habit.");
await act(async () => buttonNamed("Add a habit +").click()); await act(async () => buttonNamed("Add a habit +").click());
await settleDeferred();
const input = container.querySelector<HTMLInputElement>("form input")!; const input = container.querySelector<HTMLInputElement>("form input")!;
expect(document.activeElement).toBe(input); expect(document.activeElement).toBe(input);
await act(async () => { await act(async () => {
@@ -285,6 +305,7 @@ describe("homepage", () => {
expect(container.textContent).not.toContain("Read a little created."); expect(container.textContent).not.toContain("Read a little created.");
expect(container.querySelector(".ds-habit-chart")).not.toBeNull(); expect(container.querySelector(".ds-habit-chart")).not.toBeNull();
await act(async () => buttonNamed("Sign out").click()); await act(async () => buttonNamed("Sign out").click());
await settleDeferred();
expect(container.querySelector("#welcome-title")).not.toBeNull(); expect(container.querySelector("#welcome-title")).not.toBeNull();
expect((await f.request("/me")).status).toBe(401); expect((await f.request("/me")).status).toBe(401);
}); });
@@ -310,12 +331,14 @@ describe("homepage", () => {
expect(container.querySelectorAll('.ds-goal--tasks .ds-routine-items > li')).toHaveLength(1); expect(container.querySelectorAll('.ds-goal--tasks .ds-routine-items > li')).toHaveLength(1);
await act(async () => buttonNamed("Increase Water").click()); await act(async () => buttonNamed("Increase Water").click());
await settleDeferred();
expect(container.querySelector(".ds-welcome-progress")?.textContent).toContain("1 / 3"); expect(container.querySelector(".ds-welcome-progress")?.textContent).toContain("1 / 3");
expect(container.querySelectorAll(".ds-goal-graph .ds-calendar")).toHaveLength(3); expect(container.querySelectorAll(".ds-goal-graph .ds-calendar")).toHaveLength(3);
expect(countGoal.querySelector(".ds-goal-status")?.textContent).toBe("Complete"); expect(countGoal.querySelector(".ds-goal-status")?.textContent).toBe("Complete");
expect(container.querySelector(`#history-${water.id} .ds-date-inspector`)?.textContent).toContain("8 of 8 glasses"); expect(container.querySelector(`#history-${water.id} .ds-date-inspector`)?.textContent).toContain("8 of 8 glasses");
expect(container.textContent).not.toContain("Sunday walk"); expect(container.textContent).not.toContain("Sunday walk");
await act(async () => buttonNamed("All habits · 4").click()); await act(async () => buttonNamed("All habits · 4").click());
await settleDeferred();
const checkbox = (name: string) => [...container.querySelectorAll<HTMLInputElement>('input[type="checkbox"]')].find(input => (input.getAttribute("aria-label") || input.closest("label")?.textContent) === name)!; const checkbox = (name: string) => [...container.querySelectorAll<HTMLInputElement>('input[type="checkbox"]')].find(input => (input.getAttribute("aria-label") || input.closest("label")?.textContent) === name)!;
expect(checkbox("Sunday walk done").disabled).toBe(true); expect(checkbox("Sunday walk done").disabled).toBe(true);
await act(async () => checkbox("Reading done").click()); await act(async () => checkbox("Reading done").click());
@@ -323,6 +346,7 @@ describe("homepage", () => {
expect(container.querySelector(".ds-welcome-progress")?.textContent).toContain("3 / 3"); expect(container.querySelector(".ds-welcome-progress")?.textContent).toContain("3 / 3");
expect((await f.json("/today")).completed).toBe(3); expect((await f.json("/today")).completed).toBe(3);
await act(async () => buttonNamed("Decrease Water").click()); await act(async () => buttonNamed("Decrease Water").click());
await settleDeferred();
expect((await f.json("/today")).completed).toBe(2); expect((await f.json("/today")).completed).toBe(2);
}); });
@@ -339,6 +363,7 @@ describe("homepage", () => {
expect(calendarRequests()[0]).toContain("compact=true"); expect(calendarRequests()[0]).toContain("compact=true");
const parse = spyOn(Date, "parse"); const parse = spyOn(Date, "parse");
await act(async () => buttonNamed("All habits · 2").click()); await act(async () => buttonNamed("All habits · 2").click());
await settleDeferred();
expect(parse).not.toHaveBeenCalled(); expect(parse).not.toHaveBeenCalled();
parse.mockRestore(); parse.mockRestore();
fetchMock.mockClear(); fetchMock.mockClear();
@@ -380,6 +405,7 @@ describe("homepage", () => {
}, { preconnect: fetch.preconnect })); }, { preconnect: fetch.preconnect }));
fetchMock.mockClear(); fetchMock.mockClear();
await act(async () => buttonNamed("Increase Water").click()); await act(async () => buttonNamed("Increase Water").click());
await settleDeferred();
expect(buttonNamed("Increase Water").disabled).toBe(true); expect(buttonNamed("Increase Water").disabled).toBe(true);
const check = container.querySelector<HTMLInputElement>('[aria-label="Reading done"]')!; const check = container.querySelector<HTMLInputElement>('[aria-label="Reading done"]')!;
expect(check.disabled).toBe(false); expect(check.disabled).toBe(false);
@@ -406,6 +432,7 @@ describe("homepage", () => {
f.setTime("2026-09-04T12:00:00Z"); f.setTime("2026-09-04T12:00:00Z");
await render("/"); await render("/");
await act(async () => buttonNamed("Edit habit Water").click()); await act(async () => buttonNamed("Edit habit Water").click());
await settleDeferred();
const form = container.querySelector<HTMLFormElement>('form[aria-label="Edit habit Water"]')!; const form = container.querySelector<HTMLFormElement>('form[aria-label="Edit habit Water"]')!;
expect(document.activeElement).toBe(form.querySelector("input")); expect(document.activeElement).toBe(form.querySelector("input"));
await act(async () => { await act(async () => {
@@ -423,11 +450,15 @@ describe("homepage", () => {
expect(container.querySelector("form")).toBeNull(); expect(container.querySelector("form")).toBeNull();
expect(container.querySelector("h3")?.textContent).toContain("Daily water"); expect(container.querySelector("h3")?.textContent).toContain("Daily water");
await act(async () => buttonNamed("Archive habit Daily water").click()); await act(async () => buttonNamed("Archive habit Daily water").click());
await settleDeferred();
expect((await f.json("/habits")).habits).toHaveLength(1); expect((await f.json("/habits")).habits).toHaveLength(1);
await act(async () => buttonNamed("Cancel").click()); await act(async () => buttonNamed("Cancel").click());
await settleDeferred();
expect((await f.json("/habits")).habits).toHaveLength(1); expect((await f.json("/habits")).habits).toHaveLength(1);
await act(async () => buttonNamed("Archive habit Daily water").click()); await act(async () => buttonNamed("Archive habit Daily water").click());
await settleDeferred();
await act(async () => buttonNamed("Archive habit").click()); await act(async () => buttonNamed("Archive habit").click());
await settleDeferred();
expect((await f.json("/habits")).habits).toHaveLength(0); expect((await f.json("/habits")).habits).toHaveLength(0);
expect(container.querySelector(".ds-habit-chart")).toBeNull(); expect(container.querySelector(".ds-habit-chart")).toBeNull();
const previous = await f.json(`/habits/${habit.id}/days/2026-09-03`); const previous = await f.json(`/habits/${habit.id}/days/2026-09-03`);
@@ -446,6 +477,7 @@ describe("homepage", () => {
await f.json(`/habits/${habit.id}/days/2026-09-04/tasks/${task.id}`, "PUT", { done: true }); await f.json(`/habits/${habit.id}/days/2026-09-04/tasks/${task.id}`, "PUT", { done: true });
await render("/"); await render("/");
await act(async () => buttonNamed("Edit task Stretch").click()); await act(async () => buttonNamed("Edit task Stretch").click());
await settleDeferred();
const form = container.querySelector<HTMLFormElement>('form[aria-label="Edit task Stretch"]')!; const form = container.querySelector<HTMLFormElement>('form[aria-label="Edit task Stretch"]')!;
await act(async () => { await act(async () => {
const input = form.querySelector("input")!; const input = form.querySelector("input")!;
@@ -458,7 +490,9 @@ describe("homepage", () => {
expect(saved.schedule).toEqual({ type: "weekdays", days: [4, 5] }); expect(saved.schedule).toEqual({ type: "weekdays", days: [4, 5] });
expect((await f.json("/today")).habits[0].value).toBe(1); 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 Stretch gently").click());
await settleDeferred();
await act(async () => buttonNamed("Delete task").click()); await act(async () => buttonNamed("Delete task").click());
await settleDeferred();
const today = (await f.json("/today")).habits[0]; const today = (await f.json("/today")).habits[0];
expect(today.target).toBe(1); expect(today.target).toBe(1);
expect(today.value).toBe(0); expect(today.value).toBe(0);
@@ -474,9 +508,11 @@ describe("homepage", () => {
await render("/"); await render("/");
expect(container.querySelector(".ds-habit-chart")).toBeNull(); expect(container.querySelector(".ds-habit-chart")).toBeNull();
await act(async () => buttonNamed("All habits · 1").click()); await act(async () => buttonNamed("All habits · 1").click());
await settleDeferred();
expect(container.textContent).toContain("Not scheduled today"); expect(container.textContent).toContain("Not scheduled today");
expect(container.querySelector<HTMLInputElement>('.ds-editable-task input[type="checkbox"]')?.disabled).toBe(true); expect(container.querySelector<HTMLInputElement>('.ds-editable-task input[type="checkbox"]')?.disabled).toBe(true);
await act(async () => buttonNamed("Edit task Walk").click()); await act(async () => buttonNamed("Edit task Walk").click());
await settleDeferred();
const form = container.querySelector<HTMLFormElement>('form[aria-label="Edit task Walk"]')!; const form = container.querySelector<HTMLFormElement>('form[aria-label="Edit task Walk"]')!;
await act(async () => { await act(async () => {
const input = form.querySelector("input")!; const input = form.querySelector("input")!;
@@ -516,6 +552,7 @@ describe("homepage", () => {
{ type: "daily" }, { type: "daily" },
] as const) { ] as const) {
await act(async () => buttonNamed("Edit task Walk").click()); await act(async () => buttonNamed("Edit task Walk").click());
await settleDeferred();
const form = container.querySelector<HTMLFormElement>('form[aria-label="Edit task Walk"]')!; const form = container.querySelector<HTMLFormElement>('form[aria-label="Edit task Walk"]')!;
await selectValue(form.querySelector("select")!, schedule.type); await selectValue(form.querySelector("select")!, schedule.type);
if (schedule.type === "interval" || schedule.type === "weekly") { if (schedule.type === "interval" || schedule.type === "weekly") {
@@ -528,6 +565,7 @@ describe("homepage", () => {
if (schedule.type === "interval") expect(buttonNamed("All habits · 1").getAttribute("aria-pressed")).toBe("true"); if (schedule.type === "interval") expect(buttonNamed("All habits · 1").getAttribute("aria-pressed")).toBe("true");
expect(container.querySelector("form")).toBeNull(); expect(container.querySelector("form")).toBeNull();
await act(async () => buttonNamed("Edit task Walk").click()); await act(async () => buttonNamed("Edit task Walk").click());
await settleDeferred();
const reopened = container.querySelector<HTMLFormElement>('form[aria-label="Edit task Walk"]')!; const reopened = container.querySelector<HTMLFormElement>('form[aria-label="Edit task Walk"]')!;
expect(reopened.querySelector("select")?.value).toBe(schedule.type); expect(reopened.querySelector("select")?.value).toBe(schedule.type);
if (schedule.type === "interval" || schedule.type === "weekly") { if (schedule.type === "interval" || schedule.type === "weekly") {
@@ -535,6 +573,7 @@ describe("homepage", () => {
expect(reopened.querySelector<HTMLInputElement>('input[type="date"]')?.value).toBe(schedule.anchor); expect(reopened.querySelector<HTMLInputElement>('input[type="date"]')?.value).toBe(schedule.anchor);
} }
await act(async () => buttonNamed("Cancel").click()); await act(async () => buttonNamed("Cancel").click());
await settleDeferred();
} }
const previous = await f.json(`/habits/${habit.id}/days/2026-09-03`); const previous = await f.json(`/habits/${habit.id}/days/2026-09-03`);
expect(previous.tasks[0].done).toBe(true); expect(previous.tasks[0].done).toBe(true);
@@ -554,17 +593,20 @@ describe("homepage", () => {
}; };
const submit = async () => act(async () => container.querySelector("form")!.dispatchEvent(new dom.Event("submit", { bubbles: true, cancelable: 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 act(async () => buttonNamed("Edit habit Reading").click());
await settleDeferred();
await selectMethod("count"); await selectMethod("count");
await act(async () => container.querySelector<HTMLInputElement>('form input[type="checkbox"]')!.click()); await act(async () => container.querySelector<HTMLInputElement>('form input[type="checkbox"]')!.click());
await submit(); await submit();
expect((await f.json(`/habits/${habit.id}`)).method).toBe("count"); expect((await f.json(`/habits/${habit.id}`)).method).toBe("count");
expect((await f.json(`/habits/${habit.id}`)).carryPartialProgress).toBe(true); expect((await f.json(`/habits/${habit.id}`)).carryPartialProgress).toBe(true);
await act(async () => buttonNamed("Edit habit Reading").click()); await act(async () => buttonNamed("Edit habit Reading").click());
await settleDeferred();
expect(container.querySelector<HTMLInputElement>('form input[type="checkbox"]')?.checked).toBe(true); expect(container.querySelector<HTMLInputElement>('form input[type="checkbox"]')?.checked).toBe(true);
await selectMethod("tasks"); await selectMethod("tasks");
await submit(); await submit();
expect((await f.json(`/habits/${habit.id}`)).method).toBe("tasks"); expect((await f.json(`/habits/${habit.id}`)).method).toBe("tasks");
await act(async () => buttonNamed("Add task").click()); await act(async () => buttonNamed("Add task").click());
await settleDeferred();
const form = container.querySelector<HTMLFormElement>('form[aria-label="Add task"]')!; const form = container.querySelector<HTMLFormElement>('form[aria-label="Add task"]')!;
await act(async () => { await act(async () => {
const input = form.querySelector("input")!; const input = form.querySelector("input")!;
@@ -584,16 +626,19 @@ describe("homepage", () => {
const habit = await f.json("/habits", "POST", { name: "Reading", method: "manual" }, 201); const habit = await f.json("/habits", "POST", { name: "Reading", method: "manual" }, 201);
await render("/"); await render("/");
await act(async () => buttonNamed("Archive habit Reading").click()); await act(async () => buttonNamed("Archive habit Reading").click());
await settleDeferred();
fetchMock.mockImplementationOnce((async (input, init) => { fetchMock.mockImplementationOnce((async (input, init) => {
return f.request(String(input).replace("/api", ""), init?.method); return f.request(String(input).replace("/api", ""), init?.method);
}) as typeof fetch); }) as typeof fetch);
fetchMock.mockResolvedValueOnce(new Response(null, { status: 500 })); fetchMock.mockResolvedValueOnce(new Response(null, { status: 500 }));
await act(async () => buttonNamed("Archive habit").click()); await act(async () => buttonNamed("Archive habit").click());
await settleDeferred();
expect(container.querySelector("form")).toBeNull(); expect(container.querySelector("form")).toBeNull();
expect(container.textContent).toContain("Your change was saved, but the dashboard could not refresh"); expect(container.textContent).toContain("Your change was saved, but the dashboard could not refresh");
expect(buttonNamed("Archive habit Reading").disabled).toBe(true); expect(buttonNamed("Archive habit Reading").disabled).toBe(true);
expect((await f.json(`/habits/${habit.id}`)).archived).toBe(true); expect((await f.json(`/habits/${habit.id}`)).archived).toBe(true);
await act(async () => buttonNamed("Try again").click()); await act(async () => buttonNamed("Try again").click());
await settleDeferred();
expect(container.querySelector(".ds-habit-chart")).toBeNull(); expect(container.querySelector(".ds-habit-chart")).toBeNull();
expect(fetchMock.mock.calls.filter(call => call[1]?.method === "DELETE")).toHaveLength(1); expect(fetchMock.mock.calls.filter(call => call[1]?.method === "DELETE")).toHaveLength(1);
}); });
@@ -604,10 +649,12 @@ describe("homepage", () => {
await render("/"); await render("/");
fetchMock.mockResolvedValueOnce(Response.json({ error: "Save unavailable" }, { status: 500 })); fetchMock.mockResolvedValueOnce(Response.json({ error: "Save unavailable" }, { status: 500 }));
await act(async () => buttonNamed("Increase Water").click()); await act(async () => buttonNamed("Increase Water").click());
await settleDeferred();
expect(container.querySelector('[role="alert"]')?.textContent).toContain("Save unavailable"); expect(container.querySelector('[role="alert"]')?.textContent).toContain("Save unavailable");
expect(container.querySelector<HTMLInputElement>('[aria-label="Total Water"]')?.value).toBe("0"); expect(container.querySelector<HTMLInputElement>('[aria-label="Total Water"]')?.value).toBe("0");
expect(buttonNamed("Increase Water").disabled).toBe(false); expect(buttonNamed("Increase Water").disabled).toBe(false);
await act(async () => buttonNamed("Increase Water").click()); await act(async () => buttonNamed("Increase Water").click());
await settleDeferred();
expect(container.querySelector<HTMLInputElement>('[aria-label="Total Water"]')?.value).toBe("1"); expect(container.querySelector<HTMLInputElement>('[aria-label="Total Water"]')?.value).toBe("1");
expect((await f.json("/today")).habits[0].value).toBe(1); expect((await f.json("/today")).habits[0].value).toBe(1);
}); });

View File

@@ -1,5 +1,6 @@
import { Navigate, Route, Routes } from "react-router"; import { Navigate, Route, Routes } from "react-router";
import { DesignSystem } from "./pages/DesignSystem"; import { deferred } from "./components/design-system/deferred";
const DesignSystem = deferred(() => import("./pages/DesignSystem").then(module => module.DesignSystem));
import { Home } from "./pages/Home"; import { Home } from "./pages/Home";
import { AuthProvider } from "./components/AuthProvider"; import { AuthProvider } from "./components/AuthProvider";

View File

@@ -10,7 +10,8 @@ import { Button } from "./design-system/primitives";
import { HabitChart } from "./design-system/HabitChart"; import { HabitChart } from "./design-system/HabitChart";
import { CalendarLegend } from "./design-system/CalendarLegend"; import { CalendarLegend } from "./design-system/CalendarLegend";
import { shade } from "../habits/calendar"; import { shade } from "../habits/calendar";
import { CheckInEditor } from "./CheckInEditor"; import { deferred } from "./design-system/deferred";
const CheckInEditor = deferred(() => import("./CheckInEditor").then(module => module.CheckInEditor));
import { Field } from "./design-system/Field"; import { Field } from "./design-system/Field";
import { ItemActions } from "./design-system/ItemActions"; import { ItemActions } from "./design-system/ItemActions";

View File

@@ -0,0 +1,9 @@
import { lazy, Suspense, type ComponentProps, type ComponentType } from "react";
/** Keep the surrounding workspace usable while an optional surface loads. */
export function deferred<Component extends ComponentType<any>>(load: () => Promise<Component>) {
const Surface = lazy(async () => ({ default: await load() })) as ComponentType<ComponentProps<Component>>;
return function Deferred(props: ComponentProps<Component>) {
return <Suspense fallback={<p className="ds-supporting-copy" role="status">Loading…</p>}><Surface {...props} /></Suspense>;
};
}

View File

@@ -1,7 +1,8 @@
import { deferred } from "../components/design-system/deferred";
import { CalendarCacheProvider } from "../components/CalendarCache"; import { CalendarCacheProvider } from "../components/CalendarCache";
import { Archive, Plus, Settings2 } from "lucide-react"; import { Archive, Plus, Settings2 } from "lucide-react";
import { ArchivedHabits } from "../components/ArchivedHabits"; const ArchivedHabits = deferred(() => import("../components/ArchivedHabits").then(module => module.ArchivedHabits));
import { AccountSettings } from "../components/AccountSettings"; const AccountSettings = deferred(() => import("../components/AccountSettings").then(module => module.AccountSettings));
import { memo, useCallback, useEffect, useRef, useState } from "react"; import { memo, useCallback, useEffect, useRef, useState } from "react";
import { useAuth } from "../components/AuthProvider"; import { useAuth } from "../components/AuthProvider";
import { DiscordSignInButton } from "../components/DiscordSignInButton"; import { DiscordSignInButton } from "../components/DiscordSignInButton";
@@ -16,17 +17,17 @@ import {
import { Card, CardGrid } from "../components/design-system/Card"; import { Card, CardGrid } from "../components/design-system/Card";
import { HabitChart } from "../components/design-system/HabitChart"; import { HabitChart } from "../components/design-system/HabitChart";
import { HABIT_COLORS } from "../components/design-system/calendar-model"; import { HABIT_COLORS } from "../components/design-system/calendar-model";
import { HabitForm } from "../components/HabitForm"; const HabitForm = deferred(() => import("../components/HabitForm").then(module => module.HabitForm));
import { habitRequest, reconcileToday, scheduleLabel, type TodayHabit, type TodayResponse } from "../lib/dashboard"; import { habitRequest, reconcileToday, scheduleLabel, type TodayHabit, type TodayResponse } from "../lib/dashboard";
import { HabitHistory } from "../components/HabitHistory"; import { HabitHistory } from "../components/HabitHistory";
import { WelcomePanel } from "../components/design-system/WelcomePanel"; import { WelcomePanel } from "../components/design-system/WelcomePanel";
import type { PublicUser } from "../shared/user"; import type { PublicUser } from "../shared/user";
import { ItemActions } from "../components/design-system/ItemActions"; import { ItemActions } from "../components/design-system/ItemActions";
import { type ItemMode } from "../components/InlineItemForm"; import { type ItemMode } from "../components/InlineItemForm";
import { InlineHabitEditor } from "../components/InlineHabitEditor"; const InlineHabitEditor = deferred(() => import("../components/InlineHabitEditor").then(module => module.InlineHabitEditor));
import { InlineTaskEditor } from "../components/InlineTaskEditor"; const InlineTaskEditor = deferred(() => import("../components/InlineTaskEditor").then(module => module.InlineTaskEditor));
import type { Schedule } from "../habits/contracts"; import type { Schedule } from "../habits/contracts";
import { ShareProgress } from "../components/ShareProgress"; const ShareProgress = deferred(() => import("../components/ShareProgress").then(module => module.ShareProgress));
export function Home() { export function Home() {
const { user, loading, busy, error, accountError, signIn, signOut, retry } = useAuth(); const { user, loading, busy, error, accountError, signIn, signOut, retry } = useAuth();