Compare commits

...

2 Commits

Author SHA1 Message Date
Peter Steinberger
52edef225b fix: tolerate trailing semicolons in browser evaluate 2026-01-03 21:37:06 +00:00
Azade
609aa4a7c5 fix(browser): avoid esbuild __name helper in evaluateViaPlaywright
When tsx/esbuild compiles arrow functions, it adds a __name helper
for debugging. This helper doesn't exist in the browser context,
causing 'ReferenceError: __name is not defined' when using
page.evaluate() with inline functions.

The fix uses new Function() constructed at runtime, which esbuild
doesn't transform, avoiding the __name injection.
2026-01-03 20:54:11 +00:00
3 changed files with 65 additions and 36 deletions

View File

@ -18,6 +18,7 @@
- Discord: emit system events for reaction add/remove with per-guild reaction notifications (off|own|all|allowlist) (#140) — thanks @thewilloftheshadow.
### Fixes
- Browser tools: tolerate trailing semicolons in evaluate expressions (#153) — thanks @azade-c.
- Auto-reply: drop final payloads when block streaming to avoid duplicate Discord sends.
- Bash tool: default auto-background delay to 10s.
- Telegram: chunk block-stream replies to avoid “message is too long” errors (#124) — thanks @mukhtharcm.

View File

@ -232,4 +232,41 @@ describe("pw-tools-core", () => {
expect(dismiss).toHaveBeenCalled();
expect(accept).not.toHaveBeenCalled();
});
it("evaluates expressions with trailing semicolons", async () => {
const evaluate = vi.fn(
async (fn: (body: string) => unknown, body: string) => fn(body),
);
currentPage = { evaluate };
const mod = await importModule();
const result = await mod.evaluateViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
fn: "() => 42;",
});
expect(result).toBe(42);
expect(evaluate).toHaveBeenCalled();
});
it("evaluates element expressions with trailing semicolons", async () => {
const evaluate = vi.fn(
async (
fn: (el: { tagName: string }, body: string) => unknown,
body: string,
) => fn({ tagName: "DIV" }, body),
);
currentRefLocator = { evaluate };
currentPage = {};
const mod = await importModule();
const result = await mod.evaluateViaPlaywright({
cdpUrl: "http://127.0.0.1:18792",
fn: "(el) => el.tagName;",
ref: "1",
});
expect(result).toBe("DIV");
expect(sessionMocks.refLocator).toHaveBeenCalledWith(currentPage, "1");
});
});

View File

@ -207,53 +207,44 @@ export async function evaluateViaPlaywright(opts: {
}): Promise<unknown> {
const fnText = String(opts.fn ?? "").trim();
if (!fnText) throw new Error("function is required");
const fnBody = fnText.replace(/[;\s]+$/g, "");
if (!fnBody) throw new Error("function is required");
const page = await getPageForTargetId(opts);
ensurePageState(page);
if (opts.ref) {
const locator = refLocator(page, opts.ref);
return await locator.evaluate((el, fnBody) => {
const compileRunner = (body: string) => {
const inner = `"use strict"; const candidate = ${body}; return typeof candidate === "function" ? candidate(element) : candidate;`;
// This intentionally evaluates user-supplied code in the browser context.
// oxlint-disable-next-line typescript-eslint/no-implied-eval
return new Function("element", inner) as (element: Element) => unknown;
};
let compiled: unknown;
// Use Function constructor at runtime to avoid esbuild adding __name helper
// which doesn't exist in the browser context
const elementEvaluator = new Function(
"el",
"fnBody",
`
"use strict";
try {
compiled = compileRunner(fnBody);
var candidate = eval("(" + fnBody + ")");
return typeof candidate === "function" ? candidate(el) : candidate;
} catch (err) {
const message =
err instanceof Error
? err.message
: typeof err === "string"
? err
: "invalid expression";
throw new Error(`Invalid evaluate function: ${message}`);
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
}
return (compiled as (element: Element) => unknown)(el as Element);
}, fnText);
`,
) as (el: Element, fnBody: string) => unknown;
return await locator.evaluate(elementEvaluator, fnBody);
}
return await page.evaluate((fnBody) => {
const compileRunner = (body: string) => {
const inner = `"use strict"; const candidate = ${body}; return typeof candidate === "function" ? candidate() : candidate;`;
// This intentionally evaluates user-supplied code in the browser context.
// oxlint-disable-next-line typescript-eslint/no-implied-eval
return new Function(inner) as () => unknown;
};
let compiled: unknown;
// Use Function constructor at runtime to avoid esbuild adding __name helper
// which doesn't exist in the browser context
const browserEvaluator = new Function(
"fnBody",
`
"use strict";
try {
compiled = compileRunner(fnBody);
var candidate = eval("(" + fnBody + ")");
return typeof candidate === "function" ? candidate() : candidate;
} catch (err) {
const message =
err instanceof Error
? err.message
: typeof err === "string"
? err
: "invalid expression";
throw new Error(`Invalid evaluate function: ${message}`);
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
}
return (compiled as () => unknown)();
}, fnText);
`,
) as (fnBody: string) => unknown;
return await page.evaluate(browserEvaluator, fnBody);
}
export async function armFileUploadViaPlaywright(opts: {