From aab4ea0e2b52ae91f93bd87653a2786e23252d49 Mon Sep 17 00:00:00 2001
From: Jacob Ebey
{JSON.stringify(matches)}-
${OWN_BOUNDARY_TEXT}
- } - export default function Index() { - return ( - - ); - } - `, - - [`app/routes${NO_BOUNDARY_ACTION_FILE}.jsx`]: js` - import { Form } from "react-router-dom"; - export function action() { - throw new Response("", { status: 401 }) - } - export default function Index() { - return ( - - ) - } - `, - - [`app/routes${HAS_BOUNDARY_LOADER_FILE}.jsx`]: js` - import { useRouteError } from "react-router-dom"; - export function loader() { - throw new Response("", { status: 401 }) - } - export function ErrorBoundary() { - let error = useRouteError(); - return ( - <> -{error.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.tsx": js` - import { Outlet, useLoaderData } from "react-router-dom"; - - export function loader() { - return "PARENT"; - } - - export default function () { - return ( -
{useLoaderData()}
-{useLoaderData()}
- - > - ) - } - - export function ErrorBoundary() { - let error = useRouteError() - return{error.status} {error.data}
; - } - `, - }, - }); - - appFixture = await createAppFixture(fixture); - }); - - test.afterAll(() => { - appFixture.close(); - }); - - test("non-matching urls on document requests", async () => { - let oldConsoleError; - oldConsoleError = console.error; - console.error = () => {}; - - 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}`); - - console.error = oldConsoleError; - }); - - 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"); - }); - }); -}); diff --git a/integration/client-data-test.ts b/integration/client-data-test.ts index 030d2c7a25..4019a3e6c1 100644 --- a/integration/client-data-test.ts +++ b/integration/client-data-test.ts @@ -138,7 +138,7 @@ function getFiles({ }; } -test.describe.skip("Client Data", () => { +test.describe("Client Data", () => { let appFixture: AppFixture; test.afterAll(() => { @@ -146,7 +146,13 @@ test.describe.skip("Client Data", () => { }); function createTestFixture(init: FixtureInit, serverMode?: ServerMode) { - return createFixture(init, serverMode); + return createFixture( + { + ...init, + singleFetch: true, + }, + serverMode + ); } test.describe("clientLoader - critical route module", () => { @@ -784,68 +790,6 @@ test.describe.skip("Client Data", () => { expect(html).not.toMatch("Should not see me"); console.error = _consoleError; }); - - test("server loader errors are persisted for non-hydrating routes", async ({ - page, - }) => { - let _consoleError = console.error; - console.error = () => {}; - appFixture = await createAppFixture( - await createFixture( - { - files: { - ...getFiles({ - parentClientLoader: true, - parentClientLoaderHydrate: false, - // Hydrate the parent clientLoader but don't add a HydrateFallback - parentAdditions: js` - clientLoader.hydrate = true; - `, - childClientLoader: false, - childClientLoaderHydrate: false, - }), - "app/routes/parent.child.tsx": js` - import { json } from '@react-router/node' - import { useRouteError } from "react-router-dom" - export function loader() { - throw json({ message: 'Child Server Error'}); - } - export default function Component() { - return
{JSON.stringify(error, null, 2)}- > - ); - } - `, - }, - }, - ServerMode.Development // Avoid error sanitization - ), - ServerMode.Development // Avoid error sanitization - ); - let app = new PlaywrightFixture(appFixture, page); - - await app.goto("/parent/child", false); - let html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader"); - expect(html).toMatch("Child Server Error"); - expect(html).not.toMatch("Should not see me"); - // Ensure we hydrate and remain on the boundary - await page.waitForSelector( - ":has-text('Parent Server Loader (mutated by client)')" - ); - html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader (mutated by client)"); - expect(html).toMatch("Child Server Error"); - expect(html).not.toMatch("Should not see me"); - console.error = _consoleError; - }); }); test.describe("clientLoader - lazy route module", () => { @@ -982,13 +926,14 @@ test.describe.skip("Client Data", () => { test.describe("clientAction - critical route module", () => { test("child.clientAction", async ({ page }) => { appFixture = await createAppFixture( - await createTestFixture({ - files: getFiles({ - parentClientLoader: false, - parentClientLoaderHydrate: false, - childClientLoader: false, - childClientLoaderHydrate: false, - childAdditions: js` + await createTestFixture( + { + files: getFiles({ + parentClientLoader: false, + parentClientLoaderHydrate: false, + childClientLoader: false, + childClientLoaderHydrate: false, + childAdditions: js` export async function clientAction({ serverAction }) { let data = await serverAction(); return { @@ -996,8 +941,11 @@ test.describe.skip("Client Data", () => { } } `, - }), - }) + }), + }, + ServerMode.Development + ), + ServerMode.Development ); let app = new PlaywrightFixture(appFixture, page); await app.goto("/parent/child"); @@ -1407,1239 +1355,3 @@ test.describe.skip("Client Data", () => { }); }); }); - -// Duplicate suite of the tests above running with single fetch enabled -// TODO(v3): remove the above suite of tests and just keep these -test.describe("single fetch", () => { - test.describe("Client Data", () => { - let appFixture: AppFixture; - - test.afterAll(() => { - appFixture.close(); - }); - - function createTestFixture(init: FixtureInit, serverMode?: ServerMode) { - return createFixture( - { - ...init, - singleFetch: true, - }, - serverMode - ); - } - - test.describe("clientLoader - critical route module", () => { - test("no client loaders or fallbacks", async ({ page }) => { - appFixture = await createAppFixture( - await createTestFixture({ - files: getFiles({ - parentClientLoader: false, - parentClientLoaderHydrate: false, - childClientLoader: false, - childClientLoaderHydrate: false, - }), - }) - ); - let app = new PlaywrightFixture(appFixture, page); - - // Full SSR - normal Remix behavior due to lack of clientLoader - await app.goto("/parent/child"); - let html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader"); - expect(html).toMatch("Child Server Loader"); - }); - - test("parent.clientLoader/child.clientLoader", async ({ page }) => { - appFixture = await createAppFixture( - await createTestFixture({ - files: getFiles({ - parentClientLoader: true, - parentClientLoaderHydrate: false, - childClientLoader: true, - childClientLoaderHydrate: false, - }), - }) - ); - let app = new PlaywrightFixture(appFixture, page); - - // Full SSR - normal Remix behavior due to lack of HydrateFallback components - await app.goto("/parent/child"); - let html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader"); - expect(html).toMatch("Child Server Loader"); - }); - - test("parent.clientLoader.hydrate/child.clientLoader", async ({ - page, - }) => { - appFixture = await createAppFixture( - await createTestFixture({ - files: getFiles({ - parentClientLoader: true, - parentClientLoaderHydrate: true, - childClientLoader: true, - childClientLoaderHydrate: false, - }), - }) - ); - let app = new PlaywrightFixture(appFixture, page); - - await app.goto("/parent/child"); - let html = await app.getHtml("main"); - expect(html).toMatch("Parent Fallback"); - expect(html).not.toMatch("Parent Server Loader"); - expect(html).not.toMatch("Child Server Loader"); - - await page.waitForSelector("#child-data"); - html = await app.getHtml("main"); - expect(html).not.toMatch("Parent Fallback"); - expect(html).toMatch("Parent Server Loader (mutated by client)"); - expect(html).toMatch("Child Server Loader"); - }); - - test("parent.clientLoader/child.clientLoader.hydrate", async ({ - page, - }) => { - appFixture = await createAppFixture( - await createTestFixture({ - files: getFiles({ - parentClientLoader: true, - parentClientLoaderHydrate: false, - childClientLoader: true, - childClientLoaderHydrate: true, - }), - }) - ); - let app = new PlaywrightFixture(appFixture, page); - - await app.goto("/parent/child"); - let html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader"); - expect(html).toMatch("Child Fallback"); - expect(html).not.toMatch("Child Server Loader"); - - await page.waitForSelector("#child-data"); - html = await app.getHtml("main"); - expect(html).not.toMatch("Child Fallback"); - expect(html).toMatch("Parent Server Loader"); - expect(html).toMatch("Child Server Loader (mutated by client)"); - }); - - test("parent.clientLoader.hydrate/child.clientLoader.hydrate", async ({ - page, - }) => { - appFixture = await createAppFixture( - await createTestFixture({ - files: getFiles({ - parentClientLoader: true, - parentClientLoaderHydrate: true, - childClientLoader: true, - childClientLoaderHydrate: true, - }), - }) - ); - let app = new PlaywrightFixture(appFixture, page); - - await app.goto("/parent/child"); - let html = await app.getHtml("main"); - expect(html).toMatch("Parent Fallback"); - expect(html).not.toMatch("Parent Server Loader"); - expect(html).not.toMatch("Child Fallback"); - expect(html).not.toMatch("Child Server Loader"); - - await page.waitForSelector("#child-data"); - html = await app.getHtml("main"); - expect(html).not.toMatch("Parent Fallback"); - expect(html).not.toMatch("Child Fallback"); - expect(html).toMatch("Parent Server Loader (mutated by client)"); - expect(html).toMatch("Child Server Loader (mutated by client)"); - }); - - test("handles synchronous client loaders", async ({ page }) => { - let fixture = await createTestFixture({ - files: getFiles({ - parentClientLoader: false, - parentClientLoaderHydrate: false, - childClientLoader: false, - childClientLoaderHydrate: false, - parentAdditions: js` - export function clientLoader() { - return { message: "Parent Client Loader" }; - } - clientLoader.hydrate=true - export function HydrateFallback() { - return
Parent Fallback
- } - `, - childAdditions: js` - export function clientLoader() { - return { message: "Child Client Loader" }; - } - clientLoader.hydrate=true - `, - }), - }); - - // Ensure we SSR the fallbacks - let doc = await fixture.requestDocument("/parent/child"); - let html = await doc.text(); - expect(html).toMatch("Parent Fallback"); - - appFixture = await createAppFixture(fixture); - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/parent/child"); - await page.waitForSelector("#child-data"); - html = await app.getHtml("main"); - expect(html).toMatch("Parent Client Loader"); - expect(html).toMatch("Child Client Loader"); - }); - - test("handles deferred data through client loaders", async ({ page }) => { - let fixture = await createTestFixture({ - files: { - ...getFiles({ - parentClientLoader: false, - parentClientLoaderHydrate: false, - childClientLoader: false, - childClientLoaderHydrate: false, - }), - "app/routes/parent.child.tsx": js` - import * as React from 'react'; - import { defer, json } from '@react-router/node' - import { Await, useLoaderData } from "react-router-dom" - export function loader() { - return defer({ - message: 'Child Server Loader', - lazy: new Promise(r => setTimeout(() => r("Child Deferred Data"), 1000)), - }); - } - export async function clientLoader({ serverLoader }) { - let data = await serverLoader(); - return { - ...data, - message: data.message + " (mutated by client)", - }; - } - clientLoader.hydrate = true; - export function HydrateFallback() { - returnChild Fallback
- } - export default function Component() { - let data = useLoaderData(); - return ( - <> -{data.message}
-{value}
} -SHOULD NOT SEE ME
- } - export default function Component() { - let data = useLoaderData(); - return{data.message}
; - } - `, - }, - }); - appFixture = await createAppFixture(fixture); - - // Ensure initial document request contains the child fallback _and_ the - // subsequent streamed/resolved deferred data - let doc = await fixture.requestDocument("/parent/child"); - let html = await doc.text(); - expect(html).toMatch("Child Server Loader Data"); - expect(html).not.toMatch("SHOULD NOT SEE ME"); - - let app = new PlaywrightFixture(appFixture, page); - - await app.goto("/parent/child"); - await page.waitForSelector("#child-data"); - html = await app.getHtml("main"); - expect(html).toMatch("Child Server Loader Data"); - }); - - test("clientLoader.hydrate is automatically implied when no server loader exists (w HydrateFallback)", async ({ - page, - }) => { - appFixture = await createAppFixture( - await createTestFixture({ - files: { - ...getFiles({ - parentClientLoader: false, - parentClientLoaderHydrate: false, - childClientLoader: false, - childClientLoaderHydrate: false, - }), - "app/routes/parent.child.tsx": js` - import * as React from 'react'; - import { useLoaderData } from "react-router-dom"; - // Even without setting hydrate=true, this should run on hydration - export async function clientLoader({ serverLoader }) { - await new Promise(r => setTimeout(r, 100)); - return { - message: "Loader Data (clientLoader only)", - }; - } - export function HydrateFallback() { - returnChild Fallback
- } - export default function Component() { - let data = useLoaderData(); - return{data.message}
; - } - `, - }, - }) - ); - let app = new PlaywrightFixture(appFixture, page); - - await app.goto("/parent/child"); - let html = await app.getHtml("main"); - expect(html).toMatch("Child Fallback"); - await page.waitForSelector("#child-data"); - html = await app.getHtml("main"); - expect(html).toMatch("Loader Data (clientLoader only)"); - }); - - test("clientLoader.hydrate is automatically implied when no server loader exists (w/o HydrateFallback)", async ({ - page, - }) => { - appFixture = await createAppFixture( - await createTestFixture({ - files: { - ...getFiles({ - parentClientLoader: false, - parentClientLoaderHydrate: false, - childClientLoader: false, - childClientLoaderHydrate: false, - }), - "app/routes/parent.child.tsx": js` - import * as React from 'react'; - import { useLoaderData } from "react-router-dom"; - // Even without setting hydrate=true, this should run on hydration - export async function clientLoader({ serverLoader }) { - await new Promise(r => setTimeout(r, 100)); - return { - message: "Loader Data (clientLoader only)", - }; - } - export default function Component() { - let data = useLoaderData(); - return{data.message}
; - } - `, - }, - }) - ); - let app = new PlaywrightFixture(appFixture, page); - - await app.goto("/parent/child"); - let html = await app.getHtml(); - expect(html).toMatch( - "💿 Hey developer 👋. You can provide a way better UX than this" - ); - expect(html).not.toMatch("child-data"); - await page.waitForSelector("#child-data"); - html = await app.getHtml("main"); - expect(html).toMatch("Loader Data (clientLoader only)"); - }); - - test("throws a 400 if you call serverLoader without a server loader", async ({ - page, - }) => { - appFixture = await createAppFixture( - await createTestFixture({ - files: { - ...getFiles({ - parentClientLoader: false, - parentClientLoaderHydrate: false, - childClientLoader: false, - childClientLoaderHydrate: false, - }), - "app/routes/parent.child.tsx": js` - import * as React from 'react'; - import { useLoaderData, useRouteError } from "react-router-dom"; - export async function clientLoader({ serverLoader }) { - return await serverLoader(); - } - export default function Component() { - returnChild
; - } - export function HydrateFallback() { - returnLoading...
; - } - export function ErrorBoundary() { - let error = useRouteError(); - return{error.status} {error.data}
; - } - `, - }, - }) - ); - let app = new PlaywrightFixture(appFixture, page); - - await app.goto("/parent/child"); - await page.waitForSelector("#child-error"); - let html = await app.getHtml("#child-error"); - expect(html.replace(/\n/g, " ").replace(/ +/g, " ")).toMatch( - "400 Error: You are trying to call serverLoader() on a route that does " + - 'not have a server loader (routeId: "routes/parent.child")' - ); - }); - - test("initial hydration data check functions properly", async ({ - page, - }) => { - appFixture = await createAppFixture( - await createTestFixture({ - files: { - ...getFiles({ - parentClientLoader: false, - parentClientLoaderHydrate: false, - childClientLoader: false, - childClientLoaderHydrate: false, - }), - "app/routes/parent.child.tsx": js` - import * as React from 'react'; - import { json } from '@react-router/node'; - import { useLoaderData, useRevalidator } from "react-router-dom"; - let isFirstCall = true; - export async function loader({ serverLoader }) { - if (isFirstCall) { - isFirstCall = false - return json({ - message: "Child Server Loader Data (1)", - }); - } - return json({ - message: "Child Server Loader Data (2+)", - }); - } - export async function clientLoader({ serverLoader }) { - await new Promise(r => setTimeout(r, 100)); - let serverData = await serverLoader(); - return { - message: serverData.message + " (mutated by client)", - }; - } - clientLoader.hydrate=true; - export default function Component() { - let data = useLoaderData(); - let revalidator = useRevalidator(); - return ( - <> -{data.message}
- - > - ); - } - export function HydrateFallback() { - returnLoading...
- } - `, - }, - }) - ); - let app = new PlaywrightFixture(appFixture, page); - - await app.goto("/parent/child"); - await page.waitForSelector("#child-data"); - let html = await app.getHtml(); - expect(html).toMatch( - "Child Server Loader Data (1) (mutated by client)" - ); - app.clickElement("button"); - await page.waitForSelector( - ':has-text("Child Server Loader Data (2+)")' - ); - html = await app.getHtml("main"); - expect(html).toMatch( - "Child Server Loader Data (2+) (mutated by client)" - ); - }); - - test("initial hydration data check functions properly even if serverLoader isn't called on hydration", async ({ - page, - }) => { - appFixture = await createAppFixture( - await createTestFixture({ - files: { - ...getFiles({ - parentClientLoader: false, - parentClientLoaderHydrate: false, - childClientLoader: false, - childClientLoaderHydrate: false, - }), - "app/routes/parent.child.tsx": js` - import * as React from 'react'; - import { json } from '@react-router/node'; - import { useLoaderData, useRevalidator } from "react-router-dom"; - let isFirstCall = true; - export async function loader({ serverLoader }) { - if (isFirstCall) { - isFirstCall = false - return json({ - message: "Child Server Loader Data (1)", - }); - } - return json({ - message: "Child Server Loader Data (2+)", - }); - } - let isFirstClientCall = true; - export async function clientLoader({ serverLoader }) { - await new Promise(r => setTimeout(r, 100)); - if (isFirstClientCall) { - isFirstClientCall = false; - // First time through - don't even call serverLoader - return { - message: "Child Client Loader Data", - }; - } - // Only call the serverLoader on subsequent calls and this - // should *not* return us the initialData any longer - let serverData = await serverLoader(); - return { - message: serverData.message + " (mutated by client)", - }; - } - clientLoader.hydrate=true; - export default function Component() { - let data = useLoaderData(); - let revalidator = useRevalidator(); - return ( - <> -{data.message}
- - > - ); - } - export function HydrateFallback() { - returnLoading...
- } - `, - }, - }) - ); - let app = new PlaywrightFixture(appFixture, page); - - await app.goto("/parent/child"); - await page.waitForSelector("#child-data"); - let html = await app.getHtml(); - expect(html).toMatch("Child Client Loader Data"); - app.clickElement("button"); - await page.waitForSelector( - ':has-text("Child Server Loader Data (2+)")' - ); - html = await app.getHtml("main"); - expect(html).toMatch( - "Child Server Loader Data (2+) (mutated by client)" - ); - }); - - test("server loader errors are re-thrown from serverLoader()", async ({ - page, - }) => { - let _consoleError = console.error; - console.error = () => {}; - appFixture = await createAppFixture( - await createTestFixture( - { - files: { - ...getFiles({ - parentClientLoader: false, - parentClientLoaderHydrate: false, - childClientLoader: false, - childClientLoaderHydrate: false, - }), - "app/routes/parent.child.tsx": js` - import { ClientLoaderFunctionArgs, useRouteError } from "react-router-dom"; - - export function loader() { - throw new Error("Broken!") - } - - export async function clientLoader({ serverLoader }) { - return await serverLoader(); - } - clientLoader.hydrate = true; - - export default function Index() { - return{error.message}
; - } - `, - }, - }, - ServerMode.Development // Avoid error sanitization - ), - ServerMode.Development // Avoid error sanitization - ); - let app = new PlaywrightFixture(appFixture, page); - - await app.goto("/parent/child"); - let html = await app.getHtml("main"); - expect(html).toMatch("Broken!"); - // Ensure we hydrate and remain on the boundary - await new Promise((r) => setTimeout(r, 100)); - html = await app.getHtml("main"); - expect(html).toMatch("Broken!"); - expect(html).not.toMatch("Should not see me"); - console.error = _consoleError; - }); - }); - - test.describe("clientLoader - lazy route module", () => { - test("no client loaders or fallbacks", async ({ page }) => { - appFixture = await createAppFixture( - await createTestFixture({ - files: getFiles({ - parentClientLoader: false, - parentClientLoaderHydrate: false, - childClientLoader: false, - childClientLoaderHydrate: false, - }), - }) - ); - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/parent/child"); - await page.waitForSelector("#child-data"); - - // Normal Remix behavior due to lack of clientLoader - let html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader"); - expect(html).toMatch("Child Server Loader"); - }); - - test("parent.clientLoader", async ({ page }) => { - appFixture = await createAppFixture( - await createTestFixture({ - files: getFiles({ - parentClientLoader: true, - parentClientLoaderHydrate: false, - childClientLoader: false, - childClientLoaderHydrate: false, - }), - }) - ); - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/parent/child"); - await page.waitForSelector("#child-data"); - - let html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader (mutated by client)"); - expect(html).toMatch("Child Server Loader"); - }); - - test("child.clientLoader", async ({ page }) => { - appFixture = await createAppFixture( - await createTestFixture({ - files: getFiles({ - parentClientLoader: false, - parentClientLoaderHydrate: false, - childClientLoader: true, - childClientLoaderHydrate: false, - }), - }) - ); - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/parent/child"); - await page.waitForSelector("#child-data"); - - let html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader"); - expect(html).toMatch("Child Server Loader (mutated by client)"); - }); - - test("parent.clientLoader/child.clientLoader", async ({ page }) => { - appFixture = await createAppFixture( - await createTestFixture({ - files: getFiles({ - parentClientLoader: true, - parentClientLoaderHydrate: false, - childClientLoader: true, - childClientLoaderHydrate: false, - }), - }) - ); - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/parent/child"); - await page.waitForSelector("#child-data"); - - let html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader (mutated by client)"); - expect(html).toMatch("Child Server Loader (mutated by client"); - }); - - test("throws a 400 if you call serverLoader without a server loader", async ({ - page, - }) => { - appFixture = await createAppFixture( - await createTestFixture({ - files: { - ...getFiles({ - parentClientLoader: false, - parentClientLoaderHydrate: false, - childClientLoader: false, - childClientLoaderHydrate: false, - }), - "app/routes/parent.child.tsx": js` - import * as React from 'react'; - import { useLoaderData, useRouteError } from "react-router-dom"; - export async function clientLoader({ serverLoader }) { - return await serverLoader(); - } - export default function Component() { - returnChild
; - } - export function HydrateFallback() { - returnLoading...
; - } - export function ErrorBoundary() { - let error = useRouteError(); - return{error.status} {error.data}
; - } - `, - }, - }) - ); - let app = new PlaywrightFixture(appFixture, page); - - await app.goto("/"); - await app.clickLink("/parent/child"); - await page.waitForSelector("#child-error"); - let html = await app.getHtml("#child-error"); - expect(html.replace(/\n/g, " ").replace(/ +/g, " ")).toMatch( - "400 Error: You are trying to call serverLoader() on a route that does " + - 'not have a server loader (routeId: "routes/parent.child")' - ); - }); - }); - - test.describe("clientAction - critical route module", () => { - test("child.clientAction", async ({ page }) => { - appFixture = await createAppFixture( - await createTestFixture( - { - files: getFiles({ - parentClientLoader: false, - parentClientLoaderHydrate: false, - childClientLoader: false, - childClientLoaderHydrate: false, - childAdditions: js` - export async function clientAction({ serverAction }) { - let data = await serverAction(); - return { - message: data.message + " (mutated by client)" - } - } - `, - }), - }, - ServerMode.Development - ), - ServerMode.Development - ); - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/parent/child"); - let html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader"); - expect(html).toMatch("Child Server Loader"); - expect(html).not.toMatch("Child Server Action"); - - app.clickSubmitButton("/parent/child"); - await page.waitForSelector("#child-action-data"); - html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader"); - expect(html).toMatch("Child Server Loader"); - expect(html).toMatch("Child Server Action (mutated by client)"); - }); - - test("child.clientAction/parent.childLoader", async ({ page }) => { - appFixture = await createAppFixture( - await createTestFixture({ - files: getFiles({ - parentClientLoader: true, - parentClientLoaderHydrate: false, - childClientLoader: false, - childClientLoaderHydrate: false, - childAdditions: js` - export async function clientAction({ serverAction }) { - let data = await serverAction(); - return { - message: data.message + " (mutated by client)" - } - } - `, - }), - }) - ); - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/parent/child"); - let html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader"); - expect(html).toMatch("Child Server Loader"); - expect(html).not.toMatch("Child Server Action"); - - app.clickSubmitButton("/parent/child"); - await page.waitForSelector("#child-action-data"); - html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader"); // still revalidating - expect(html).toMatch("Child Server Loader"); - expect(html).toMatch("Child Server Action (mutated by client)"); - - await page.waitForSelector( - ':has-text("Parent Server Loader (mutated by client)")' - ); - html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader (mutated by client)"); - expect(html).toMatch("Child Server Loader"); - expect(html).toMatch("Child Server Action (mutated by client)"); - }); - - test("child.clientAction/child.clientLoader", async ({ page }) => { - appFixture = await createAppFixture( - await createTestFixture({ - files: getFiles({ - parentClientLoader: false, - parentClientLoaderHydrate: false, - childClientLoader: true, - childClientLoaderHydrate: false, - childAdditions: js` - export async function clientAction({ serverAction }) { - let data = await serverAction(); - return { - message: data.message + " (mutated by client)" - } - } - `, - }), - }) - ); - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/parent/child"); - let html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader"); - expect(html).toMatch("Child Server Loader"); - expect(html).not.toMatch("Child Server Action"); - - app.clickSubmitButton("/parent/child"); - await page.waitForSelector("#child-action-data"); - html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader"); // still revalidating - expect(html).toMatch("Child Server Loader"); - expect(html).toMatch("Child Server Action (mutated by client)"); - - await page.waitForSelector( - ':has-text("Child Server Loader (mutated by client)")' - ); - html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader"); - expect(html).toMatch("Child Server Loader (mutated by client)"); - expect(html).toMatch("Child Server Action (mutated by client)"); - }); - - test("child.clientAction/parent.childLoader/child.clientLoader", async ({ - page, - }) => { - appFixture = await createAppFixture( - await createTestFixture({ - files: getFiles({ - parentClientLoader: true, - parentClientLoaderHydrate: false, - childClientLoader: true, - childClientLoaderHydrate: false, - childAdditions: js` - export async function clientAction({ serverAction }) { - let data = await serverAction(); - return { - message: data.message + " (mutated by client)" - } - } - `, - }), - }) - ); - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/parent/child"); - let html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader"); - expect(html).toMatch("Child Server Loader"); - expect(html).not.toMatch("Child Server Action"); - - app.clickSubmitButton("/parent/child"); - await page.waitForSelector("#child-action-data"); - html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader"); // still revalidating - expect(html).toMatch("Child Server Loader"); // still revalidating - expect(html).toMatch("Child Server Action (mutated by client)"); - - await page.waitForSelector( - ':has-text("Child Server Loader (mutated by client)")' - ); - html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader (mutated by client)"); - expect(html).toMatch("Child Server Loader (mutated by client)"); - expect(html).toMatch("Child Server Action (mutated by client)"); - }); - - test("throws a 400 if you call serverAction without a server action", async ({ - page, - }) => { - appFixture = await createAppFixture( - await createTestFixture({ - files: { - ...getFiles({ - parentClientLoader: false, - parentClientLoaderHydrate: false, - childClientLoader: false, - childClientLoaderHydrate: false, - }), - "app/routes/parent.child.tsx": js` - import * as React from 'react'; - import { json } from '@react-router/node'; - import { Form, useRouteError } from "react-router-dom"; - export async function clientAction({ serverAction }) { - return await serverAction(); - } - export default function Component() { - return ( - - ); - } - export function ErrorBoundary() { - let error = useRouteError(); - return{error.status} {error.data}
; - } - `, - }, - }) - ); - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/parent/child"); - app.clickSubmitButton("/parent/child"); - await page.waitForSelector("#child-error"); - let html = await app.getHtml("#child-error"); - expect(html.replace(/\n/g, " ").replace(/ +/g, " ")).toMatch( - "400 Error: You are trying to call serverAction() on a route that does " + - 'not have a server action (routeId: "routes/parent.child")' - ); - }); - }); - - test.describe("clientAction - lazy route module", () => { - test("child.clientAction", async ({ page }) => { - appFixture = await createAppFixture( - await createTestFixture({ - files: getFiles({ - parentClientLoader: false, - parentClientLoaderHydrate: false, - childClientLoader: false, - childClientLoaderHydrate: false, - childAdditions: js` - export async function clientAction({ serverAction }) { - let data = await serverAction(); - return { - message: data.message + " (mutated by client)" - } - } - `, - }), - }) - ); - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/parent/child"); - await page.waitForSelector("#child-data"); - let html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader"); - expect(html).toMatch("Child Server Loader"); - expect(html).not.toMatch("Child Server Action"); - - app.clickSubmitButton("/parent/child"); - await page.waitForSelector("#child-action-data"); - html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader"); - expect(html).toMatch("Child Server Loader"); - expect(html).toMatch("Child Server Action (mutated by client)"); - }); - - test("child.clientAction/parent.childLoader", async ({ page }) => { - appFixture = await createAppFixture( - await createTestFixture({ - files: getFiles({ - parentClientLoader: true, - parentClientLoaderHydrate: false, - childClientLoader: false, - childClientLoaderHydrate: false, - childAdditions: js` - export async function clientAction({ serverAction }) { - let data = await serverAction(); - return { - message: data.message + " (mutated by client)" - } - } - `, - }), - }) - ); - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/parent/child"); - await page.waitForSelector("#child-data"); - let html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader"); - expect(html).toMatch("Child Server Loader"); - expect(html).not.toMatch("Child Server Action"); - - app.clickSubmitButton("/parent/child"); - await page.waitForSelector("#child-action-data"); - html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader"); // still revalidating - expect(html).toMatch("Child Server Loader"); - expect(html).toMatch("Child Server Action (mutated by client)"); - - await page.waitForSelector( - ':has-text("Parent Server Loader (mutated by client)")' - ); - html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader (mutated by client)"); - expect(html).toMatch("Child Server Loader"); - expect(html).toMatch("Child Server Action (mutated by client)"); - }); - - test("child.clientAction/child.clientLoader", async ({ page }) => { - appFixture = await createAppFixture( - await createTestFixture({ - files: getFiles({ - parentClientLoader: false, - parentClientLoaderHydrate: false, - childClientLoader: true, - childClientLoaderHydrate: false, - childAdditions: js` - export async function clientAction({ serverAction }) { - let data = await serverAction(); - return { - message: data.message + " (mutated by client)" - } - } - `, - }), - }) - ); - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/parent/child"); - await page.waitForSelector("#child-data"); - let html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader"); - expect(html).toMatch("Child Server Loader"); - expect(html).not.toMatch("Child Server Action"); - - app.clickSubmitButton("/parent/child"); - await page.waitForSelector("#child-action-data"); - html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader"); // still revalidating - expect(html).toMatch("Child Server Loader"); - expect(html).toMatch("Child Server Action (mutated by client)"); - - await page.waitForSelector( - ':has-text("Child Server Loader (mutated by client)")' - ); - html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader"); - expect(html).toMatch("Child Server Loader (mutated by client)"); - expect(html).toMatch("Child Server Action (mutated by client)"); - }); - - test("child.clientAction/parent.childLoader/child.clientLoader", async ({ - page, - }) => { - appFixture = await createAppFixture( - await createTestFixture({ - files: getFiles({ - parentClientLoader: true, - parentClientLoaderHydrate: false, - childClientLoader: true, - childClientLoaderHydrate: false, - childAdditions: js` - export async function clientAction({ serverAction }) { - let data = await serverAction(); - return { - message: data.message + " (mutated by client)" - } - } - `, - }), - }) - ); - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.clickLink("/parent/child"); - await page.waitForSelector("#child-data"); - let html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader"); - expect(html).toMatch("Child Server Loader"); - expect(html).not.toMatch("Child Server Action"); - - app.clickSubmitButton("/parent/child"); - await page.waitForSelector("#child-action-data"); - html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader"); // still revalidating - expect(html).toMatch("Child Server Loader"); // still revalidating - expect(html).toMatch("Child Server Action (mutated by client)"); - - await page.waitForSelector( - ':has-text("Child Server Loader (mutated by client)")' - ); - html = await app.getHtml("main"); - expect(html).toMatch("Parent Server Loader (mutated by client)"); - expect(html).toMatch("Child Server Loader (mutated by client)"); - expect(html).toMatch("Child Server Action (mutated by client)"); - }); - - test("throws a 400 if you call serverAction without a server action", async ({ - page, - }) => { - appFixture = await createAppFixture( - await createTestFixture({ - files: { - ...getFiles({ - parentClientLoader: false, - parentClientLoaderHydrate: false, - childClientLoader: false, - childClientLoaderHydrate: false, - }), - "app/routes/parent.child.tsx": js` - import * as React from 'react'; - import { json } from '@react-router/node'; - import { Form, useRouteError } from "react-router-dom"; - export async function clientAction({ serverAction }) { - return await serverAction(); - } - export default function Component() { - return ( - - ); - } - export function ErrorBoundary() { - let error = useRouteError(); - return{error.status} {error.data}
; - } - `, - }, - }) - ); - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - await app.goto("/parent/child"); - await page.waitForSelector("form"); - app.clickSubmitButton("/parent/child"); - await page.waitForSelector("#child-error"); - let html = await app.getHtml("#child-error"); - expect(html.replace(/\n/g, " ").replace(/ +/g, " ")).toMatch( - "400 Error: You are trying to call serverAction() on a route that does " + - 'not have a server action (routeId: "routes/parent.child")' - ); - }); - }); - }); -}); diff --git a/integration/defer-loader-test.ts b/integration/defer-loader-test.ts index 0b1d50b361..09575f3335 100644 --- a/integration/defer-loader-test.ts +++ b/integration/defer-loader-test.ts @@ -11,62 +11,63 @@ import { PlaywrightFixture } from "./helpers/playwright-fixture.js"; let fixture: Fixture; let appFixture: AppFixture; -test.describe.skip("deferred loaders", () => { +test.describe("deferred loaders", () => { test.beforeAll(async () => { fixture = await createFixture({ + singleFetch: true, files: { "app/routes/_index.tsx": js` - import { useLoaderData, Link } from "react-router-dom"; - export default function Index() { - return ( -${val}
`; + } + test("works with critical JSON like data", async ({ page }) => { let response = await fixture.requestDocument("/"); let html = await response.text(); let criticalHTML = html.slice(0, html.indexOf("") + 7); - expect(criticalHTML).toContain(ROOT_ID); - expect(criticalHTML).toContain(INDEX_ID); + expect(criticalHTML).toContain(counterHtml(ROOT_ID, 0)); + expect(criticalHTML).toContain(counterHtml(INDEX_ID, 0)); let deferredHTML = html.slice(html.indexOf("") + 7); - expect(deferredHTML).toBe(""); + expect(deferredHTML).not.toBe(""); + expect(deferredHTML).not.toContain('{ let response = await fixture.requestDocument("/deferred-noscript-resolved"); let html = await response.text(); let criticalHTML = html.slice(0, html.indexOf("