| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | import { build } from "esbuild"; |
| 4 | import { createRequire } from "node:module"; |
| 5 | import { mkdtemp, copyFile, rm } from "node:fs/promises"; |
| 6 | import { tmpdir } from "node:os"; |
| 7 | import { resolve, join } from "node:path"; |
| 8 | import { JSDOM } from "jsdom"; |
| 9 | |
| 10 | test("production minification cannot leak main-world bundler helpers into page scripts", async () => { |
| 11 | const dir = await mkdtemp(join(tmpdir(), "reasonix-page-bundle-")); |
| 12 | const dom = new JSDOM('<body><button aria-label="Save">Save</button></body>', { runScripts: "outside-only", pretendToBeVisual: true }); |
| 13 | try { |
| 14 | // Same failure mechanism as the former nested snapshot walker: keepNames |
| 15 | // introduces a module helper which Function#toString cannot carry along. |
| 16 | const legacy = await build({ stdin: { contents: 'export function pageSnapshot() { const role = element => element.getAttribute("aria-label"); return role(document.querySelector("button")); }', loader: "js" }, bundle: true, platform: "node", format: "cjs", minify: true, keepNames: true, write: false }); |
| 17 | const oldModule = { exports: {} as { pageSnapshot?: () => string } }; |
| 18 | new Function("module", "exports", legacy.outputFiles[0].text)(oldModule, oldModule.exports); |
| 19 | assert.throws(() => dom.window.eval(`(${oldModule.exports.pageSnapshot!.toString()})()`), /not defined/, "the old serialization mechanism must fail in the same isolated page environment"); |
| 20 | for (const name of ["browser-page.js", "browser-page.json"]) await copyFile(resolve("dist", name), join(dir, name)); |
| 21 | await build({ entryPoints: [resolve("src/main/browser/pageScripts.ts")], bundle: true, platform: "node", target: "node22", format: "cjs", minify: true, keepNames: true, outfile: join(dir, "main.cjs"), logLevel: "silent" }); |
| 22 | const runtime = createRequire(import.meta.url)(join(dir, "main.cjs")) as { scriptCall(method: string, input: unknown): string }; |
| 23 | dom.window.Element.prototype.getBoundingClientRect = () => ({ width: 50, height: 20, x: 0, y: 0, left: 0, top: 0, right: 50, bottom: 20, toJSON() {} }); |
| 24 | const out = dom.window.eval(runtime.scriptCall("pageSnapshot", { key: "test", snapshotId: "s1", prefix: "", selector: "", budget: 4000 })) as { tree: string; docId: string }; |
| 25 | assert.match(out.tree, /button "Save" ref=e1/); |
| 26 | assert.equal(dom.window.eval("typeof __name"), "undefined"); |
| 27 | assert.equal(dom.window.eval(runtime.scriptCall("pageIdentity", { key: "test", docId: out.docId })), true); |
| 28 | const picking = dom.window.eval(runtime.scriptCall("pagePickElement", { key: "test" })) as Promise<unknown>; |
| 29 | assert.equal(dom.window.document.querySelectorAll("div").length, 1); |
| 30 | dom.window.eval(runtime.scriptCall("pageCancelPicker", {})); |
| 31 | assert.equal(await picking, null); |
| 32 | assert.equal(dom.window.document.querySelectorAll("div").length, 0, "closing the panel cancels and removes the picker overlay"); |
| 33 | } finally { |
| 34 | dom.window.close(); |
| 35 | await rm(dir, { recursive: true, force: true }); |
| 36 | } |
| 37 | }); |
| 38 |