feat: add Home page with user dashboard and habit tracking features
- Implemented Home component with user authentication and loading states. - Created Welcome component for unauthenticated users with a sign-in option. - Developed Dashboard component to display user's habits and progress. - Added functionality for habit management, including adding, updating, and deleting habits. - Integrated HabitChart and HabitHistory components for visual representation of habits. - Introduced sharing functionality for progress via Discord integration. feat: establish Discord sharing configuration and routes - Added DiscordSharingConfig type and readDiscordSharingConfig function for environment variable management. - Created sharing contracts for input validation and data structure. - Implemented sharing routes for previewing and sending progress images to Discord. - Added tests for sharing routes to ensure authentication and proper error handling.
This commit is contained in:
321
src/App.test.tsx
321
src/App.test.tsx
@@ -14,6 +14,7 @@ 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/" });
|
||||
@@ -22,6 +23,7 @@ let createRoot: typeof import("react-dom/client").createRoot;
|
||||
let root: Root;
|
||||
let container: HTMLDivElement;
|
||||
let fetchMock: ReturnType<typeof spyOn<typeof globalThis, "fetch">>;
|
||||
let apiFixture: ReturnType<typeof fixture> | undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
for (const key of [
|
||||
@@ -64,6 +66,8 @@ afterEach(async () => {
|
||||
await act(async () => root.unmount());
|
||||
container.remove();
|
||||
fetchMock.mockRestore();
|
||||
apiFixture?.close();
|
||||
apiFixture = undefined;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
@@ -85,7 +89,7 @@ function LocationProbe() {
|
||||
</>;
|
||||
}
|
||||
|
||||
async function render(path = "/") {
|
||||
async function render(path = "/design-system") {
|
||||
await act(async () =>
|
||||
root.render(
|
||||
<MemoryRouter initialEntries={[path]}>
|
||||
@@ -96,6 +100,317 @@ async function render(path = "/") {
|
||||
);
|
||||
}
|
||||
|
||||
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<HTMLButtonElement>("button")]
|
||||
.find(button => button.textContent === name || button.getAttribute("aria-label") === name)!;
|
||||
|
||||
describe("homepage", () => {
|
||||
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(".ds-counter output")?.textContent).toBe("4 / 8");
|
||||
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<Response>(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<HTMLInputElement>("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`)?.textContent).toContain("8 of 8 glasses");
|
||||
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);
|
||||
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<HTMLFormElement>('form[aria-label="Edit habit Water"]')!;
|
||||
expect(document.activeElement).toBe(form.querySelector("input"));
|
||||
await act(async () => {
|
||||
const name = form.querySelector<HTMLInputElement>("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<HTMLInputElement>('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("Delete 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("Delete habit Daily water").click());
|
||||
await act(async () => buttonNamed("Delete 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<HTMLFormElement>('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.textContent).toContain("Not scheduled today");
|
||||
expect(container.querySelector<HTMLInputElement>('.ds-editable-task input[type="checkbox"]')?.disabled).toBe(true);
|
||||
await act(async () => buttonNamed("Edit task Walk").click());
|
||||
const form = container.querySelector<HTMLFormElement>('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<HTMLFormElement>('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);
|
||||
expect(container.querySelector("form")).toBeNull();
|
||||
await act(async () => buttonNamed("Edit task Walk").click());
|
||||
const reopened = container.querySelector<HTMLFormElement>('form[aria-label="Edit task Walk"]')!;
|
||||
expect(reopened.querySelector("select")?.value).toBe(schedule.type);
|
||||
if (schedule.type === "interval" || schedule.type === "weekly") {
|
||||
expect(reopened.querySelector<HTMLInputElement>('input[type="number"]')?.value).toBe(String(schedule.every));
|
||||
expect(reopened.querySelector<HTMLInputElement>('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<HTMLSelectElement>('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<HTMLInputElement>('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<HTMLInputElement>('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<HTMLFormElement>('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("Delete 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("Delete habit").click());
|
||||
expect(container.querySelector("form")).toBeNull();
|
||||
expect(container.textContent).toContain("Your change was saved, but the dashboard could not refresh");
|
||||
expect(buttonNamed("Delete 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(".ds-counter output")?.textContent).toBe("0 / 8");
|
||||
expect(buttonNamed("Increase Water").disabled).toBe(false);
|
||||
await act(async () => buttonNamed("Increase Water").click());
|
||||
expect(container.querySelector(".ds-counter output")?.textContent).toBe("1 / 8");
|
||||
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(
|
||||
@@ -163,8 +478,8 @@ test("unscheduled dates stay inspectable without a progress-square fill", async
|
||||
).toBe("true");
|
||||
});
|
||||
|
||||
describe("design-system-only routing", () => {
|
||||
for (const path of ["/design-system", "/design-system/", "/", "/about", "/settings", "/missing", "/?auth_error=denied"]) {
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user