test(desktop): browser_evaluate on pages that allow and forbid eval

This commit is contained in:
Abdulhakeem Mustapha
2026-11-06 19:22:37 +01:00
parent 03c531dfeb
commit 7d146fd6f8
@@ -1,4 +1,5 @@
import { resolve as resolvePath } from "node:path";
import { createContext, runInContext } from "node:vm";
import { describe, expect, test, vi } from "vitest";
import type {
@@ -434,6 +435,34 @@ function containsScript(tab: FakeTab, ...parts: string[]): boolean {
return tab.scripts.some((script) => parts.every((part) => script.includes(part)));
}
/**
* Runs the script the host injects into a page the way Chromium runs it on a page whose Content
* Security Policy omits 'unsafe-eval': injected code runs, but code in the page cannot turn a
* string into code (eval, new Function). Node's vm refuses string code generation the same way.
*/
function pageRunningInjectedScripts(input: {
allowEval: boolean;
globals?: Record<string, unknown>;
}) {
const page = createContext(
{ document: { title: "Fixture" }, ...input.globals },
{ codeGeneration: { strings: input.allowEval, wasm: input.allowEval } },
);
return (script: string): unknown => runInContext(script, page);
}
/** A browser whose tab runs the evaluate script in `page`; its other scripts keep the fake answers. */
function browserEvaluatingIn(page: (script: string) => unknown) {
const browser = new BrowserAutomationHarness();
const fakeAnswer = browser.tab.executeJavaScript.bind(browser.tab);
browser.tab.executeJavaScript = async (script: string) => {
if (!script.includes("__PASEO_BROWSER_EVALUATE__")) return fakeAnswer(script);
browser.tab.scripts.push(script);
return page(script);
};
return browser;
}
function requireSnapshotRefs(result: Awaited<ReturnType<BrowserAutomationHarness["snapshot"]>>) {
expect(result).toEqual({
requestId: "req-snapshot",
@@ -1558,6 +1587,97 @@ describe("executeAutomationCommand", () => {
expect(containsScript(browser.tab, "resultJson.slice(0, 79000)")).toBe(true);
});
describe.each([
{ page: "allows eval", allowEval: true },
{ page: "forbids eval (its Content Security Policy omits 'unsafe-eval')", allowEval: false },
])("evaluate on a page that $page", ({ allowEval }) => {
const evaluate = (browser: BrowserAutomationHarness, source: string) =>
browser.execute({
command: "evaluate",
args: { browserId: BROWSER_A, function: source },
});
test("runs the function and returns its result as JSON", async () => {
const browser = browserEvaluatingIn(pageRunningInjectedScripts({ allowEval }));
expect(await evaluate(browser, "() => document.title")).toEqual({
requestId: "req-evaluate",
ok: true,
result: {
command: "evaluate",
browserId: BROWSER_A,
resultJson: '"Fixture"',
truncated: false,
},
});
});
test("awaits an async function", async () => {
const browser = browserEvaluatingIn(pageRunningInjectedScripts({ allowEval }));
const result = await evaluate(
browser,
"async () => { await Promise.resolve(); return 6 * 7; }",
);
expect(result).toMatchObject({ ok: true, result: { resultJson: "42" } });
});
test("resolves names against the page's globals, never the wrapper's own", async () => {
const globals = {
element: "page element",
args: "page args",
value: "page value",
resultJson: "page json",
userFunction: "page function",
};
const browser = browserEvaluatingIn(pageRunningInjectedScripts({ allowEval, globals }));
const result = await evaluate(
browser,
"() => [element, args, value, resultJson, userFunction]",
);
expect(result).toMatchObject({
ok: true,
result: { resultJson: JSON.stringify(Object.values(globals)) },
});
});
test("reports what the function throws", async () => {
const browser = browserEvaluatingIn(pageRunningInjectedScripts({ allowEval }));
expect(await evaluate(browser, "() => { throw new Error('boom'); }")).toMatchObject({
ok: false,
error: { code: "browser_unknown_error", message: "boom" },
});
});
test("reports an expression that is not a function", async () => {
const browser = browserEvaluatingIn(pageRunningInjectedScripts({ allowEval }));
expect(await evaluate(browser, "document.title")).toMatchObject({
ok: false,
error: {
code: "browser_unknown_error",
message: "browser_evaluate input must evaluate to a function.",
},
});
});
test("reports a syntax error with the compiler's own message, without touching the page", async () => {
const browser = browserEvaluatingIn(pageRunningInjectedScripts({ allowEval }));
const result = await evaluate(browser, "() => {");
expect(result).toMatchObject({
ok: false,
error: { code: "browser_unknown_error", message: expect.stringMatching(/^Unexpected /) },
});
expect(containsScript(browser.tab, "__PASEO_BROWSER_EVALUATE__")).toBe(false);
});
});
test("screenshot captures the painted viewport", async () => {
const browser = new BrowserAutomationHarness();