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
FROM node:latest
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
WORKDIR /puppeteer
RUN apt-get update \
&& apt-get install -y \
fonts-liberation \
gconf-service \
libasound2 \
libatk1.0-0 \
libcairo2 \
libcups2 \
libfontconfig1 \
libgbm-dev \
libgdk-pixbuf-2.0-0 \
libgtk-3-0 \
libicu-dev \
libjpeg-dev \
libnspr4 \
libnss3 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libpng-dev \
libx11-6 \
libx11-xcb1 \
libxcb1 \
libxcomposite1 \
libxcursor1 \
libxdamage1 \
libxext6 \
libxfixes3 \
libxi6 \
libxrandr2 \
libxrender1 \
libxss1 \
libxtst6 \
xdg-utils \
chromium
RUN npm install puppeteer
RUN npm ci
RUN 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
const puppeteer = require('puppeteer');
var somethingToLookFor = 'JQMIGRATE'
var someFunkyUrl = 'https://alistapart.com/'
function doTheThing(var1) {
console.log("callback func", var1);
}
let thisWouldBeANiceFunction = async function (doThisThing) {
let vars = {
results: "Not installed",
found: null
}
let browser;
let myPromise;
try {
myPromise = new Promise(async (resolve, reject) => {
browser = await puppeteer.launch({
headless: true,
args: ['--use-gl=el', '--no-sandbox'],
executablePath: '/usr/bin/chromium-browser'
});
let page = await browser.newPage();
page.on('console', async (msg) => {
if (msg.text().includes(somethingToLookFor)) {
vars.found = msg.text();
vars.results = "Found"
resolve(vars);
}
})
await page.goto(someFunkyUrl);
})
await myPromise.then(async (resp) => {
doThisThing(resp)
})
} catch (err) {
console.log(err);
} finally {
// await browser.close();
}
}
thisWouldBeANiceFunction(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:
docker build -t puppeteer:latest .
That builds me an image tagged puppeteer:latest
that I can call when I need to run puppet, like so:
docker 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 tagnode 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
let sendBack = async ()=>{return await myPromise} let answer = await sendBack(); return answer;
ps: In other words, using then() after the awaits was wrong.