diff --git a/.changeset/v2-remove-catch-boundary.md b/.changeset/v2-remove-catch-boundary.md new file mode 100644 index 00000000000..f8084f8fbcd --- /dev/null +++ b/.changeset/v2-remove-catch-boundary.md @@ -0,0 +1,8 @@ +--- +"@remix-run/dev": major +"@remix-run/react": major +"@remix-run/server-runtime": major +"@remix-run/testing": major +--- + +Remove `v2_errorBoundary` flag and `CatchBoundary` implementation diff --git a/docs/api/conventions.md b/docs/api/conventions.md index 64697bbd5cb..a0dc6a1c001 100644 --- a/docs/api/conventions.md +++ b/docs/api/conventions.md @@ -174,10 +174,6 @@ title: Conventions [Moved →][moved-41] -### CatchBoundary - -[Moved →][moved-42] - ### ErrorBoundary [Moved →][moved-43] @@ -238,7 +234,6 @@ title: Conventions [moved-39]: ../route/links [moved-40]: ../route/links#htmllinkdescriptor [moved-41]: ../route/links#pagelinkdescriptor -[moved-42]: ../route/catch-boundary [moved-43]: ../route/error-boundary [moved-44]: ../route/handle [moved-48]: ../other-api/asset-imports diff --git a/docs/file-conventions/routes-files.md b/docs/file-conventions/routes-files.md index f32a8b5df42..37616858b9e 100644 --- a/docs/file-conventions/routes-files.md +++ b/docs/file-conventions/routes-files.md @@ -22,7 +22,7 @@ app/ The file in `app/root.tsx` is your root layout, or "root route" (very sorry for those of you who pronounce those words the same way!). It works just like all other routes: - You can export a [`loader`][loader], [`action`][action], [`meta`][meta], [`headers`][headers], or [`links`][links] function -- You can export an [`ErrorBoundary`][error-boundary] or [`CatchBoundary`][catch-boundary] +- You can export an [`ErrorBoundary`][error-boundary] - Your default export is the layout component that renders the rest of your app in an [``][outlet] ## Basic Routes @@ -306,7 +306,6 @@ Because some characters have special meaning, you must use our escaping syntax i [headers]: ../route/headers [links]: ../route/links [error-boundary]: ../route/error-boundary -[catch-boundary]: ../route/catch-boundary [outlet]: ../components/outlet [view-example-app]: https://github.com/remix-run/examples/tree/main/multiple-params [use-params]: https://reactrouter.com/hooks/use-params diff --git a/docs/guides/data-loading.md b/docs/guides/data-loading.md index 6d173c2675b..dadd8d5a349 100644 --- a/docs/guides/data-loading.md +++ b/docs/guides/data-loading.md @@ -14,7 +14,7 @@ One of the primary features of Remix is simplifying interactions with the server - Ensure the data in the UI is in sync with the data on the server by revalidating after [actions][action] - Excellent scroll restoration on back/forward clicks (even across domains) - Handle server-side errors with [error boundaries][error-boundary] -- Enable solid UX for "Not Found" and "Unauthorized" with [catch boundaries][catch-boundary] +- Enable solid UX for "Not Found" and "Unauthorized" with [error boundaries][error-boundary] - Help you keep the happy path of your UI happy ## Basics @@ -257,7 +257,7 @@ export default function Product() { ## Not Found -While loading data it's common for a record to be "not found". As soon as you know you can't render the component as expected, `throw` a response and Remix will stop executing code in the current loader and switch over to the nearest [catch boundary][catch-boundary]. +While loading data it's common for a record to be "not found". As soon as you know you can't render the component as expected, `throw` a response and Remix will stop executing code in the current loader and switch over to the nearest [error boundary][error-boundary]. ```tsx lines=[10-13] export const loader = async ({ @@ -684,7 +684,7 @@ That said, if you bring an external data library and sidestep Remix's own data c - Ensure the data in the UI is in sync with the data on the server by revalidating after actions - Excellent scroll restoration on back/forward clicks (even across domains) - Handle server-side errors with [error boundaries][error-boundary] -- Enable solid UX for "Not Found" and "Unauthorized" with [catch boundaries][catch-boundary] +- Enable solid UX for "Not Found" and "Unauthorized" with [error boundaries][error-boundary] - Help you keep the happy path of your UI happy. Instead you'll need to do extra work to provide a good user experience. @@ -736,7 +736,6 @@ export default function RouteComp() { ``` [action]: ../route/action -[catch-boundary]: ../route/catch-boundary [cloudflare-kv-setup]: https://developers.cloudflare.com/workers/cli-wrangler/commands#kv [cloudflare-kv]: https://developers.cloudflare.com/workers/learning/how-kv-works [error-boundary]: ../route/error-boundary diff --git a/docs/guides/not-found.md b/docs/guides/not-found.md index 96b01cc94cd..a0923059d38 100644 --- a/docs/guides/not-found.md +++ b/docs/guides/not-found.md @@ -34,19 +34,19 @@ export async function loader({ params }: LoaderArgs) { } ``` -Remix will catch the response and send your app down the [Catch Boundary][catch-boundary] path. It's actually exactly like Remix's automatic [error handling][errors], but instead of exporting an `ErrorBoundary`, you export a `CatchBoundary`. +Remix will catch the response and send your app down the [Error Boundary][error-boundary] path. It's actually exactly like Remix's automatic [error handling][errors], but instead of receiving an `Error` from `useRouteError()`, you'll receive an object with your response `status`, `statusText`, and extracted `data`. What's nice about throwing a response is that code in your loader _stops executing_. The rest of your code doesn't have to deal with the chance that the page is defined or not (this is especially handy for TypeScript). Throwing also ensures that your route component doesn't render if the loader wasn't successful. Your route components only have to consider the "happy path". They don't need pending states, error states, or in our case here, not-found states. -## Root Catch Boundary +## Root Error Boundary -You probably already have one at the root of your app. This will handle all thrown responses that weren't handled in a nested route (more on that in a sec). Here's a sample: +You probably already have one at the root of your app. This will handle all thrown responses that weren't handled in a nested route. Here's a sample: ```tsx -export function CatchBoundary() { - const caught = useCatch(); +export function ErrorBoundary() { + const error = useRouteError(); return ( @@ -56,7 +56,11 @@ export function CatchBoundary() {

- {caught.status} {caught.statusText} + {isRouteErrorResponse(error) + ? `${error.status} ${error.statusText}` + : error instanceof Error + ? error.message + : "Unknown Error"}

