I recently
tweeted a demo
of one of our e2e tests, and some people were curious to know how it worked. So
here we are!
For simplicity’s sake, I will show you how to test a basic realtime todo app,
one of our examples.
Before we jump into the code, I want to explain why we chose
Puppeteer and
Jest instead of other popular e2e test solutions. If you
only want to see how it works, you can skip the following sections or directly
look at the
code on Github.
Usually, when I have to write e2e web tests, my go-to tool is
Cypress. If you’re not familiar with Cypress, it’s a
complete solution to write/record/debug/run e2e tests with a slick
API/interface. It’s a great open-source tool overall.
However, Cypress has a significant limitation that I can’t ignore in the context
of Liveblocks. Cypress made the
permanent trade-off
of not supporting multiple tabs/browsers. This trade-off is understandable and
fair! Supporting multiple tabs/browsers could negatively impact API
surface/developer experience for the common e2e tests.
Because we’re building APIs for collaboration, 99% of the end-to-end tests we do
involve multiple browsers. Mocking is not an option because we want to go
through all our infrastructure.
I didn’t know it existed before I started writing e2e tests at Liveblocks 🙃 I
never tried it, so I don’t have any opinion.
Playwright supports a multi-browsers environment, so
it would be a high contender for refactoring if one day Puppeteer & Jest are not
enough!
Puppeteer is a node package that lets you control Chrome (or Firefox). It’s not
made to write end-to-end tests, so you have to use it with the test framework of
your choice. I decided to go with Jest because we use it for unit/integration
tests.
The testing process is pretty simple. Every step has its own file that we
reference in the jest.config.js.
setup.js - Start a few browsers before launching any tests.
puppeteer_environment.js - Create a jest testing environment that will
expose the browsers to a test suite. This is where we use
jest-environment-node.
test/todo-list.test.js - Navigate to the URL you want to test and validate
that everything works as expected.
teardown.js - Close the browsers once all the tests are over.
In setup.js, we’re starting multiple browsers before executing any test suite.
To keep it simple, the code below launches two instances of Chrome side to side.
It should be relatively easy to update the code to change the number of browsers
and their positions.
Puppeteer controls Chrome and Firefox via a websocket. Once the browsers are
ready, we write these websocket endpoints in a temporary file that will be read
from the test environment.
Then we create a
custom test environment
to connect to the browsers we just started. And we expose the browsers to the
test suite with a global variable.
Now, the actual test. We use Puppeteers
APIs to interact
with the browsers/pages and then we use Jest to do the assertions.
constTIMEOUT=60*1000; constURL="https://liveblocks.io/examples/todo-list?room=e2e-"+Date.now(); describe("liveblocks.io / todo-list example",()=>{test("creating a todo on page A should appear on page B",async()=>{/** * By default, puppeteer.launch opens a window * with a single tab so we use it here. * It's also possible to open a new page and close it * for each test with beforeEach / afterEach. */const pageA =awaitgetFirstTab(browsers[0]);const pageB =awaitgetFirstTab(browsers[1]); for(const page of[pageA, pageB]){// Navigate to the todo list on all browsers.await page.goto(URL);// Close an overlay that we have on all our examplesawait page.click("#close-example-info");} // The input appears once the todo list is loadedconst input =await pageA.waitForSelector("input"); // Types some text and press enter to create a todoawait input.type("Do a blog post about puppeteer & jest\\n",{delay:50,// This slows down the typing}); // Validate text of the first todoexpect(awaitgetTextContent(pageB,"#todo-0")).toBe("Do a blog post about puppeteer & jest"); // waiting for the sake of the demoawaitwait(1000); // cleanupawait pageA.click("#delete-todo-0"); // waiting for the sake of the demoawaitwait(1000);},TIMEOUT);}); asyncfunctiongetTextContent(page, selector){const element =await page.waitForSelector(selector);returnawait element.evaluate((el)=> el.textContent);} asyncfunctiongetFirstTab(browser){const[firstTab]=await browser.pages();return firstTab;} functionwait(ms){returnnewPromise((resolve)=>setTimeout(resolve, ms));}
Finally, we close all the browsers after the tests are over.
Now, you only need to run jest to execute your test. Add the following script
in your package.json.
At this point, you probably have a good idea about how Puppeteer & Jest can work
together to test multi-player apps. Puppeteer is a powerful tool; it’s also
possible to launch
Firefox,
simulate slow
network/CPU,
take screenshots
(for UI testing), and much more.
At Liveblocks, we’re running e2e tests before every deployment/release to ensure
that all our components work well together. We also do
e2e fuzz testing
to find edge cases in various environments.