selenium-webext-bridge 0.1.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/LICENSE +21 -0
- package/README.md +272 -0
- package/extension/background.js +309 -0
- package/extension/direct-bridge.js +211 -0
- package/extension/manifest.json +37 -0
- package/extension/test-api.html +64 -0
- package/extension/test-api.js +150 -0
- package/extension/uuid-injector.js +56 -0
- package/index.js +19 -0
- package/lib/test-bridge.js +686 -0
- package/lib/test-helpers.js +430 -0
- package/lib/test-http-server.js +79 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Eric Muyser
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# selenium-webext-bridge
|
|
2
|
+
|
|
3
|
+
[](https://github.com/MrEricSir/selenium-webext-bridge/actions/workflows/test.yml)
|
|
4
|
+
[](https://codecov.io/gh/MrEricSir/selenium-webext-bridge)
|
|
5
|
+
|
|
6
|
+
Build integration tests for your Firefox extensions with ease.
|
|
7
|
+
|
|
8
|
+
This test bridge runs alongside your Firefox extension, allowing Selenium tests written with Node to interact with browser tabs, windows, and communicate with your extension. All with a straightforward API.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install selenium-webext-bridge selenium-webdriver geckodriver
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**Note:** You will need [Firefox](https://www.mozilla.org/firefox/) installed.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Install From Source
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
git clone https://github.com/MrEricSir/selenium-webext-bridge.git
|
|
23
|
+
cd selenium-webext-bridge
|
|
24
|
+
npm install
|
|
25
|
+
npm install selenium-webdriver geckodriver
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Getting Started
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
const { launchBrowser, cleanupBrowser, createTestServer } = require('selenium-webext-bridge');
|
|
32
|
+
|
|
33
|
+
// Start the test server to establish its communications channel.
|
|
34
|
+
const server = await createTestServer({ port: 8080 });
|
|
35
|
+
|
|
36
|
+
// Launch Firefox with the bridge and your extension installed.
|
|
37
|
+
const browser = await launchBrowser({
|
|
38
|
+
extensions: ['/path/to/your/extension']
|
|
39
|
+
});
|
|
40
|
+
const bridge = browser.testBridge;
|
|
41
|
+
|
|
42
|
+
// Communicate with your extension.
|
|
43
|
+
const response = await bridge.sendToExtension('your-ext@id', {
|
|
44
|
+
action: 'getState'
|
|
45
|
+
});
|
|
46
|
+
console.log(response); // Message sent back from your extension.
|
|
47
|
+
|
|
48
|
+
// Try the bridge APIs.
|
|
49
|
+
const tabs = await bridge.getTabs();
|
|
50
|
+
const tab = await bridge.createTab('https://example.com');
|
|
51
|
+
const title = await bridge.executeInTab(tab.id, 'document.title');
|
|
52
|
+
const screenshot = await bridge.captureScreenshot();
|
|
53
|
+
|
|
54
|
+
// Leave everything in a clean state. This would most likely live in a finally {} block.
|
|
55
|
+
await cleanupBrowser(browser);
|
|
56
|
+
server.close();
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Going Deeper
|
|
60
|
+
|
|
61
|
+
The `launchBrowser()` and `cleanupBrowser()` functions are provided for convenience.
|
|
62
|
+
|
|
63
|
+
`launchBrowser()` creates a temporary Firefox profile, installs the bridge extension, initializes it, and then installs any local extensions you specify. Pass `headless: true` to run without a visible browser window (or set the `HEADLESS=1` environment variable.)
|
|
64
|
+
|
|
65
|
+
`cleanupBrowser()` quits the browser and removes the temporary profile.
|
|
66
|
+
|
|
67
|
+
If you need complete control over the browser configuration, you can set up Firefox manually instead. Note that you'll need to handle headless mode, profile management, and extension installs yourself if you go this route.
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
const { Builder } = require('selenium-webdriver');
|
|
71
|
+
const firefox = require('selenium-webdriver/firefox');
|
|
72
|
+
const { TestBridge, extensionDir, sleep } = require('selenium-webext-bridge');
|
|
73
|
+
|
|
74
|
+
const options = new firefox.Options();
|
|
75
|
+
options.addArguments('-headless');
|
|
76
|
+
|
|
77
|
+
const driver = await new Builder()
|
|
78
|
+
.forBrowser('firefox')
|
|
79
|
+
.setFirefoxOptions(options)
|
|
80
|
+
.build();
|
|
81
|
+
|
|
82
|
+
await driver.installAddon(extensionDir, true);
|
|
83
|
+
await sleep(2000);
|
|
84
|
+
|
|
85
|
+
const bridge = new TestBridge(driver);
|
|
86
|
+
await bridge.init();
|
|
87
|
+
|
|
88
|
+
await driver.installAddon('/path/to/your/extension', true);
|
|
89
|
+
await sleep(2000);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Designing Your Extension For Testability
|
|
93
|
+
|
|
94
|
+
Firefox extensions talk to each other using a messaging API. The bridge uses this to communicate with your extension during tests.
|
|
95
|
+
|
|
96
|
+
**Step 1:** Add a listener in your extension's background script to receive messages:
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
// your-extension/background.js
|
|
100
|
+
browser.runtime.onMessageExternal.addListener(async (message, sender) => {
|
|
101
|
+
switch (message.action) {
|
|
102
|
+
case 'getData':
|
|
103
|
+
// Return whatever data your tests need to check.
|
|
104
|
+
return { success: true, data: { count: 42 } };
|
|
105
|
+
case 'doThing':
|
|
106
|
+
// Or perform an action based on the message.
|
|
107
|
+
doThing();
|
|
108
|
+
return { success: true };
|
|
109
|
+
default:
|
|
110
|
+
// Implement some kind of sanity check to handle unknown messages.
|
|
111
|
+
return { success: false, error: 'Unknown action' };
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Step 2:** In your Selenium tests, use `sendToExtension()` to send messages to your listener via the bridge API.
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
// In your tests:
|
|
120
|
+
const response = await bridge.sendToExtension('your-ext@id', {
|
|
121
|
+
action: 'getData'
|
|
122
|
+
});
|
|
123
|
+
console.log(response); // { success: true, data: { count: 42 } }
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
It's up to you what to implement in your listener. Some possibilities include returning internal state, resetting variables between tests, and interacting with the UI.
|
|
127
|
+
|
|
128
|
+
**Note:** It's somewhat common for extensions to filter out unexpected `sender.id` values. In your extension the `sender.id` of messages sent this way will be the bridge's extension ID: `selenium-webext-bridge@test.local`.
|
|
129
|
+
|
|
130
|
+
## API
|
|
131
|
+
|
|
132
|
+
### TestBridge
|
|
133
|
+
|
|
134
|
+
#### Core
|
|
135
|
+
| Method | Description |
|
|
136
|
+
|:-------|:------------|
|
|
137
|
+
| `new TestBridge(driver)` | Creates a test bridge instance |
|
|
138
|
+
| `init()` | Navigates to a page and waits for the bridge content script to inject |
|
|
139
|
+
| `ping()` | Verifies the bridge is working (returns `"pong"`) |
|
|
140
|
+
| `reset()` | Resets the bridge by navigating to an HTTP page and re-initializing. Use after visiting extension or `about:` pages. |
|
|
141
|
+
| `captureScreenshot(format?)` | Screenshots the active tab (returns `data:image/png;...`) |
|
|
142
|
+
| `getExtensionUrl(extensionId)` | Returns the `moz-extension://` URL for an installed extension by its ID (the `id` field from the extension's `manifest.json`). |
|
|
143
|
+
| `getExtensionUrlByName(name)` | Returns the `moz-extension://` URL for an installed extension by its `name` field from `manifest.json`. Useful for extensions without a fixed ID. |
|
|
144
|
+
| `clickBrowserAction(extensionId)` | Clicks an extension's toolbar button. Requires `launchBrowser({ firefoxArgs: ['-remote-allow-system-access'] })`. |
|
|
145
|
+
|
|
146
|
+
#### Extension Forwarding
|
|
147
|
+
| Method | Description |
|
|
148
|
+
|:-------|:------------|
|
|
149
|
+
| `sendToExtension(extensionId, payload)` | Forwards a message to any installed extension |
|
|
150
|
+
|
|
151
|
+
#### Tab Queries
|
|
152
|
+
| Method | Description |
|
|
153
|
+
|:-------|:------------|
|
|
154
|
+
| `getTabs()` | Gets all browser tabs |
|
|
155
|
+
| `getTabById(tabId)` | Gets a single tab's full state |
|
|
156
|
+
| `getActiveTab()` | Gets the currently active tab in the current window |
|
|
157
|
+
| `getTabGroups()` | Gets all tab groups (empty array if not supported) |
|
|
158
|
+
|
|
159
|
+
#### Tab Lifecycle
|
|
160
|
+
| Method | Description |
|
|
161
|
+
|:-------|:------------|
|
|
162
|
+
| `createTab(url, active?)` | Opens a new tab (without switching Selenium focus) |
|
|
163
|
+
| `closeTab(tabId)` | Closes a tab by ID |
|
|
164
|
+
| `updateTab(tabId, { url?, active?, muted?, pinned? })` | Updates properties of a tab |
|
|
165
|
+
| `reloadTab(tabId)` | Reloads a tab |
|
|
166
|
+
|
|
167
|
+
#### Tab State
|
|
168
|
+
| Method | Description |
|
|
169
|
+
|:-------|:------------|
|
|
170
|
+
| `moveTab(tabId, index)` | Moves a tab to a new position |
|
|
171
|
+
| `pinTab(tabId)` / `unpinTab(tabId)` | Pins or unpins a tab |
|
|
172
|
+
| `muteTab(tabId)` / `unmuteTab(tabId)` | Mutes or unmutes a tab |
|
|
173
|
+
| `groupTabs(tabIds, title, color?, groupId?)` | Groups tabs into a new or existing tab group |
|
|
174
|
+
| `ungroupTabs(tabIds)` | Ungroups tabs |
|
|
175
|
+
|
|
176
|
+
#### Tab Execution and Events
|
|
177
|
+
| Method | Description |
|
|
178
|
+
|:-------|:------------|
|
|
179
|
+
| `executeInTab(tabId, code)` | Runs JavaScript in a specific tab and returns the result |
|
|
180
|
+
| `getTabEvents(clear?)` | Gets buffered tab created/updated/removed events (last 100). Pass `true` to clear. |
|
|
181
|
+
|
|
182
|
+
#### Tab Waiters
|
|
183
|
+
| Method | Description |
|
|
184
|
+
|:-------|:------------|
|
|
185
|
+
| `waitForTabCount(n, timeout?)` | Waits until the browser has exactly `n` tabs |
|
|
186
|
+
| `waitForTabUrl(pattern, timeout?)` | Waits for any tab URL to contain `pattern` (returns the tab, or `null` on timeout) |
|
|
187
|
+
| `waitForTabEvent(eventType, timeout?)` | Waits for a specific tab event type (e.g. `'created'`, `'removed'`). Returns the event, or `null` on timeout. |
|
|
188
|
+
| `waitForTabLoad(tabId, timeout?)` | Waits for a tab to finish loading and returns the loaded tab, or `null` on timeout. |
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
#### Window Management
|
|
192
|
+
| Method | Description |
|
|
193
|
+
|:-------|:------------|
|
|
194
|
+
| `getWindows()` | Lists all windows with their tabs |
|
|
195
|
+
| `createWindow(url?, options?)` | Opens a new browser window. Options: `{ type, state, width, height, left, top }` |
|
|
196
|
+
| `closeWindow(windowId)` | Closes a window |
|
|
197
|
+
| `getWindowById(windowId)` | Gets a single window's state with its tabs |
|
|
198
|
+
| `updateWindow(windowId, props)` | Updates window properties (`{ state, width, height, left, top, focused }`) |
|
|
199
|
+
|
|
200
|
+
#### Window Misc.
|
|
201
|
+
| Method | Description |
|
|
202
|
+
|:-------|:------------|
|
|
203
|
+
| `getWindowEvents(clear?)` | Gets buffered window created/removed events (last 100). Pass `true` to clear. |
|
|
204
|
+
| `waitForWindowCount(n, timeout?)` | Waits until the browser has exactly `n` windows |
|
|
205
|
+
|
|
206
|
+
### Helpers
|
|
207
|
+
|
|
208
|
+
| Export | Description |
|
|
209
|
+
|:-------|:------------|
|
|
210
|
+
| `launchBrowser(options?)` | Launches Firefox with the bridge extension installed. Options: `{ extensions, BridgeClass, headless, waitForInit, preferences, firefoxArgs }`. Returns `{ driver, testBridge, profilePath }` |
|
|
211
|
+
| `cleanupBrowser(browser)` | Quits the browser and removes its temporary profile |
|
|
212
|
+
| `extensionDir` | Path to the bridge extension directory (for manual setup with `driver.installAddon()`) |
|
|
213
|
+
| `sleep(ms)` | Promise-based delay |
|
|
214
|
+
| `waitForCondition(conditionFn, timeout?, interval?)` | Calls `conditionFn` until it returns a truthy value |
|
|
215
|
+
| `generateTestUrl(name?, port?)` | Generates `http://127.0.0.1:<port>/<name>-<timestamp>` URLs on the test bridge server |
|
|
216
|
+
| `createTestServer({ port?, host? })` | Starts the local test bridge server |
|
|
217
|
+
| `TabUtils` | Helper class for opening/closing/switching tabs via Selenium |
|
|
218
|
+
| `Assert` | Simple assertion utilities (`equal`, `greaterThan`, `includes`, `isTrue`, ...) |
|
|
219
|
+
| `TestResults` | Tracks test results with `pass()`, `fail()`, `error()`, `summary()` |
|
|
220
|
+
| `Command` | Makes the Command class from `selenium-webdriver` easily available |
|
|
221
|
+
|
|
222
|
+
### Creating a TestBridge Subclass
|
|
223
|
+
|
|
224
|
+
Need custom functionality for your own extension? Add it with a `TestBridge` subclass:
|
|
225
|
+
|
|
226
|
+
```js
|
|
227
|
+
const { TestBridge } = require('selenium-webext-bridge');
|
|
228
|
+
|
|
229
|
+
class MyExtBridge extends TestBridge {
|
|
230
|
+
constructor(driver) {
|
|
231
|
+
super(driver);
|
|
232
|
+
this.extId = 'my-ext@example.com';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async getState() {
|
|
236
|
+
const resp = await this.sendToExtension(this.extId, { action: 'getState' });
|
|
237
|
+
if (!resp.success) throw new Error(resp.error);
|
|
238
|
+
return resp.data;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Then pass it to `launchBrowser()` so it creates your subclass instead of the default:
|
|
244
|
+
|
|
245
|
+
```js
|
|
246
|
+
const browser = await launchBrowser({
|
|
247
|
+
extensions: ['/path/to/your/extension'],
|
|
248
|
+
BridgeClass: MyExtBridge
|
|
249
|
+
});
|
|
250
|
+
const bridge = browser.testBridge; // instanceof MyExtBridge
|
|
251
|
+
const state = await bridge.getState();
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Examples
|
|
255
|
+
|
|
256
|
+
See [`examples/hello-world/`](examples/hello-world/) for a complete minimal example of a Firefox extension with a Selenium integration test.
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
cd examples/hello-world
|
|
260
|
+
node test.js
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
There is a full test suite in [`tests/bridge-api.test.js`](tests/bridge-api.test.js) that makes use of every method. This can provide helpful examples for your own extension tests.
|
|
264
|
+
|
|
265
|
+
## Under The Hood
|
|
266
|
+
|
|
267
|
+
The test bridge extension works around some limitations in Firefox with a bit of trickery. Here's how:
|
|
268
|
+
|
|
269
|
+
1. A **content script** injects `window.TestBridge` into every webpage.
|
|
270
|
+
2. Selenium calls `window.TestBridge` methods via `driver.executeScript()`
|
|
271
|
+
3. The content script relays requests to the bridge's **background script.**
|
|
272
|
+
4. The background script either handles browser API calls directly (getTabs, createTab, executeInTab, etc.) or forwards messages to your extension via `browser.runtime.sendMessage(targetId, payload)`
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selenium WebExt Bridge - Background Script
|
|
3
|
+
*
|
|
4
|
+
* Provides WebExtension API access to Selenium tests
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
console.log('[BRIDGE] Background script loading...');
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
|
|
11
|
+
// Get our own extension URL for UUID detection
|
|
12
|
+
const TEST_API_URL = browser.runtime.getURL('test-api.html');
|
|
13
|
+
const TEST_BRIDGE_UUID = TEST_API_URL.match(/moz-extension:\/\/([^\/]+)/)?.[1];
|
|
14
|
+
console.log('[BRIDGE] Test API URL:', TEST_API_URL);
|
|
15
|
+
console.log('[BRIDGE] UUID:', TEST_BRIDGE_UUID);
|
|
16
|
+
|
|
17
|
+
// Inject UUID into all pages when they load
|
|
18
|
+
browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
|
|
19
|
+
// Only inject when page is loaded and it's a regular page
|
|
20
|
+
if (changeInfo.status === 'complete' && tab.url && !tab.url.startsWith('about:') && !tab.url.startsWith('moz-extension:')) {
|
|
21
|
+
try {
|
|
22
|
+
await browser.tabs.executeScript(tabId, {
|
|
23
|
+
code: `
|
|
24
|
+
window.__TEST_BRIDGE_UUID = ${JSON.stringify(TEST_BRIDGE_UUID)};
|
|
25
|
+
window.__TEST_BRIDGE_URL = ${JSON.stringify(TEST_API_URL)};
|
|
26
|
+
console.log('[BRIDGE] UUID injected:', window.__TEST_BRIDGE_UUID);
|
|
27
|
+
`,
|
|
28
|
+
runAt: 'document_start'
|
|
29
|
+
});
|
|
30
|
+
} catch (error) {
|
|
31
|
+
// Ignore errors (e.g., privileged pages)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// --- Tab Event Buffer ---
|
|
37
|
+
const TAB_EVENT_BUFFER_SIZE = 100;
|
|
38
|
+
const tabEventBuffer = [];
|
|
39
|
+
|
|
40
|
+
function pushTabEvent(event) {
|
|
41
|
+
tabEventBuffer.push(event);
|
|
42
|
+
if (tabEventBuffer.length > TAB_EVENT_BUFFER_SIZE) {
|
|
43
|
+
tabEventBuffer.shift();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
browser.tabs.onCreated.addListener((tab) => {
|
|
48
|
+
pushTabEvent({ type: 'created', tab, timestamp: Date.now() });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
|
52
|
+
pushTabEvent({ type: 'updated', tabId, changeInfo, tab, timestamp: Date.now() });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
browser.tabs.onRemoved.addListener((tabId, removeInfo) => {
|
|
56
|
+
pushTabEvent({ type: 'removed', tabId, removeInfo, timestamp: Date.now() });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// --- Window Event Buffer ---
|
|
60
|
+
const WINDOW_EVENT_BUFFER_SIZE = 100;
|
|
61
|
+
const windowEventBuffer = [];
|
|
62
|
+
|
|
63
|
+
function pushWindowEvent(event) {
|
|
64
|
+
windowEventBuffer.push(event);
|
|
65
|
+
if (windowEventBuffer.length > WINDOW_EVENT_BUFFER_SIZE) {
|
|
66
|
+
windowEventBuffer.shift();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
browser.windows.onCreated.addListener((window) => {
|
|
71
|
+
pushWindowEvent({ type: 'created', window, timestamp: Date.now() });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
browser.windows.onRemoved.addListener((windowId) => {
|
|
75
|
+
pushWindowEvent({ type: 'removed', windowId, timestamp: Date.now() });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// --- Message Handler ---
|
|
79
|
+
console.log('[BRIDGE] Registering message listener...');
|
|
80
|
+
browser.runtime.onMessage.addListener(async (message, sender) => {
|
|
81
|
+
try {
|
|
82
|
+
switch (message.action) {
|
|
83
|
+
// --- Existing APIs ---
|
|
84
|
+
|
|
85
|
+
case 'getTabs':
|
|
86
|
+
const tabs = await browser.tabs.query({});
|
|
87
|
+
return { success: true, data: tabs };
|
|
88
|
+
|
|
89
|
+
case 'getTabGroups':
|
|
90
|
+
if (browser.tabGroups) {
|
|
91
|
+
const groups = await browser.tabGroups.query({});
|
|
92
|
+
return { success: true, data: groups };
|
|
93
|
+
} else {
|
|
94
|
+
return { success: true, data: [] };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
case 'ping':
|
|
98
|
+
return { success: true, data: 'pong' };
|
|
99
|
+
|
|
100
|
+
case 'moveTab':
|
|
101
|
+
const movedTab = await browser.tabs.move(message.tabId, { index: message.index });
|
|
102
|
+
return { success: true, data: movedTab };
|
|
103
|
+
|
|
104
|
+
case 'pinTab':
|
|
105
|
+
const pinnedTab = await browser.tabs.update(message.tabId, { pinned: true });
|
|
106
|
+
return { success: true, data: pinnedTab };
|
|
107
|
+
|
|
108
|
+
case 'unpinTab':
|
|
109
|
+
const unpinnedTab = await browser.tabs.update(message.tabId, { pinned: false });
|
|
110
|
+
return { success: true, data: unpinnedTab };
|
|
111
|
+
|
|
112
|
+
case 'muteTab': {
|
|
113
|
+
const mutedTab = await browser.tabs.update(message.tabId, { muted: true });
|
|
114
|
+
return { success: true, data: mutedTab };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
case 'unmuteTab': {
|
|
118
|
+
const unmutedTab = await browser.tabs.update(message.tabId, { muted: false });
|
|
119
|
+
return { success: true, data: unmutedTab };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
case 'reloadTab': {
|
|
123
|
+
await browser.tabs.reload(message.tabId);
|
|
124
|
+
return { success: true, data: null };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
case 'getActiveTab': {
|
|
128
|
+
const activeTabs = await browser.tabs.query({ active: true, currentWindow: true });
|
|
129
|
+
return { success: true, data: activeTabs[0] || null };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
case 'groupTabs':
|
|
133
|
+
if (!browser.tabGroups) {
|
|
134
|
+
return { success: false, error: 'Tab Groups API not available' };
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
let groupId = message.groupId;
|
|
138
|
+
if (!groupId || groupId === -1) {
|
|
139
|
+
groupId = await browser.tabs.group({ tabIds: message.tabIds });
|
|
140
|
+
} else {
|
|
141
|
+
await browser.tabs.group({ groupId, tabIds: message.tabIds });
|
|
142
|
+
}
|
|
143
|
+
const group = await browser.tabGroups.update(groupId, {
|
|
144
|
+
title: message.title,
|
|
145
|
+
color: message.color || 'blue'
|
|
146
|
+
});
|
|
147
|
+
return { success: true, data: group };
|
|
148
|
+
} catch (error) {
|
|
149
|
+
return { success: false, error: `Tab group operation failed: ${error.message}` };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case 'ungroupTabs':
|
|
153
|
+
if (!browser.tabGroups) {
|
|
154
|
+
return { success: false, error: 'Tab Groups API not available' };
|
|
155
|
+
}
|
|
156
|
+
await Promise.all(
|
|
157
|
+
message.tabIds.map(tabId => browser.tabs.ungroup(tabId))
|
|
158
|
+
);
|
|
159
|
+
return { success: true, data: null };
|
|
160
|
+
|
|
161
|
+
case 'forwardToExtension':
|
|
162
|
+
try {
|
|
163
|
+
const resp = await browser.runtime.sendMessage(
|
|
164
|
+
message.targetExtensionId,
|
|
165
|
+
message.payload
|
|
166
|
+
);
|
|
167
|
+
return { success: true, data: resp };
|
|
168
|
+
} catch (error) {
|
|
169
|
+
return { success: false, error: `Extension not responding: ${error.message}` };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// --- New: Tab Lifecycle ---
|
|
173
|
+
|
|
174
|
+
case 'createTab': {
|
|
175
|
+
const createProps = { url: message.url || 'about:blank' };
|
|
176
|
+
if (message.active !== undefined) createProps.active = message.active;
|
|
177
|
+
if (message.windowId !== undefined) createProps.windowId = message.windowId;
|
|
178
|
+
const newTab = await browser.tabs.create(createProps);
|
|
179
|
+
return { success: true, data: newTab };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
case 'closeTab':
|
|
183
|
+
await browser.tabs.remove(message.tabId);
|
|
184
|
+
return { success: true, data: null };
|
|
185
|
+
|
|
186
|
+
case 'getTabById': {
|
|
187
|
+
const tab = await browser.tabs.get(message.tabId);
|
|
188
|
+
return { success: true, data: tab };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
case 'updateTab': {
|
|
192
|
+
const updateProps = {};
|
|
193
|
+
if (message.url !== undefined) updateProps.url = message.url;
|
|
194
|
+
if (message.active !== undefined) updateProps.active = message.active;
|
|
195
|
+
if (message.muted !== undefined) updateProps.muted = message.muted;
|
|
196
|
+
if (message.pinned !== undefined) updateProps.pinned = message.pinned;
|
|
197
|
+
const updatedTab = await browser.tabs.update(message.tabId, updateProps);
|
|
198
|
+
return { success: true, data: updatedTab };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// --- New: Tab Waiting ---
|
|
202
|
+
|
|
203
|
+
case 'waitForTabUrl': {
|
|
204
|
+
const pattern = message.pattern;
|
|
205
|
+
const timeout = message.timeout || 10000;
|
|
206
|
+
const startTime = Date.now();
|
|
207
|
+
while (Date.now() - startTime < timeout) {
|
|
208
|
+
const allTabs = await browser.tabs.query({});
|
|
209
|
+
const match = allTabs.find(t => t.url && t.url.includes(pattern));
|
|
210
|
+
if (match) return { success: true, data: match };
|
|
211
|
+
await new Promise(r => setTimeout(r, 250));
|
|
212
|
+
}
|
|
213
|
+
return { success: true, data: null };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// --- New: Execute in Tab ---
|
|
217
|
+
|
|
218
|
+
case 'executeInTab': {
|
|
219
|
+
const results = await browser.tabs.executeScript(message.tabId, {
|
|
220
|
+
code: message.code
|
|
221
|
+
});
|
|
222
|
+
return { success: true, data: results ? results[0] : null };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// --- New: Screenshots ---
|
|
226
|
+
|
|
227
|
+
case 'captureScreenshot': {
|
|
228
|
+
const dataUrl = await browser.tabs.captureVisibleTab(null, {
|
|
229
|
+
format: message.format || 'png'
|
|
230
|
+
});
|
|
231
|
+
return { success: true, data: dataUrl };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// --- New: Window Management ---
|
|
235
|
+
|
|
236
|
+
case 'createWindow': {
|
|
237
|
+
const winProps = { type: 'normal' };
|
|
238
|
+
if (message.url) winProps.url = message.url;
|
|
239
|
+
if (message.type) winProps.type = message.type;
|
|
240
|
+
if (message.state) winProps.state = message.state;
|
|
241
|
+
if (message.width !== undefined) winProps.width = message.width;
|
|
242
|
+
if (message.height !== undefined) winProps.height = message.height;
|
|
243
|
+
if (message.left !== undefined) winProps.left = message.left;
|
|
244
|
+
if (message.top !== undefined) winProps.top = message.top;
|
|
245
|
+
const newWindow = await browser.windows.create(winProps);
|
|
246
|
+
return { success: true, data: newWindow };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
case 'closeWindow':
|
|
250
|
+
await browser.windows.remove(message.windowId);
|
|
251
|
+
return { success: true, data: null };
|
|
252
|
+
|
|
253
|
+
case 'getWindows': {
|
|
254
|
+
const windows = await browser.windows.getAll({ populate: true });
|
|
255
|
+
return { success: true, data: windows };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
case 'getWindowById': {
|
|
259
|
+
const win = await browser.windows.get(message.windowId, { populate: true });
|
|
260
|
+
return { success: true, data: win };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
case 'updateWindow': {
|
|
264
|
+
const updateWinProps = {};
|
|
265
|
+
if (message.state !== undefined) updateWinProps.state = message.state;
|
|
266
|
+
if (message.width !== undefined) updateWinProps.width = message.width;
|
|
267
|
+
if (message.height !== undefined) updateWinProps.height = message.height;
|
|
268
|
+
if (message.left !== undefined) updateWinProps.left = message.left;
|
|
269
|
+
if (message.top !== undefined) updateWinProps.top = message.top;
|
|
270
|
+
if (message.focused !== undefined) updateWinProps.focused = message.focused;
|
|
271
|
+
const updatedWindow = await browser.windows.update(message.windowId, updateWinProps);
|
|
272
|
+
return { success: true, data: updatedWindow };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// --- New: Tab Events ---
|
|
276
|
+
|
|
277
|
+
case 'getTabEvents': {
|
|
278
|
+
const events = [...tabEventBuffer];
|
|
279
|
+
if (message.clear) {
|
|
280
|
+
tabEventBuffer.length = 0;
|
|
281
|
+
}
|
|
282
|
+
return { success: true, data: events };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// --- New: Window Events ---
|
|
286
|
+
|
|
287
|
+
case 'getWindowEvents': {
|
|
288
|
+
const winEvents = [...windowEventBuffer];
|
|
289
|
+
if (message.clear) {
|
|
290
|
+
windowEventBuffer.length = 0;
|
|
291
|
+
}
|
|
292
|
+
return { success: true, data: winEvents };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
default:
|
|
296
|
+
return { success: false, error: 'Unknown action: ' + message.action };
|
|
297
|
+
}
|
|
298
|
+
} catch (error) {
|
|
299
|
+
console.error('[BRIDGE] Error handling message:', error);
|
|
300
|
+
return { success: false, error: error.message };
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
console.log('[BRIDGE] Ready to serve test requests');
|
|
305
|
+
|
|
306
|
+
} catch (error) {
|
|
307
|
+
console.error('[BRIDGE] FATAL ERROR during initialization:', error);
|
|
308
|
+
console.error('[BRIDGE] Stack:', error.stack);
|
|
309
|
+
}
|