browser-extension-manager 1.3.48 → 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.
Files changed (47) hide show
  1. package/README.md +109 -70
  2. package/dist/background.js +4 -0
  3. package/dist/build.js +3 -0
  4. package/dist/cli.js +1 -0
  5. package/dist/commands/test.js +55 -0
  6. package/dist/content.js +4 -0
  7. package/dist/defaults/CLAUDE.md +66 -6
  8. package/dist/gulp/tasks/defaults.js +20 -4
  9. package/dist/gulp/tasks/distribute.js +36 -33
  10. package/dist/gulp/tasks/html.js +82 -79
  11. package/dist/gulp/tasks/utils/template-transform.js +35 -35
  12. package/dist/offscreen.js +4 -0
  13. package/dist/options.js +4 -0
  14. package/dist/page.js +4 -0
  15. package/dist/popup.js +4 -0
  16. package/dist/sidepanel.js +4 -0
  17. package/dist/test/assert.js +120 -0
  18. package/dist/test/fixtures/consumer-extension/dist/background.js +15 -0
  19. package/dist/test/fixtures/consumer-extension/dist/manifest.json +20 -0
  20. package/dist/test/fixtures/consumer-extension/dist/options.html +10 -0
  21. package/dist/test/fixtures/consumer-extension/dist/popup.html +11 -0
  22. package/dist/test/harness/extension/background.js +26 -0
  23. package/dist/test/harness/extension/manifest.json +27 -0
  24. package/dist/test/harness/extension/options.html +12 -0
  25. package/dist/test/harness/extension/popup.html +12 -0
  26. package/dist/test/harness/extension/sidepanel.html +12 -0
  27. package/dist/test/index.js +63 -0
  28. package/dist/test/runner.js +407 -0
  29. package/dist/test/runners/boot.js +201 -0
  30. package/dist/test/runners/chromium.js +399 -0
  31. package/dist/test/suites/background/messaging.test.js +42 -0
  32. package/dist/test/suites/background/storage.test.js +44 -0
  33. package/dist/test/suites/background/sw-context.test.js +47 -0
  34. package/dist/test/suites/boot/extension-loads.test.js +56 -0
  35. package/dist/test/suites/build/affiliatizer.test.js +63 -0
  36. package/dist/test/suites/build/cli.test.js +123 -0
  37. package/dist/test/suites/build/expect.test.js +47 -0
  38. package/dist/test/suites/build/exports.test.js +52 -0
  39. package/dist/test/suites/build/extension-fallback.test.js +41 -0
  40. package/dist/test/suites/build/logger-lite.test.js +59 -0
  41. package/dist/test/suites/build/manager.test.js +162 -0
  42. package/dist/test/suites/build/mode-helpers.test.js +96 -0
  43. package/dist/test/suites/view/options-and-sidepanel.test.js +14 -0
  44. package/dist/test/suites/view/popup-context.test.js +51 -0
  45. package/dist/test/suites/view/sidepanel.test.js +14 -0
  46. package/dist/utils/mode-helpers.js +92 -0
  47. package/package.json +16 -13
@@ -2,7 +2,7 @@
2
2
  const Manager = new (require('../../build.js'));
3
3
  const logger = Manager.logger('html');
4
4
  const { src, dest, watch, series } = require('gulp');
5
- const through2 = require('through2');
5
+ const { Transform } = require('node:stream');
6
6
  const jetpack = require('fs-jetpack');
7
7
  const path = require('path');
8
8
  const { template } = require('node-powertools');
