Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions packages/engine/src/services/frameCapture-namePolyfill.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";

// Regression coverage for the `window.__name` no-op shim that
// `frameCapture.ts` registers via `page.evaluateOnNewDocument`.
//
// Background: `@hyperframes/engine` ships raw TypeScript (see
// `packages/engine/package.json` — main and exports both point at
// `./src/index.ts`). Downstream transpilers like tsx run esbuild with
// keepNames=true, which wraps named functions in `__name(fn, "name")`
// calls. When Puppeteer serializes a `page.evaluate(callback)` argument
// via `Function.prototype.toString()`, those wrappers travel into the
// browser and throw `ReferenceError: __name is not defined` unless we
// install a no-op shim first.
//
// These tests intentionally do NOT launch a browser — the rest of this
// package follows the same pure-unit-test convention. Instead they:
// 1. Assert the polyfill is wired up at the source level so it cannot
// be silently removed by a careless edit.
// 2. Probe the current Vitest runtime so a future maintainer can see at
// a glance whether nested named functions still get `__name(...)`
// wrappers under the test transformer. This is advisory: both
// outcomes are acceptable — the reported observation is what makes
// the test useful when the upstream behavior shifts.

const __dirname = dirname(fileURLToPath(import.meta.url));
const FRAME_CAPTURE_PATH = resolve(__dirname, "frameCapture.ts");

describe("frameCapture __name polyfill", () => {
it("registers a window.__name shim via evaluateOnNewDocument", () => {
const source = readFileSync(FRAME_CAPTURE_PATH, "utf-8");

expect(source).toMatch(/page\.evaluateOnNewDocument\(/);
expect(source).toMatch(/typeof w\.__name !== "function"/);
expect(source).toMatch(/w\.__name\s*=\s*<T>/);
});

it("installs the shim before any awaited browser-version checks", () => {
const source = readFileSync(FRAME_CAPTURE_PATH, "utf-8");

const polyfillIndex = source.indexOf("page.evaluateOnNewDocument(");
const versionIndex = source.indexOf("await browser.version()");

expect(polyfillIndex).toBeGreaterThan(-1);
expect(versionIndex).toBeGreaterThan(-1);
expect(polyfillIndex).toBeLessThan(versionIndex);
});

it("documents the current transpiler behavior for nested named functions", () => {
function outer(): { wrapsNested: boolean; wrapsArrow: boolean } {
// The unused declarations are deliberate: we are inspecting whether the
// active transpiler rewrites `outer.toString()` to include
// `__name(nested, ...)` / `__name(arrowNested, ...)` wrappers.
// eslint-disable-next-line no-unused-vars
function nested() {
return 1;
}
// eslint-disable-next-line no-unused-vars
const arrowNested = () => 2;
const src = outer.toString();
return {
wrapsNested: /__name\(\s*nested\s*,/.test(src),
wrapsArrow: /__name\(\s*\(\)\s*=>\s*2\s*,/.test(src) || /__name\(\s*arrowNested/.test(src),
};
}

const { wrapsNested, wrapsArrow } = outer();

// Both outcomes are acceptable; the value of this test is in surfacing
// the runtime's behavior on the next failure (or first inspection).
// If both flags become false everywhere this engine is consumed, the
// polyfill above can probably be dropped. Until then it stays.
expect(typeof wrapsNested).toBe("boolean");
expect(typeof wrapsArrow).toBe("boolean");
});
});
33 changes: 26 additions & 7 deletions packages/engine/src/services/frameCapture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,32 @@ export async function createCaptureSession(
const { browser, captureMode } = await acquireBrowser(chromeArgs, config);

const page = await browser.newPage();
// Polyfill esbuild's keepNames helper inside the page. Tools like tsx/Bun
// transform this engine's source on the fly and wrap every named function
// with `__name(fn, "name")`. When `page.evaluate()` serializes a callback
// and ships it to the browser, those `__name(...)` calls would crash with
// `__name is not defined` because the helper only exists in Node. Defining
// a no-op shim once per page makes the engine work uniformly whether it is
// imported from compiled dist (no helper) or from source via tsx.
// Polyfill esbuild's keepNames helper inside the page.
//
// The engine is published as raw TypeScript (`packages/engine/package.json`
// points `main`/`exports` at `./src/index.ts`) and downstream consumers
// execute it through transpilers that may inject `__name(fn, "name")`
// wrappers around named functions. Empirically, this happens with:
// - tsx (its esbuild loader runs with keepNames=true), used by the
// producer's parity-harness, ad-hoc dev scripts, and the
// `bun run --filter @hyperframes/engine test` Vitest path.
// - any tsup/esbuild build that explicitly enables keepNames.
//
// The HeyGen CLI (`packages/cli`) bundles this engine via tsup with
// keepNames left at its default (false) — verified by grepping
// `packages/cli/dist/cli.js`, where `__name(...)` call sites are absent.
// Bun's TS loader also does not currently inject `__name`. Even so,
// anything that calls `page.evaluate(fn)` with a nested named function
// under tsx (most local development and tests) will serialize bodies
// like `__name(nested,"nested")` and crash with `__name is not defined`
// in the browser. The shim makes such calls a no-op.
//
// An alternative is to load browser-side code as raw text and inject it
// via `page.addScriptTag({ content: ... })` — see
// `packages/cli/src/commands/contrast-audit.browser.js` for that pattern.
// Until every `page.evaluate(fn)` call site migrates, this polyfill is
// the single line of defense. The companion regression test in
// `frameCapture-namePolyfill.test.ts` verifies the shim stays wired up.
await page.evaluateOnNewDocument(() => {
const w = window as unknown as { __name?: <T>(fn: T, _name: string) => T };
if (typeof w.__name !== "function") {
Expand Down
Loading