import test from 'node:test'; import assert from 'node:assert/strict'; import vm from 'node:vm'; import { readFileSync } from 'node:fs'; import { spawnSync } from 'node:child_process'; import { patchPdfClient, prelude, workerPrefix } from './install-webview-compat.mjs'; function realm() { const ctx = vm.createContext({ assert }); vm.runInContext('delete Map.prototype.getOrInsert; delete Map.prototype.getOrInsertComputed;', ctx); return ctx; } test('reproduces missing API, then caches one Promise per key', async () => { const ctx = realm(); assert.throws(() => vm.runInContext('new Map().getOrInsertComputed("x", () => 1)', ctx), /not a function/); vm.runInContext(prelude, ctx); await vm.runInContext( '(async () => { const map = new Map(); let calls = 0; const create = key => { assert.equal(key, "metadata"); calls++; return Promise.resolve(42); }; const a = map.getOrInsertComputed("metadata", create); const b = map.getOrInsertComputed("metadata", create); assert.equal(a, b); assert.equal(await a, 42); assert.equal(calls, 1); })()', ctx); }); test('preserves falsy values, callback semantics, key identity and exceptions', () => { const ctx = realm(); vm.runInContext(prelude, ctx); vm.runInContext( 'const map = new Map(); for (const v of [undefined, null, false, 0, ""]) { map.set("x", v); assert.equal(map.getOrInsertComputed("x", () => { throw Error("unexpected"); }), v); assert.equal(map.getOrInsert("x", 99), v); }' + 'assert.throws(() => map.getOrInsertComputed("x", null), /callable/); assert.throws(() => Map.prototype.getOrInsert.call({}, "x", 1));' + 'const key = {}; assert.equal(map.getOrInsertComputed(key, k => k), key); assert.equal(map.getOrInsertComputed(NaN, () => 3), 3); assert.equal(map.getOrInsertComputed(NaN, () => 4), 3);' + 'map.getOrInsertComputed(-0, k => { assert.equal(Object.is(k, -0), false); return 0; });' + 'assert.throws(() => map.getOrInsertComputed("error", () => { throw Error("boom"); }), /boom/); assert.equal(map.has("error"), false);' + 'assert.equal(map.getOrInsertComputed("reentrant", () => { map.set("reentrant", 1); return 2; }), 2); assert.equal(map.get("reentrant"), 2);' + 'map.getOrInsertComputed("this", function () { "use strict"; assert.equal(this, undefined); });' + 'assert.equal(Object.getOwnPropertyDescriptor(Map.prototype, "getOrInsertComputed").enumerable, false);', ctx); }); test('does not replace existing implementations and is safe to run twice', () => { const ctx = realm(); vm.runInContext(prelude, ctx); vm.runInContext('const original = Map.prototype.getOrInsertComputed; const direct = Map.prototype.getOrInsert;', ctx); vm.runInContext(prelude, ctx); vm.runInContext('assert.equal(Map.prototype.getOrInsertComputed, original); assert.equal(Map.prototype.getOrInsert, direct);', ctx); }); const nl = String.fromCharCode(10); const worker = '/* pdfjsVersion = 6.3.289 */' + nl + 'globalThis.result = new Map().getOrInsertComputed("worker", () => 7);'; const fixture = '// this.#methodPromises.getOrInsertComputed(' + nl + workerPrefix + JSON.stringify(worker) + ';'; const patch = source => patchPdfClient(source, '0.2.1-rc.2', '0.2.1-rc.2'); test('patch initializes both page and isolated embedded worker before use', () => { const output = patch(fixture); const page = realm(); vm.runInContext(output, page); const isolated = realm(); vm.runInContext(page._dsh_pdf_worker_default, isolated); assert.equal(isolated.result, 7); assert.ok(output.startsWith(prelude)); }); test('rejects version drift, missing/duplicate worker and repeated patch', () => { assert.throws(() => patchPdfClient(fixture, 'other', 'other'), /unsupported/); assert.throws(() => patchPdfClient(fixture, '0.2.1-rc.2', 'other'), /unsupported/); assert.throws(() => patch(''), /expected/); assert.throws(() => patch(fixture + nl + fixture), /exactly one/); assert.throws(() => patch(fixture.replace('6.3.289', '0.0.0')), /unexpected/); assert.throws(() => patch(patch(fixture)), /already applied/); }); test('actual installed PDF bundle and embedded worker patch and parse', { skip: !process.env.DSH_PDF_CLIENT }, () => { const source = readFileSync(process.env.DSH_PDF_CLIENT, 'utf8'); const output = patch(source); const line = output.split(nl).find(line => line.startsWith(workerPrefix)); const embedded = vm.runInNewContext(line.slice(workerPrefix.length, -1), {}, { timeout: 1000 }); assert.ok(embedded.startsWith(prelude)); for (const code of [output, embedded]) { const result = spawnSync(process.execPath, ['--input-type=module', '--check'], { input: code, encoding: 'utf8' }); assert.equal(result.status, 0, result.stderr); } // Exercise the exact upstream cache method body with the API removed. const body = source.split('#cacheSimpleMethod(name, data = null) {')[1].split(nl)[1].trim(); const ctx = realm(); vm.runInContext(prelude, ctx); vm.runInContext('class Transport { #methodPromises = new Map(); calls = 0; messageHandler = { sendWithPromise: () => { this.calls++; return Promise.resolve(1); } }; cached(name, data = null) { ' + body + ' } } const t = new Transport(); assert.equal(t.cached("GetMetadata"), t.cached("GetMetadata")); assert.equal(t.calls, 1);', ctx); });