testTracesAcrossTheBrowser
Traces across the browser: a page goes on in the trace the server began as it sent the page; its view, its
navigations and each call it makes are spans, a call saying which definition made it, with the server's spans for
it inside it; and tcm traces says where a definition's calls came from.
import { CHROME, withPage } from "#codebase/tcmts/cli/test/browser"; import { okIn, plainEnv, strip, tcmBin, tempDir, withChildren } from "#codebase/tcmts/cli/test/e2e/harness"; import { BROWSER_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 testTracesAcrossTheBrowser = () => withChildren(async (keep) => { if (!CHROME) return console.log("skipped: no Chrome"); const dir = tempDir("traces-browser"); 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"), BROWSER_BOARD); tcm("add"); 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]!; const trace = await withPage(url, async (page) => { await page.waitFor<boolean>("!!document.querySelector('button')", (v) => v); await page.evaluate("document.querySelector('button').click()"); await page.waitFor<string>("document.body.dataset.added", (v) => v === "1"); // a navigation without a page load: the next call is under it await page.evaluate("history.pushState({}, '', '/other')"); await page.evaluate("document.querySelector('button').click()"); await page.waitFor<string>("document.body.dataset.added", (v) => v === "2"); // leaving sends what the page recorded await page.evaluate("dispatchEvent(new Event('pagehide'))"); assert.deepEqual(page.errors, []); return page.evaluate<string>("__TCM_TRACE__.split('-')[1]"); }); assert.match(trace, /^[0-9a-f]{32}$/, "the page goes on in the trace the server began"); await waitFor( () => /client board\.add/.test(tcm("traces", "--trace", trace)), "the page's spans", ); const tree = tcm("traces", "--trace", trace); // the page the server sent, the browser's view of it, its call (the definition calling it said), the server's // answer inside it, and its statement inside that; then the navigation, and the call made under it assert.match(tree, /ms {2}page {2}\/\n/); assert.match(tree, /ms {4}view {2}\/\n/); assert.match(tree, /ms {6}client board\.add {2}from board\.submit, \/\n/); assert.match(tree, /ms {8}call board\.add\n/); assert.match(tree, /ms {10}sql posts INSERT/); assert.match(tree, /ms {6}navigate {2}\/other\n/); assert.match(tree, /ms {8}client board\.add {2}from board\.submit, \/other\n/); // where a definition's calls come from, and how long the page waited assert.match( tcm("traces", "board.add"), /from the browser[^\n]*\n[^\n]*\nboard\.submit\s+2\s+0\s/, ); assert.match(tcm("traces"), /board\.add\s+client\s+2\s/); })
uses
- tcmts.cli.test.browser.CHROME
- tcmts.cli.test.browser.withPage
- tcmts.cli.test.e2e.harness.okIn
- tcmts.cli.test.e2e.harness.plainEnv
- tcmts.cli.test.e2e.harness.strip
- tcmts.cli.test.e2e.harness.tcmBin
- tcmts.cli.test.e2e.harness.tempDir
- tcmts.cli.test.e2e.harness.withChildren
- tcmts.cli.test.e2e.traces.BROWSER_BOARD