2026-08-10 15:49:01 -03:00
|
|
|
// *************************************************************************
|
|
|
|
|
// * (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
|
2026-08-10 16:14:56 -03:00
|
|
|
// exactly the configured origin.
|
2026-08-10 15:49:01 -03:00
|
|
|
//
|
|
|
|
|
// make smoke
|
2026-08-10 16:14:56 -03:00
|
|
|
// node tools/sw-smoke.mjs [path/to/background.js]
|
2026-08-10 15:49:01 -03:00
|
|
|
|
|
|
|
|
import { readFileSync } from 'node:fs';
|
|
|
|
|
import vm from 'node:vm';
|
|
|
|
|
|
2026-08-10 16:14:56 -03:00
|
|
|
const BUNDLE = process.argv[2] ?? 'build/chrome/background.js';
|
2026-08-10 15:49:01 -03:00
|
|
|
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`);
|