testPackageComponentsRenderInTheBrowser

tcmts.cli.test.e2e.react.testPackageComponentsRenderInTheBrowser · #sfvzygaa · term

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