From 7d146fd6f8d17708d052a2e6b2dfb81b6270e5a9 Mon Sep 17 00:00:00 2001 From: Abdulhakeem Mustapha Date: Fri, 6 Nov 2026 19:22:37 +0100 Subject: [PATCH] test(desktop): browser_evaluate on pages that allow and forbid eval --- .../browser-automation/service.test.ts | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/packages/desktop/src/features/browser-automation/service.test.ts b/packages/desktop/src/features/browser-automation/service.test.ts index 6bc054c6a5..d6b252533e 100644 --- a/packages/desktop/src/features/browser-automation/service.test.ts +++ b/packages/desktop/src/features/browser-automation/service.test.ts @@ -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; +}) { + 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>) { 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();