testAppUnderABase

tcmts.cli.test.e2e.prod.testAppUnderABase · #4qzp433e · term

tcm start --base, behind tcm serve --proxy: the app is served under a path of the site, its page, assets, server functions and cookies under it, and its links (the react library's href) too, rendered on the server and followed in the browser

import { CHROME, hydrated, withPage } from "#codebase/tcmts/cli/test/browser";
import { okIn, plainEnv, tcmBin, tempDir, withChildren } from "#codebase/tcmts/cli/test/e2e/harness";
import { UNDER_A_BASE } from "#codebase/tcmts/cli/test/e2e/prod";
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import fs from "node:fs";
import path from "node:path";

export const testAppUnderABase = () =>
  withChildren(async (keep) => {
    const dir = tempDir("under a base");
    const tcm = (...args: string[]) => okIn(dir, ...args);
    tcm("init");
    tcm("lib", "add", "tcm", "server", "--at", "lib.server");
    fs.rmSync(path.join(dir, "scratch.ts"));
    tcm("lib", "add", "tcm", "react", "--at", "ui.lib.react");
    fs.writeFileSync(path.join(dir, "ui.tsx"), UNDER_A_BASE);
    tcm("add", "ui.tsx");
    // each server says where it is once it listens
    const listening = (args: string[], pattern: RegExp) =>
      new Promise<string>((resolve, reject) => {
        const child = keep(
          spawn(process.execPath, [tcmBin(), ...args], { cwd: dir, env: plainEnv() }),
        );
        let out = "";
        const seen = (d: string) => {
          out += d;
          const m = pattern.exec(out);
          if (m) resolve(m[1]!);
        };
        child.stdout.setEncoding("utf8").on("data", seen);
        child.stderr.setEncoding("utf8").on("data", seen);
        child.on("exit", (code) => reject(new Error(`tcm ${args[0]} exited ${code}:\n${out}`)));
      });
    const app = await listening(
      ["start", "ui.App", "--port", "0", "--base", "/apps/x"],
      /url\s+(http:\/\/[\d.]+:\d+)\/apps\/x\//,
    );
    const site = await listening(
      [
        "serve",
        "--port",
        "0",
        "--token",
        "t",
        "--proxy",
        `/apps/x=http://127.0.0.1:9|${app},/apps/gone=http://127.0.0.1:9`,
      ],
      /url\s+(https?:\/\/[\d.]+:\d+)/,
    );

    // (the app's path has two servers, as while one version replaces another: the first isn't answering, so every
    // request below, pages and server calls alike, is the second's)
    // the base is the app's page, with its slash; what's outside it isn't the app's
    const bare = await fetch(`${site}/apps/x`, { redirect: "manual" });
    assert.equal(bare.status, 308);
    assert.equal(bare.headers.get("location"), "/apps/x/");
    assert.equal((await fetch(`${app}/about`)).status, 404, "the app answers only under its base");
    // an app that isn't answering (starting): a page says so and tries again by itself, anything else when to try
    const starting = await fetch(`${site}/apps/gone/`, { headers: { accept: "text/html" } });
    assert.equal(starting.status, 503);
    assert.equal(starting.headers.get("retry-after"), "5");
    assert.match(await starting.text(), /http-equiv="refresh"/);
    assert.equal((await fetch(`${site}/apps/gone/__tcm/rpc/x`, { method: "POST" })).status, 503);
    // the first paint: the route within the base, its links and assets under it, the base said to the page
    const page = await (
      await fetch(`${site}/apps/x/about`, { headers: { accept: "text/html" } })
    ).text();
    assert.match(page, /<h1>about<\/h1>/);
    assert.match(page, /href="\/apps\/x\/about"/);
    assert.match(page, /var __TCM_BASE__ = "\/apps\/x"/);
    assert.doesNotMatch(page, /"\/_app\//);
    const script = /<script type="module"[^>]*src="([^"]+)"/.exec(page)?.[1];
    assert.ok(script?.startsWith("/apps/x/_app/assets/"), script);
    assert.equal((await fetch(`${site}${script}`)).status, 200);
    // the server, and cookies for the app's path
    if (CHROME) {
      await withPage(`${site}/apps/x/`, async (p) => {
        assert.ok(await hydrated(p));
        assert.equal(await p.evaluate(`document.querySelector("h1").textContent`), "home");
        await p.evaluate(`window.__stayed = 1; document.querySelector("#about").click()`);
        assert.equal(
          await p.waitFor<string>(
            `document.querySelector("h1").textContent`,
            (t) => t === "about",
            3000,
          ),
          "about",
        );
        assert.equal(
          await p.evaluate(`location.pathname + ":" + window.__stayed`),
          "/apps/x/about:1",
          "followed in the app",
        );
        await p.evaluate(`document.querySelector("#greet").click()`);
        assert.equal(
          await p.waitFor<string>(
            `document.querySelector("#said").textContent`,
            (t) => t === "hello you",
            5000,
          ),
          "hello you",
        );
        assert.match(
          await p.evaluate<string>("document.cookie"),
          /seen=yes/,
          "its cookie, for its path",
        );
      });
      const elsewhere = await withPage(`${site}/`, (p) => p.evaluate<string>("document.cookie"));
      assert.doesNotMatch(elsewhere ?? "", /seen=yes/, "and not the rest of the site's");
    }
  })

uses