UNPKG

rwsdk

Version:

Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime

888 lines (887 loc) 41.3 kB
import React from "react"; import { describe, expect, it } from "vitest"; import { defineRoutes, layout, matchPath, prefix, render, route, } from "./router"; describe("matchPath", () => { // Test case 1: Static paths it("should match static paths", () => { expect(matchPath("/about/", "/about/")).toEqual({}); expect(matchPath("/contact/", "/contact/")).toEqual({}); }); it("should not match different static paths", () => { expect(matchPath("/about/", "/service/")).toBeNull(); }); // Test case 2: Paths with parameters it("should match paths with parameters and extract them", () => { expect(matchPath("/users/:id/", "/users/123/")).toEqual({ id: "123" }); expect(matchPath("/posts/:category/:slug/", "/posts/tech/my-first-post/")).toEqual({ category: "tech", slug: "my-first-post" }); }); it("should not match if parameter is missing", () => { expect(matchPath("/users/:id/", "/users/")).toBeNull(); }); // Test case 3: Paths with wildcards it("should match paths with wildcards and extract them", () => { expect(matchPath("/files/*/", "/files/document.pdf/")).toEqual({ $0: "document.pdf", }); expect(matchPath("/data/*/content/", "/data/archive/content/")).toEqual({ $0: "archive", }); expect(matchPath("/assets/*/*/", "/assets/images/pic.png/")).toEqual({ $0: "images", $1: "pic.png", }); }); it("should match empty wildcard", () => { expect(matchPath("/files/*/", "/files//")).toEqual({ $0: "" }); }); // Test case 4: Paths with both parameters and wildcards it("should match paths with both parameters and wildcards", () => { expect(matchPath("/products/:productId/*/", "/products/abc/details/more/")).toEqual({ productId: "abc", $0: "details/more" }); }); // Test case 5: Paths that don't match it("should return null for non-matching paths", () => { expect(matchPath("/specific/path/", "/a/different/path/")).toBeNull(); }); // Test case 6: Edge cases it("should handle trailing slashes correctly", () => { // Current implementation in defineRoutes adds a trailing slash if missing, // and route() function also enforces it. matchPath itself doesn't normalize. expect(matchPath("/path/", "/path")).toBeNull(); // Path to match must end with / expect(matchPath("/path/", "/path/")).toEqual({}); }); it("should handle paths with multiple parameters and wildcards interspersed", () => { expect(matchPath("/type/:typeId/item/*/:itemId/*/", "/type/a/item/image/b/thumb/")).toEqual({ typeId: "a", $0: "image", itemId: "b", $1: "thumb" }); }); it("should not allow named parameters or wildcards in the same path", () => { expect(() => matchPath("/type/:typeId:is:broken", "/type/a-thumb-drive")).toThrow(); expect(() => matchPath("/type/**", "/type/a-thumb-drive")).toThrow(); }); }); describe("defineRoutes - Request Handling Behavior", () => { // Helper to create mock dependencies using dependency injection const createMockDependencies = () => { const mockRequestInfo = { request: new Request("http://localhost:3000/"), params: {}, ctx: {}, rw: { nonce: "test-nonce", Document: () => React.createElement("html"), rscPayload: true, ssr: true, databases: new Map(), scriptsToBeLoaded: new Set(), entryScripts: new Set(), inlineScripts: new Set(), pageRouteResolved: undefined, }, cf: {}, response: { headers: new Headers() }, isAction: false, }; const mockRenderPage = async (requestInfo, Page, onError) => { return new Response(`Rendered: ${Page.name || "Component"}`, { headers: { "content-type": "text/html" }, }); }; const mockRscActionHandler = async (request) => { return { actionResult: "test-action-result" }; }; const mockRunWithRequestInfoOverrides = async (overrides, fn) => { // Merge overrides into the mock request info Object.assign(mockRequestInfo, overrides); return await fn(); }; return { mockRequestInfo, mockRenderPage, mockRscActionHandler, mockRunWithRequestInfoOverrides, getRequestInfo: () => mockRequestInfo, onError: (error) => { throw error; }, }; }; describe("Sequential Route Evaluation", () => { it("should process routes in the exact order they are defined", async () => { const executionOrder = []; const middleware1 = (requestInfo) => { executionOrder.push("middleware1"); }; const middleware2 = (requestInfo) => { executionOrder.push("middleware2"); }; const PageComponent = () => { executionOrder.push("PageComponent"); return React.createElement("div", {}, "Page"); }; const router = defineRoutes([ middleware1, middleware2, route("/test/", PageComponent), ]); const deps = createMockDependencies(); deps.mockRequestInfo.request = new Request("http://localhost:3000/test/"); const request = new Request("http://localhost:3000/test/"); await router.handle({ request, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(executionOrder).toEqual([ "middleware1", "middleware2", "PageComponent", ]); }); }); describe("Middleware Short-Circuiting", () => { it("should stop processing when middleware returns a Response", async () => { const executionOrder = []; const middleware1 = (requestInfo) => { executionOrder.push("middleware1"); }; const middleware2 = (requestInfo) => { executionOrder.push("middleware2"); return new Response("Middleware2 Response", { status: 200 }); }; const middleware3 = (requestInfo) => { executionOrder.push("middleware3"); }; const PageComponent = () => { executionOrder.push("PageComponent"); return React.createElement("div", {}, "Page"); }; const router = defineRoutes([ middleware1, middleware2, middleware3, route("/test/", PageComponent), ]); const deps = createMockDependencies(); deps.mockRequestInfo.request = new Request("http://localhost:3000/test/"); const request = new Request("http://localhost:3000/test/"); const response = await router.handle({ request, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(executionOrder).toEqual(["middleware1", "middleware2"]); expect(await response.text()).toBe("Middleware2 Response"); expect(response.status).toBe(200); }); it("should stop processing when middleware returns a JSX element", async () => { const executionOrder = []; const middleware1 = (requestInfo) => { executionOrder.push("middleware1"); }; const middleware2 = (requestInfo) => { executionOrder.push("middleware2"); return React.createElement("div", {}, "Middleware JSX"); }; const PageComponent = () => { executionOrder.push("PageComponent"); return React.createElement("div", {}, "Page"); }; const router = defineRoutes([ middleware1, middleware2, route("/test/", PageComponent), ]); const deps = createMockDependencies(); deps.mockRequestInfo.request = new Request("http://localhost:3000/test/"); const request = new Request("http://localhost:3000/test/"); const response = await router.handle({ request, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(executionOrder).toEqual(["middleware1", "middleware2"]); expect(await response.text()).toBe("Rendered: Element"); }); }); describe("Prefix Handling", () => { it("should only run middleware within the specified prefix", async () => { const executionOrder = []; const prefixedMiddleware = () => { executionOrder.push("prefixedMiddleware"); }; const PageComponent = () => { executionOrder.push("PageComponent"); return React.createElement("div", {}, "Page"); }; const AdminPageComponent = () => { executionOrder.push("AdminPageComponent"); return React.createElement("div", {}, "Admin Page"); }; const router = defineRoutes([ ...prefix("/admin", [ prefixedMiddleware, route("/", AdminPageComponent), ]), route("/", PageComponent), ]); const deps = createMockDependencies(); // Test 1: Request to a path outside the prefix deps.mockRequestInfo.request = new Request("http://localhost:3000/"); const request1 = new Request("http://localhost:3000/"); await router.handle({ request: request1, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(executionOrder).toEqual(["PageComponent"]); // Reset execution order executionOrder.length = 0; // Test 2: Request to a path inside the prefix deps.mockRequestInfo.request = new Request("http://localhost:3000/admin/"); const request2 = new Request("http://localhost:3000/admin/"); await router.handle({ request: request2, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(executionOrder).toEqual([ "prefixedMiddleware", "AdminPageComponent", ]); }); it("should short-circuit from a prefixed middleware", async () => { const executionOrder = []; const prefixedMiddleware = () => { executionOrder.push("prefixedMiddleware"); return new Response("From prefixed middleware"); }; const AdminPageComponent = () => { executionOrder.push("AdminPageComponent"); return React.createElement("div", {}, "Admin Page"); }; const router = defineRoutes([ ...prefix("/admin", [ prefixedMiddleware, route("/", AdminPageComponent), ]), ]); const deps = createMockDependencies(); // Request to a path inside the prefix deps.mockRequestInfo.request = new Request("http://localhost:3000/admin/"); const request = new Request("http://localhost:3000/admin/"); const response = await router.handle({ request, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(executionOrder).toEqual(["prefixedMiddleware"]); expect(await response.text()).toBe("From prefixed middleware"); }); }); describe("RSC Action Handling", () => { it("should handle RSC actions before the first route definition", async () => { const executionOrder = []; const middleware1 = (requestInfo) => { executionOrder.push("middleware1"); }; const PageComponent = () => { executionOrder.push("PageComponent"); return React.createElement("div", {}, "Page"); }; const router = defineRoutes([ middleware1, route("/test/", PageComponent), ]); const deps = createMockDependencies(); deps.mockRequestInfo.request = new Request("http://localhost:3000/test/?__rsc_action_id=test"); deps.mockRscActionHandler = async (request) => { executionOrder.push("rscActionHandler"); return { actionResult: "test-result" }; }; const request = new Request("http://localhost:3000/test/?__rsc_action_id=test"); await router.handle({ request, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(executionOrder).toEqual([ "middleware1", "rscActionHandler", "PageComponent", ]); expect(deps.mockRequestInfo.rw.actionResult).toEqual({ actionResult: "test-result", }); }); it("should not handle RSC actions multiple times for multiple routes", async () => { const executionOrder = []; const PageComponent1 = () => { executionOrder.push("PageComponent1"); return React.createElement("div", {}, "Page1"); }; const PageComponent2 = () => { executionOrder.push("PageComponent2"); return React.createElement("div", {}, "Page2"); }; const router = defineRoutes([ route("/other/", PageComponent1), route("/test/", PageComponent2), ]); const deps = createMockDependencies(); deps.mockRequestInfo.request = new Request("http://localhost:3000/test/?__rsc_action_id=test"); deps.mockRscActionHandler = async (request) => { executionOrder.push("rscActionHandler"); return { actionResult: "test-result" }; }; const request = new Request("http://localhost:3000/test/?__rsc_action_id=test"); await router.handle({ request, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(executionOrder).toEqual(["rscActionHandler", "PageComponent2"]); // Should only call action handler once, even though there are multiple routes }); }); describe("Page Route Matching and Rendering", () => { it("should match the first route that matches the path", async () => { const executionOrder = []; const PageComponent1 = () => { executionOrder.push("PageComponent1"); return React.createElement("div", {}, "Page1"); }; const PageComponent2 = () => { executionOrder.push("PageComponent2"); return React.createElement("div", {}, "Page2"); }; const router = defineRoutes([ route("/test/", PageComponent1), route("/test/", PageComponent2), // This should never be reached ]); const deps = createMockDependencies(); deps.mockRequestInfo.request = new Request("http://localhost:3000/test/"); const request = new Request("http://localhost:3000/test/"); await router.handle({ request, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(executionOrder).toEqual(["PageComponent1"]); }); it("should continue to next route if path does not match", async () => { const executionOrder = []; const PageComponent1 = () => { executionOrder.push("PageComponent1"); return React.createElement("div", {}, "Page1"); }; const PageComponent2 = () => { executionOrder.push("PageComponent2"); return React.createElement("div", {}, "Page2"); }; const router = defineRoutes([ route("/other/", PageComponent1), route("/test/", PageComponent2), ]); const deps = createMockDependencies(); deps.mockRequestInfo.request = new Request("http://localhost:3000/test/"); const request = new Request("http://localhost:3000/test/"); await router.handle({ request, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(executionOrder).toEqual(["PageComponent2"]); }); it("should return 404 when no routes match", async () => { const PageComponent = () => React.createElement("div", {}, "Page"); const router = defineRoutes([route("/other/", PageComponent)]); const deps = createMockDependencies(); deps.mockRequestInfo.request = new Request("http://localhost:3000/test/"); const request = new Request("http://localhost:3000/test/"); const response = await router.handle({ request, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(response.status).toBe(404); expect(await response.text()).toBe("Not Found"); }); }); describe("Multiple Render Blocks with SSR Configuration", () => { it("should short-circuit on first matching render block and not apply later configurations", async () => { const executionOrder = []; const ssrSettings = []; const Document1 = () => React.createElement("html", {}, "Doc1"); const Document2 = () => React.createElement("html", {}, "Doc2"); const PageComponent1 = (requestInfo) => { executionOrder.push("PageComponent1"); ssrSettings.push(requestInfo.rw.ssr); return React.createElement("div", {}, "Page1"); }; const PageComponent2 = (requestInfo) => { executionOrder.push("PageComponent2"); ssrSettings.push(requestInfo.rw.ssr); return React.createElement("div", {}, "Page2"); }; const router = defineRoutes([ ...render(Document1, [route("/test/", PageComponent1)], { ssr: true }), ...render(Document2, [route("/other/", PageComponent2)], { ssr: false, }), ]); const deps = createMockDependencies(); deps.mockRequestInfo.request = new Request("http://localhost:3000/test/"); const request = new Request("http://localhost:3000/test/"); await router.handle({ request, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(executionOrder).toEqual(["PageComponent1"]); expect(ssrSettings).toEqual([true]); // The second render block's ssr: false should not have been applied expect(deps.mockRequestInfo.rw.Document).toBe(Document1); }); }); describe("Route-Specific Middleware", () => { it("should execute route-specific middleware before the component", async () => { const executionOrder = []; const globalMiddleware = (requestInfo) => { executionOrder.push("globalMiddleware"); }; const routeMiddleware = (requestInfo) => { executionOrder.push("routeMiddleware"); }; const PageComponent = () => { executionOrder.push("PageComponent"); return React.createElement("div", {}, "Page"); }; const router = defineRoutes([ globalMiddleware, route("/test/", [routeMiddleware, PageComponent]), ]); const deps = createMockDependencies(); deps.mockRequestInfo.request = new Request("http://localhost:3000/test/"); const request = new Request("http://localhost:3000/test/"); await router.handle({ request, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(executionOrder).toEqual([ "globalMiddleware", "routeMiddleware", "PageComponent", ]); }); it("should short-circuit if route-specific middleware returns a Response", async () => { const executionOrder = []; const routeMiddleware = (requestInfo) => { executionOrder.push("routeMiddleware"); return new Response("Route Middleware Response"); }; const PageComponent = () => { executionOrder.push("PageComponent"); return React.createElement("div", {}, "Page"); }; const router = defineRoutes([ route("/test/", [routeMiddleware, PageComponent]), ]); const deps = createMockDependencies(); deps.mockRequestInfo.request = new Request("http://localhost:3000/test/"); const request = new Request("http://localhost:3000/test/"); const response = await router.handle({ request, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(executionOrder).toEqual(["routeMiddleware"]); expect(await response.text()).toBe("Route Middleware Response"); }); }); describe("Layout Handling", () => { it("should wrap components with layouts", async () => { const executionOrder = []; const TestLayout = ({ children }) => { executionOrder.push("TestLayout"); return React.createElement("div", { className: "layout" }, children); }; const PageComponent = () => { executionOrder.push("PageComponent"); return React.createElement("div", {}, "Page Content"); }; const router = defineRoutes([ ...layout(TestLayout, [route("/test/", PageComponent)]), ]); const deps = createMockDependencies(); deps.mockRequestInfo.request = new Request("http://localhost:3000/test/"); // Mock renderPage to track layout wrapping deps.mockRenderPage = async (requestInfo, WrappedComponent, onError) => { // The component should be wrapped with layouts const element = React.createElement(WrappedComponent); return new Response(`Rendered with layouts`); }; const request = new Request("http://localhost:3000/test/"); const response = await router.handle({ request, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(await response.text()).toBe("Rendered with layouts"); }); }); describe("Parameter Extraction", () => { it("should extract path parameters and make them available in request info", async () => { let extractedParams = null; const PageComponent = (requestInfo) => { extractedParams = requestInfo.params; return React.createElement("div", {}, `User: ${requestInfo.params.id}`); }; const router = defineRoutes([route("/users/:id/", PageComponent)]); const deps = createMockDependencies(); deps.mockRequestInfo.request = new Request("http://localhost:3000/users/123/"); const request = new Request("http://localhost:3000/users/123/"); await router.handle({ request, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(extractedParams).toEqual({ id: "123" }); }); }); describe("HTTP Method Routing", () => { it("should route GET request to get handler", async () => { const router = defineRoutes([ route("/test/", { get: () => new Response("GET Response"), post: () => new Response("POST Response"), }), ]); const deps = createMockDependencies(); deps.mockRequestInfo.request = new Request("http://localhost:3000/test/", { method: "GET", }); const request = new Request("http://localhost:3000/test/", { method: "GET", }); const response = await router.handle({ request, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(await response.text()).toBe("GET Response"); }); it("should route POST request to post handler", async () => { const router = defineRoutes([ route("/test/", { get: () => new Response("GET Response"), post: () => new Response("POST Response"), }), ]); const deps = createMockDependencies(); deps.mockRequestInfo.request = new Request("http://localhost:3000/test/", { method: "POST", }); const request = new Request("http://localhost:3000/test/", { method: "POST", }); const response = await router.handle({ request, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(await response.text()).toBe("POST Response"); }); it("should return 405 for unsupported method with Allow header", async () => { const router = defineRoutes([ route("/test/", { get: () => new Response("GET Response"), post: () => new Response("POST Response"), }), ]); const deps = createMockDependencies(); deps.mockRequestInfo.request = new Request("http://localhost:3000/test/", { method: "DELETE", }); const request = new Request("http://localhost:3000/test/", { method: "DELETE", }); const response = await router.handle({ request, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(response.status).toBe(405); expect(await response.text()).toBe("Method Not Allowed"); expect(response.headers.get("Allow")).toBe("GET, OPTIONS, POST"); }); it("should handle OPTIONS request with Allow header", async () => { const router = defineRoutes([ route("/test/", { get: () => new Response("GET Response"), post: () => new Response("POST Response"), }), ]); const deps = createMockDependencies(); deps.mockRequestInfo.request = new Request("http://localhost:3000/test/", { method: "OPTIONS", }); const request = new Request("http://localhost:3000/test/", { method: "OPTIONS", }); const response = await router.handle({ request, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(response.status).toBe(204); expect(response.headers.get("Allow")).toBe("GET, OPTIONS, POST"); }); it("should support custom methods (case-insensitive)", async () => { const router = defineRoutes([ route("/test/", { custom: { report: () => new Response("REPORT Response"), }, }), ]); const deps = createMockDependencies(); deps.mockRequestInfo.request = new Request("http://localhost:3000/test/", { method: "REPORT", }); const request = new Request("http://localhost:3000/test/", { method: "REPORT", }); const response = await router.handle({ request, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(await response.text()).toBe("REPORT Response"); }); it("should normalize custom method keys to lowercase", async () => { const router = defineRoutes([ route("/test/", { custom: { REPORT: () => new Response("REPORT Response"), }, }), ]); const deps = createMockDependencies(); deps.mockRequestInfo.request = new Request("http://localhost:3000/test/", { method: "report", }); const request = new Request("http://localhost:3000/test/", { method: "report", }); const response = await router.handle({ request, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(await response.text()).toBe("REPORT Response"); }); it("should disable 405 when config.disable405 is true", async () => { const router = defineRoutes([ route("/test/", { get: () => new Response("GET Response"), config: { disable405: true, }, }), ]); const deps = createMockDependencies(); deps.mockRequestInfo.request = new Request("http://localhost:3000/test/", { method: "POST", }); const request = new Request("http://localhost:3000/test/", { method: "POST", }); const response = await router.handle({ request, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(response.status).toBe(404); expect(await response.text()).toBe("Not Found"); }); it("should disable OPTIONS when config.disableOptions is true", async () => { const router = defineRoutes([ route("/test/", { get: () => new Response("GET Response"), post: () => new Response("POST Response"), config: { disableOptions: true, }, }), ]); const deps = createMockDependencies(); deps.mockRequestInfo.request = new Request("http://localhost:3000/test/", { method: "OPTIONS", }); const request = new Request("http://localhost:3000/test/", { method: "OPTIONS", }); const response = await router.handle({ request, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(response.status).toBe(405); expect(await response.text()).toBe("Method Not Allowed"); expect(response.headers.get("Allow")).toBe("GET, POST"); }); it("should support middleware arrays in method handlers", async () => { const executionOrder = []; const authMiddleware = () => { executionOrder.push("authMiddleware"); }; const getHandler = () => { executionOrder.push("getHandler"); return new Response("GET Response"); }; const router = defineRoutes([ route("/test/", { get: [authMiddleware, getHandler], }), ]); const deps = createMockDependencies(); deps.mockRequestInfo.request = new Request("http://localhost:3000/test/", { method: "GET", }); const request = new Request("http://localhost:3000/test/", { method: "GET", }); const response = await router.handle({ request, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(executionOrder).toEqual(["authMiddleware", "getHandler"]); expect(await response.text()).toBe("GET Response"); }); }); describe("Edge Cases", () => { it("should handle middleware-only apps with RSC actions", async () => { const executionOrder = []; const middleware1 = (requestInfo) => { executionOrder.push("middleware1"); }; const middleware2 = (requestInfo) => { executionOrder.push("middleware2"); return new Response("Middleware Response"); }; // No route definitions, only middleware const router = defineRoutes([middleware1, middleware2]); const deps = createMockDependencies(); deps.mockRequestInfo.request = new Request("http://localhost:3000/test/?__rsc_action_id=test"); deps.mockRscActionHandler = async (request) => { executionOrder.push("rscActionHandler"); return { actionResult: "test-result" }; }; const request = new Request("http://localhost:3000/test/?__rsc_action_id=test"); const response = await router.handle({ request, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); // Action should still be handled even with no route definitions expect(executionOrder).toEqual(["middleware1", "middleware2"]); expect(await response.text()).toBe("Middleware Response"); }); it("should handle trailing slash normalization", async () => { const PageComponent = () => React.createElement("div", {}, "Page"); const router = defineRoutes([route("/test/", PageComponent)]); const deps = createMockDependencies(); // Request without trailing slash should be normalized deps.mockRequestInfo.request = new Request("http://localhost:3000/test"); const request = new Request("http://localhost:3000/test"); const response = await router.handle({ request, renderPage: deps.mockRenderPage, getRequestInfo: deps.getRequestInfo, onError: deps.onError, runWithRequestInfoOverrides: deps.mockRunWithRequestInfoOverrides, rscActionHandler: deps.mockRscActionHandler, }); expect(response.status).not.toBe(404); expect(await response.text()).toBe("Rendered: Element"); }); }); });