Running Typescript Tests with Playwright in Docker
I spent some hours this week on getting Playwright setup to run tests for the website of my upcoming micro VPN service ESPLink.
I needed the tests to execute in a Docker container and while there is a Docker image and some documentation, I was a little disappointed by the lack of any detail.
So lets fill in this gap.
Setup
Go ahead and follow the instructions for installing Playwright, but opt to not install the Playwright browsers. We are going to be using the Docker image to run them.
I assume you already have a Docker Compose file for setting up and running your local test environment. We will amend this file with an entry for the Playwright test runner container.
Add the following snippet to your compose.yml
1 playwright:
2 image: mcr.microsoft.com/playwright:v1.43.0-jammy
3 command: npx playwright test
4 working_dir: /app
5 ipc: host
6 profiles: [tests]
7 environment:
8 APP_PORT: ${YOUR_APP_HTTP_PORT}
9 APP_HOST: ${YOUR_APP_CONTAINER_NAME}
10 ports:
11 - localhost:9323:9323
12 depends_on:
13 - ${YOUR_APP_CONTAINER_NAME}
14 volumes:
15 - type: bind
16 source: ${YOUR_APP_ROOT_DIRECTORY}
17 target: /app
Let's take a close look at this configuraiton in the next section.
Dissecting the compose.yml
This configuration has a couple of arguments that you will either need to
provide to your docker compose
invocation or replace with their corresponding
values in your setup. But we'll get to those in just a second, let's look at
the basics first.
1 image: mcr.microsoft.com/playwright:v1.43.0-jammy
2 command: npx playwright test
3 working_dir: /app
This sets up the container with the correct image from the Microsoft registry and sets the command for actually running the tests within the container. It also sets the working directory to a canonical location.
1 ipc: host
The little Playwright Docker documentation that exists sets the IPC mode of the container explicitly to host, so I adopted this. It doesn't seem required to me, but the documentation mentions that Chrome might run out of memory without it, so I didn't gamble and just left it.
1 profiles: [tests]
Setting a test profile for this container will prevent it from being started
with the regular docker compose up
invocation and only if the tests profile
was explicitly selected. You can learn more about
profiles in the Docker Compose
documentation.
1 environment:
2 APP_PORT: ${YOUR_APP_HTTP_PORT}
3 APP_HOST: ${YOUR_APP_CONTAINER_NAME}
Here we set two environment variables for consumption in our tests. These will allow us to navigate the browsers to the correct URL when running our tests. You could hard-code those in your tests, but that would make changing these later much more cumbersome.
${YOUR_APP_CONTAINER_NAME}
is the name of your application container or HTTP
server hosting the site you want to test, e.g. your node or nginx container and
${YOUR_APP_HTTP_PORT}
is the port your application or HTTP server is listening
on.
1 ports:
2 - localhost:9323:9323
We expose port 9323 from the container to localhost on the host. This is the port that the test runner will publish the HTML test report on.
1 depends_on:
2 - ${YOUR_APP_CONTAINER_NAME}
Set your application container or webserver as a dependency so that it can start
up before your test runner starts. Consider using the service_healthy
condition
if the startup of your application takes some time. You can learn more about
how to use service_healthy and healthcheck
in the Docker Compose documentation.
1 volumes:
2 - type: bind
3 source: ${YOUR_APP_ROOT_DIRECTORY}
4 target: /app
Bind your application root into the container. ${YOUR_APP_ROOT_DIRECTORY}
is
the directory that contains your node_modules
and tests
directories.
You can now run your tests with the following command:
1docker compose --profile tests run -P playwright
The -P
argument will make sure to publish the ports to your host machine so
you can inspect the resulting HTML report.
To be able to actually access the web server from outside the container we need
to instruct playwright to bind to a non-local interface. We do this with a small
change in the playwright.config.ts
file:
1 reporter: [['html', { host: '0.0.0.0' }]],
To actually access the html report web server you will still have to access localhost:9323 on your host machine. Playwright will otput a link to 0.0.0.0:9323, but that URL will not work on most host machines.
This setup should now work and if all you want to do is manually run some tests, this will likely be good enough for you. But there are some limitations:
-
This will reuse and run your tests against your regular local development environment. This might be acceptable for you, but it could very much be a show stopper.
-
There won't be any isolation between test runs either. Again, this might be acceptable, but may also not be.
-
Upon test failure, this will host the test run report on an HTTP server and block. While this is great when manually invocing it, it's not great if you are trying to utilize the test invocation as part of a script (e.g. Git pre-commit or pre-push hooks).
So let's isolate test runs from the development environment and each other and also setup some git hooks to automatically run the tests before pushing.
Isolation
The easiest way to achieve test isolation is to run the tests using a dedicated
project within Docker. Doing this is as simple as passing the --project project-name
flag to the docker compose run
invocation. In addition we will
need to drop the -P flag to avoid exposing any ports to the host that might
conflict with ports from the development environment.
1docker compose --profile tests --project tests run playwright
This will run the tests in it's own Docker Compose "instance" called "tests". If
you want to isolate test runs from each other you could use a semi-random
project-name, e.g. based on /dev/random
output. This might be useful if you
need to run multiple instances of the tests in parallel, let's say on a CI
server or similar.
There's now two new issues though. Having removed the -P
will make it
impossible to access the web server hosting the test results. At the same
time, the playwright invocation will still block to make the web server
available when the tests fail.
We can prevent the latter by changing the default configuration in
playwright.config.ts
to not automatically open the html reporter on failure:
1 reporter: [['html', { host: '0.0.0.0', open: 'never' }]],
This will still generate the html report, but will not automatically spin up the web server anymore.
To still view the the html report, simply run the following command:
1docker --profile tests --project tests run --no-deps -P playwright npx playwright show-report --host 0.0.0.0
This will spin up the playwright web server hosting the html report (npx playwright show-report
), without starting any of the other Docker Compose
dependencies (--no-deps
). The --host 0.0.0.0
argument is required because
Playwright will not read the reporter settings from playwright.config.ts
when invoked like this and will bind to the local loopback interface inside the
container by default.
Given the increasing number of commands with long argument lists it's probably adviseable to make use of the scripts section of your package.json or to add a Makefile to your project. I personally prefer Makefiles, but most people will probably find the package.json approach more convenient.
1 "scripts": {
2 "tests": "docker compose --profile tests --project tests run playwright",
3 "report": "docker compose --profile tests --project tests run --no-deps -P playwright npx playwright show-report --host 0.0.0.0"
4 }
To achieve isolation between test runs we wil need to bring the Docker Compose project down again. It would be amazing if docker compose run had a --rm or --down option to do this automatically once the run completes, but it currently does not, so we'll need to take care of it ourselves.
The tricky part is to run the tests, bring down the prject and then return the test runner's exit code. Otherwise we won't be able to use the test runner effecticvely from scripts like Git hooks.
The easiest way is probably to write a small shell script for this.
1#!/bin/sh
2
3docker compose --profile tests --project tests run playwright
4RESULT=$?
5docker compose --profile tests --project tests down
6exit $RESULT
Don't forget to replace the "tests"
script in your package.json with this
shell script.
Now we can run npm run tests
to run the tests and npm run report
to open the
HTML report. The tests will run in their own Docker Compose project which will
spin down and get removed again after each test run.
Automatically running tests
Setting up a push hook is as simple as creating a script called pre-push
in
.git/hooks
, making sure it's executable and calling npm run tests
from it.
E.g.
1npm run tests || exit 1
npm run tests will return a non-zero exist code when the tests fail, which will
cause the pre-push hook to exit with an exit code of 1. Using || exit 1
is
neccesary here, becase the Git hooks reserve the exit code 2 and require it not
to be returned by the hook itself. Since we do not control the exit code of the
playwright invocation directly, we need to make sure to not return anything
other than 0 or 1 here.
Now you can run git push
and your tests will automatically run and git push
will abort should your tests fail.
Wrapup
And that's it. If you like content like this, please make sure to checkouy my YouTube channel, too. If you are interested in Home Automation, Home Assistant and ESPHome, you might also be interested in the ESPLink Micro VPN service I'm building or ESPHome Web App (Github), a standalone web application for provisioning, configuring and controlling ESPHome based devices.