Miss Piggy - a test runner for Puppeteer
These days I did (again) a bunch of changes in Google tag manager. This time however was more of a refactoring exercise. So, I had to prove that the tags that were placed before the refactoring exist after the refactoring. And what we programmers do in such cases - we write tests.
What is Miss Piggy
Before everything else we should say that Miss Piggy is a Muppet character. Quite successful I would say. I wanted to use my favorite, Kermit, but it was taken so Miss Piggy ๐ท.
miss-piggy is a small test runner for Puppeteer. A Node library that provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Ideal for my use case where I wanted to open a page, do some clicks and expect the generated HTML, dataLayer
, HTTP requests and so on. I know that we have Cypress, WebDriver or Selenium but I needed something lighter. Puppeteer promised quick start with its simple API so I decided to give it a try.
I knew about jest-puppeteer and this was my first candidate since I'm already a heavy Jest user. However, I didn't see much benefits. I could have been using the Jest API but my overall idea was to write custom expectations and logs. So I did it and realized that I could actually extract all those stuff into an independent module. Which immediately made my work so far portable for solving other testing needs.
A single file for the win
Last couple of years my main goal is to write as less code as possible. Less code means less bugs, easier maintenance and extensibility. I tried designing the API around this mindset. The simplest variant was to write a single spec file and pass it to the runner.
That spec file had to consist of two parts - steps containing potentially async operations and assertions against various properties of the browser (like generated HTML, dataLayer
, requests etc.). A couple of hours later I ended up with the following:
// test.spec.js
module.exports = {
description: "Verifying miss-piggy's npm package description",
steps: [
[
"Opening miss-piggy GitHub page",
async (context) => {
await context.page.goto(`https://github.com/krasimir/miss-piggy`, {
waitUntil: "domcontentloaded",
});
},
],
[
"Clicking on the package.json file",
async (context) => {
await context.clickByText("package.json");
},
],
],
expectations: [
{
where: "html",
value: "Test runner for Puppeteer",
},
],
};
The steps
field is an array of async items which are executed one after each other. The context
object is the toolbox that we have to use to interact with the page. The first step above is directly using the page
object from Puppeteer. However, the clickByText()
in the second step is miss-piggy's API. There are also type()
, screenshot()
, delay()
and a couple of other methods that we will probably need.
The second portion of the test is the expectations
field. It should contain items with where
and value
fields. Which is basically "where to search" and "for what". This example here searches for a string in the HTML of the page. If we run this file with miss-piggy --verbose --spec=./test.spec.js
we will get:
๐ฅ๏ธ Spec files found in /Users/krasimir/Work/Krasimir/misspiggyex:
โ๏ธ /miss-piggy-scenario.spec.js
-----------------------------------------------------------------
Description: Verifying miss-piggy's npm package description
File: miss-piggy-scenario.spec.js
โ๏ธ Opening miss-piggy GitHub page
โณ about:blank
โ https://github.com/krasimir/miss-piggy
โ
html: "Test runner for Puppeteer"
โ๏ธ Clicking on the package.json file
โณ https://github.com/krasimir/miss-piggy
๐ ๏ธ clicking on <a class="js-navigation-open link-gray-dark" title="package.json" href="/krasimir/miss-piggy/blob/main/package.json">package.json</a> (total matches: 3)
โ https://github.com/krasimir/miss-piggy/blob/main/package.json
๐ Test summary:
โ
All 1 expectations for miss-piggy-scenario.spec.js are satisfied.
The /logs/miss-piggy-scenario.spec.js/report.log file is generated.
-----------------------------------------------------------------
โจ Results:
โ
/miss-piggy-scenario.spec.js
To search through the requests that the browser did we may use the following:
{
where: "request",
value: {
method: "GET",
url: "facebook.net/signals/config/xxxx"
}
}
What else
I wanted to write more extensive article on this little project of mine but the truth is that there is no much to say It has minimalistic API and as such it is indeed just steps and expectations.
I will for sure write some more test with Miss Piggy. If you like the direction, jump to the official repo, give it a star or maybe even try it on your own.