browser-extension-manager 1.3.49 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -70
- package/dist/background.js +4 -0
- package/dist/build.js +3 -0
- package/dist/cli.js +1 -0
- package/dist/commands/test.js +55 -0
- package/dist/content.js +4 -0
- package/dist/defaults/CLAUDE.md +66 -6
- package/dist/gulp/tasks/defaults.js +15 -2
- package/dist/offscreen.js +4 -0
- package/dist/options.js +4 -0
- package/dist/page.js +4 -0
- package/dist/popup.js +4 -0
- package/dist/sidepanel.js +4 -0
- package/dist/test/assert.js +120 -0
- package/dist/test/fixtures/consumer-extension/dist/background.js +15 -0
- package/dist/test/fixtures/consumer-extension/dist/manifest.json +20 -0
- package/dist/test/fixtures/consumer-extension/dist/options.html +10 -0
- package/dist/test/fixtures/consumer-extension/dist/popup.html +11 -0
- package/dist/test/harness/extension/background.js +26 -0
- package/dist/test/harness/extension/manifest.json +27 -0
- package/dist/test/harness/extension/options.html +12 -0
- package/dist/test/harness/extension/popup.html +12 -0
- package/dist/test/harness/extension/sidepanel.html +12 -0
- package/dist/test/index.js +63 -0
- package/dist/test/runner.js +407 -0
- package/dist/test/runners/boot.js +201 -0
- package/dist/test/runners/chromium.js +399 -0
- package/dist/test/suites/background/messaging.test.js +42 -0
- package/dist/test/suites/background/storage.test.js +44 -0
- package/dist/test/suites/background/sw-context.test.js +47 -0
- package/dist/test/suites/boot/extension-loads.test.js +56 -0
- package/dist/test/suites/build/affiliatizer.test.js +63 -0
- package/dist/test/suites/build/cli.test.js +123 -0
- package/dist/test/suites/build/expect.test.js +47 -0
- package/dist/test/suites/build/exports.test.js +52 -0
- package/dist/test/suites/build/extension-fallback.test.js +41 -0
- package/dist/test/suites/build/logger-lite.test.js +59 -0
- package/dist/test/suites/build/manager.test.js +162 -0
- package/dist/test/suites/build/mode-helpers.test.js +96 -0
- package/dist/test/suites/view/options-and-sidepanel.test.js +14 -0
- package/dist/test/suites/view/popup-context.test.js +51 -0
- package/dist/test/suites/view/sidepanel.test.js +14 -0
- package/dist/utils/mode-helpers.js +92 -0
- package/package.json +7 -4
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Build-layer tests for cli.js alias resolution. We instantiate Main and call
|
|
2
|
+
// process({ _: ['<alias>'] }) — the resolver looks up the alias map and require()s
|
|
3
|
+
// the command module. To avoid actually running the command, we stub each command
|
|
4
|
+
// to a no-op via require.cache injection before calling process().
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const CLI_PATH = path.join(__dirname, '..', '..', '..', 'cli.js');
|
|
9
|
+
const COMMANDS_DIR = path.join(__dirname, '..', '..', '..', 'commands');
|
|
10
|
+
|
|
11
|
+
function stubCommand(name, fn) {
|
|
12
|
+
const file = path.join(COMMANDS_DIR, `${name}.js`);
|
|
13
|
+
require.cache[file] = {
|
|
14
|
+
id: file,
|
|
15
|
+
filename: file,
|
|
16
|
+
loaded: true,
|
|
17
|
+
children: [],
|
|
18
|
+
paths: [],
|
|
19
|
+
exports: fn,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function unstub(name) {
|
|
24
|
+
delete require.cache[path.join(COMMANDS_DIR, `${name}.js`)];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function freshCli() {
|
|
28
|
+
delete require.cache[CLI_PATH];
|
|
29
|
+
return require(CLI_PATH);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = {
|
|
33
|
+
type: 'suite',
|
|
34
|
+
layer: 'build',
|
|
35
|
+
description: 'cli — alias resolution + dispatch',
|
|
36
|
+
tests: [
|
|
37
|
+
{
|
|
38
|
+
name: 'positional "test" routes to commands/test.js',
|
|
39
|
+
run: async (ctx) => {
|
|
40
|
+
let invoked = false;
|
|
41
|
+
stubCommand('test', async () => { invoked = true; });
|
|
42
|
+
try {
|
|
43
|
+
const Main = freshCli();
|
|
44
|
+
await new Main().process({ _: ['test'] });
|
|
45
|
+
ctx.expect(invoked).toBe(true);
|
|
46
|
+
} finally {
|
|
47
|
+
unstub('test');
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'alias "-t" routes to test command',
|
|
53
|
+
run: async (ctx) => {
|
|
54
|
+
let invoked = false;
|
|
55
|
+
stubCommand('test', async () => { invoked = true; });
|
|
56
|
+
try {
|
|
57
|
+
const Main = freshCli();
|
|
58
|
+
await new Main().process({ _: [], t: true });
|
|
59
|
+
ctx.expect(invoked).toBe(true);
|
|
60
|
+
} finally {
|
|
61
|
+
unstub('test');
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'positional "clean" routes to commands/clean.js',
|
|
67
|
+
run: async (ctx) => {
|
|
68
|
+
let invoked = false;
|
|
69
|
+
stubCommand('clean', async () => { invoked = true; });
|
|
70
|
+
try {
|
|
71
|
+
const Main = freshCli();
|
|
72
|
+
await new Main().process({ _: ['clean'] });
|
|
73
|
+
ctx.expect(invoked).toBe(true);
|
|
74
|
+
} finally {
|
|
75
|
+
unstub('clean');
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'no command defaults to setup',
|
|
81
|
+
run: async (ctx) => {
|
|
82
|
+
let invoked = false;
|
|
83
|
+
stubCommand('setup', async () => { invoked = true; });
|
|
84
|
+
try {
|
|
85
|
+
const Main = freshCli();
|
|
86
|
+
await new Main().process({ _: [] });
|
|
87
|
+
ctx.expect(invoked).toBe(true);
|
|
88
|
+
} finally {
|
|
89
|
+
unstub('setup');
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'unknown command throws "Command not found"',
|
|
95
|
+
run: async (ctx) => {
|
|
96
|
+
const Main = freshCli();
|
|
97
|
+
let threw = false;
|
|
98
|
+
try {
|
|
99
|
+
await new Main().process({ _: ['totally-not-a-command-xyz'] });
|
|
100
|
+
} catch (e) {
|
|
101
|
+
threw = true;
|
|
102
|
+
ctx.expect(e.message).toContain('not found');
|
|
103
|
+
}
|
|
104
|
+
ctx.expect(threw).toBe(true);
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'command options are forwarded',
|
|
109
|
+
run: async (ctx) => {
|
|
110
|
+
let received = null;
|
|
111
|
+
stubCommand('test', async (opts) => { received = opts; });
|
|
112
|
+
try {
|
|
113
|
+
const Main = freshCli();
|
|
114
|
+
await new Main().process({ _: ['test'], layer: 'build', filter: 'foo' });
|
|
115
|
+
ctx.expect(received.layer).toBe('build');
|
|
116
|
+
ctx.expect(received.filter).toBe('foo');
|
|
117
|
+
} finally {
|
|
118
|
+
unstub('test');
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Build-layer test of the test framework's own expect() matchers. Self-test —
|
|
2
|
+
// if this doesn't pass, the test framework itself is broken and downstream
|
|
3
|
+
// suites can't be trusted.
|
|
4
|
+
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const expect = require(path.join(__dirname, '..', '..', 'assert.js'));
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
type: 'group',
|
|
11
|
+
layer: 'build',
|
|
12
|
+
description: 'expect() — matcher self-test',
|
|
13
|
+
tests: [
|
|
14
|
+
{ name: 'toBe passes on ===', run: () => expect(1).toBe(1) },
|
|
15
|
+
{ name: 'toBe.not passes on !==', run: () => expect(1).not.toBe(2) },
|
|
16
|
+
{ name: 'toEqual deep-equals objects', run: () => expect({ a: [1, 2] }).toEqual({ a: [1, 2] }) },
|
|
17
|
+
{ name: 'toEqual fails on shape diff', run: () => {
|
|
18
|
+
let threw = false;
|
|
19
|
+
try { expect({ a: 1 }).toEqual({ a: 2 }); } catch (_) { threw = true; }
|
|
20
|
+
if (!threw) throw new Error('expected toEqual to throw');
|
|
21
|
+
} },
|
|
22
|
+
{ name: 'toBeTruthy / toBeFalsy', run: () => {
|
|
23
|
+
expect(1).toBeTruthy();
|
|
24
|
+
expect(0).toBeFalsy();
|
|
25
|
+
expect('').toBeFalsy();
|
|
26
|
+
expect('x').toBeTruthy();
|
|
27
|
+
} },
|
|
28
|
+
{ name: 'toBeDefined / toBeUndefined', run: () => {
|
|
29
|
+
expect(null).toBeDefined();
|
|
30
|
+
expect(undefined).toBeUndefined();
|
|
31
|
+
} },
|
|
32
|
+
{ name: 'toContain (string)', run: () => expect('hello world').toContain('world') },
|
|
33
|
+
{ name: 'toContain (array)', run: () => expect([1, 2, 3]).toContain(2) },
|
|
34
|
+
{ name: 'toHaveProperty', run: () => expect({ foo: 1 }).toHaveProperty('foo') },
|
|
35
|
+
{ name: 'toMatch (regex)', run: () => expect('abc123').toMatch(/^abc\d+$/) },
|
|
36
|
+
{ name: 'toBeGreaterThan / toBeLessThan', run: () => {
|
|
37
|
+
expect(10).toBeGreaterThan(5);
|
|
38
|
+
expect(5).toBeLessThan(10);
|
|
39
|
+
} },
|
|
40
|
+
{ name: 'toThrow on sync function', run: async () => {
|
|
41
|
+
await expect(() => { throw new Error('boom'); }).toThrow();
|
|
42
|
+
} },
|
|
43
|
+
{ name: 'toThrow with regex matcher', run: async () => {
|
|
44
|
+
await expect(() => { throw new Error('boom: 42'); }).toThrow(/42$/);
|
|
45
|
+
} },
|
|
46
|
+
],
|
|
47
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Build-layer test that every entry in package.json#exports is require()-able from
|
|
2
|
+
// the built dist/ tree (or src/ if dist hasn't been built yet — the test resolves
|
|
3
|
+
// relative to whichever directory THIS file lives in).
|
|
4
|
+
//
|
|
5
|
+
// The dist/ paths in package.json#exports point to dist/foo.js — when running the
|
|
6
|
+
// test framework, this file IS the one shipped into dist/test/suites/build/exports.test.js,
|
|
7
|
+
// so the relative path back to dist root via `../../../..` lands at <bxm>/dist, and
|
|
8
|
+
// each export key resolves correctly.
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
// Each export key (e.g. './main' / './lib/logger-lite') maps to a relative dist path.
|
|
13
|
+
// We strip the './dist/' prefix and resolve against `<bxm>/dist` (this file's grandparent
|
|
14
|
+
// chain: dist/test/suites/build → ../../../ → dist).
|
|
15
|
+
const BXM_ROOT_FROM_SUITE = path.resolve(__dirname, '..', '..', '..', '..');
|
|
16
|
+
const DIST_ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
17
|
+
const pkg = require(path.join(BXM_ROOT_FROM_SUITE, 'package.json'));
|
|
18
|
+
|
|
19
|
+
// Browser-context modules can't be plain-required from Node (they touch chrome.* /
|
|
20
|
+
// window.* without try/catch around the top-level usage). Skip those — the build-layer
|
|
21
|
+
// only asserts the "node-safe" surface of BXM. Browser-context modules are exercised
|
|
22
|
+
// by the background/view test layers.
|
|
23
|
+
const BROWSER_CONTEXT_KEYS = new Set([
|
|
24
|
+
'.',
|
|
25
|
+
'./background',
|
|
26
|
+
'./content',
|
|
27
|
+
'./popup',
|
|
28
|
+
'./sidepanel',
|
|
29
|
+
'./options',
|
|
30
|
+
'./page',
|
|
31
|
+
'./offscreen',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
type: 'group',
|
|
36
|
+
layer: 'build',
|
|
37
|
+
description: 'package.json#exports — node-safe entries resolve',
|
|
38
|
+
tests: Object.entries(pkg.exports || {})
|
|
39
|
+
.filter(([key]) => !BROWSER_CONTEXT_KEYS.has(key))
|
|
40
|
+
.map(([key, relDistPath]) => ({
|
|
41
|
+
name: `${key} → ${relDistPath}`,
|
|
42
|
+
run: (ctx) => {
|
|
43
|
+
// relDistPath looks like "./dist/lib/logger-lite.js" — strip the leading
|
|
44
|
+
// "./dist/" and resolve against the suite's actual dist root.
|
|
45
|
+
const rel = relDistPath.replace(/^\.\/dist\//, '');
|
|
46
|
+
const abs = path.join(DIST_ROOT, rel);
|
|
47
|
+
delete require.cache[abs];
|
|
48
|
+
const mod = require(abs);
|
|
49
|
+
ctx.expect(mod).toBeDefined();
|
|
50
|
+
},
|
|
51
|
+
})),
|
|
52
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Build-layer test for lib/extension.js — verifies the safe-fallback behavior.
|
|
2
|
+
// When `chrome` / `window` / `browser` globals are absent (Node context), the
|
|
3
|
+
// module's per-API try/catch blocks must swallow the ReferenceError and leave
|
|
4
|
+
// every API property set to null. This is how the same module imports cleanly
|
|
5
|
+
// in background SW, content scripts, AND build-time Node tooling.
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const ext = require(path.join(__dirname, '..', '..', '..', 'lib', 'extension.js'));
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
type: 'group',
|
|
13
|
+
layer: 'build',
|
|
14
|
+
description: 'lib/extension — safe-fallback in Node context',
|
|
15
|
+
tests: [
|
|
16
|
+
{
|
|
17
|
+
name: 'module exports an object',
|
|
18
|
+
run: (ctx) => {
|
|
19
|
+
ctx.expect(typeof ext).toBe('object');
|
|
20
|
+
ctx.expect(ext).not.toBeNull();
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'common API slots are null when no chrome global is present',
|
|
25
|
+
run: (ctx) => {
|
|
26
|
+
ctx.expect(ext.runtime).toBeNull();
|
|
27
|
+
ctx.expect(ext.storage).toBeNull();
|
|
28
|
+
ctx.expect(ext.tabs).toBeNull();
|
|
29
|
+
ctx.expect(ext.action).toBeNull();
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'requiring the module did not throw (smoke)',
|
|
34
|
+
run: (ctx) => {
|
|
35
|
+
// The fact that we reached this test means require() didn't throw —
|
|
36
|
+
// record an explicit pass so the suite output documents the property.
|
|
37
|
+
ctx.expect(true).toBe(true);
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Build-layer test for lib/logger-lite.js — verifies the timestamp prefix format
|
|
2
|
+
// [HH:MM:SS] name: ... and the five method surface (log/error/warn/info/debug).
|
|
3
|
+
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const Logger = require(path.join(__dirname, '..', '..', '..', 'lib', 'logger-lite.js'));
|
|
7
|
+
|
|
8
|
+
function captureConsole(method, fn) {
|
|
9
|
+
const captured = [];
|
|
10
|
+
const orig = console[method];
|
|
11
|
+
console[method] = function (...args) { captured.push(args); };
|
|
12
|
+
try { fn(); } finally { console[method] = orig; }
|
|
13
|
+
return captured;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = {
|
|
17
|
+
type: 'suite',
|
|
18
|
+
layer: 'build',
|
|
19
|
+
description: 'lib/logger-lite — timestamp prefix + five-method surface',
|
|
20
|
+
tests: [
|
|
21
|
+
{
|
|
22
|
+
name: 'constructor stores name',
|
|
23
|
+
run: (ctx) => {
|
|
24
|
+
const log = new Logger('my-component');
|
|
25
|
+
ctx.expect(log.name).toBe('my-component');
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'log() prefixes with [HH:MM:SS] name:',
|
|
30
|
+
run: (ctx) => {
|
|
31
|
+
const log = new Logger('feature-x');
|
|
32
|
+
const captured = captureConsole('log', () => log.log('hello', 'world'));
|
|
33
|
+
ctx.expect(captured.length).toBe(1);
|
|
34
|
+
const [prefix, ...rest] = captured[0];
|
|
35
|
+
ctx.expect(prefix).toMatch(/^\[\d{2}:\d{2}:\d{2}\] feature-x:$/);
|
|
36
|
+
ctx.expect(rest).toEqual(['hello', 'world']);
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'exposes log/error/warn/info/debug',
|
|
41
|
+
run: (ctx) => {
|
|
42
|
+
const log = new Logger('s');
|
|
43
|
+
for (const m of ['log', 'error', 'warn', 'info', 'debug']) {
|
|
44
|
+
ctx.expect(typeof log[m]).toBe('function');
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'error() routes through console.error',
|
|
50
|
+
run: (ctx) => {
|
|
51
|
+
const log = new Logger('err-comp');
|
|
52
|
+
const captured = captureConsole('error', () => log.error('boom'));
|
|
53
|
+
ctx.expect(captured.length).toBe(1);
|
|
54
|
+
ctx.expect(captured[0][0]).toMatch(/err-comp:/);
|
|
55
|
+
ctx.expect(captured[0][1]).toBe('boom');
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Build-layer tests for Manager.getConfig() / getManifest() / getPackage() / getEnvironment().
|
|
2
|
+
// Stages a temp project dir with config/browser-extension-manager.json + src/manifest.json
|
|
3
|
+
// + package.json, sets process.cwd() to it, then exercises Manager's getters.
|
|
4
|
+
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
|
|
9
|
+
function stageProject(opts = {}) {
|
|
10
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'bxm-getconfig-'));
|
|
11
|
+
if (opts.config !== undefined) {
|
|
12
|
+
fs.mkdirSync(path.join(tmp, 'config'), { recursive: true });
|
|
13
|
+
fs.writeFileSync(path.join(tmp, 'config', 'browser-extension-manager.json'), opts.config);
|
|
14
|
+
}
|
|
15
|
+
if (opts.manifest !== undefined) {
|
|
16
|
+
fs.mkdirSync(path.join(tmp, 'src'), { recursive: true });
|
|
17
|
+
fs.writeFileSync(path.join(tmp, 'src', 'manifest.json'), opts.manifest);
|
|
18
|
+
}
|
|
19
|
+
if (opts.pkg !== undefined) {
|
|
20
|
+
fs.writeFileSync(path.join(tmp, 'package.json'), opts.pkg);
|
|
21
|
+
}
|
|
22
|
+
return tmp;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Runs `fn` with process.cwd() pinned to `dir`. Manager.getConfig() / getManifest()
|
|
26
|
+
// resolve their files against process.cwd() at CALL time, so we hold the chdir until
|
|
27
|
+
// after the test body runs (not just during require()).
|
|
28
|
+
function inDir(dir, fn) {
|
|
29
|
+
const oldCwd = process.cwd();
|
|
30
|
+
for (const k of Object.keys(require.cache)) {
|
|
31
|
+
if (k.includes('/build.js')) delete require.cache[k];
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
process.chdir(dir);
|
|
35
|
+
return fn(require(path.join(__dirname, '..', '..', '..', 'build.js')));
|
|
36
|
+
} finally {
|
|
37
|
+
process.chdir(oldCwd);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
type: 'suite',
|
|
43
|
+
layer: 'build',
|
|
44
|
+
description: 'Manager — config / manifest / package / environment getters',
|
|
45
|
+
tests: [
|
|
46
|
+
{
|
|
47
|
+
name: 'getConfig returns parsed JSON5',
|
|
48
|
+
run: (ctx) => {
|
|
49
|
+
const tmp = stageProject({ config: `{ brand: { id: 'somiibo', name: 'Somiibo' } }` });
|
|
50
|
+
try {
|
|
51
|
+
inDir(tmp, (Manager) => {
|
|
52
|
+
const cfg = Manager.getConfig();
|
|
53
|
+
ctx.expect(cfg.brand.id).toBe('somiibo');
|
|
54
|
+
ctx.expect(cfg.brand.name).toBe('Somiibo');
|
|
55
|
+
});
|
|
56
|
+
} finally {
|
|
57
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'getManifest returns parsed JSON5 from src/manifest.json',
|
|
63
|
+
run: (ctx) => {
|
|
64
|
+
const tmp = stageProject({ manifest: `{ manifest_version: 3, name: 'Test', version: '1.0.0' }` });
|
|
65
|
+
try {
|
|
66
|
+
inDir(tmp, (Manager) => {
|
|
67
|
+
const m = Manager.getManifest();
|
|
68
|
+
ctx.expect(m.manifest_version).toBe(3);
|
|
69
|
+
ctx.expect(m.name).toBe('Test');
|
|
70
|
+
ctx.expect(m.version).toBe('1.0.0');
|
|
71
|
+
});
|
|
72
|
+
} finally {
|
|
73
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'getManifest returns {} when src/manifest.json is absent',
|
|
79
|
+
run: (ctx) => {
|
|
80
|
+
const tmp = stageProject({});
|
|
81
|
+
try {
|
|
82
|
+
inDir(tmp, (Manager) => {
|
|
83
|
+
ctx.expect(Manager.getManifest()).toEqual({});
|
|
84
|
+
});
|
|
85
|
+
} finally {
|
|
86
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'getPackage("project") reads cwd package.json',
|
|
92
|
+
run: (ctx) => {
|
|
93
|
+
const tmp = stageProject({ pkg: `{ "name": "test-ext", "version": "2.0.0" }` });
|
|
94
|
+
try {
|
|
95
|
+
inDir(tmp, (Manager) => {
|
|
96
|
+
const pkg = Manager.getPackage('project');
|
|
97
|
+
ctx.expect(pkg.name).toBe('test-ext');
|
|
98
|
+
ctx.expect(pkg.version).toBe('2.0.0');
|
|
99
|
+
});
|
|
100
|
+
} finally {
|
|
101
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'getPackage("main") reads BXM\'s own package.json',
|
|
107
|
+
run: (ctx) => {
|
|
108
|
+
const Manager = require(path.join(__dirname, '..', '..', '..', 'build.js'));
|
|
109
|
+
const pkg = Manager.getPackage('main');
|
|
110
|
+
ctx.expect(pkg.name).toBe('browser-extension-manager');
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'getEnvironment returns "development" when BXM_BUILD_MODE !== "true"',
|
|
115
|
+
run: (ctx) => {
|
|
116
|
+
const Manager = require(path.join(__dirname, '..', '..', '..', 'build.js'));
|
|
117
|
+
const original = process.env.BXM_BUILD_MODE;
|
|
118
|
+
delete process.env.BXM_BUILD_MODE;
|
|
119
|
+
try {
|
|
120
|
+
ctx.expect(Manager.getEnvironment()).toBe('development');
|
|
121
|
+
} finally {
|
|
122
|
+
if (original !== undefined) process.env.BXM_BUILD_MODE = original;
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'getEnvironment returns "production" when BXM_BUILD_MODE === "true"',
|
|
128
|
+
run: (ctx) => {
|
|
129
|
+
const Manager = require(path.join(__dirname, '..', '..', '..', 'build.js'));
|
|
130
|
+
const original = process.env.BXM_BUILD_MODE;
|
|
131
|
+
process.env.BXM_BUILD_MODE = 'true';
|
|
132
|
+
try {
|
|
133
|
+
ctx.expect(Manager.getEnvironment()).toBe('production');
|
|
134
|
+
} finally {
|
|
135
|
+
if (original === undefined) delete process.env.BXM_BUILD_MODE;
|
|
136
|
+
else process.env.BXM_BUILD_MODE = original;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'getLiveReloadPort defaults to 35729',
|
|
142
|
+
run: (ctx) => {
|
|
143
|
+
const Manager = require(path.join(__dirname, '..', '..', '..', 'build.js'));
|
|
144
|
+
const original = process.env.BXM_LIVERELOAD_PORT;
|
|
145
|
+
delete process.env.BXM_LIVERELOAD_PORT;
|
|
146
|
+
try {
|
|
147
|
+
ctx.expect(Manager.getLiveReloadPort()).toBe(35729);
|
|
148
|
+
} finally {
|
|
149
|
+
if (original !== undefined) process.env.BXM_LIVERELOAD_PORT = original;
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'getRootPath("project") returns cwd, getRootPath("main") returns BXM root',
|
|
155
|
+
run: (ctx) => {
|
|
156
|
+
const Manager = require(path.join(__dirname, '..', '..', '..', 'build.js'));
|
|
157
|
+
ctx.expect(Manager.getRootPath('project')).toBe(process.cwd());
|
|
158
|
+
ctx.expect(Manager.getRootPath('main')).toBe(path.resolve(__dirname, '..', '..', '..', '..'));
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Build-layer test for utils/mode-helpers.js — verifies the cross-context helpers
|
|
2
|
+
// (isDevelopment / isProduction / isTesting / getVersion) behave correctly in a
|
|
3
|
+
// Node context (no `chrome` global), driven by env vars + cwd package.json.
|
|
4
|
+
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
|
|
9
|
+
const helpers = require(path.join(__dirname, '..', '..', '..', 'utils', 'mode-helpers.js'));
|
|
10
|
+
|
|
11
|
+
function withEnv(overrides, fn) {
|
|
12
|
+
const originals = {};
|
|
13
|
+
for (const [k, v] of Object.entries(overrides)) {
|
|
14
|
+
originals[k] = process.env[k];
|
|
15
|
+
if (v === null) delete process.env[k];
|
|
16
|
+
else process.env[k] = v;
|
|
17
|
+
}
|
|
18
|
+
try { return fn(); } finally {
|
|
19
|
+
for (const [k, v] of Object.entries(originals)) {
|
|
20
|
+
if (v === undefined) delete process.env[k];
|
|
21
|
+
else process.env[k] = v;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
type: 'suite',
|
|
28
|
+
layer: 'build',
|
|
29
|
+
description: 'utils/mode-helpers — cross-context isDevelopment/isTesting/getVersion',
|
|
30
|
+
tests: [
|
|
31
|
+
{
|
|
32
|
+
name: 'exports { attachTo, isDevelopment, isProduction, isTesting, getVersion }',
|
|
33
|
+
run: (ctx) => {
|
|
34
|
+
ctx.expect(typeof helpers.attachTo).toBe('function');
|
|
35
|
+
ctx.expect(typeof helpers.isDevelopment).toBe('function');
|
|
36
|
+
ctx.expect(typeof helpers.isProduction).toBe('function');
|
|
37
|
+
ctx.expect(typeof helpers.isTesting).toBe('function');
|
|
38
|
+
ctx.expect(typeof helpers.getVersion).toBe('function');
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'isTesting() reads BXM_TEST_MODE env var',
|
|
43
|
+
run: (ctx) => {
|
|
44
|
+
withEnv({ BXM_TEST_MODE: 'true' }, () => {
|
|
45
|
+
ctx.expect(helpers.isTesting()).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
withEnv({ BXM_TEST_MODE: null }, () => {
|
|
48
|
+
ctx.expect(helpers.isTesting()).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'isDevelopment() is true under NODE_ENV=development',
|
|
54
|
+
run: (ctx) => {
|
|
55
|
+
withEnv({ NODE_ENV: 'development', BXM_BUILD_MODE: null }, () => {
|
|
56
|
+
ctx.expect(helpers.isDevelopment()).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'isDevelopment() is false when BXM_BUILD_MODE=true',
|
|
62
|
+
run: (ctx) => {
|
|
63
|
+
withEnv({ NODE_ENV: null, BXM_BUILD_MODE: 'true' }, () => {
|
|
64
|
+
ctx.expect(helpers.isDevelopment()).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'attachTo() mixes helpers into a constructor + its prototype',
|
|
70
|
+
run: (ctx) => {
|
|
71
|
+
function FakeManager() {}
|
|
72
|
+
helpers.attachTo(FakeManager);
|
|
73
|
+
ctx.expect(typeof FakeManager.isDevelopment).toBe('function');
|
|
74
|
+
ctx.expect(typeof FakeManager.prototype.isDevelopment).toBe('function');
|
|
75
|
+
ctx.expect(typeof FakeManager.isTesting).toBe('function');
|
|
76
|
+
ctx.expect(typeof FakeManager.prototype.isTesting).toBe('function');
|
|
77
|
+
ctx.expect(typeof FakeManager.getVersion).toBe('function');
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'getVersion() reads cwd package.json#version in Node context',
|
|
82
|
+
run: (ctx) => {
|
|
83
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'bxm-modehelpers-'));
|
|
84
|
+
fs.writeFileSync(path.join(tmp, 'package.json'), JSON.stringify({ name: 'x', version: '9.9.9' }));
|
|
85
|
+
const oldCwd = process.cwd();
|
|
86
|
+
try {
|
|
87
|
+
process.chdir(tmp);
|
|
88
|
+
ctx.expect(helpers.getVersion()).toBe('9.9.9');
|
|
89
|
+
} finally {
|
|
90
|
+
process.chdir(oldCwd);
|
|
91
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// View-layer test that the same Manager surface is reachable from the options
|
|
2
|
+
// page. Confirms that view tests can target ANY of popup / options / sidepanel
|
|
3
|
+
// just by setting the `context` field — same `ctx.expect / state / skip` API
|
|
4
|
+
// applies. (Sidepanel is exercised separately in sidepanel.test.js.)
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
layer: 'view',
|
|
8
|
+
context: 'options',
|
|
9
|
+
description: 'view/options — DOM + context attribute',
|
|
10
|
+
run: async (ctx) => {
|
|
11
|
+
ctx.expect(document.body.dataset.bxmContext).toBe('options');
|
|
12
|
+
ctx.expect(document.title).toContain('Options');
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// View-layer smoke for the popup page. Verifies the test code is executing
|
|
2
|
+
// inside a tab that has loaded popup.html (chrome-extension://<id>/popup.html),
|
|
3
|
+
// has DOM + chrome.* APIs, and matches the harness's expected page shape.
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
type: 'suite',
|
|
7
|
+
layer: 'view',
|
|
8
|
+
context: 'popup',
|
|
9
|
+
description: 'view/popup — DOM + chrome surface',
|
|
10
|
+
tests: [
|
|
11
|
+
{
|
|
12
|
+
name: 'document is present and body has data-bxm-context="popup"',
|
|
13
|
+
run: async (ctx) => {
|
|
14
|
+
ctx.expect(typeof document).toBe('object');
|
|
15
|
+
ctx.expect(document.body.dataset.bxmContext).toBe('popup');
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'page title matches the harness popup.html',
|
|
20
|
+
run: async (ctx) => {
|
|
21
|
+
ctx.expect(document.title).toContain('Popup');
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'chrome.* APIs are exposed in extension-page context',
|
|
26
|
+
run: async (ctx) => {
|
|
27
|
+
ctx.expect(typeof chrome).toBe('object');
|
|
28
|
+
ctx.expect(typeof chrome.runtime).toBe('object');
|
|
29
|
+
ctx.expect(typeof chrome.runtime.id).toBe('string');
|
|
30
|
+
ctx.expect(typeof chrome.storage).toBe('object');
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'window globals (fetch, URL) are available',
|
|
35
|
+
run: async (ctx) => {
|
|
36
|
+
ctx.expect(typeof fetch).toBe('function');
|
|
37
|
+
ctx.expect(typeof URL).toBe('function');
|
|
38
|
+
const u = new URL('https://example.com/path?q=1');
|
|
39
|
+
ctx.expect(u.searchParams.get('q')).toBe('1');
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'popup ↔ background messaging round-trip',
|
|
44
|
+
run: async (ctx) => {
|
|
45
|
+
const reply = await chrome.runtime.sendMessage({ type: 'bxm:test:ping' });
|
|
46
|
+
ctx.expect(reply.pong).toBe(true);
|
|
47
|
+
ctx.expect(typeof reply.ts).toBe('number');
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// View-layer test for the sidepanel context. Most consumers of BXM use
|
|
2
|
+
// chrome.sidePanel for the Chrome 114+ UI panel surface — verify that
|
|
3
|
+
// the test harness can target it just like popup/options.
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
layer: 'view',
|
|
7
|
+
context: 'sidepanel',
|
|
8
|
+
description: 'view/sidepanel — DOM + context attribute',
|
|
9
|
+
run: async (ctx) => {
|
|
10
|
+
ctx.expect(document.body.dataset.bxmContext).toBe('sidepanel');
|
|
11
|
+
ctx.expect(document.title).toContain('Side Panel');
|
|
12
|
+
ctx.expect(typeof chrome.runtime.id).toBe('string');
|
|
13
|
+
},
|
|
14
|
+
};
|