testProductionPageErrorsNameTheirDefinition

tcmts.cli.test.e2e.traces.testProductionPageErrorsNameTheirDefinition · #ppdv47y4 · term

A production page's uncaught error is traced to the definition that threw it, through its chunk's source map, which the build keeps apart: the chunk doesn't name it, and it isn't served.

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

export const testProductionPageErrorsNameTheirDefinition = () =>
  withChildren(async (keep) => {
    if (!CHROME) return console.log("skipped: no Chrome");
    const dir = tempDir("traces-prod-errors");
    const tcm = (...args: string[]) => strip(okIn(dir, ...args));
    const waitFor = async (cond: () => boolean | Promise<boolean>, what: string, ms = 20000) => {
      for (
        const end = Date.now() + ms;
        Date.now() < end;
        await new Promise((r) => setTimeout(r, 200))
      )
        if (await cond()) return;
      throw new Error(`timed out waiting for ${what}`);
    };
    tcm("init");
    tcm("lib", "add", "tcm", "server", "--at", "lib.server");
    fs.writeFileSync(path.join(dir, "scratch.ts"), FAILING_BOARD);
    tcm("add", "-m", "Board: posts that fail");
    const child = keep(
      spawn(process.execPath, [tcmBin(), "start", "board.main", "--port", "0"], {
        cwd: dir,
        env: plainEnv(),
      }),
    );
    let out = "";
    child.stdout.setEncoding("utf8").on("data", (d: string) => (out += d));
    child.stderr.setEncoding("utf8").on("data", (d: string) => (out += d));
    await waitFor(() => /traces\s+kept/.test(out), "tcm start");
    const url = /url\s+(http:\/\/\S+)/.exec(out)![1]!;

    // the chunks don't name their maps, and the maps aren't served
    const maps = path.join(dir, ".tcm", "apps", "maps");
    const kept = fs.readdirSync(maps).filter((f) => f.endsWith(".js.map"));
    assert.ok(kept.length, "the build keeps its chunks' maps");
    for (const m of kept) {
      const chunk = await fetch(new URL(`/_app/assets/${m.replace(/\.map$/, "")}`, url));
      assert.equal(chunk.status, 200);
      assert.doesNotMatch(await chunk.text(), /sourceMappingURL/);
      assert.equal((await fetch(new URL(`/_app/assets/${m}`, url))).status, 404);
    }

    await withPage(url, async (page) => {
      await page.waitFor<boolean>("!!document.getElementById('boom')", (v) => v);
      // (a call first, so the page's trace is one the server took part in)
      await page.evaluate("document.getElementById('call').click()");
      await page.waitFor<string>("document.body.dataset.failed", (v) => v === "1");
      await page.evaluate("document.getElementById('boom').click()");
      await page.waitFor<boolean>("new Promise((r) => setTimeout(() => r(true), 100))", (v) => v);
      await page.evaluate("dispatchEvent(new Event('pagehide'))");
    });
    await waitFor(() => /in the browser/.test(tcm("traces", "--errors")), "the page's error");
    assert.match(
      tcm("traces", "--errors"),
      /\nboard\.press in the browser {2}Error: pressed \d+\n {2}1 time\(s\)[^\n]*\n {2}began with #[a-z2-7]{8}: "Board: posts that fail"/,
    );
  })

uses