1
2
17
18(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
19import { act } from "react";
20import { createRoot, type Root } from "react-dom/client";
21import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
22import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
23
24vi.mock("@/lib/api", () => ({
25 workspaceApi: {
26 get: vi.fn(async (idOrSlug: string) => ({ id: idOrSlug, name: `WS ${idOrSlug}` })),
27 },
28}));
29
30import { workspaceApi } from "@/lib/api";
31import { useWorkspace } from "../use-workspace";
32import type { UseQueryResult } from "@tanstack/react-query";
33import type { Workspace } from "@/lib/types";
34
35const getMock = vi.mocked(workspaceApi.get);
36
37let container: HTMLDivElement;
38let root: Root;
39let client: QueryClient;
40let last: UseQueryResult<Workspace, Error> | undefined;
41
42function renderHook(slugOrId: string) {
43 function Probe() {
44 last = useWorkspace(slugOrId);
45 return null;
46 }
47 root.render(
48 <QueryClientProvider client={client}>
49 <Probe />
50 </QueryClientProvider>
51 );
52}
53
54async function flush(ms = 50) {
55 await act(async () => {
56 await new Promise((r) => setTimeout(r, ms));
57 });
58}
59
60
61async function until(predicate: () => boolean, budgetMs = 2000) {
62 const started = Date.now();
63 while (!predicate()) {
64 if (Date.now() - started > budgetMs) return;
65 await flush(10);
66 }
67}
68
69beforeEach(() => {
70 container = document.createElement("div");
71 document.body.appendChild(container);
72 root = createRoot(container);
73 client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
74
75 getMock.mockReset();
76 getMock.mockImplementation(async (idOrSlug: string) => ({
77 id: idOrSlug,
78 name: `WS ${idOrSlug}`,
79 slug: idOrSlug,
80 plan: "free",
81 role: "owner",
82 memberCount: 1,
83 createdAt: "2026-01-01T00:00:00Z"
84 }));
85 last = undefined;
86});
87
88afterEach(() => {
89 act(() => root.unmount());
90 container.remove();
91});
92
93describe("useWorkspace", () => {
94 it("does NOT fetch the `default` magic alias (the guaranteed-404 regression)", async () => {
95 renderHook("default");
96 await flush();
97
98 expect(getMock).not.toHaveBeenCalled();
99
100
101 expect(last).toMatchObject({ isError: false, data: undefined, isLoading: false });
102 });
103
104 it("fetches a real slug (or id) as before", async () => {
105 renderHook("demo");
106 await until(() => last?.data !== undefined);
107
108 expect(getMock).toHaveBeenCalledWith("demo");
109 expect(last?.data).toMatchObject({ id: "demo" });
110 expect(last?.isError).toBe(false);
111 });
112
113 it("does NOT fetch the empty route param (pre-existing guard preserved)", async () => {
114 renderHook("");
115 await flush();
116
117 expect(getMock).not.toHaveBeenCalled();
118 expect(last?.isError).toBe(false);
119 });
120});
121