diff options
Diffstat (limited to 'test-suite/unit/background-policy.test.mjs')
| -rw-r--r-- | test-suite/unit/background-policy.test.mjs | 469 |
1 files changed, 469 insertions, 0 deletions
diff --git a/test-suite/unit/background-policy.test.mjs b/test-suite/unit/background-policy.test.mjs new file mode 100644 index 0000000..e20d0ab --- /dev/null +++ b/test-suite/unit/background-policy.test.mjs @@ -0,0 +1,469 @@ +// Runtime regressions for the Pi session policy and the real worker dispatch/input paths. +// Chrome APIs are mocked: these tests prove explicit focus calls, not OS/Spaces behavior. +import assert from "node:assert/strict"; +import fs from "node:fs"; +import vm from "node:vm"; +import path from "node:path"; +import { stripTypeScriptTypes } from "node:module"; +import { test } from "node:test"; + +const indexSource = fs.readFileSync(new URL("../../extensions/chrome-profile-bridge/index.ts", import.meta.url), "utf8"); +const workerSource = fs.readFileSync(new URL("../../extensions/chrome-profile-bridge/browser-extension/service_worker.js", import.meta.url), "utf8"); +const clone = (value) => JSON.parse(JSON.stringify(value)); +function section(start, end) { + const from = indexSource.indexOf(start); + const to = indexSource.indexOf(end, from); + assert.ok(from >= 0 && to > from, `Missing source section: ${start}`); + return indexSource.slice(from, to); +} + +// Load the shipped policy, existing command handler, and actual tool registrations. Only the +// bridge/Pi UI/typebox/formatting/filesystem boundaries are replaced; no live server is opened. +function piHarness({ session = "alpha", send } = {}) { + const calls = [], tools = new Map(), notices = [], writes = []; + let authorized = true; + const ctx = { key: `session:${session}`, title: `Pi Session: ${session}`, cwd: "/fixture", ui: { notify: (...args) => notices.push(args) } }; + const bridge = { + connected: true, status: () => ({}), + async send(action, params, timeout, signal) { + calls.push({ action, params: clone(params), timeout, signal }); + if (signal?.aborted) throw new Error("Chrome command aborted"); + if (send) return send(action, params, timeout, signal); + if (action === "tab.version") return { capabilities: { hardBackground: true } }; + if (action === "tab.list") return []; + if (action.startsWith("page.screenshot")) return { dataUrl: "data:image/png;base64,dGVzdA==", method: "cdp" }; + return {}; + }, + }; + const Type = new Proxy({}, { get: () => (value = {}) => value }); + const sandbox = { + console, Buffer, Type, + Date, + DEFAULT_HOST: "127.0.0.1", DEFAULT_PORT: 17318, DEFAULT_TIMEOUT_MS: 30000, MAX_ELEMENTS: 80, + BACKGROUND_PARAM_DESCRIPTION: "background policy", + ChromeProfileBridge: function () { return bridge; }, + requireChromeControlAuthorized() { if (!authorized) throw new Error("Chrome control locked"); }, + sessionCtx: ctx, sessionKeyFor: (c) => c?.key, sessionGroupTitle: (c) => c.title, + chromeToolsRegistered: false, StringEnum: () => ({}), + tabActionValues: [], snapshotModeValues: [], waitForValues: [], imageFormatValues: [], + safeJson: JSON.stringify, truncateText: (s) => s, formatChromeSnapshot: JSON.stringify, + formatChromeInspect: JSON.stringify, summarizeActionResult: () => "", formatIncludedSnapshotText: (_r, text) => text, + workspaceCwd: () => ctx.cwd, ...path, + mkdir: async () => {}, writeFile: async (...args) => writes.push(args), + }; + const registrations = indexSource.slice(indexSource.indexOf("function registerChromeTools(pi:"), indexSource.lastIndexOf("\n}")); + vm.runInNewContext(stripTypeScriptTypes([ + section("const bridge = new ChromeProfileBridge(", "\n\tlet chromeAuthorizedUntil:"), + section("const authorizedBridgeSend =", '\n\tpi.on("session_start",'), + section("const BACKGROUND_DESC:", "\n\tconst authorizeFor ="), + registrations, + "globalThis.send = authorizedBridgeSend; globalThis.background = backgroundHandler;", + ].join("\n")), sandbox); + sandbox.registerChromeTools({ registerTool: (tool) => tools.set(tool.name, tool) }); + return { + calls, tools, notices, writes, ctx, + send: async (...args) => sandbox.send(...args), + background: (arg) => sandbox.background(ctx, arg), + tool: (name, params = {}, signal) => tools.get(name).execute("test", params, signal, undefined, ctx), + authorize: (value) => { authorized = value; }, + }; +} + +const flagCases = [ + [{}, false], [{ background: false }, false], [{ background: true }, true], + [{ foreground: true }, false], [{ foreground: false }, true], + [{ background: false, foreground: false }, false], [{ background: true, foreground: true }, true], +]; + +test("session background on overrides all per-call/legacy foreground flags; off preserves per-call background", async () => { + const h = piHarness(); + for (const mode of ["on", "off"]) { + await h.background(mode); + for (const [flags, requestedBackground] of flagCases) { + const params = Object.freeze({ ...flags, targetId: "2" }); + await h.send("page.snapshot", params); + const wire = h.calls.at(-1).params; + assert.equal(wire.background, mode === "on" || requestedBackground); + assert.equal(wire.foreground, !wire.background); + assert.equal(wire.sessionKey, "session:alpha"); + assert.equal(wire.sessionGroupTitle, "Pi Session: alpha"); + assert.equal(wire.joinSessionGroup, true); + assert.deepEqual(params, { ...flags, targetId: "2" }, "caller params not mutated"); + } + } +}); + +test("every registered page tool, tab.new, and chrome_launch(url) use the central policy", async () => { + const h = piHarness(); + const cases = [ + ["chrome_launch", { url: "https://example.test" }, "tab.new"], + ["chrome_tab", { action: "new", group: false, groupTitle: "other" }, "tab.new"], + ["chrome_snapshot", {}, "page.snapshot"], ["chrome_find", { query: "button" }, "page.snapshot"], + ["chrome_inspect", { uid: "el-1" }, "page.inspect"], ["chrome_navigate", { url: "https://example.test" }, "page.navigate"], + ["chrome_evaluate", { expression: "1" }, "page.evaluate"], ["chrome_click", { uid: "el-1" }, "page.click"], + ["chrome_type", { text: "abc" }, "page.type"], ["chrome_fill", { text: "abc" }, "page.fill"], + ["chrome_key", { key: "Enter" }, "page.key"], ["chrome_wait_for", { kind: "expression", value: "true" }, "page.waitFor"], + ["chrome_list_console_messages", {}, "page.console.list"], ["chrome_list_network_requests", {}, "page.network.list"], + ["chrome_get_network_request", { requestId: "1" }, "page.network.get"], ["chrome_screenshot", {}, "page.screenshot"], + ["chrome_hover", {}, "page.hover"], ["chrome_drag", {}, "page.drag"], ["chrome_tap", {}, "page.tap"], + ["chrome_scroll", {}, "page.scroll"], ["chrome_upload_file", { paths: ["fixture.txt"] }, "page.upload"], + ]; + for (const [name, params, action] of cases) { + await h.tool(name, { ...params, background: false, foreground: true }); + const call = h.calls.at(-1); + assert.equal(call.action, action === "tab.new" || action === "page.screenshot" ? `${action}.background` : action, name); + assert.equal(call.params.foreground, false, name); + assert.equal(call.params.background, true, name); + if (action === "tab.new") assert.equal(call.params.groupTitle, "Pi Session: alpha"); + } + assert.equal(h.writes[0][1].toString(), "test", "screenshot tool still writes decoded bytes"); +}); + +test("activation is rejected before dispatch; existing on/off/toggle/status commands suffice", async () => { + const h = piHarness(); + await assert.rejects(h.tool("chrome_tab", { action: "activate", targetId: "2", background: false }), /background mode.*\/chrome background off/); + assert.equal(h.calls.length, 0); + await h.background("off"); + await h.tool("chrome_tab", { action: "activate", targetId: "2" }); + assert.equal(h.calls.at(-1).params.foreground, true); + await h.background("toggle"); + await h.background("status"); + assert.match(h.notices.at(-1)[0], /on.*Hard background/); + for (const arg of ["lock", "unlock", "invalid"]) { + await h.background(arg); + assert.match(h.notices.at(-1)[0], /Unknown background setting/); + await assert.rejects(h.send("tab.activate", {}), /background mode/); + } + await h.background(""); + await h.send("tab.activate", {}); + for (const arg of ["true", "1", "on"]) { + await h.background(arg); + await assert.rejects(h.send("tab.activate", {}), /background mode/); + } + for (const arg of ["false", "0", "off"]) { + await h.background(arg); + await h.send("tab.activate", {}); + } +}); + +test("older companions reject background-only wire actions; launch and screenshots never retry unsafe actions", async () => { + const h = piHarness({ send: async (action) => { throw new Error(`Unknown action: ${action}`); } }); + for (const [tool, params] of [ + ["chrome_tab", { action: "new" }], ["chrome_launch", { url: "https://example.test" }], + ["chrome_screenshot", {}], ["chrome_screenshot", { fullPage: true }], + ]) { + await assert.rejects(h.tool(tool, params), /Reload Pi Chrome Connector/); + } + assert.deepEqual(h.calls.map((c) => c.action), ["tab.new.background", "tab.new.background", "page.screenshot.background", "page.screenshot.background"]); +}); + +test("transport failures propagate without fallback; single dispatch preserves authorization/timeout/signal", async () => { + const offline = piHarness({ send: async () => { throw new Error("offline"); } }); + await assert.rejects(offline.send("tab.new", {}), /offline/); + assert.equal(offline.calls.length, 1); + const h = piHarness(), controller = new AbortController(); + await h.send("tab.new", {}, 900, controller.signal); + assert.equal(h.calls.length, 1); + assert.equal(h.calls[0].timeout, 900); + assert.equal(h.calls[0].signal, controller.signal); + controller.abort(); + await assert.rejects(h.send("tab.new", {}, 900, controller.signal), /Chrome command aborted/); + h.authorize(false); + await assert.rejects(h.send("tab.new", {}), /Chrome control locked/); + assert.equal(h.calls.length, 2, "locked call does not reach bridge"); +}); + +test("shared-owner sessions retain independent policies; per-call background still works with mode off", async () => { + const sharedCalls = []; + const send = async (action, params) => { sharedCalls.push({ action, params }); return {}; }; + const a = piHarness({ session: "A", send }), b = piHarness({ session: "B", send }); + await b.background("off"); + await a.send("tab.new", { background: false }); + await b.send("tab.new", {}); + assert.equal(sharedCalls[0].params.sessionKey, "session:A"); + assert.equal(sharedCalls[0].params.foreground, false); + assert.equal(sharedCalls[0].action, "tab.new.background"); + assert.equal(sharedCalls[1].params.sessionKey, "session:B"); + assert.equal(sharedCalls[1].params.foreground, true); + assert.equal(sharedCalls[1].action, "tab.new"); + await b.send("tab.new", { background: true }); + assert.equal(sharedCalls[2].action, "tab.new.background"); + assert.equal(sharedCalls[2].params.foreground, false); +}); + +function workerHarness({ withWindows = true } = {}) { + const calls = []; + const tabs = new Map([ + [1, { id: 1, windowId: 1, active: true, groupId: -1, url: "https://user.test/", title: "User" }], + [2, { id: 2, windowId: 1, active: false, groupId: -1, url: "https://target.test/", title: "Target" }], + ]); + let focusedWindow = 1, nextTab = 3, nextWindow = 2; + const storage = {}, groups = new Map(); + const listener = { addListener() {}, removeListener() {} }; + const chrome = { + runtime: { id: "test", getManifest: () => ({ version: "0.0.0" }), onInstalled: listener, onStartup: listener }, + alarms: { onAlarm: listener, create() {} }, action: { onClicked: listener }, webNavigation: { onCommitted: listener }, + storage: { session: { get: async (key) => ({ [key]: storage[key] }), set: async (value) => Object.assign(storage, value) } }, + tabs: { + onUpdated: listener, + query: async (query = {}) => [...tabs.values()].filter((t) => (!query.active || t.active) && (query.windowId === undefined || query.windowId === t.windowId)).map(clone), + get: async (id) => { if (!tabs.has(id)) throw new Error("No tab"); return clone(tabs.get(id)); }, + create: async (params) => { + calls.push({ action: "tabs.create", params }); + const tab = { id: nextTab++, windowId: params.windowId ?? 1, active: params.active, url: params.url, groupId: -1 }; + if (tab.active) for (const t of tabs.values()) if (t.windowId === tab.windowId) t.active = false; + tabs.set(tab.id, tab); + return clone(tab); + }, + update: async (id, params) => { + calls.push({ action: "tabs.update", id, params }); + const tab = tabs.get(id); + if (params.active) for (const t of tabs.values()) if (t.windowId === tab.windowId) t.active = false; + Object.assign(tab, params); + return clone(tab); + }, + remove: async (id) => tabs.delete(id), + group: async ({ groupId, tabIds }) => { + const id = groupId ?? groups.size + 1; + if (!groups.has(id)) groups.set(id, { id, windowId: tabs.get(tabIds[0]).windowId }); + for (const tid of tabIds) tabs.get(tid).groupId = id; + return id; + }, + ungroup: async (id) => { tabs.get(id).groupId = -1; }, + captureVisibleTab: async () => { calls.push({ action: "captureVisibleTab" }); throw new Error("activation-based capture forbidden"); }, + }, + tabGroups: { + query: async ({ windowId } = {}) => [...groups.values()].filter((g) => windowId === undefined || g.windowId === windowId).map(clone), + get: async (id) => groups.get(id), + update: async (id, props) => Object.assign(groups.get(id), props), + }, + windows: { + update: async (id, params) => { calls.push({ action: "windows.update", id, params }); if (params.focused) focusedWindow = id; }, + }, + debugger: { + onDetach: listener, getTargets: (cb) => cb([]), attach: async () => {}, detach: async () => {}, + sendCommand: (target, method, params, callback) => { + calls.push({ action: "cdp", target, method, params }); + try { callback(h.cdpResult(method, params)); } + catch (error) { chrome.runtime.lastError = { message: error.message }; callback(); chrome.runtime.lastError = undefined; } + }, + }, + scripting: { executeScript: async () => [{ result: { ok: true, value: { elements: [], url: "https://target.test/" } } }] }, + }; + if (withWindows) chrome.windows.create = async (params) => { + calls.push({ action: "windows.create", params }); + const id = nextWindow++; + if (params.focused) focusedWindow = id; + const tab = { id: nextTab++, windowId: id, active: true, url: params.url, groupId: -1 }; + tabs.set(tab.id, tab); + return { id, tabs: [clone(tab)] }; + }; + const w = { + chrome, console, setTimeout, clearTimeout, setInterval: () => 0, clearInterval() {}, + fetch: async () => { throw new Error("no network in unit tests"); }, navigator: { userAgent: "unit-test" }, + }; + w.self = w; + vm.runInNewContext(workerSource, w); + w.sleep = async () => {}; // Skip human timing, not CDP dispatch. + w.resolveTargetInTab = async () => ({ found: true, x: 40, y: 50, tag: "BUTTON" }); + const h = { + w, chrome, calls, tabs, focusedWindow: () => focusedWindow, + cdpResult: (method) => method === "Page.captureScreenshot" ? { data: "c2NyZWVuc2hvdA==" } + : method === "DOM.requestNode" ? { nodeId: 1 } + : { result: { type: "boolean", value: true, objectId: "file-input" } }, + }; + return h; +} +function assertNoFocus(h) { + assert.equal(h.focusedWindow(), 1); + assert.equal(h.tabs.get(1).active, true); + assert.ok(!h.calls.some((c) => c.action === "windows.update" || c.action === "captureVisibleTab" || (c.action === "tabs.update" && c.params.active) || (c.action === "tabs.create" && c.params.active))); +} + +test("Pi-to-worker background aliases preserve grouping, screenshot output, and ownership-aware cleanup", async () => { + const h = workerHarness(); + const pi = piHarness({ send: (action, params) => h.w.dispatch(action, params) }); + const opened = await pi.tool("chrome_tab", { action: "new", group: false, groupTitle: "wrong", background: false }); + assert.equal(opened.details.result.group.title, "Pi Session: alpha"); + const launched = await pi.tool("chrome_launch", { url: "https://fixture.test" }); + const shot = await pi.tool("chrome_screenshot", { targetId: "2", background: false }); + assert.equal(shot.details.method, "cdp"); + assert.equal(pi.writes[0][1].toString(), "screenshot"); + assertNoFocus(h); + await h.w.dispatch("automation.cleanup", { sessionKey: "session:alpha" }); + assert.ok(!h.tabs.has(opened.details.result.tab.id)); + assert.ok(!h.tabs.has(launched.details.result.tab.id)); + assert.ok(h.tabs.has(1) && h.tabs.has(2), "user/adopted tabs survive cleanup"); + assert.equal(h.tabs.get(2).groupId, -1, "adopted tab is safely ungrouped"); +}); + +test("worker advertises support, keeps new tabs inactive, and rejects activation before target resolution", async () => { + const h = workerHarness(); + assert.equal((await h.w.dispatch("tab.version", {})).capabilities.hardBackground, true); + for (const flags of [{}, { foreground: false }, { background: true, foreground: true }]) { + const result = await h.w.dispatch("tab.new", { ...flags, sessionKey: "alpha", groupTitle: "Pi Session: alpha" }); + assert.equal(result.tab.active, false); + await assert.rejects(h.w.dispatch("tab.activate", { ...flags, targetId: "missing" }), /background mode/); + } + assertNoFocus(h); + const active = await h.w.dispatch("tab.activate", { targetId: "2", foreground: true, background: false }); + assert.equal(active.id, 2); + assert.equal(active.active, true); + const opened = await h.w.dispatch("tab.new", { foreground: true }); + assert.equal(opened.tab.active, true); +}); + +test("background-only worker aliases force safe behavior even with foreground flags", async () => { + const h = workerHarness(); + const opened = await h.w.dispatch("tab.new.background", { foreground: true, background: false }); + assert.equal(opened.tab.active, false); + const shot = await h.w.dispatch("page.screenshot.background", { targetId: "2", foreground: true, background: false }); + assert.equal(shot.method, "cdp"); + assertNoFocus(h); +}); + +test("implicit automation windows stay unfocused; tab fallback stays inactive", async () => { + for (const withWindows of [true, false]) { + const h = workerHarness({ withWindows }); + const tab = await h.w.dispatch("page.navigate", { url: "https://fixture.test", waitUntilLoad: false, background: true, foreground: true, sessionKey: "alpha" }); + assert.notEqual(tab.id, 1); + assertNoFocus(h); + assert.ok(h.calls.filter((c) => c.action === "windows.create").every((c) => c.params.focused === false)); + } +}); + +test("all worker page/input paths veto conflicting foreground flags without replacing trusted input", async () => { + const h = workerHarness(); + const actions = [ + ["navigate", { url: "https://target.test/", waitUntilLoad: false }], ["snapshot", {}], ["inspect", { uid: "el-1" }], + ["evaluate", { expression: "true" }], ["waitFor", { kind: "expression", value: "true" }], + ["console.list", {}], ["network.list", {}], ["network.get", { requestId: "1" }], + ["click", { x: 40, y: 50, includeSnapshot: true }], ["fill", { selector: "input", text: "a", submit: true }], + ["type", { selector: "input", text: "a", pressEnter: true }], ["key", { key: "Enter" }], ["hover", { x: 40, y: 50 }], + ["drag", { fromX: 40, fromY: 50, toX: 80, toY: 90 }], ["tap", { x: 40, y: 50 }], + ["scroll", { deltaY: 10 }], ["upload", { selector: "input", paths: ["/fixture.txt"] }], + ]; + for (const [action, params] of actions) { + const result = await h.w.dispatch(`page.${action}`, { targetId: "2", ...params, background: true, foreground: true, domFallback: false }); + if (["click", "fill", "type", "key", "hover", "drag", "tap", "scroll"].includes(action)) assert.equal((result.result ?? result).input, "chrome", action); + assertNoFocus(h); + } + const methods = new Set(h.calls.filter((c) => c.action === "cdp").map((c) => c.method)); + for (const method of ["Input.dispatchMouseEvent", "Input.dispatchKeyEvent", "Input.dispatchTouchEvent", "DOM.setFileInputFiles"]) assert.ok(methods.has(method), method); + h.chrome.debugger = undefined; + for (const action of ["click", "fill"]) { + await assert.rejects(h.w.dispatch(`page.${action}`, { targetId: "2", selector: "input", text: "a", background: true, domFallback: false }), /chrome.debugger API unavailable/); + } + assertNoFocus(h); +}); + +test("CDP PNG/JPEG screenshots preserve active tabs and pass viewport/quality options", async () => { + for (const targetId of ["1", "2"]) { + for (const options of [{}, { format: "png", quality: 70 }, { format: "jpeg", quality: 0 }, { format: "jpeg", quality: 83 }]) { + const h = workerHarness(); + const result = await h.w.dispatch("page.screenshot", { targetId, background: true, foreground: true, ...options }); + const capture = h.calls.find((c) => c.method === "Page.captureScreenshot"); + const format = options.format ?? "png"; + assert.equal(result.dataUrl, `data:image/${format};base64,c2NyZWVuc2hvdA==`); + assert.equal(result.method, "cdp"); + assert.equal(result.tab.id, Number(targetId)); + assert.equal(capture.target.tabId, Number(targetId)); + assert.deepEqual(clone(capture.params), { format, fromSurface: true, captureBeyondViewport: false, ...(format === "jpeg" ? { quality: options.quality } : {}) }); + assertNoFocus(h); + } + } +}); + +test("screenshot failures never fall back to activation/visible-tab capture", async () => { + for (const failure of ["attach", "capture", "empty", "missing"]) { + const h = workerHarness(); + if (failure === "attach") h.chrome.debugger.attach = async () => { throw new Error("permission denied"); }; + else h.cdpResult = () => { + if (failure === "capture") throw new Error("capture failed"); + return failure === "empty" ? { data: "" } : undefined; + }; + await assert.rejects(h.w.dispatch("page.screenshot", { targetId: "2", background: true }), /no tab-activation fallback/); + assertNoFocus(h); + } +}); + +test("screenshot does not restore/override a user tab switch during capture; watch mode explicitly activates", async () => { + const h = workerHarness(); + h.cdpResult = () => { + h.tabs.get(1).active = false; + h.tabs.get(2).active = true; // Human selection while CDP is pending. + return { data: "dGVzdA==" }; + }; + const result = await h.w.dispatch("page.screenshot", { targetId: "1", background: true }); + assert.equal(result.tab.id, 1); + assert.equal(h.tabs.get(2).active, true); + assert.ok(!h.calls.some((c) => c.action === "tabs.update" || c.action === "windows.update")); + + const watch = workerHarness(); + const shot = await watch.w.dispatch("page.screenshot", { targetId: "2", foreground: true, background: false }); + assert.equal(shot.tab.active, true); + assert.equal(watch.calls.filter((c) => c.action === "tabs.update" && c.params.active).length, 1); + assert.equal(watch.calls.filter((c) => c.action === "windows.update").length, 1); + assert.ok(!watch.calls.some((c) => c.action === "captureVisibleTab")); +}); + +test("challenge 43 grader rejects activation/synthetic input and retains sticky failures", () => { + const lib = fs.readFileSync(new URL("../_lib.js", import.meta.url), "utf8"); + const html = fs.readFileSync(new URL("../challenges/43-hard-background.html", import.meta.url), "utf8"); + const script = html.match(/<script>([\s\S]*?)<\/script>/)[1]; + function page() { + const listeners = new Map(); + const add = (name, fn) => listeners.set(name, fn); + const document = { + hidden: true, get visibilityState() { return this.hidden ? "hidden" : "visible"; }, hasFocus: () => false, + body: { insertBefore() {} }, createElement: () => ({ style: {} }), + getElementById: (id) => id === "verify" ? { addEventListener: add } : null, + addEventListener: add, + }; + const p = { document, addEventListener: add, location: { search: "" }, URLSearchParams, + performance: { now: () => 1 }, localStorage: { setItem() {} } }; + p.window = p; + vm.runInNewContext(`${lib}\n${script}`, p); + return { p, document, fire: (name, event = {}) => listeners.get(name)(event) }; + } + const visible = page(); + visible.document.hidden = false; + assert.throws(() => visible.p.armBackgroundProbe(), /Select another tab/); + const good = page(); + good.p.armBackgroundProbe(); + assert.throws(() => good.p.armBackgroundProbe(), /Already armed/); + good.fire("click", { isTrusted: true }); + assert.equal(good.p.__verdict, "PASS"); + good.document.hidden = false; + good.fire("visibilitychange"); + assert.equal(good.p.__verdict, "FAIL", "late activation must invalidate an earlier pass"); + const synthetic = page(); + synthetic.p.armBackgroundProbe(); + synthetic.fire("click", { isTrusted: false }); + synthetic.fire("click", { isTrusted: true }); + assert.equal(synthetic.p.__verdict, "FAIL", "trusted retry cannot hide synthetic input"); +}); + +test("full-page tiles remain CDP-only, pin the resolved target, and restore both scroll axes after success/failure", async () => { + for (const fail of [false, true]) { + const h = workerHarness(), scrolls = []; + h.w.executeInTab = async (params, fn, args) => { + assert.equal(params.targetId, 2); + assert.equal(params.foreground, false); + if (fn.name === "captureFullPageTiles") return { width: 800, height: 1200, viewportHeight: 600, dpr: 2, originalScrollY: 73, originalScrollX: 31, tiles: [{ y: 0, scrollY: 0 }, { y: 600, scrollY: 600 }] }; + scrolls.push(clone(args)); + }; + let captures = 0; + h.cdpResult = () => { if (++captures === 2 && fail) throw new Error("tile failed"); return { data: "dGlsZQ==" }; }; + const shot = h.w.dispatch("page.screenshot", { urlIncludes: "target.test", fullPage: true, background: true, foreground: true }); + if (fail) await assert.rejects(shot, /tile failed/); + else { + const result = await shot; + assert.equal(result.method, "cdp"); + assert.equal(result.tiles.length, 2); + assert.equal(result.dimensions.dpr, 2); + } + assert.deepEqual(scrolls, [[0], [600], [73, 31]]); + assertNoFocus(h); + } +}); |
