testServerFunctionsAsProps
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")); }); }