testServerFunctionsAsProps

tcmts.cli.test.e2e.react.testServerFunctionsAsProps · #qb3x3tij · term

A server component gives browser components server functions as props (onAdd={add}, .bind(null, name) with an argument bound): the browser calls them as it calls any server function, by hash, and the page's server components render again. Only those: a server function the server component just calls isn't the browser's to call.

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 testServerFunctionsAsProps = async () => {
  if (!CHROME) return console.log("skipped: no Chrome");
  await withChildren(async (keep) => {
    const dir = tempDir("react props");
    const tcm = (...args: string[]) => okIn(dir, ...args);
    tcm("init");
    fs.rmSync(path.join(dir, "scratch.ts"));
    fs.writeFileSync(
      path.join(dir, "sfp.tsx"),
      `// tcm: namespace sfp
import { useState } from "react";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

const store = path.join(os.tmpdir(), \`tcm-sfp-\${process.pid}.json\`);

export async function items(): Promise<string[]> {
  return fs.existsSync(store) ? JSON.parse(fs.readFileSync(store, "utf8")) : ["one", "two"];
}

export async function remove(name: string): Promise<void> {
  fs.writeFileSync(store, JSON.stringify((await items()).filter((i) => i !== name)));
}

export async function add(name: string): Promise<void> {
  fs.writeFileSync(store, JSON.stringify([...(await items()), name]));
}

export function Remover({ name, onRemove }: { name: string; onRemove: () => Promise<void> }) {
  const [busy, setBusy] = useState(false);
  return <button className="remove" onClick={() => { setBusy(true); void onRemove(); }}>{busy ? "…" : name}</button>;
}

export function Adder({ onAdd }: { onAdd: (name: string) => Promise<void> }) {
  return <button id="add" onClick={() => void onAdd("three")}>add</button>;
}

export async function List() {
  const all = await items();
  return (
    <ul>
      {all.map((n) => <li key={n}><Remover name={n} onRemove={remove.bind(null, n)} /></li>)}
      <Adder onAdd={add} />
    </ul>
  );
}

export function App() {
  return <main><List /></main>;
}
`,
    );
    tcm("add", "sfp.tsx");
    const check = async (url: string) => {
      const call = (name: string) =>
        fetch(new URL(`/__tcm/rpc/${hashOf(dir, name)}`, url), {
          method: "POST",
          headers: { "content-type": "application/json" },
          body: '["x"]',
        });
      // (refused: 403, or under `tcm start` 410, as code of the codebase no build serves)
      const refused = (await call("sfp.items")).status;
      assert.ok(
        [403, 410].includes(refused),
        `what the server component only calls stays on the server (${refused})`,
      );
      await withPage(url, async (p) => {
        assert.ok(await hydrated(p));
        const names = `[...document.querySelectorAll("li")].map((li) => li.textContent).join(",")`;
        assert.equal(await p.waitFor<string>(names, (t) => t === "one,two"), "one,two");
        await p.evaluate(`document.querySelector(".remove").click()`);
        assert.equal(
          await p.waitFor<string>(names, (t) => t === "two"),
          "two",
          "removed, with the name bound",
        );
        await p.evaluate(`document.querySelector("#add").click()`);
        assert.equal(
          await p.waitFor<string>(names, (t) => t === "two,three"),
          "two,three",
          "added, with the browser's argument",
        );
        assert.deepEqual(p.errors, []);
      });
    };
    await check(await serve(keep, dir, "dev", "sfp.App"));
    tcm("build", "sfp.App");
    await check(await serve(keep, dir, "start", "sfp.App"));
  });
}

uses