Skip to content

feat: turbo-stream v3 #12945

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: dev
Choose a base branch
from
6 changes: 6 additions & 0 deletions .changeset/turbo-v3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"integration": minor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont think we need entries for this

Suggested change
"integration": minor

"react-router": minor
---

feat: turbo-stream v3
2,035 changes: 1,637 additions & 398 deletions integration/defer-test.ts

Large diffs are not rendered by default.

268 changes: 259 additions & 9 deletions integration/error-boundary-v2-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ test.describe("ErrorBoundary", () => {
`,

"app/routes/parent.tsx": js`
import { useEffect } from "react";
import {
Link,
Outlet,
Expand All @@ -48,11 +49,25 @@ test.describe("ErrorBoundary", () => {
useRouteError,
} from "react-router";

export function loader() {
return "PARENT LOADER";
export function loader({ request }) {
const url = new URL(request.url);
return {message: "PARENT LOADER", error: url.searchParams.has('error') };
}

export default function Component() {
export default function Component({ loaderData }) {
useEffect(() => {
let ogFetch = window.fetch;
if (loaderData.error) {
window.fetch = async (...args) => {
return new Response('CDN Error!', { status: 500 });
};

return () => {
window.fetch = ogFetch;
};
}
}, [loaderData.error]);

return (
<div>
<nav>
Expand All @@ -66,7 +81,7 @@ test.describe("ErrorBoundary", () => {
<li><Link to="/parent/child-without-boundary?type=render">Link</Link></li>
</ul>
</nav>
<p id="parent-data">{useLoaderData()}</p>
<p id="parent-data">{loaderData.message}</p>
<Outlet />
</div>
)
Expand Down Expand Up @@ -166,21 +181,256 @@ test.describe("ErrorBoundary", () => {
test("Network errors that never reach the Remix server", async ({
page,
}) => {
let app = new PlaywrightFixture(appFixture, page);
// Cause a .data request to trigger an HTTP error that never reaches the
// Remix server, and ensure we properly handle it at the ErrorBoundary
await page.route(/\/parent\/child-with-boundary\.data$/, (route) => {
route.fulfill({ status: 500, body: "CDN Error!" });
});
await app.goto("/parent?error");
await app.clickLink("/parent/child-with-boundary");
await waitForAndAssert(page, app, "#parent-error", "500");
});
});

function runBoundaryTests() {
test("No errors", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/parent");
await app.clickLink("/parent/child-with-boundary");
await waitForAndAssert(page, app, "#child-data", "CHILD LOADER");
});

test("Throwing a Response to own boundary", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/parent");
await app.clickLink("/parent/child-with-boundary?type=response");
await waitForAndAssert(
page,
app,
"#child-error-response",
"418 Loader Response"
);
});

test("Throwing an Error to own boundary", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/parent");
await app.clickLink("/parent/child-with-boundary?type=error");
await waitForAndAssert(page, app, "#child-error", "Loader Error");
});

test("Throwing a render error to own boundary", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/parent");
await app.clickLink("/parent/child-with-boundary?type=render");
await waitForAndAssert(page, app, "#child-error", "Render Error");
});

test("Throwing a Response to parent boundary", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/parent");
await app.clickLink("/parent/child-without-boundary?type=response");
await waitForAndAssert(
page,
app,
"#parent-error",
"Unable to decode turbo-stream response"
"#parent-error-response",
"418 Loader Response"
);
});

test("Throwing an Error to parent boundary", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/parent");
await app.clickLink("/parent/child-without-boundary?type=error");
await waitForAndAssert(page, app, "#parent-error", "Loader Error");
});

test("Throwing a render error to parent boundary", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/parent");
await app.clickLink("/parent/child-without-boundary?type=render");
await waitForAndAssert(page, app, "#parent-error", "Render Error");
});
}
});

