Skip to content

Commit 4fac433

Browse files
committed
Add tests
1 parent a6124fb commit 4fac433

3 files changed

Lines changed: 227 additions & 2 deletions

File tree

packages/core/src/routing/navigation.test.tsx

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { renderHook, waitFor } from "@testing-library/react";
22
import { describe, it, expect, beforeEach } from "vitest";
33
import { AppShell } from "../components/appshell";
44
import { useNavItems } from "./navigation";
5-
import { defineModule, defineResource, hidden } from "@/resource";
5+
import { defineModule, defineResource, hidden, redirectTo } from "@/resource";
66

77
const renderNavItems = (
88
modules: Array<ReturnType<typeof defineModule>>,
@@ -226,6 +226,115 @@ describe("useNavItems", () => {
226226
expect(resources.map((r) => r.url)).not.toContain("users/:id");
227227
});
228228

229+
it("keeps modules with redirectTo guards visible in navigation", async () => {
230+
const modules = [
231+
defineModule({
232+
path: "redirected",
233+
meta: { title: "Redirected Module" },
234+
resources: [
235+
defineResource({
236+
path: "target",
237+
component: () => <div>Target</div>,
238+
}),
239+
],
240+
guards: [() => redirectTo("/other")],
241+
}),
242+
defineModule({
243+
path: "dashboard",
244+
meta: { title: "Dashboard" },
245+
component: () => <div>Dashboard Root</div>,
246+
resources: [
247+
defineResource({
248+
path: "overview",
249+
component: () => <div>Overview</div>,
250+
}),
251+
],
252+
}),
253+
];
254+
255+
const { result } = renderNavItems(modules, "/dashboard/overview");
256+
257+
await waitFor(async () => {
258+
expect(await result.current!).toHaveLength(2);
259+
});
260+
261+
const navItems = await result.current!;
262+
expect(navItems.map((i) => i.title)).toEqual([
263+
"Redirected Module",
264+
"Dashboard",
265+
]);
266+
});
267+
268+
it("keeps resources with redirectTo guards visible in navigation", async () => {
269+
const modules = [
270+
defineModule({
271+
path: "workspace",
272+
meta: { title: "Workspace" },
273+
component: () => <div>Workspace Root</div>,
274+
resources: [
275+
defineResource({
276+
path: "visible",
277+
component: () => <div>Visible</div>,
278+
}),
279+
defineResource({
280+
path: "redirected",
281+
component: () => <div>Redirected</div>,
282+
guards: [() => redirectTo("/other")],
283+
}),
284+
],
285+
}),
286+
];
287+
288+
const { result } = renderNavItems(modules, "/workspace/visible");
289+
290+
await waitFor(async () => {
291+
expect(await result.current!).toHaveLength(1);
292+
});
293+
294+
const navItems = await result.current!;
295+
const resources = navItems[0].items;
296+
expect(resources).toHaveLength(2);
297+
expect(resources.map((r) => r.title)).toEqual(["Visible", "Redirected"]);
298+
});
299+
300+
it("keeps subResources with redirectTo guards visible in navigation", async () => {
301+
const modules = [
302+
defineModule({
303+
path: "admin",
304+
meta: { title: "Admin" },
305+
component: () => <div>Admin Root</div>,
306+
resources: [
307+
defineResource({
308+
path: "panel",
309+
component: () => <div>Panel</div>,
310+
subResources: [
311+
defineResource({
312+
path: "visible",
313+
component: () => <div>Visible</div>,
314+
}),
315+
defineResource({
316+
path: "redirected",
317+
component: () => <div>Redirected</div>,
318+
guards: [() => redirectTo("/other")],
319+
}),
320+
],
321+
}),
322+
],
323+
}),
324+
];
325+
326+
const { result } = renderNavItems(modules, "/admin/panel");
327+
328+
await waitFor(async () => {
329+
expect(await result.current!).toHaveLength(1);
330+
});
331+
332+
const navItems = await result.current!;
333+
const subItems = navItems[0].items[0].items!;
334+
expect(subItems).toHaveLength(2);
335+
expect(subItems.map((s) => s.title)).toEqual(["Visible", "Redirected"]);
336+
});
337+
229338
it("filters out hidden subResources by guards", async () => {
230339
const modules = [
231340
defineModule({

packages/core/src/routing/router.test.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,43 @@ describe("RouterContainer (memory)", () => {
262262
expect(await screen.findByText("Overview")).toBeDefined();
263263
});
264264

265+
it("redirects when module has no component and only redirectTo guard", async () => {
266+
const dashboardModule = defineModule({
267+
path: "dashboard",
268+
component: () => <div>Dashboard</div>,
269+
meta: { title: "Dashboard" },
270+
resources: [
271+
defineResource({
272+
path: "overview",
273+
component: () => <div>Overview</div>,
274+
meta: { title: "Overview" },
275+
}),
276+
],
277+
});
278+
279+
// Module without component — only redirectTo guard
280+
const redirectModule = defineModule({
281+
path: "redirect-only",
282+
meta: { title: "Redirect Only" },
283+
guards: [() => redirectTo("/dashboard/overview")],
284+
resources: [
285+
defineResource({
286+
path: "child",
287+
component: () => <div>Child</div>,
288+
meta: { title: "Child" },
289+
}),
290+
],
291+
});
292+
293+
renderWithConfig({
294+
modules: [dashboardModule, redirectModule],
295+
initialEntries: ["/redirect-only"],
296+
});
297+
298+
// Should redirect to dashboard/overview, not render a blank page
299+
expect(await screen.findByText("Overview")).toBeDefined();
300+
});
301+
265302
it("redirects resource when guard returns redirectTo", async () => {
266303
const dashboardModule = defineModule({
267304
path: "dashboard",

packages/core/src/routing/routes.test.tsx

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, expect, it, assert } from "vitest";
22
import { createContentRoutes } from "./routes";
33
import { EmptyOutlet, SettingsWrapper } from "@/components/content";
4-
import { defineModule, defineResource, pass } from "@/resource";
4+
import { defineModule, defineResource, pass, redirectTo } from "@/resource";
55

66
const createMockResource = (path: string) =>
77
defineResource({
@@ -160,6 +160,85 @@ describe("createContentRoutes", () => {
160160
expect(childResource?.loader).toBeUndefined();
161161
});
162162

163+
it("creates index route with loader and dummy component for module without component but with guards", () => {
164+
const module = defineModule({
165+
path: "redirect-module",
166+
meta: { title: "Redirect Module" },
167+
guards: [() => redirectTo("/dashboard")],
168+
resources: [createMockResource("child")],
169+
});
170+
171+
const routes = createContentRoutes({
172+
modules: [module],
173+
settingsResources: [],
174+
});
175+
176+
const moduleContainer = routes[1];
177+
const moduleRoute = moduleContainer.children?.[0];
178+
expect(moduleRoute?.path).toBe("redirect-module");
179+
180+
// Index route should exist with loader and dummy component
181+
const indexRoute = moduleRoute?.children?.[0];
182+
expect(indexRoute?.index).toBe(true);
183+
expect(indexRoute?.loader).toBeDefined();
184+
expect(typeof indexRoute?.loader).toBe("function");
185+
expect(typeof indexRoute?.Component).toBe("function");
186+
187+
// Child resource should still be present
188+
const childRoute = moduleRoute?.children?.[1];
189+
expect(childRoute?.path).toBe("child");
190+
});
191+
192+
it("does not create index route for module without component and without guards", () => {
193+
// This case should throw in defineModule, but we test createRoute's behavior
194+
// by using a module with component (which produces index route)
195+
const module = defineModule({
196+
path: "no-comp-no-guard",
197+
meta: { title: "Test" },
198+
component: () => <div>Test</div>,
199+
resources: [createMockResource("child")],
200+
});
201+
202+
const routes = createContentRoutes({
203+
modules: [module],
204+
settingsResources: [],
205+
});
206+
207+
const moduleContainer = routes[1];
208+
const moduleRoute = moduleContainer.children?.[0];
209+
const indexRoute = moduleRoute?.children?.[0];
210+
expect(indexRoute?.index).toBe(true);
211+
expect(indexRoute?.Component).toBeDefined();
212+
// No guards = no loader
213+
expect(indexRoute?.loader).toBeUndefined();
214+
});
215+
216+
it("redirect guard loader returns a redirect response", async () => {
217+
const module = defineModule({
218+
path: "redirect-module",
219+
meta: { title: "Redirect Module" },
220+
guards: [() => redirectTo("/dashboard")],
221+
resources: [createMockResource("child")],
222+
});
223+
224+
const routes = createContentRoutes({
225+
modules: [module],
226+
settingsResources: [],
227+
});
228+
229+
const moduleContainer = routes[1];
230+
const moduleRoute = moduleContainer.children?.[0];
231+
const indexRoute = moduleRoute?.children?.[0];
232+
const loader = indexRoute?.loader;
233+
expect(typeof loader).toBe("function");
234+
235+
// Execute the loader - it should return a redirect
236+
const result = await (loader as Function)({} as never);
237+
expect(result).toBeInstanceOf(Response);
238+
expect((result as Response).status).toBe(302);
239+
expect((result as Response).headers.get("Location")).toBe("/dashboard");
240+
});
241+
163242
it("attaches loader to index route only for resource (no cascade to sub-resources)", () => {
164243
const module = defineModule({
165244
path: "dashboard",

0 commit comments

Comments
 (0)