diff --git a/integration/middleware-test.ts b/integration/middleware-test.ts
index 1227f97ca0..7d9738e27b 100644
--- a/integration/middleware-test.ts
+++ b/integration/middleware-test.ts
@@ -10,6 +10,8 @@ import {
} from "react-router";
import {
+ type AppFixture,
+ type Fixture,
createAppFixture,
createFixture,
js,
@@ -17,75 +19,8 @@ import {
import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
import { reactRouterConfig } from "./helpers/vite.js";
-let ENTRY_SERVER_WITH_HANDLE_ERROR = js`
-import { PassThrough } from "node:stream";
-
-import type { AppLoadContext, EntryContext } from "react-router";
-import { createReadableStreamFromReadable } from "@react-router/node";
-import { ServerRouter } from "react-router";
-import type { RenderToPipeableStreamOptions } from "react-dom/server";
-import { renderToPipeableStream } from "react-dom/server";
-
-export const streamTimeout = 5_000;
-
-export function handleError(error, { request }) {
- if (!request.signal.aborted) {
- let {pathname, search} = new URL(request.url);
- console.error("handleError", request.method, pathname + search, error);
- }
-}
-
-export default function handleRequest(
- request: Request,
- responseStatusCode: number,
- responseHeaders: Headers,
- routerContext: EntryContext,
-) {
- return new Promise((resolve, reject) => {
- let shellRendered = false;
-
- const { pipe, abort } = renderToPipeableStream(
- ,
- {
- onShellReady() {
- shellRendered = true;
- const body = new PassThrough();
- const stream = createReadableStreamFromReadable(body);
-
- responseHeaders.set("Content-Type", "text/html");
-
- resolve(
- new Response(stream, {
- headers: responseHeaders,
- status: responseStatusCode,
- }),
- );
-
- pipe(body);
- },
- onShellError(error: unknown) {
- reject(error);
- },
- onError(error: unknown) {
- responseStatusCode = 500;
- // Log streaming rendering errors from inside the shell. Don't log
- // errors encountered during initial shell rendering since they'll
- // reject and get logged in handleDocumentRequest.
- if (shellRendered) {
- console.error(error);
- }
- },
- },
- );
-
- setTimeout(abort, streamTimeout + 1000);
- });
-}
- `;
-
test.describe("Middleware", () => {
let originalConsoleError = console.error;
-
test.beforeEach(() => {
console.error = () => {};
});
@@ -95,10 +30,14 @@ test.describe("Middleware", () => {
});
test.describe("SPA Mode", () => {
- test("calls clientMiddleware before/after loaders", async ({ page }) => {
- let fixture = await createFixture({
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+
+ test.beforeAll(async () => {
+ fixture = await createFixture({
spaMode: true,
files: {
+ // ...existing code...
"react-router.config.ts": reactRouterConfig({
ssr: false,
v8_middleware: true,
@@ -106,20 +45,15 @@ test.describe("Middleware", () => {
"vite.config.ts": js`
import { defineConfig } from "vite";
import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true, minify: false },
- plugins: [reactRouter()],
- });
+ export default defineConfig({ build: { manifest: true, minify: false }, plugins: [reactRouter()] });
`,
"app/context.ts": js`
import { createContext } from 'react-router'
export const orderContext = createContext([]);
`,
- "app/routes/_index.tsx": js`
+ "app/routes/loaders._index.tsx": js`
import { Link } from 'react-router'
import { orderContext } from '../context'
-
export const clientMiddleware = [
({ context }) => {
context.set(orderContext, [...context.get(orderContext), 'a']);
@@ -128,23 +62,20 @@ test.describe("Middleware", () => {
context.set(orderContext, [...context.get(orderContext), 'b']);
},
];
-
export async function clientLoader({ request, context }) {
return context.get(orderContext).join(',');
}
-
export default function Component({ loaderData }) {
return (
<>
Index: {loaderData}
- Go to about
+ Go to about
>
);
}
`,
- "app/routes/about.tsx": js`
+ "app/routes/loaders.about.tsx": js`
import { orderContext } from '../context'
-
export const clientMiddleware = [
({ context }) => {
context.set(orderContext, [...context.get(orderContext), 'c']);
@@ -153,40 +84,286 @@ test.describe("Middleware", () => {
context.set(orderContext, [...context.get(orderContext), 'd']);
},
];
-
export async function clientLoader({ context }) {
return context.get(orderContext).join(',');
}
-
export default function Component({ loaderData }) {
return About: {loaderData}
;
}
`,
+ "app/routes/actions._index.tsx": js`
+ import { Form } from 'react-router'
+ import { orderContext } from '../context';
+ export const clientMiddleware = [
+ ({ request, context }) => {
+ context.set(orderContext, ['a']);
+ },
+ ({ request, context }) => {
+ context.set(orderContext, [...context.get(orderContext), 'b']);
+ },
+ ];
+ export async function clientAction({ request, context }) {
+ return context.get(orderContext).join(',');
+ }
+ export async function clientLoader({ request, context }) {
+ return context.get(orderContext).join(',');
+ }
+ export default function Component({ loaderData, actionData }) {
+ return (
+ <>
+ Index: {loaderData} - {actionData || 'empty'}
+
+ >
+ );
+ }
+ `,
+ "app/routes/redirect-down._index.tsx": js`
+ import { Link } from 'react-router'
+ export default function Component({ loaderData, actionData }) {
+ return Link;
+ }
+ `,
+ "app/routes/redirect-down.redirect.tsx": js`
+ import { Link, redirect } from 'react-router'
+ export const clientMiddleware = [
+ ({ request, context }) => { throw redirect('/redirect-down/target'); }
+ ]
+ export default function Component() {
+ return Redirect
+ }
+ `,
+ "app/routes/redirect-down.target.tsx": js`
+ export default function Component() {
+ return Target
+ }
+ `,
+ "app/routes/redirect-up._index.tsx": js`
+ import { Link } from 'react-router';
+
+ export default function Component() {
+ return Link;
+ }
+ `,
+ "app/routes/redirect-up.redirect.tsx": js`
+ import { Link, redirect } from 'react-router';
+
+ export const clientMiddleware = [
+ async ({ request, context }, next) => {
+ await next();
+ throw redirect('/redirect-up/target');
+ }
+ ];
+
+ export default function Component() {
+ return Redirect
;
+ }
+ `,
+ "app/routes/redirect-up.target.tsx": js`
+ export default function Component() {
+ return Target
;
+ }
+ `,
+ "app/routes/error-down._index.tsx": js`
+ import { Link } from 'react-router';
+
+ export default function Component() {
+ return Link;
+ }
+ `,
+ "app/routes/error-down.broken.tsx": js`
+ export const clientMiddleware = [
+ async ({ request, context }, next) => {
+ throw new Error('broken!');
+ }
+ ];
+
+ export default function Component() {
+ return Should not see me
;
+ }
+
+ export function ErrorBoundary({ error }) {
+ return {error.message}
;
+ }
+ `,
+ "app/routes/error-up._index.tsx": js`
+ import { Link } from 'react-router';
+
+ export default function Component() {
+ return Link;
+ }
+ `,
+ "app/routes/error-up.broken.tsx": js`
+ export const clientMiddleware = [
+ async ({ request, context }, next) => {
+ await next();
+ throw new Error('broken!');
+ }
+ ];
+
+ export function clientLoader() {
+ return "nope";
+ }
+
+ export default function Component() {
+ return Should not see me
;
+ }
+
+ export function ErrorBoundary({ loaderData, error }) {
+ return (
+ <>
+ {error.message}
+ {loaderData ?? 'empty'}
+ >
+ );
+ }
+ `,
+ "app/routes/middleware-chain._index.tsx": js`
+ import { Link } from 'react-router'
+ export default function Component({ loaderData }) {
+ return Link;
+ }
+ `,
+ "app/routes/middleware-chain.a.tsx": js`
+ import { Outlet } from 'react-router'
+ import { orderContext } from '../context';
+ export const clientMiddleware = [
+ ({ context }) => {
+ context.set(orderContext, [...context.get(orderContext), 'a']);
+ },
+ ];
+
+ export function clientLoader({ context }) {
+ return context.get(orderContext).join(',');
+ }
+
+ export default function Component({ loaderData }) {
+ return <>A: {loaderData}
>;
+ }
+ `,
+ "app/routes/middleware-chain.a.b.tsx": js`
+ import { Outlet } from 'react-router'
+ import { orderContext } from '../context';
+ export const clientMiddleware = [
+ ({ context }) => {
+ context.set(orderContext, [...context.get(orderContext), 'b']);
+ }
+ ];
+
+ export default function Component() {
+ return <>B
>;
+ }
+ `,
+ "app/routes/middleware-chain.a.b.c.tsx": js`
+ import { orderContext } from '../context';
+ export function clientLoader({ context }) {
+ return context.get(orderContext).join(',');
+ }
+
+ export default function Component({ loaderData }) {
+ return C: {loaderData}
;
+ }
+ `,
},
});
+ appFixture = await createAppFixture(fixture);
+ });
- let appFixture = await createAppFixture(fixture);
+ test.afterAll(() => {
+ appFixture.close();
+ });
+ test("calls clientMiddleware before/after loaders", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
+ await app.goto("/loaders");
await page.waitForSelector('[data-route]:has-text("Index")');
expect(await page.locator("[data-route]").textContent()).toBe(
"Index: a,b",
);
- (await page.$('a[href="/about"]'))?.click();
+ (await page.$('a[href="/loaders/about"]'))?.click();
await page.waitForSelector('[data-route]:has-text("About")');
expect(await page.locator("[data-route]").textContent()).toBe(
"About: c,d",
);
+ });
- appFixture.close();
+ test("calls clientMiddleware before/after actions", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/actions");
+ await page.waitForSelector('[data-route]:has-text("Index")');
+ expect(await page.locator("[data-route]").textContent()).toBe(
+ "Index: a,b - empty",
+ );
+
+ (await page.getByRole("button"))?.click();
+ await new Promise((r) => setTimeout(r, 1000));
+ await page.waitForSelector('[data-route]:has-text("- a,b")');
+ expect(await page.locator("[data-route]").textContent()).toBe(
+ "Index: a,b - a,b",
+ );
});
- test("calls clientMiddleware before/after loaders with split route modules", async ({
+ test("handles redirects thrown on the way down", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/redirect-down");
+ await page.waitForSelector('a:has-text("Link")');
+
+ (await page.getByRole("link"))?.click();
+ await page.waitForSelector('h1:has-text("Target")');
+ });
+
+ test("handles redirects thrown on the way up", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/redirect-up");
+ await page.waitForSelector('a:has-text("Link")');
+
+ (await page.getByRole("link"))?.click();
+ await page.waitForSelector('h1:has-text("Target")');
+ });
+
+ test("handles errors thrown on the way down", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/error-down");
+ await page.waitForSelector('a:has-text("Link")');
+
+ (await page.getByRole("link"))?.click();
+ await page.waitForSelector("[data-error]");
+ expect(await page.innerText("[data-error]")).toBe("broken!");
+ });
+
+ test("handles errors thrown on the way up", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/error-up");
+ await page.waitForSelector('a:has-text("Link")');
+
+ (await page.getByRole("link"))?.click();
+ await page.waitForSelector("[data-error]");
+ expect(await page.innerText("[data-error]")).toBe("broken!");
+ expect(await page.innerText("pre")).toBe("empty");
+ });
+
+ test("calls clientMiddleware for routes even without a loader", async ({
page,
}) => {
- let fixture = await createFixture({
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/middleware-chain", true);
+ (await page.$('a[href="/middleware-chain/a/b/c"]'))?.click();
+ await page.waitForSelector("h4");
+ expect(await page.innerText("h2")).toBe("A: a,b");
+ expect(await page.innerText("h3")).toBe("B");
+ expect(await page.innerText("h4")).toBe("C: a,b");
+ });
+ });
+
+ test.describe("SPA Mode + Split Route Modules", () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+
+ test.beforeAll(async () => {
+ fixture = await createFixture({
spaMode: true,
files: {
"react-router.config.ts": reactRouterConfig({
@@ -207,7 +384,7 @@ test.describe("Middleware", () => {
import { createContext } from 'react-router'
export const orderContext = createContext([]);
`,
- "app/routes/_index.tsx": js`
+ "app/routes/loaders._index.tsx": js`
import { Link } from 'react-router'
import { orderContext } from '../context'
@@ -228,12 +405,12 @@ test.describe("Middleware", () => {
return (
<>
Index: {loaderData}
- Go to about
+ Go to about
>
);
}
`,
- "app/routes/about.tsx": js`
+ "app/routes/loaders.about.tsx": js`
import { orderContext } from '../context'
export const clientMiddleware = [
@@ -256,66 +433,111 @@ test.describe("Middleware", () => {
},
});
- let appFixture = await createAppFixture(fixture);
+ appFixture = await createAppFixture(fixture);
+ });
+
+ test.afterAll(() => {
+ appFixture.close();
+ });
+ test("calls clientMiddleware before/after loaders with split route modules", async ({
+ page,
+ }) => {
let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
+ await app.goto("/loaders");
await page.waitForSelector('[data-route]:has-text("Index")');
expect(await page.locator("[data-route]").textContent()).toBe(
"Index: a,b",
);
- (await page.$('a[href="/about"]'))?.click();
+ (await page.$('a[href="/loaders/about"]'))?.click();
await page.waitForSelector('[data-route]:has-text("About")');
expect(await page.locator("[data-route]").textContent()).toBe(
"About: c,d",
);
-
- appFixture.close();
});
+ });
- test("calls clientMiddleware before/after actions", async ({ page }) => {
- let fixture = await createFixture({
- spaMode: true,
+ test.describe("Client Middleware", () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+
+ test.beforeAll(async () => {
+ fixture = await createFixture({
files: {
"react-router.config.ts": reactRouterConfig({
- ssr: false,
v8_middleware: true,
}),
"vite.config.ts": js`
import { defineConfig } from "vite";
import { reactRouter } from "@react-router/dev/vite";
-
export default defineConfig({
- build: { manifest: true },
+ build: { manifest: true, minify: false },
plugins: [reactRouter()],
});
`,
"app/context.ts": js`
import { createContext } from 'react-router'
export const orderContext = createContext([]);
+ export const countContext = createContext({ parent: 0, child: 0, index: 0 });
`,
- "app/routes/_index.tsx": js`
- import { Form } from 'react-router'
- import { orderContext } from '../context';
-
+ "app/routes/loaders._index.tsx": js`
+ import { Link } from 'react-router'
+ import { orderContext } from '../context'
export const clientMiddleware = [
- ({ request, context }) => {
+ ({ context }) => {
+ context.set(orderContext, [...context.get(orderContext), 'a']);
+ },
+ ({ context }) => {
+ context.set(orderContext, [...context.get(orderContext), 'b']);
+ },
+ ];
+ export async function clientLoader({ request, context }) {
+ return context.get(orderContext).join(',');
+ }
+ export default function Component({ loaderData }) {
+ return (
+ <>
+ Index: {loaderData}
+ Go to about
+ >
+ );
+ }
+ `,
+ "app/routes/loaders.about.tsx": js`
+ import { orderContext } from '../context'
+ export const clientMiddleware = [
+ ({ context }) => {
+ context.set(orderContext, ['c']);
+ },
+ ({ context }) => {
+ context.set(orderContext, [...context.get(orderContext), 'd']);
+ },
+ ];
+ export async function clientLoader({ context }) {
+ return context.get(orderContext).join(',');
+ }
+ export default function Component({ loaderData }) {
+ return About: {loaderData}
;
+ }
+ `,
+ "app/routes/actions._index.tsx": js`
+ import { Form } from 'react-router'
+ import { orderContext } from '../context';
+ export const clientMiddleware = [
+ ({ context }) => {
context.set(orderContext, ['a']);
},
- ({ request, context }) => {
+ ({ context }) => {
context.set(orderContext, [...context.get(orderContext), 'b']);
},
];
-
export async function clientAction({ request, context }) {
return context.get(orderContext).join(',');
}
-
export async function clientLoader({ request, context }) {
return context.get(orderContext).join(',');
}
-
export default function Component({ loaderData, actionData }) {
return (
<>
@@ -325,90 +547,232 @@ test.describe("Middleware", () => {
>
- );
+ );
+ }
+ `,
+ "app/routes/redirect-down._index.tsx": js`
+ import { Link } from 'react-router'
+ export default function Component() {
+ return Link;
+ }
+ `,
+ "app/routes/redirect-down.redirect.tsx": js`
+ import { Link, redirect } from 'react-router'
+ export const clientMiddleware = [
+ ({ request, context }) => { throw redirect('/redirect-down/target'); }
+ ];
+ export default function Component() {
+ return Redirect
;
+ }
+ `,
+ "app/routes/redirect-down.target.tsx": js`
+ export default function Component() {
+ return Target
;
+ }
+ `,
+ "app/routes/redirect-up._index.tsx": js`
+ import { Link } from 'react-router';
+ export default function Component() {
+ return Link;
+ }
+ `,
+ "app/routes/redirect-up.redirect.tsx": js`
+ import { Link, redirect } from 'react-router';
+ export const clientMiddleware = [
+ async ({ request, context }, next) => {
+ await next();
+ throw redirect('/redirect-up/target');
+ }
+ ];
+ export default function Component() {
+ return Redirect
;
+ }
+ `,
+ "app/routes/redirect-up.target.tsx": js`
+ export default function Component() {
+ return Target
;
+ }
+ `,
+ "app/routes/error-down._index.tsx": js`
+ import { Link } from 'react-router';
+ export default function Component() {
+ return Link;
+ }
+ `,
+ "app/routes/error-down.broken.tsx": js`
+ export const clientMiddleware = [
+ async ({ request, context }, next) => {
+ throw new Error('broken!');
+ }
+ ];
+ export default function Component() {
+ return Should not see me
;
+ }
+ export function ErrorBoundary({ error }) {
+ return {error.message}
;
+ }
+ `,
+ "app/routes/error-up._index.tsx": js`
+ import { Link } from 'react-router';
+ export default function Component() {
+ return Link;
+ }
+ `,
+ "app/routes/error-up.broken.tsx": js`
+ export const clientMiddleware = [
+ async ({ request, context }, next) => {
+ await next();
+ throw new Error('broken!');
+ }
+ ];
+ export function clientLoader() {
+ return "nope";
+ }
+ export default function Component() {
+ return Should not see me
;
+ }
+ export function ErrorBoundary({ loaderData, error }) {
+ return (
+ <>
+ {error.message}
+ {loaderData ?? 'empty'}
+ >
+ );
+ }
+ `,
+ "app/routes/middleware-chain._index.tsx": js`
+ import { Link } from 'react-router'
+ export default function Component({ loaderData }) {
+ return Link;
+ }
+ `,
+ "app/routes/middleware-chain.a.tsx": js`
+ import { Outlet } from 'react-router'
+ import { orderContext } from '../context';
+ export const clientMiddleware = [
+ ({ context }) => {
+ context.set(orderContext, [...context.get(orderContext), 'a']);
+ },
+ ];
+ export function clientLoader({ context }) {
+ return context.get(orderContext).join(',');
+ }
+ export default function Component({ loaderData }) {
+ return (
+ <>
+ A: {loaderData}
+
+ >
+ );
+ }
+ `,
+ "app/routes/middleware-chain.a.b.tsx": js`
+ import { Outlet } from 'react-router'
+ import { orderContext } from '../context';
+ export const clientMiddleware = [
+ ({ context }) => {
+ context.set(orderContext, [...context.get(orderContext), 'b']);
+ },
+ ];
+ export default function Component() {
+ return (
+ <>
+ B
+
+ >
+ );
+ }
+ `,
+ "app/routes/middleware-chain.a.b.c.tsx": js`
+ import { orderContext } from '../context';
+ export function clientLoader({ context }) {
+ return context.get(orderContext).join(',');
+ }
+ export default function Component({ loaderData }) {
+ return C: {loaderData}
;
}
`,
},
});
+ appFixture = await createAppFixture(fixture);
+ });
- let appFixture = await createAppFixture(fixture);
+ test.afterAll(() => {
+ appFixture.close();
+ });
+ test("calls clientMiddleware before/after loaders", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
+ await app.goto("/loaders");
await page.waitForSelector('[data-route]:has-text("Index")');
expect(await page.locator("[data-route]").textContent()).toBe(
- "Index: a,b - empty",
+ "Index: a,b",
);
- (await page.getByRole("button"))?.click();
- await new Promise((r) => setTimeout(r, 1000));
- await page.waitForSelector('[data-route]:has-text("- a,b")');
+ (await page.$('a[href="/loaders/about"]'))?.click();
+ await page.waitForSelector('[data-route]:has-text("About")');
expect(await page.locator("[data-route]").textContent()).toBe(
- "Index: a,b - a,b",
+ "About: c,d",
);
-
- appFixture.close();
});
test("handles redirects thrown on the way down", async ({ page }) => {
- let fixture = await createFixture({
- spaMode: true,
- files: {
- "react-router.config.ts": reactRouterConfig({
- ssr: false,
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/redirect-down");
+ await page.waitForSelector('a:has-text("Link")');
- export default defineConfig({
- build: { manifest: true },
- plugins: [reactRouter()],
- });
- `,
- "app/routes/_index.tsx": js`
- import { Link } from 'react-router'
+ (await page.getByRole("link"))?.click();
+ await page.waitForSelector('h1:has-text("Target")');
+ });
- export default function Component({ loaderData, actionData }) {
- return Link;
- }
- `,
- "app/routes/redirect.tsx": js`
- import { Link, redirect } from 'react-router'
- export const clientMiddleware = [
- ({ request, context }) => { throw redirect('/target'); }
- ]
- export default function Component() {
- return Redirect
- }
- `,
- "app/routes/target.tsx": js`
- export default function Component() {
- return Target
- }
- `,
- },
- });
+ test("handles redirects thrown on the way up", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/redirect-up");
+ await page.waitForSelector('a:has-text("Link")');
- let appFixture = await createAppFixture(fixture);
+ (await page.getByRole("link"))?.click();
+ await page.waitForSelector('h1:has-text("Target")');
+ });
+ test("handles errors thrown on the way down", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
+ await app.goto("/error-down");
await page.waitForSelector('a:has-text("Link")');
(await page.getByRole("link"))?.click();
- await page.waitForSelector('h1:has-text("Target")');
+ await page.waitForSelector("[data-error]");
+ expect(await page.innerText("[data-error]")).toBe("broken!");
+ });
- appFixture.close();
+ test("handles errors thrown on the way up", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/error-up");
+ await page.waitForSelector('a:has-text("Link")');
+
+ (await page.getByRole("link"))?.click();
+ await page.waitForSelector("[data-error]");
+ expect(await page.innerText("[data-error]")).toBe("broken!");
+ expect(await page.innerText("pre")).toBe("empty");
});
- test("handles redirects thrown on the way up", async ({ page }) => {
+ test("calls clientMiddleware for routes even without a loader", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/middleware-chain");
+ (await page.$('a[href="/middleware-chain/a/b/c"]'))?.click();
+ await page.waitForSelector("h4");
+ expect(await page.innerText("h2")).toBe("A: a,b");
+ expect(await page.innerText("h3")).toBe("B");
+ expect(await page.innerText("h4")).toBe("C: a,b");
+ });
+
+ test("calls clientMiddleware once when multiple server requests happen", async ({
+ page,
+ }) => {
let fixture = await createFixture({
- spaMode: true,
files: {
"react-router.config.ts": reactRouterConfig({
- ssr: false,
v8_middleware: true,
}),
"vite.config.ts": js`
@@ -416,32 +780,67 @@ test.describe("Middleware", () => {
import { reactRouter } from "@react-router/dev/vite";
export default defineConfig({
- build: { manifest: true },
+ build: { manifest: true, minify: false },
plugins: [reactRouter()],
});
`,
+ "app/context.ts": js`
+ import { createContext } from 'react-router'
+ export const countContext = createContext({
+ parent: 0,
+ child: 0,
+ });
+ `,
"app/routes/_index.tsx": js`
import { Link } from 'react-router'
-
- export default function Component({ loaderData, actionData }) {
- return Link;
+ export default function Component({ loaderData }) {
+ return Go to /parent/child;
}
`,
- "app/routes/redirect.tsx": js`
- import { Link, redirect } from 'react-router'
+ "app/routes/parent.tsx": js`
+ import { countContext } from '../context';
+ import { Outlet } from 'react-router';
+ export function loader() {
+ return 'PARENT'
+ }
export const clientMiddleware = [
- async ({ request, context }, next) => {
- await next();
- throw redirect('/target');
+ ({ context }) => { context.get(countContext).parent++ },
+ ];
+
+ export async function clientLoader({ serverLoader, context }) {
+ return {
+ serverData: await serverLoader(),
+ context: context.get(countContext)
}
- ]
- export default function Component() {
- return Redirect
+ }
+
+ export default function Component({ loaderData }) {
+ return (
+ <>
+ {JSON.stringify(loaderData)}
+
+ >
+ );
}
`,
- "app/routes/target.tsx": js`
- export default function Component() {
- return Target
+ "app/routes/parent.child.tsx": js`
+ import { countContext } from '../context';
+ export function loader() {
+ return 'CHILD'
+ }
+ export const clientMiddleware = [
+ ({ context }) => { context.get(countContext).child++ },
+ ];
+
+ export async function clientLoader({ serverLoader, context }) {
+ return {
+ serverData: await serverLoader(),
+ context: context.get(countContext)
+ }
+ }
+
+ export default function Component({ loaderData }) {
+ return {JSON.stringify(loaderData)}
;
}
`,
},
@@ -449,152 +848,53 @@ test.describe("Middleware", () => {
let appFixture = await createAppFixture(fixture);
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- await page.waitForSelector('a:has-text("Link")');
-
- (await page.getByRole("link"))?.click();
- await page.waitForSelector('h1:has-text("Target")');
-
- appFixture.close();
- });
-
- test("handles errors thrown on the way down", async ({ page }) => {
- let fixture = await createFixture(
- {
- spaMode: true,
- files: {
- "react-router.config.ts": reactRouterConfig({
- ssr: false,
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true },
- plugins: [reactRouter()],
- });
- `,
- "app/routes/_index.tsx": js`
- import { Link } from 'react-router'
-
- export default function Component() {
- return Link;
- }
- `,
- "app/routes/broken.tsx": js`
- export const clientMiddleware = [
- async ({ request, context }, next) => {
- throw new Error('broken!');
- }
- ]
- export default function Component() {
- return Should not see me
- }
- export function ErrorBoundary({ error }) {
- return {error.message}
- }
- `,
- },
- },
- UNSAFE_ServerMode.Development,
- );
-
- let appFixture = await createAppFixture(
- fixture,
- UNSAFE_ServerMode.Development,
- );
+ let requests: string[] = [];
+ page.on("request", (request: PlaywrightRequest) => {
+ if (request.url().includes(".data")) {
+ requests.push(request.url());
+ }
+ });
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/");
- await page.waitForSelector('a:has-text("Link")');
-
- (await page.getByRole("link"))?.click();
- await page.waitForSelector("[data-error]");
- expect(await page.innerText("[data-error]")).toBe("broken!");
-
- appFixture.close();
- });
-
- test("handles errors thrown on the way up", async ({ page }) => {
- let fixture = await createFixture(
- {
- spaMode: true,
- files: {
- "react-router.config.ts": reactRouterConfig({
- ssr: false,
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
+ (await page.$('a[href="/parent/child"]'))?.click();
+ await page.waitForSelector("[data-child]");
- export default defineConfig({
- build: { manifest: true },
- plugins: [reactRouter()],
- });
- `,
- "app/routes/_index.tsx": js`
- import { Link } from 'react-router'
+ // 2 separate server requests made
+ expect(requests.sort()).toEqual([
+ expect.stringContaining("/parent/child.data?_routes=routes%2Fparent"),
+ expect.stringContaining(
+ "/parent/child.data?_routes=routes%2Fparent.child",
+ ),
+ ]);
- export default function Component() {
- return Link;
- }
- `,
- "app/routes/broken.tsx": js`
- export const clientMiddleware = [
- async ({ request, context }, next) => {
- await next();
- throw new Error('broken!');
- }
- ]
- export function clientLoader() {
- return "nope"
- }
- export default function Component() {
- return Should not see me
- }
- export function ErrorBoundary({ loaderData, error }) {
- return (
- <>
- {error.message}
- {loaderData ?? 'empty'}
- >
- );
- }
- `,
- },
+ // But client middlewares only ran once
+ let json = (await page.locator("[data-parent]").textContent()) as string;
+ expect(JSON.parse(json)).toEqual({
+ serverData: "PARENT",
+ context: {
+ parent: 1,
+ child: 1,
},
- UNSAFE_ServerMode.Development,
- );
-
- let appFixture = await createAppFixture(
- fixture,
- UNSAFE_ServerMode.Development,
- );
-
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- await page.waitForSelector('a:has-text("Link")');
-
- (await page.getByRole("link"))?.click();
- await page.waitForSelector("[data-error]");
- expect(await page.innerText("[data-error]")).toBe("broken!");
- expect(await page.innerText("pre")).toBe("empty");
+ });
+ json = (await page.locator("[data-child]").textContent()) as string;
+ expect(JSON.parse(json)).toEqual({
+ serverData: "CHILD",
+ context: {
+ parent: 1,
+ child: 1,
+ },
+ });
appFixture.close();
});
- test("calls clientMiddleware for routes even without a loader", async ({
+ test("calls clientMiddleware once when multiple server requests happen and some routes opt out", async ({
page,
}) => {
let fixture = await createFixture({
- spaMode: true,
files: {
"react-router.config.ts": reactRouterConfig({
- ssr: false,
v8_middleware: true,
}),
"vite.config.ts": js`
@@ -608,77 +908,160 @@ test.describe("Middleware", () => {
`,
"app/context.ts": js`
import { createContext } from 'react-router'
- export const orderContext = createContext([]);
+ export const countContext = createContext({
+ parent: 0,
+ child: 0,
+ index: 0,
+ });
`,
"app/routes/_index.tsx": js`
import { Link } from 'react-router'
export default function Component({ loaderData }) {
- return Link;
+ return Go to /parent/child;
}
`,
- "app/routes/a.tsx": js`
- import { Outlet } from 'react-router'
- import { orderContext } from '../context';
+ "app/routes/parent.tsx": js`
+ import { Outlet } from 'react-router';
+ import { countContext } from '../context';
+ export function loader() {
+ return 'PARENT'
+ }
export const clientMiddleware = [
- ({ context }) => {
- context.set(orderContext, [...context.get(orderContext), 'a']);
- },
+ ({ context }) => { context.get(countContext).parent++ },
];
-
- export function clientLoader({ context }) {
- return context.get(orderContext).join(',');
- }
-
export default function Component({ loaderData }) {
- return <>A: {loaderData}
>;
+ return (
+ <>
+ {loaderData}
+
+ >
+ );
}
- `,
- "app/routes/a.b.tsx": js`
- import { Outlet } from 'react-router'
- import { orderContext } from '../context';
- export const clientMiddleware = [
- ({ context }) => {
- context.set(orderContext, [...context.get(orderContext), 'b']);
- }
- ];
-
- export default function Component() {
- return <>B
>;
+ export function shouldRevalidate() {
+ return false;
}
`,
- "app/routes/a.b.c.tsx": js`
- import { orderContext } from '../context';
- export function clientLoader({ context }) {
- return context.get(orderContext).join(',');
+ "app/routes/parent.child.tsx": js`
+ import { Outlet } from 'react-router';
+ import { countContext } from '../context';
+ export function loader() {
+ return 'CHILD'
}
-
+ export const clientMiddleware = [
+ ({ context }) => { context.get(countContext).child++ },
+ ];
export default function Component({ loaderData }) {
- return C: {loaderData}
;
+ return (
+ <>
+ {loaderData}
+
+ >
+ );
}
`,
- },
- });
+ "app/routes/parent.child._index.tsx": js`
+ import { Form } from 'react-router';
+ import { countContext } from '../context';
+ export function action() {
+ return 'INDEX ACTION'
+ }
+ export function loader() {
+ return 'INDEX'
+ }
+ export const clientMiddleware = [
+ ({ context }) => { context.get(countContext).index++ },
+ ];
+ export async function clientLoader({ serverLoader, context }) {
+ return {
+ serverData: await serverLoader(),
+ context: context.get(countContext)
+ }
+ }
+ export default function Component({ loaderData, actionData }) {
+ return (
+ <>
+ {JSON.stringify(loaderData)}
+
+ {actionData ? {JSON.stringify(actionData)}
: null}
+ >
+ );
+ }
+ `,
+ },
+ });
let appFixture = await createAppFixture(fixture);
+ let requests: string[] = [];
+ page.on("request", (request: PlaywrightRequest) => {
+ if (request.method() === "GET" && request.url().includes(".data")) {
+ requests.push(request.url());
+ }
+ });
+
let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/", true);
- (await page.$('a[href="/a/b/c"]'))?.click();
- await page.waitForSelector("h4");
- expect(await page.innerText("h2")).toBe("A: a,b");
- expect(await page.innerText("h3")).toBe("B");
- expect(await page.innerText("h4")).toBe("C: a,b");
+ await app.goto("/");
+ (await page.$('a[href="/parent/child"]'))?.click();
+ await page.waitForSelector("[data-child]");
+ expect(await page.locator("[data-parent]").textContent()).toBe("PARENT");
+ expect(await page.locator("[data-child]").textContent()).toBe("CHILD");
+ expect(
+ JSON.parse((await page.locator("[data-index]").textContent())!),
+ ).toEqual({
+ serverData: "INDEX",
+ context: {
+ parent: 1,
+ child: 1,
+ index: 1,
+ },
+ });
+
+ requests = []; // clear before form submission
+ (await page.$('button[type="submit"]'))?.click();
+ await page.waitForSelector("[data-action]");
+
+ // 2 separate server requests made
+ expect(requests.sort()).toEqual([
+ // This is the normal request but only included parent.child because parent opted out
+ expect.stringMatching(
+ /\/parent\/child\.data\?_routes=routes%2Fparent\.child$/,
+ ),
+ // index gets it's own due to clientLoader
+ expect.stringMatching(
+ /\/parent\/child\.data\?_routes=routes%2Fparent\.child\._index$/,
+ ),
+ ]);
+
+ // But client middlewares only ran once for the action and once for the revalidation
+ expect(await page.locator("[data-parent]").textContent()).toBe("PARENT");
+ expect(await page.locator("[data-child]").textContent()).toBe("CHILD");
+ expect(
+ JSON.parse((await page.locator("[data-index]").textContent())!),
+ ).toEqual({
+ serverData: "INDEX",
+ context: {
+ parent: 3,
+ child: 3,
+ index: 3,
+ },
+ });
appFixture.close();
});
});
- test.describe("Client Middleware", () => {
- test("calls clientMiddleware before/after loaders", async ({ page }) => {
- let fixture = await createFixture({
+ test.describe("Client Middleware + Split Route Modules", () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+
+ test.beforeAll(async () => {
+ fixture = await createFixture({
files: {
"react-router.config.ts": reactRouterConfig({
v8_middleware: true,
+ splitRouteModules: true,
}),
"vite.config.ts": js`
import { defineConfig } from "vite";
@@ -693,7 +1076,7 @@ test.describe("Middleware", () => {
import { createContext } from 'react-router'
export const orderContext = createContext([]);
`,
- "app/routes/_index.tsx": js`
+ "app/routes/loaders._index.tsx": js`
import { Link } from 'react-router'
import { orderContext } from "../context";;
@@ -714,12 +1097,12 @@ test.describe("Middleware", () => {
return (
<>
Index: {loaderData}
- Go to about
+ Go to about
>
);
}
`,
- "app/routes/about.tsx": js`
+ "app/routes/loaders.about.tsx": js`
import { orderContext } from "../context";;
export const clientMiddleware = [
({ context }) => {
@@ -741,524 +1124,375 @@ test.describe("Middleware", () => {
},
});
- let appFixture = await createAppFixture(fixture);
+ appFixture = await createAppFixture(fixture);
+ });
+ test.afterAll(() => {
+ appFixture.close();
+ });
+
+ test("calls clientMiddleware before/after loaders with split route modules", async ({
+ page,
+ }) => {
let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
+ await app.goto("/loaders");
await page.waitForSelector('[data-route]:has-text("Index")');
expect(await page.locator("[data-route]").textContent()).toBe(
"Index: a,b",
);
- (await page.$('a[href="/about"]'))?.click();
+ (await page.$('a[href="/loaders/about"]'))?.click();
await page.waitForSelector('[data-route]:has-text("About")');
expect(await page.locator("[data-route]").textContent()).toBe(
"About: c,d",
);
-
- appFixture.close();
});
+ });
- test("calls clientMiddleware before/after loaders with split route modules", async ({
- page,
- }) => {
- let fixture = await createFixture({
+ test.describe("Server Middleware", () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+
+ test.beforeAll(async () => {
+ fixture = await createFixture({
files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- splitRouteModules: true,
- }),
+ "react-router.config.ts": reactRouterConfig({ v8_middleware: true }),
"vite.config.ts": js`
import { defineConfig } from "vite";
import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true, minify: false },
- plugins: [reactRouter()],
- });
+ export default defineConfig({ build: { manifest: true, minify: false }, plugins: [reactRouter()] });
`,
"app/context.ts": js`
import { createContext } from 'react-router'
export const orderContext = createContext([]);
`,
- "app/routes/_index.tsx": js`
+ "app/routes/loaders._index.tsx": js`
import { Link } from 'react-router'
- import { orderContext } from "../context";;
-
- export const clientMiddleware = [
- ({ context }) => {
- context.set(orderContext, [...context.get(orderContext), 'a']);
- },
- ({ context }) => {
- context.set(orderContext, [...context.get(orderContext), 'b']);
- },
+ import { orderContext } from '../context'
+ export const middleware = [
+ ({ context }) => { context.set(orderContext, [...context.get(orderContext), 'a']); },
+ ({ context }) => { context.set(orderContext, [...context.get(orderContext), 'b']); },
];
-
- export async function clientLoader({ request, context }) {
- return context.get(orderContext).join(',');
- }
-
+ export async function loader({ request, context }) { return context.get(orderContext).join(','); }
export default function Component({ loaderData }) {
- return (
- <>
- Index: {loaderData}
- Go to about
- >
- );
+ return (<>Index: {loaderData}
Go to about>);
}
`,
- "app/routes/about.tsx": js`
- import { orderContext } from "../context";;
- export const clientMiddleware = [
- ({ context }) => {
- context.set(orderContext, ['c']); // reset order from hydration
- },
- ({ context }) => {
- context.set(orderContext, [...context.get(orderContext), 'd']);
- },
+ "app/routes/loaders.about.tsx": js`
+ import { orderContext } from '../context'
+ export const middleware = [
+ ({ context }) => { context.set(orderContext, ['c']); },
+ ({ context }) => { context.set(orderContext, [...context.get(orderContext), 'd']); },
];
-
- export async function clientLoader({ context }) {
- return context.get(orderContext).join(',');
- }
-
+ export async function loader({ context }) { return context.get(orderContext).join(','); }
export default function Component({ loaderData }) {
return About: {loaderData}
;
}
`,
- },
- });
-
- let appFixture = await createAppFixture(fixture);
-
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- await page.waitForSelector('[data-route]:has-text("Index")');
- expect(await page.locator("[data-route]").textContent()).toBe(
- "Index: a,b",
- );
-
- (await page.$('a[href="/about"]'))?.click();
- await page.waitForSelector('[data-route]:has-text("About")');
- expect(await page.locator("[data-route]").textContent()).toBe(
- "About: c,d",
- );
-
- appFixture.close();
- });
-
- test("calls clientMiddleware when no loaders exist", async ({ page }) => {
- let fixture = await createFixture({
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true, minify: false },
- plugins: [reactRouter()],
- });
- `,
- "app/routes/_index.tsx": js`
- import { Link } from 'react-router'
+ "app/routes/no-loaders.parent.tsx": js`
+ import { Link, Outlet } from 'react-router'
- export const clientMiddleware = [
- ({ context }) => {
- console.log('running index middleware')
+ export const middleware = [
+ ({ request }) => {
+ console.log('Running parent middleware', new URL(request.url).pathname)
},
];
export default function Component() {
return (
<>
- Index
- Go to about
+ Parent
+ Go to A
+ Go to B
+
>
);
}
`,
- "app/routes/about.tsx": js`
- import { Link } from 'react-router'
- export const clientMiddleware = [
- ({ context }) => {
- console.log('running about middleware')
+ "app/routes/no-loaders.parent.a.tsx": js`
+ export const middleware = [
+ ({ request }) => {
+ console.log('Running A middleware', new URL(request.url).pathname)
},
];
export default function Component() {
- return (
- <>
- About
- Go to index
- >
- );
+ return A
;
}
`,
- },
- });
-
- let appFixture = await createAppFixture(fixture);
-
- let logs: string[] = [];
- page.on("console", (msg) => logs.push(msg.text()));
-
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
-
- (await page.$('a[href="/about"]'))?.click();
- await page.waitForSelector('[data-route]:has-text("About")');
- expect(logs).toEqual(["running about middleware"]);
- logs.splice(0);
-
- (await page.$('a[href="/"]'))?.click();
- await page.waitForSelector('[data-route]:has-text("Index")');
- expect(logs).toEqual(["running index middleware"]);
-
- appFixture.close();
- });
-
- test("calls clientMiddleware before/after actions", async ({ page }) => {
- let fixture = await createFixture({
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
+ "app/routes/no-loaders.parent.b.tsx": js`
+ export const middleware = [
+ ({ request }) => {
+ console.log('Running B middleware', new URL(request.url).pathname)
+ },
+ ];
- export default defineConfig({
- build: { manifest: true },
- plugins: [reactRouter()],
- });
- `,
- "app/context.ts": js`
- import { createContext } from 'react-router'
- export const orderContext = createContext([]);
+ export default function Component() {
+ return B
;
+ }
`,
- "app/routes/_index.tsx": js`
- import { Form } from 'react-router'
- import { orderContext } from "../context";;
- export const clientMiddleware = [
- ({ context }) => {
- context.set(orderContext, ['a']);
- },
- ({ context }) => {
- context.set(orderContext, [...context.get(orderContext), 'b']);
- },
+ "app/routes/actions._index.tsx": js`
+ import { Form } from 'react-router'
+ import { orderContext } from '../context';
+ export const middleware = [
+ ({ context }) => { context.set(orderContext, [...context.get(orderContext), 'a']); },
+ ({ context }) => { context.set(orderContext, [...context.get(orderContext), 'b']); },
];
-
- export async function clientAction({ request, context }) {
- return context.get(orderContext).join(',');
- }
-
- export async function clientLoader({ request, context }) {
- return context.get(orderContext).join(',');
- }
-
+ export async function action({ request, context }) { return context.get(orderContext).join(','); }
+ export async function loader({ request, context }) { return context.get(orderContext).join(','); }
export default function Component({ loaderData, actionData }) {
- return (
- <>
- Index: {loaderData} - {actionData || 'empty'}
-
- >
- );
+ return (<>Index: {loaderData} - {actionData || 'empty'}
>);
}
`,
- },
- });
-
- let appFixture = await createAppFixture(fixture);
-
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- await page.waitForSelector('[data-route]:has-text("Index")');
- expect(await page.locator("[data-route]").textContent()).toBe(
- "Index: a,b - empty",
- );
-
- (await page.getByRole("button"))?.click();
- await new Promise((r) => setTimeout(r, 1000));
- await page.waitForSelector('[data-route]:has-text("- a,b")');
- expect(await page.locator("[data-route]").textContent()).toBe(
- "Index: a,b - a,b",
- );
-
- appFixture.close();
- });
-
- test("handles redirects thrown on the way down", async ({ page }) => {
- let fixture = await createFixture({
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true },
- plugins: [reactRouter()],
- });
- `,
- "app/routes/_index.tsx": js`
+ "app/routes/redirect-down._index.tsx": js`
import { Link } from 'react-router'
-
- export default function Component({ loaderData, actionData }) {
- return Link;
+ export default function Component() {
+ return Link;
}
`,
- "app/routes/redirect.tsx": js`
+ "app/routes/redirect-down.redirect.tsx": js`
import { Link, redirect } from 'react-router'
- export const clientMiddleware = [
- ({ request, context }) => { throw redirect('/target'); }
- ]
+ export const middleware = [({ request, context }) => {
+ throw redirect('/redirect-down/target');
+ }];
+ export function loader() {
+ return null;
+ }
export default function Component() {
- return Redirect
+ return Redirect
;
}
`,
- "app/routes/target.tsx": js`
+ "app/routes/redirect-down.target.tsx": js`
export default function Component() {
- return Target
+ return Target
;
}
`,
- },
- });
-
- let appFixture = await createAppFixture(fixture);
-
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- await page.waitForSelector('a:has-text("Link")');
-
- (await page.getByRole("link"))?.click();
- await page.waitForSelector('h1:has-text("Target")');
-
- appFixture.close();
- });
-
- test("handles redirects thrown on the way up", async ({ page }) => {
- let fixture = await createFixture({
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true },
- plugins: [reactRouter()],
- });
+ "app/routes/redirect-up._index.tsx": js`
+ import { Link } from 'react-router';
+ export default function Component() {
+ return Link;
+ }
`,
- "app/routes/_index.tsx": js`
- import { Link } from 'react-router'
-
- export default function Component({ loaderData, actionData }) {
- return Link;
+ "app/routes/redirect-up.redirect.tsx": js`
+ import { Link, redirect } from 'react-router';
+ export const middleware = [async ({ request, context }, next) => {
+ await next(); throw redirect('/redirect-up/target');
+ }];
+ export function loader() {
+ return null;
+ }
+ export default function Component() {
+ return Redirect
;
+ }
+ `,
+ "app/routes/redirect-up.target.tsx": js`
+ export default function Component() {
+ return Target
;
}
`,
- "app/routes/redirect.tsx": js`
+ "app/routes/single-fetch-serialize.redirect.tsx": js`
import { Link, redirect } from 'react-router'
- export const clientMiddleware = [
+ export const middleware = [
async ({ request, context }, next) => {
- await next();
- throw redirect('/target');
+ let res = await next();
+ // Should still be a normal redirect here, not yet encoded into
+ // a single fetch redirect
+ res.headers.set("X-Status", res.status);
+ res.headers.set("X-Location", res.headers.get('Location'));
+ return res;
}
]
+ export function loader() {
+ throw redirect('/single-fetch-serialize/target');
+ }
export default function Component() {
return Redirect
}
`,
- "app/routes/target.tsx": js`
+ "app/routes/single-fetch-serialize.target.tsx": js`
export default function Component() {
return Target
}
`,
- },
- });
-
- let appFixture = await createAppFixture(fixture);
+ "app/routes/throw-data-before._index.tsx": js`
+ import { Link } from 'react-router'
+ export default function Component({ loaderData }) {
+ return /a/b/c;
+ }
+ `,
+ "app/routes/throw-data-before.a.tsx": js`
+ import { Outlet } from 'react-router'
+ export const middleware = [
+ async (_, next) => {
+ let res = await next();
+ res.headers.set('x-a', 'true');
+ return res;
+ }
+ ];
+ export default function Component() {
+ return
+ }
+ export function ErrorBoundary({ error }) {
+ return (
+ <>
+ A Error Boundary
+ {error.data}
+ >
+ );
+ }
+ `,
+ "app/routes/throw-data-before.a.b.tsx": js`
+ import { Link, Outlet } from 'react-router'
+ export const middleware = [
+ async (_, next) => {
+ let res = await next();
+ res.headers.set('x-b', 'true');
+ return res;
+ }
+ ];
+ export default function Component({ loaderData }) {
+ return ;
+ }
+ `,
+ "app/routes/throw-data-before.a.b.c.tsx": js`
+ import { data } from "react-router";
+ export const middleware = [(_, next) => {
+ throw data('C ERROR', { status: 418, statusText: "I'm a teapot" })
+ }];
+ // Force middleware to run on client side navs
+ export function loader() {
+ return null;
+ }
+ export default function Component({ loaderData }) {
+ return C
+ }
+ `,
+ "app/routes/throw-data-after._index.tsx": js`
+ import { Link } from 'react-router'
+ export default function Component({ loaderData }) {
+ return /a/b/c;
+ }
+ `,
+ "app/routes/throw-data-after.a.tsx": js`
+ import { Outlet } from 'react-router'
+ export const middleware = [
+ async (_, next) => {
+ let res = await next();
+ res.headers.set('x-a', 'true');
+ return res;
+ }
+ ];
+ export function loader() {
+ return "A LOADER";
+ }
+ export default function Component() {
+ return
+ }
+ export function ErrorBoundary({ error, loaderData }) {
+ return (
+ <>
+ A Error Boundary
+ {error.data}
+ {loaderData}
+ >
+ );
+ }
+ `,
+ "app/routes/throw-data-after.a.b.tsx": js`
+ import { Link, Outlet } from 'react-router'
+ export const middleware = [
+ async (_, next) => {
+ let res = await next();
+ res.headers.set('x-b', 'true');
+ return res;
+ }
+ ];
+ export default function Component({ loaderData }) {
+ return ;
+ }
+ `,
+ "app/routes/throw-data-after.a.b.c.tsx": js`
+ import { data } from "react-router";
+ export const middleware = [async (_, next) => {
+ let res = await next();
+ throw data('C ERROR', { status: 418, statusText: "I'm a teapot" })
+ }];
+ // Force middleware to run on client side navs
+ export function loader() {
+ return null;
+ }
+ export default function Component({ loaderData }) {
+ return C
+ }
+ `,
+ "app/routes/granular._index.tsx": js`
+ import { Link } from 'react-router'
+ export default function Component({ loaderData }) {
+ return Link;
+ }
+ `,
+ "app/routes/granular.a.tsx": js`
+ import { Outlet } from 'react-router'
+ import { orderContext } from '../context';
+ export const middleware = [
+ ({ context }) => { context.set(orderContext, ['a']); },
+ ];
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- await page.waitForSelector('a:has-text("Link")');
+ export async function loader({ context }) {
+ return context.get(orderContext).join(',');
+ }
- (await page.getByRole("link"))?.click();
- await page.waitForSelector('h1:has-text("Target")');
+ // Force a granular call for this route
+ export function clientLoader({ serverLoader }) {
+ return serverLoader()
+ }
- appFixture.close();
- });
+ export default function Component({ loaderData }) {
+ return (
+ <>
+ A: {loaderData}
+
+ >
+ );
+ }
+ `,
+ "app/routes/granular.a.b.tsx": js`
+ import { Outlet } from 'react-router'
+ import { orderContext } from '../context';
+ export const middleware = [
+ ({ context }) => {
+ context.set(orderContext, [...context.get(orderContext), 'b']);
+ },
+ ];
- test("handles errors thrown on the way down", async ({ page }) => {
- let fixture = await createFixture(
- {
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
+ export async function loader({ context }) {
+ return context.get(orderContext).join(',');
+ }
- export default defineConfig({
- build: { manifest: true },
- plugins: [reactRouter()],
- });
- `,
- "app/routes/_index.tsx": js`
- import { Link } from 'react-router'
-
- export default function Component() {
- return Link;
- }
- `,
- "app/routes/broken.tsx": js`
- export const clientMiddleware = [
- async ({ request, context }, next) => {
- throw new Error('broken!')
- }
- ]
- export default function Component() {
- return Should not see me
- }
- export function ErrorBoundary({ error }) {
- return {error.message}
- }
- `,
- },
- },
- UNSAFE_ServerMode.Development,
- );
-
- let appFixture = await createAppFixture(
- fixture,
- UNSAFE_ServerMode.Development,
- );
-
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- await page.waitForSelector('a:has-text("Link")');
-
- (await page.getByRole("link"))?.click();
- await page.waitForSelector("[data-error]");
- expect(await page.innerText("[data-error]")).toBe("broken!");
-
- appFixture.close();
- });
-
- test("handles errors thrown on the way up", async ({ page }) => {
- let fixture = await createFixture(
- {
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true },
- plugins: [reactRouter()],
- });
- `,
- "app/routes/_index.tsx": js`
- import { Link } from 'react-router'
-
- export default function Component() {
- return Link;
- }
- `,
- "app/routes/broken.tsx": js`
- import { useRouteError } from 'react-router'
- export const clientMiddleware = [
- async ({ request, context }, next) => {
- await next();
- throw new Error('broken!')
- }
- ]
- export function clientLoader() {
- return "nope"
- }
- export default function Component() {
- return Should not see me
- }
- export function ErrorBoundary({ loaderData, error }) {
- return (
- <>
- {error.message}
- {loaderData ?? 'empty'}
- >
- );
- }
- `,
- },
- },
- UNSAFE_ServerMode.Development,
- );
-
- let appFixture = await createAppFixture(
- fixture,
- UNSAFE_ServerMode.Development,
- );
-
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- await page.waitForSelector('a:has-text("Link")');
-
- (await page.getByRole("link"))?.click();
- await page.waitForSelector("[data-error]");
- expect(await page.innerText("[data-error]")).toBe("broken!");
- expect(await page.innerText("pre")).toBe("empty");
-
- appFixture.close();
- });
-
- test("calls clientMiddleware for routes even without a loader", async ({
- page,
- }) => {
- let fixture = await createFixture({
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
+ // Force a granular call for this route
+ export function clientLoader({ serverLoader }) {
+ return serverLoader()
+ }
- export default defineConfig({
- build: { manifest: true, minify: false },
- plugins: [reactRouter()],
- });
- `,
- "app/context.ts": js`
- import { createContext } from 'react-router'
- export const orderContext = createContext([]);
+ export default function Component({ loaderData }) {
+ return B: {loaderData}
;
+ }
`,
- "app/routes/_index.tsx": js`
+ "app/routes/without-loader-document._index.tsx": js`
import { Link } from 'react-router'
export default function Component({ loaderData }) {
- return Link;
+ return Link;
}
`,
- "app/routes/a.tsx": js`
+ "app/routes/without-loader-document.a.tsx": js`
import { Outlet } from 'react-router'
import { orderContext } from '../context';
- export const clientMiddleware = [
+ export const middleware = [
({ context }) => { context.set(orderContext, ['a']); }
];
- export function clientLoader({ context }) {
+ export function loader({ context }) {
return context.get(orderContext).join(',');
}
@@ -1266,10 +1500,10 @@ test.describe("Middleware", () => {
return <>A: {loaderData}
>;
}
`,
- "app/routes/a.b.tsx": js`
+ "app/routes/without-loader-document.a.b.tsx": js`
import { Outlet } from 'react-router'
import { orderContext } from '../context';
- export const clientMiddleware = [
+ export const middleware = [
({ context }) => {
context.set(orderContext, [...context.get(orderContext), 'b']);
},
@@ -1279,9 +1513,9 @@ test.describe("Middleware", () => {
return <>B
>;
}
`,
- "app/routes/a.b.c.tsx": js`
+ "app/routes/without-loader-document.a.b.c.tsx": js`
import { orderContext } from '../context';
- export function clientLoader({ context }) {
+ export function loader({ context }) {
return context.get(orderContext).join(',');
}
@@ -1289,741 +1523,364 @@ test.describe("Middleware", () => {
return C: {loaderData}
;
}
`,
- },
- });
-
- let appFixture = await createAppFixture(fixture);
-
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- (await page.$('a[href="/a/b/c"]'))?.click();
- await page.waitForSelector("h4");
- expect(await page.innerText("h2")).toBe("A: a,b");
- expect(await page.innerText("h3")).toBe("B");
- expect(await page.innerText("h4")).toBe("C: a,b");
-
- appFixture.close();
- });
-
- test("calls clientMiddleware once when multiple server requests happen", async ({
- page,
- }) => {
- let fixture = await createFixture({
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true, minify: false },
- plugins: [reactRouter()],
- });
- `,
- "app/context.ts": js`
- import { createContext } from 'react-router'
- export const countContext = createContext({
- parent: 0,
- child: 0,
- });
- `,
- "app/routes/_index.tsx": js`
+ "app/routes/without-loader-data._index.tsx": js`
import { Link } from 'react-router'
export default function Component({ loaderData }) {
- return Go to /parent/child;
+ return Link;
}
`,
- "app/routes/parent.tsx": js`
- import { countContext } from '../context';
- import { Outlet } from 'react-router';
- export function loader() {
- return 'PARENT'
- }
- export const clientMiddleware = [
- ({ context }) => { context.get(countContext).parent++ },
+ "app/routes/without-loader-data.a.tsx": js`
+ import { Outlet } from 'react-router'
+ import { orderContext } from '../context';
+ export const middleware = [
+ ({ context }) => { context.set(orderContext, ['a']); }
];
- export async function clientLoader({ serverLoader, context }) {
- return {
- serverData: await serverLoader(),
- context: context.get(countContext)
- }
+ export function loader({ context }) {
+ return context.get(orderContext).join(',');
}
export default function Component({ loaderData }) {
- return (
- <>
- {JSON.stringify(loaderData)}
-
- >
- );
+ return <>A: {loaderData}
>;
}
`,
- "app/routes/parent.child.tsx": js`
- import { countContext } from '../context';
- export function loader() {
- return 'CHILD'
- }
- export const clientMiddleware = [
- ({ context }) => { context.get(countContext).child++ },
+ "app/routes/without-loader-data.a.b.tsx": js`
+ import { Outlet } from 'react-router'
+ import { orderContext } from '../context';
+ export const middleware = [
+ ({ context }) => {
+ context.set(orderContext, [...context.get(orderContext), 'b']);
+ },
];
- export async function clientLoader({ serverLoader, context }) {
- return {
- serverData: await serverLoader(),
- context: context.get(countContext)
- }
+ export default function Component() {
+ return <>B
>;
+ }
+ `,
+ "app/routes/without-loader-data.a.b.c.tsx": js`
+ import { orderContext } from '../context';
+ export function loader({ context }) {
+ return context.get(orderContext).join(',');
}
export default function Component({ loaderData }) {
- return {JSON.stringify(loaderData)}
;
+ return C: {loaderData}
;
}
`,
- },
- });
-
- let appFixture = await createAppFixture(fixture);
- let requests: string[] = [];
- page.on("request", (request: PlaywrightRequest) => {
- if (request.url().includes(".data")) {
- requests.push(request.url());
- }
- });
+ "app/routes/resource._index.tsx": js`
+ import * as React from 'react'
+ import { useFetcher } from 'react-router'
+ export default function Component({ loaderData }) {
+ let fetcher = useFetcher();
+ let [data, setData] = React.useState();
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- (await page.$('a[href="/parent/child"]'))?.click();
- await page.waitForSelector("[data-child]");
+ async function rawFetch() {
+ let res = await fetch('/resource/a/b?raw');
+ let text = await res.text();
+ setData(text);
+ }
- // 2 separate server requests made
- expect(requests.sort()).toEqual([
- expect.stringContaining("/parent/child.data?_routes=routes%2Fparent"),
- expect.stringContaining(
- "/parent/child.data?_routes=routes%2Fparent.child",
- ),
- ]);
-
- // But client middlewares only ran once
- let json = (await page.locator("[data-parent]").textContent()) as string;
- expect(JSON.parse(json)).toEqual({
- serverData: "PARENT",
- context: {
- parent: 1,
- child: 1,
- },
- });
- json = (await page.locator("[data-child]").textContent()) as string;
- expect(JSON.parse(json)).toEqual({
- serverData: "CHILD",
- context: {
- parent: 1,
- child: 1,
- },
- });
-
- appFixture.close();
- });
-
- test("calls clientMiddleware once when multiple server requests happen and some routes opt out", async ({
- page,
- }) => {
- let fixture = await createFixture({
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true, minify: false },
- plugins: [reactRouter()],
- });
- `,
- "app/context.ts": js`
- import { createContext } from 'react-router'
- export const countContext = createContext({
- parent: 0,
- child: 0,
- index: 0,
- });
- `,
- "app/routes/_index.tsx": js`
- import { Link } from 'react-router'
- export default function Component({ loaderData }) {
- return Go to /parent/child;
- }
- `,
- "app/routes/parent.tsx": js`
- import { Outlet } from 'react-router';
- import { countContext } from '../context';
- export function loader() {
- return 'PARENT'
- }
- export const clientMiddleware = [
- ({ context }) => { context.get(countContext).parent++ },
- ];
- export default function Component({ loaderData }) {
return (
<>
- {loaderData}
-
+
+ {fetcher.data ? {fetcher.data} : null}
+
+
+
+
+ {data ? {data} : null}
>
);
}
- export function shouldRevalidate() {
- return false;
- }
`,
- "app/routes/parent.child.tsx": js`
- import { Outlet } from 'react-router';
- import { countContext } from '../context';
- export function loader() {
- return 'CHILD'
- }
- export const clientMiddleware = [
- ({ context }) => { context.get(countContext).child++ },
+ "app/routes/resource.a.tsx": js`
+ import { orderContext } from '../context';
+ export const middleware = [
+ async ({ context }, next) => {
+ context.set(orderContext, ['a']);
+ let res = await next();
+ res.headers.set('x-a', 'true');
+ return res;
+ },
];
- export default function Component({ loaderData }) {
- return (
- <>
- {loaderData}
-
- >
- );
- }
`,
- "app/routes/parent.child._index.tsx": js`
- import { Form } from 'react-router';
- import { countContext } from '../context';
- export function action() {
- return 'INDEX ACTION'
- }
- export function loader() {
- return 'INDEX'
- }
- export const clientMiddleware = [
- ({ context }) => { context.get(countContext).index++ },
+ "app/routes/resource.a.b.tsx": js`
+ import { orderContext } from '../context';
+ export const middleware = [
+ async ({ context }, next) => {
+ context.set(orderContext, [...context.get(orderContext), 'b']);
+ let res = await next();
+ res.headers.set('x-b', 'true');
+ return res;
+ },
];
- export async function clientLoader({ serverLoader, context }) {
- return {
- serverData: await serverLoader(),
- context: context.get(countContext)
- }
- }
- export default function Component({ loaderData, actionData }) {
- return (
- <>
- {JSON.stringify(loaderData)}
-
- {actionData ? {JSON.stringify(actionData)}
: null}
- >
- );
+
+ export async function loader({ request, context }) {
+ let data = context.get(orderContext).join(',');
+ let isRaw = new URL(request.url).searchParams.has('raw');
+ return isRaw ? new Response(data) : data;
}
`,
},
});
+ appFixture = await createAppFixture(fixture);
+ });
- let appFixture = await createAppFixture(fixture);
-
- let requests: string[] = [];
- page.on("request", (request: PlaywrightRequest) => {
- if (request.method() === "GET" && request.url().includes(".data")) {
- requests.push(request.url());
- }
- });
+ test.afterAll(() => {
+ appFixture.close();
+ });
+ test("calls middleware before/after loaders", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- (await page.$('a[href="/parent/child"]'))?.click();
- await page.waitForSelector("[data-child]");
- expect(await page.locator("[data-parent]").textContent()).toBe("PARENT");
- expect(await page.locator("[data-child]").textContent()).toBe("CHILD");
- expect(
- JSON.parse((await page.locator("[data-index]").textContent())!),
- ).toEqual({
- serverData: "INDEX",
- context: {
- parent: 1,
- child: 1,
- index: 1,
- },
- });
+ await app.goto("/loaders");
+ await page.waitForSelector('[data-route]:has-text("Index")');
+ expect(await page.locator("[data-route]").textContent()).toBe(
+ "Index: a,b",
+ );
- requests = []; // clear before form submission
- (await page.$('button[type="submit"]'))?.click();
- await page.waitForSelector("[data-action]");
+ (await page.$('a[href="/loaders/about"]'))?.click();
+ await page.waitForSelector('[data-route]:has-text("About")');
+ expect(await page.locator("[data-route]").textContent()).toBe(
+ "About: c,d",
+ );
+ });
- // 2 separate server requests made
- expect(requests.sort()).toEqual([
- // This is the normal request but only included parent.child because parent opted out
- expect.stringMatching(
- /\/parent\/child\.data\?_routes=routes%2Fparent\.child$/,
- ),
- // index gets it's own due to clientLoader
- expect.stringMatching(
- /\/parent\/child\.data\?_routes=routes%2Fparent\.child\._index$/,
- ),
+ test("calls middleware when no loaders exist on document, but not data requests", async ({
+ page,
+ }) => {
+ let oldConsoleLog = console.log;
+ let logs: any[] = [];
+ console.log = (...args) => logs.push(args);
+
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/no-loaders/parent/a");
+ await page.waitForSelector('h2:has-text("Parent")');
+ await page.waitForSelector('h3:has-text("A")');
+ expect(logs).toEqual([
+ ["Running parent middleware", "/no-loaders/parent/a"],
+ ["Running A middleware", "/no-loaders/parent/a"],
]);
- // But client middlewares only ran once for the action and once for the revalidation
- expect(await page.locator("[data-parent]").textContent()).toBe("PARENT");
- expect(await page.locator("[data-child]").textContent()).toBe("CHILD");
- expect(
- JSON.parse((await page.locator("[data-index]").textContent())!),
- ).toEqual({
- serverData: "INDEX",
- context: {
- parent: 3,
- child: 3,
- index: 3,
- },
- });
+ (await page.$('a[href="/no-loaders/parent/b"]'))?.click();
+ await page.waitForSelector('h3:has-text("B")');
+ expect(logs).toEqual([
+ ["Running parent middleware", "/no-loaders/parent/a"],
+ ["Running A middleware", "/no-loaders/parent/a"],
+ ]);
- appFixture.close();
+ console.log = oldConsoleLog;
});
- });
- test.describe("Server Middleware", () => {
- test("calls middleware before/after loaders", async ({ page }) => {
- let fixture = await createFixture({
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
+ test("calls middleware before/after actions", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/actions");
+ await page.waitForSelector('[data-route]:has-text("Index")');
+ expect(await page.locator("[data-route]").textContent()).toBe(
+ "Index: a,b - empty",
+ );
- export default defineConfig({
- build: { manifest: true, minify: false },
- plugins: [reactRouter()],
- });
- `,
- "app/context.ts": js`
- import { createContext } from 'react-router'
- export const orderContext = createContext([]);
- `,
- "app/routes/_index.tsx": js`
- import { Link } from 'react-router'
- import { orderContext } from "../context";;
+ (await page.getByRole("button"))?.click();
+ await new Promise((r) => setTimeout(r, 1000));
+ await page.waitForSelector('[data-route]:has-text("- a,b")');
+ expect(await page.locator("[data-route]").textContent()).toBe(
+ "Index: a,b - a,b",
+ );
+ });
- export const middleware = [
- ({ context }) => {
- context.set(orderContext, [...context.get(orderContext), 'a']);
- },
- ({ context }) => {
- context.set(orderContext, [...context.get(orderContext), 'b']);
- },
- ];
+ test("handles redirects thrown on the way down", async ({ page }) => {
+ let res = await fixture.requestDocument("/redirect-down/redirect");
+ expect(res.status).toBe(302);
+ expect(res.headers.get("location")).toBe("/redirect-down/target");
+ expect(res.body).toBeNull();
- export async function loader({ request, context }) {
- return context.get(orderContext).join(',');
- }
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/redirect-down", true);
+ await page.waitForSelector('a:has-text("Link")');
- export default function Component({ loaderData }) {
- return (
- <>
- Index: {loaderData}
- Go to about
- >
- );
- }
- `,
- "app/routes/about.tsx": js`
- import { orderContext } from "../context";;
- export const middleware = [
- ({ context }) => {
- context.set(orderContext, ['c']);
- },
- ({ context }) => {
- context.set(orderContext, [...context.get(orderContext), 'd']);
- }
- ];
+ (await page.$('a[href="/redirect-down/redirect"]'))?.click();
+ await page.waitForSelector('h1:has-text("Target")');
+ });
- export async function loader({ context }) {
- return context.get(orderContext).join(',');
- }
+ test("handles redirects thrown on the way up", async ({ page }) => {
+ let res = await fixture.requestDocument("/redirect-up/redirect");
+ expect(res.status).toBe(302);
+ expect(res.headers.get("location")).toBe("/redirect-up/target");
+ expect(res.body).toBeNull();
- export default function Component({ loaderData }) {
- return About: {loaderData}
;
- }
- `,
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/redirect-up", true);
+ await page.waitForSelector('a:has-text("Link")');
+
+ (await page.$('a[href="/redirect-up/redirect"]'))?.click();
+ await page.waitForSelector('h1:has-text("Target")');
+ });
+
+ test("doesn't serialize single fetch redirects until after the middleware chain", async () => {
+ let res = await fixture.requestSingleFetchData(
+ "/single-fetch-serialize/redirect.data",
+ );
+ expect(res.status).toBe(202);
+ expect(res.headers.get("location")).toBe(null);
+ expect(res.headers.get("x-status")).toBe("302");
+ expect(res.headers.get("x-location")).toBe(
+ "/single-fetch-serialize/target",
+ );
+ expect(res.data).toEqual({
+ [UNSAFE_SingleFetchRedirectSymbol]: {
+ redirect: "/single-fetch-serialize/target",
+ reload: false,
+ replace: false,
+ revalidate: false,
+ status: 302,
},
});
+ });
- let appFixture = await createAppFixture(fixture);
+ test("bubbles response up the chain when middleware throws data() before next", async ({
+ page,
+ }) => {
+ let res = await fixture.requestDocument("/throw-data-before/a/b/c");
+ expect(res.status).toBe(418);
+ expect(res.headers.get("x-a")).toBe("true");
+ expect(res.headers.get("x-b")).toBe("true");
+ let html = await res.text();
+ expect(html).toContain("A Error Boundary");
+ expect(html).toContain("C ERROR");
+
+ let data = await fixture.requestSingleFetchData(
+ "/throw-data-before/a/b/c.data",
+ );
+ expect(data.status).toBe(418);
+ expect(data.headers.get("x-a")).toBe("true");
+ expect(data.headers.get("x-b")).toBe("true");
+ expect((data.data as any)["routes/throw-data-before.a.b.c"]).toEqual({
+ error: new UNSAFE_ErrorResponseImpl(418, "I'm a teapot", "C ERROR"),
+ });
let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- await page.waitForSelector('[data-route]:has-text("Index")');
- expect(await page.locator("[data-route]").textContent()).toBe(
- "Index: a,b",
+ await app.goto("/throw-data-before");
+ await app.clickLink("/throw-data-before/a/b/c");
+ await page.waitForSelector("[data-error]");
+ expect(await page.locator("[data-error]").textContent()).toBe(
+ "A Error Boundary",
);
+ expect(await page.locator("pre").textContent()).toBe("C ERROR");
+ });
- (await page.$('a[href="/about"]'))?.click();
- await page.waitForSelector('[data-route]:has-text("About")');
- expect(await page.locator("[data-route]").textContent()).toBe(
- "About: c,d",
+ test("bubbles response up the chain when middleware throws data() after next", async ({
+ page,
+ }) => {
+ let res = await fixture.requestDocument("/throw-data-after/a/b/c");
+ expect(res.status).toBe(418);
+ expect(res.headers.get("x-a")).toBe("true");
+ expect(res.headers.get("x-b")).toBe("true");
+ let html = await res.text();
+ expect(html).toContain("A Error Boundary");
+ expect(html).toContain("C ERROR");
+ expect(html).toContain("A LOADER");
+
+ let data = await fixture.requestSingleFetchData(
+ "/throw-data-after/a/b/c.data",
);
+ expect(data.status).toBe(418);
+ expect(data.headers.get("x-a")).toBe("true");
+ expect(data.headers.get("x-b")).toBe("true");
+ expect((data.data as any)["routes/throw-data-after.a"]).toEqual({
+ data: "A LOADER",
+ });
+ expect((data.data as any)["routes/throw-data-after.a.b.c"]).toEqual({
+ error: new UNSAFE_ErrorResponseImpl(418, "I'm a teapot", "C ERROR"),
+ });
- appFixture.close();
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/throw-data-after");
+ await app.clickLink("/throw-data-after/a/b/c");
+ await page.waitForSelector("[data-error]");
+ expect(await page.locator("[data-error]").textContent()).toBe(
+ "A Error Boundary",
+ );
+ expect(await page.locator("pre").textContent()).toBe("C ERROR");
+ expect(await page.locator("p").textContent()).toBe("A LOADER");
});
- test("calls middleware when no loaders exist on document, but not data requests", async ({
+ test("still calls middleware for all matches on granular data requests", async ({
page,
}) => {
- let oldConsoleLog = console.log;
- let logs: any[] = [];
- console.log = (...args) => logs.push(args);
-
- let fixture = await createFixture({
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
+ let appFixture = await createAppFixture(fixture);
- export default defineConfig({
- build: { manifest: true, minify: false },
- plugins: [reactRouter()],
- });
- `,
- "app/routes/parent.tsx": js`
- import { Link, Outlet } from 'react-router'
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/granular");
+ await page.waitForSelector('a[href="/granular/a/b"]');
- export const middleware = [
- ({ request }) => {
- console.log('Running parent middleware', new URL(request.url).pathname)
- },
- ];
+ (await page.$('a[href="/granular/a/b"]'))?.click();
+ await page.waitForSelector("[data-b]");
+ expect(await page.locator("[data-a]").textContent()).toBe("A: a,b");
+ expect(await page.locator("[data-b]").textContent()).toBe("B: a,b");
+ });
- export default function Component() {
- return (
- <>
- Parent
- Go to A
- Go to B
-
- >
- );
- }
- `,
- "app/routes/parent.a.tsx": js`
- export const middleware = [
- ({ request }) => {
- console.log('Running A middleware', new URL(request.url).pathname)
- },
- ];
+ test("calls middleware for routes even without a loader (document)", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/without-loader-document/a/b/c");
+ expect(await page.innerText("h2")).toBe("A: a,b");
+ expect(await page.innerText("h3")).toBe("B");
+ expect(await page.innerText("h4")).toBe("C: a,b");
+ });
- export default function Component() {
- return A
;
- }
- `,
- "app/routes/parent.b.tsx": js`
- export const middleware = [
- ({ request }) => {
- console.log('Running B middleware', new URL(request.url).pathname)
- },
- ];
+ test("calls middleware for routes even without a loader (data)", async ({
+ page,
+ }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/without-loader-data");
+ (await page.$('a[href="/without-loader-data/a/b/c"]'))?.click();
+ await page.waitForSelector("h4");
+ expect(await page.innerText("h2")).toBe("A: a,b");
+ expect(await page.innerText("h3")).toBe("B");
+ expect(await page.innerText("h4")).toBe("C: a,b");
+ });
- export default function Component() {
- return B
;
- }
- `,
- },
+ test("calls middleware on resource routes", async ({ page }) => {
+ let fetcherHeaders: ReturnType | undefined;
+ let fetchHeaders: ReturnType | undefined;
+ page.on("request", async (r: PlaywrightRequest) => {
+ if (r.url().includes("/resource/a/b.data")) {
+ let res = await r.response();
+ fetcherHeaders = res?.headers();
+ } else if (r.url().endsWith("/resource/a/b?raw")) {
+ let res = await r.response();
+ fetchHeaders = res?.headers();
+ }
});
- let appFixture = await createAppFixture(fixture);
-
let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/parent/a");
- await page.waitForSelector('h2:has-text("Parent")');
- await page.waitForSelector('h3:has-text("A")');
- expect(logs).toEqual([
- ["Running parent middleware", "/parent/a"],
- ["Running A middleware", "/parent/a"],
- ]);
+ await app.goto("/resource");
- (await page.$('a[href="/parent/b"]'))?.click();
- await page.waitForSelector('h3:has-text("B")');
- expect(logs).toEqual([
- ["Running parent middleware", "/parent/a"],
- ["Running A middleware", "/parent/a"],
- ]);
+ (await page.$("#fetcher"))?.click();
+ await page.waitForSelector("[data-fetcher]");
+ expect(await page.locator("[data-fetcher]").textContent()).toBe("a,b");
+ expect(fetcherHeaders!["x-a"]).toBe("true");
+ expect(fetcherHeaders!["x-b"]).toBe("true");
- appFixture.close();
- console.log = oldConsoleLog;
+ (await page.$("#fetch"))?.click();
+ await page.waitForSelector("[data-fetch]");
+ expect(await page.locator("[data-fetch]").textContent()).toBe("a,b");
+ expect(fetchHeaders!["x-a"]).toBe("true");
+ expect(fetchHeaders!["x-b"]).toBe("true");
});
+ });
- test("calls middleware before/after actions", async ({ page }) => {
- let fixture = await createFixture({
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true },
- plugins: [reactRouter()],
- });
- `,
- "app/context.ts": js`
- import { createContext } from 'react-router'
- export const orderContext = createContext([]);
- `,
- "app/routes/_index.tsx": js`
- import { Form } from 'react-router'
- import { orderContext } from "../context";;
+ test.describe("Server Middleware (dev)", () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
- export const middleware = [
- ({ context }) => {
- context.set(orderContext, [...context.get(orderContext), 'a']);
- },
- ({ context }) => {
- context.set(orderContext, [...context.get(orderContext), 'b']);
- },
- ];
-
- export async function action({ request, context }) {
- return context.get(orderContext).join(',');
- }
-
- export async function loader({ request, context }) {
- return context.get(orderContext).join(',');
- }
-
- export default function Component({ loaderData, actionData }) {
- return (
- <>
- Index: {loaderData} - {actionData || 'empty'}
-
- >
- );
- }
- `,
- },
- });
-
- let appFixture = await createAppFixture(fixture);
-
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- await page.waitForSelector('[data-route]:has-text("Index")');
- expect(await page.locator("[data-route]").textContent()).toBe(
- "Index: a,b - empty",
- );
-
- (await page.getByRole("button"))?.click();
- await new Promise((r) => setTimeout(r, 1000));
- await page.waitForSelector('[data-route]:has-text("- a,b")');
- expect(await page.locator("[data-route]").textContent()).toBe(
- "Index: a,b - a,b",
- );
-
- appFixture.close();
- });
-
- test("handles redirects thrown on the way down", async ({ page }) => {
- let fixture = await createFixture({
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true },
- plugins: [reactRouter()],
- });
- `,
- "app/routes/_index.tsx": js`
- import { Link } from 'react-router'
-
- export default function Component({ loaderData, actionData }) {
- return Link;
- }
- `,
- "app/routes/redirect.tsx": js`
- import { Link, redirect } from 'react-router'
- export const middleware = [
- ({ request, context }) => { throw redirect('/target'); }
- ]
- export function loader() {
- return null;
- }
- export default function Component() {
- return Redirect
- }
- `,
- "app/routes/target.tsx": js`
- export default function Component() {
- return Target
- }
- `,
- },
- });
-
- let res = await fixture.requestDocument("/redirect");
- expect(res.status).toBe(302);
- expect(res.headers.get("location")).toBe("/target");
- expect(res.body).toBeNull();
-
- let appFixture = await createAppFixture(fixture);
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- await page.waitForSelector('a:has-text("Link")');
-
- (await page.getByRole("link"))?.click();
- await page.waitForSelector('h1:has-text("Target")');
-
- appFixture.close();
- });
-
- test("handles redirects thrown on the way up", async ({ page }) => {
- let fixture = await createFixture({
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true },
- plugins: [reactRouter()],
- });
- `,
- "app/routes/_index.tsx": js`
- import { Link } from 'react-router'
-
- export default function Component({ loaderData, actionData }) {
- return Link;
- }
- `,
- "app/routes/redirect.tsx": js`
- import { Link, redirect } from 'react-router'
- export const middleware = [
- async ({ request, context }, next) => {
- await next();
- throw redirect('/target');
- }
- ]
- export function loader() {
- return null;
- }
- export default function Component() {
- return Redirect
- }
- `,
- "app/routes/target.tsx": js`
- export default function Component() {
- return Target
- }
- `,
- },
- });
-
- let res = await fixture.requestDocument("/redirect");
- expect(res.status).toBe(302);
- expect(res.headers.get("location")).toBe("/target");
- expect(res.body).toBeNull();
-
- let appFixture = await createAppFixture(fixture);
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- await page.waitForSelector('a:has-text("Link")');
-
- (await page.getByRole("link"))?.click();
- await page.waitForSelector('h1:has-text("Target")');
-
- appFixture.close();
- });
-
- test("doesn't serialize single fetch redirects until after the middleware chain", async ({
- page,
- }) => {
- let fixture = await createFixture({
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true },
- plugins: [reactRouter()],
- });
- `,
- "app/routes/redirect.tsx": js`
- import { Link, redirect } from 'react-router'
- export const middleware = [
- async ({ request, context }, next) => {
- let res = await next();
- // Should still be a normal redirect here, not yet encoded into
- // a single fetch redirect
- res.headers.set("X-Status", res.status);
- res.headers.set("X-Location", res.headers.get('Location'));
- return res;
- }
- ]
- export function loader() {
- throw redirect('/target');
- }
- export default function Component() {
- return Redirect
- }
- `,
- "app/routes/target.tsx": js`
- export default function Component() {
- return Target
- }
- `,
- },
- });
-
- let res = await fixture.requestSingleFetchData("/redirect.data");
- expect(res.status).toBe(202);
- expect(res.headers.get("location")).toBe(null);
- expect(res.headers.get("x-status")).toBe("302");
- expect(res.headers.get("x-location")).toBe("/target");
- expect(res.data).toEqual({
- [UNSAFE_SingleFetchRedirectSymbol]: {
- redirect: "/target",
- reload: false,
- replace: false,
- revalidate: false,
- status: 302,
- },
- });
- });
-
- test("handles errors thrown on the way down (document)", async ({
- page,
- }) => {
- let errors: any[] = [];
- console.error = (...args) => errors.push(args);
-
- let fixture = await createFixture(
+ test.beforeAll(async () => {
+ fixture = await createFixture(
{
files: {
"react-router.config.ts": reactRouterConfig({
@@ -2038,7 +1895,71 @@ test.describe("Middleware", () => {
plugins: [reactRouter()],
});
`,
- "app/entry.server.tsx": ENTRY_SERVER_WITH_HANDLE_ERROR,
+ "app/entry.server.tsx": js`
+ import { PassThrough } from "node:stream";
+
+ import type { AppLoadContext, EntryContext } from "react-router";
+ import { createReadableStreamFromReadable } from "@react-router/node";
+ import { ServerRouter } from "react-router";
+ import type { RenderToPipeableStreamOptions } from "react-dom/server";
+ import { renderToPipeableStream } from "react-dom/server";
+
+ export const streamTimeout = 5_000;
+
+ export function handleError(error, { request }) {
+ if (!request.signal.aborted) {
+ let {pathname, search} = new URL(request.url);
+ console.error("handleError", request.method, pathname + search, error);
+ }
+ }
+
+ export default function handleRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ routerContext: EntryContext,
+ ) {
+ return new Promise((resolve, reject) => {
+ let shellRendered = false;
+
+ const { pipe, abort } = renderToPipeableStream(
+ ,
+ {
+ onShellReady() {
+ shellRendered = true;
+ const body = new PassThrough();
+ const stream = createReadableStreamFromReadable(body);
+
+ responseHeaders.set("Content-Type", "text/html");
+
+ resolve(
+ new Response(stream, {
+ headers: responseHeaders,
+ status: responseStatusCode,
+ }),
+ );
+
+ pipe(body);
+ },
+ onShellError(error: unknown) {
+ reject(error);
+ },
+ onError(error: unknown) {
+ responseStatusCode = 500;
+ // Log streaming rendering errors from inside the shell. Don't log
+ // errors encountered during initial shell rendering since they'll
+ // reject and get logged in handleDocumentRequest.
+ if (shellRendered) {
+ console.error(error);
+ }
+ },
+ },
+ );
+
+ setTimeout(abort, streamTimeout + 1000);
+ });
+ }
+ `,
"app/routes/_index.tsx": js`
import { Link } from 'react-router'
@@ -2059,53 +1980,34 @@ test.describe("Middleware", () => {
return {error.message}
}
`,
- },
- },
- UNSAFE_ServerMode.Development,
- );
-
- let appFixture = await createAppFixture(
- fixture,
- UNSAFE_ServerMode.Development,
- );
-
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/broken");
- expect(await page.innerText("[data-error]")).toBe("broken!");
- expect(errors).toEqual([
- ["handleError", "GET", "/broken", new Error("broken!")],
- ]);
-
- appFixture.close();
- });
-
- test("handles errors thrown on the way down (data)", async ({ page }) => {
- let errors: any[] = [];
- console.error = (...args) => errors.push(args);
- let fixture = await createFixture(
- {
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true },
- plugins: [reactRouter()],
- });
- `,
- "app/entry.server.tsx": ENTRY_SERVER_WITH_HANDLE_ERROR,
- "app/routes/_index.tsx": js`
+ "app/routes/error-down-document._index.tsx": js`
import { Link } from 'react-router'
export default function Component() {
- return Link;
+ return Link;
}
`,
- "app/routes/broken.tsx": js`
+ "app/routes/error-down-document.broken.tsx": js`
+ export const middleware = [
+ async ({ request, context }, next) => {
+ throw new Error('broken!');
+ }
+ ]
+ export default function Component() {
+ return Should not see me
+ }
+ export function ErrorBoundary({ error }) {
+ return {error.message}
+ }
+ `,
+ "app/routes/error-down-data._index.tsx": js`
+ import { Link } from 'react-router'
+
+ export default function Component() {
+ return Link;
+ }
+ `,
+ "app/routes/error-down-data.broken.tsx": js`
export const middleware = [
async ({ request, context }, next) => {
throw new Error('broken!');
@@ -2121,56 +2023,14 @@ test.describe("Middleware", () => {
return {error.message}
}
`,
- },
- },
- UNSAFE_ServerMode.Development,
- );
-
- let appFixture = await createAppFixture(
- fixture,
- UNSAFE_ServerMode.Development,
- );
-
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
-
- (await page.$('a[href="/broken"]'))?.click();
- await page.waitForSelector("[data-error]");
- expect(await page.innerText("[data-error]")).toBe("broken!");
- expect(errors).toEqual([
- ["handleError", "GET", "/broken.data", new Error("broken!")],
- ]);
-
- appFixture.close();
- });
-
- test("handles errors thrown on the way up (document)", async ({ page }) => {
- let errors: any[] = [];
- console.error = (...args) => errors.push(args);
- let fixture = await createFixture(
- {
- files: {
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true },
- plugins: [reactRouter()],
- });
- `,
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "app/entry.server.tsx": ENTRY_SERVER_WITH_HANDLE_ERROR,
- "app/routes/_index.tsx": js`
+ "app/routes/error-up-document._index.tsx": js`
import { Link } from 'react-router'
export default function Component() {
- return Link;
+ return Link;
}
`,
- "app/routes/broken.tsx": js`
+ "app/routes/error-up-document.broken.tsx": js`
export const middleware = [
async ({ request, context }, next) => {
await next();
@@ -2192,54 +2052,14 @@ test.describe("Middleware", () => {
);
}
`,
- },
- },
- UNSAFE_ServerMode.Development,
- );
-
- let appFixture = await createAppFixture(
- fixture,
- UNSAFE_ServerMode.Development,
- );
-
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/broken");
- expect(await page.innerText("[data-error]")).toBe("broken!");
- expect(await page.innerText("pre")).toBe("empty");
- expect(errors).toEqual([
- ["handleError", "GET", "/broken", new Error("broken!")],
- ]);
-
- appFixture.close();
- });
-
- test("handles errors thrown on the way up (data)", async ({ page }) => {
- let errors: any[] = [];
- console.error = (...args) => errors.push(args);
- let fixture = await createFixture(
- {
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true },
- plugins: [reactRouter()],
- });
- `,
- "app/entry.server.tsx": ENTRY_SERVER_WITH_HANDLE_ERROR,
- "app/routes/_index.tsx": js`
+ "app/routes/error-up-data._index.tsx": js`
import { Link } from 'react-router'
export default function Component() {
- return Link;
+ return Link;
}
`,
- "app/routes/broken.tsx": js`
+ "app/routes/error-up-data.broken.tsx": js`
export const middleware = [
async ({ request, context }, next) => {
await next()
@@ -2261,57 +2081,14 @@ test.describe("Middleware", () => {
);
}
`,
- },
- },
- UNSAFE_ServerMode.Development,
- );
-
- let appFixture = await createAppFixture(
- fixture,
- UNSAFE_ServerMode.Development,
- );
-
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
-
- (await page.$('a[href="/broken"]'))?.click();
- await page.waitForSelector("h1");
- await page.waitForSelector("[data-error]");
- expect(await page.innerText("[data-error]")).toBe("broken!");
- expect(await page.innerText("pre")).toBe("empty");
- expect(errors).toEqual([
- ["handleError", "GET", "/broken.data", new Error("broken!")],
- ]);
-
- appFixture.close();
- });
-
- test("bubbles errors up on document requests", async ({ page }) => {
- let errors: any[] = [];
- console.error = (...args) => errors.push(args);
- let fixture = await createFixture(
- {
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
- export default defineConfig({
- build: { manifest: true, minify: false },
- plugins: [reactRouter()],
- });
- `,
- "app/entry.server.tsx": ENTRY_SERVER_WITH_HANDLE_ERROR,
- "app/routes/_index.tsx": js`
+ "app/routes/bubble-document._index.tsx": js`
import { Link } from 'react-router'
export default function Component({ loaderData }) {
- return Link
+ return Link
}
`,
- "app/routes/a.tsx": js`
+ "app/routes/bubble-document.a.tsx": js`
import { Outlet } from 'react-router'
export const middleware = [
@@ -2334,7 +2111,7 @@ test.describe("Middleware", () => {
return <>A Error Boundary
{error.message}>
}
`,
- "app/routes/a.b.tsx": js`
+ "app/routes/bubble-document.a.b.tsx": js`
export const middleware = [
async ({ context }, next) => {
let res = await next();
@@ -2350,53 +2127,13 @@ test.describe("Middleware", () => {
return B
;
}
`,
- },
- },
- UNSAFE_ServerMode.Development,
- );
-
- let appFixture = await createAppFixture(
- fixture,
- UNSAFE_ServerMode.Development,
- );
-
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/a/b");
- expect(await page.locator("h1").textContent()).toBe("A Error Boundary");
- expect(await page.locator("pre").textContent()).toBe("broken!");
- expect(errors).toEqual([
- ["handleError", "GET", "/a/b", new Error("broken!")],
- ]);
-
- appFixture.close();
- });
-
- test("bubbles errors up on data requests", async ({ page }) => {
- let errors: any[] = [];
- console.error = (...args) => errors.push(args);
- let fixture = await createFixture(
- {
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true, minify: false },
- plugins: [reactRouter()],
- });
- `,
- "app/entry.server.tsx": ENTRY_SERVER_WITH_HANDLE_ERROR,
- "app/routes/_index.tsx": js`
+ "app/routes/bubble-data._index.tsx": js`
import { Link } from 'react-router'
export default function Component({ loaderData }) {
- return Link
+ return Link
}
`,
- "app/routes/a.tsx": js`
+ "app/routes/bubble-data.a.tsx": js`
import { Outlet } from 'react-router'
export const middleware = [
@@ -2419,7 +2156,7 @@ test.describe("Middleware", () => {
return <>A Error Boundary
{error.message}>
}
`,
- "app/routes/a.b.tsx": js`
+ "app/routes/bubble-data.a.b.tsx": js`
export const middleware = [
async ({ context }, next) => {
let res = await next();
@@ -2435,58 +2172,13 @@ test.describe("Middleware", () => {
return B
;
}
`,
- },
- },
- UNSAFE_ServerMode.Development,
- );
-
- let appFixture = await createAppFixture(
- fixture,
- UNSAFE_ServerMode.Development,
- );
-
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
-
- (await page.$('a[href="/a/b"]'))?.click();
- await page.waitForSelector("pre");
- expect(await page.locator("h1").textContent()).toBe("A Error Boundary");
- expect(await page.locator("pre").textContent()).toBe("broken!");
- expect(errors).toEqual([
- ["handleError", "GET", "/a/b.data", new Error("broken!")],
- ]);
-
- appFixture.close();
- });
-
- test("bubbles errors on the way down up to at least the highest route with a loader", async ({
- page,
- }) => {
- let errors: any[] = [];
- console.error = (...args) => errors.push(args);
- let fixture = await createFixture(
- {
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true, minify: false },
- plugins: [reactRouter()],
- });
- `,
- "app/entry.server.tsx": ENTRY_SERVER_WITH_HANDLE_ERROR,
- "app/routes/_index.tsx": js`
+ "app/routes/bubble-down-a._index.tsx": js`
import { Link } from 'react-router'
export default function Component({ loaderData }) {
- return Link
+ return Link
}
`,
- "app/routes/a.tsx": js`
+ "app/routes/bubble-down-a.a.tsx": js`
import { Outlet } from 'react-router'
export default function Component() {
return
@@ -2495,7 +2187,7 @@ test.describe("Middleware", () => {
return <>A Error Boundary
{error.message}>
}
`,
- "app/routes/a.b.tsx": js`
+ "app/routes/bubble-down-a.a.b.tsx": js`
import { Outlet } from 'react-router'
export function loader() {
return null;
@@ -2504,7 +2196,7 @@ test.describe("Middleware", () => {
return
}
`,
- "app/routes/a.b.c.tsx": js`
+ "app/routes/bubble-down-a.a.b.c.tsx": js`
import { Outlet } from 'react-router'
export default function Component() {
return
@@ -2513,77 +2205,26 @@ test.describe("Middleware", () => {
return <>C Error Boundary
{error.message}>
}
`,
- "app/routes/a.b.c.d.tsx": js`
+ "app/routes/bubble-down-a.a.b.c.d.tsx": js`
import { Outlet } from 'react-router'
export const middleware = [() => { throw new Error("broken!") }]
export default function Component() {
return
}
`,
- },
- },
- UNSAFE_ServerMode.Development,
- );
-
- let appFixture = await createAppFixture(
- fixture,
- UNSAFE_ServerMode.Development,
- );
-
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/a/b/c/d");
- expect(await page.locator("h1").textContent()).toBe("A Error Boundary");
- expect(await page.locator("pre").textContent()).toBe("broken!");
- expect(errors).toEqual([
- ["handleError", "GET", "/a/b/c/d", new Error("broken!")],
- ]);
- errors.splice(0);
-
- await app.goto("/");
- await app.clickLink("/a/b/c/d");
- expect(await page.locator("h1").textContent()).toBe("A Error Boundary");
- expect(await page.locator("pre").textContent()).toBe("broken!");
- expect(errors).toEqual([
- ["handleError", "GET", "/a/b/c/d.data", new Error("broken!")],
- ]);
-
- appFixture.close();
- });
-
- test("bubbles errors on the way down up to the deepest error boundary when loaders aren't revalidating", async ({
- page,
- }) => {
- let errors: any[] = [];
- console.error = (...args) => errors.push(args);
- let fixture = await createFixture(
- {
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true, minify: false },
- plugins: [reactRouter()],
- });
- `,
- "app/entry.server.tsx": ENTRY_SERVER_WITH_HANDLE_ERROR,
- "app/routes/_index.tsx": js`
+ "app/routes/bubble-down-b._index.tsx": js`
import { Link } from 'react-router'
export default function Component({ loaderData }) {
return (
<>
- /a/b
+ /a/b
- /a/b/c/d
+ /a/b/c/d
>
);
}
`,
- "app/routes/a.tsx": js`
+ "app/routes/bubble-down-b.a.tsx": js`
import { Outlet } from 'react-router'
export default function Component() {
return
@@ -2592,7 +2233,7 @@ test.describe("Middleware", () => {
return <>A Error Boundary
{error.message}>
}
`,
- "app/routes/a.b.tsx": js`
+ "app/routes/bubble-down-b.a.b.tsx": js`
import { Link, Outlet } from 'react-router'
export function loader() {
return { message: "DATA" };
@@ -2601,7 +2242,7 @@ test.describe("Middleware", () => {
return (
<>
AB: {loaderData.message}
- /a/b/c/d
+ /a/b/c/d
>
);
@@ -2610,7 +2251,7 @@ test.describe("Middleware", () => {
return false;
}
`,
- "app/routes/a.b.c.tsx": js`
+ "app/routes/bubble-down-b.a.b.c.tsx": js`
import { Outlet } from 'react-router'
export default function Component() {
return
@@ -2619,7 +2260,7 @@ test.describe("Middleware", () => {
return <>C Error Boundary
{error.message}>
}
`,
- "app/routes/a.b.c.d.tsx": js`
+ "app/routes/bubble-down-b.a.b.c.d.tsx": js`
import { Outlet } from 'react-router'
export const middleware = [() => { throw new Error("broken!") }]
export const loader = () => null;
@@ -2627,64 +2268,13 @@ test.describe("Middleware", () => {
return
}
`,
- },
- },
- UNSAFE_ServerMode.Development,
- );
-
- let appFixture = await createAppFixture(
- fixture,
- UNSAFE_ServerMode.Development,
- );
-
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- await app.clickLink("/a/b");
- await page.waitForSelector("[data-ab]");
- expect(await page.locator("[data-ab]").textContent()).toBe("AB: DATA");
- expect(errors).toEqual([]);
-
- await app.clickLink("/a/b/c/d");
- await page.waitForSelector("[data-error-c]");
- expect(await page.locator("h1").textContent()).toBe("C Error Boundary");
- expect(await page.locator("pre").textContent()).toBe("broken!");
- expect(errors).toEqual([
- [
- "handleError",
- "GET",
- "/a/b/c/d.data?_routes=routes%2Fa.b.c.d",
- new Error("broken!"),
- ],
- ]);
-
- appFixture.close();
- });
-
- test("bubbles response up the chain when middleware throws before next", async ({
- page,
- }) => {
- let fixture = await createFixture(
- {
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true, minify: false },
- plugins: [reactRouter()],
- });
- `,
- "app/routes/_index.tsx": js`
+ "app/routes/bubble-response-before._index.tsx": js`
import { Link } from 'react-router'
export default function Component({ loaderData }) {
- return /a/b/c;
+ return /a/b/c;
}
`,
- "app/routes/a.tsx": js`
+ "app/routes/bubble-response-before.a.tsx": js`
import { Outlet } from 'react-router'
export const middleware = [
async (_, next) => {
@@ -2700,7 +2290,7 @@ test.describe("Middleware", () => {
return <>A Error Boundary
{error.message}>
}
`,
- "app/routes/a.b.tsx": js`
+ "app/routes/bubble-response-before.a.b.tsx": js`
import { Link, Outlet } from 'react-router'
export const middleware = [
async (_, next) => {
@@ -2713,7 +2303,7 @@ test.describe("Middleware", () => {
return ;
}
`,
- "app/routes/a.b.c.tsx": js`
+ "app/routes/bubble-response-before.a.b.c.tsx": js`
export const middleware = [(_, next) => {
throw new Error('C ERROR')
}];
@@ -2725,69 +2315,13 @@ test.describe("Middleware", () => {
return C
}
`,
- },
- },
- UNSAFE_ServerMode.Development,
- );
-
- let appFixture = await createAppFixture(
- fixture,
- UNSAFE_ServerMode.Development,
- );
-
- let res = await fixture.requestDocument("/a/b/c");
- expect(res.status).toBe(500);
- expect(res.headers.get("x-a")).toBe("true");
- expect(res.headers.get("x-b")).toBe("true");
- let html = await res.text();
- expect(html).toContain("A Error Boundary");
- expect(html).toContain("C ERROR");
-
- let data = await fixture.requestSingleFetchData("/a/b/c.data");
- expect(data.status).toBe(500);
- expect(data.headers.get("x-a")).toBe("true");
- expect(data.headers.get("x-b")).toBe("true");
- expect((data.data as any)["routes/a.b.c"]).toEqual({
- error: new Error("C ERROR"),
- });
-
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- await app.clickLink("/a/b/c");
- await page.waitForSelector("[data-error]");
- expect(await page.locator("[data-error]").textContent()).toBe(
- "A Error Boundary",
- );
- expect(await page.locator("pre").textContent()).toBe("C ERROR");
-
- appFixture.close();
- });
-
- test("bubbles response up the chain when middleware throws after next", async ({
- page,
- }) => {
- let fixture = await createFixture(
- {
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true, minify: false },
- plugins: [reactRouter()],
- });
- `,
- "app/routes/_index.tsx": js`
+ "app/routes/bubble-response-after._index.tsx": js`
import { Link } from 'react-router'
export default function Component({ loaderData }) {
- return /a/b/c;
+ return /a/b/c;
}
`,
- "app/routes/a.tsx": js`
+ "app/routes/bubble-response-after.a.tsx": js`
import { Outlet } from 'react-router'
export const middleware = [
async (_, next) => {
@@ -2812,7 +2346,7 @@ test.describe("Middleware", () => {
);
}
`,
- "app/routes/a.b.tsx": js`
+ "app/routes/bubble-response-after.a.b.tsx": js`
import { Link, Outlet } from 'react-router'
export const middleware = [
async (_, next) => {
@@ -2825,7 +2359,7 @@ test.describe("Middleware", () => {
return ;
}
`,
- "app/routes/a.b.c.tsx": js`
+ "app/routes/bubble-response-after.a.b.c.tsx": js`
export const middleware = [async (_, next) => {
let res = await next();
throw new Error('C ERROR')
@@ -2838,77 +2372,13 @@ test.describe("Middleware", () => {
return C
}
`,
- },
- },
- UNSAFE_ServerMode.Development,
- );
-
- let appFixture = await createAppFixture(
- fixture,
- UNSAFE_ServerMode.Development,
- );
-
- let res = await fixture.requestDocument("/a/b/c");
- expect(res.status).toBe(500);
- expect(res.headers.get("x-a")).toBe("true");
- expect(res.headers.get("x-b")).toBe("true");
- let html = await res.text();
- expect(html).toContain("A Error Boundary");
- expect(html).toContain("C ERROR");
- expect(html).toContain("A LOADER");
-
- let data = await fixture.requestSingleFetchData("/a/b/c.data");
- expect(data.status).toBe(500);
- expect(data.headers.get("x-a")).toBe("true");
- expect(data.headers.get("x-b")).toBe("true");
- expect((data.data as any)["routes/a"]).toEqual({
- data: "A LOADER",
- });
- expect((data.data as any)["routes/a.b.c"]).toEqual({
- error: new Error("C ERROR"),
- });
-
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- await app.clickLink("/a/b/c");
- await page.waitForSelector("[data-error]");
- expect(await page.locator("[data-error]").textContent()).toBe(
- "A Error Boundary",
- );
- expect(await page.locator("pre").textContent()).toBe("C ERROR");
- expect(await page.locator("p").textContent()).toBe("A LOADER");
-
- appFixture.close();
- });
-
- test("bubbles response up the chain when multiple middlewares throw in sequence", async ({
- page,
- }) => {
- let errors: any[] = [];
- console.error = (...args) => errors.push(args);
- let fixture = await createFixture(
- {
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true, minify: false },
- plugins: [reactRouter()],
- });
- `,
- "app/entry.server.tsx": ENTRY_SERVER_WITH_HANDLE_ERROR,
- "app/routes/_index.tsx": js`
+ "app/routes/bubble-response-multiple._index.tsx": js`
import { Link } from 'react-router'
export default function Component({ loaderData }) {
- return /a/b/c;
+ return /a/b/c;
}
`,
- "app/routes/a.tsx": js`
+ "app/routes/bubble-response-multiple.a.tsx": js`
import { Outlet } from 'react-router'
export const middleware = [
async (_, next) => {
@@ -2933,7 +2403,7 @@ test.describe("Middleware", () => {
);
}
`,
- "app/routes/a.b.tsx": js`
+ "app/routes/bubble-response-multiple.a.b.tsx": js`
import { Link, Outlet } from 'react-router'
export const middleware = [async (_, next) => {
let res = await next();
@@ -2943,7 +2413,7 @@ test.describe("Middleware", () => {
return ;
}
`,
- "app/routes/a.b.c.tsx": js`
+ "app/routes/bubble-response-multiple.a.b.c.tsx": js`
export const middleware = [async (_, next) => {
let res = await next();
throw new Error('C ERROR')
@@ -2956,840 +2426,434 @@ test.describe("Middleware", () => {
return C
}
`,
+ "app/routes/error-down-resource.a.tsx": js`
+ export const middleware = [
+ async ({ context }, next) => {
+ throw new Error("broken!");
+ },
+ ];
+ `,
+ "app/routes/error-down-resource.a.b.tsx": js`
+ export async function loader({ request, context }) {
+ return new Response("ok");
+ }
+ `,
+ "app/routes/error-up-resource.a.tsx": js`
+ export const middleware = [
+ async ({ context }, next) => {
+ let res = await next()
+ throw new Error("broken!");
+ },
+ ];
+ `,
+ "app/routes/error-up-resource.a.b.tsx": js`
+ export async function loader({ request, context }) {
+ return new Response("ok");
+ }
+ `,
},
},
UNSAFE_ServerMode.Development,
);
-
- let appFixture = await createAppFixture(
+ appFixture = await createAppFixture(
fixture,
UNSAFE_ServerMode.Development,
);
+ });
- let res = await fixture.requestDocument("/a/b/c");
- expect(res.status).toBe(500);
- expect(res.headers.get("x-a")).toBe("true");
- expect(res.headers.get("x-b")).toBe(null);
- let html = await res.text();
- expect(html).toContain("A Error Boundary");
- expect(html).toContain("B ERROR");
- expect(html).toContain("A LOADER");
- expect(errors).toEqual([
- ["handleError", "GET", "/a/b/c", new Error("C ERROR")],
- ["handleError", "GET", "/a/b/c", new Error("B ERROR")],
- ]);
- errors.splice(0);
+ test.afterAll(() => {
+ appFixture.close();
+ });
- let data = await fixture.requestSingleFetchData("/a/b/c.data");
- expect(data.status).toBe(500);
- expect(data.headers.get("x-a")).toBe("true");
- expect(data.headers.get("x-b")).toBe(null);
- expect((data.data as any)["routes/a"]).toEqual({
- data: "A LOADER",
- });
- expect((data.data as any)["routes/a.b"]).toEqual({
- error: new Error("B ERROR"),
- });
+ test("handles errors thrown on the way down (document)", async ({
+ page,
+ }) => {
+ let errors: any[] = [];
+ console.error = (...args) => errors.push(args);
+
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/error-down-document/broken");
+ expect(await page.innerText("[data-error]")).toBe("broken!");
expect(errors).toEqual([
- ["handleError", "GET", "/a/b/c.data", new Error("C ERROR")],
- ["handleError", "GET", "/a/b/c.data", new Error("B ERROR")],
+ [
+ "handleError",
+ "GET",
+ "/error-down-document/broken",
+ new Error("broken!"),
+ ],
]);
- errors.splice(0);
+ });
+
+ test("handles errors thrown on the way down (data)", async ({ page }) => {
+ let errors: any[] = [];
+ console.error = (...args) => errors.push(args);
let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- await app.clickLink("/a/b/c");
+ await app.goto("/error-down-data");
+
+ (await page.$('a[href="/error-down-data/broken"]'))?.click();
await page.waitForSelector("[data-error]");
- expect(await page.locator("[data-error]").textContent()).toBe(
- "A Error Boundary",
- );
- expect(await page.locator("pre").textContent()).toBe("B ERROR");
- expect(await page.locator("p").textContent()).toBe("A LOADER");
+ expect(await page.innerText("[data-error]")).toBe("broken!");
expect(errors).toEqual([
- ["handleError", "GET", "/a/b/c.data", new Error("C ERROR")],
- ["handleError", "GET", "/a/b/c.data", new Error("B ERROR")],
+ [
+ "handleError",
+ "GET",
+ "/error-down-data/broken.data",
+ new Error("broken!"),
+ ],
]);
- errors.splice(0);
+ });
- appFixture.close();
+ test("handles errors thrown on the way up (document)", async ({ page }) => {
+ let errors: any[] = [];
+ console.error = (...args) => errors.push(args);
+
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/error-up-document/broken");
+ expect(await page.innerText("[data-error]")).toBe("broken!");
+ expect(await page.innerText("pre")).toBe("empty");
+ expect(errors).toEqual([
+ [
+ "handleError",
+ "GET",
+ "/error-up-document/broken",
+ new Error("broken!"),
+ ],
+ ]);
});
- test("bubbles response up the chain when middleware throws data() before next", async ({
- page,
- }) => {
- let fixture = await createFixture(
- {
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
+ test("handles errors thrown on the way up (data)", async ({ page }) => {
+ let errors: any[] = [];
+ console.error = (...args) => errors.push(args);
- export default defineConfig({
- build: { manifest: true, minify: false },
- plugins: [reactRouter()],
- });
- `,
- "app/routes/_index.tsx": js`
- import { Link } from 'react-router'
- export default function Component({ loaderData }) {
- return /a/b/c;
- }
- `,
- "app/routes/a.tsx": js`
- import { Outlet } from 'react-router'
- export const middleware = [
- async (_, next) => {
- let res = await next();
- res.headers.set('x-a', 'true');
- return res;
- }
- ];
- export default function Component() {
- return
- }
- export function ErrorBoundary({ error }) {
- return (
- <>
- A Error Boundary
- {error.data}
- >
- );
- }
- `,
- "app/routes/a.b.tsx": js`
- import { Link, Outlet } from 'react-router'
- export const middleware = [
- async (_, next) => {
- let res = await next();
- res.headers.set('x-b', 'true');
- return res;
- }
- ];
- export default function Component({ loaderData }) {
- return ;
- }
- `,
- "app/routes/a.b.c.tsx": js`
- import { data } from "react-router";
- export const middleware = [(_, next) => {
- throw data('C ERROR', { status: 418, statusText: "I'm a teapot" })
- }];
- // Force middleware to run on client side navs
- export function loader() {
- return null;
- }
- export default function Component({ loaderData }) {
- return C
- }
- `,
- },
- },
- UNSAFE_ServerMode.Development,
- );
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/error-up-data");
- let appFixture = await createAppFixture(
- fixture,
- UNSAFE_ServerMode.Development,
- );
+ (await page.$('a[href="/error-up-data/broken"]'))?.click();
+ await page.waitForSelector("h1");
+ await page.waitForSelector("[data-error]");
+ expect(await page.innerText("[data-error]")).toBe("broken!");
+ expect(await page.innerText("pre")).toBe("empty");
+ expect(errors).toEqual([
+ [
+ "handleError",
+ "GET",
+ "/error-up-data/broken.data",
+ new Error("broken!"),
+ ],
+ ]);
+ });
- let res = await fixture.requestDocument("/a/b/c");
- expect(res.status).toBe(418);
- expect(res.headers.get("x-a")).toBe("true");
- expect(res.headers.get("x-b")).toBe("true");
- let html = await res.text();
- expect(html).toContain("A Error Boundary");
- expect(html).toContain("C ERROR");
+ test("bubbles errors up on document requests", async ({ page }) => {
+ let errors: any[] = [];
+ console.error = (...args) => errors.push(args);
- let data = await fixture.requestSingleFetchData("/a/b/c.data");
- expect(data.status).toBe(418);
- expect(data.headers.get("x-a")).toBe("true");
- expect(data.headers.get("x-b")).toBe("true");
- expect((data.data as any)["routes/a.b.c"]).toEqual({
- error: new UNSAFE_ErrorResponseImpl(418, "I'm a teapot", "C ERROR"),
- });
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/bubble-document/a/b");
+ expect(await page.locator("h1").textContent()).toBe("A Error Boundary");
+ expect(await page.locator("pre").textContent()).toBe("broken!");
+ expect(errors).toEqual([
+ ["handleError", "GET", "/bubble-document/a/b", new Error("broken!")],
+ ]);
+ });
+
+ test("bubbles errors up on data requests", async ({ page }) => {
+ let errors: any[] = [];
+ console.error = (...args) => errors.push(args);
let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- await app.clickLink("/a/b/c");
- await page.waitForSelector("[data-error]");
- expect(await page.locator("[data-error]").textContent()).toBe(
- "A Error Boundary",
- );
- expect(await page.locator("pre").textContent()).toBe("C ERROR");
+ await app.goto("/bubble-data");
- appFixture.close();
+ (await page.$('a[href="/bubble-data/a/b"]'))?.click();
+ await page.waitForSelector("pre");
+ expect(await page.locator("h1").textContent()).toBe("A Error Boundary");
+ expect(await page.locator("pre").textContent()).toBe("broken!");
+ expect(errors).toEqual([
+ ["handleError", "GET", "/bubble-data/a/b.data", new Error("broken!")],
+ ]);
});
- test("bubbles response up the chain when middleware throws data() after next", async ({
+ test("bubbles errors on the way down up to at least the highest route with a loader", async ({
page,
}) => {
- let fixture = await createFixture(
- {
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true, minify: false },
- plugins: [reactRouter()],
- });
- `,
- "app/routes/_index.tsx": js`
- import { Link } from 'react-router'
- export default function Component({ loaderData }) {
- return /a/b/c;
- }
- `,
- "app/routes/a.tsx": js`
- import { Outlet } from 'react-router'
- export const middleware = [
- async (_, next) => {
- let res = await next();
- res.headers.set('x-a', 'true');
- return res;
- }
- ];
- export function loader() {
- return "A LOADER";
- }
- export default function Component() {
- return
- }
- export function ErrorBoundary({ error, loaderData }) {
- return (
- <>
- A Error Boundary
- {error.data}
- {loaderData}
- >
- );
- }
- `,
- "app/routes/a.b.tsx": js`
- import { Link, Outlet } from 'react-router'
- export const middleware = [
- async (_, next) => {
- let res = await next();
- res.headers.set('x-b', 'true');
- return res;
- }
- ];
- export default function Component({ loaderData }) {
- return ;
- }
- `,
- "app/routes/a.b.c.tsx": js`
- import { data } from "react-router";
- export const middleware = [async (_, next) => {
- let res = await next();
- throw data('C ERROR', { status: 418, statusText: "I'm a teapot" })
- }];
- // Force middleware to run on client side navs
- export function loader() {
- return null;
- }
- export default function Component({ loaderData }) {
- return C
- }
- `,
- },
- },
- UNSAFE_ServerMode.Development,
- );
+ let errors: any[] = [];
+ console.error = (...args) => errors.push(args);
- let appFixture = await createAppFixture(
- fixture,
- UNSAFE_ServerMode.Development,
- );
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/bubble-down-a/a/b/c/d");
+ expect(await page.locator("h1").textContent()).toBe("A Error Boundary");
+ expect(await page.locator("pre").textContent()).toBe("broken!");
+ expect(errors).toEqual([
+ ["handleError", "GET", "/bubble-down-a/a/b/c/d", new Error("broken!")],
+ ]);
+ errors.splice(0);
- let res = await fixture.requestDocument("/a/b/c");
- expect(res.status).toBe(418);
- expect(res.headers.get("x-a")).toBe("true");
- expect(res.headers.get("x-b")).toBe("true");
- let html = await res.text();
- expect(html).toContain("A Error Boundary");
- expect(html).toContain("C ERROR");
- expect(html).toContain("A LOADER");
+ await app.goto("/bubble-down-a");
+ await app.clickLink("/bubble-down-a/a/b/c/d");
+ expect(await page.locator("h1").textContent()).toBe("A Error Boundary");
+ expect(await page.locator("pre").textContent()).toBe("broken!");
+ expect(errors).toEqual([
+ [
+ "handleError",
+ "GET",
+ "/bubble-down-a/a/b/c/d.data",
+ new Error("broken!"),
+ ],
+ ]);
+ });
- let data = await fixture.requestSingleFetchData("/a/b/c.data");
- expect(data.status).toBe(418);
- expect(data.headers.get("x-a")).toBe("true");
- expect(data.headers.get("x-b")).toBe("true");
- expect((data.data as any)["routes/a"]).toEqual({
- data: "A LOADER",
- });
- expect((data.data as any)["routes/a.b.c"]).toEqual({
- error: new UNSAFE_ErrorResponseImpl(418, "I'm a teapot", "C ERROR"),
- });
+ test("bubbles errors on the way down up to the deepest error boundary when loaders aren't revalidating", async ({
+ page,
+ }) => {
+ let errors: any[] = [];
+ console.error = (...args) => errors.push(args);
let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- await app.clickLink("/a/b/c");
- await page.waitForSelector("[data-error]");
- expect(await page.locator("[data-error]").textContent()).toBe(
- "A Error Boundary",
- );
- expect(await page.locator("pre").textContent()).toBe("C ERROR");
- expect(await page.locator("p").textContent()).toBe("A LOADER");
+ await app.goto("/bubble-down-b");
+ await app.clickLink("/bubble-down-b/a/b");
+ await page.waitForSelector("[data-ab]");
+ expect(await page.locator("[data-ab]").textContent()).toBe("AB: DATA");
+ expect(errors).toEqual([]);
- appFixture.close();
+ await app.clickLink("/bubble-down-b/a/b/c/d");
+ await page.waitForSelector("[data-error-c]");
+ expect(await page.locator("h1").textContent()).toBe("C Error Boundary");
+ expect(await page.locator("pre").textContent()).toBe("broken!");
+ expect(errors).toEqual([
+ [
+ "handleError",
+ "GET",
+ "/bubble-down-b/a/b/c/d.data?_routes=routes%2Fbubble-down-b.a.b.c.d",
+ new Error("broken!"),
+ ],
+ ]);
});
- test("still calls middleware for all matches on granular data requests", async ({
+ test("bubbles response up the chain when middleware throws before next", async ({
page,
}) => {
- let fixture = await createFixture({
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true, minify: false },
- plugins: [reactRouter()],
- });
- `,
- "app/context.ts": js`
- import { createContext } from 'react-router'
- export const orderContext = createContext([]);
- `,
- "app/routes/_index.tsx": js`
- import { Link } from 'react-router'
- export default function Component({ loaderData }) {
- return Link;
- }
- `,
- "app/routes/a.tsx": js`
- import { Outlet } from 'react-router'
- import { orderContext } from '../context';
- export const middleware = [
- ({ context }) => { context.set(orderContext, ['a']); },
- ];
-
- export async function loader({ context }) {
- return context.get(orderContext).join(',');
- }
-
- // Force a granular call for this route
- export function clientLoader({ serverLoader }) {
- return serverLoader()
- }
-
- export default function Component({ loaderData }) {
- return (
- <>
- A: {loaderData}
-
- >
- );
- }
- `,
- "app/routes/a.b.tsx": js`
- import { Outlet } from 'react-router'
- import { orderContext } from '../context';
- export const middleware = [
- ({ context }) => {
- context.set(orderContext, [...context.get(orderContext), 'b']);
- },
- ];
-
- export async function loader({ context }) {
- return context.get(orderContext).join(',');
- }
-
- // Force a granular call for this route
- export function clientLoader({ serverLoader }) {
- return serverLoader()
- }
-
- export default function Component({ loaderData }) {
- return B: {loaderData}
;
- }
- `,
- },
- });
-
- let appFixture = await createAppFixture(fixture);
-
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- await page.waitForSelector('a[href="/a/b"]');
-
- (await page.$('a[href="/a/b"]'))?.click();
- await page.waitForSelector("[data-b]");
- expect(await page.locator("[data-a]").textContent()).toBe("A: a,b");
- expect(await page.locator("[data-b]").textContent()).toBe("B: a,b");
-
- appFixture.close();
- });
-
- test("calls middleware for routes even without a loader (document)", async ({
- page,
- }) => {
- let fixture = await createFixture({
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true, minify: false },
- plugins: [reactRouter()],
- });
- `,
- "app/context.ts": js`
- import { createContext } from 'react-router'
- export const orderContext = createContext([]);
- `,
- "app/routes/_index.tsx": js`
- import { Link } from 'react-router'
- export default function Component({ loaderData }) {
- return Link;
- }
- `,
- "app/routes/a.tsx": js`
- import { Outlet } from 'react-router'
- import { orderContext } from '../context';
- export const middleware = [
- ({ context }) => { context.set(orderContext, ['a']); }
- ];
-
- export function loader({ context }) {
- return context.get(orderContext).join(',');
- }
-
- export default function Component({ loaderData }) {
- return <>A: {loaderData}
>;
- }
- `,
- "app/routes/a.b.tsx": js`
- import { Outlet } from 'react-router'
- import { orderContext } from '../context';
- export const middleware = [
- ({ context }) => {
- context.set(orderContext, [...context.get(orderContext), 'b']);
- },
- ];
-
- export default function Component() {
- return <>B
>;
- }
- `,
- "app/routes/a.b.c.tsx": js`
- import { orderContext } from '../context';
- export function loader({ context }) {
- return context.get(orderContext).join(',');
- }
-
- export default function Component({ loaderData }) {
- return C: {loaderData}
;
- }
- `,
- },
- });
-
- let appFixture = await createAppFixture(fixture);
-
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/a/b/c");
- expect(await page.innerText("h2")).toBe("A: a,b");
- expect(await page.innerText("h3")).toBe("B");
- expect(await page.innerText("h4")).toBe("C: a,b");
-
- appFixture.close();
- });
-
- test("calls middleware for routes even without a loader (data)", async ({
- page,
- }) => {
- let fixture = await createFixture({
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true, minify: false },
- plugins: [reactRouter()],
- });
- `,
- "app/context.ts": js`
- import { createContext } from 'react-router'
- export const orderContext = createContext([]);
- `,
- "app/routes/_index.tsx": js`
- import { Link } from 'react-router'
- export default function Component({ loaderData }) {
- return Link;
- }
- `,
- "app/routes/a.tsx": js`
- import { Outlet } from 'react-router'
- import { orderContext } from '../context';
- export const middleware = [
- ({ context }) => { context.set(orderContext, ['a']); }
- ];
-
- export function loader({ context }) {
- return context.get(orderContext).join(',');
- }
-
- export default function Component({ loaderData }) {
- return <>A: {loaderData}
>;
- }
- `,
- "app/routes/a.b.tsx": js`
- import { Outlet } from 'react-router'
- import { orderContext } from '../context';
- export const middleware = [
- ({ context }) => {
- context.set(orderContext, [...context.get(orderContext), 'b']);
- },
- ];
-
- export default function Component() {
- return <>B
>;
- }
- `,
- "app/routes/a.b.c.tsx": js`
- import { orderContext } from '../context';
- export function loader({ context }) {
- return context.get(orderContext).join(',');
- }
-
- export default function Component({ loaderData }) {
- return C: {loaderData}
;
- }
- `,
- },
- });
-
- let appFixture = await createAppFixture(fixture);
-
- let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
- (await page.$('a[href="/a/b/c"]'))?.click();
- await page.waitForSelector("h4");
- expect(await page.innerText("h2")).toBe("A: a,b");
- expect(await page.innerText("h3")).toBe("B");
- expect(await page.innerText("h4")).toBe("C: a,b");
-
- appFixture.close();
- });
-
- test("calls middleware on resource routes", async ({ page }) => {
- let fixture = await createFixture({
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true, minify: false },
- plugins: [reactRouter()],
- });
- `,
- "app/context.ts": js`
- import { createContext } from 'react-router'
- export const orderContext = createContext([]);
- `,
- "app/routes/_index.tsx": js`
- import * as React from 'react'
- import { useFetcher } from 'react-router'
- export default function Component({ loaderData }) {
- let fetcher = useFetcher();
- let [data, setData] = React.useState();
-
- async function rawFetch() {
- let res = await fetch('/a/b?raw');
- let text = await res.text();
- setData(text);
- }
-
- return (
- <>
-
- {fetcher.data ? {fetcher.data} : null}
-
-
-
-
- {data ? {data} : null}
- >
- );
- }
- `,
- "app/routes/a.tsx": js`
- import { orderContext } from '../context';
- export const middleware = [
- async ({ context }, next) => {
- context.set(orderContext, ['a']);
- let res = await next();
- res.headers.set('x-a', 'true');
- return res;
- },
- ];
- `,
- "app/routes/a.b.tsx": js`
- import { orderContext } from '../context';
- export const middleware = [
- async ({ context }, next) => {
- context.set(orderContext, [...context.get(orderContext), 'b']);
- let res = await next();
- res.headers.set('x-b', 'true');
- return res;
- },
- ];
-
- export async function loader({ request, context }) {
- let data = context.get(orderContext).join(',');
- let isRaw = new URL(request.url).searchParams.has('raw');
- return isRaw ? new Response(data) : data;
- }
- `,
- },
- });
-
- let appFixture = await createAppFixture(fixture);
+ let res = await fixture.requestDocument("/bubble-response-before/a/b/c");
+ expect(res.status).toBe(500);
+ expect(res.headers.get("x-a")).toBe("true");
+ expect(res.headers.get("x-b")).toBe("true");
+ let html = await res.text();
+ expect(html).toContain("A Error Boundary");
+ expect(html).toContain("C ERROR");
- let fetcherHeaders: ReturnType | undefined;
- let fetchHeaders: ReturnType | undefined;
- page.on("request", async (r: PlaywrightRequest) => {
- if (r.url().includes("/a/b.data")) {
- let res = await r.response();
- fetcherHeaders = res?.headers();
- } else if (r.url().endsWith("/a/b?raw")) {
- let res = await r.response();
- fetchHeaders = res?.headers();
- }
- });
+ let data = await fixture.requestSingleFetchData(
+ "/bubble-response-before/a/b/c.data",
+ );
+ expect(data.status).toBe(500);
+ expect(data.headers.get("x-a")).toBe("true");
+ expect(data.headers.get("x-b")).toBe("true");
+ expect((data.data as any)["routes/bubble-response-before.a.b.c"]).toEqual(
+ {
+ error: new Error("C ERROR"),
+ },
+ );
let app = new PlaywrightFixture(appFixture, page);
- await app.goto("/");
+ await app.goto("/bubble-response-before");
+ await app.clickLink("/bubble-response-before/a/b/c");
+ await page.waitForSelector("[data-error]");
+ expect(await page.locator("[data-error]").textContent()).toBe(
+ "A Error Boundary",
+ );
+ expect(await page.locator("pre").textContent()).toBe("C ERROR");
+ });
- (await page.$("#fetcher"))?.click();
- await page.waitForSelector("[data-fetcher]");
- expect(await page.locator("[data-fetcher]").textContent()).toBe("a,b");
- expect(fetcherHeaders!["x-a"]).toBe("true");
- expect(fetcherHeaders!["x-b"]).toBe("true");
+ test("bubbles response up the chain when middleware throws after next", async ({
+ page,
+ }) => {
+ let res = await fixture.requestDocument("/bubble-response-after/a/b/c");
+ expect(res.status).toBe(500);
+ expect(res.headers.get("x-a")).toBe("true");
+ expect(res.headers.get("x-b")).toBe("true");
+ let html = await res.text();
+ expect(html).toContain("A Error Boundary");
+ expect(html).toContain("C ERROR");
+ expect(html).toContain("A LOADER");
- (await page.$("#fetch"))?.click();
- await page.waitForSelector("[data-fetch]");
- expect(await page.locator("[data-fetch]").textContent()).toBe("a,b");
- expect(fetchHeaders!["x-a"]).toBe("true");
- expect(fetchHeaders!["x-b"]).toBe("true");
+ let data = await fixture.requestSingleFetchData(
+ "/bubble-response-after/a/b/c.data",
+ );
+ expect(data.status).toBe(500);
+ expect(data.headers.get("x-a")).toBe("true");
+ expect(data.headers.get("x-b")).toBe("true");
+ expect((data.data as any)["routes/bubble-response-after.a"]).toEqual({
+ data: "A LOADER",
+ });
+ expect((data.data as any)["routes/bubble-response-after.a.b.c"]).toEqual({
+ error: new Error("C ERROR"),
+ });
- appFixture.close();
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/bubble-response-after");
+ await app.clickLink("/bubble-response-after/a/b/c");
+ await page.waitForSelector("[data-error]");
+ expect(await page.locator("[data-error]").textContent()).toBe(
+ "A Error Boundary",
+ );
+ expect(await page.locator("pre").textContent()).toBe("C ERROR");
+ expect(await page.locator("p").textContent()).toBe("A LOADER");
});
- test("handles errors on the way down on resource routes (document)", async () => {
+ test("bubbles response up the chain when multiple middlewares throw in sequence", async ({
+ page,
+ }) => {
let errors: any[] = [];
console.error = (...args) => errors.push(args);
- let fixture = await createFixture(
- {
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
- export default defineConfig({
- build: { manifest: true, minify: false },
- plugins: [reactRouter()],
- });
- `,
- "app/entry.server.tsx": ENTRY_SERVER_WITH_HANDLE_ERROR,
- "app/routes/a.tsx": js`
- export const middleware = [
- async ({ context }, next) => {
- throw new Error("broken!");
- },
- ];
- `,
- "app/routes/a.b.tsx": js`
- export async function loader({ request, context }) {
- return new Response("ok");
- }
- `,
- },
- },
- UNSAFE_ServerMode.Development,
+ let res = await fixture.requestDocument(
+ "/bubble-response-multiple/a/b/c",
);
-
- let res = await fixture.requestResource("/a/b");
expect(res.status).toBe(500);
- await expect(res.text()).resolves.toBe(
- "Unexpected Server Error\n\nError: broken!",
- );
+ expect(res.headers.get("x-a")).toBe("true");
+ expect(res.headers.get("x-b")).toBe(null);
+ let html = await res.text();
+ expect(html).toContain("A Error Boundary");
+ expect(html).toContain("B ERROR");
+ expect(html).toContain("A LOADER");
expect(errors).toEqual([
- ["handleError", "GET", "/a/b", new Error("broken!")],
+ [
+ "handleError",
+ "GET",
+ "/bubble-response-multiple/a/b/c",
+ new Error("C ERROR"),
+ ],
+ [
+ "handleError",
+ "GET",
+ "/bubble-response-multiple/a/b/c",
+ new Error("B ERROR"),
+ ],
]);
- });
+ errors.splice(0);
- test("handles errors on the way down on resource routes (data)", async () => {
- let errors: any[] = [];
- console.error = (...args) => errors.push(args);
- let fixture = await createFixture(
+ let data = await fixture.requestSingleFetchData(
+ "/bubble-response-multiple/a/b/c.data",
+ );
+ expect(data.status).toBe(500);
+ expect(data.headers.get("x-a")).toBe("true");
+ expect(data.headers.get("x-b")).toBe(null);
+ expect((data.data as any)["routes/bubble-response-multiple.a"]).toEqual({
+ data: "A LOADER",
+ });
+ expect((data.data as any)["routes/bubble-response-multiple.a.b"]).toEqual(
{
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true, minify: false },
- plugins: [reactRouter()],
- });
- `,
- "app/entry.server.tsx": ENTRY_SERVER_WITH_HANDLE_ERROR,
- "app/routes/a.tsx": js`
- export const middleware = [
- async ({ context }, next) => {
- throw new Error("broken!");
- },
- ];
- `,
- "app/routes/a.b.tsx": js`
- export async function loader({ request, context }) {
- return new Response("ok");
- }
- `,
- },
+ error: new Error("B ERROR"),
},
- UNSAFE_ServerMode.Development,
);
+ expect(errors).toEqual([
+ [
+ "handleError",
+ "GET",
+ "/bubble-response-multiple/a/b/c.data",
+ new Error("C ERROR"),
+ ],
+ [
+ "handleError",
+ "GET",
+ "/bubble-response-multiple/a/b/c.data",
+ new Error("B ERROR"),
+ ],
+ ]);
+ errors.splice(0);
- let res = await fixture.requestSingleFetchData("/a/b.data");
- expect(res.status).toBe(500);
- expect(res.data).toEqual({
- "routes/a": { error: new Error("broken!") },
- });
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/bubble-response-multiple");
+ await app.clickLink("/bubble-response-multiple/a/b/c");
+ await page.waitForSelector("[data-error]");
+ expect(await page.locator("[data-error]").textContent()).toBe(
+ "A Error Boundary",
+ );
+ expect(await page.locator("pre").textContent()).toBe("B ERROR");
+ expect(await page.locator("p").textContent()).toBe("A LOADER");
expect(errors).toEqual([
- ["handleError", "GET", "/a/b.data", new Error("broken!")],
+ [
+ "handleError",
+ "GET",
+ "/bubble-response-multiple/a/b/c.data",
+ new Error("C ERROR"),
+ ],
+ [
+ "handleError",
+ "GET",
+ "/bubble-response-multiple/a/b/c.data",
+ new Error("B ERROR"),
+ ],
]);
+ errors.splice(0);
});
- test("handles errors on the way up on resource routes (document)", async () => {
+ test("handles errors on the way down on resource routes", async () => {
let errors: any[] = [];
console.error = (...args) => errors.push(args);
- let fixture = await createFixture(
- {
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
-
- export default defineConfig({
- build: { manifest: true, minify: false },
- plugins: [reactRouter()],
- });
- `,
- "app/entry.server.tsx": ENTRY_SERVER_WITH_HANDLE_ERROR,
- "app/routes/a.tsx": js`
- export const middleware = [
- async ({ context }, next) => {
- let res = await next()
- throw new Error("broken!");
- },
- ];
- `,
- "app/routes/a.b.tsx": js`
- export async function loader({ request, context }) {
- return new Response("ok");
- }
- `,
- },
- },
- UNSAFE_ServerMode.Development,
- );
- let res = await fixture.requestResource("/a/b");
+ let res = await fixture.requestResource("/error-down-resource/a/b");
expect(res.status).toBe(500);
await expect(res.text()).resolves.toBe(
"Unexpected Server Error\n\nError: broken!",
);
expect(errors).toEqual([
- ["handleError", "GET", "/a/b", new Error("broken!")],
+ [
+ "handleError",
+ "GET",
+ "/error-down-resource/a/b",
+ new Error("broken!"),
+ ],
+ ]);
+ errors.splice(0);
+
+ let res2 = await fixture.requestSingleFetchData(
+ "/error-down-resource/a/b.data",
+ );
+ expect(res2.status).toBe(500);
+ expect(res2.data).toEqual({
+ "routes/error-down-resource.a": { error: new Error("broken!") },
+ });
+ expect(errors).toEqual([
+ [
+ "handleError",
+ "GET",
+ "/error-down-resource/a/b.data",
+ new Error("broken!"),
+ ],
]);
+ errors.splice(0);
});
- test("handles errors on the way up on resource routes (data)", async () => {
+ test("handles errors on the way up on resource routes", async () => {
let errors: any[] = [];
console.error = (...args) => errors.push(args);
- let fixture = await createFixture(
- {
- files: {
- "react-router.config.ts": reactRouterConfig({
- v8_middleware: true,
- }),
- "vite.config.ts": js`
- import { defineConfig } from "vite";
- import { reactRouter } from "@react-router/dev/vite";
- export default defineConfig({
- build: { manifest: true, minify: false },
- plugins: [reactRouter()],
- });
- `,
- "app/entry.server.tsx": ENTRY_SERVER_WITH_HANDLE_ERROR,
- "app/routes/a.tsx": js`
- export const middleware = [
- async ({ context }, next) => {
- let res = await next();
- throw new Error("broken!");
- },
- ];
- `,
- "app/routes/a.b.tsx": js`
- export async function loader({ request, context }) {
- return "ok"
- }
- `,
- },
- },
- UNSAFE_ServerMode.Development,
+ let res = await fixture.requestResource("/error-up-resource/a/b");
+ expect(res.status).toBe(500);
+ await expect(res.text()).resolves.toBe(
+ "Unexpected Server Error\n\nError: broken!",
);
+ expect(errors).toEqual([
+ ["handleError", "GET", "/error-up-resource/a/b", new Error("broken!")],
+ ]);
+ errors.splice(0);
- let res = await fixture.requestSingleFetchData("/a/b.data");
- expect(res.status).toBe(500);
- expect(res.data).toEqual({
- "routes/a": { error: new Error("broken!") },
- "routes/a.b": { data: "ok" },
+ let res2 = await fixture.requestSingleFetchData(
+ "/error-up-resource/a/b.data",
+ );
+ expect(res2.status).toBe(500);
+ expect(res2.data).toEqual({
+ "routes/error-up-resource.a": { error: new Error("broken!") },
+ "routes/error-up-resource.a.b": { data: "ok" },
});
expect(errors).toEqual([
- ["handleError", "GET", "/a/b.data", new Error("broken!")],
+ [
+ "handleError",
+ "GET",
+ "/error-up-resource/a/b.data",
+ new Error("broken!"),
+ ],
]);
+ errors.splice(0);
});
});
});