feat: add theme provider and control components for light/dark mode
- Implemented ThemeProvider to manage theme state and preferences. - Added ThemeControl component for users to switch between system, light, and dark themes. - Integrated localStorage for theme preference persistence. - Updated document styles based on theme changes. test: add tests for permanent deletion of habits and related records - Created tests to ensure permanent deletion of habits removes all dependent records. - Verified that deletion requires ownership, authentication, and checks for archived habits. - Added tests to confirm rollback behavior on deletion failures. test: add tests for habit palette and progress card functionality - Implemented tests for habit color progression and theme-based color shading. - Added tests for progress card calculations and calendar display logic. feat: define theme types and preferences - Introduced Theme and ThemePreference types for better type safety. - Created utility functions for resolving theme based on user preference and system settings. - Defined theme palettes for light and dark modes to ensure adequate contrast.
This commit is contained in:
@@ -184,6 +184,30 @@ describe("homepage", () => {
|
||||
expect((await f.json(`/habits/${h.id}/days/2026-09-03`)).complete).toBe(true);
|
||||
});
|
||||
|
||||
test("archive permanent deletion supports cancel, failure, retry, and removal", async () => {
|
||||
const f = connectAccount();
|
||||
const habit = await f.json("/habits", "POST", { name: "Discard me", method: "manual" }, 201);
|
||||
await f.request(`/habits/${habit.id}`, "DELETE");
|
||||
await render("/");
|
||||
await act(async () => buttonNamed("Archived habits").click());
|
||||
await act(async () => buttonNamed("Delete permanently Discard me").click());
|
||||
expect(container.querySelector("#delete-archived-habit")?.textContent).toContain("This cannot be undone.");
|
||||
expect(document.activeElement).toBe(buttonNamed("Keep habit"));
|
||||
await act(async () => buttonNamed("Keep habit").click());
|
||||
expect((await f.request(`/habits/${habit.id}`)).status).toBe(200);
|
||||
await act(async () => buttonNamed("Delete permanently Discard me").click());
|
||||
const confirm = () => container.querySelector<HTMLButtonElement>("#delete-archived-habit .ds-action-row button:last-child")!;
|
||||
fetchMock.mockResolvedValueOnce(Response.json({ error: "Temporary failure" }, { status: 500 }));
|
||||
await act(async () => confirm().click());
|
||||
expect(container.querySelector("#delete-archived-habit")?.textContent).toContain("Temporary failure");
|
||||
expect((await f.request(`/habits/${habit.id}`)).status).toBe(200);
|
||||
await act(async () => confirm().click());
|
||||
expect(container.querySelector("#delete-archived-habit")).toBeNull();
|
||||
expect(container.textContent).toContain("Discard me permanently deleted.");
|
||||
expect(container.textContent).toContain("No archived habits.");
|
||||
expect((await f.request(`/habits/${habit.id}`)).status).toBe(404);
|
||||
});
|
||||
|
||||
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");
|
||||
@@ -191,10 +215,8 @@ describe("homepage", () => {
|
||||
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");
|
||||
expect(container.querySelector(".ds-goal-graph .ds-calendar")?.getAttribute("data-months")).toBe("3");
|
||||
expect(container.querySelector(".ds-goal-graph [hidden]")).toBeNull();
|
||||
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");
|
||||
await act(async () => buttonNamed("Edit selected check-in").click());
|
||||
@@ -279,17 +301,18 @@ describe("homepage", () => {
|
||||
const countGoal = container.querySelector(".ds-goal--count")!;
|
||||
expect(countGoal.querySelector('.ds-goal-count input[aria-label="Total Water"]')).not.toBeNull();
|
||||
expect(countGoal.querySelector(".ds-goal-unit")?.textContent).toBe("glasses");
|
||||
expect(countGoal.textContent).not.toContain("Count target");
|
||||
expect(countGoal.textContent).toContain("Count target");
|
||||
expect(countGoal.querySelector(".ds-goal-status")?.textContent).toBe("In progress");
|
||||
const checkIn = container.querySelector<HTMLInputElement>('.ds-goal--manual input[type="checkbox"]')!;
|
||||
expect(container.querySelector('.ds-goal--manual h3 label')?.getAttribute("for")).toBe(checkIn.id);
|
||||
expect(container.querySelector('.ds-goal--manual .ds-goal-check-in > label[for]')?.getAttribute("for")).toBe(checkIn.id);
|
||||
expect(checkIn.getAttribute("aria-label")).toBe("Reading done");
|
||||
expect(container.querySelector('.ds-goal--tasks details')).toBeNull();
|
||||
expect(container.querySelector('.ds-goal--tasks .ds-goal-tasks details')).toBeNull();
|
||||
expect(container.querySelectorAll('.ds-goal--tasks .ds-routine-items > li')).toHaveLength(1);
|
||||
|
||||
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.querySelectorAll(".ds-goal-graph .ds-calendar")).toHaveLength(3);
|
||||
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.textContent).not.toContain("Sunday walk");
|
||||
await act(async () => buttonNamed("All habits · 4").click());
|
||||
@@ -469,7 +492,7 @@ describe("homepage", () => {
|
||||
await selectMethod("tasks");
|
||||
await submit();
|
||||
expect((await f.json(`/habits/${habit.id}`)).method).toBe("tasks");
|
||||
await act(async () => buttonNamed("Add task +").click());
|
||||
await act(async () => buttonNamed("Add task").click());
|
||||
const form = container.querySelector<HTMLFormElement>('form[aria-label="Add task"]')!;
|
||||
await act(async () => {
|
||||
const input = form.querySelector("input")!;
|
||||
@@ -560,11 +583,11 @@ test("unscheduled dates stay inspectable without a progress-square fill", async
|
||||
);
|
||||
expect(
|
||||
container.querySelector<HTMLElement>(".ds-day--due")!.style.backgroundColor,
|
||||
).toBe("#ebedf0");
|
||||
).toBe("var(--ds-empty)");
|
||||
expect(
|
||||
container.querySelector<HTMLElement>(".ds-day--future")!.style
|
||||
.backgroundColor,
|
||||
).toBe("#ebedf0");
|
||||
).toBe("var(--ds-empty)");
|
||||
expect(container.querySelector(".ds-legend-swatch--not-due")).not.toBeNull();
|
||||
await act(async () => restDay.click());
|
||||
expect(restDay.getAttribute("aria-pressed")).toBe("true");
|
||||
|
||||
Reference in New Issue
Block a user