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.
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Direct Bridge - Content script that relays messages between page and background
3
+ */
4
+
5
+ console.log('[DIRECT BRIDGE] Initializing...');
6
+
7
+ // Inject TestBridge API into page
8
+ const script = document.createElement('script');
9
+ script.textContent = `
10
+ (function() {
11
+ console.log('[DIRECT BRIDGE] Creating API in page context...');
12
+
13
+ let requestId = 0;
14
+ const pendingRequests = new Map();
15
+
16
+ // Listen for responses from content script via postMessage
17
+ window.addEventListener('message', (event) => {
18
+ if (event.source !== window) return;
19
+ if (event.data && event.data.type === 'bridge-response') {
20
+ const { id, response, error } = event.data;
21
+ const { resolve, reject } = pendingRequests.get(id) || {};
22
+ if (resolve) {
23
+ pendingRequests.delete(id);
24
+ if (error) reject(new Error(error));
25
+ else resolve(response);
26
+ }
27
+ }
28
+ });
29
+
30
+ // Send request to content script via postMessage
31
+ function sendRequest(action, data) {
32
+ return new Promise((resolve, reject) => {
33
+ const id = ++requestId;
34
+ pendingRequests.set(id, { resolve, reject });
35
+ window.postMessage({ type: 'bridge-request', id, action, data }, '*');
36
+ // Timeout after 15 seconds (some ops like waitForTabUrl can take a while)
37
+ setTimeout(() => {
38
+ if (pendingRequests.has(id)) {
39
+ pendingRequests.delete(id);
40
+ reject(new Error('Request timed out'));
41
+ }
42
+ }, 15000);
43
+ });
44
+ }
45
+
46
+ window.TestBridge = {
47
+ // --- Basics ---
48
+ async ping() {
49
+ return await sendRequest('ping');
50
+ },
51
+
52
+ // --- Tab Queries ---
53
+ async getTabs() {
54
+ return await sendRequest('getTabs');
55
+ },
56
+ async getTabById(tabId) {
57
+ return await sendRequest('getTabById', { tabId });
58
+ },
59
+ async getTabGroups() {
60
+ return await sendRequest('getTabGroups');
61
+ },
62
+
63
+ // --- Tab Lifecycle ---
64
+ async createTab(url, active) {
65
+ return await sendRequest('createTab', { url, active });
66
+ },
67
+ async closeTab(tabId) {
68
+ return await sendRequest('closeTab', { tabId });
69
+ },
70
+ async updateTab(tabId, props) {
71
+ return await sendRequest('updateTab', { tabId, ...props });
72
+ },
73
+
74
+ // --- Tab State ---
75
+ async moveTab(tabId, index) {
76
+ return await sendRequest('moveTab', { tabId, index });
77
+ },
78
+ async pinTab(tabId) {
79
+ return await sendRequest('pinTab', { tabId });
80
+ },
81
+ async unpinTab(tabId) {
82
+ return await sendRequest('unpinTab', { tabId });
83
+ },
84
+ async groupTabs(tabIds, title, color, groupId) {
85
+ return await sendRequest('groupTabs', { tabIds, title, color: color || 'blue', groupId });
86
+ },
87
+ async ungroupTabs(tabIds) {
88
+ return await sendRequest('ungroupTabs', { tabIds });
89
+ },
90
+ async muteTab(tabId) {
91
+ return await sendRequest('muteTab', { tabId });
92
+ },
93
+ async unmuteTab(tabId) {
94
+ return await sendRequest('unmuteTab', { tabId });
95
+ },
96
+ async reloadTab(tabId) {
97
+ return await sendRequest('reloadTab', { tabId });
98
+ },
99
+ async getActiveTab() {
100
+ return await sendRequest('getActiveTab');
101
+ },
102
+
103
+ // --- Tab Waiting ---
104
+ async waitForTabUrl(pattern, timeout) {
105
+ return await sendRequest('waitForTabUrl', { pattern, timeout });
106
+ },
107
+ async waitForCondition(conditionFn, timeout) {
108
+ timeout = timeout || 5000;
109
+ const start = Date.now();
110
+ while (Date.now() - start < timeout) {
111
+ if (await conditionFn()) return true;
112
+ await new Promise(r => setTimeout(r, 100));
113
+ }
114
+ return false;
115
+ },
116
+
117
+ // --- Execute in Tab ---
118
+ async executeInTab(tabId, code) {
119
+ return await sendRequest('executeInTab', { tabId, code });
120
+ },
121
+
122
+ // --- Screenshots ---
123
+ async captureScreenshot(format) {
124
+ return await sendRequest('captureScreenshot', { format });
125
+ },
126
+
127
+ // --- Window Management ---
128
+ async createWindow(url, options) {
129
+ return await sendRequest('createWindow', { url, ...options });
130
+ },
131
+ async closeWindow(windowId) {
132
+ return await sendRequest('closeWindow', { windowId });
133
+ },
134
+ async getWindows() {
135
+ return await sendRequest('getWindows');
136
+ },
137
+ async getWindowById(windowId) {
138
+ return await sendRequest('getWindowById', { windowId });
139
+ },
140
+ async updateWindow(windowId, props) {
141
+ return await sendRequest('updateWindow', { windowId, ...props });
142
+ },
143
+
144
+ // --- Tab Events ---
145
+ async getTabEvents(clear) {
146
+ return await sendRequest('getTabEvents', { clear });
147
+ },
148
+
149
+ // --- Window Events ---
150
+ async getWindowEvents(clear) {
151
+ return await sendRequest('getWindowEvents', { clear });
152
+ },
153
+
154
+ // --- Extension Forwarding ---
155
+ async forwardToExtension(targetExtensionId, payload) {
156
+ return await sendRequest('forwardToExtension', { targetExtensionId, payload });
157
+ }
158
+ };
159
+
160
+ console.log('[DIRECT BRIDGE] API ready');
161
+ })();
162
+ `;
163
+ document.documentElement.appendChild(script);
164
+ script.remove();
165
+
166
+ // All actions that route through the background script
167
+ const BG_ACTIONS = new Set([
168
+ 'getTabs', 'getTabGroups', 'moveTab', 'pinTab', 'unpinTab',
169
+ 'muteTab', 'unmuteTab', 'reloadTab', 'getActiveTab',
170
+ 'groupTabs', 'ungroupTabs', 'forwardToExtension',
171
+ 'createTab', 'closeTab', 'getTabById', 'updateTab',
172
+ 'waitForTabUrl', 'executeInTab', 'captureScreenshot',
173
+ 'createWindow', 'closeWindow', 'getWindows', 'getWindowById',
174
+ 'updateWindow', 'getTabEvents', 'getWindowEvents'
175
+ ]);
176
+
177
+ // Listen for requests from page via postMessage
178
+ window.addEventListener('message', async (event) => {
179
+ // Only accept messages from same window
180
+ if (event.source !== window) return;
181
+ if (!event.data || event.data.type !== 'bridge-request') return;
182
+
183
+ const { id, action, data } = event.data;
184
+
185
+ try {
186
+ let response;
187
+
188
+ if (action === 'ping') {
189
+ response = 'pong';
190
+ } else if (BG_ACTIONS.has(action)) {
191
+ // Route through background script
192
+ const bgResponse = await browser.runtime.sendMessage({ action, ...data });
193
+ if (bgResponse && bgResponse.success) {
194
+ response = bgResponse.data;
195
+ } else if (bgResponse && bgResponse.error) {
196
+ throw new Error(bgResponse.error);
197
+ } else {
198
+ response = bgResponse;
199
+ }
200
+ } else {
201
+ throw new Error('Unknown action: ' + action);
202
+ }
203
+
204
+ window.postMessage({ type: 'bridge-response', id, response }, '*');
205
+ } catch (error) {
206
+ console.error('[DIRECT BRIDGE] Error:', error);
207
+ window.postMessage({ type: 'bridge-response', id, error: error.message }, '*');
208
+ }
209
+ });
210
+
211
+ console.log('[DIRECT BRIDGE] Ready');
@@ -0,0 +1,37 @@
1
+ {
2
+ "manifest_version": 2,
3
+ "name": "Selenium WebExt Bridge",
4
+ "version": "1.0.0",
5
+ "description": "Test bridge extension for automated testing of Firefox WebExtensions",
6
+
7
+ "browser_specific_settings": {
8
+ "gecko": {
9
+ "id": "selenium-webext-bridge@test.local"
10
+ }
11
+ },
12
+
13
+ "permissions": [
14
+ "tabs",
15
+ "tabGroups",
16
+ "storage",
17
+ "<all_urls>"
18
+ ],
19
+
20
+ "background": {
21
+ "scripts": ["background.js"]
22
+ },
23
+
24
+ "content_scripts": [
25
+ {
26
+ "matches": ["<all_urls>"],
27
+ "js": ["direct-bridge.js"],
28
+ "run_at": "document_idle",
29
+ "all_frames": false
30
+ }
31
+ ],
32
+
33
+ "web_accessible_resources": [
34
+ "test-api.html",
35
+ "test-api.js"
36
+ ]
37
+ }
@@ -0,0 +1,64 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Selenium WebExt Bridge</title>
6
+ <style>
7
+ body {
8
+ font-family: monospace;
9
+ padding: 20px;
10
+ background: #1e1e1e;
11
+ color: #d4d4d4;
12
+ }
13
+ h1 {
14
+ color: #4ec9b0;
15
+ }
16
+ .status {
17
+ padding: 10px;
18
+ margin: 10px 0;
19
+ border-radius: 4px;
20
+ background: #2d2d2d;
21
+ }
22
+ .ready {
23
+ border-left: 4px solid #4ec9b0;
24
+ }
25
+ .error {
26
+ border-left: 4px solid #f48771;
27
+ }
28
+ pre {
29
+ background: #252525;
30
+ padding: 10px;
31
+ border-radius: 4px;
32
+ overflow-x: auto;
33
+ }
34
+ code {
35
+ color: #ce9178;
36
+ }
37
+ </style>
38
+ </head>
39
+ <body>
40
+ <h1>Selenium WebExt Bridge</h1>
41
+ <div id="status" class="status">
42
+ <strong>Status:</strong> <span id="status-text">Initializing...</span>
43
+ </div>
44
+
45
+ <h2>Usage from Selenium:</h2>
46
+ <pre><code>// Navigate to this page
47
+ await driver.get('moz-extension://&lt;uuid&gt;/test-api.html');
48
+
49
+ // Call API methods
50
+ const tabs = await driver.executeScript(() => {
51
+ return window.TestBridge.getTabs();
52
+ });
53
+
54
+ // Forward messages to your extension
55
+ const state = await driver.executeScript(() => {
56
+ return window.TestBridge.forwardToExtension('your-ext@id', { action: 'getState' });
57
+ });</code></pre>
58
+
59
+ <h2>Available Methods:</h2>
60
+ <div id="methods"></div>
61
+
62
+ <script src="test-api.js"></script>
63
+ </body>
64
+ </html>
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Selenium WebExt Bridge API - Exposed to Selenium tests
3
+ */
4
+
5
+ console.log('[BRIDGE API] Initializing...');
6
+
7
+ // Helper: send message to background and unwrap response
8
+ async function bgCall(msg) {
9
+ const response = await browser.runtime.sendMessage(msg);
10
+ if (!response.success) throw new Error(response.error);
11
+ return response.data;
12
+ }
13
+
14
+ // Create the API that Selenium will call
15
+ window.TestBridge = {
16
+ // --- Basics ---
17
+ async ping() {
18
+ return await bgCall({ action: 'ping' });
19
+ },
20
+
21
+ // --- Tab Queries ---
22
+ async getTabs() {
23
+ return await bgCall({ action: 'getTabs' });
24
+ },
25
+ async getTabById(tabId) {
26
+ return await bgCall({ action: 'getTabById', tabId });
27
+ },
28
+ async getTabGroups() {
29
+ return await bgCall({ action: 'getTabGroups' });
30
+ },
31
+
32
+ // --- Tab Lifecycle ---
33
+ async createTab(url, active) {
34
+ return await bgCall({ action: 'createTab', url, active });
35
+ },
36
+ async closeTab(tabId) {
37
+ return await bgCall({ action: 'closeTab', tabId });
38
+ },
39
+ async updateTab(tabId, props) {
40
+ return await bgCall({ action: 'updateTab', tabId, ...props });
41
+ },
42
+
43
+ // --- Tab State ---
44
+ async moveTab(tabId, index) {
45
+ return await bgCall({ action: 'moveTab', tabId, index });
46
+ },
47
+ async pinTab(tabId) {
48
+ return await bgCall({ action: 'pinTab', tabId });
49
+ },
50
+ async unpinTab(tabId) {
51
+ return await bgCall({ action: 'unpinTab', tabId });
52
+ },
53
+ async groupTabs(tabIds, title, color = 'blue', groupId = null) {
54
+ return await bgCall({ action: 'groupTabs', tabIds, title, color, groupId });
55
+ },
56
+ async ungroupTabs(tabIds) {
57
+ return await bgCall({ action: 'ungroupTabs', tabIds });
58
+ },
59
+ async muteTab(tabId) {
60
+ return await bgCall({ action: 'muteTab', tabId });
61
+ },
62
+ async unmuteTab(tabId) {
63
+ return await bgCall({ action: 'unmuteTab', tabId });
64
+ },
65
+ async reloadTab(tabId) {
66
+ return await bgCall({ action: 'reloadTab', tabId });
67
+ },
68
+ async getActiveTab() {
69
+ return await bgCall({ action: 'getActiveTab' });
70
+ },
71
+
72
+ // --- Tab Waiting ---
73
+ async waitForTabUrl(pattern, timeout) {
74
+ return await bgCall({ action: 'waitForTabUrl', pattern, timeout });
75
+ },
76
+ async waitForCondition(conditionFn, timeout = 5000) {
77
+ const start = Date.now();
78
+ while (Date.now() - start < timeout) {
79
+ if (await conditionFn()) return true;
80
+ await new Promise(r => setTimeout(r, 100));
81
+ }
82
+ return false;
83
+ },
84
+
85
+ // --- Execute in Tab ---
86
+ async executeInTab(tabId, code) {
87
+ return await bgCall({ action: 'executeInTab', tabId, code });
88
+ },
89
+
90
+ // --- Screenshots ---
91
+ async captureScreenshot(format) {
92
+ return await bgCall({ action: 'captureScreenshot', format });
93
+ },
94
+
95
+ // --- Window Management ---
96
+ async createWindow(url, options) {
97
+ return await bgCall({ action: 'createWindow', url, ...options });
98
+ },
99
+ async closeWindow(windowId) {
100
+ return await bgCall({ action: 'closeWindow', windowId });
101
+ },
102
+ async getWindows() {
103
+ return await bgCall({ action: 'getWindows' });
104
+ },
105
+ async getWindowById(windowId) {
106
+ return await bgCall({ action: 'getWindowById', windowId });
107
+ },
108
+ async updateWindow(windowId, props) {
109
+ return await bgCall({ action: 'updateWindow', windowId, ...props });
110
+ },
111
+
112
+ // --- Tab Events ---
113
+ async getTabEvents(clear) {
114
+ return await bgCall({ action: 'getTabEvents', clear });
115
+ },
116
+
117
+ // --- Window Events ---
118
+ async getWindowEvents(clear) {
119
+ return await bgCall({ action: 'getWindowEvents', clear });
120
+ },
121
+
122
+ // --- Extension Forwarding ---
123
+ async forwardToExtension(targetExtensionId, payload) {
124
+ return await bgCall({ action: 'forwardToExtension', targetExtensionId, payload });
125
+ }
126
+ };
127
+
128
+ // Update UI with available methods
129
+ document.addEventListener('DOMContentLoaded', async () => {
130
+ const statusText = document.getElementById('status-text');
131
+ const statusDiv = document.getElementById('status');
132
+ const methodsDiv = document.getElementById('methods');
133
+
134
+ try {
135
+ const pong = await window.TestBridge.ping();
136
+ statusText.textContent = `Ready! (${pong})`;
137
+ statusDiv.classList.add('ready');
138
+
139
+ const methods = Object.keys(window.TestBridge);
140
+ methodsDiv.innerHTML = '<ul>' +
141
+ methods.map(m => `<li><code>TestBridge.${m}()</code></li>`).join('') +
142
+ '</ul>';
143
+
144
+ console.log('[BRIDGE API] Ready!');
145
+ } catch (error) {
146
+ statusText.textContent = 'Error: ' + error.message;
147
+ statusDiv.classList.add('error');
148
+ console.error('[BRIDGE API] Error:', error);
149
+ }
150
+ });
@@ -0,0 +1,56 @@
1
+ /**
2
+ * UUID Injector Content Script
3
+ *
4
+ * Injects the test bridge extension UUID into every page so Selenium can find it
5
+ */
6
+
7
+ (function() {
8
+ // Get our own extension UUID
9
+ const extensionURL = browser.runtime.getURL('test-api.html');
10
+ const uuidMatch = extensionURL.match(/moz-extension:\/\/([^\/]+)/);
11
+
12
+ if (!uuidMatch) {
13
+ console.error('[BRIDGE] Failed to extract UUID from:', extensionURL);
14
+ return;
15
+ }
16
+
17
+ const uuid = uuidMatch[1];
18
+
19
+ // CRITICAL: Content scripts run in an isolated world
20
+ // We need to inject into the page's actual context, not the content script context
21
+ // We do this by injecting a <script> tag that runs in the page context
22
+
23
+ function injectIntoPageContext() {
24
+ const script = document.createElement('script');
25
+ script.textContent = `
26
+ window.__TEST_BRIDGE_UUID = ${JSON.stringify(uuid)};
27
+ window.__TEST_BRIDGE_URL = ${JSON.stringify(extensionURL)};
28
+ console.log('[BRIDGE] UUID injected into page context:', ${JSON.stringify(uuid)});
29
+ `;
30
+
31
+ // Inject at the very beginning of document
32
+ if (document.documentElement) {
33
+ document.documentElement.appendChild(script);
34
+ script.remove(); // Clean up after injection
35
+ } else {
36
+ // If documentElement doesn't exist yet, wait
37
+ setTimeout(injectIntoPageContext, 10);
38
+ }
39
+ }
40
+
41
+ // Also create a meta tag marker
42
+ function injectMarker() {
43
+ if (document.head) {
44
+ const marker = document.createElement('meta');
45
+ marker.name = 'test-bridge-uuid';
46
+ marker.content = uuid;
47
+ document.head.appendChild(marker);
48
+ } else {
49
+ setTimeout(injectMarker, 10);
50
+ }
51
+ }
52
+
53
+ // Inject immediately
54
+ injectIntoPageContext();
55
+ injectMarker();
56
+ })();
package/index.js ADDED
@@ -0,0 +1,19 @@
1
+ const { TestBridge } = require('./lib/test-bridge');
2
+ const { sleep, generateTestUrl, waitForCondition, TabUtils, Assert, TestResults, launchBrowser, cleanupBrowser, extensionDir } = require('./lib/test-helpers');
3
+ const { createTestServer } = require('./lib/test-http-server');
4
+ const { Command } = require('selenium-webdriver/lib/command');
5
+
6
+ module.exports = {
7
+ TestBridge,
8
+ sleep,
9
+ generateTestUrl,
10
+ waitForCondition,
11
+ TabUtils,
12
+ Assert,
13
+ TestResults,
14
+ createTestServer,
15
+ launchBrowser,
16
+ cleanupBrowser,
17
+ extensionDir,
18
+ Command
19
+ };