testWebUi
the web UI on tcm serve: the codebase's pages and a contribution's review, rendered on the server in any order,
a contributor signed in by the link tcm contribute prints, commenting as their handle, and the maintainer
merging; a site served beside it keeps its own pages
import { CHROME, withPage } from "#codebase/tcmts/cli/test/browser"; import { codebase, LIB, startServer, uiPage } from "#codebase/tcmts/cli/test/e2e/contributions"; import { tempDir, withChildren } from "#codebase/tcmts/cli/test/e2e/harness"; import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; export const testWebUi = async () => { const root = tempDir("contributions"); await withChildren(async (keep) => { const server = codebase(root, "served"); server.write(LIB); server.tcm("add"); fs.mkdirSync(path.join(root, "site")); fs.writeFileSync(path.join(root, "site", "index.html"), "<h1>docs</h1>"); fs.writeFileSync(path.join(root, "site", "contributions.html"), "<h1>About contributions</h1>"); const url = await startServer(keep, server.dir, "tok", [ "--public", "--site", path.join(root, "site"), ]); const owner = codebase(root, "owner"); owner.tcm("remote", "add", "origin", url, "--token", "tok"); const token = /their token \(shown once[^)]*\): (\S+)/.exec( owner.tcm("user", "add", "grace"), )![1]!; const grace = codebase(root, "grace"); grace.tcm("remote", "add", "origin", url, "--token", token); grace.tcm("pull"); grace.tcm("branch", "cubes"); grace.tcm("switch", "cubes"); grace.write( `// tcm: namespace lib\nexport const square = (x: number): number => x ** 2;\n/** A cube. */\nexport const cube = (x: number): number => x ** 3;\n`, ); grace.tcm("update"); const link = /review: tcm contribution view 1 · (\S+)/.exec( grace.tcm("contribute", "--title", "Cubes <3"), )![1]!; assert.equal(link, `${url}/forge/contributions/1#token=${token}`); // the site keeps its pages; the UI is under /forge assert.match(await (await fetch(`${url}/`)).text(), /docs/); assert.match(await (await fetch(`${url}/contributions`)).text(), /About contributions/); // the icon browsers ask the server for is the web UI's, the site having none assert.match( (await fetch(`${url}/favicon.ico`)).headers.get("content-type") ?? "", /image\/svg\+xml/, ); // every page renders on the server, whichever comes first: the front page, then a review (its browser components // are references, never loaded as server code) const front = await uiPage(`${url}/forge/`); assert.equal(front.status, 200, front.html); assert.match(front.html, /class="map"/); assert.match(front.html, /<b>lib<\/b>|>lib</); const changes = await uiPage(`${url}/forge/contributions/1?tab=changes`); assert.match(changes.html, /Review path/); const zipped = await fetch(`${url}/forge/contributions/1?tab=changes`, { headers: { accept: "text/html", "accept-encoding": "gzip" }, }); assert.equal( zipped.headers.get("content-encoding"), "gzip", "the web UI's pages come compressed", ); assert.match(await zipped.text(), /Review path/); assert.match(changes.html, /Cubes <3/, "titles are text, not HTML"); assert.match(changes.html, /lib\.square/); // (the diff's lines, coloured as code: their text, tags aside) assert.match(changes.html.replace(/<[^>]+>/g, ""), /x \*\* 2/); assert.match(changes.html, /Merges as a fast-forward/); assert.match((await uiPage(`${url}/forge/code/lib/square`)).html, /Used by/); // (not yet on main: what the pages keep for main's head is read again when a merge moves it, below) assert.doesNotMatch((await uiPage(`${url}/forge/code/lib`)).html, /lib\/cube/); assert.match((await uiPage(`${url}/forge/history`)).html, /class="hrow"/); assert.match((await uiPage(`${url}/forge/people`)).html, /@grace/); // a reader can't act: no merge button, and a server function refuses assert.doesNotMatch(changes.html, /Merge into main/); if (!CHROME) return console.log("skipped the browser part: no Chrome"); const typeInto = (selector: string, text: string) => `(() => { const el = document.querySelector(${JSON.stringify(selector)}); const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; Object.getOwnPropertyDescriptor(proto, "value").set.call(el, ${JSON.stringify(text)}); el.dispatchEvent(new Event("input", { bubbles: true })); return true; })()`; // the link signs grace in; she comments as herself await withPage(link, async (page) => { await page.waitFor<string>( `document.querySelector(".who")?.textContent ?? ""`, (t) => t.includes("@grace"), 60_000, ); assert.equal(await page.evaluate(`location.hash`), "", "the token leaves the address bar"); await page.evaluate(typeInto("form.say textarea", "Ready for review.")); await page.evaluate(`document.querySelector("form.say button.primary").click()`); await page.waitFor<string>( `[...document.querySelectorAll(".comment .cb")].map((e) => e.textContent).join("|")`, (t) => t.includes("Ready for review."), 30_000, ); assert.doesNotMatch( await page.evaluate<string>(`document.body.textContent`), /Merge into main/, "a contributor doesn't merge", ); }); // the maintainer, signed in by the server's token, merges await withPage(`${url}/forge/contributions/1#token=tok`, async (page) => { await page.waitFor<string>( `document.querySelector(".who")?.textContent ?? ""`, (t) => t.includes("maintainer"), 60_000, ); await page.waitFor<boolean>( `!!document.querySelector(".mergebox button.primary")`, (b) => b, 30_000, ); await page.evaluate(`document.querySelector(".mergebox button.primary").click()`); await page.waitFor<string>( `document.body.textContent`, (t) => t.includes("Merged into"), 60_000, ); assert.deepEqual(page.errors, []); }); // the pages follow main's head: the merged definition is listed, and the history has the entry that added it assert.match((await uiPage(`${url}/forge/code/lib`)).html, /lib\/cube/); assert.match((await uiPage(`${url}/forge/code/lib/cube`)).html, /A cube\./); assert.match((await uiPage(`${url}/forge/history`)).html, /lib\/cube/); const view = owner.tcm("contribution", "view", "1"); assert.match(view, /merged into main/); assert.match(view, /@grace \S+ \S+\n {4}Ready for review\./); assert.equal(server.tcm("run", "lib.cube", "2").trim(), "8"); }); }