@@ -65,59 +69,6 @@ export function CatchBoundary() { } ``` -## Nested Catch Boundaries - -Just like [errors], nested routes can export their own catch boundary to handle the 404 UI without taking down all of the parent layouts around it, and add some nice UX touches right in context. Bots are happy, SEO is happy, CDNs are happy, users are happy, and your code stays in context, so it seems like everybody involved is happy with this. - -```tsx filename=app/routes/pages/$pageId.tsx -import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno -import { - Form, - useLoaderData, - useParams, -} from "@remix-run/react"; - -export async function loader({ params }: LoaderArgs) { - const page = await db.page.findOne({ - where: { slug: params.slug }, - }); - - if (!page) { - throw new Response(null, { - status: 404, - statusText: "Not Found", - }); - } - - return json(page); -} - -export function CatchBoundary() { - const params = useParams(); - return ( -
-

We couldn't find that page!

-
- -
-
- ); -} - -export default function Page() { - return ()} />; -} -``` - -As you can probably tell, this mechanism isn't just limited to 404s. You can throw any response from a loader or action to send your app down the catch boundary path. For more information, check out the [Catch Boundary][catch-boundary] docs. - -[catch-boundary]: ../route/catch-boundary [errors]: ./errors [404-status-code]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404 [splat-route]: ./routing#splats diff --git a/docs/pages/api-development-strategy.md b/docs/pages/api-development-strategy.md index 4ba18fe57c5..60ed13b57ff 100644 --- a/docs/pages/api-development-strategy.md +++ b/docs/pages/api-development-strategy.md @@ -50,13 +50,12 @@ The lifecycle is thus either: ## Current Future Flags -| Flag | Description | -| -------------------- | --------------------------------------------------------------------- | -| `v2_dev` | Enable the new development server (including HMR/HDR support) | -| `v2_errorBoundary` | Combine `ErrorBoundary`/`CatchBoundary` into a single `ErrorBoundary` | -| `v2_headers` | Leverage ancestor `headers` if children do not export `headers` | -| `v2_meta` | Enable the new API for your `meta` functions | -| `v2_routeConvention` | Enable the flat routes style of file-based routing | +| Flag | Description | +| -------------------- | --------------------------------------------------------------- | +| `v2_dev` | Enable the new development server (including HMR/HDR support) | +| `v2_headers` | Leverage ancestor `headers` if children do not export `headers` | +| `v2_meta` | Enable the new API for your `meta` functions | +| `v2_routeConvention` | Enable the flat routes style of file-based routing | [future-flags-blog-post]: https://remix.run/blog/future-flags [feature-flowchart]: /docs-images/feature-flowchart.png diff --git a/docs/pages/faq.md b/docs/pages/faq.md index 080f70fcf7c..cabe0a51928 100644 --- a/docs/pages/faq.md +++ b/docs/pages/faq.md @@ -220,11 +220,4 @@ Again, `formData.getAll()` is often all you need, we encourage you to give it a [form-data]: https://developer.mozilla.org/en-US/docs/Web/API/FormData [query-string]: https://www.npmjs.com/package/query-string [ramda]: https://www.npmjs.com/package/ramda - -## What's the difference between `CatchBoundary` & `ErrorBoundary`? - -Error boundaries render when your application throws an error and you had no clue it was going to happen. Most apps just go blank or have spinners spin forever. In remix the error boundary renders and you have granular control over it. - -Catch boundaries render when you decide in a loader that you can't proceed down the happy path to render the UI you want (auth required, record not found, etc.), so you throw a response and let some catch boundary up the tree handle it. - [watch-on-you-tube]: https://www.youtube.com/watch?v=w2i-9cYxSdc&ab_channel=Remix diff --git a/docs/pages/v2.md b/docs/pages/v2.md index a4a40f9fb81..4851da9466b 100644 --- a/docs/pages/v2.md +++ b/docs/pages/v2.md @@ -263,9 +263,7 @@ The [meta v2][meta-v2] docs have more tips on merging route meta. ```js filename=remix.config.js /** @type {import('@remix-run/dev').AppConfig} */ module.exports = { - future: { - v2_errorBoundary: true, - }, + future: {}, }; ``` diff --git a/docs/route/catch-boundary.md b/docs/route/catch-boundary.md deleted file mode 100644 index b19ab105731..00000000000 --- a/docs/route/catch-boundary.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -title: CatchBoundary ---- - -# `CatchBoundary` - -The behaviors of `CatchBoundary` and `ErrorBoundary` are changing in v2. You can prepare for this change at your convenience with the `v2_errorBoundary` future flag. For instructions on making this change see the [v2 guide][v2guide]. - -A `CatchBoundary` is a React component that renders whenever an action or loader throws a `Response`. - -**Note:** We use the word "catch" to represent the codepath taken when a `Response` type is thrown; you thought about bailing from the "happy path". This is different from an uncaught error you did not expect to occur. - -A Remix `CatchBoundary` component works just like a route component, but instead of `useLoaderData` you have access to `useCatch`. When a response is thrown in an action or loader, the `CatchBoundary` will be rendered in its place, nested inside parent routes. - -A `CatchBoundary` component has access to the status code and thrown response data through `useCatch`. - -```tsx -import { useCatch } from "@remix-run/react"; - -export function CatchBoundary() { - const caught = useCatch(); - - return ( -
-

Caught

-

Status: {caught.status}

-
-        {JSON.stringify(caught.data, null, 2)}
-      
-
- ); -} -``` - -[error-boundary-v2]: ./error-boundary-v2 -[v2guide]: ../pages/v2#catchboundary-and-errorboundary diff --git a/docs/route/error-boundary-v2.md b/docs/route/error-boundary-v2.md deleted file mode 100644 index 6937bf28504..00000000000 --- a/docs/route/error-boundary-v2.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: ErrorBoundary (v2) -new: true ---- - -# `ErrorBoundary (v2)` - -You can opt-in to the new ErrorBoundary API with a future flag in Remix config. - -```js filename=remix.config.js -/** @type {import('@remix-run/dev').AppConfig} */ -module.exports = { - future: { - v2_errorBoundary: true, - }, -}; -``` - -A Remix `ErrorBoundary` component works just like normal React [error boundaries][error-boundaries], but with a few extra capabilities. When there is an error in your route component, the `ErrorBoundary` will be rendered in its place, nested inside any parent routes. `ErrorBoundary` components also render when there is an error in the `loader` or `action` functions for a route, so all errors for that route may be handled in one spot. - -The most common use-cases tend to be: - -- You may intentionally throw a 4xx `Response` to trigger an error UI - - Throwing a 400 on bad user input - - Throwing a 401 for unauthorized access - - Throwing a 404 when you can't find requested data -- React may unintentionally throw an `Error` if it encounters a runtime error during rendering - -To obtain the thrown object, you can use the [`useRouteError`][use-route-error] hook. When a `Response` is thrown, it will be automatically unwrapped into an `ErrorResponse` instance with `state`/`statusText`/`data` fields so that you don't need to bother with `await response.json()` in your component. To differentiate thrown `Response`'s from thrown `Error`'s' you can use the [`isRouteErrorResponse`][is-route-error-response] utility. - -```tsx -import { - isRouteErrorResponse, - useRouteError, -} from "@remix-run/react"; - -export function ErrorBoundary() { - const error = useRouteError(); - - if (isRouteErrorResponse(error)) { - return ( -
-

- {error.status} {error.statusText} -

-

{error.data}

-
- ); - } else if (error instanceof Error) { - return ( -
-

Error

-

{error.message}

-

The stack trace is:

-
{error.stack}
-
- ); - } else { - return

Unknown Error

; - } -} -``` - -[error-boundaries]: https://reactjs.org/docs/error-boundaries.html -[rr-error-boundary]: https://reactrouter.com/en/main/route/error-element -[use-route-error]: ../hooks/use-route-error -[is-route-error-response]: ../utils/is-route-error-response diff --git a/docs/route/error-boundary.md b/docs/route/error-boundary.md index 13fb9e00831..3f35fb66676 100644 --- a/docs/route/error-boundary.md +++ b/docs/route/error-boundary.md @@ -1,30 +1,55 @@ --- title: ErrorBoundary +new: true --- # `ErrorBoundary` -The behaviors of `CatchBoundary` and `ErrorBoundary` are changing in v2. You can prepare for this change at your convenience with the `v2_errorBoundary` future flag. For instructions on making this change see the [v2 guide][v2guide]. +A Remix `ErrorBoundary` component works just like normal React [error boundaries][error-boundaries], but with a few extra capabilities. When there is an error in your route component, the `ErrorBoundary` will be rendered in its place, nested inside any parent routes. `ErrorBoundary` components also render when there is an error in the `loader` or `action` functions for a route, so all errors for that route may be handled in one spot. -An `ErrorBoundary` is a React component that renders whenever there is an error anywhere on the route, either during rendering or during data loading. We use the word "error" to mean an uncaught exception; something you didn't anticipate happening. You can intentionally throw a `Response` to render the `CatchBoundary`, but everything else that is thrown is handled by the `ErrorBoundary`. +The most common use-cases tend to be: -A Remix `ErrorBoundary` component works just like normal React [error boundaries][error-boundaries], but with a few extra capabilities. When there is an error in your route component, the `ErrorBoundary` will be rendered in its place, nested inside any parent routes. `ErrorBoundary` components also render when there is an error in the `loader` or `action` functions for a route, so all errors for that route may be handled in one spot. +- You may intentionally throw a 4xx `Response` to trigger an error UI + - Throwing a 400 on bad user input + - Throwing a 401 for unauthorized access + - Throwing a 404 when you can't find requested data +- React may unintentionally throw an `Error` if it encounters a runtime error during rendering -An `ErrorBoundary` component receives one prop: the `error` that occurred. +To obtain the thrown object, you can use the [`useRouteError`][use-route-error] hook. When a `Response` is thrown, it will be automatically unwrapped into an `ErrorResponse` instance with `state`/`statusText`/`data` fields so that you don't need to bother with `await response.json()` in your component. To differentiate thrown `Response`'s from thrown `Error`'s' you can use the [`isRouteErrorResponse`][is-route-error-response] utility. ```tsx -export function ErrorBoundary({ error }) { - return ( -
-

Error

-

{error.message}

-

The stack trace is:

-
{error.stack}
-
- ); +import { + isRouteErrorResponse, + useRouteError, +} from "@remix-run/react"; + +export function ErrorBoundary() { + const error = useRouteError(); + + if (isRouteErrorResponse(error)) { + return ( +
+

+ {error.status} {error.statusText} +

+

{error.data}

+
+ ); + } else if (error instanceof Error) { + return ( +
+

Error

+

{error.message}

+

The stack trace is:

+
{error.stack}
+
+ ); + } else { + return

Unknown Error

; + } } ``` [error-boundaries]: https://reactjs.org/docs/error-boundaries.html -[error-boundary-v2]: ./error-boundary-v2 -[v2guide]: ../pages/v2#catchboundary-and-errorboundary +[use-route-error]: ../hooks/use-route-error +[is-route-error-response]: ../utils/is-route-error-response diff --git a/docs/route/loader.md b/docs/route/loader.md index 8bdbd367591..639968614ec 100644 --- a/docs/route/loader.md +++ b/docs/route/loader.md @@ -197,18 +197,12 @@ See also: Along with returning responses, you can also throw `Response` objects from your loaders. This allows you to break through the call stack and do one of two things: - Redirect to another URL -- Show an alternate UI with contextual data through the `CatchBoundary` +- Show an alternate UI with contextual data through the `ErrorBoundary` Here is a full example showing how you can create utility functions that throw responses to stop code execution in the loader and show an alternative UI. ```ts filename=app/db.ts import { json } from "@remix-run/node"; // or cloudflare/deno -import type { ThrownResponse } from "@remix-run/react"; - -export type InvoiceNotFoundResponse = ThrownResponse< - 404, - string ->; export function getInvoice(id, user) { const invoice = db.invoice.find({ where: { id } }); @@ -232,7 +226,7 @@ export async function requireUserSession(request) { // You can throw our helpers like `redirect` and `json` because they // return `Response` objects. A `redirect` response will redirect to // another URL, while other responses will trigger the UI rendered - // in the `CatchBoundary`. + // in the `ErrorBoundary`. throw redirect("/login", 302); } return session.get("user"); @@ -242,21 +236,14 @@ export async function requireUserSession(request) { ```tsx filename=app/routes/invoice/$invoiceId.tsx import type { LoaderArgs } from "@remix-run/node"; // or cloudflare/deno import { json } from "@remix-run/node"; // or cloudflare/deno -import type { ThrownResponse } from "@remix-run/react"; -import { useCatch, useLoaderData } from "@remix-run/react"; +import { + isRouteErrorResponse, + useLoaderData, +} from "@remix-run/react"; import { getInvoice } from "~/db"; -import type { InvoiceNotFoundResponse } from "~/db"; import { requireUserSession } from "~/http"; -type InvoiceCatchData = { - invoiceOwnerEmail: string; -}; - -type ThrownResponses = - | InvoiceNotFoundResponse - | ThrownResponse<401, InvoiceCatchData>; - export const loader = async ({ params, request, @@ -279,31 +266,37 @@ export default function InvoiceRoute() { return ; } -export function CatchBoundary() { - // this returns { data, status, statusText } - const caught = useCatch(); - - switch (caught.status) { - case 401: - return ( -
-

You don't have access to this invoice.

-

- Contact {caught.data.invoiceOwnerEmail} to get - access -

-
- ); - case 404: - return
Invoice not found!
; +export function ErrorBoundary() { + const error = useRouteError(); + + if (isRouteErrorResponse(error)) { + switch (error.status) { + case 401: + return ( +
+

You don't have access to this invoice.

+

+ Contact {error.data.invoiceOwnerEmail} to get + access +

+
+ ); + case 404: + return
Invoice not found!
; + } + + return ( +
+ Something went wrong: {error.status}{" "} + {error.statusText} +
+ ); } - // You could also `throw new Error("Unknown status in catch boundary")`. - // This will be caught by the closest `ErrorBoundary`. return (
- Something went wrong: {caught.status}{" "} - {caught.statusText} + Something went wrong:{" "} + {error?.message || "Unknown Error"}
); } diff --git a/integration/action-test.ts b/integration/action-test.ts index 51e57026860..84f4edee4e6 100644 --- a/integration/action-test.ts +++ b/integration/action-test.ts @@ -20,7 +20,6 @@ test.describe("actions", () => { config: { future: { v2_routeConvention: true, - v2_errorBoundary: true, }, }, files: { diff --git a/integration/catch-boundary-data-test.ts b/integration/catch-boundary-data-test.ts index 89e9e86fabd..9665527553e 100644 --- a/integration/catch-boundary-data-test.ts +++ b/integration/catch-boundary-data-test.ts @@ -31,224 +31,12 @@ test.beforeEach(async ({ context }) => { }); }); -test.beforeAll(async () => { - fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, - files: { - "app/root.jsx": js` - import { json } from "@remix-run/node"; - import { - Links, - Meta, - Outlet, - Scripts, - useLoaderData, - useMatches, - } from "@remix-run/react"; - - export const loader = () => json("${ROOT_DATA}"); - - export default function Root() { - const data = useLoaderData(); - - return ( - - - - - - -
{data}
- - - - - ); - } - - export function CatchBoundary() { - let matches = useMatches(); - let { data } = matches.find(match => match.id === "root"); - - return ( - - - -
${ROOT_BOUNDARY_TEXT}
-
{data}
- - - - ); - } - `, - - "app/routes/_index.jsx": js` - import { Link } from "@remix-run/react"; - export default function Index() { - return ( -
- ${NO_BOUNDARY_LOADER} - ${HAS_BOUNDARY_LAYOUT_NESTED_LOADER} - ${HAS_BOUNDARY_NESTED_LOADER} -
- ); - } - `, - - [`app/routes${NO_BOUNDARY_LOADER_FILE}.jsx`]: js` - export function loader() { - throw new Response("", { status: 401 }); - } - export default function Index() { - return
; - } - `, - - [`app/routes${HAS_BOUNDARY_LAYOUT_NESTED_LOADER_FILE}.jsx`]: js` - import { useMatches } from "@remix-run/react"; - export function loader() { - return "${LAYOUT_DATA}"; - } - export default function Layout() { - return
; - } - export function CatchBoundary() { - let matches = useMatches(); - let { data } = matches.find(match => match.id === "routes${HAS_BOUNDARY_LAYOUT_NESTED_LOADER_FILE}"); - - return ( -
-
${LAYOUT_BOUNDARY_TEXT}
-
{data}
-
- ); - } - `, - - [`app/routes${HAS_BOUNDARY_LAYOUT_NESTED_LOADER_FILE}._index.jsx`]: js` - export function loader() { - throw new Response("", { status: 401 }); - } - export default function Index() { - return
; - } - `, - - [`app/routes${HAS_BOUNDARY_NESTED_LOADER_FILE}.jsx`]: js` - import { Outlet, useLoaderData } from "@remix-run/react"; - export function loader() { - return "${LAYOUT_DATA}"; - } - export default function Layout() { - let data = useLoaderData(); - return ( -
-
{data}
- -
- ); - } - `, - - [`app/routes${HAS_BOUNDARY_NESTED_LOADER_FILE}._index.jsx`]: js` - export function loader() { - throw new Response("", { status: 401 }); - } - export default function Index() { - return
; - } - export function CatchBoundary() { - return ( -
${OWN_BOUNDARY_TEXT}
- ); - } - `, - }, - }); - - appFixture = await createAppFixture(fixture); -}); - -test.afterAll(() => { - appFixture.close(); -}); - -test("renders root boundary with data available", async () => { - let res = await fixture.requestDocument(NO_BOUNDARY_LOADER); - expect(res.status).toBe(401); - let html = await res.text(); - expect(html).toMatch(ROOT_BOUNDARY_TEXT); - expect(html).toMatch(ROOT_DATA); -}); - -test("renders root boundary with data available on transition", async ({ - page, -}) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(NO_BOUNDARY_LOADER); - await page.waitForSelector("#root-boundary"); - await page.waitForSelector(`#root-boundary-data:has-text("${ROOT_DATA}")`); -}); - -test("renders layout boundary with data available", async () => { - let res = await fixture.requestDocument(HAS_BOUNDARY_LAYOUT_NESTED_LOADER); - expect(res.status).toBe(401); - let html = await res.text(); - expect(html).toMatch(ROOT_DATA); - expect(html).toMatch(LAYOUT_BOUNDARY_TEXT); - expect(html).toMatch(LAYOUT_DATA); -}); - -test("renders layout boundary with data available on transition", async ({ - page, -}) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(HAS_BOUNDARY_LAYOUT_NESTED_LOADER); - await page.waitForSelector(`#root-data:has-text("${ROOT_DATA}")`); - await page.waitForSelector( - `#layout-boundary:has-text("${LAYOUT_BOUNDARY_TEXT}")` - ); - await page.waitForSelector( - `#layout-boundary-data:has-text("${LAYOUT_DATA}")` - ); -}); - -test("renders self boundary with layout data available", async () => { - let res = await fixture.requestDocument(HAS_BOUNDARY_NESTED_LOADER); - expect(res.status).toBe(401); - let html = await res.text(); - expect(html).toMatch(ROOT_DATA); - expect(html).toMatch(LAYOUT_DATA); - expect(html).toMatch(OWN_BOUNDARY_TEXT); -}); - -test("renders self boundary with layout data available on transition", async ({ - page, -}) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(HAS_BOUNDARY_NESTED_LOADER); - await page.waitForSelector(`#root-data:has-text("${ROOT_DATA}")`); - await page.waitForSelector(`#layout-data:has-text("${LAYOUT_DATA}")`); - await page.waitForSelector(`#own-boundary:has-text("${OWN_BOUNDARY_TEXT}")`); -}); - -// Copy/Paste of the above tests altered to use v2_errorBoundary. In v2 we can: -// - delete the above tests -// - remove this describe block -// - remove the v2_errorBoundary flag -test.describe("v2_errorBoundary", () => { +test.describe("ErrorBoundary (thrown responses)", () => { test.beforeAll(async () => { fixture = await createFixture({ config: { future: { v2_routeConvention: true, - v2_errorBoundary: true, }, }, files: { diff --git a/integration/catch-boundary-test.ts b/integration/catch-boundary-test.ts index 9fc70460f19..00527d2c481 100644 --- a/integration/catch-boundary-test.ts +++ b/integration/catch-boundary-test.ts @@ -4,7 +4,7 @@ import { createAppFixture, createFixture, js } from "./helpers/create-fixture"; import type { Fixture, AppFixture } from "./helpers/create-fixture"; import { PlaywrightFixture } from "./helpers/playwright-fixture"; -test.describe("CatchBoundary", () => { +test.describe("ErrorBoundary (thrown responses)", () => { let fixture: Fixture; let appFixture: AppFixture; @@ -22,383 +22,15 @@ test.describe("CatchBoundary", () => { let NOT_FOUND_HREF = "/not/found"; - test.beforeEach(async ({ context }) => { - await context.route(/_data/, async (route) => { - await new Promise((resolve) => setTimeout(resolve, 50)); - route.continue(); - }); - }); - test.beforeAll(async () => { fixture = await createFixture({ config: { - future: { v2_routeConvention: true }, + future: { + v2_routeConvention: true, + }, }, files: { "app/root.jsx": js` - import { json } from "@remix-run/node"; - import { Links, Meta, Outlet, Scripts, useMatches } from "@remix-run/react"; - - export function loader() { - return json({ data: "ROOT LOADER" }); - } - - export default function Root() { - return ( - - - - - - - - - - - ); - } - - export function CatchBoundary() { - let matches = useMatches() - return ( - - - -
${ROOT_BOUNDARY_TEXT}
-
{JSON.stringify(matches)}
- - - - ) - } - `, - - "app/routes/_index.jsx": js` - import { Link, Form } from "@remix-run/react"; - export default function() { - return ( -
- ${NOT_FOUND_HREF} - -
-
- ) - } - `, - - [`app/routes${HAS_BOUNDARY_ACTION_FILE}.jsx`]: js` - import { Form } from "@remix-run/react"; - export async function action() { - throw new Response("", { status: 401 }) - } - export function CatchBoundary() { - return

${OWN_BOUNDARY_TEXT}

- } - export default function Index() { - return ( -
- -
- ); - } - `, - - [`app/routes${NO_BOUNDARY_ACTION_FILE}.jsx`]: js` - import { Form } from "@remix-run/react"; - export function action() { - throw new Response("", { status: 401 }) - } - export default function Index() { - return ( -
- -
- ) - } - `, - - [`app/routes${HAS_BOUNDARY_LOADER_FILE}.jsx`]: js` - import { useCatch } from '@remix-run/react'; - export function loader() { - throw new Response("", { status: 401 }) - } - export function CatchBoundary() { - let caught = useCatch(); - return ( - <> -
${OWN_BOUNDARY_TEXT}
-
{caught.status}
- - ); - } - export default function Index() { - return
- } - `, - - [`app/routes${HAS_BOUNDARY_LOADER_FILE}.child.jsx`]: js` - export function loader() { - throw new Response("", { status: 404 }) - } - export default function Index() { - return
- } - `, - - [`app/routes${NO_BOUNDARY_LOADER_FILE}.jsx`]: js` - export function loader() { - throw new Response("", { status: 401 }) - } - export default function Index() { - return
- } - `, - - "app/routes/action.jsx": js` - import { Outlet, useLoaderData } from "@remix-run/react"; - - export function loader() { - return "PARENT"; - } - - export default function () { - return ( -
-

{useLoaderData()}

- -
- ) - } - `, - - "app/routes/action.child-catch.jsx": js` - import { Form, useCatch, useLoaderData } from "@remix-run/react"; - - export function loader() { - return "CHILD"; - } - - export function action() { - throw new Response("Caught!", { status: 400 }); - } - - export default function () { - return ( - <> -

{useLoaderData()}

-
- -
- - ) - } - - export function CatchBoundary() { - let caught = useCatch() - return

{caught.status} {caught.data}

; - } - `, - }, - }); - - appFixture = await createAppFixture(fixture); - }); - - test.afterAll(() => { - appFixture.close(); - }); - - test("non-matching urls on document requests", async () => { - let res = await fixture.requestDocument(NOT_FOUND_HREF); - expect(res.status).toBe(404); - let html = await res.text(); - expect(html).toMatch(ROOT_BOUNDARY_TEXT); - - // There should be no loader data on the root route - let expected = JSON.stringify([ - { id: "root", pathname: "", params: {} }, - ]).replace(/"/g, """); - expect(html).toContain(`
${expected}
`); - }); - - test("non-matching urls on client transitions", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(NOT_FOUND_HREF, { wait: false }); - await page.waitForSelector("#root-boundary"); - - // Root loader data sticks around from previous load - let expected = JSON.stringify([ - { id: "root", pathname: "", params: {}, data: { data: "ROOT LOADER" } }, - ]); - expect(await app.getHtml("#matches")).toContain(expected); - }); - - test("own boundary, action, document request", async () => { - let params = new URLSearchParams(); - let res = await fixture.postDocument(HAS_BOUNDARY_ACTION, params); - expect(res.status).toBe(401); - expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); - }); - - test("own boundary, action, client transition from other route", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(HAS_BOUNDARY_ACTION); - await page.waitForSelector("#action-boundary"); - }); - - test("own boundary, action, client transition from itself", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto(HAS_BOUNDARY_ACTION); - await app.clickSubmitButton(HAS_BOUNDARY_ACTION); - await page.waitForSelector("#action-boundary"); - }); - - test("bubbles to parent in action document requests", async () => { - let params = new URLSearchParams(); - let res = await fixture.postDocument(NO_BOUNDARY_ACTION, params); - expect(res.status).toBe(401); - expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); - }); - - test("bubbles to parent in action script transitions from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(NO_BOUNDARY_ACTION); - await page.waitForSelector("#root-boundary"); - }); - - test("bubbles to parent in action script transitions from self", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto(NO_BOUNDARY_ACTION); - await app.clickSubmitButton(NO_BOUNDARY_ACTION); - await page.waitForSelector("#root-boundary"); - }); - - test("own boundary, loader, document request", async () => { - let res = await fixture.requestDocument(HAS_BOUNDARY_LOADER); - expect(res.status).toBe(401); - expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); - }); - - test("own boundary, loader, client transition", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(HAS_BOUNDARY_LOADER); - await page.waitForSelector("#boundary-loader"); - }); - - test("bubbles to parent in loader document requests", async () => { - let res = await fixture.requestDocument(NO_BOUNDARY_LOADER); - expect(res.status).toBe(401); - expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); - }); - - test("bubbles to parent in loader transitions from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(NO_BOUNDARY_LOADER); - await page.waitForSelector("#root-boundary"); - }); - - test("uses correct catch boundary on server action errors", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto(`/action/child-catch`); - expect(await app.getHtml("#parent-data")).toMatch("PARENT"); - expect(await app.getHtml("#child-data")).toMatch("CHILD"); - await page.click("button[type=submit]"); - await page.waitForSelector("#child-catch"); - // Preserves parent loader data - expect(await app.getHtml("#parent-data")).toMatch("PARENT"); - expect(await app.getHtml("#child-catch")).toMatch("400"); - expect(await app.getHtml("#child-catch")).toMatch("Caught!"); - }); - - test("prefers parent catch when child loader also bubbles, document request", async () => { - let res = await fixture.requestDocument(`${HAS_BOUNDARY_LOADER}/child`); - expect(res.status).toBe(401); - let text = await res.text(); - expect(text).toMatch(OWN_BOUNDARY_TEXT); - expect(text).toMatch('
401
'); - }); - - test("prefers parent catch when child loader also bubbles, client transition", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(`${HAS_BOUNDARY_LOADER}/child`); - await page.waitForSelector("#boundary-loader"); - expect(await app.getHtml("#boundary-loader")).toMatch(OWN_BOUNDARY_TEXT); - expect(await app.getHtml("#status")).toMatch("401"); - }); -}); - -// Copy/Paste of the above tests altered to use v2_errorBoundary. In v2 we can: -// - delete the above tests -// - remove this describe block -// - remove the v2_errorBoundary flag -test.describe("v2_errorBoundary", () => { - test.describe("CatchBoundary", () => { - let fixture: Fixture; - let appFixture: AppFixture; - - let ROOT_BOUNDARY_TEXT = "ROOT_TEXT" as const; - let OWN_BOUNDARY_TEXT = "OWN_BOUNDARY_TEXT" as const; - - let HAS_BOUNDARY_LOADER = "/yes/loader" as const; - let HAS_BOUNDARY_LOADER_FILE = "/yes.loader" as const; - let HAS_BOUNDARY_ACTION = "/yes/action" as const; - let HAS_BOUNDARY_ACTION_FILE = "/yes.action" as const; - let NO_BOUNDARY_ACTION = "/no/action" as const; - let NO_BOUNDARY_ACTION_FILE = "/no.action" as const; - let NO_BOUNDARY_LOADER = "/no/loader" as const; - let NO_BOUNDARY_LOADER_FILE = "/no.loader" as const; - - let NOT_FOUND_HREF = "/not/found"; - - test.beforeAll(async () => { - fixture = await createFixture({ - config: { - future: { - v2_routeConvention: true, - v2_errorBoundary: true, - }, - }, - files: { - "app/root.jsx": js` import { json } from "@remix-run/node"; import { Links, Meta, Outlet, Scripts, useMatches } from "@remix-run/react"; @@ -436,7 +68,7 @@ test.describe("v2_errorBoundary", () => { } `, - "app/routes/_index.jsx": js` + "app/routes/_index.jsx": js` import { Link, Form } from "@remix-run/react"; export default function() { return ( @@ -462,7 +94,7 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${HAS_BOUNDARY_ACTION_FILE}.jsx`]: js` + [`app/routes${HAS_BOUNDARY_ACTION_FILE}.jsx`]: js` import { Form } from "@remix-run/react"; export async function action() { throw new Response("", { status: 401 }) @@ -481,7 +113,7 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${NO_BOUNDARY_ACTION_FILE}.jsx`]: js` + [`app/routes${NO_BOUNDARY_ACTION_FILE}.jsx`]: js` import { Form } from "@remix-run/react"; export function action() { throw new Response("", { status: 401 }) @@ -497,7 +129,7 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${HAS_BOUNDARY_LOADER_FILE}.jsx`]: js` + [`app/routes${HAS_BOUNDARY_LOADER_FILE}.jsx`]: js` import { useRouteError } from '@remix-run/react'; export function loader() { throw new Response("", { status: 401 }) @@ -516,7 +148,7 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${HAS_BOUNDARY_LOADER_FILE}.child.jsx`]: js` + [`app/routes${HAS_BOUNDARY_LOADER_FILE}.child.jsx`]: js` export function loader() { throw new Response("", { status: 404 }) } @@ -525,7 +157,7 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${NO_BOUNDARY_LOADER_FILE}.jsx`]: js` + [`app/routes${NO_BOUNDARY_LOADER_FILE}.jsx`]: js` export function loader() { throw new Response("", { status: 401 }) } @@ -534,7 +166,7 @@ test.describe("v2_errorBoundary", () => { } `, - "app/routes/action.jsx": js` + "app/routes/action.jsx": js` import { Outlet, useLoaderData } from "@remix-run/react"; export function loader() { @@ -551,7 +183,7 @@ test.describe("v2_errorBoundary", () => { } `, - "app/routes/action.child-catch.jsx": js` + "app/routes/action.child-catch.jsx": js` import { Form, useLoaderData, useRouteError } from "@remix-run/react"; export function loader() { @@ -580,152 +212,157 @@ test.describe("v2_errorBoundary", () => { return

{error.status} {error.data}

; } `, - }, - }); - - appFixture = await createAppFixture(fixture); + }, }); - test.afterAll(() => { - appFixture.close(); - }); + appFixture = await createAppFixture(fixture); + }); - test("non-matching urls on document requests", async () => { - let res = await fixture.requestDocument(NOT_FOUND_HREF); - expect(res.status).toBe(404); - let html = await res.text(); - expect(html).toMatch(ROOT_BOUNDARY_TEXT); - - // There should be no loader data on the root route - let expected = JSON.stringify([ - { id: "root", pathname: "", params: {} }, - ]).replace(/"/g, """); - expect(html).toContain(`
${expected}
`); - }); + test.afterAll(() => { + appFixture.close(); + }); - test("non-matching urls on client transitions", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(NOT_FOUND_HREF, { wait: false }); - await page.waitForSelector("#root-boundary"); - - // Root loader data sticks around from previous load - let expected = JSON.stringify([ - { id: "root", pathname: "", params: {}, data: { data: "ROOT LOADER" } }, - ]); - expect(await app.getHtml("#matches")).toContain(expected); - }); + test("non-matching urls on document requests", async () => { + let oldConsoleError; + oldConsoleError = console.error; + console.error = () => {}; - test("own boundary, action, document request", async () => { - let params = new URLSearchParams(); - let res = await fixture.postDocument(HAS_BOUNDARY_ACTION, params); - expect(res.status).toBe(401); - expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); - }); + let res = await fixture.requestDocument(NOT_FOUND_HREF); + expect(res.status).toBe(404); + let html = await res.text(); + expect(html).toMatch(ROOT_BOUNDARY_TEXT); - test("own boundary, action, client transition from other route", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(HAS_BOUNDARY_ACTION); - await page.waitForSelector("#action-boundary"); - }); + // There should be no loader data on the root route + let expected = JSON.stringify([ + { id: "root", pathname: "", params: {} }, + ]).replace(/"/g, """); + expect(html).toContain(`
${expected}
`); - test("own boundary, action, client transition from itself", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto(HAS_BOUNDARY_ACTION); - await app.clickSubmitButton(HAS_BOUNDARY_ACTION); - await page.waitForSelector("#action-boundary"); - }); + console.error = oldConsoleError; + }); - test("bubbles to parent in action document requests", async () => { - let params = new URLSearchParams(); - let res = await fixture.postDocument(NO_BOUNDARY_ACTION, params); - expect(res.status).toBe(401); - expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); - }); + test("non-matching urls on client transitions", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickLink(NOT_FOUND_HREF, { wait: false }); + await page.waitForSelector("#root-boundary"); - test("bubbles to parent in action script transitions from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(NO_BOUNDARY_ACTION); - await page.waitForSelector("#root-boundary"); - }); + // Root loader data sticks around from previous load + let expected = JSON.stringify([ + { id: "root", pathname: "", params: {}, data: { data: "ROOT LOADER" } }, + ]); + expect(await app.getHtml("#matches")).toContain(expected); + }); - test("bubbles to parent in action script transitions from self", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto(NO_BOUNDARY_ACTION); - await app.clickSubmitButton(NO_BOUNDARY_ACTION); - await page.waitForSelector("#root-boundary"); - }); + test("own boundary, action, document request", async () => { + let params = new URLSearchParams(); + let res = await fixture.postDocument(HAS_BOUNDARY_ACTION, params); + expect(res.status).toBe(401); + expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); + }); - test("own boundary, loader, document request", async () => { - let res = await fixture.requestDocument(HAS_BOUNDARY_LOADER); - expect(res.status).toBe(401); - expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); - }); + test("own boundary, action, client transition from other route", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickSubmitButton(HAS_BOUNDARY_ACTION); + await page.waitForSelector("#action-boundary"); + }); - test("own boundary, loader, client transition", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(HAS_BOUNDARY_LOADER); - await page.waitForSelector("#boundary-loader"); - }); + test("own boundary, action, client transition from itself", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto(HAS_BOUNDARY_ACTION); + await app.clickSubmitButton(HAS_BOUNDARY_ACTION); + await page.waitForSelector("#action-boundary"); + }); - test("bubbles to parent in loader document requests", async () => { - let res = await fixture.requestDocument(NO_BOUNDARY_LOADER); - expect(res.status).toBe(401); - expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); - }); + test("bubbles to parent in action document requests", async () => { + let params = new URLSearchParams(); + let res = await fixture.postDocument(NO_BOUNDARY_ACTION, params); + expect(res.status).toBe(401); + expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); + }); - test("bubbles to parent in loader transitions from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(NO_BOUNDARY_LOADER); - await page.waitForSelector("#root-boundary"); - }); + test("bubbles to parent in action script transitions from other routes", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickSubmitButton(NO_BOUNDARY_ACTION); + await page.waitForSelector("#root-boundary"); + }); - test("uses correct catch boundary on server action errors", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto(`/action/child-catch`); - expect(await app.getHtml("#parent-data")).toMatch("PARENT"); - expect(await app.getHtml("#child-data")).toMatch("CHILD"); - await page.click("button[type=submit]"); - await page.waitForSelector("#child-catch"); - // Preserves parent loader data - expect(await app.getHtml("#parent-data")).toMatch("PARENT"); - expect(await app.getHtml("#child-catch")).toMatch("400"); - expect(await app.getHtml("#child-catch")).toMatch("Caught!"); - }); + test("bubbles to parent in action script transitions from self", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto(NO_BOUNDARY_ACTION); + await app.clickSubmitButton(NO_BOUNDARY_ACTION); + await page.waitForSelector("#root-boundary"); + }); - test("prefers parent catch when child loader also bubbles, document request", async () => { - let res = await fixture.requestDocument(`${HAS_BOUNDARY_LOADER}/child`); - expect(res.status).toBe(401); - let text = await res.text(); - expect(text).toMatch(OWN_BOUNDARY_TEXT); - expect(text).toMatch('
401
'); - }); + test("own boundary, loader, document request", async () => { + let res = await fixture.requestDocument(HAS_BOUNDARY_LOADER); + expect(res.status).toBe(401); + expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); + }); - test("prefers parent catch when child loader also bubbles, client transition", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(`${HAS_BOUNDARY_LOADER}/child`); - await page.waitForSelector("#boundary-loader"); - expect(await app.getHtml("#boundary-loader")).toMatch(OWN_BOUNDARY_TEXT); - expect(await app.getHtml("#status")).toMatch("401"); - }); + test("own boundary, loader, client transition", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickLink(HAS_BOUNDARY_LOADER); + await page.waitForSelector("#boundary-loader"); + }); + + test("bubbles to parent in loader document requests", async () => { + let res = await fixture.requestDocument(NO_BOUNDARY_LOADER); + expect(res.status).toBe(401); + expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); + }); + + test("bubbles to parent in loader transitions from other routes", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickLink(NO_BOUNDARY_LOADER); + await page.waitForSelector("#root-boundary"); + }); + + test("uses correct catch boundary on server action errors", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto(`/action/child-catch`); + expect(await app.getHtml("#parent-data")).toMatch("PARENT"); + expect(await app.getHtml("#child-data")).toMatch("CHILD"); + await page.click("button[type=submit]"); + await page.waitForSelector("#child-catch"); + // Preserves parent loader data + expect(await app.getHtml("#parent-data")).toMatch("PARENT"); + expect(await app.getHtml("#child-catch")).toMatch("400"); + expect(await app.getHtml("#child-catch")).toMatch("Caught!"); + }); + + test("prefers parent catch when child loader also bubbles, document request", async () => { + let res = await fixture.requestDocument(`${HAS_BOUNDARY_LOADER}/child`); + expect(res.status).toBe(401); + let text = await res.text(); + expect(text).toMatch(OWN_BOUNDARY_TEXT); + expect(text).toMatch('
401
'); + }); + + test("prefers parent catch when child loader also bubbles, client transition", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickLink(`${HAS_BOUNDARY_LOADER}/child`); + await page.waitForSelector("#boundary-loader"); + expect(await app.getHtml("#boundary-loader")).toMatch(OWN_BOUNDARY_TEXT); + expect(await app.getHtml("#status")).toMatch("401"); }); }); diff --git a/integration/defer-test.ts b/integration/defer-test.ts index 9ffd25ab828..91b42b07ff9 100644 --- a/integration/defer-test.ts +++ b/integration/defer-test.ts @@ -45,7 +45,6 @@ test.describe("non-aborted", () => { config: { future: { v2_routeConvention: true, - v2_errorBoundary: true, }, }, files: { @@ -600,7 +599,6 @@ test.describe("non-aborted", () => { test("works with critical JSON like data", async ({ page }) => { let response = await fixture.requestDocument("/"); let html = await response.text(); - console.log(html); let criticalHTML = html.slice(0, html.indexOf("") + 7); expect(criticalHTML).toContain(ROOT_ID); expect(criticalHTML).toContain(INDEX_ID); diff --git a/integration/error-boundary-test.ts b/integration/error-boundary-test.ts index ae08c21d1fe..1680e24c8b6 100644 --- a/integration/error-boundary-test.ts +++ b/integration/error-boundary-test.ts @@ -37,1382 +37,18 @@ test.describe("ErrorBoundary", () => { // packages/remix-react/errorBoundaries.tsx let INTERNAL_ERROR_BOUNDARY_HEADING = "Application Error"; - test.beforeEach(async ({ context }) => { - await context.route(/_data/, async (route) => { - await new Promise((resolve) => setTimeout(resolve, 50)); - route.continue(); - }); - }); - - test.beforeAll(async () => { - _consoleError = console.error; - console.error = () => {}; - fixture = await createFixture( - { - config: { - future: { v2_routeConvention: true }, - }, - files: { - "app/root.jsx": js` - import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; - - export default function Root() { - return ( - - - - - - -
- -
- - - - ); - } - - export function ErrorBoundary() { - return ( - - - -
-
${ROOT_BOUNDARY_TEXT}
-
- - - - ) - } - `, - - "app/routes/_index.jsx": js` - import { Link, Form } from "@remix-run/react"; - export default function () { - return ( -
- ${NOT_FOUND_HREF} - -
- - - - -
- - - ${HAS_BOUNDARY_LOADER} - - - ${NO_BOUNDARY_LOADER} - - - ${HAS_BOUNDARY_RENDER} - - - ${NO_BOUNDARY_RENDER} - -
- ) - } - `, - - [`app/routes${HAS_BOUNDARY_ACTION_FILE}.jsx`]: js` - import { Form } from "@remix-run/react"; - export async function action() { - throw new Error("Kaboom!") - } - export function ErrorBoundary() { - return

${OWN_BOUNDARY_TEXT}

- } - export default function () { - return ( -
- -
- ); - } - `, - - [`app/routes${NO_BOUNDARY_ACTION_FILE}.jsx`]: js` - import { Form } from "@remix-run/react"; - export function action() { - throw new Error("Kaboom!") - } - export default function () { - return ( -
- -
- ) - } - `, - - [`app/routes${HAS_BOUNDARY_LOADER_FILE}.jsx`]: js` - export function loader() { - throw new Error("Kaboom!") - } - export function ErrorBoundary() { - return
${OWN_BOUNDARY_TEXT}
- } - export default function () { - return
- } - `, - - [`app/routes${NO_BOUNDARY_LOADER_FILE}.jsx`]: js` - export function loader() { - throw new Error("Kaboom!") - } - export default function () { - return
- } - `, - - [`app/routes${NO_BOUNDARY_RENDER_FILE}.jsx`]: js` - export default function () { - throw new Error("Kaboom!") - return
- } - `, - - [`app/routes${HAS_BOUNDARY_RENDER_FILE}.jsx`]: js` - export default function () { - throw new Error("Kaboom!") - return
- } - - export function ErrorBoundary() { - return
${OWN_BOUNDARY_TEXT}
- } - `, - - [`app/routes${HAS_BOUNDARY_NO_LOADER_OR_ACTION_FILE}.jsx`]: js` - export function ErrorBoundary() { - return
${OWN_BOUNDARY_TEXT}
- } - export default function Index() { - return
- } - `, - - [`app/routes${NO_BOUNDARY_NO_LOADER_OR_ACTION_FILE}.jsx`]: js` - export default function Index() { - return
- } - `, - - "app/routes/fetcher-boundary.jsx": js` - import { useFetcher } from "@remix-run/react"; - export function ErrorBoundary() { - return

${OWN_BOUNDARY_TEXT}

- } - export default function() { - let fetcher = useFetcher(); - - return ( -
- -
- ) - } - `, - - "app/routes/fetcher-no-boundary.jsx": js` - import { useFetcher } from "@remix-run/react"; - export default function() { - let fetcher = useFetcher(); - - return ( -
- - - -
- ) - } - `, - - "app/routes/action.jsx": js` - import { Outlet, useLoaderData } from "@remix-run/react"; - - export function loader() { - return "PARENT"; - } - - export default function () { - return ( -
-

{useLoaderData()}

- -
- ) - } - `, - - "app/routes/action.child-error.jsx": js` - import { Form, useLoaderData } from "@remix-run/react"; - - export function loader() { - return "CHILD"; - } - - export function action() { - throw new Error("Broken!"); - } - - export default function () { - return ( - <> -

{useLoaderData()}

-
- -
- - ) - } - - export function ErrorBoundary({ error }) { - return

{error.message}

; - } - `, - }, - }, - ServerMode.Development - ); - - appFixture = await createAppFixture(fixture, ServerMode.Development); - }); - - test.afterAll(() => { - console.error = _consoleError; - appFixture.close(); - }); - - test("invalid request methods", async () => { - let res = await fixture.requestDocument("/", { method: "OPTIONS" }); - expect(res.status).toBe(405); - expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); - }); - - test("own boundary, action, document request", async () => { - let params = new URLSearchParams(); - let res = await fixture.postDocument(HAS_BOUNDARY_ACTION, params); - expect(res.status).toBe(500); - expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); - }); - - test("own boundary, action, client transition from other route", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(HAS_BOUNDARY_ACTION); - await page.waitForSelector(`text=${OWN_BOUNDARY_TEXT}`); - expect(await app.getHtml("main")).toMatch(OWN_BOUNDARY_TEXT); - }); - - test("own boundary, action, client transition from itself", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto(HAS_BOUNDARY_ACTION); - await app.clickSubmitButton(HAS_BOUNDARY_ACTION); - await page.waitForSelector(`text=${OWN_BOUNDARY_TEXT}`); - expect(await app.getHtml("main")).toMatch(OWN_BOUNDARY_TEXT); - }); - - test("bubbles to parent in action document requests", async () => { - let params = new URLSearchParams(); - let res = await fixture.postDocument(NO_BOUNDARY_ACTION, params); - expect(res.status).toBe(500); - expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); - }); - - test("bubbles to parent in action script transitions from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(NO_BOUNDARY_ACTION); - await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`); - expect(await app.getHtml("main")).toMatch(ROOT_BOUNDARY_TEXT); - }); - - test("bubbles to parent in action script transitions from self", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto(NO_BOUNDARY_ACTION); - await app.clickSubmitButton(NO_BOUNDARY_ACTION); - await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`); - expect(await app.getHtml("main")).toMatch(ROOT_BOUNDARY_TEXT); - }); - - test("own boundary, loader, document request", async () => { - let res = await fixture.requestDocument(HAS_BOUNDARY_LOADER); - expect(res.status).toBe(500); - expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); - }); - - test("own boundary, loader, client transition", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(HAS_BOUNDARY_LOADER); - await page.waitForSelector(`text=${OWN_BOUNDARY_TEXT}`); - expect(await app.getHtml("main")).toMatch(OWN_BOUNDARY_TEXT); - }); - - test("bubbles to parent in loader document requests", async () => { - let res = await fixture.requestDocument(NO_BOUNDARY_LOADER); - expect(res.status).toBe(500); - expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); - }); - - test("bubbles to parent in loader script transitions from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(NO_BOUNDARY_LOADER); - await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`); - expect(await app.getHtml("main")).toMatch(ROOT_BOUNDARY_TEXT); - }); - - test("ssr rendering errors with no boundary", async () => { - let res = await fixture.requestDocument(NO_BOUNDARY_RENDER); - expect(res.status).toBe(500); - expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); - }); - - test("script transition rendering errors with no boundary", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(NO_BOUNDARY_RENDER); - await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`); - expect(await app.getHtml("main")).toMatch(ROOT_BOUNDARY_TEXT); - }); - - test("ssr rendering errors with boundary", async () => { - let res = await fixture.requestDocument(HAS_BOUNDARY_RENDER); - expect(res.status).toBe(500); - expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); - }); - - test("script transition rendering errors with boundary", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(HAS_BOUNDARY_RENDER); - await page.waitForSelector(`text=${OWN_BOUNDARY_TEXT}`); - expect(await app.getHtml("main")).toMatch(OWN_BOUNDARY_TEXT); - }); - - test("uses correct error boundary on server action errors in nested routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto(`/action/child-error`); - expect(await app.getHtml("#parent-data")).toMatch("PARENT"); - expect(await app.getHtml("#child-data")).toMatch("CHILD"); - await page.click("button[type=submit]"); - await page.waitForSelector("#child-error"); - // Preserves parent loader data - expect(await app.getHtml("#parent-data")).toMatch("PARENT"); - expect(await app.getHtml("#child-error")).toMatch("Broken"); - }); - - test("renders own boundary in fetcher action submission without action from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/fetcher-boundary"); - await app.clickSubmitButton(NO_BOUNDARY_NO_LOADER_OR_ACTION); - await page.waitForSelector("#fetcher-boundary"); - }); - - test("renders root boundary in fetcher action submission without action from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/fetcher-no-boundary"); - await app.clickSubmitButton(NO_BOUNDARY_NO_LOADER_OR_ACTION); - await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`); - }); - - test("renders root boundary in document POST without action requests", async () => { - let res = await fixture.requestDocument(NO_BOUNDARY_NO_LOADER_OR_ACTION, { - method: "post", - }); - expect(res.status).toBe(405); - expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); - }); - - test("renders root boundary in action script transitions without action from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(NO_BOUNDARY_NO_LOADER_OR_ACTION); - await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`); - }); - - test("renders own boundary in document POST without action requests", async () => { - let res = await fixture.requestDocument(HAS_BOUNDARY_NO_LOADER_OR_ACTION, { - method: "post", - }); - expect(res.status).toBe(405); - expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); - }); - - test("renders own boundary in action script transitions without action from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(HAS_BOUNDARY_NO_LOADER_OR_ACTION); - await page.waitForSelector("#boundary-no-loader-or-action"); - }); - - test.describe("if no error boundary exists in the app", () => { - let NO_ROOT_BOUNDARY_LOADER = "/loader-bad" as const; - let NO_ROOT_BOUNDARY_ACTION = "/action-bad" as const; - let NO_ROOT_BOUNDARY_LOADER_RETURN = "/loader-no-return" as const; - let NO_ROOT_BOUNDARY_ACTION_RETURN = "/action-no-return" as const; - - test.beforeAll(async () => { - fixture = await createFixture({ - config: { - future: { - v2_routeConvention: true, - }, - }, - files: { - "app/root.jsx": js` - import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; - - export default function Root() { - return ( - - - - - - - - - - - ); - } - `, - - "app/routes/_index.jsx": js` - import { Link, Form } from "@remix-run/react"; - - export default function () { - return ( -
-

Home

- Loader no return -
- - -
-
- ) - } - `, - - [`app/routes${NO_ROOT_BOUNDARY_LOADER}.jsx`]: js` - export async function loader() { - throw Error("BLARGH"); - } - - export default function () { - return ( -
-

Hello

-
- ) - } - `, - - [`app/routes${NO_ROOT_BOUNDARY_ACTION}.jsx`]: js` - export async function action() { - throw Error("YOOOOOOOO WHAT ARE YOU DOING"); - } - - export default function () { - return ( -
-

Goodbye

-
- ) - } - `, - - [`app/routes${NO_ROOT_BOUNDARY_LOADER_RETURN}.jsx`]: js` - import { useLoaderData } from "@remix-run/react"; - - export async function loader() {} - - export default function () { - let data = useLoaderData(); - return ( -
-

{data}

-
- ) - } - `, - - [`app/routes${NO_ROOT_BOUNDARY_ACTION_RETURN}.jsx`]: js` - import { useActionData } from "@remix-run/react"; - - export async function action() {} - - export default function () { - let data = useActionData(); - return ( -
-

{data}

-
- ) - } - `, - }, - }); - appFixture = await createAppFixture(fixture); - }); - - test("bubbles to internal boundary in loader document requests", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto(NO_ROOT_BOUNDARY_LOADER); - expect(await app.getHtml("h1")).toMatch(INTERNAL_ERROR_BOUNDARY_HEADING); - }); - - test("bubbles to internal boundary in action script transitions from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(NO_ROOT_BOUNDARY_ACTION); - await page.waitForSelector(`text=${INTERNAL_ERROR_BOUNDARY_HEADING}`); - expect(await app.getHtml("h1")).toMatch(INTERNAL_ERROR_BOUNDARY_HEADING); - }); - - test("bubbles to internal boundary if loader doesn't return (document requests)", async () => { - let res = await fixture.requestDocument(NO_ROOT_BOUNDARY_LOADER_RETURN); - expect(res.status).toBe(500); - expect(await res.text()).toMatch(INTERNAL_ERROR_BOUNDARY_HEADING); - }); - - test("bubbles to internal boundary if loader doesn't return", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(NO_ROOT_BOUNDARY_LOADER_RETURN); - await page.waitForSelector(`text=${INTERNAL_ERROR_BOUNDARY_HEADING}`); - expect(await app.getHtml("h1")).toMatch(INTERNAL_ERROR_BOUNDARY_HEADING); - }); - - test("bubbles to internal boundary if action doesn't return (document requests)", async () => { - let res = await fixture.requestDocument(NO_ROOT_BOUNDARY_ACTION_RETURN, { - method: "post", - }); - expect(res.status).toBe(500); - expect(await res.text()).toMatch(INTERNAL_ERROR_BOUNDARY_HEADING); - }); - - test("bubbles to internal boundary if action doesn't return", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(NO_ROOT_BOUNDARY_ACTION_RETURN); - await page.waitForSelector(`text=${INTERNAL_ERROR_BOUNDARY_HEADING}`); - expect(await app.getHtml("h1")).toMatch(INTERNAL_ERROR_BOUNDARY_HEADING); - }); - }); -}); - -test.describe("loaderData in ErrorBoundary", () => { - let fixture: Fixture; - let appFixture: AppFixture; - let consoleErrors: string[]; - let oldConsoleError: () => void; - - test.beforeAll(async () => { - fixture = await createFixture({ - config: { - future: { v2_routeConvention: true }, - }, - files: { - "app/root.jsx": js` - import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; - - export default function Root() { - return ( - - - - - - -
- -
- - - - ); - } - `, - - "app/routes/parent.jsx": js` - import { Outlet, useLoaderData, useMatches } from "@remix-run/react"; - - export function loader() { - return "PARENT"; - } - - export default function () { - return ( -
-

{useLoaderData()}

- -
- ) - } - - export function ErrorBoundary({ error }) { - return ( - <> -

{useLoaderData()}

-

- {useMatches().find(m => m.id === 'routes/parent').data} -

-

{error.message}

- - ); - } - `, - - "app/routes/parent.child-with-boundary.jsx": js` - import { Form, useLoaderData } from "@remix-run/react"; - - export function loader() { - return "CHILD"; - } - - export function action() { - throw new Error("Broken!"); - } - - export default function () { - return ( - <> -

{useLoaderData()}

-
- -
- - ) - } - - export function ErrorBoundary({ error }) { - return ( - <> -

{useLoaderData()}

-

{error.message}

- - ); - } - `, - - "app/routes/parent.child-without-boundary.jsx": js` - import { Form, useLoaderData } from "@remix-run/react"; - - export function loader() { - return "CHILD"; - } - - export function action() { - throw new Error("Broken!"); - } - - export default function () { - return ( - <> -

{useLoaderData()}

-
- -
- - ) - } - `, - }, - }); - - appFixture = await createAppFixture(fixture, ServerMode.Development); - }); - - test.afterAll(() => { - appFixture.close(); - }); - - test.beforeEach(({ page }) => { - oldConsoleError = console.error; - console.error = () => {}; - consoleErrors = []; - // Listen for all console events and handle errors - page.on("console", (msg) => { - if (msg.type() === "error") { - consoleErrors.push(msg.text()); - } - }); - }); - - test.afterEach(() => { - console.error = oldConsoleError; - }); - - test.describe("without JavaScript", () => { - test.use({ javaScriptEnabled: false }); - runBoundaryTests(); - }); - - test.describe("with JavaScript", () => { - test.use({ javaScriptEnabled: true }); - runBoundaryTests(); - }); - - function runBoundaryTests() { - test("Prevents useLoaderData in self ErrorBoundary", async ({ - page, - javaScriptEnabled, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/parent/child-with-boundary"); - - expect(await app.getHtml("#parent-data")).toEqual( - '

PARENT

' - ); - expect(await app.getHtml("#child-data")).toEqual( - '

CHILD

' - ); - expect(consoleErrors).toEqual([]); - - await app.clickSubmitButton("/parent/child-with-boundary"); - await page.waitForSelector("#child-error"); - - expect(await app.getHtml("#child-error")).toEqual( - '

Broken!

' - ); - expect(await app.getHtml("#parent-data")).toEqual( - '

PARENT

' - ); - expect(await app.getHtml("#child-data")).toEqual( - '

' - ); - - // Only look for this message. Chromium browsers will also log the - // network error but firefox does not - // "Failed to load resource: the server responded with a status of 500 (Internal Server Error)", - let msg = - "You cannot `useLoaderData` in an errorElement (routeId: routes/parent.child-with-boundary)"; - if (javaScriptEnabled) { - expect(consoleErrors.filter((m) => m === msg)).toEqual([msg]); - } else { - // We don't get the useLoaderData message in the client when JS is disabled - expect(consoleErrors.filter((m) => m === msg)).toEqual([]); - } - }); - - test("Prevents useLoaderData in bubbled ErrorBoundary", async ({ - page, - javaScriptEnabled, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/parent/child-without-boundary"); - - expect(await app.getHtml("#parent-data")).toEqual( - '

PARENT

' - ); - expect(await app.getHtml("#child-data")).toEqual( - '

CHILD

' - ); - expect(consoleErrors).toEqual([]); - - await app.clickSubmitButton("/parent/child-without-boundary"); - await page.waitForSelector("#parent-error"); - - expect(await app.getHtml("#parent-error")).toEqual( - '

Broken!

' - ); - expect(await app.getHtml("#parent-matches-data")).toEqual( - '

' - ); - expect(await app.getHtml("#parent-data")).toEqual( - '

' - ); - - // Only look for this message. Chromium browsers will also log the - // network error but firefox does not - // "Failed to load resource: the server responded with a status of 500 (Internal Server Error)", - let msg = - "You cannot `useLoaderData` in an errorElement (routeId: routes/parent)"; - if (javaScriptEnabled) { - expect(consoleErrors.filter((m) => m === msg)).toEqual([msg]); - } else { - // We don't get the useLoaderData message in the client when JS is disabled - expect(consoleErrors.filter((m) => m === msg)).toEqual([]); - } - }); - } -}); - -test.describe("Default ErrorBoundary", () => { - let fixture: Fixture; - let appFixture: AppFixture; - let _consoleError: any; - - function getFiles({ - includeRootErrorBoundary = false, - rootErrorBoundaryThrows = false, - } = {}) { - let errorBoundaryCode = !includeRootErrorBoundary - ? "" - : rootErrorBoundaryThrows - ? js` - export function ErrorBoundary({ error }) { - return ( - - - -
-
Root Error Boundary
-

{error.message}

-

{oh.no.what.have.i.done}

-
- - - - ) - } - ` - : js` - export function ErrorBoundary({ error }) { - return ( - - - -
-
Root Error Boundary
-

{error.message}

-
- - - - ) - } - `; - - return { - "app/root.jsx": js` - import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; - - export default function Root() { - return ( - - - - - - -
- -
- - - - ); - } - - ${errorBoundaryCode} - `, - - "app/routes/_index.jsx": js` - import { Link } from "@remix-run/react"; - export default function () { - return ( -
-

Index

- Loader Error - Render Error -
- ); - } - `, - - "app/routes/loader-error.jsx": js` - export function loader() { - throw new Error('Loader Error'); - } - export default function () { - return

Loader Error

- } - `, - - "app/routes/render-error.jsx": js` - export default function () { - throw new Error("Render Error") - } - `, - }; - } - - test.beforeAll(async () => { - _consoleError = console.error; - console.error = () => {}; - }); - - test.afterAll(async () => { - console.error = _consoleError; - appFixture.close(); - }); - - test.describe("When the root route does not have a boundary", () => { - test.beforeAll(async () => { - fixture = await createFixture( - { - files: getFiles({ includeRootErrorBoundary: false }), - config: { - future: { - v2_routeConvention: true, - }, - }, - }, - ServerMode.Development - ); - appFixture = await createAppFixture(fixture, ServerMode.Development); - }); - - test.afterAll(() => appFixture.close()); - - test.describe("document requests", () => { - test("renders default boundary on loader errors", async () => { - let res = await fixture.requestDocument("/loader-error"); - expect(res.status).toBe(500); - let text = await res.text(); - expect(text).toMatch("Application Error"); - expect(text).toMatch("Loader Error"); - expect(text).not.toMatch("Root Error Boundary"); - }); - - test("renders default boundary on render errors", async () => { - let res = await fixture.requestDocument("/render-error"); - expect(res.status).toBe(500); - let text = await res.text(); - expect(text).toMatch("Application Error"); - expect(text).toMatch("Render Error"); - expect(text).not.toMatch("Root Error Boundary"); - }); - }); - - test.describe("SPA navigations", () => { - test("renders default boundary on loader errors", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/loader-error"); - await page.waitForSelector("pre"); - let html = await app.getHtml(); - expect(html).toMatch("Application Error"); - expect(html).toMatch("Loader Error"); - expect(html).not.toMatch("Root Error Boundary"); - - // Ensure we can click back to our prior page - await app.goBack(); - await page.waitForSelector("h1#index"); - }); - - test("renders default boundary on render errors", async ({ - page, - }, workerInfo) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/render-error"); - await page.waitForSelector("pre"); - let html = await app.getHtml(); - expect(html).toMatch("Application Error"); - // Chromium seems to be the only one that includes the message in the stack - if (workerInfo.project.name === "chromium") { - expect(html).toMatch("Render Error"); - } - expect(html).not.toMatch("Root Error Boundary"); - - // Ensure we can click back to our prior page - await app.goBack(); - await page.waitForSelector("h1#index"); - }); - }); - }); - - test.describe("When the root route has a boundary", () => { - test.beforeAll(async () => { - fixture = await createFixture( - { - config: { - future: { - v2_routeConvention: true, - }, - }, - files: getFiles({ includeRootErrorBoundary: true }), - }, - ServerMode.Development - ); - appFixture = await createAppFixture(fixture, ServerMode.Development); - }); - - test.afterAll(() => appFixture.close()); - - test.describe("document requests", () => { - test("renders root boundary on loader errors", async () => { - let res = await fixture.requestDocument("/loader-error"); - expect(res.status).toBe(500); - let text = await res.text(); - expect(text).toMatch("Root Error Boundary"); - expect(text).toMatch("Loader Error"); - expect(text).not.toMatch("Application Error"); - }); - - test("renders root boundary on render errors", async () => { - let res = await fixture.requestDocument("/render-error"); - expect(res.status).toBe(500); - let text = await res.text(); - expect(text).toMatch("Root Error Boundary"); - expect(text).toMatch("Render Error"); - expect(text).not.toMatch("Application Error"); - }); - }); - - test.describe("SPA navigations", () => { - test("renders root boundary on loader errors", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/loader-error"); - await page.waitForSelector("#root-error-boundary"); - let html = await app.getHtml(); - expect(html).toMatch("Root Error Boundary"); - expect(html).toMatch("Loader Error"); - expect(html).not.toMatch("Application Error"); - - // Ensure we can click back to our prior page - await app.goBack(); - await page.waitForSelector("h1#index"); - }); - - test("renders root boundary on render errors", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/render-error"); - await page.waitForSelector("#root-error-boundary"); - let html = await app.getHtml(); - expect(html).toMatch("Root Error Boundary"); - expect(html).toMatch("Render Error"); - expect(html).not.toMatch("Application Error"); - - // Ensure we can click back to our prior page - await app.goBack(); - await page.waitForSelector("h1#index"); - }); - }); - }); - - test.describe("When the root route has a boundary but it also throws 😦", () => { - test.beforeAll(async () => { - fixture = await createFixture( - { - config: { - future: { - v2_routeConvention: true, - }, + test.beforeAll(async () => { + _consoleError = console.error; + console.error = () => {}; + fixture = await createFixture( + { + config: { + future: { + v2_routeConvention: true, }, - files: getFiles({ - includeRootErrorBoundary: true, - rootErrorBoundaryThrows: true, - }), }, - ServerMode.Development - ); - appFixture = await createAppFixture(fixture, ServerMode.Development); - }); - - test.afterAll(() => appFixture.close()); - - test.describe("document requests", () => { - test("tries to render root boundary on loader errors but bubbles to default boundary", async () => { - let res = await fixture.requestDocument("/loader-error"); - expect(res.status).toBe(500); - let text = await res.text(); - expect(text).toMatch("Unexpected Server Error"); - expect(text).not.toMatch("Application Error"); - expect(text).not.toMatch("Loader Error"); - expect(text).not.toMatch("Root Error Boundary"); - }); - - test("tries to render root boundary on render errors but bubbles to default boundary", async () => { - let res = await fixture.requestDocument("/render-error"); - expect(res.status).toBe(500); - let text = await res.text(); - expect(text).toMatch("Unexpected Server Error"); - expect(text).not.toMatch("Application Error"); - expect(text).not.toMatch("Render Error"); - expect(text).not.toMatch("Root Error Boundary"); - }); - }); - - test.describe("SPA navigations", () => { - test("tries to render root boundary on loader errors but bubbles to default boundary", async ({ - page, - }, workerInfo) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/loader-error"); - await page.waitForSelector("pre"); - let html = await app.getHtml(); - expect(html).toMatch("Application Error"); - if (workerInfo.project.name === "chromium") { - expect(html).toMatch("ReferenceError: oh is not defined"); - } - expect(html).not.toMatch("Loader Error"); - expect(html).not.toMatch("Root Error Boundary"); - - // Ensure we can click back to our prior page - await app.goBack(); - await page.waitForSelector("h1#index"); - }); - - test("tries to render root boundary on render errors but bubbles to default boundary", async ({ - page, - }, workerInfo) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/render-error"); - await page.waitForSelector("pre"); - let html = await app.getHtml(); - expect(html).toMatch("Application Error"); - if (workerInfo.project.name === "chromium") { - expect(html).toMatch("ReferenceError: oh is not defined"); - } - expect(html).not.toMatch("Render Error"); - expect(html).not.toMatch("Root Error Boundary"); - - // Ensure we can click back to our prior page - await app.goBack(); - await page.waitForSelector("h1#index"); - }); - }); - }); -}); - -test("Allows back-button out of an error boundary after a hard reload", async ({ - page, - browserName, -}) => { - let _consoleError = console.error; - console.error = () => {}; - - let fixture = await createFixture({ - config: { - future: { - v2_routeConvention: true, - v2_errorBoundary: false, - }, - }, - files: { - "app/root.jsx": js` - import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; - - export default function App() { - return ( - - - - - - - - - - - ); - } - - export function ErrorBoundary({ error }) { - return ( - - - Oh no! - - - - -

ERROR BOUNDARY

- - - - ); - } - `, - "app/routes/_index.jsx": js` - import { Link } from "@remix-run/react"; - - export default function Index() { - return ( -
-

INDEX

- This will error -
- ); - } - `, - - "app/routes/boom.jsx": js` - import { json } from "@remix-run/node"; - export function loader() { return boom(); } - export default function() { return my page; } - `, - }, - }); - - let appFixture = await createAppFixture(fixture); - let app = new PlaywrightFixture(appFixture, page); - - await app.goto("/"); - await page.waitForSelector("#index"); - expect(app.page.url()).not.toMatch("/boom"); - - await app.clickLink("/boom"); - await page.waitForSelector("#error"); - expect(app.page.url()).toMatch("/boom"); - - await app.reload(); - await page.waitForSelector("#error"); - expect(app.page.url()).toMatch("boom"); - - await app.goBack(); - - // Here be dragons - // - Playwright sets the Firefox `fission.webContentIsolationStrategy=0` preference - // for reasons having to do with out-of-process iframes: - // https://github.com/microsoft/playwright/issues/22640#issuecomment-1543287282 - // - That preference exposes a bug in firefox where a hard reload adds to the - // history stack: https://bugzilla.mozilla.org/show_bug.cgi?id=1832341 - // - Your can disable this preference via the Playwright `firefoxUserPrefs` config, - // but that is broken until 1.34: - // https://github.com/microsoft/playwright/issues/22640#issuecomment-1546230104 - // https://github.com/microsoft/playwright/issues/15405 - // - We can't yet upgrade to 1.34 because it drops support for Node 14: - // https://github.com/microsoft/playwright/releases/tag/v1.34.0 - // - // So for now when in firefox we just navigate back twice to work around the issue - if (browserName === "firefox") { - await app.goBack(); - } - - await page.waitForSelector("#index"); - expect(app.page.url()).not.toContain("boom"); - - appFixture.close(); - console.error = _consoleError; -}); - -// Copy/Paste of the above tests altered to use v2_errorBoundary. In v2 we can: -// - delete the above tests -// - remove this describe block -// - remove the v2_errorBoundary flag -test.describe("v2_errorBoundary", () => { - test.describe("ErrorBoundary", () => { - let fixture: Fixture; - let appFixture: AppFixture; - let _consoleError: any; - - let ROOT_BOUNDARY_TEXT = "ROOT_BOUNDARY_TEXT"; - let OWN_BOUNDARY_TEXT = "OWN_BOUNDARY_TEXT"; - - let HAS_BOUNDARY_LOADER = "/yes/loader" as const; - let HAS_BOUNDARY_LOADER_FILE = "/yes.loader" as const; - let HAS_BOUNDARY_ACTION = "/yes/action" as const; - let HAS_BOUNDARY_ACTION_FILE = "/yes.action" as const; - let HAS_BOUNDARY_RENDER = "/yes/render" as const; - let HAS_BOUNDARY_RENDER_FILE = "/yes.render" as const; - let HAS_BOUNDARY_NO_LOADER_OR_ACTION = "/yes/no-loader-or-action" as const; - let HAS_BOUNDARY_NO_LOADER_OR_ACTION_FILE = - "/yes.no-loader-or-action" as const; - - let NO_BOUNDARY_ACTION = "/no/action" as const; - let NO_BOUNDARY_ACTION_FILE = "/no.action" as const; - let NO_BOUNDARY_LOADER = "/no/loader" as const; - let NO_BOUNDARY_LOADER_FILE = "/no.loader" as const; - let NO_BOUNDARY_RENDER = "/no/render" as const; - let NO_BOUNDARY_RENDER_FILE = "/no.render" as const; - let NO_BOUNDARY_NO_LOADER_OR_ACTION = "/no/no-loader-or-action" as const; - let NO_BOUNDARY_NO_LOADER_OR_ACTION_FILE = - "/no.no-loader-or-action" as const; - - let NOT_FOUND_HREF = "/not/found"; - - // packages/remix-react/errorBoundaries.tsx - let INTERNAL_ERROR_BOUNDARY_HEADING = "Application Error"; - - test.beforeAll(async () => { - _consoleError = console.error; - console.error = () => {}; - fixture = await createFixture( - { - config: { - future: { - v2_routeConvention: true, - v2_errorBoundary: true, - }, - }, - files: { - "app/root.jsx": js` + files: { + "app/root.jsx": js` import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; export default function Root() { @@ -1447,7 +83,7 @@ test.describe("v2_errorBoundary", () => { } `, - "app/routes/_index.jsx": js` + "app/routes/_index.jsx": js` import { Link, Form } from "@remix-run/react"; export default function () { return ( @@ -1486,7 +122,7 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${HAS_BOUNDARY_ACTION_FILE}.jsx`]: js` + [`app/routes${HAS_BOUNDARY_ACTION_FILE}.jsx`]: js` import { Form } from "@remix-run/react"; export async function action() { throw new Error("Kaboom!") @@ -1505,7 +141,7 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${NO_BOUNDARY_ACTION_FILE}.jsx`]: js` + [`app/routes${NO_BOUNDARY_ACTION_FILE}.jsx`]: js` import { Form } from "@remix-run/react"; export function action() { throw new Error("Kaboom!") @@ -1521,7 +157,7 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${HAS_BOUNDARY_LOADER_FILE}.jsx`]: js` + [`app/routes${HAS_BOUNDARY_LOADER_FILE}.jsx`]: js` export function loader() { throw new Error("Kaboom!") } @@ -1533,7 +169,7 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${NO_BOUNDARY_LOADER_FILE}.jsx`]: js` + [`app/routes${NO_BOUNDARY_LOADER_FILE}.jsx`]: js` export function loader() { throw new Error("Kaboom!") } @@ -1542,14 +178,14 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${NO_BOUNDARY_RENDER_FILE}.jsx`]: js` + [`app/routes${NO_BOUNDARY_RENDER_FILE}.jsx`]: js` export default function () { throw new Error("Kaboom!") return
} `, - [`app/routes${HAS_BOUNDARY_RENDER_FILE}.jsx`]: js` + [`app/routes${HAS_BOUNDARY_RENDER_FILE}.jsx`]: js` export default function () { throw new Error("Kaboom!") return
@@ -1560,7 +196,7 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${HAS_BOUNDARY_NO_LOADER_OR_ACTION_FILE}.jsx`]: js` + [`app/routes${HAS_BOUNDARY_NO_LOADER_OR_ACTION_FILE}.jsx`]: js` export function ErrorBoundary() { return
${OWN_BOUNDARY_TEXT}
} @@ -1569,13 +205,13 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${NO_BOUNDARY_NO_LOADER_OR_ACTION_FILE}.jsx`]: js` + [`app/routes${NO_BOUNDARY_NO_LOADER_OR_ACTION_FILE}.jsx`]: js` export default function Index() { return
} `, - "app/routes/fetcher-boundary.jsx": js` + "app/routes/fetcher-boundary.jsx": js` import { useFetcher } from "@remix-run/react"; export function ErrorBoundary() { return

${OWN_BOUNDARY_TEXT}

@@ -1593,7 +229,7 @@ test.describe("v2_errorBoundary", () => { } `, - "app/routes/fetcher-no-boundary.jsx": js` + "app/routes/fetcher-no-boundary.jsx": js` import { useFetcher } from "@remix-run/react"; export default function() { let fetcher = useFetcher(); @@ -1610,7 +246,7 @@ test.describe("v2_errorBoundary", () => { } `, - "app/routes/action.jsx": js` + "app/routes/action.jsx": js` import { Outlet, useLoaderData } from "@remix-run/react"; export function loader() { @@ -1627,7 +263,7 @@ test.describe("v2_errorBoundary", () => { } `, - "app/routes/action.child-error.jsx": js` + "app/routes/action.child-error.jsx": js` import { Form, useLoaderData, useRouteError } from "@remix-run/react"; export function loader() { @@ -1656,225 +292,220 @@ test.describe("v2_errorBoundary", () => { return

{error.message}

; } `, - }, }, - ServerMode.Development - ); + }, + ServerMode.Development + ); - appFixture = await createAppFixture(fixture, ServerMode.Development); - }); + appFixture = await createAppFixture(fixture, ServerMode.Development); + }); - test.afterAll(() => { - console.error = _consoleError; - appFixture.close(); - }); + test.afterAll(() => { + console.error = _consoleError; + appFixture.close(); + }); - test("invalid request methods", async () => { - let res = await fixture.requestDocument("/", { method: "OPTIONS" }); - expect(res.status).toBe(405); - expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); - }); + test("invalid request methods", async () => { + let res = await fixture.requestDocument("/", { method: "OPTIONS" }); + expect(res.status).toBe(405); + expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); + }); - test("own boundary, action, document request", async () => { - let params = new URLSearchParams(); - let res = await fixture.postDocument(HAS_BOUNDARY_ACTION, params); - expect(res.status).toBe(500); - expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); - }); + test("own boundary, action, document request", async () => { + let params = new URLSearchParams(); + let res = await fixture.postDocument(HAS_BOUNDARY_ACTION, params); + expect(res.status).toBe(500); + expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); + }); - test("own boundary, action, client transition from other route", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(HAS_BOUNDARY_ACTION); - await page.waitForSelector(`text=${OWN_BOUNDARY_TEXT}`); - expect(await app.getHtml("main")).toMatch(OWN_BOUNDARY_TEXT); - }); + test("own boundary, action, client transition from other route", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickSubmitButton(HAS_BOUNDARY_ACTION); + await page.waitForSelector(`text=${OWN_BOUNDARY_TEXT}`); + expect(await app.getHtml("main")).toMatch(OWN_BOUNDARY_TEXT); + }); - test("own boundary, action, client transition from itself", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto(HAS_BOUNDARY_ACTION); - await app.clickSubmitButton(HAS_BOUNDARY_ACTION); - await page.waitForSelector(`text=${OWN_BOUNDARY_TEXT}`); - expect(await app.getHtml("main")).toMatch(OWN_BOUNDARY_TEXT); - }); + test("own boundary, action, client transition from itself", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto(HAS_BOUNDARY_ACTION); + await app.clickSubmitButton(HAS_BOUNDARY_ACTION); + await page.waitForSelector(`text=${OWN_BOUNDARY_TEXT}`); + expect(await app.getHtml("main")).toMatch(OWN_BOUNDARY_TEXT); + }); - test("bubbles to parent in action document requests", async () => { - let params = new URLSearchParams(); - let res = await fixture.postDocument(NO_BOUNDARY_ACTION, params); - expect(res.status).toBe(500); - expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); - }); + test("bubbles to parent in action document requests", async () => { + let params = new URLSearchParams(); + let res = await fixture.postDocument(NO_BOUNDARY_ACTION, params); + expect(res.status).toBe(500); + expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); + }); - test("bubbles to parent in action script transitions from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(NO_BOUNDARY_ACTION); - await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`); - expect(await app.getHtml("main")).toMatch(ROOT_BOUNDARY_TEXT); - }); + test("bubbles to parent in action script transitions from other routes", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickSubmitButton(NO_BOUNDARY_ACTION); + await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`); + expect(await app.getHtml("main")).toMatch(ROOT_BOUNDARY_TEXT); + }); - test("bubbles to parent in action script transitions from self", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto(NO_BOUNDARY_ACTION); - await app.clickSubmitButton(NO_BOUNDARY_ACTION); - await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`); - expect(await app.getHtml("main")).toMatch(ROOT_BOUNDARY_TEXT); - }); + test("bubbles to parent in action script transitions from self", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto(NO_BOUNDARY_ACTION); + await app.clickSubmitButton(NO_BOUNDARY_ACTION); + await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`); + expect(await app.getHtml("main")).toMatch(ROOT_BOUNDARY_TEXT); + }); - test("own boundary, loader, document request", async () => { - let res = await fixture.requestDocument(HAS_BOUNDARY_LOADER); - expect(res.status).toBe(500); - expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); - }); + test("own boundary, loader, document request", async () => { + let res = await fixture.requestDocument(HAS_BOUNDARY_LOADER); + expect(res.status).toBe(500); + expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); + }); - test("own boundary, loader, client transition", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(HAS_BOUNDARY_LOADER); - await page.waitForSelector(`text=${OWN_BOUNDARY_TEXT}`); - expect(await app.getHtml("main")).toMatch(OWN_BOUNDARY_TEXT); - }); + test("own boundary, loader, client transition", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickLink(HAS_BOUNDARY_LOADER); + await page.waitForSelector(`text=${OWN_BOUNDARY_TEXT}`); + expect(await app.getHtml("main")).toMatch(OWN_BOUNDARY_TEXT); + }); - test("bubbles to parent in loader document requests", async () => { - let res = await fixture.requestDocument(NO_BOUNDARY_LOADER); - expect(res.status).toBe(500); - expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); - }); + test("bubbles to parent in loader document requests", async () => { + let res = await fixture.requestDocument(NO_BOUNDARY_LOADER); + expect(res.status).toBe(500); + expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); + }); - test("bubbles to parent in loader script transitions from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(NO_BOUNDARY_LOADER); - await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`); - expect(await app.getHtml("main")).toMatch(ROOT_BOUNDARY_TEXT); - }); + test("bubbles to parent in loader script transitions from other routes", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickLink(NO_BOUNDARY_LOADER); + await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`); + expect(await app.getHtml("main")).toMatch(ROOT_BOUNDARY_TEXT); + }); - test("ssr rendering errors with no boundary", async () => { - let res = await fixture.requestDocument(NO_BOUNDARY_RENDER); - expect(res.status).toBe(500); - expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); - }); + test("ssr rendering errors with no boundary", async () => { + let res = await fixture.requestDocument(NO_BOUNDARY_RENDER); + expect(res.status).toBe(500); + expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); + }); - test("script transition rendering errors with no boundary", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(NO_BOUNDARY_RENDER); - await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`); - expect(await app.getHtml("main")).toMatch(ROOT_BOUNDARY_TEXT); - }); + test("script transition rendering errors with no boundary", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickLink(NO_BOUNDARY_RENDER); + await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`); + expect(await app.getHtml("main")).toMatch(ROOT_BOUNDARY_TEXT); + }); - test("ssr rendering errors with boundary", async () => { - let res = await fixture.requestDocument(HAS_BOUNDARY_RENDER); - expect(res.status).toBe(500); - expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); - }); + test("ssr rendering errors with boundary", async () => { + let res = await fixture.requestDocument(HAS_BOUNDARY_RENDER); + expect(res.status).toBe(500); + expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); + }); - test("script transition rendering errors with boundary", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(HAS_BOUNDARY_RENDER); - await page.waitForSelector(`text=${OWN_BOUNDARY_TEXT}`); - expect(await app.getHtml("main")).toMatch(OWN_BOUNDARY_TEXT); - }); + test("script transition rendering errors with boundary", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickLink(HAS_BOUNDARY_RENDER); + await page.waitForSelector(`text=${OWN_BOUNDARY_TEXT}`); + expect(await app.getHtml("main")).toMatch(OWN_BOUNDARY_TEXT); + }); - test("uses correct error boundary on server action errors in nested routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto(`/action/child-error`); - expect(await app.getHtml("#parent-data")).toMatch("PARENT"); - expect(await app.getHtml("#child-data")).toMatch("CHILD"); - await page.click("button[type=submit]"); - await page.waitForSelector("#child-error"); - // Preserves parent loader data - expect(await app.getHtml("#parent-data")).toMatch("PARENT"); - expect(await app.getHtml("#child-error")).toMatch("Broken!"); - }); + test("uses correct error boundary on server action errors in nested routes", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto(`/action/child-error`); + expect(await app.getHtml("#parent-data")).toMatch("PARENT"); + expect(await app.getHtml("#child-data")).toMatch("CHILD"); + await page.click("button[type=submit]"); + await page.waitForSelector("#child-error"); + // Preserves parent loader data + expect(await app.getHtml("#parent-data")).toMatch("PARENT"); + expect(await app.getHtml("#child-error")).toMatch("Broken!"); + }); - test("renders own boundary in fetcher action submission without action from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/fetcher-boundary"); - await app.clickSubmitButton(NO_BOUNDARY_NO_LOADER_OR_ACTION); - await page.waitForSelector("#fetcher-boundary"); - }); + test("renders own boundary in fetcher action submission without action from other routes", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/fetcher-boundary"); + await app.clickSubmitButton(NO_BOUNDARY_NO_LOADER_OR_ACTION); + await page.waitForSelector("#fetcher-boundary"); + }); - test("renders root boundary in fetcher action submission without action from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/fetcher-no-boundary"); - await app.clickSubmitButton(NO_BOUNDARY_NO_LOADER_OR_ACTION); - await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`); - }); + test("renders root boundary in fetcher action submission without action from other routes", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/fetcher-no-boundary"); + await app.clickSubmitButton(NO_BOUNDARY_NO_LOADER_OR_ACTION); + await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`); + }); - test("renders root boundary in document POST without action requests", async () => { - let res = await fixture.requestDocument(NO_BOUNDARY_NO_LOADER_OR_ACTION, { - method: "post", - }); - expect(res.status).toBe(405); - expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); + test("renders root boundary in document POST without action requests", async () => { + let res = await fixture.requestDocument(NO_BOUNDARY_NO_LOADER_OR_ACTION, { + method: "post", }); + expect(res.status).toBe(405); + expect(await res.text()).toMatch(ROOT_BOUNDARY_TEXT); + }); - test("renders root boundary in action script transitions without action from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(NO_BOUNDARY_NO_LOADER_OR_ACTION); - await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`); - }); + test("renders root boundary in action script transitions without action from other routes", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickSubmitButton(NO_BOUNDARY_NO_LOADER_OR_ACTION); + await page.waitForSelector(`text=${ROOT_BOUNDARY_TEXT}`); + }); - test("renders own boundary in document POST without action requests", async () => { - let res = await fixture.requestDocument( - HAS_BOUNDARY_NO_LOADER_OR_ACTION, - { - method: "post", - } - ); - expect(res.status).toBe(405); - expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); + test("renders own boundary in document POST without action requests", async () => { + let res = await fixture.requestDocument(HAS_BOUNDARY_NO_LOADER_OR_ACTION, { + method: "post", }); + expect(res.status).toBe(405); + expect(await res.text()).toMatch(OWN_BOUNDARY_TEXT); + }); - test("renders own boundary in action script transitions without action from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(HAS_BOUNDARY_NO_LOADER_OR_ACTION); - await page.waitForSelector("#boundary-no-loader-or-action"); - }); + test("renders own boundary in action script transitions without action from other routes", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickSubmitButton(HAS_BOUNDARY_NO_LOADER_OR_ACTION); + await page.waitForSelector("#boundary-no-loader-or-action"); + }); - test.describe("if no error boundary exists in the app", () => { - let NO_ROOT_BOUNDARY_LOADER = "/loader-bad" as const; - let NO_ROOT_BOUNDARY_ACTION = "/action-bad" as const; - let NO_ROOT_BOUNDARY_LOADER_RETURN = "/loader-no-return" as const; - let NO_ROOT_BOUNDARY_ACTION_RETURN = "/action-no-return" as const; + test.describe("if no error boundary exists in the app", () => { + let NO_ROOT_BOUNDARY_LOADER = "/loader-bad" as const; + let NO_ROOT_BOUNDARY_ACTION = "/action-bad" as const; + let NO_ROOT_BOUNDARY_LOADER_RETURN = "/loader-no-return" as const; + let NO_ROOT_BOUNDARY_ACTION_RETURN = "/action-no-return" as const; - test.beforeAll(async () => { - fixture = await createFixture({ - config: { - future: { - v2_routeConvention: true, - }, + test.beforeAll(async () => { + fixture = await createFixture({ + config: { + future: { + v2_routeConvention: true, }, - files: { - "app/root.jsx": js` + }, + files: { + "app/root.jsx": js` import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; export default function Root() { @@ -1893,7 +524,7 @@ test.describe("v2_errorBoundary", () => { } `, - "app/routes/_index.jsx": js` + "app/routes/_index.jsx": js` import { Link, Form } from "@remix-run/react"; export default function () { @@ -1914,7 +545,7 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${NO_ROOT_BOUNDARY_LOADER}.jsx`]: js` + [`app/routes${NO_ROOT_BOUNDARY_LOADER}.jsx`]: js` export async function loader() { throw Error("BLARGH"); } @@ -1928,7 +559,7 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${NO_ROOT_BOUNDARY_ACTION}.jsx`]: js` + [`app/routes${NO_ROOT_BOUNDARY_ACTION}.jsx`]: js` export async function action() { throw Error("YOOOOOOOO WHAT ARE YOU DOING"); } @@ -1942,7 +573,7 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${NO_ROOT_BOUNDARY_LOADER_RETURN}.jsx`]: js` + [`app/routes${NO_ROOT_BOUNDARY_LOADER_RETURN}.jsx`]: js` import { useLoaderData } from "@remix-run/react"; export async function loader() {} @@ -1957,7 +588,7 @@ test.describe("v2_errorBoundary", () => { } `, - [`app/routes${NO_ROOT_BOUNDARY_ACTION_RETURN}.jsx`]: js` + [`app/routes${NO_ROOT_BOUNDARY_ACTION_RETURN}.jsx`]: js` import { useActionData } from "@remix-run/react"; export async function action() {} @@ -1971,92 +602,80 @@ test.describe("v2_errorBoundary", () => { ) } `, - }, - }); - appFixture = await createAppFixture(fixture); + }, }); + appFixture = await createAppFixture(fixture); + }); - test("bubbles to internal boundary in loader document requests", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto(NO_ROOT_BOUNDARY_LOADER); - expect(await app.getHtml("h1")).toMatch( - INTERNAL_ERROR_BOUNDARY_HEADING - ); - }); + test("bubbles to internal boundary in loader document requests", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto(NO_ROOT_BOUNDARY_LOADER); + expect(await app.getHtml("h1")).toMatch(INTERNAL_ERROR_BOUNDARY_HEADING); + }); - test("bubbles to internal boundary in action script transitions from other routes", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(NO_ROOT_BOUNDARY_ACTION); - await page.waitForSelector(`text=${INTERNAL_ERROR_BOUNDARY_HEADING}`); - expect(await app.getHtml("h1")).toMatch( - INTERNAL_ERROR_BOUNDARY_HEADING - ); - }); + test("bubbles to internal boundary in action script transitions from other routes", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickSubmitButton(NO_ROOT_BOUNDARY_ACTION); + await page.waitForSelector(`text=${INTERNAL_ERROR_BOUNDARY_HEADING}`); + expect(await app.getHtml("h1")).toMatch(INTERNAL_ERROR_BOUNDARY_HEADING); + }); - test("bubbles to internal boundary if loader doesn't return (document requests)", async () => { - let res = await fixture.requestDocument(NO_ROOT_BOUNDARY_LOADER_RETURN); - expect(res.status).toBe(500); - expect(await res.text()).toMatch(INTERNAL_ERROR_BOUNDARY_HEADING); - }); + test("bubbles to internal boundary if loader doesn't return (document requests)", async () => { + let res = await fixture.requestDocument(NO_ROOT_BOUNDARY_LOADER_RETURN); + expect(res.status).toBe(500); + expect(await res.text()).toMatch(INTERNAL_ERROR_BOUNDARY_HEADING); + }); - test("bubbles to internal boundary if loader doesn't return", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink(NO_ROOT_BOUNDARY_LOADER_RETURN); - await page.waitForSelector(`text=${INTERNAL_ERROR_BOUNDARY_HEADING}`); - expect(await app.getHtml("h1")).toMatch( - INTERNAL_ERROR_BOUNDARY_HEADING - ); - }); + test("bubbles to internal boundary if loader doesn't return", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickLink(NO_ROOT_BOUNDARY_LOADER_RETURN); + await page.waitForSelector(`text=${INTERNAL_ERROR_BOUNDARY_HEADING}`); + expect(await app.getHtml("h1")).toMatch(INTERNAL_ERROR_BOUNDARY_HEADING); + }); - test("bubbles to internal boundary if action doesn't return (document requests)", async () => { - let res = await fixture.requestDocument( - NO_ROOT_BOUNDARY_ACTION_RETURN, - { - method: "post", - } - ); - expect(res.status).toBe(500); - expect(await res.text()).toMatch(INTERNAL_ERROR_BOUNDARY_HEADING); + test("bubbles to internal boundary if action doesn't return (document requests)", async () => { + let res = await fixture.requestDocument(NO_ROOT_BOUNDARY_ACTION_RETURN, { + method: "post", }); + expect(res.status).toBe(500); + expect(await res.text()).toMatch(INTERNAL_ERROR_BOUNDARY_HEADING); + }); - test("bubbles to internal boundary if action doesn't return", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickSubmitButton(NO_ROOT_BOUNDARY_ACTION_RETURN); - await page.waitForSelector(`text=${INTERNAL_ERROR_BOUNDARY_HEADING}`); - expect(await app.getHtml("h1")).toMatch( - INTERNAL_ERROR_BOUNDARY_HEADING - ); - }); + test("bubbles to internal boundary if action doesn't return", async ({ + page, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickSubmitButton(NO_ROOT_BOUNDARY_ACTION_RETURN); + await page.waitForSelector(`text=${INTERNAL_ERROR_BOUNDARY_HEADING}`); + expect(await app.getHtml("h1")).toMatch(INTERNAL_ERROR_BOUNDARY_HEADING); }); }); +}); - test.describe("loaderData in ErrorBoundary", () => { - let fixture: Fixture; - let appFixture: AppFixture; - let consoleErrors: string[]; - let oldConsoleError: () => void; +test.describe("loaderData in ErrorBoundary", () => { + let fixture: Fixture; + let appFixture: AppFixture; + let consoleErrors: string[]; + let oldConsoleError: () => void; - test.beforeAll(async () => { - fixture = await createFixture({ - config: { - future: { - v2_routeConvention: true, - v2_errorBoundary: true, - }, + test.beforeAll(async () => { + fixture = await createFixture({ + config: { + future: { + v2_routeConvention: true, }, - files: { - "app/root.jsx": js` + }, + files: { + "app/root.jsx": js` import { Links, Meta, Outlet, Scripts } from "@remix-run/react"; export default function Root() { @@ -2077,7 +696,7 @@ test.describe("v2_errorBoundary", () => { } `, - "app/routes/parent.jsx": js` + "app/routes/parent.jsx": js` import { Outlet, useLoaderData, useMatches, useRouteError } from "@remix-run/react"; export function loader() { @@ -2107,7 +726,7 @@ test.describe("v2_errorBoundary", () => { } `, - "app/routes/parent.child-with-boundary.jsx": js` + "app/routes/parent.child-with-boundary.jsx": js` import { Form, useLoaderData, useRouteError } from "@remix-run/react"; export function loader() { @@ -2142,7 +761,7 @@ test.describe("v2_errorBoundary", () => { } `, - "app/routes/parent.child-without-boundary.jsx": js` + "app/routes/parent.child-without-boundary.jsx": js` import { Form, useLoaderData } from "@remix-run/react"; export function loader() { @@ -2166,140 +785,140 @@ test.describe("v2_errorBoundary", () => { ) } `, - }, - }); - - appFixture = await createAppFixture(fixture, ServerMode.Development); + }, }); - test.afterAll(() => { - appFixture.close(); - }); + appFixture = await createAppFixture(fixture, ServerMode.Development); + }); - test.beforeEach(({ page }) => { - oldConsoleError = console.error; - console.error = () => {}; - consoleErrors = []; - // Listen for all console events and handle errors - page.on("console", (msg) => { - if (msg.type() === "error") { - consoleErrors.push(msg.text()); - } - }); - }); + test.afterAll(() => { + appFixture.close(); + }); - test.afterEach(() => { - console.error = oldConsoleError; + test.beforeEach(({ page }) => { + oldConsoleError = console.error; + console.error = () => {}; + consoleErrors = []; + // Listen for all console events and handle errors + page.on("console", (msg) => { + if (msg.type() === "error") { + consoleErrors.push(msg.text()); + } }); + }); - test.describe("without JavaScript", () => { - test.use({ javaScriptEnabled: false }); - runBoundaryTests(); - }); + test.afterEach(() => { + console.error = oldConsoleError; + }); + + test.describe("without JavaScript", () => { + test.use({ javaScriptEnabled: false }); + runBoundaryTests(); + }); + + test.describe("with JavaScript", () => { + test.use({ javaScriptEnabled: true }); + runBoundaryTests(); + }); + + function runBoundaryTests() { + test("Prevents useLoaderData in self ErrorBoundary", async ({ + page, + javaScriptEnabled, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/parent/child-with-boundary"); + + expect(await app.getHtml("#parent-data")).toEqual( + '

PARENT

' + ); + expect(await app.getHtml("#child-data")).toEqual( + '

CHILD

' + ); + expect(consoleErrors).toEqual([]); + + await app.clickSubmitButton("/parent/child-with-boundary"); + await page.waitForSelector("#child-error"); + + expect(await app.getHtml("#child-error")).toEqual( + '

Broken!

' + ); + expect(await app.getHtml("#parent-data")).toEqual( + '

PARENT

' + ); + expect(await app.getHtml("#child-data")).toEqual( + '

' + ); - test.describe("with JavaScript", () => { - test.use({ javaScriptEnabled: true }); - runBoundaryTests(); + // Only look for this message. Chromium browsers will also log the + // network error but firefox does not + // "Failed to load resource: the server responded with a status of 500 (Internal Server Error)", + let msg = + "You cannot `useLoaderData` in an errorElement (routeId: routes/parent.child-with-boundary)"; + if (javaScriptEnabled) { + expect(consoleErrors.filter((m) => m === msg)).toEqual([msg]); + } else { + // We don't get the useLoaderData message in the client when JS is disabled + expect(consoleErrors.filter((m) => m === msg)).toEqual([]); + } }); - function runBoundaryTests() { - test("Prevents useLoaderData in self ErrorBoundary", async ({ - page, - javaScriptEnabled, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/parent/child-with-boundary"); - - expect(await app.getHtml("#parent-data")).toEqual( - '

PARENT

' - ); - expect(await app.getHtml("#child-data")).toEqual( - '

CHILD

' - ); - expect(consoleErrors).toEqual([]); - - await app.clickSubmitButton("/parent/child-with-boundary"); - await page.waitForSelector("#child-error"); - - expect(await app.getHtml("#child-error")).toEqual( - '

Broken!

' - ); - expect(await app.getHtml("#parent-data")).toEqual( - '

PARENT

' - ); - expect(await app.getHtml("#child-data")).toEqual( - '

' - ); - - // Only look for this message. Chromium browsers will also log the - // network error but firefox does not - // "Failed to load resource: the server responded with a status of 500 (Internal Server Error)", - let msg = - "You cannot `useLoaderData` in an errorElement (routeId: routes/parent.child-with-boundary)"; - if (javaScriptEnabled) { - expect(consoleErrors.filter((m) => m === msg)).toEqual([msg]); - } else { - // We don't get the useLoaderData message in the client when JS is disabled - expect(consoleErrors.filter((m) => m === msg)).toEqual([]); - } - }); + test("Prevents useLoaderData in bubbled ErrorBoundary", async ({ + page, + javaScriptEnabled, + }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/parent/child-without-boundary"); - test("Prevents useLoaderData in bubbled ErrorBoundary", async ({ - page, - javaScriptEnabled, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/parent/child-without-boundary"); - - expect(await app.getHtml("#parent-data")).toEqual( - '

PARENT

' - ); - expect(await app.getHtml("#child-data")).toEqual( - '

CHILD

' - ); - expect(consoleErrors).toEqual([]); - - await app.clickSubmitButton("/parent/child-without-boundary"); - await page.waitForSelector("#parent-error"); - - expect(await app.getHtml("#parent-error")).toEqual( - '

Broken!

' - ); - expect(await app.getHtml("#parent-matches-data")).toEqual( - '

' - ); - expect(await app.getHtml("#parent-data")).toEqual( - '

' - ); - - // Only look for this message. Chromium browsers will also log the - // network error but firefox does not - // "Failed to load resource: the server responded with a status of 500 (Internal Server Error)", - let msg = - "You cannot `useLoaderData` in an errorElement (routeId: routes/parent)"; - if (javaScriptEnabled) { - expect(consoleErrors.filter((m) => m === msg)).toEqual([msg]); - } else { - // We don't get the useLoaderData message in the client when JS is disabled - expect(consoleErrors.filter((m) => m === msg)).toEqual([]); - } - }); - } - }); + expect(await app.getHtml("#parent-data")).toEqual( + '

PARENT

' + ); + expect(await app.getHtml("#child-data")).toEqual( + '

CHILD

' + ); + expect(consoleErrors).toEqual([]); + + await app.clickSubmitButton("/parent/child-without-boundary"); + await page.waitForSelector("#parent-error"); + + expect(await app.getHtml("#parent-error")).toEqual( + '

Broken!

' + ); + expect(await app.getHtml("#parent-matches-data")).toEqual( + '

' + ); + expect(await app.getHtml("#parent-data")).toEqual( + '

' + ); + + // Only look for this message. Chromium browsers will also log the + // network error but firefox does not + // "Failed to load resource: the server responded with a status of 500 (Internal Server Error)", + let msg = + "You cannot `useLoaderData` in an errorElement (routeId: routes/parent)"; + if (javaScriptEnabled) { + expect(consoleErrors.filter((m) => m === msg)).toEqual([msg]); + } else { + // We don't get the useLoaderData message in the client when JS is disabled + expect(consoleErrors.filter((m) => m === msg)).toEqual([]); + } + }); + } +}); + +test.describe("Default ErrorBoundary", () => { + let fixture: Fixture; + let appFixture: AppFixture; + let _consoleError: any; - test.describe("Default ErrorBoundary", () => { - let fixture: Fixture; - let appFixture: AppFixture; - let _consoleError: any; - - function getFiles({ - includeRootErrorBoundary = false, - rootErrorBoundaryThrows = false, - } = {}) { - let errorBoundaryCode = !includeRootErrorBoundary - ? "" - : rootErrorBoundaryThrows - ? js` + function getFiles({ + includeRootErrorBoundary = false, + rootErrorBoundaryThrows = false, + } = {}) { + let errorBoundaryCode = !includeRootErrorBoundary + ? "" + : rootErrorBoundaryThrows + ? js` export function ErrorBoundary() { let error = useRouteError(); return ( @@ -2317,7 +936,7 @@ test.describe("v2_errorBoundary", () => { ) } ` - : js` + : js` export function ErrorBoundary() { let error = useRouteError(); return ( @@ -2335,8 +954,8 @@ test.describe("v2_errorBoundary", () => { } `; - return { - "app/root.jsx": js` + return { + "app/root.jsx": js` import { Links, Meta, Outlet, Scripts, useRouteError } from "@remix-run/react"; export default function Root() { @@ -2359,7 +978,7 @@ test.describe("v2_errorBoundary", () => { ${errorBoundaryCode} `, - "app/routes/_index.jsx": js` + "app/routes/_index.jsx": js` import { Link } from "@remix-run/react"; export default function () { return ( @@ -2372,281 +991,279 @@ test.describe("v2_errorBoundary", () => { } `, - "app/routes/loader-error.jsx": js` - export function loader() { - throw new Error('Loader Error'); - } - export default function () { - return

Loader Error

- } - `, + "app/routes/loader-error.jsx": js` + export function loader() { + throw new Error('Loader Error'); + } + export default function () { + return

Loader Error

+ } + `, + + "app/routes/render-error.jsx": js` + export default function () { + throw new Error("Render Error") + } + `, + }; + } + + test.beforeAll(async () => { + _consoleError = console.error; + console.error = () => {}; + }); + + test.afterAll(async () => { + console.error = _consoleError; + appFixture.close(); + }); + + test.describe("When the root route does not have a boundary", () => { + test.beforeAll(async () => { + fixture = await createFixture( + { + config: { + future: { + v2_routeConvention: true, + }, + }, + files: getFiles({ includeRootErrorBoundary: false }), + }, + ServerMode.Development + ); + appFixture = await createAppFixture(fixture, ServerMode.Development); + }); + + test.afterAll(() => appFixture.close()); + + test.describe("document requests", () => { + test("renders default boundary on loader errors", async () => { + let res = await fixture.requestDocument("/loader-error"); + expect(res.status).toBe(500); + let text = await res.text(); + expect(text).toMatch("Application Error"); + expect(text).toMatch("Loader Error"); + expect(text).not.toMatch("Root Error Boundary"); + }); + + test("renders default boundary on render errors", async () => { + let res = await fixture.requestDocument("/render-error"); + expect(res.status).toBe(500); + let text = await res.text(); + expect(text).toMatch("Application Error"); + expect(text).toMatch("Render Error"); + expect(text).not.toMatch("Root Error Boundary"); + }); + }); + + test.describe("SPA navigations", () => { + test("renders default boundary on loader errors", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickLink("/loader-error"); + await page.waitForSelector("pre"); + let html = await app.getHtml(); + expect(html).toMatch("Application Error"); + expect(html).toMatch("Loader Error"); + expect(html).not.toMatch("Root Error Boundary"); - "app/routes/render-error.jsx": js` - export default function () { - throw new Error("Render Error") - } - `, - }; - } + // Ensure we can click back to our prior page + await app.goBack(); + await page.waitForSelector("h1#index"); + }); - test.beforeAll(async () => { - _consoleError = console.error; - console.error = () => {}; - }); + test("renders default boundary on render errors", async ({ + page, + }, workerInfo) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickLink("/render-error"); + await page.waitForSelector("pre"); + let html = await app.getHtml(); + expect(html).toMatch("Application Error"); + // Chromium seems to be the only one that includes the message in the stack + if (workerInfo.project.name === "chromium") { + expect(html).toMatch("Render Error"); + } + expect(html).not.toMatch("Root Error Boundary"); - test.afterAll(async () => { - console.error = _consoleError; - appFixture.close(); + // Ensure we can click back to our prior page + await app.goBack(); + await page.waitForSelector("h1#index"); + }); }); + }); - test.describe("When the root route does not have a boundary", () => { - test.beforeAll(async () => { - fixture = await createFixture( - { - config: { - future: { - v2_routeConvention: true, - v2_errorBoundary: true, - }, + test.describe("When the root route has a boundary", () => { + test.beforeAll(async () => { + fixture = await createFixture( + { + config: { + future: { + v2_routeConvention: true, }, - files: getFiles({ includeRootErrorBoundary: false }), }, - ServerMode.Development - ); - appFixture = await createAppFixture(fixture, ServerMode.Development); + files: getFiles({ includeRootErrorBoundary: true }), + }, + ServerMode.Development + ); + appFixture = await createAppFixture(fixture, ServerMode.Development); + }); + + test.afterAll(() => appFixture.close()); + + test.describe("document requests", () => { + test("renders root boundary on loader errors", async () => { + let res = await fixture.requestDocument("/loader-error"); + expect(res.status).toBe(500); + let text = await res.text(); + expect(text).toMatch("Root Error Boundary"); + expect(text).toMatch("Loader Error"); + expect(text).not.toMatch("Application Error"); }); - test.afterAll(() => appFixture.close()); - - test.describe("document requests", () => { - test("renders default boundary on loader errors", async () => { - let res = await fixture.requestDocument("/loader-error"); - expect(res.status).toBe(500); - let text = await res.text(); - expect(text).toMatch("Application Error"); - expect(text).toMatch("Loader Error"); - expect(text).not.toMatch("Root Error Boundary"); - }); - - test("renders default boundary on render errors", async () => { - let res = await fixture.requestDocument("/render-error"); - expect(res.status).toBe(500); - let text = await res.text(); - expect(text).toMatch("Application Error"); - expect(text).toMatch("Render Error"); - expect(text).not.toMatch("Root Error Boundary"); - }); + test("renders root boundary on render errors", async () => { + let res = await fixture.requestDocument("/render-error"); + expect(res.status).toBe(500); + let text = await res.text(); + expect(text).toMatch("Root Error Boundary"); + expect(text).toMatch("Render Error"); + expect(text).not.toMatch("Application Error"); }); + }); - test.describe("SPA navigations", () => { - test("renders default boundary on loader errors", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/loader-error"); - await page.waitForSelector("pre"); - let html = await app.getHtml(); - expect(html).toMatch("Application Error"); - expect(html).toMatch("Loader Error"); - expect(html).not.toMatch("Root Error Boundary"); - - // Ensure we can click back to our prior page - await app.goBack(); - await page.waitForSelector("h1#index"); - }); - - test("renders default boundary on render errors", async ({ - page, - }, workerInfo) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/render-error"); - await page.waitForSelector("pre"); - let html = await app.getHtml(); - expect(html).toMatch("Application Error"); - // Chromium seems to be the only one that includes the message in the stack - if (workerInfo.project.name === "chromium") { - expect(html).toMatch("Render Error"); - } - expect(html).not.toMatch("Root Error Boundary"); + test.describe("SPA navigations", () => { + test("renders root boundary on loader errors", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickLink("/loader-error"); + await page.waitForSelector("#root-error-boundary"); + let html = await app.getHtml(); + expect(html).toMatch("Root Error Boundary"); + expect(html).toMatch("Loader Error"); + expect(html).not.toMatch("Application Error"); - // Ensure we can click back to our prior page - await app.goBack(); - await page.waitForSelector("h1#index"); - }); + // Ensure we can click back to our prior page + await app.goBack(); + await page.waitForSelector("h1#index"); + }); + + test("renders root boundary on render errors", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickLink("/render-error"); + await page.waitForSelector("#root-error-boundary"); + let html = await app.getHtml(); + expect(html).toMatch("Root Error Boundary"); + expect(html).toMatch("Render Error"); + expect(html).not.toMatch("Application Error"); + + // Ensure we can click back to our prior page + await app.goBack(); + await page.waitForSelector("h1#index"); }); }); + }); - test.describe("When the root route has a boundary", () => { - test.beforeAll(async () => { - fixture = await createFixture( - { - config: { - future: { - v2_routeConvention: true, - }, - }, - files: getFiles({ includeRootErrorBoundary: true }), + test.describe("When the root route has a boundary but it also throws 😦", () => { + test.beforeAll(async () => { + fixture = await createFixture({ + config: { + future: { + v2_routeConvention: true, }, - ServerMode.Development - ); - appFixture = await createAppFixture(fixture, ServerMode.Development); + }, + files: getFiles({ + includeRootErrorBoundary: true, + rootErrorBoundaryThrows: true, + }), }); + appFixture = await createAppFixture(fixture, ServerMode.Development); + }); - test.afterAll(() => appFixture.close()); - - test.describe("document requests", () => { - test("renders root boundary on loader errors", async () => { - let res = await fixture.requestDocument("/loader-error"); - expect(res.status).toBe(500); - let text = await res.text(); - expect(text).toMatch("Root Error Boundary"); - expect(text).toMatch("Loader Error"); - expect(text).not.toMatch("Application Error"); - }); - - test("renders root boundary on render errors", async () => { - let res = await fixture.requestDocument("/render-error"); - expect(res.status).toBe(500); - let text = await res.text(); - expect(text).toMatch("Root Error Boundary"); - expect(text).toMatch("Render Error"); - expect(text).not.toMatch("Application Error"); - }); - }); + test.afterAll(() => appFixture.close()); - test.describe("SPA navigations", () => { - test("renders root boundary on loader errors", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/loader-error"); - await page.waitForSelector("#root-error-boundary"); - let html = await app.getHtml(); - expect(html).toMatch("Root Error Boundary"); - expect(html).toMatch("Loader Error"); - expect(html).not.toMatch("Application Error"); - - // Ensure we can click back to our prior page - await app.goBack(); - await page.waitForSelector("h1#index"); - }); - - test("renders root boundary on render errors", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/render-error"); - await page.waitForSelector("#root-error-boundary"); - let html = await app.getHtml(); - expect(html).toMatch("Root Error Boundary"); - expect(html).toMatch("Render Error"); - expect(html).not.toMatch("Application Error"); + test.describe("document requests", () => { + test("tries to render root boundary on loader errors but bubbles to default boundary", async () => { + let res = await fixture.requestDocument("/loader-error"); + expect(res.status).toBe(500); + let text = await res.text(); + expect(text).toMatch("Unexpected Server Error"); + expect(text).not.toMatch("Application Error"); + expect(text).not.toMatch("Loader Error"); + expect(text).not.toMatch("Root Error Boundary"); + }); - // Ensure we can click back to our prior page - await app.goBack(); - await page.waitForSelector("h1#index"); - }); + test("tries to render root boundary on render errors but bubbles to default boundary", async () => { + let res = await fixture.requestDocument("/render-error"); + expect(res.status).toBe(500); + let text = await res.text(); + expect(text).toMatch("Unexpected Server Error"); + expect(text).not.toMatch("Application Error"); + expect(text).not.toMatch("Render Error"); + expect(text).not.toMatch("Root Error Boundary"); }); }); - test.describe("When the root route has a boundary but it also throws 😦", () => { - test.beforeAll(async () => { - fixture = await createFixture({ - config: { - future: { - v2_routeConvention: true, - }, - }, - files: getFiles({ - includeRootErrorBoundary: true, - rootErrorBoundaryThrows: true, - }), - }); - appFixture = await createAppFixture(fixture, ServerMode.Development); - }); + test.describe("SPA navigations", () => { + test("tries to render root boundary on loader errors but bubbles to default boundary", async ({ + page, + }, workerInfo) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickLink("/loader-error"); + await page.waitForSelector("pre"); + let html = await app.getHtml(); + expect(html).toMatch("Application Error"); + if (workerInfo.project.name === "chromium") { + expect(html).toMatch("ReferenceError: oh is not defined"); + } + expect(html).not.toMatch("Loader Error"); + expect(html).not.toMatch("Root Error Boundary"); - test.afterAll(() => appFixture.close()); - - test.describe("document requests", () => { - test("tries to render root boundary on loader errors but bubbles to default boundary", async () => { - let res = await fixture.requestDocument("/loader-error"); - expect(res.status).toBe(500); - let text = await res.text(); - expect(text).toMatch("Unexpected Server Error"); - expect(text).not.toMatch("Application Error"); - expect(text).not.toMatch("Loader Error"); - expect(text).not.toMatch("Root Error Boundary"); - }); - - test("tries to render root boundary on render errors but bubbles to default boundary", async () => { - let res = await fixture.requestDocument("/render-error"); - expect(res.status).toBe(500); - let text = await res.text(); - expect(text).toMatch("Unexpected Server Error"); - expect(text).not.toMatch("Application Error"); - expect(text).not.toMatch("Render Error"); - expect(text).not.toMatch("Root Error Boundary"); - }); + // Ensure we can click back to our prior page + await app.goBack(); + await page.waitForSelector("h1#index"); }); - test.describe("SPA navigations", () => { - test("tries to render root boundary on loader errors but bubbles to default boundary", async ({ - page, - }, workerInfo) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/loader-error"); - await page.waitForSelector("pre"); - let html = await app.getHtml(); - expect(html).toMatch("Application Error"); - if (workerInfo.project.name === "chromium") { - expect(html).toMatch("ReferenceError: oh is not defined"); - } - expect(html).not.toMatch("Loader Error"); - expect(html).not.toMatch("Root Error Boundary"); - - // Ensure we can click back to our prior page - await app.goBack(); - await page.waitForSelector("h1#index"); - }); - - test("tries to render root boundary on render errors but bubbles to default boundary", async ({ - page, - }, workerInfo) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/render-error"); - await page.waitForSelector("pre"); - let html = await app.getHtml(); - expect(html).toMatch("Application Error"); - if (workerInfo.project.name === "chromium") { - expect(html).toMatch("ReferenceError: oh is not defined"); - } - expect(html).not.toMatch("Render Error"); - expect(html).not.toMatch("Root Error Boundary"); + test("tries to render root boundary on render errors but bubbles to default boundary", async ({ + page, + }, workerInfo) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + await app.clickLink("/render-error"); + await page.waitForSelector("pre"); + let html = await app.getHtml(); + expect(html).toMatch("Application Error"); + if (workerInfo.project.name === "chromium") { + expect(html).toMatch("ReferenceError: oh is not defined"); + } + expect(html).not.toMatch("Render Error"); + expect(html).not.toMatch("Root Error Boundary"); - // Ensure we can click back to our prior page - await app.goBack(); - await page.waitForSelector("h1#index"); - }); + // Ensure we can click back to our prior page + await app.goBack(); + await page.waitForSelector("h1#index"); }); }); }); +}); - test("Allows back-button out of an error boundary after a hard reload", async ({ - page, - browserName, - }) => { - let _consoleError = console.error; - console.error = () => {}; +test("Allows back-button out of an error boundary after a hard reload", async ({ + page, + browserName, +}) => { + let _consoleError = console.error; + console.error = () => {}; - let fixture = await createFixture({ - config: { - future: { - v2_routeConvention: true, - v2_errorBoundary: true, - }, + let fixture = await createFixture({ + config: { + future: { + v2_routeConvention: true, }, - files: { - "app/root.jsx": js` + }, + files: { + "app/root.jsx": js` import { Links, Meta, Outlet, Scripts, useRouteError } from "@remix-run/react"; export default function App() { @@ -2681,7 +1298,7 @@ test.describe("v2_errorBoundary", () => { ); } `, - "app/routes/_index.jsx": js` + "app/routes/_index.jsx": js` import { Link } from "@remix-run/react"; export default function Index() { @@ -2694,53 +1311,52 @@ test.describe("v2_errorBoundary", () => { } `, - "app/routes/boom.jsx": js` + "app/routes/boom.jsx": js` import { json } from "@remix-run/node"; export function loader() { return boom(); } export default function() { return my page; } `, - }, - }); + }, + }); - let appFixture = await createAppFixture(fixture); - let app = new PlaywrightFixture(appFixture, page); + let appFixture = await createAppFixture(fixture); + let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await page.waitForSelector("#index"); - expect(app.page.url()).not.toMatch("/boom"); + await app.goto("/"); + await page.waitForSelector("#index"); + expect(app.page.url()).not.toMatch("/boom"); + + await app.clickLink("/boom"); + await page.waitForSelector("#error"); + expect(app.page.url()).toMatch("/boom"); - await app.clickLink("/boom"); - await page.waitForSelector("#error"); - expect(app.page.url()).toMatch("/boom"); + await app.reload(); + await page.waitForSelector("#error"); + expect(app.page.url()).toMatch("boom"); - await app.reload(); - await page.waitForSelector("#error"); - expect(app.page.url()).toMatch("boom"); + await app.goBack(); + // Here be dragons + // - Playwright sets the Firefox `fission.webContentIsolationStrategy=0` preference + // for reasons having to do with out-of-process iframes: + // https://github.com/microsoft/playwright/issues/22640#issuecomment-1543287282 + // - That preference exposes a bug in firefox where a hard reload adds to the + // history stack: https://bugzilla.mozilla.org/show_bug.cgi?id=1832341 + // - Your can disable this preference via the Playwright `firefoxUserPrefs` config, + // but that is broken until 1.34: + // https://github.com/microsoft/playwright/issues/22640#issuecomment-1546230104 + // https://github.com/microsoft/playwright/issues/15405 + // - We can't yet upgrade to 1.34 because it drops support for Node 14: + // https://github.com/microsoft/playwright/releases/tag/v1.34.0 + // + // So for now when in firefox we just navigate back twice to work around the issue + if (browserName === "firefox") { await app.goBack(); + } - // Here be dragons - // - Playwright sets the Firefox `fission.webContentIsolationStrategy=0` preference - // for reasons having to do with out-of-process iframes: - // https://github.com/microsoft/playwright/issues/22640#issuecomment-1543287282 - // - That preference exposes a bug in firefox where a hard reload adds to the - // history stack: https://bugzilla.mozilla.org/show_bug.cgi?id=1832341 - // - Your can disable this preference via the Playwright `firefoxUserPrefs` config, - // but that is broken until 1.34: - // https://github.com/microsoft/playwright/issues/22640#issuecomment-1546230104 - // https://github.com/microsoft/playwright/issues/15405 - // - We can't yet upgrade to 1.34 because it drops support for Node 14: - // https://github.com/microsoft/playwright/releases/tag/v1.34.0 - // - // So for now when in firefox we just navigate back twice to work around the issue - if (browserName === "firefox") { - await app.goBack(); - } - - await page.waitForSelector("#index"); - expect(app.page.url()).not.toContain("boom"); + await page.waitForSelector("#index"); + expect(app.page.url()).not.toContain("boom"); - appFixture.close(); - console.error = _consoleError; - }); + appFixture.close(); + console.error = _consoleError; }); diff --git a/integration/error-boundary-v2-test.ts b/integration/error-boundary-v2-test.ts index 9e972418a58..8d76adc5a4f 100644 --- a/integration/error-boundary-v2-test.ts +++ b/integration/error-boundary-v2-test.ts @@ -6,7 +6,7 @@ import { createAppFixture, createFixture, js } from "./helpers/create-fixture"; import type { Fixture, AppFixture } from "./helpers/create-fixture"; import { PlaywrightFixture } from "./helpers/playwright-fixture"; -test.describe("V2 Singular ErrorBoundary (future.v2_errorBoundary)", () => { +test.describe("ErrorBoundary", () => { let fixture: Fixture; let appFixture: AppFixture; let oldConsoleError: () => void; @@ -15,7 +15,6 @@ test.describe("V2 Singular ErrorBoundary (future.v2_errorBoundary)", () => { fixture = await createFixture({ config: { future: { - v2_errorBoundary: true, v2_routeConvention: true, }, }, diff --git a/integration/error-sanitization-test.ts b/integration/error-sanitization-test.ts index a575557469b..8f356e59e03 100644 --- a/integration/error-sanitization-test.ts +++ b/integration/error-sanitization-test.ts @@ -153,9 +153,7 @@ test.describe("Error Sanitization", () => { fixture = await createFixture( { config: { - future: { - v2_errorBoundary: true, - }, + future: {}, }, files: routeFiles, }, @@ -307,9 +305,7 @@ test.describe("Error Sanitization", () => { fixture = await createFixture( { config: { - future: { - v2_errorBoundary: true, - }, + future: {}, }, files: routeFiles, }, @@ -477,9 +473,7 @@ test.describe("Error Sanitization", () => { fixture = await createFixture( { config: { - future: { - v2_errorBoundary: true, - }, + future: {}, }, files: { "app/entry.server.tsx": js` diff --git a/integration/headers-test.ts b/integration/headers-test.ts index a34dfe72854..17955377815 100644 --- a/integration/headers-test.ts +++ b/integration/headers-test.ts @@ -18,7 +18,7 @@ test.describe("headers export", () => { config: { future: { v2_routeConvention: true, - v2_errorBoundary: true, + v2_headers: true, }, }, @@ -439,7 +439,7 @@ test.describe("v1 behavior (future.v2_headers=false)", () => { config: { future: { v2_routeConvention: true, - v2_errorBoundary: true, + v2_headers: false, }, }, diff --git a/integration/hmr-log-test.ts b/integration/hmr-log-test.ts index 589c2b54ad1..4dc69a71426 100644 --- a/integration/hmr-log-test.ts +++ b/integration/hmr-log-test.ts @@ -19,7 +19,7 @@ let fixture = (options: { appPort: number; devPort: number }): FixtureInit => ({ port: options.devPort, }, v2_routeConvention: true, - v2_errorBoundary: true, + v2_meta: true, v2_headers: true, }, diff --git a/integration/hmr-test.ts b/integration/hmr-test.ts index b58e7cc037a..476040777ba 100644 --- a/integration/hmr-test.ts +++ b/integration/hmr-test.ts @@ -19,7 +19,7 @@ let fixture = (options: { appPort: number; devPort: number }): FixtureInit => ({ port: options.devPort, }, v2_routeConvention: true, - v2_errorBoundary: true, + v2_meta: true, v2_headers: true, }, diff --git a/integration/link-test.ts b/integration/link-test.ts index 2f279cafdfb..b301a0fb4e7 100644 --- a/integration/link-test.ts +++ b/integration/link-test.ts @@ -35,7 +35,6 @@ test.describe("route module link export", () => { config: { future: { v2_routeConvention: true, - v2_errorBoundary: true, }, }, files: { diff --git a/integration/resource-routes-test.ts b/integration/resource-routes-test.ts index 84b4f670ec3..1282200a86a 100644 --- a/integration/resource-routes-test.ts +++ b/integration/resource-routes-test.ts @@ -243,7 +243,6 @@ test.describe("Development server", async () => { config: { future: { v2_routeConvention: true, - v2_errorBoundary: true, }, }, files: { diff --git a/packages/remix-cloudflare/index.ts b/packages/remix-cloudflare/index.ts index 36910883313..35fb287eb9b 100644 --- a/packages/remix-cloudflare/index.ts +++ b/packages/remix-cloudflare/index.ts @@ -37,7 +37,6 @@ export type { CookieSignatureOptions, DataFunctionArgs, EntryContext, - ErrorBoundaryComponent, HandleDataRequestFunction, HandleDocumentRequestFunction, HeadersArgs, @@ -56,7 +55,6 @@ export type { HandleErrorFunction, PageLinkDescriptor, RequestHandler, - RouteComponent, RouteHandle, SerializeFrom, ServerBuild, diff --git a/packages/remix-deno/index.ts b/packages/remix-deno/index.ts index e426df4653d..40346581e68 100644 --- a/packages/remix-deno/index.ts +++ b/packages/remix-deno/index.ts @@ -40,7 +40,6 @@ export type { CookieSignatureOptions, DataFunctionArgs, EntryContext, - ErrorBoundaryComponent, HandleDataRequestFunction, HandleDocumentRequestFunction, HandleErrorFunction, @@ -59,7 +58,6 @@ export type { MetaFunction, PageLinkDescriptor, RequestHandler, - RouteComponent, RouteHandle, SerializeFrom, ServerBuild, diff --git a/packages/remix-dev/__tests__/readConfig-test.ts b/packages/remix-dev/__tests__/readConfig-test.ts index a290e99a73d..580b2db0ad2 100644 --- a/packages/remix-dev/__tests__/readConfig-test.ts +++ b/packages/remix-dev/__tests__/readConfig-test.ts @@ -27,7 +27,6 @@ describe("readConfig", () => { future: { unstable_postcss: expect.any(Boolean), unstable_tailwind: expect.any(Boolean), - v2_errorBoundary: expect.any(Boolean), v2_headers: expect.any(Boolean), v2_meta: expect.any(Boolean), v2_routeConvention: expect.any(Boolean), @@ -48,7 +47,6 @@ describe("readConfig", () => { "unstable_postcss": Any, "unstable_tailwind": Any, "v2_dev": false, - "v2_errorBoundary": Any, "v2_headers": Any, "v2_meta": Any, "v2_routeConvention": Any, diff --git a/packages/remix-dev/compiler/js/plugins/routes.ts b/packages/remix-dev/compiler/js/plugins/routes.ts index d94f3f71792..c9891585a0d 100644 --- a/packages/remix-dev/compiler/js/plugins/routes.ts +++ b/packages/remix-dev/compiler/js/plugins/routes.ts @@ -9,7 +9,6 @@ import type { Context } from "../../context"; type Route = RemixConfig["routes"][string]; const browserSafeRouteExports: { [name: string]: boolean } = { - CatchBoundary: true, ErrorBoundary: true, default: true, handle: true, diff --git a/packages/remix-dev/compiler/manifest.ts b/packages/remix-dev/compiler/manifest.ts index e803cd93a2c..ef29bf2581c 100644 --- a/packages/remix-dev/compiler/manifest.ts +++ b/packages/remix-dev/compiler/manifest.ts @@ -98,7 +98,6 @@ export async function create({ imports: resolveImports(output.imports), hasAction: sourceExports.includes("action"), hasLoader: sourceExports.includes("loader"), - hasCatchBoundary: sourceExports.includes("CatchBoundary"), hasErrorBoundary: sourceExports.includes("ErrorBoundary"), }; } diff --git a/packages/remix-dev/config.ts b/packages/remix-dev/config.ts index 64ad86f0804..e076acd4204 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -49,7 +49,6 @@ interface FutureConfig { unstable_postcss: boolean; /** @deprecated Use the `tailwind` config option instead */ unstable_tailwind: boolean; - v2_errorBoundary: boolean; v2_headers: boolean; v2_meta: boolean; v2_routeConvention: boolean; @@ -417,10 +416,6 @@ export async function readConfig( } } - if (!appConfig.future?.v2_errorBoundary) { - errorBoundaryWarning(); - } - if (!appConfig.future?.v2_meta) { metaWarning(); } @@ -811,7 +806,6 @@ export async function readConfig( v2_dev: appConfig.future?.v2_dev ?? false, unstable_postcss: appConfig.future?.unstable_postcss === true, unstable_tailwind: appConfig.future?.unstable_tailwind === true, - v2_errorBoundary: appConfig.future?.v2_errorBoundary === true, v2_headers: appConfig.future?.v2_headers === true, v2_meta: appConfig.future?.v2_meta === true, v2_routeConvention: appConfig.future?.v2_routeConvention === true, @@ -992,12 +986,6 @@ let flatRoutesWarning = futureFlagWarning({ link: "https://remix.run/docs/en/v1.15.0/pages/v2#file-system-route-convention", }); -let errorBoundaryWarning = futureFlagWarning({ - message: "The `CatchBoundary` and `ErrorBoundary` API is changing in v2", - flag: "v2_errorBoundary", - link: "https://remix.run/docs/en/v1.15.0/pages/v2#catchboundary-and-errorboundary", -}); - let metaWarning = futureFlagWarning({ message: "The route `meta` API is changing in v2", flag: "v2_meta", diff --git a/packages/remix-dev/manifest.ts b/packages/remix-dev/manifest.ts index d1572979e78..f72beae48e9 100644 --- a/packages/remix-dev/manifest.ts +++ b/packages/remix-dev/manifest.ts @@ -16,7 +16,6 @@ export type Manifest = { imports?: string[]; hasAction: boolean; hasLoader: boolean; - hasCatchBoundary: boolean; hasErrorBoundary: boolean; }; }; diff --git a/packages/remix-node/index.ts b/packages/remix-node/index.ts index f16ec506627..c42cd61c626 100644 --- a/packages/remix-node/index.ts +++ b/packages/remix-node/index.ts @@ -63,7 +63,6 @@ export type { CookieSignatureOptions, DataFunctionArgs, EntryContext, - ErrorBoundaryComponent, HandleDataRequestFunction, HandleDocumentRequestFunction, HeadersArgs, @@ -82,7 +81,6 @@ export type { HandleErrorFunction, PageLinkDescriptor, RequestHandler, - RouteComponent, RouteHandle, SerializeFrom, ServerBuild, diff --git a/packages/remix-react/__tests__/components-test.tsx b/packages/remix-react/__tests__/components-test.tsx index 748f4b12cf9..616a7de3c19 100644 --- a/packages/remix-react/__tests__/components-test.tsx +++ b/packages/remix-react/__tests__/components-test.tsx @@ -92,7 +92,6 @@ function itPrefetchesPageLinks< idk: { hasLoader: true, hasAction: false, - hasCatchBoundary: false, hasErrorBoundary: false, id: "idk", module: "idk.js", diff --git a/packages/remix-react/__tests__/deferred-scripts-test.tsx b/packages/remix-react/__tests__/deferred-scripts-test.tsx index b20a31d89ea..d52a64d2f60 100644 --- a/packages/remix-react/__tests__/deferred-scripts-test.tsx +++ b/packages/remix-react/__tests__/deferred-scripts-test.tsx @@ -34,7 +34,6 @@ describe(" with activeDeferreds", () => { root: { hasLoader: false, hasAction: false, - hasCatchBoundary: false, hasErrorBoundary: false, id: "root", module: "root.js", diff --git a/packages/remix-react/__tests__/scroll-restoration-test.tsx b/packages/remix-react/__tests__/scroll-restoration-test.tsx index 7574b5874ab..e2f1e8f9524 100644 --- a/packages/remix-react/__tests__/scroll-restoration-test.tsx +++ b/packages/remix-react/__tests__/scroll-restoration-test.tsx @@ -35,7 +35,6 @@ describe("", () => { root: { hasLoader: false, hasAction: false, - hasCatchBoundary: false, hasErrorBoundary: false, id: "root", module: "root.js", diff --git a/packages/remix-react/browser.tsx b/packages/remix-react/browser.tsx index c877d25d180..f628ef329a7 100644 --- a/packages/remix-react/browser.tsx +++ b/packages/remix-react/browser.tsx @@ -5,10 +5,7 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom"; import { RemixContext } from "./components"; import type { EntryContext, FutureConfig } from "./entry"; -import { - RemixErrorBoundary, - RemixRootDefaultErrorBoundary, -} from "./errorBoundaries"; +import { RemixErrorBoundary } from "./errorBoundaries"; import { deserializeErrors } from "./errors"; import type { RouteModules } from "./routeModules"; import { @@ -120,10 +117,6 @@ if (import.meta && import.meta.hot) { ? window.__remixRouteModules[id]?.default ?? imported.default : imported.default, - CatchBoundary: imported.CatchBoundary - ? window.__remixRouteModules[id]?.CatchBoundary ?? - imported.CatchBoundary - : imported.CatchBoundary, ErrorBoundary: imported.ErrorBoundary ? window.__remixRouteModules[id]?.ErrorBoundary ?? imported.ErrorBoundary @@ -242,10 +235,7 @@ export function RemixBrowser(_props: RemixBrowserProps): ReactElement { future: window.__remixContext.future, }} > - + ; - } - throw error; - } - - // Provide defaults for the root route if they are not present - if (id === "root") { - CatchBoundary ||= RemixRootDefaultCatchBoundary; - ErrorBoundary ||= RemixRootDefaultErrorBoundary; - } - - if (isRouteErrorResponse(error)) { - let tError = error; - if (!!tError?.error && tError.status !== 404 && ErrorBoundary) { - // Internal framework-thrown ErrorResponses - return ; - } - if (CatchBoundary) { - // User-thrown ErrorResponses - return ; - } - } + let { ErrorBoundary } = routeModules[id]; - if (error instanceof Error && ErrorBoundary) { - // User- or framework-thrown Errors - return ; + if (ErrorBoundary) { + return ; + } else if (id === "root") { + return ; } throw error; diff --git a/packages/remix-react/entry.ts b/packages/remix-react/entry.ts index d038c37d7f9..bfb537a3c9d 100644 --- a/packages/remix-react/entry.ts +++ b/packages/remix-react/entry.ts @@ -38,7 +38,6 @@ export interface FutureConfig { unstable_postcss: boolean; /** @deprecated Use the `tailwind` config option instead */ unstable_tailwind: boolean; - v2_errorBoundary: boolean; v2_headers: boolean; v2_meta: boolean; v2_routeConvention: boolean; diff --git a/packages/remix-react/errorBoundaries.tsx b/packages/remix-react/errorBoundaries.tsx index 24d63344aed..3b61c33d651 100644 --- a/packages/remix-react/errorBoundaries.tsx +++ b/packages/remix-react/errorBoundaries.tsx @@ -1,16 +1,9 @@ -import React, { useContext } from "react"; -import type { ErrorResponse, Location } from "@remix-run/router"; -import { isRouteErrorResponse, useRouteError } from "react-router-dom"; - -import type { - CatchBoundaryComponent, - ErrorBoundaryComponent, -} from "./routeModules"; -import type { ThrownResponse } from "./errors"; +import * as React from "react"; +import type { Location } from "@remix-run/router"; +import { isRouteErrorResponse } from "react-router-dom"; type RemixErrorBoundaryProps = React.PropsWithChildren<{ location: Location; - component: ErrorBoundaryComponent; error?: Error; }>; @@ -25,7 +18,6 @@ export class RemixErrorBoundary extends React.Component< > { constructor(props: RemixErrorBoundaryProps) { super(props); - this.state = { error: props.error || null, location: props.location }; } @@ -59,7 +51,7 @@ export class RemixErrorBoundary extends React.Component< render() { if (this.state.error) { - return ; + return ; } else { return this.props.children; } @@ -69,57 +61,22 @@ export class RemixErrorBoundary extends React.Component< /** * When app's don't provide a root level ErrorBoundary, we default to this. */ -export function RemixRootDefaultErrorBoundary({ error }: { error: Error }) { - // Only log client side to avoid double-logging on the server - React.useEffect(() => { - console.error(error); - }, [error]); - return ( - - - - - Application Error! - - -
-

Application Error

- {error.stack ? ( -
-              {error.stack}
-            
- ) : null} -
-