web-ext icon indicating copy to clipboard operation
web-ext copied to clipboard

web-ext debugger not accepting connection from Puppeteer or Playwright

Open lfilho opened this issue 4 years ago • 14 comments

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?

  1. Firefox would open (web-ext)
  2. Puppeteer would connect to it
  3. Puppeteer would navigate to example.org

What happens instead?

  1. Firefox opens (web-ext)
  2. 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 of http://127.0.0.1:${runningInfo.debuggerPort}
  • browserWSEndpoint as a parameter to connect() instead of browserURL
  • 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)

lfilho avatar Jun 13 '20 08:06 lfilho

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)

lfilho avatar Jun 13 '20 08:06 lfilho

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 });
...

rpl avatar Jun 16 '20 13:06 rpl

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!

lfilho avatar Jun 16 '20 20:06 lfilho

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({ [...] }); returns UnhandledPromiseRejectionWarning: 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

lfilho avatar Jun 16 '20 22:06 lfilho

I create a simple PR to fix the examples in the docs: https://github.com/mozilla/web-ext/pull/1933

lfilho avatar Jun 17 '20 01:06 lfilho

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

aslushnikov avatar Jun 23 '20 00:06 aslushnikov

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.

lfilho avatar Jun 27 '20 20:06 lfilho

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.

alexanderby avatar Oct 09 '20 13:10 alexanderby

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 😅

ech0-de avatar Oct 12 '20 08:10 ech0-de

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 avatar Oct 12 '20 12:10 alexanderby

@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.

s-h-a-d-o-w avatar Apr 14 '22 09:04 s-h-a-d-o-w

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

chrmod avatar Jan 19 '23 16:01 chrmod

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.

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.

rpl avatar Jan 20 '23 11:01 rpl

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 puppeteer/puppeteer#9548

Loading/accessing extension pages works with protocol: 'webDriverBiDi' in puppeteers.launch🙌🏼

JVariance avatar Feb 02 '24 18:02 JVariance