// ************************************************************************* // * (C)opyright 2026 by Ruben Carlo Benante * // * * // * This program is free software; you can redistribute it and/or modify * // * it under the terms of the GNU General Public License as published by * // * the Free Software Foundation, either version 3 of the License, or * // * (at your option) any later version. * // * * // * This program is distributed in the hope that it will be useful, * // * but WITHOUT ANY WARRANTY; without even the implied warranty of * // * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * // * GNU General Public License for more details. * // * * // * You should have received a copy of the GNU General Public License * // * along with this program. If not, see http://www.gnu.org/licenses/. * // * * // * Contact author at: * // * Ruben Carlo Benante * // * rcb@beco.cc * // ************************************************************************* // Smoke test for the background bundle under Chrome's execution model. // // Chrome MV3 runs the background as a service worker, where there is no // `window` and no `document`, and the global object is `self`. Firefox runs // the same file as an event page, where those do exist -- so a reference that // creeps in through a shared import breaks Chrome only, and breaks it silently // at runtime rather than at build time. // // This evaluates the built bundle in a worker-shaped sandbox with a stubbed // extension API, and asserts that it registers exactly one content script, for // exactly the configured origin. // // make smoke // node tools/sw-smoke.mjs [path/to/background.js] import { readFileSync } from 'node:fs'; import vm from 'node:vm'; const BUNDLE = process.argv[2] ?? 'build/chrome/background.js'; const CONFIGURED_ORIGIN = 'https://portal.example.br'; const permissionChecks = []; const registered = []; const listeners = { onInstalled: 0, onStartup: 0, onRemoved: 0, onChanged: 0, onMessage: 0, }; const chrome = { runtime: { onInstalled: { addListener: () => listeners.onInstalled++ }, onStartup: { addListener: () => listeners.onStartup++ }, onMessage: { addListener: () => listeners.onMessage++ }, }, permissions: { onRemoved: { addListener: () => listeners.onRemoved++ }, contains: async (p) => { permissionChecks.push(p.origins); return true; }, }, storage: { local: { get: async () => ({ config: { url: `${CONFIGURED_ORIGIN}/`, ra: '2000101010', dn: '01/02/1999', cpf: '123.456.789-01', autoSubmit: true, }, }), set: async () => {}, }, onChanged: { addListener: () => listeners.onChanged++ }, }, scripting: { getRegisteredContentScripts: async () => [], unregisterContentScripts: async () => {}, registerContentScripts: async (scripts) => registered.push(...scripts), }, }; // Deliberately no window and no document: referencing either must fail here // exactly as it would inside a service worker. const sandbox = { chrome, console, setTimeout, clearTimeout, queueMicrotask, URL }; sandbox.self = sandbox; vm.createContext(sandbox); const failures = []; try { vm.runInContext(readFileSync(BUNDLE, 'utf8'), sandbox, { filename: BUNDLE }); } catch (error) { console.error(`FAIL: ${BUNDLE} threw on evaluation: ${error.message}`); process.exit(1); } // Give the top-level resync() a turn of the event loop to settle. await new Promise((resolve) => setTimeout(resolve, 50)); for (const [name, count] of Object.entries(listeners)) { if (count !== 1) failures.push(`${name} registered ${count} times, expected 1`); } if (registered.length !== 1) { failures.push(`registered ${registered.length} content scripts, expected 1`); } else { const script = registered[0]; const expected = `${CONFIGURED_ORIGIN}/*`; if (script.matches?.length !== 1 || script.matches[0] !== expected) { failures.push(`matches ${JSON.stringify(script.matches)}, expected ["${expected}"]`); } if (script.js?.[0] !== 'content.js') { failures.push(`js ${JSON.stringify(script.js)}, expected ["content.js"]`); } if (script.persistAcrossSessions !== false) { failures.push('persistAcrossSessions must be false, or startup registers twice'); } } if (permissionChecks.length !== 1) { failures.push(`checked permissions ${permissionChecks.length} times, expected 1`); } if (failures.length > 0) { for (const failure of failures) console.error(`FAIL: ${failure}`); process.exit(1); } console.log('ok - background bundle runs as a service worker'); console.log(`ok - registers content.js for ${CONFIGURED_ORIGIN}/* and nothing else`);