pi-mono/packages/chord/test/node/web/page.test.ts
Mario Zechner ff33ef42c4 feat(chord): telemetry from pages and workers stored by the server, who a span was for
The four telemetry gaps (rows on commits, chord.host, microsecond times, a read-only store) and: pages record and
send span batches over their link to the server's ingest object (checked, capped, rate-limited, stamped browser and
with the link's principal); workers send theirs too, so the server is the store's only writer; principalAttributes
lets an app choose what a span records about its principal. The tutorial shows a real page's click traced to the
worker, with the trace viewer.
2026-11-04 11:59:14 +00:00

131 lines
6.2 KiB
TypeScript

// Tests of a page (slice 11 §2, §4b) in a real browser (Chromium; skipped without one): `startPage` loads the plugins
// of the combined file, keeps its link, applies a plugin edit in place keeping a component's state, writes the active
// theme as CSS variables following the colour scheme, and reloads when the server's base bundle changed.
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { renderTrace } from "@earendil-works/pi-telemetry/render";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { BACKGROUND_CONTEXT as context } from "../../../src/context/index.ts";
import { openNodeSqliteDatabase } from "../../../src/sqlite/node.ts";
import { createSqliteTelemetry } from "../../../src/telemetry-sqlite/index.ts";
import { NOTES_PLUGIN, tempDir } from "../app/support.ts";
import { Browser, CHROMIUM, poll } from "./browser.ts";
import { freePort, panelPlugin, signIn, versionedEntry, webApp } from "./support.ts";
describe.skipIf(CHROMIUM === undefined)("a page", () => {
let browser: Browser;
beforeAll(async () => {
browser = await Browser.start();
}, 120_000);
afterAll(async () => {
await browser?.close();
});
it("loads the plugins, applies an edit in place keeping the page's state, and follows the theme", async () => {
const { app, root } = await webApp(await versionedEntry("1"), await freePort(), {
plugins: { notes: panelPlugin("Notes", "#123456") },
});
try {
await browser.call("Emulation.setEmulatedMedia", {
features: [{ name: "prefers-color-scheme", value: "dark" }],
});
await signIn(browser, app.ownerLink());
await browser.wait('document.querySelector(".panel")?.textContent === "Notes"');
await browser.wait('globalThis.connection === "connected"');
await browser.wait(
'getComputedStyle(document.documentElement).getPropertyValue("--accent").trim() === "#123456"',
);
// Component state, then a plugin edit rolled out: the page applies it in place.
await browser.evaluate('document.getElementById("draft").focus()');
await browser.type("half-typed");
await writeFile(join(root, "notes", "web.ts"), panelPlugin("Notes v2", "#654321")["web.ts"]!);
while (!app.plugins.state.value.dirty.includes("@test/notes"))
await new Promise((resolve) => setTimeout(resolve, 10));
const result = await app.applyChanges(context);
expect(result.steps.map(({ name }) => name)).toEqual(["server", "web"]);
await browser.wait('document.querySelector(".panel")?.textContent === "Notes v2"');
await browser.wait(
'getComputedStyle(document.documentElement).getPropertyValue("--accent").trim() === "#654321"',
);
expect(await browser.evaluate<string>('document.getElementById("draft").value')).toBe("half-typed");
expect(await browser.evaluate<number>("globalThis.loads")).toBe(1);
// The colour scheme changes: the light theme (here the fallback), without a reload.
await browser.call("Emulation.setEmulatedMedia", {
features: [{ name: "prefers-color-scheme", value: "light" }],
});
await browser.wait(
'getComputedStyle(document.documentElement).getPropertyValue("--accent").trim() === "#0b62d6"',
);
expect(await browser.evaluate<string>("getComputedStyle(document.documentElement).colorScheme")).toBe("light");
} finally {
await app.close(context);
}
}, 120_000);
it("reloads when the server comes back with another base bundle", async () => {
const port = await freePort();
const entry = await versionedEntry("1");
const first = await webApp(entry, port, { plugins: { notes: panelPlugin("Notes", "#123456") } });
await signIn(browser, first.app.ownerLink());
await browser.wait('globalThis.version === "1" && globalThis.connection === "connected"');
await first.app.close(context);
await browser.wait('globalThis.connection === "waiting" || globalThis.connection === "connecting"');
await versionedEntry("2", join(entry, ".."));
const second = await webApp(entry, port, { dataDir: first.dataDir, root: first.root });
try {
expect(second.app.web!.base).not.toBe(first.app.web!.base);
await browser.wait('globalThis.version === "2" && globalThis.connection === "connected"', 60_000);
// The panel follows the connection by a frame or two (Opus L6).
await browser.wait('document.querySelector(".panel")?.textContent === "Notes"');
} finally {
await second.app.close(context);
}
}, 120_000);
it("records a click as the root of one trace through the server into the worker, stored by the server", async () => {
const telemetry = await createSqliteTelemetry(await openNodeSqliteDatabase(join(await tempDir(), "t.sqlite")));
const entry = fileURLToPath(new URL("../fixtures/web/telemetry.ts", import.meta.url));
const { app } = await webApp(entry, await freePort(), {
plugins: { notes: NOTES_PLUGIN },
withWorkers: true,
telemetry,
});
try {
await signIn(browser, app.ownerLink());
await browser.evaluate("document.getElementById('write').click()");
await browser.wait("globalThis.written === 1");
// The page sends every few seconds, the worker every second, the server writes every second.
let spans: Awaited<ReturnType<typeof telemetry.findTraces>>["traces"][number]["spans"] = [];
await poll(
async () => {
await telemetry.flush();
spans = (await telemetry.findTraces({ name: "page.write" })).traces[0]?.spans ?? [];
return spans.some(({ attributes }) => attributes.type === "test.store");
},
30_000,
"the click's trace",
);
const tree = renderTrace(
spans.map((span) => ({ ...span, attributes: { host: span.attributes["chord.host"] ?? "?" } })),
).replaceAll(/ \(\+?[\d.]+ms\)/g, "");
expect(tree.split("\n").filter((line) => !line.includes("·"))).toEqual([
'page.write host="browser"',
' chord.call host="browser"',
' chord.relay host="server"',
' chord.method host="project|p1"',
' chord.method host="project|p1"',
]);
const [click] = spans;
expect(click!.id).toMatch(/^browser:[0-9a-f-]{36}:\d+$/);
expect(click!.attributes["chord.principal"]).toBe('{"kind":"owner"}');
expect(new Set(spans.map(({ traceId }) => traceId))).toEqual(new Set([click!.id]));
} finally {
await app.close(context);
await telemetry.close();
}
}, 120_000);
});