@@ -51,86 +51,89 @@ function html(complete) {
51
51
 
52
52
  // Process HTML transform
53
53
  function processHtml(templateContent) {
54
- return through2.obj(function (file, _, callback) {
55
- // Skip if it's a directory
56
- if (file.isDirectory()) {
57
- return callback(null, file);
58
- }
59
-
60
- try {
61
- // Get the view name from the file path
62
- const viewName = path.basename(file.path, '.html');
63
- const relativePath = path.relative(path.join(rootPathProject, 'src/views'), file.dirname);
64
- const viewNameWithPath = relativePath ? `${relativePath}/${viewName}` : viewName;
65
-
66
- // Read the content (body HTML)
67
- const bodyContent = file.contents.toString();
68
-
69
- // Determine the component name for CSS/JS loading
70
- // Template already includes /components/ prefix, so just provide the component path
71
- // Pages can have multiple files (index, pricing, login, etc.)
72
- // Other components (popup, options, etc.) only have index.html
73
- let componentName;
74
- if (relativePath.startsWith('pages/') || relativePath === 'pages') {
75
- // Pages directory: include full path with filename
76
- // pages/index.html -> pages/index
77
- // pages/pricing.html -> pages/pricing
78
- componentName = viewNameWithPath;
79
- } else if (relativePath && viewName === 'index') {
80
- // Other components with index.html: use just the directory name
81
- // popup/index.html -> popup
82
- // options/index.html -> options
83
- componentName = relativePath;
84
- } else if (relativePath) {
85
- // Non-index files in other directories: use full path
86
- componentName = viewNameWithPath;
87
- } else {
88
- // No path: use viewName
89
- componentName = viewName;
54
+ return new Transform({
55
+ objectMode: true,
56
+ transform(file, _, callback) {
57
+ // Skip if it's a directory
58
+ if (file.isDirectory()) {
59
+ return callback(null, file);
90
60
  }
91
61
 
92
- // Prepare template data
93
- const data = {
94
- content: bodyContent,
95
- page: {
96
- name: componentName,
97
- path: viewNameWithPath,
98
- title: config.brand?.name || 'Extension',
99
- },
100
- theme: {
101
- appearance: config.theme?.appearance || 'dark',
102
- },
103
- brand: config.brand || {},
104
- cacheBust: Date.now(),
105
- };
106
-
107
- // Apply template with custom brackets
108
- // First, template the body content to replace any {{ }} placeholders in the view
109
- const templatedBody = template(bodyContent, data, {
110
- brackets: ['{{', '}}'],
111
- });
112
-
113
- // Update data with templated body
114
- data.content = templatedBody;
115
-
116
- // Then template the outer page template
117
- const rendered = template(templateContent, data, {
118
- brackets: ['{{', '}}'],
119
- });
120
-
121
- // Update file contents
122
- file.contents = Buffer.from(rendered);
123
-
124
- // Log
125
- logger.log(`Processed: ${viewNameWithPath}.html`);
126
-
127
- // Push the file
128
- this.push(file);
129
- return callback();
130
- } catch (error) {
131
- logger.error('Error processing HTML:', error);
132
- return callback(error);
133
- }
62
+ try {
63
+ // Get the view name from the file path
64
+ const viewName = path.basename(file.path, '.html');
65
+ const relativePath = path.relative(path.join(rootPathProject, 'src/views'), file.dirname);
66
+ const viewNameWithPath = relativePath ? `${relativePath}/${viewName}` : viewName;
67
+
68
+ // Read the content (body HTML)
69
+ const bodyContent = file.contents.toString();
70
+
71
+ // Determine the component name for CSS/JS loading
72
+ // Template already includes /components/ prefix, so just provide the component path
73
+ // Pages can have multiple files (index, pricing, login, etc.)
74
+ // Other components (popup, options, etc.) only have index.html
75
+ let componentName;
76
+ if (relativePath.startsWith('pages/') || relativePath === 'pages') {
77
+ // Pages directory: include full path with filename
78
+ // pages/index.html -> pages/index
79
+ // pages/pricing.html -> pages/pricing
80
+ componentName = viewNameWithPath;
81
+ } else if (relativePath && viewName === 'index') {
82
+ // Other components with index.html: use just the directory name
83
+ // popup/index.html -> popup
84
+ // options/index.html -> options
85
+ componentName = relativePath;
86
+ } else if (relativePath) {
87
+ // Non-index files in other directories: use full path
88
+ componentName = viewNameWithPath;
89
+ } else {
90
+ // No path: use viewName
91
+ componentName = viewName;
92
+ }
93
+
94
+ // Prepare template data
95
+ const data = {
96
+ content: bodyContent,
97
+ page: {
98
+ name: componentName,
99
+ path: viewNameWithPath,
100
+ title: config.brand?.name || 'Extension',
101
+ },
102
+ theme: {
103
+ appearance: config.theme?.appearance || 'dark',
104
+ },
105
+ brand: config.brand || {},
106
+ cacheBust: Date.now(),
107
+ };
108
+
109
+ // Apply template with custom brackets
110
+ // First, template the body content to replace any {{ }} placeholders in the view
111
+ const templatedBody = template(bodyContent, data, {
112
+ brackets: ['{{', '}}'],
113
+ });
114
+
115
+ // Update data with templated body
116
+ data.content = templatedBody;
117
+
118
+ // Then template the outer page template
119
+ const rendered = template(templateContent, data, {
120
+ brackets: ['{{', '}}'],
121
+ });
122
+
123
+ // Update file contents
124
+ file.contents = Buffer.from(rendered);
125
+
126
+ // Log
127
+ logger.log(`Processed: ${viewNameWithPath}.html`);
128
+
129
+ // Push the file
130
+ this.push(file);
131
+ return callback();
132
+ } catch (error) {
133
+ logger.error('Error processing HTML:', error);
134
+ return callback(error);
135
+ }
136
+ },
134
137
  });
135
138
  }
136
139
 
@@ -1,49 +1,49 @@
1
1
  // Libraries
2
- const through2 = require('through2');
2
+ const { Transform } = require('node:stream');
3
3
  const { template } = require('node-powertools');
4
4
  const path = require('path');
5
5
 
6
6
  /**
7
- * Creates a through2 transform stream that processes template variables in files
7
+ * Creates a transform stream that processes template variables in files
8
8
  **/
9
9
  function createTemplateTransform(data) {
10
10
  const extensions = ['html', 'md', 'liquid', 'json', 'yml', 'yaml']
11
11
 
12
- return through2.obj(function(file, encoding, callback) {
13
- // Skip directories
14
- if (file.isDirectory()) {
15
- return callback(null, file);
16
- }
17
-
18
- // Check if file extension matches
19
- const ext = path.extname(file.path).toLowerCase().slice(1);
20
- if (!extensions.includes(ext)) {
21
- return callback(null, file);
22
- }
23
-
24
- // Log
25
- // console.log(`Processing file: ${file.path}`);
26
-
27
- // Process the file contents
28
- try {
29
- const contents = file.contents.toString();
30
-
31
- // Process templates
32
- const templated = template(contents, data, {
33
- brackets: ['[', ']'],
34
- });
35
-
36
- // Update file contents if changed
37
- if (contents !== templated) {
38
- file.contents = Buffer.from(templated);
39
- const relativePath = file.relative || file.path;
12
+ return new Transform({
13
+ objectMode: true,
14
+ transform(file, encoding, callback) {
15
+ // Skip directories
16
+ if (file.isDirectory()) {
17
+ return callback(null, file);
40
18
  }
41
- } catch (error) {
42
- console.error(`Error processing templates in ${file.path}:`, error);
43
- }
44
19
 
45
- // Pass the file through
46
- callback(null, file);
20
+ // Check if file extension matches
21
+ const ext = path.extname(file.path).toLowerCase().slice(1);
22
+ if (!extensions.includes(ext)) {
23
+ return callback(null, file);
24
+ }
25
+
26
+ // Process the file contents
27
+ try {
28
+ const contents = file.contents.toString();
29
+
30
+ // Process templates
31
+ const templated = template(contents, data, {
32
+ brackets: ['[', ']'],
33
+ });
34
+
35
+ // Update file contents if changed
36
+ if (contents !== templated) {
37
+ file.contents = Buffer.from(templated);
38
+ const relativePath = file.relative || file.path;
39
+ }
40
+ } catch (error) {
41
+ console.error(`Error processing templates in ${file.path}:`, error);
42
+ }
43
+
44
+ // Pass the file through
45
+ callback(null, file);
46
+ },
47
47
  });
48
48
  }
49
49
 
package/dist/offscreen.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Libraries
2
2
  import extension from './lib/extension.js';
3
3
  import LoggerLite from './lib/logger-lite.js';
4
+ import { attachTo as attachModeHelpers } from './utils/mode-helpers.js';
4
5
 
5
6
  // Class
6
7
  class Manager {
@@ -23,5 +24,8 @@ class Manager {
23
24
  }
24
25
  }
25
26
 
27
+ // Cross-context helpers — Manager.isTesting() / isDevelopment() / etc.
28
+ attachModeHelpers(Manager);
29
+
26
30
  // Export
27
31
  export default Manager;
package/dist/options.js CHANGED
@@ -3,6 +3,7 @@ import webManager from 'web-manager';
3
3
  import extension from './lib/extension.js';
4
4
  import LoggerLite from './lib/logger-lite.js';
5
5
  import { syncWithBackground, setupAuthBroadcastListener, setupSignOutListener, setupAuthEventListeners, openAuthPage as openAuthPageHelper } from './lib/auth-helpers.js';
6
+ import { attachTo as attachModeHelpers } from './utils/mode-helpers.js';
6
7
 
7
8
  // Import theme (exposes Bootstrap to window.bootstrap)
8
9
  import '__theme__/_theme.js';
@@ -59,5 +60,8 @@ class Manager {
59
60
  }
60
61
  }
61
62
 
63
+ // Cross-context helpers — Manager.isTesting() / isDevelopment() / etc.
64
+ attachModeHelpers(Manager);
65
+
62
66
  // Export
63
67
  export default Manager;
package/dist/page.js CHANGED
@@ -3,6 +3,7 @@ import webManager from 'web-manager';
3
3
  import extension from './lib/extension.js';
4
4
  import LoggerLite from './lib/logger-lite.js';
5
5
  import { syncWithBackground, setupAuthBroadcastListener, setupSignOutListener, setupAuthEventListeners, openAuthPage as openAuthPageHelper } from './lib/auth-helpers.js';
6
+ import { attachTo as attachModeHelpers } from './utils/mode-helpers.js';
6
7
 
7
8
  // Import theme (exposes Bootstrap to window.bootstrap)
8
9
  import '__theme__/_theme.js';
@@ -59,5 +60,8 @@ class Manager {
59
60
  }
60
61
  }
61
62
 
63
+ // Cross-context helpers — Manager.isTesting() / isDevelopment() / etc.
64
+ attachModeHelpers(Manager);
65
+
62
66
  // Export
63
67
  export default Manager;
package/dist/popup.js CHANGED
@@ -3,6 +3,7 @@ import webManager from 'web-manager';
3
3
  import extension from './lib/extension.js';
4
4
  import LoggerLite from './lib/logger-lite.js';
5
5
  import { syncWithBackground, setupAuthBroadcastListener, setupSignOutListener, setupAuthEventListeners, openAuthPage as openAuthPageHelper } from './lib/auth-helpers.js';
6
+ import { attachTo as attachModeHelpers } from './utils/mode-helpers.js';
6
7
 
7
8
  // Import theme (exposes Bootstrap to window.bootstrap)
8
9
  import '__theme__/_theme.js';
@@ -59,5 +60,8 @@ class Manager {
59
60
  }
60
61
  }
61
62
 
63
+ // Cross-context helpers — Manager.isTesting() / isDevelopment() / etc.
64
+ attachModeHelpers(Manager);
65
+
62
66
  // Export
63
67
  export default Manager;
package/dist/sidepanel.js CHANGED
@@ -3,6 +3,7 @@ import webManager from 'web-manager';
3
3
  import extension from './lib/extension.js';
4
4
  import LoggerLite from './lib/logger-lite.js';
5
5
  import { syncWithBackground, setupAuthBroadcastListener, setupSignOutListener, setupAuthEventListeners, openAuthPage as openAuthPageHelper } from './lib/auth-helpers.js';
6
+ import { attachTo as attachModeHelpers } from './utils/mode-helpers.js';
6
7
 
7
8
  // Import theme (exposes Bootstrap to window.bootstrap)
8
9
  import '__theme__/_theme.js';
@@ -59,5 +60,8 @@ class Manager {
59
60
  }
60
61
  }
61
62
 
63
+ // Cross-context helpers — Manager.isTesting() / isDevelopment() / etc.
64
+ attachModeHelpers(Manager);
65
+
62
66
  // Export
63
67
  export default Manager;
@@ -0,0 +1,120 @@
1
+ // Tiny expect() — Jest/Vitest-compatible subset.
2
+ // Each matcher throws on failure; the runner catches.
3
+
4
+ function deepEqual(a, b) {
5
+ if (a === b) return true;
6
+ if (typeof a !== typeof b) return false;
7
+ if (a === null || b === null) return a === b;
8
+ if (typeof a !== 'object') return false;
9
+
10
+ if (Array.isArray(a)) {
11
+ if (!Array.isArray(b) || a.length !== b.length) return false;
12
+ return a.every((x, i) => deepEqual(x, b[i]));
13
+ }
14
+
15
+ const ka = Object.keys(a);
16
+ const kb = Object.keys(b);
17
+ if (ka.length !== kb.length) return false;
18
+ return ka.every((k) => deepEqual(a[k], b[k]));
19
+ }
20
+
21
+ function fmt(v) {
22
+ if (typeof v === 'string') return JSON.stringify(v);
23
+ if (v === undefined) return 'undefined';
24
+ try {
25
+ return JSON.stringify(v);
26
+ } catch (e) {
27
+ return String(v);
28
+ }
29
+ }
30
+
31
+ function fail(message) {
32
+ const err = new Error(message);
33
+ err.name = 'AssertionError';
34
+ throw err;
35
+ }
36
+
37
+ function buildMatchers(actual, negated) {
38
+ const not = negated ? 'not ' : '';
39
+
40
+ function check(cond, message) {
41
+ if (negated) cond = !cond;
42
+ if (!cond) fail(message);
43
+ }
44
+
45
+ return {
46
+ toBe(expected) {
47
+ check(actual === expected, `expected ${fmt(actual)} ${not}to be ${fmt(expected)}`);
48
+ },
49
+ toEqual(expected) {
50
+ check(deepEqual(actual, expected), `expected ${fmt(actual)} ${not}to deeply equal ${fmt(expected)}`);
51
+ },
52
+ toBeTruthy() {
53
+ check(!!actual, `expected ${fmt(actual)} ${not}to be truthy`);
54
+ },
55
+ toBeFalsy() {
56
+ check(!actual, `expected ${fmt(actual)} ${not}to be falsy`);
57
+ },
58
+ toBeDefined() {
59
+ check(actual !== undefined, `expected ${fmt(actual)} ${not}to be defined`);
60
+ },
61
+ toBeUndefined() {
62
+ check(actual === undefined, `expected ${fmt(actual)} ${not}to be undefined`);
63
+ },
64
+ toBeNull() {
65
+ check(actual === null, `expected ${fmt(actual)} ${not}to be null`);
66
+ },
67
+ toContain(item) {
68
+ const has = Array.isArray(actual)
69
+ ? actual.includes(item)
70
+ : (typeof actual === 'string' && actual.includes(item));
71
+ check(has, `expected ${fmt(actual)} ${not}to contain ${fmt(item)}`);
72
+ },
73
+ toHaveProperty(key) {
74
+ const has = actual != null && Object.prototype.hasOwnProperty.call(actual, key);
75
+ check(has, `expected ${fmt(actual)} ${not}to have property "${key}"`);
76
+ },
77
+ toMatch(regex) {
78
+ check(regex.test(actual), `expected ${fmt(actual)} ${not}to match ${regex}`);
79
+ },
80
+ toBeInstanceOf(cls) {
81
+ check(actual instanceof cls, `expected value ${not}to be instance of ${cls.name}`);
82
+ },
83
+ toBeGreaterThan(n) {
84
+ check(actual > n, `expected ${fmt(actual)} ${not}to be > ${n}`);
85
+ },
86
+ toBeLessThan(n) {
87
+ check(actual < n, `expected ${fmt(actual)} ${not}to be < ${n}`);
88
+ },
89
+ async toThrow(matcher) {
90
+ let threw = false;
91
+ let thrown;
92
+ try {
93
+ if (typeof actual === 'function') {
94
+ await actual();
95
+ }
96
+ } catch (e) {
97
+ threw = true;
98
+ thrown = e;
99
+ }
100
+ if (!threw) {
101
+ return check(false, `expected function ${not}to throw`);
102
+ }
103
+ if (matcher instanceof RegExp) {
104
+ check(matcher.test(thrown.message), `expected thrown message ${not}to match ${matcher} (got: ${thrown.message})`);
105
+ } else if (typeof matcher === 'string') {
106
+ check(thrown.message.includes(matcher), `expected thrown message ${not}to contain "${matcher}" (got: ${thrown.message})`);
107
+ } else {
108
+ check(true, '');
109
+ }
110
+ },
111
+ };
112
+ }
113
+
114
+ function expect(actual) {
115
+ const m = buildMatchers(actual, false);
116
+ m.not = buildMatchers(actual, true);
117
+ return m;
118
+ }
119
+
120
+ module.exports = expect;
@@ -0,0 +1,15 @@
1
+ // BXM fixture consumer — pretends to be a real BXM-based extension's background.
2
+ // Boot tests verify this SW comes up cleanly and exposes a couple of probe hooks.
3
+
4
+ globalThis.__bxmFixtureBooted = true;
5
+ globalThis.__bxmFixtureBootedAt = Date.now();
6
+
7
+ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
8
+ if (msg && msg.type === 'fixture:hello') {
9
+ sendResponse({ ok: true, version: chrome.runtime.getManifest().version });
10
+ return false;
11
+ }
12
+ return false;
13
+ });
14
+
15
+ console.log('[bxm-fixture] background ready');
@@ -0,0 +1,20 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "BXM Fixture Consumer",
4
+ "version": "0.1.0",
5
+ "description": "Used by BXM's boot-layer tests as a stand-in for a real consumer extension.",
6
+ "background": {
7
+ "service_worker": "background.js"
8
+ },
9
+ "action": {
10
+ "default_popup": "popup.html",
11
+ "default_title": "BXM Fixture Consumer"
12
+ },
13
+ "options_ui": {
14
+ "page": "options.html",
15
+ "open_in_tab": true
16
+ },
17
+ "permissions": [
18
+ "storage"
19
+ ]
20
+ }
@@ -0,0 +1,10 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>BXM Fixture Consumer — Options</title>
6
+ </head>
7
+ <body>
8
+ <h1>Fixture Options</h1>
9
+ </body>
10
+ </html>
@@ -0,0 +1,11 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>BXM Fixture Consumer — Popup</title>
6
+ </head>
7
+ <body>
8
+ <h1 id="main-content">Fixture Popup</h1>
9
+ <p>Boot tests assert that this page renders inside Chromium when loaded as an unpacked extension.</p>
10
+ </body>
11
+ </html>
@@ -0,0 +1,26 @@
1
+ // BXM test-harness background service worker.
2
+ //
3
+ // This SW does ONE job: stay alive long enough for the chromium runner to attach
4
+ // a CDP session and inject test code via Runtime.evaluate. The runner discovers
5
+ // this SW via Puppeteer's service-worker target API, then drives test execution
6
+ // directly from the parent Node process — this file is intentionally minimal.
7
+ //
8
+ // We set a couple of globals the injected test code can rely on:
9
+ // globalThis.BXM_TEST_MODE — picked up by Manager.isTesting()
10
+ // globalThis.__bxmTestEmit — defined by the runner before each test;
11
+ // used by injected code to report results
12
+ //
13
+ // We also publish a `chrome.runtime.onMessage` ping handler so view-layer tests
14
+ // (running in popup/options/sidepanel tabs) can verify the SW is alive.
15
+
16
+ globalThis.BXM_TEST_MODE = true;
17
+
18
+ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
19
+ if (msg && msg.type === 'bxm:test:ping') {
20
+ sendResponse({ pong: true, ts: Date.now() });
21
+ return false; // sync response
22
+ }
23
+ return false;
24
+ });
25
+
26
+ console.log('[bxm-harness] service worker ready');
@@ -0,0 +1,27 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "BXM Test Harness",
4
+ "version": "1.0.0",
5
+ "description": "Loaded by the chromium runner to host background + view layer tests.",
6
+ "background": {
7
+ "service_worker": "background.js"
8
+ },
9
+ "action": {
10
+ "default_popup": "popup.html",
11
+ "default_title": "BXM Test Harness"
12
+ },
13
+ "options_ui": {
14
+ "page": "options.html",
15
+ "open_in_tab": true
16
+ },
17
+ "side_panel": {
18
+ "default_path": "sidepanel.html"
19
+ },
20
+ "permissions": [
21
+ "storage",
22
+ "sidePanel"
23
+ ],
24
+ "host_permissions": [
25
+ "<all_urls>"
26
+ ]
27
+ }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>BXM Test Harness — Options</title>
6
+ </head>
7
+ <body data-bxm-context="options">
8
+ <h3 id="title">BXM Test Harness — Options</h3>
9
+ <p id="status">ready</p>
10
+ <pre id="log"></pre>
11
+ </body>
12
+ </html>
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>BXM Test Harness — Popup</title>
6
+ </head>
7
+ <body data-bxm-context="popup">
8
+ <h3 id="title">BXM Test Harness</h3>
9
+ <p id="status">ready</p>
10
+ <pre id="log"></pre>
11
+ </body>
12
+ </html>
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>BXM Test Harness — Side Panel</title>
6
+ </head>
7
+ <body data-bxm-context="sidepanel">
8
+ <h3 id="title">BXM Test Harness — Side Panel</h3>
9
+ <p id="status">ready</p>
10
+ <pre id="log"></pre>
11
+ </body>
12
+ </html>