testPackageComponentsRenderInTheBrowser
A package's browser components (one whose module says "use client", Radix's; one that uses hooks, React Aria's),
rendered by a server component: sent as references to them, as tcm's own browser components are, so the
server doesn't run them (React's server build has no hooks) and the browser, which has them, renders them.
import { CHROME, hydrated, withPage } from "#codebase/tcmts/cli/test/browser"; import { okIn, plainEnv, tcmBin, tempDir, withChildren } from "#codebase/tcmts/cli/test/e2e/harness"; import { hashOf, serve } from "#codebase/tcmts/cli/test/e2e/react"; import assert from "node:assert/strict"; import { spawn } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; export const testPackageComponentsRenderInTheBrowser = async () => { await withChildren(async (keep) => { const dir = tempDir("react packages"); const tcm = (...args: string[]) => okIn(dir, ...args); tcm("init"); fs.rmSync(path.join(dir, "scratch.ts")); fs.writeFileSync( path.join(dir, "pk.tsx"), `// tcm: namespace pk import { Button } from "react-aria-components"; import { Separator } from "@radix-ui/react-separator"; export async function Panel() { return <section><Button id="press">press</Button><Separator id="sep" /></section>; } export function App() { return <main><Panel /></main>; } `, ); tcm("add", "pk.tsx"); const check = async (url: string) => { const tree = await ( await fetch(new URL(`/__tcm/rpc/${hashOf(dir, "pk.Panel")}`, url), { method: "POST", headers: { "content-type": "application/json" }, body: "[{}]", }) ).text(); // (references, by the package's export: Flight puts the key in a row of its own) assert.match(tree, /:I\[/, tree.slice(0, 800)); assert.match(tree, /"npm:react-aria-components@1#Button"/); assert.match(tree, /"npm:@radix-ui\/react-separator@1#Separator"/); const html = await (await fetch(url, { headers: { accept: "text/html" } })).text(); assert.match(html, /<button[^>]*id="press"/, "server-rendered, as browser components are"); assert.match(html, /id="sep"/); if (!CHROME) return; await withPage(url, async (p) => { assert.ok(await hydrated(p)); assert.match( await p.evaluate<string>(`document.querySelector("#press").className`), /react-aria-Button/, ); assert.deepEqual(p.errors, []); }); }; // `tcm dev` says so const dev = spawn(process.execPath, [tcmBin(), "dev", "pk.App", "--port", "0"], { cwd: dir, env: plainEnv(), }); keep(dev); let said = ""; dev.stdout.setEncoding("utf8").on("data", (d: string) => (said += d)); dev.stderr.setEncoding("utf8").on("data", (d: string) => (said += d)); const url = await new Promise<string>((resolve, reject) => { dev.on("exit", (code) => reject(new Error(`tcm dev exited ${code}:\n${said}`))); const t = setInterval(() => { const m = /url\s+(http:\/\/\S+)/.exec(said); if (m && /rendered in the browser inside server components \(packages/.test(said)) { clearInterval(t); resolve(m[1]!); } }, 50); }); assert.match(said, /react-aria-components's Button, @radix-ui\/react-separator's Separator/); await check(url); tcm("build", "pk.App"); await check(await serve(keep, dir, "start", "pk.App")); }); }
uses
- tcmts.cli.test.browser.CHROME
- tcmts.cli.test.browser.hydrated
- tcmts.cli.test.browser.withPage
- tcmts.cli.test.e2e.harness.okIn
- tcmts.cli.test.e2e.harness.plainEnv
- tcmts.cli.test.e2e.harness.tcmBin
- tcmts.cli.test.e2e.harness.tempDir
- tcmts.cli.test.e2e.harness.withChildren
- tcmts.cli.test.e2e.react.hashOf
- tcmts.cli.test.e2e.react.serve