testWebUi

tcmts.cli.test.e2e.contributions.testWebUi · #cd6irqx4 · term

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

uses