testServerComponentsAreDebuggable

tcmts.cli.test.e2e.react.testServerComponentsAreDebuggable · #7s4fjhh6 · term

Server components are React's, so React's tools see them: in tcm dev, their trees come with what React DevTools shows of them (their names, the server as where they ran, their stacks, whose code tcm dev serves by the definition's name), and what they log is said again in the browser's console. tcm start sends none of it.

import { CHROME, hydrated, withPage } from "#codebase/tcmts/cli/test/browser";
import { okIn, 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 fs from "node:fs";
import path from "node:path";

export const testServerComponentsAreDebuggable = async () => {
  if (!CHROME) return console.log("skipped: no Chrome");
  await withChildren(async (keep) => {
    const dir = tempDir("react debug");
    const tcm = (...args: string[]) => okIn(dir, ...args);
    tcm("init");
    fs.rmSync(path.join(dir, "scratch.ts"));
    fs.writeFileSync(
      path.join(dir, "dbg.tsx"),
      `// tcm: namespace dbg
import { useState } from "react";

export function Clicker({ label }: { label: string }) {
  const [n, setN] = useState(0);
  return <button id="click" onClick={() => setN(n + 1)}>{label} {n}</button>;
}

export async function Greeting() {
  console.log("hello from the server");
  return <section id="greeting"><Clicker label="clicks" /></section>;
}

export function App() {
  return <main><Greeting /></main>;
}
`,
    );
    tcm("add", "dbg.tsx");
    const tree = async (url: string) =>
      (
        await fetch(new URL(`/__tcm/rpc/${hashOf(dir, "dbg.Greeting")}`, url), {
          method: "POST",
          headers: { "content-type": "application/json" },
          body: "[{}]",
        })
      ).text();

    const dev = await serve(keep, dir, "dev", "dbg.App");
    const said = await tree(dev);
    assert.match(said, /"name":"Greeting"/, "the component, by name");
    assert.match(said, /"env":"Server"/, "where it ran");
    assert.match(said, /hello from the server/, "what it logged");
    // a stack frame's code: the module's, named for the definition in it
    const file = /"(file:\/\/[^"]+\/[a-z2-7]{52}\.tsx)"/.exec(said)?.[1];
    assert.ok(file, said.slice(0, 600));
    const map = (await (
      await fetch(
        new URL(`/__parcel_source_map?filename=${encodeURIComponent(file!)}&env=Server`, dev),
      )
    ).json()) as { sources: string[]; sourcesContent: string[] };
    assert.deepEqual(map.sources, ["codebase/dbg.Greeting.tsx"]);
    assert.match(map.sourcesContent[0]!, /hello from the server/);
    const outside = await fetch(
      new URL(`/__parcel_source_map?filename=${encodeURIComponent("file:///etc/hosts")}`, dev),
    );
    assert.equal(outside.status, 404, "only the codebase's modules");
    await withPage(dev, async (p) => {
      assert.ok(await hydrated(p));
      // (the wait gives back its expression's value, not whether it came: the logs are checked after it)
      const logged = () => p.logs.some((l) => /hello from the server/.test(l));
      await p.waitFor<boolean>("true", logged);
      assert.ok(logged(), `the server's log, in the browser's console: ${JSON.stringify(p.logs)}`);
      await p.evaluate(`document.querySelector("#click").click()`);
      assert.equal(
        await p.waitFor<string>(
          `document.querySelector("#click").textContent`,
          (t) => t === "clicks 1",
        ),
        "clicks 1",
      );
      assert.deepEqual(p.errors, []);
    });

    // production: the tree alone
    tcm("build", "dbg.App");
    const prod = await serve(keep, dir, "start", "dbg.App");
    const plain = await tree(prod);
    assert.match(plain, /clicks/);
    assert.doesNotMatch(plain, /"env":"Server"|hello from the server|file:\/\//);
  });
}

uses