test.describe("ErrorBoundary turboV3", () => {
let fixture: Fixture;
let appFixture: AppFixture;
let oldConsoleError: () => void;

test.beforeAll(async () => {
fixture = await createFixture({
turboV3: true,
files: {
"app/root.tsx": js`
import { Links, Meta, Outlet, Scripts } from "react-router";

export default function Root() {
return (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<main>
<Outlet />
</main>
<Scripts />
</body>
</html>
);
}
`,

"app/routes/parent.tsx": js`
import { useEffect } from "react";
import {
Link,
Outlet,
isRouteErrorResponse,
useLoaderData,
useRouteError,
} from "react-router";

export function loader({ request }) {
const url = new URL(request.url);
return {message: "PARENT LOADER", error: url.searchParams.has('error') };
}

export default function Component({ loaderData }) {
useEffect(() => {
let ogFetch = window.fetch;
if (loaderData.error) {
window.fetch = async (...args) => {
return new Response('CDN Error!', { status: 500 });
};

return () => {
window.fetch = ogFetch;
};
}
}, [loaderData.error]);

return (
<div>
<nav>
<ul>
<li><Link to="/parent/child-with-boundary">Link</Link></li>
<li><Link to="/parent/child-with-boundary?type=error">Link</Link></li>
<li><Link to="/parent/child-with-boundary?type=response">Link</Link></li>
<li><Link to="/parent/child-with-boundary?type=render">Link</Link></li>
<li><Link to="/parent/child-without-boundary?type=error">Link</Link></li>
<li><Link to="/parent/child-without-boundary?type=response">Link</Link></li>
<li><Link to="/parent/child-without-boundary?type=render">Link</Link></li>
</ul>
</nav>
<p id="parent-data">{loaderData.message}</p>
<Outlet />
</div>
)
}

export function ErrorBoundary() {
let error = useRouteError();
return isRouteErrorResponse(error) ?
<p id="parent-error-response">{error.status + ' ' + error.data}</p> :
<p id="parent-error">{error.message}</p>;
}
`,

"app/routes/parent.child-with-boundary.tsx": js`
import {
isRouteErrorResponse,
useLoaderData,
useLocation,
useRouteError,
} from "react-router";

export function loader({ request }) {
let errorType = new URL(request.url).searchParams.get('type');
if (errorType === 'response') {
throw new Response('Loader Response', { status: 418 });
} else if (errorType === 'error') {
throw new Error('Loader Error');
}
return "CHILD LOADER";
}

export default function Component() {;
let data = useLoaderData();
if (new URLSearchParams(useLocation().search).get('type') === "render") {
throw new Error("Render Error");
}
return <p id="child-data">{data}</p>;
}

export function ErrorBoundary() {
let error = useRouteError();
return isRouteErrorResponse(error) ?
<p id="child-error-response">{error.status + ' ' + error.data}</p> :
<p id="child-error">{error.message}</p>;
}
`,

"app/routes/parent.child-without-boundary.tsx": js`
import { useLoaderData, useLocation } from "react-router";

export function loader({ request }) {
let errorType = new URL(request.url).searchParams.get('type');
if (errorType === 'response') {
throw new Response('Loader Response', { status: 418 });
} else if (errorType === 'error') {
throw new Error('Loader Error');
}
return "CHILD LOADER";
}

export default function Component() {;
let data = useLoaderData();
if (new URLSearchParams(useLocation().search).get('type') === "render") {
throw new Error("Render Error");
}
return <p id="child-data">{data}</p>;
}
`,
},
});

appFixture = await createAppFixture(fixture, ServerMode.Development);
});

test.afterAll(() => {
appFixture.close();
});

test.beforeEach(({ page }) => {
oldConsoleError = console.error;
console.error = () => {};
});

test.afterEach(() => {
console.error = oldConsoleError;
});

test.describe("without JavaScript", () => {
test.use({ javaScriptEnabled: false });
runBoundaryTests();
});

test.describe("with JavaScript", () => {
test.use({ javaScriptEnabled: true });
runBoundaryTests();

test("Network errors that never reach the Remix server", async ({
page,
}) => {
let app = new PlaywrightFixture(appFixture, page);
// Cause a .data request to trigger an HTTP error that never reaches the
// Remix server, and ensure we properly handle it at the ErrorBoundary
await app.goto("/parent?error");
await app.clickLink("/parent/child-with-boundary");
await waitForAndAssert(page, app, "#parent-error", "500");
});
});

function runBoundaryTests() {
Expand Down
Loading