web-ext
web-ext copied to clipboard
web-ext debugger not accepting connection from Puppeteer or Playwright
Is this a feature request or a bug?
Bug
Problem
Ultimately what I want is to programatically test a WebExtension i'm writing with something like jest
. Since sideloading doesn't work since FF 73 (https://blog.mozilla.org/addons/2019/10/31/firefox-to-discontinue-sideloaded-extensions/). I can't see a way of automatically installing an extension in my dev environment. web-ext
seems to be the only option. Hence I need to connect puppeteer
(or playwright
) to the FF that web-ext
sping up, so finally I could use it with jest to run my automated tests.
I'm trying to use puppeteer to connect to web-ext's Firefox as suggested in https://github.com/puppeteer/puppeteer/issues/5532#issuecomment-605710420 . Didn't work.
Steps to reproduce
Tell us about your environment:
node --version && npm --version && web-ext --version
v14.4.0
6.14.5
4.2.0
- Puppeteer version: 3.3.0 (puppeteer-core)
- Firefox version: 76.0.1
- Platform / OS version: macos mojave / 10.14.6
- URLs (if applicable):
- Node.js version: 13.5.0 and 14.4.0
What steps will reproduce the problem?
Save the below to a file an run node my-file.js
:
import child_process from 'child_process';
import webExt from 'web-ext';
import pptr from 'puppeteer-core';
(async () => {
webExt.default.util.logger.consoleStream.makeVerbose();
const runningInfo = await webExt.default.cmd
.run(
{
sourceDir: `${process.cwd()}/src`,
},
{
shouldExitProgram: false,
}
)
.then((runner) => runner.extensionRunners[0].runningInfo);
child_process.execSync('sleep 5');
const browserURL = `http://127.0.0.1:${runningInfo.debuggerPort}`;
const browser = await pptr.connect({
browserURL,
});
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.org');
})();
What is the expected result?
- Firefox would open (web-ext)
- Puppeteer would connect to it
- Puppeteer would navigate to example.org
What happens instead?
- Firefox opens (web-ext)
- Puppeteer error out (see below)
Error:
(node:77010) UnhandledPromiseRejectionWarning: Error: Failed to fetch browser webSocket url from http://127.0.0.1:58779/json/version: Parse Error: Expected HTTP/
at Socket.socketOnData (_http_client.js:507:22)
at Socket.emit (events.js:315:20)
at addChunk (_stream_readable.js:302:12)
at readableAddChunk (_stream_readable.js:278:9)
at Socket.Readable.push (_stream_readable.js:217:10)
at TCP.onStreamRead (internal/stream_base_commons.js:186:23)
What else I tried:
-
ws://127.0.0.1:${runningInfo.debuggerPort}
instead ofhttp://127.0.0.1:${runningInfo.debuggerPort}
-
browserWSEndpoint
as a parameter toconnect()
instead ofbrowserURL
- Specifying a firefox nightly binary
Full log for the above script:
node test.js
[cmd/run.js][info] Running web extension from /Users/user/workspace/test-project/src
[util/manifest.js][debug] Validating manifest at /Users/user/workspace/test-project/src/manifest.json
(node:77010) Warning: Accessing non-existent property 'cat' of module exports inside circular dependency
(Use `node --trace-warnings ...` to show where the warning was created)
(node:77010) Warning: Accessing non-existent property 'cd' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'chmod' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'cp' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'dirs' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'pushd' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'popd' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'echo' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'tempdir' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'pwd' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'exec' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'ls' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'find' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'grep' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'head' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'ln' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'mkdir' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'rm' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'mv' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'sed' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'set' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'sort' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'tail' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'test' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'to' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'toEnd' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'touch' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'uniq' of module exports inside circular dependency
(node:77010) Warning: Accessing non-existent property 'which' of module exports inside circular dependency
[extension-runners/firefox-desktop.js][debug] Creating new Firefox profile
[firefox/index.js][debug] Running Firefox with profile at /var/folders/fm/51pbdqt51bgf480pk0r1w4sdthm6sd/T/012a10fa-052d-4e8a-81c0-fac715ea753e
[firefox/index.js][debug] Executing Firefox binary: /Applications/Firefox.app/Contents/MacOS/firefox-bin
[firefox/index.js][debug] Firefox args: -start-debugger-server 58779 -foreground -no-remote -profile /var/folders/fm/51pbdqt51bgf480pk0r1w4sdthm6sd/T/012a10fa-052d-4e8a-81c0-fac715ea753e
[firefox/index.js][info] Use --verbose or open Tools > Web Developer > Browser Console to see logging
[firefox/remote.js][debug] Connecting to the remote Firefox debugger
[firefox/remote.js][debug] Connecting to Firefox on port 58779
[firefox/remote.js][debug] Retrying Firefox (0); connection error: Error: connect ECONNREFUSED 127.0.0.1:58779
[firefox/remote.js][debug] Connecting to Firefox on port 58779
[firefox/remote.js][debug] Retrying Firefox (1); connection error: Error: connect ECONNREFUSED 127.0.0.1:58779
[firefox/remote.js][debug] Connecting to Firefox on port 58779
[firefox/remote.js][debug] Retrying Firefox (2); connection error: Error: connect ECONNREFUSED 127.0.0.1:58779
[firefox/remote.js][debug] Connecting to Firefox on port 58779
[firefox/index.js][debug] Firefox stdout: 1592034808630 [email protected] WARN Loading extension '[email protected]': Reading manifest: Invalid extension permission: networkStatus
[firefox/index.js][debug] Firefox stdout: 1592034808648 [email protected] WARN Loading extension '[email protected]': Reading manifest: Invalid extension permission: mozillaAddons
1592034808648 [email protected] WARN Loading extension '[email protected]': Reading manifest: Invalid extension permission: telemetry
[firefox/index.js][debug] Firefox stdout: 1592034808648 [email protected] WARN Loading extension '[email protected]': Reading manifest: Invalid extension permission: resource://pdf.js/
1592034808648 [email protected] WARN Loading extension '[email protected]': Reading manifest: Invalid extension permission: about:reader*
[firefox/remote.js][debug] Retrying Firefox (3); connection error: Error: connect ECONNREFUSED 127.0.0.1:58779
[firefox/remote.js][debug] Connecting to Firefox on port 58779
[firefox/remote.js][debug] Retrying Firefox (4); connection error: Error: connect ECONNREFUSED 127.0.0.1:58779
[firefox/remote.js][debug] Connecting to Firefox on port 58779
[firefox/index.js][debug] Firefox stdout: Started devtools server on 58779
[firefox/remote.js][debug] Retrying Firefox (5); connection error: Error: connect ECONNREFUSED 127.0.0.1:58779
[firefox/remote.js][debug] Connecting to Firefox on port 58779
[firefox/remote.js][debug] Connected to the remote Firefox debugger on port 58779
[firefox/remote.js][debug] installTemporaryAddon: {"addon":{"id":"b72edf14bd71f7760d2c00a1609c9e956f322efb@temporary-addon","actor":false},"from":"server1.conn0.addonsActor2"}
[firefox/remote.js][info] Installed /Users/user/workspace/test-project/src as a temporary add-on
[cmd/run.js][info] The extension will reload if any source file changes
[util/file-filter.js][debug] Resolved path **/*.xpi with sourceDir /Users/user/workspace/test-project/src to /Users/user/workspace/test-project/src/**/*.xpi
[util/file-filter.js][debug] Resolved path **/*.zip with sourceDir /Users/user/workspace/test-project/src to /Users/user/workspace/test-project/src/**/*.zip
[util/file-filter.js][debug] Resolved path **/.* with sourceDir /Users/user/workspace/test-project/src to /Users/user/workspace/test-project/src/**/.*
[util/file-filter.js][debug] Resolved path **/.*/**/* with sourceDir /Users/user/workspace/test-project/src to /Users/user/workspace/test-project/src/**/.*/**/*
[util/file-filter.js][debug] Resolved path **/node_modules with sourceDir /Users/user/workspace/test-project/src to /Users/user/workspace/test-project/src/**/node_modules
[util/file-filter.js][debug] Resolved path **/node_modules/**/* with sourceDir /Users/user/workspace/test-project/src to /Users/user/workspace/test-project/src/**/node_modules/**/*
[watcher.js][debug] Watching for file changes in /Users/user/workspace/test-project/src
[extension-runners/index.js][info] Press R to reload (and Ctrl-C to quit)
(node:77010) UnhandledPromiseRejectionWarning: Error: Failed to fetch browser webSocket url from http://127.0.0.1:58779/json/version: Parse Error: Expected HTTP/
at Socket.socketOnData (_http_client.js:507:22)
at Socket.emit (events.js:315:20)
at addChunk (_stream_readable.js:302:12)
at readableAddChunk (_stream_readable.js:278:9)
at Socket.Readable.push (_stream_readable.js:217:10)
at TCP.onStreamRead (internal/stream_base_commons.js:186:23)
I have also tested with Playwright
1.1.1, both their version of Firefox and the one that web-ext spins by default, no luck.
The error message is different, but I believe to be the same problem. Relvant part changed on the script to make it work with Playwright:
const browserURL = `ws://127.0.0.1:${runningInfo.debuggerPort}`;
// Also tried with "http:" instead of "ws:"
const browser = await browserWrapper.firefox.connect({
wsEndpoint: browserURL,
logger: {
isEnabled: () => true,
log: (name, severity, message, args) => console.log(`${name} ${message}`),
},
});
output:
api => firefox.connect started
pw:api => firefox.connect started +0ms
browser <ws connecting> ws://127.0.0.1:50192
pw:browser <ws connecting> ws://127.0.0.1:50192 +0ms
browser <ws connect error> ws://127.0.0.1:50192 Parse Error: Expected HTTP/
pw:browser <ws connect error> ws://127.0.0.1:50192 Parse Error: Expected HTTP/ +11ms
browser <ws disconnected> ws://127.0.0.1:50192
pw:browser <ws disconnected> ws://127.0.0.1:50192 +0ms
api <= firefox.connect failed
pw:api <= firefox.connect failed +13ms
browser <ws disconnecting> ws://127.0.0.1:50192
pw:browser <ws disconnecting> ws://127.0.0.1:50192 +2ms
(node:96872) UnhandledPromiseRejectionWarning: Error: WebSocket error: Parse Error: Expected HTTP/
=================== firefox.connect logs ===================
<ws connecting> ws://127.0.0.1:50192
<ws connect error> ws://127.0.0.1:50192 Parse Error: Expected HTTP/
<ws disconnected> ws://127.0.0.1:50192
============================================================
Note: use DEBUG=pw:api environment variable and rerun to capture Playwright logs.
at WebSocket.<anonymous> (/Users/user/workspace/test-project/node_modules/playwright-firefox/lib/transport.js:119:24)
at WebSocket.onError (/Users/user/workspace/test-project/node_modules/playwright-firefox/node_modules/ws/lib/event-target.js:128:16)
at WebSocket.emit (events.js:327:22)
at ClientRequest.<anonymous> (/Users/user/workspace/test-project/node_modules/playwright-firefox/node_modules/ws/lib/websocket.js:568:15)
at ClientRequest.emit (events.js:315:20)
at Socket.socketOnData (_http_client.js:514:9)
at Socket.emit (events.js:315:20)
at addChunk (_stream_readable.js:302:12)
at readableAddChunk (_stream_readable.js:278:9)
at Socket.Readable.push (_stream_readable.js:217:10)
I took a quick look into this and based on the documentation related to the CDP protocol support in Firefox: https://firefox-source-docs.mozilla.org/remote/Usage.html it seems that:
- there are separate command line options that have to be passed to ask Firefox enable the CDP agent on a certain port (--remote-debugger=:PORTNUM)
- at the moment the CDP protocol is only enabled in Firefox Nightly builds (this may not be needed for Playwright because they actually use a custom Firefox build patched by them)
The debugging port that web-ext is using is not the one the CDP one that puppeteer is supposed to connect to (web-ext is using the Firefox DevTools remote debugging protocol to interact with the Firefox instance, which isn't the same protocol that puppeteer or playwright are using), and so you may need to ask web-ext to pass the additional command line parameters and points the web-ext instance to a Firefox Nightly binary.
I haven't tried with Playwright, but Puppeteer seems to be able to connect successfully using something like the following:
// with cdpPort set to the port where we want the CDP protocol to listen for connections:
await webExt.default.cmd.run(
{
sourceDir: `${process.cwd()}/src`,
// Pass the options to ask Firefox to start a CDP agent on a given port.
args: [`--remote-debugger=:${cdpPort`]
// Point web-ext to a Firefox Nightly build
firefox: "/path/to/firefox-nightly/firefox",
},
...
);
...
const browserURL = `http://127.0.0.1:${cdpPort}`;
const browser = await pptr.connect({ browserURL });
...
Hi @rpl, thanks for looking into this.
That --remote-debugger
is not documented in neither https://developer.mozilla.org/en-US/docs/Mozilla/Command_Line_Options nor /Applications/Firefox.app/Contents/MacOS/firefox-bin --help
which lead me to believe that --remote-debugger
(which I indeed had seen elsewhere in older posts in puppeteer github issues) was deprecated.
The fact that web-ext
passes a -start-debugger-server 58779
made it worse in my head: "surely web-ext folks know better so that must be the new/correct arg instead of --remote-debugger one" 😅
After your message I ran /Applications/Firefox\ Nightly.app/Contents/MacOS/firefox-bin --help
and --remote-debugger
was indeed there.
Will try again and report back soon. Thanks again!
Ok, learnings so far:
1. web-ext doesn't seem to work with ESM modules as the documentation suggest:
- Using the example from the main README in this repo:
import webExt from 'web-ext'; webExt.cmd.run({ [...] });
returnsUnhandledPromiseRejectionWarning: TypeError: Cannot read property 'run' of undefined
- If I change it to
webExt.default.cmd
it works, but feels like a hack (especially given the example on the docs)
I'll break down several repro codes and their issues:
ESM + Puppeteer only
node ./test-esm-puppeteer-only.js
import child_process from 'child_process';
import webExt from 'web-ext';
import getPort from 'get-port';
import pptr from 'puppeteer-core';
let page;
let browser;
(async () => {
const cdpPort = await getPort();
await webExt.default.cmd.run(
{
sourceDir: `${process.cwd()}/src`,
args: [`--remote-debugger=localhost:${cdpPort}`, `--verbose`],
firefox: '/Applications/Firefox Nightly.app/Contents/MacOS/firefox-bin',
},
{ shouldExitProgram: false }
);
// Needed because `webExt.cmd.run` returns before the DevTools agent starts running.
// Alternative would be to wrap the call to pptr.connect() with some custom retry logic
child_process.execSync('sleep 5');
const browserURL = `http://127.0.0.1:${cdpPort}`;
browser = await pptr.connect({
browserURL,
logger: {
isEnabled: () => true,
log: (name, severity, message, args) => {
console.log(`[${severity}] ${name} ${message}. Args: ${args}`);
},
},
});
page = await browser.newPage();
await page.goto('https://mozilla.org');
})();
Result: works. Firefox nightly gets started and i see the page.
ESM + Jest + Puppeter (WITHOUT web-ext)
node --experimental-vm-modules node_modules/jest/bin/jest.js ./test-esm-jest-pup-no-webext.spec.js
import pptr from 'puppeteer-core';
let page, browser;
beforeAll(async () => {
browser = await pptr.launch({
product: 'firefox',
executablePath:
'/Applications/Firefox Nightly.app/Contents/MacOS/firefox-bin',
headless: false,
});
page = await browser.newPage();
});
afterAll(async () => {
await browser.close();
});
describe('loads mozilla', () => {
it('tests for a random element', async () => {
await page.goto('https://mozilla.org');
const text = await page.evaluate(() => {
return document.querySelector('#home');
});
expect(text).toBeTruthy();
});
});
Results: works. Browser opens, loads mozilla.org and test pass.
ESM + Jest + Puppeter (WITH web-ext)
node --experimental-vm-modules node_modules/jest/bin/jest.js ./test-esm-jest-pup-with-webext.spec.js
import child_process from 'child_process';
import webExt from 'web-ext';
import getPort from 'get-port';
import pptr from 'puppeteer-core';
let page, browser;
beforeAll(async () => {
const cdpPort = await getPort();
webExt.default.util.logger.consoleStream.makeVerbose();
await webExt.default.cmd.run(
{
noInput: true,
sourceDir: `${process.cwd()}/src`,
args: [`--remote-debugger=localhost:${cdpPort}`, `--verbose`],
firefox: '/Applications/Firefox Nightly.app/Contents/MacOS/firefox-bin',
},
{ shouldExitProgram: false }
);
child_process.execSync('sleep 15');
const browserURL = `http://127.0.0.1:${cdpPort}`;
browser = await pptr.connect({
browserURL,
logger: {
isEnabled: () => true,
log: (name, severity, message, args) => {
console.log(`[${severity}] ${name} ${message}. Args: ${args}`);
},
},
});
page = await browser.newPage();
});
afterAll(async () => {
await browser.close();
});
describe('loads mozilla', () => {
it('tests for a random element', async () => {
await page.goto('https://mozilla.org');
const text = await page.evaluate(() => {
return document.querySelector('#home');
});
expect(text).toBeTruthy();
});
});
Results: does not work! Logs:
node --experimental-vm-modules node_modules/jest/bin/jest.js ./test-esm-jest-pup-with-webext.spec.js
(node:64634) ExperimentalWarning: VM Modules is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
[cmd/run.js][info] Running web extension from /Users/user/workspace/project/src
[util/manifest.js][debug] Validating manifest at /Users/user/workspace/project/src/manifest.json
[extension-runners/firefox-desktop.js][debug] Creating new Firefox profile
[firefox/index.js][debug] Running Firefox with profile at /var/folders/fm/51pbdqt51bgf480pk0r1w4sdthm6sd/T/c04a339d-a296-4b91-869b-6c2126fa7bd8
[firefox/index.js][debug] Executing Firefox binary: /Applications/Firefox Nightly.app/Contents/MacOS/firefox-bin
[firefox/index.js][debug] Firefox args: -start-debugger-server 60059 -foreground -no-remote -profile /var/folders/fm/51pbdqt51bgf480pk0r1w4sdthm6sd/T/c04a339d-a296-4b91-869b-6c2126fa7bd8 --remote-debugger=localhost:60058 --verbose
[firefox/index.js][info] Use --verbose or open Tools > Web Developer > Browser Console to see logging
[firefox/remote.js][debug] Connecting to the remote Firefox debugger
[firefox/remote.js][debug] Connecting to Firefox on port 60059
[firefox/remote.js][debug] Retrying Firefox (0); connection error: Error: connect ECONNREFUSED 127.0.0.1:60059
[firefox/remote.js][debug] Connecting to Firefox on port 60059
[firefox/remote.js][debug] Retrying Firefox (1); connection error: Error: connect ECONNREFUSED 127.0.0.1:60059
[firefox/remote.js][debug] Connecting to Firefox on port 60059
[firefox/remote.js][debug] Retrying Firefox (2); connection error: Error: connect ECONNREFUSED 127.0.0.1:60059
[firefox/remote.js][debug] Connecting to Firefox on port 60059
[firefox/index.js][debug] Firefox stderr: _RegisterApplication(), FAILED TO establish the default connection to the WindowServer, _CGSDefaultConnection() is NULL.
[firefox/remote.js][debug] Retrying Firefox (3); connection error: Error: connect ECONNREFUSED 127.0.0.1:60059
[firefox/remote.js][debug] Connecting to Firefox on port 60059
[firefox/remote.js][debug] Retrying Firefox (4); connection error: Error: connect ECONNREFUSED 127.0.0.1:60059
[firefox/remote.js][debug] Connecting to Firefox on port 60059
[firefox/remote.js][debug] Retrying Firefox (5); connection error: Error: connect ECONNREFUSED 127.0.0.1:60059
[firefox/remote.js][debug] Connecting to Firefox on port 60059
[firefox/remote.js][debug] Retrying Firefox (6); connection error: Error: connect ECONNREFUSED 127.0.0.1:60059
[firefox/remote.js][debug] Connecting to Firefox on port 60059
[firefox/index.js][debug] Firefox stdout: Started devtools server on 60059
[firefox/remote.js][debug] Retrying Firefox (7); connection error: Error: connect ECONNREFUSED 127.0.0.1:60059
[firefox/remote.js][debug] Connecting to Firefox on port 60059
[firefox/remote.js][debug] Connected to the remote Firefox debugger on port 60059
[firefox/remote.js][debug] installTemporaryAddon: {"addon":{"id":"53e5bcd98a2d83b35e8c2742912680add2fd2bf5@temporary-addon","actor":false},"from":"server1.conn0.addonsActor2"}
[firefox/remote.js][info] Installed /Users/user/workspace/project/src as a temporary add-on
[cmd/run.js][info] The extension will reload if any source file changes
[extension-runners/index.js][debug] Input has been disabled because of noInput==true
[util/file-filter.js][debug] Resolved path **/*.xpi with sourceDir /Users/user/workspace/project/src to /Users/user/workspace/project/src/**/*.xpi
[util/file-filter.js][debug] Resolved path **/*.zip with sourceDir /Users/user/workspace/project/src to /Users/user/workspace/project/src/**/*.zip
[util/file-filter.js][debug] Resolved path **/.* with sourceDir /Users/user/workspace/project/src to /Users/user/workspace/project/src/**/.*
[util/file-filter.js][debug] Resolved path **/.*/**/* with sourceDir /Users/user/workspace/project/src to /Users/user/workspace/project/src/**/.*/**/*
[util/file-filter.js][debug] Resolved path **/node_modules with sourceDir /Users/user/workspace/project/src to /Users/user/workspace/project/src/**/node_modules
[util/file-filter.js][debug] Resolved path **/node_modules/**/* with sourceDir /Users/user/workspace/project/src to /Users/user/workspace/project/src/**/node_modules/**/*
[watcher.js][debug] Watching for file changes in /Users/user/workspace/project/src
FAIL test ./test-esm-jest-pup-with-webext.spec.js
● Test suite failed to run
TypeError: Cannot read property 'close' of undefined
36 |
37 | afterAll(async () => {
> 38 | await browser.close();
| ^
39 | });
40 |
41 | describe('loads mozilla', () => {
at Object.<anonymous> (test-esm-jest-pup-with-webext.spec.js:38:17)
Test Suites: 1 failed, 1 total
Tests: 0 total
Snapshots: 0 total
Time: 19.872 s
Ran all test suites matching /.\/test-esm-jest-pup-with-webext.spec.js/i.
[firefox/index.js][debug] Firefox stderr: DevTools listening on ws://localhost:60058/devtools/browser/8f93509b-b8b7-5e4a-8bae-a6bdab88bd2b
ReferenceError: You are trying to `import` a file after the Jest environment has been torn down.
at readdir (node_modules/readdirp/readdirp.js:49:25)
at FSWatcher.<anonymous> (node_modules/chokidar/lib/fsevents-handler.js:354:7)
console.error
Unhandled error
at process.uncaught (node_modules/jest-jasmine2/build/jasmine/Env.js:248:21)
console.error
TypeError: require(...) is not a function
at readdir (/Users/user/workspace/project/node_modules/readdirp/readdirp.js:49:48)
at FSWatcher.<anonymous> (/Users/user/workspace/project/node_modules/chokidar/lib/fsevents-handler.js:354:7)
at FSReqCallback.oncomplete (fs.js:176:5)
at process.uncaught (node_modules/jest-jasmine2/build/jasmine/Env.js:249:21)
ReferenceError: You are trying to `import` a file after the Jest environment has been torn down.
at WatcherManager.getDirectoryWatcher (node_modules/watchpack/lib/watcherManager.js:14:25)
at WatcherManager.watchDirectory (node_modules/watchpack/lib/watcherManager.js:32:14)
at DirectoryWatcher.createNestedWatcher (node_modules/watchpack/lib/DirectoryWatcher.js:176:51)
at DirectoryWatcher.setDirectory (node_modules/watchpack/lib/DirectoryWatcher.js:140:11)
at DirectoryWatcher.<anonymous> (node_modules/watchpack/lib/DirectoryWatcher.js:315:12)
console.error
Unhandled error
at process.uncaught (node_modules/jest-jasmine2/build/jasmine/Env.js:248:21)
console.error
TypeError: DirectoryWatcher is not a constructor
at WatcherManager.getDirectoryWatcher (/Users/user/workspace/project/node_modules/watchpack/lib/watcherManager.js:18:33)
at WatcherManager.watchDirectory (/Users/user/workspace/project/node_modules/watchpack/lib/watcherManager.js:32:14)
at DirectoryWatcher.createNestedWatcher (/Users/user/workspace/project/node_modules/watchpack/lib/DirectoryWatcher.js:176:51)
at DirectoryWatcher.setDirectory (/Users/user/workspace/project/node_modules/watchpack/lib/DirectoryWatcher.js:140:11)
at DirectoryWatcher.<anonymous> (/Users/user/workspace/project/node_modules/watchpack/lib/DirectoryWatcher.js:315:12)
at callback (/Users/user/workspace/project/node_modules/graceful-fs/polyfills.js:295:20)
at FSReqCallback.oncomplete (fs.js:176:5)
at process.uncaught (node_modules/jest-jasmine2/build/jasmine/Env.js:249:21)
console.error
Unhandled error
at process.uncaught (node_modules/jest-jasmine2/build/jasmine/Env.js:248:21)
console.error
TypeError: DirectoryWatcher is not a constructor
at WatcherManager.getDirectoryWatcher (/Users/user/workspace/project/node_modules/watchpack/lib/watcherManager.js:18:33)
at WatcherManager.watchDirectory (/Users/user/workspace/project/node_modules/watchpack/lib/watcherManager.js:32:14)
at DirectoryWatcher.createNestedWatcher (/Users/user/workspace/project/node_modules/watchpack/lib/DirectoryWatcher.js:176:51)
at DirectoryWatcher.setDirectory (/Users/user/workspace/project/node_modules/watchpack/lib/DirectoryWatcher.js:140:11)
at DirectoryWatcher.<anonymous> (/Users/user/workspace/project/node_modules/watchpack/lib/DirectoryWatcher.js:315:12)
at callback (/Users/user/workspace/project/node_modules/graceful-fs/polyfills.js:295:20)
at FSReqCallback.oncomplete (fs.js:176:5)
at process.uncaught (node_modules/jest-jasmine2/build/jasmine/Env.js:249:21)
console.error
Unhandled error
at process.uncaught (node_modules/jest-jasmine2/build/jasmine/Env.js:248:21)
console.error
TypeError: DirectoryWatcher is not a constructor
at WatcherManager.getDirectoryWatcher (/Users/user/workspace/project/node_modules/watchpack/lib/watcherManager.js:18:33)
at WatcherManager.watchDirectory (/Users/user/workspace/project/node_modules/watchpack/lib/watcherManager.js:32:14)
at DirectoryWatcher.createNestedWatcher (/Users/user/workspace/project/node_modules/watchpack/lib/DirectoryWatcher.js:176:51)
at DirectoryWatcher.setDirectory (/Users/user/workspace/project/node_modules/watchpack/lib/DirectoryWatcher.js:140:11)
at DirectoryWatcher.<anonymous> (/Users/user/workspace/project/node_modules/watchpack/lib/DirectoryWatcher.js:315:12)
at callback (/Users/user/workspace/project/node_modules/graceful-fs/polyfills.js:295:20)
at FSReqCallback.oncomplete (fs.js:176:5)
at process.uncaught (node_modules/jest-jasmine2/build/jasmine/Env.js:249:21)
console.error
Unhandled error
at process.uncaught (node_modules/jest-jasmine2/build/jasmine/Env.js:248:21)
console.error
TypeError: DirectoryWatcher is not a constructor
at WatcherManager.getDirectoryWatcher (/Users/user/workspace/project/node_modules/watchpack/lib/watcherManager.js:18:33)
at WatcherManager.watchDirectory (/Users/user/workspace/project/node_modules/watchpack/lib/watcherManager.js:32:14)
at DirectoryWatcher.createNestedWatcher (/Users/user/workspace/project/node_modules/watchpack/lib/DirectoryWatcher.js:176:51)
at DirectoryWatcher.setDirectory (/Users/user/workspace/project/node_modules/watchpack/lib/DirectoryWatcher.js:140:11)
at DirectoryWatcher.<anonymous> (/Users/user/workspace/project/node_modules/watchpack/lib/DirectoryWatcher.js:315:12)
at callback (/Users/user/workspace/project/node_modules/graceful-fs/polyfills.js:295:20)
at FSReqCallback.oncomplete (fs.js:176:5)
at process.uncaught (node_modules/jest-jasmine2/build/jasmine/Env.js:249:21)
Jest did not exit one second after the test run has completed.
This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with `--detectOpenHandles` to troubleshoot this issue.
Not sure what could be the problem here :(
Notice I put a high sleep number in order to give plenty of time for the remote debugger to start... It looks like something in the way how web-ext spins a browser up conflicts with how jest handles asynchronous calls? Not sure
I create a simple PR to fix the examples in the docs: https://github.com/mozilla/web-ext/pull/1933
I digged a little into this while triaging Playwright bug.
Here's how to launch firefox with webextension and connect Playwright to it:
const path = require('path');
const {firefox} = require('playwright');
const webExt = require('web-ext').default;
(async () => {
// Enable verbose logging and start capturing logs.
webExt.util.logger.consoleStream.makeVerbose();
webExt.util.logger.consoleStream.startCapturing();
// Launch firefox
await webExt.cmd.run({
sourceDir: path.join(__dirname, 'webextension'),
firefox: firefox.executablePath(),
args: [`-juggler=1234`],
}, {
shouldExitProgram: false,
});
// Parse firefox logs and extract juggler endpoint.
const JUGGLER_MESSAGE = `Juggler listening on`;
const message = webExt.util.logger.consoleStream.capturedMessages.find(msg => msg.includes(JUGGLER_MESSAGE));
const wsEndpoint = message.split(JUGGLER_MESSAGE).pop();
// Connect playwright and start driving browser.
const browser = await firefox.connect({ wsEndpoint });
const page = await browser.newPage();
await page.goto('https://mozilla.org');
// .... go on driving ....
})();
Hope it helps! Repository could be found at aslushnikov/demo-playwright-with-firefox-web-extension
Thank you so much, @aslushnikov . I confirm the above works! And here's the ESM version of it:
import path from 'path';
import playwrightWrapper from 'playwright-firefox';
import webExtWrapper from 'web-ext';
const { firefox } = playwrightWrapper;
const webExt = webExtWrapper.default;
(async () => {
// Enable verbose logging and start capturing logs.
webExt.util.logger.consoleStream.makeVerbose();
webExt.util.logger.consoleStream.startCapturing();
// Launch firefox
await webExt.cmd.run(
{
sourceDir: path.join(process.cwd(), 'src'),
firefox: firefox.executablePath(),
args: [`-juggler=1234`],
},
{
shouldExitProgram: false,
}
);
// Parse firefox logs and extract juggler endpoint.
const JUGGLER_MESSAGE = `Juggler listening on`;
const message = webExt.util.logger.consoleStream.capturedMessages.find(
(msg) => msg.includes(JUGGLER_MESSAGE)
);
const wsEndpoint = message.split(JUGGLER_MESSAGE).pop();
// Connect playwright and start driving browser.
const browser = await firefox.connect({ wsEndpoint });
const page = await browser.newPage();
await page.goto('https://mozilla.org');
// .... go on driving ....
})();
I'm still having troubles having it work with Jest 26.0 though, but I know this is out of scope for this repo. But for the interested ones, here is my code and the error I'm getting:
import { jest, afterAll } from '@jest/globals';
import path from 'path';
import playwrightWrapper from 'playwright-firefox';
import webExtWrapper from 'web-ext';
import getPort from 'get-port';
const { firefox } = playwrightWrapper;
const webExt = webExtWrapper.default;
jest.mock('../../../src/feedback_sender.js');
webExt.util.logger.consoleStream.makeVerbose();
webExt.util.logger.consoleStream.startCapturing();
let browser, page, webExtRunner;
beforeAll(async () => {
webExtRunner = await webExt.cmd.run(
{
sourceDir: path.join(process.cwd(), 'src'),
firefox: firefox.executablePath(),
args: [`-juggler=${getPort()}`],
},
{
shouldExitProgram: false,
}
);
// Parse firefox logs and extract juggler endpoint.
const JUGGLER_MESSAGE = `Juggler listening on`;
const message = webExt.util.logger.consoleStream.capturedMessages.find(
(msg) => msg.includes(JUGGLER_MESSAGE)
);
const wsEndpoint = message.split(JUGGLER_MESSAGE).pop();
// Connect playwright and start driving browser.
browser = await firefox.connect({ wsEndpoint });
});
afterAll(async () => {
await browser.close();
await webExtRunner.exit();
});
beforeEach(async () => {
page = await browser.newPage();
});
afterEach(async () => {
jest.resetModules();
await page.close();
});
describe('Browser Action', () => {
it('button exists', async () => {
await page.goto(`https://mozilla.org`);
const submitButton = await page.$('#fxa-learn-primary');
await expect(submitButton).toBeTruthy();
});
});
Error:
TypeError: Cannot read property 'close' of undefined
57 |
58 | afterAll(async () => {
> 59 | await browser.close();
| ^
60 | await webExtRunner.exit();
61 | });
62 |
...
ReferenceError: You are trying to `import` a file after the Jest environment has been torn down.
at readdir (node_modules/readdirp/readdirp.js:49:25)
at FSWatcher.<anonymous> (node_modules/chokidar/lib/fsevents-handler.js:354:7)
ReferenceError: You are trying to `import` a file after the Jest environment has been torn down.
at WatcherManager.getDirectoryWatcher (node_modules/watchpack/lib/watcherManager.js:14:25)
at WatcherManager.watchDirectory (node_modules/watchpack/lib/watcherManager.js:32:14)
at DirectoryWatcher.createNestedWatcher (node_modules/watchpack/lib/DirectoryWatcher.js:176:51)
at DirectoryWatcher.setDirectory (node_modules/watchpack/lib/DirectoryWatcher.js:140:11)
at DirectoryWatcher.<anonymous> (node_modules/watchpack/lib/DirectoryWatcher.js:315:12)
I'm not sure if it's related to the experimental ESM support Jest has or not. If I have time I will convert it to commonjs to see if it that solves the problem or not.
Any suggestion for the latest Web-Ext, Firefox and Puppeteer?
Here's my code
// @ts-check
const path = require('path');
const puppeteer = require('puppeteer-core');
const webExt = require('web-ext');
async function openFirefox() {
const runner = await webExt.cmd.run({
sourceDir: path.join(__dirname, 'extension'),
firefox: {
'darwin': '/Applications/Firefox Nightly.app/Contents/MacOS/firefox-bin',
'win32': `${process.env.PROGRAMFILES}\\Firefox Nightly\\firefox.exe`,
}[process.platform],
}, {
shouldExitProgram: false,
});
const {debuggerPort} = runner.extensionRunners[0].runningInfo;
return await puppeteer.connect({
browserWSEndpoint: `ws://localhost:${debuggerPort}`,
});
}
async function test() {
try {
await openFirefox();
console.log('WORKS');
} catch (err) {
console.error(err);
process.exit(13);
}
}
test();
that fails with
ErrorEvent {
target: WebSocket {
_events: [Object: null prototype] { open: [Function], error: [Function] },
_eventsCount: 2,
_maxListeners: undefined,
readyState: 3,
protocol: '',
_binaryType: 'nodebuffer',
_closeFrameReceived: false,
_closeFrameSent: false,
_closeMessage: '',
_closeTimer: null,
_closeCode: 1006,
_extensions: {},
_receiver: null,
_sender: null,
_socket: null,
_bufferedAmount: 0,
_isServer: false,
_redirects: 0,
url: 'ws://localhost:51029',
_req: null,
[Symbol(kCapture)]: false
},
type: 'error',
message: 'socket hang up',
error: Error: socket hang up
at connResetException (internal/errors.js:610:14)
at Socket.socketOnEnd (_http_client.js:453:23)
at Socket.emit (events.js:327:22)
at endReadableNT (_stream_readable.js:1220:12)
at processTicksAndRejections (internal/process/task_queues.js:84:21) {
code: 'ECONNRESET'
}
}
The suggestion by @aslushnikov breaks on Cannot read property 'split' of undefined
.
Any suggestion for the latest Web-Ext, Firefox and Puppeteer?
After some tinkering I came up with the following code that seems to work flawlessly:
const path = require('path');
const webExt = require('web-ext');
const puppeteer = require('puppeteer');
(async () => {
const browserFetcher = puppeteer.createBrowserFetcher({ product: 'firefox' });
const revisionInfo = await browserFetcher.download('83.0a1');
const cdpPort = 12345;
const extensionRunner = await webExt.cmd.run({
firefox: revisionInfo.executablePath,
sourceDir: path.resolve(__dirname, '../browser-extension'),
args: [ '--remote-debugging-port', cdpPort ]
}, { shouldExitProgram: false });
const browser = await puppeteer.connect({
browserURL: `http://localhost:${cdpPort}`,
product: 'firefox'
});
// ...
})();
Note that I'm passing the port number as an additional array element instead of a single element --remote-debugging-port ${cdpPort}
as this will be ignored by Firefox. Even though that's pretty obvious in hindsight, it took me a while to find that issue 😅
Thank you @ech0-de! So, the actual solution is adding a port as a second item in args
array and also I had to do await new Promise(resolve => setTimeout(resolve, 2000));
between running web-ext
and puppeteer
.
@alexanderby
This has stopped working, right? I can only open a new page, go to a url, bring it to the front but as soon as I try to execute a selector on it, jest freezes.
For everyone interested in running Puppeteer with Firefox extension - this code sample loads the extension, but hangs on loading the extension page. The page loads but Puppeteer fails to detect that fact and waits until timeout. Playwright behave in the same way.
@rpl it would be nice if web-ext
node module would export connect
.
import puppeteer from 'puppeteer';
import { firefox } from 'playwright-firefox';
import getPort from 'get-port';
import { connect } from './node_modules/web-ext/lib/firefox/remote.js';
import fs from 'node:fs';
import path from 'node:path';
const ADDON_UUID = 'd56a5b99-51b6-4e83-ab23-796216679614';
const ADDON_ID = JSON.parse(fs.readFileSync(path.join('webextension-dummy', 'manifest.json'))).browser_specific_settings.gecko.id;
const rppPort = await getPort();
const browser = await puppeteer.launch({
headless: false,
product: 'firefox',
args: [
`--start-debugger-server=${rppPort}`,
],
extraPrefsFirefox: {
'devtools.chrome.enabled': true,
'devtools.debugger.prompt-connection': false,
'devtools.debugger.remote-enabled': true,
'toolkit.telemetry.reportingpolicy.firstRun': false,
'extensions.webextensions.uuids': `{"${ADDON_ID}": "${ADDON_UUID}"}`,
},
executablePath: firefox.executablePath(),
});
const rdp = await connect(rppPort);
await rdp.installTemporaryAddon(path.resolve('webextension-dummy'));
const page = await browser.newPage();
await page.goto(`moz-extension://${ADDON_UUID}/index.html`);
await page.click('button');
await browser.close();
Linked puppeteer issue https://github.com/puppeteer/puppeteer/issues/9548
For everyone interested in running Puppeteer with Firefox extension - this code sample loads the extension, but hangs on loading the extension page. The page loads but Puppeteer fails to detect that fact and waits until timeout. Playwright behave in the same way.
@rpl it would be nice if
web-ext
node module would exportconnect
.
I see, so in your snippet you are starting the Firefox instance through puppeteer helpers and playwright's firefox build and then connecting to that instance through Firefox Remote Debugging Protocol and installing the extension.
@chrmod now that web-ext is ES modules based, it should be just a matter of adding one or two additional exports from the package.json here: https://github.com/mozilla/web-ext/blob/66590798539e7e959651486d5e8bd8bbb81b7c55/package.json#L7-L11
Personally I wouldn't be against adding firefox/index.js and firefox/remote.js to the modules that we provides "exports" shortcuts for.
For everyone interested in running Puppeteer with Firefox extension - this code sample loads the extension, but hangs on loading the extension page. The page loads but Puppeteer fails to detect that fact and waits until timeout. Playwright behave in the same way.
@rpl it would be nice if
web-ext
node module would exportconnect
.import puppeteer from 'puppeteer'; import { firefox } from 'playwright-firefox'; import getPort from 'get-port'; import { connect } from './node_modules/web-ext/lib/firefox/remote.js'; import fs from 'node:fs'; import path from 'node:path'; const ADDON_UUID = 'd56a5b99-51b6-4e83-ab23-796216679614'; const ADDON_ID = JSON.parse(fs.readFileSync(path.join('webextension-dummy', 'manifest.json'))).browser_specific_settings.gecko.id; const rppPort = await getPort(); const browser = await puppeteer.launch({ headless: false, product: 'firefox', args: [ `--start-debugger-server=${rppPort}`, ], extraPrefsFirefox: { 'devtools.chrome.enabled': true, 'devtools.debugger.prompt-connection': false, 'devtools.debugger.remote-enabled': true, 'toolkit.telemetry.reportingpolicy.firstRun': false, 'extensions.webextensions.uuids': `{"${ADDON_ID}": "${ADDON_UUID}"}`, }, executablePath: firefox.executablePath(), }); const rdp = await connect(rppPort); await rdp.installTemporaryAddon(path.resolve('webextension-dummy')); const page = await browser.newPage(); await page.goto(`moz-extension://${ADDON_UUID}/index.html`); await page.click('button'); await browser.close();
Linked puppeteer issue puppeteer/puppeteer#9548
Loading/accessing extension pages works with protocol: 'webDriverBiDi' in puppeteers.launch🙌🏼