import { test, expect } from "@playwright/test";
import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
import {
createAppFixture,
createFixture,
js,
} from "./helpers/create-fixture.js";
const ROOT_ID = "ROOT_ID";
const INDEX_ID = "INDEX_ID";
const DEFERRED_ID = "DEFERRED_ID";
const RESOLVED_DEFERRED_ID = "RESOLVED_DEFERRED_ID";
const FALLBACK_ID = "FALLBACK_ID";
const ERROR_ID = "ERROR_ID";
const ERROR_BOUNDARY_ID = "ERROR_BOUNDARY_ID";
const MANUAL_RESOLVED_ID = "MANUAL_RESOLVED_ID";
const MANUAL_FALLBACK_ID = "MANUAL_FALLBACK_ID";
const MANUAL_ERROR_ID = "MANUAL_ERROR_ID";
let originalConsoleError: typeof console.error;
declare global {
var __deferredManualResolveCache: {
nextId: number;
deferreds: Record<
string,
{ resolve: (value: any) => void; reject: (error: Error) => void }
>;
};
}
function counterHtml(id: string, val: number) {
return `
${val}
`;
}
const deferredHTMLStartString = " {
let fixture: Fixture;
let appFixture: AppFixture;
test.beforeAll(async () => {
fixture = await createFixture({
files: {
"app/components/counter.tsx": js`
import { useState } from "react";
export default function Counter({ id }) {
let [count, setCount] = useState(0);
return (
{count}
)
}
`,
"app/components/interactive.tsx": js`
import { useEffect, useState } from "react";
export default function Interactive() {
let [interactive, setInteractive] = useState(false);
useEffect(() => {
setInteractive(true);
}, []);
return interactive ? (
) : null;
}
`,
"app/root.tsx": js`
import { Links, Meta, Outlet, Scripts, useLoaderData } from "react-router";
import Counter from "~/components/counter";
import Interactive from "~/components/interactive";
export const meta: MetaFunction = () => {
return [{ title: "New Remix App" }];
};
export const loader = () => ({
id: "${ROOT_ID}",
});
export default function Root() {
let { id } = useLoaderData();
return (
{/* Send arbitrary data so safari renders the initial shell before
the document finishes downloading. */}
{Array(1000).fill(null).map((_, i)=>YOOOOOOOOOO {i}
)}
);
}
`,
"app/routes/_index.tsx": js`
import { Link, useLoaderData } from "react-router";
import Counter from "~/components/counter";
export function loader() {
return {
id: "${INDEX_ID}",
};
}
export default function Index() {
let { id } = useLoaderData();
return (
{id}
- deferred-script-resolved
- deferred-script-unresolved
- deferred-script-rejected
- deferred-script-unrejected
- deferred-script-rejected-no-error-element
- deferred-script-unrejected-no-error-element
);
}
`,
"app/routes/deferred-noscript-resolved.tsx": js`
import { Suspense } from "react";
import { Await, Link, useLoaderData } from "react-router";
import Counter from "~/components/counter";
export function loader() {
return {
deferredId: "${DEFERRED_ID}",
resolvedId: Promise.resolve("${RESOLVED_DEFERRED_ID}"),
};
}
export default function Deferred() {
let { deferredId, resolvedId } = useLoaderData();
return (
}>
(
)}
/>
);
}
`,
"app/routes/deferred-noscript-unresolved.tsx": js`
import { Suspense } from "react";
import { Await, Link, useLoaderData } from "react-router";
import Counter from "~/components/counter";
export function loader() {
return {
deferredId: "${DEFERRED_ID}",
resolvedId: new Promise(
(resolve) => setTimeout(() => {
resolve("${RESOLVED_DEFERRED_ID}");
}, 10)
),
};
}
export default function Deferred() {
let { deferredId, resolvedId } = useLoaderData();
return (
}>
(
)}
/>
);
}
`,
"app/routes/deferred-script-resolved.tsx": js`
import { Suspense } from "react";
import { Await, Link, useLoaderData } from "react-router";
import Counter from "~/components/counter";
export function loader() {
return {
deferredId: "${DEFERRED_ID}",
resolvedId: Promise.resolve("${RESOLVED_DEFERRED_ID}"),
deferredUndefined: Promise.resolve(undefined),
};
}
export default function Deferred() {
let { deferredId, resolvedId } = useLoaderData();
return (
}>
(
)}
/>
);
}
`,
"app/routes/deferred-script-unresolved.tsx": js`
import { Suspense } from "react";
import { Await, Link, useLoaderData } from "react-router";
import Counter from "~/components/counter";
export function loader() {
return {
deferredId: "${DEFERRED_ID}",
resolvedId: new Promise(
(resolve) => setTimeout(() => {
resolve("${RESOLVED_DEFERRED_ID}");
}, 10)
),
deferredUndefined: new Promise(
(resolve) => setTimeout(() => {
resolve(undefined);
}, 10)
),
};
}
export default function Deferred() {
let { deferredId, resolvedId } = useLoaderData();
return (
}>
(
)}
/>
);
}
`,
"app/routes/deferred-script-rejected.tsx": js`
import { Suspense } from "react";
import { Await, Link, useLoaderData } from "react-router";
import Counter from "~/components/counter";
export function loader() {
return {
deferredId: "${DEFERRED_ID}",
resolvedId: Promise.reject(new Error("${RESOLVED_DEFERRED_ID}")),
};
}
export default function Deferred() {
let { deferredId, resolvedId } = useLoaderData();
return (
}>
error
}
children={(resolvedDeferredId) => (
)}
/>
);
}
`,
"app/routes/deferred-script-unrejected.tsx": js`
import { Suspense } from "react";
import { Await, Link, useLoaderData } from "react-router";
import Counter from "~/components/counter";
export function loader() {
return {
deferredId: "${DEFERRED_ID}",
resolvedId: new Promise(
(_, reject) => setTimeout(() => {
reject(new Error("${RESOLVED_DEFERRED_ID}"));
}, 10)
),
};
}
export default function Deferred() {
let { deferredId, resolvedId, resolvedUndefined } = useLoaderData();
return (
}>
error
}
children={(resolvedDeferredId) => (
)}
/>
);
}
`,
"app/routes/deferred-script-rejected-no-error-element.tsx": js`
import { Suspense } from "react";
import { Await, Link, useLoaderData } from "react-router";
import Counter from "~/components/counter";
export function loader() {
return {
deferredId: "${DEFERRED_ID}",
resolvedId: Promise.reject(new Error("${RESOLVED_DEFERRED_ID}")),
};
}
export default function Deferred() {
let { deferredId, resolvedId } = useLoaderData();
return (
}>
(
)}
/>
);
}
export function ErrorBoundary() {
return (
error
);
}
`,
"app/routes/deferred-script-unrejected-no-error-element.tsx": js`
import { Suspense } from "react";
import { Await, Link, useLoaderData } from "react-router";
import Counter from "~/components/counter";
export function loader() {
return {
deferredId: "${DEFERRED_ID}",
resolvedId: new Promise(
(_, reject) => setTimeout(() => {
reject(new Error("${RESOLVED_DEFERRED_ID}"));
}, 10)
),
};
}
export default function Deferred() {
let { deferredId, resolvedId } = useLoaderData();
return (
}>
(
)}
/>
);
}
export function ErrorBoundary() {
return (
error
);
}
`,
"app/routes/deferred-manual-resolve.tsx": js`
import { Suspense } from "react";
import { Await, Link, useLoaderData } from "react-router";
import Counter from "~/components/counter";
export function loader() {
global.__deferredManualResolveCache = global.__deferredManualResolveCache || {
nextId: 1,
deferreds: {},
};
let id = "" + global.__deferredManualResolveCache.nextId++;
let promise = new Promise((resolve, reject) => {
global.__deferredManualResolveCache.deferreds[id] = { resolve, reject };
});
return {
deferredId: "${DEFERRED_ID}",
resolvedId: new Promise(
(resolve) => setTimeout(() => {
resolve("${RESOLVED_DEFERRED_ID}");
}, 10)
),
id,
manualValue: promise,
};
}
export default function Deferred() {
let { deferredId, resolvedId, id, manualValue } = useLoaderData();
return (
}>
(
)}
/>
manual fallback}>
error
}
children={(value) => (
)}
/>
);
}
`,
},
});
appFixture = await createAppFixture(fixture);
originalConsoleError = console.error;
console.error = () => {};
});
test.afterAll(() => {
console.error = originalConsoleError;
appFixture.close();
});
test("works with critical JSON like data", async () => {
let { status, criticalHTML, deferredHTML } = await getHtmlSections(
fixture,
"/",
);
expect(status).toBe(200);
expect(criticalHTML).toContain(counterHtml(ROOT_ID, 0));
expect(criticalHTML).toContain(counterHtml(INDEX_ID, 0));
expect(deferredHTML.replace("