loading...

How to use Puppeteer in a Chrome extension

tchan profile image Travis Updated on ・3 min read

At my current job I would sometimes input client details into our CRM platform and would subsequently input these same details while setting them up on another third party platform. As such, there is a lot of copying data around and manual labor.

Automating this process via a Chrome Extension seemed like a good idea because I could also share it with non-technical colleagues that weren't comfortable with running scripts. Also at this company, Chrome is the default browser for all of the machines which run on Windows. With all the stars aligned, I managed to develop a working solution, although a bit hacky but works for my needs. This post was inspired by this thread.

Some prerequisite knowledge

  • basic understanding of Chrome Extension structure
  • basic understanding of Puppeteer

I had no prior experience working with Chrome Extensions, so I highly recommend The Coding Train's Playlist which is an excellent primer to developing them. It gave me everything I needed to get up and running.

With Puppeteer, the docs is quite thorough.

Bringing it all together

The trick is to use the bundled version of puppeteer. This link points to an old commit as this is no longer within the scope of their project. S

  1. Bundle the repository per above documentation
  2. Place it in your chrome extension folder
  3. Reference the newly bundled folder in your popup.html with something like
<script src="./puppeteer/utils/browser/puppeteer-web.js"></script>

This is the hacky part. You'll then need to take advantage of Chrome's remote debugging functionality as puppeteer-web can't start its own instance via puppeteer.launch() and can only use puppeteer.connect() to connect to an already existing chrome instance. Add --remote-debugging-port=9222 to the end of the target field of your chrome.exe short cut. To learn more about this, read here.

Once remote debugging is activated you'll be able to see the webSocketDebuggerUrl property by visiting http://localhost:9222/json/version on your browser. This is the browserWSEndpoint the connect method will invoke.

Example response when visiting http://localhost:9222/json/version

{
   "Browser": "Chrome/79.0.3945.130",
   "Protocol-Version": "1.3",
   "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36",
   "V8-Version": "7.9.317.33",
   "WebKit-Version": "537.36 (@e22de67c28798d98833a7137c0e22876237fc40a)",
   "webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/browser/343998a5-ce82-456f-944c-a214ebb59e83"
}

You will also need to add the port address to the permissions array in the Chrome Extension's manifest.json file otherwise ajax requests won't work and you'll suffer from cors errors.

Example manifest.json file.

{
  "name": "Chrome Extension",
  "version": "0.1",
  "content_scripts": [
    {
      "matches": [
        "https://url-that-you-want-to-scrape/*"
      ],
      "js": ["content.js"]
    }
  ],
  "background": {
    "scripts": ["background.js"],
    "persistent": true
  },
  "permissions": [ "tabs" , "identity","http://localhost:9222/*"],
  "browser_action": {
    "default_title": "Chrome Extension",
    "default_popup": "popup.html"
  }
   "content_security_policy": "script-src 'self' 'unsafe-eval' https://apis.google.com/; object-src 'self'",
   "manifest_version": 2
}

Example popup.html file

<!DOCTYPE html>
<html>
  <head>
    <title>Example popup</title>
    <link rel="stylesheet" type="text/css" href="style.css">
  </head>
  <body>
    <div>
      <button id='puppeteer-button'>Do Puppeteer Things</button>
      <script src="./puppeteer/utils/browser/puppeteer-web.js"></script>
      <script type="module" src="popup.js"></script>
    </div>
  </body>
</html>

Example popup.js file

let browserWSEndpoint = '';
const puppeteer = require("puppeteer");

async function initiatePuppeteer() {
  await fetch("http://localhost:9222/json/version")
    .then(response => response.json())
    .then(function(data) {
        browserWSEndpoint = data.webSocketDebuggerUrl;
      })
    .catch(error => console.log(error));
}

initiatePuppeteer();

// Assign button to puppeteer function

document
  .getElementById("puppeteer-button")
  .addEventListener("click", doPuppeteerThings);

async function doPuppeteerThings() {
  const browser = await puppeteer.connect({
    browserWSEndpoint: browserWSEndpoint
  });
  const page = await browser.newPage();

  // Your puppeteer code goes here

}

Hopefully this is of help to someone. Happy automating.

Edit: Upon reflection, I'm pretty sure this can be accomplished with just basic javascript and dom manipulation via chrome.tabs.create and chrome.tabs.executeScript API and editing .value of input fields and what not. In my use case, it involved logging into multiple platforms and clicking various buttons through various states which may have been messy with plain javascript and I've already committed to puppeteer. Although if you just had a form that needed to be inputted and submitted, that's a different story.

Discussion

markdown guide
 

Hi Travis,

I enjoyed reading your article. In looking at your manifest, see that you also have content_scripts and background but you don't provide any example as how they fit into everything. Would you mind providing a sample repo where we could look at all the moving parts?

 

Hi Matt, thanks for your comment!

I'll try editing the article when I have time, but content_script and background script are the building blocks for chrome extensions. I would recommend watching this video a better overview/primer.

Essentially, the content script allows for manipulation of the DOM and the background script, like the name implies sits in the background, handle state related things, or communicate to other aspects of your chrome extension, commonly the popup.js that is associated with the popup html when you click your chrome extension icon in the browser.

Hope that helps.

 

Hi Travis,

Thanks for your response. I understand about both the content_scripts and background scripts but neither are necessary for running Puppeteer in the browser. I look forward to a working repo.

I was able to get it working but I had to have two instances of Chrome open. One to interact with the extension and the other to open the new page. Not sure why though?

Also, how do you debug the popup.js when the first thing you do in Puppeteer is create a new page for your automation? The very act of launching a new page closes the popup and the ability to debug that popup.js session.

Thanks again for your response and I look forward to your working repo.

Best regards,

Matt

From memory, passing data between content, background and popup scopes was a real pain in the ass when dealing with Chrome extensions.

I believe that's one of the limitations of puppeteer-web in general is that it opens up a new web page as it only has access to the connect api as opposed to launch which can open up a brand new tab in your original instance. It looks like the link I posted initially to the official docs is 404'd :/ but I found my initial info here.

When I was debugging popup.js, I just clicked my chrome extension next to the url bar to open up the popup.html viewport, right click -> inspect and added a breakpoint to my popup.js file. That way when the new browser instance launched via puppeteer, the popup window doesn't close. Let me know if you've already tried that, sorry I couldn't be more help.

Okay, so it seems like everything is working as I expected. I am using Chrome Canary for my testing and it seems to be working but the debugger tools are not behaving perfectly. I can debug though.

Thanks for following up.

 

Hi Travis,

Actually, I am facing some issues doing the same. It would be great if you could send me the final bundled version / any public repo you worked on.

 

Also, this is not headless, right.