Building a Docker container for Puppeteer testing

I hate recipes online that wax philosophically for years, take me to the code

I've started following some coder channels on Twitch and having a great time doing it. I really enjoy the Twitch channels that have interactive hosts, and found LadyOfCode to be interesting and entertaining. Enough that I decided to join her Discord and see what it was like there.

While there a fellow member posted an issue they were having getting Puppeteer running - Puppeteer being a NodeJS browser automation/ testing framework. They posted their JS and I could read it - so even though I didn't know Puppeteer I tried offering some advise. The advise wasn't working so I decided to build a Docker container for Puppeteer and see if I could help further. The Google searches on getting Puppeteer were miss and miss (ie. they didn't work) so I screwed up my courage and my minimal Docker skills to build my own container.

Looks like I had been paying attention to the server team at work as I got it working a lot faster than I expected. Here's what I made:

The Code

Dockerfile

 1FROM node:latest
 2ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
 3WORKDIR /puppeteer
 4RUN apt-get update \ 
 5    && apt-get install -y \
 6    fonts-liberation \
 7    gconf-service \
 8    libasound2 \
 9    libatk1.0-0 \
10    libcairo2 \
11    libcups2 \
12    libfontconfig1 \
13    libgbm-dev \
14    libgdk-pixbuf-2.0-0 \
15    libgtk-3-0 \
16    libicu-dev \
17    libjpeg-dev \
18    libnspr4 \
19    libnss3 \
20    libpango-1.0-0 \
21    libpangocairo-1.0-0 \
22    libpng-dev \
23    libx11-6 \
24    libx11-xcb1 \
25    libxcb1 \
26    libxcomposite1 \
27    libxcursor1 \
28    libxdamage1 \
29    libxext6 \
30    libxfixes3 \
31    libxi6 \
32    libxrandr2 \
33    libxrender1 \
34    libxss1 \
35    libxtst6 \
36    xdg-utils \
37    chromium
38RUN npm install puppeteer
39RUN npm ci    
40RUN ln -s /usr/bin/chromium /usr/bin/chromium-browser

That's the collected "this works" from combining a bunch of found Dockerfile bits. Note that one of the machines I used this on was a new M1 Mac so I had to not install the Puppeteer Chromium (using the PUPPETEER_SKIP_CHROMIUM_DOWNLOAD env variable), and instead installed it via apt-get.

tests/thisscript.js

 1const puppeteer = require('puppeteer');
 2var somethingToLookFor = 'JQMIGRATE'
 3var someFunkyUrl = 'https://alistapart.com/'
 4
 5function doTheThing(var1) {
 6    console.log("callback func", var1);
 7}
 8
 9let thisWouldBeANiceFunction = async function (doThisThing) {
10    let vars = {
11        results: "Not installed",
12        found: null
13    }
14    let browser;
15    let myPromise;
16    try {
17        myPromise = new Promise(async (resolve, reject) => {
18            browser = await puppeteer.launch({
19                headless: true,
20                args: ['--use-gl=el', '--no-sandbox'],
21                executablePath: '/usr/bin/chromium-browser'
22            });            
23            let page = await browser.newPage();
24            page.on('console', async (msg) => {
25                if (msg.text().includes(somethingToLookFor)) {
26                    vars.found = msg.text();
27                    vars.results = "Found"
28                    resolve(vars);
29                }
30            })
31            await page.goto(someFunkyUrl);
32        })
33        await myPromise.then(async (resp) => {
34            doThisThing(resp)
35        })
36    } catch (err) {
37        console.log(err);
38    } finally {
39        // await browser.close();
40    }
41}
42
43thisWouldBeANiceFunction(doTheThing)

This JS just tests a site to see if JQMigrate is installed as it puts a message in the console. Lots of async and other Puppeteer parallel calls. It works!

Calling out specific changes I made to make it work - I added in the "--use-gl=el","--no-sandbox" bit to run headless in docker. I also added the executablePath: '/usr/bin/chromium-browser' to tell Puppet where I'd put Chromium to run.

So to run it there's:

1docker build -t puppeteer:latest .

That builds me an image tagged puppeteer:latest that I can call when I need to run puppet, like so:

1docker run -i --rm -v ./tests:/puppeteer/tests -t puppeteer:latest node tests/quicktest.js
  • -i is for an interactive shell
  • --rm is to remove the built image once execution is over, so it doesn't fill up my HD
  • -v ./tests:/puppeteer/tests mounts my local tests directory to /puppeteer/tests in the image, so puppeteer can access it
  • -t puppeteer:latest tells it which image to use by its tag
  • node tests/quicktest.js is the command to run in the container

Interested in recommendations to improve, as always.

Update 2021-12-26

Said friend managed to find a way to do this without needing the callback function! You can see his writeup on the puppeteer issue raised

1
2 let sendBack = async ()=>{return await myPromise}
3 let answer = await sendBack();
4 return answer; 

ps: In other words, using then() after the awaits was wrong.