Why We Needed End-to-End CI Tests
For a long time, our team relied on unit tests and a handful of manual checks to validate frontend changes. It felt “good enough” until reality proved otherwise.
We were regularly delivering new features without any guarantee that the UI wouldn’t break elsewhere.
First of all, unit tests only covered logic, not real user interactions. They didn’t click buttons, fill forms, navigate pages, or handle permission side.
Secondly, manual testing wasn’t good either: slow, repetitive, incomplete, and easily skipped.
And then a critical bug hit production. The backend CI was green and the deployment went out smoothly, but users immediately faced broken UI behavior.
That incident made it clear: our CI pipeline was blind to the frontend.
We needed a reliable way to catch frontend issues before they reached production.
Why We Choose Playwright ?
We needed a way to test the frontend exactly as users experience it, automatically, on every code change.
Our requirements were simple but non-negotiable:
Fully reproducible and automated
Fast and reliable execution, suitable for CI pipelines
Easy to maintain and extend as the app grows
Cross-browser support (Chromium, Firefox )
Provides detailed failure artifacts (screenshots, videos, traces)
Able to simulate real user behavior: clicks, navigation, form inputs, and async interactions.
After evaluating options, we chose Playwright. It runs tests in real browsers (Chromium, Firefox…), screenshots, videos, and traces on failure, and fits naturally into CI workflows.
With this setup, we finally had confidence that every release would behave correctly for our users, before it ever reached production.
Implementing Playwright in Our CI Pipeline
Once we decided on Playwright, the next challenge was integrating it into our CI workflow. The goal: run end-to-end tests automatically on every code change.
First, install Playwright:
npm init playwright@latest
This command generates a ready to use test structure, including:
Sample tests
Test runner configuration (
playwright.config.ts)Browser binaries (Chromium, Firefox … )
Next, verify that the tests run locally:
npx playwright test
This ensures everything works before integrating into CI.
CI Pipeline Integration
Once Playwright was working locally, the final step was integrating it into our CI pipeline. This important because without automated browser tests in CI, bugs would still slip into production. We needed the pipeline to behave like a real user:
Launch the full stack (frontend, backend, database, cache) via Docker
Run Playwright tests against that running environment
Collect screenshots and traces when something breaks
At the top of the workflow, we define when the CI should run Playwright tests:
This means the workflow will run on any pull request made on the project:
name: Playwright Tests
on:
pull_request:
branches:
- "*"
workflow_dispatch:
With this part, we ask a runner that runs on ubuntu-22.04 and configure the trigger workflow condition. We exclude PRs with the draft label.
jobs:
playwright:
name: Playwright test CI
runs-on: ubuntu-22.04
environment: ci
if: ${{!contains(github.event.pull_request.labels.*.name, 'draft')}}
As a monorepo, we need to checkout the main repository.
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_PAT }}
This step guarantees that both the frontend (where Playwright tests run) and the backend (required for the API) are present in the runner.
Installing Dependencies
Next, we install project dependencies. To speed up CI runs, we cache npm, so we can restore it:
- name: Cache npm dependencies
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('backend/package-lock.json') }}
Then we install dependencies for both backend and frontend using npm ci to ensure clean installs:
- name: Install dependencies
run: |
cd backend/
npm ci
cd ../frontend/
npm ci
Caching & Installing Playwright Browsers
Playwright uses real browser binaries (Chromium, Firefox).
These can take time to download, so we cache them:
- name: Cache Playwright Browsers
id: playwright-cache
uses: actions/cache@v3
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('frontend/package-lock.json') }}
If the cache is empty, Playwright will install the browsers:
- name: Install Playwright Browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: |
cd frontend/
npx playwright install --with-deps
Starting the Application With Docker Compose
Before running Playwright tests, we must start the entire stack.
In our case, the frontend depends on:
The backend API
PostgreSQL database
GitHub Actions starts everything via Docker Compose:
- name: Build & run docker Compose
run: |
docker compose up -d database frontend backend
env:
DATABASE_URL: postgresql://postgres:root@database:5432/db?schema=public
CLIENT_HOST: localhost
API_URL: "http://localhost:8080"
CLIENT_PORT: 3000
Running Playwright Tests
Once the application is up and running, we execute the test suite:
- name: Run Playwright Tests
working-directory: frontend/
run: npx playwright test
Playwright will:
Launch a headless browser
Navigate through the app
Perform real user actions
Screenshot failures
Generate traces for debugging
This converts the PR into a full end-to-end test session.
Uploading Artifacts on Failure
When a test fails, Playwright produces:
Full-page screenshots
HTML trace logs
We upload these artifacts so developers can inspect what went wrong:
- name: Upload screenshots
if: always() && failure()
uses: actions/upload-artifact@v4
with:
name: test-screenshots
path: frontend/test-results/
retention-days: 1
For Debugging Purpose
Locally, you can simulate or run the test and see what Playwright is doing.
This one will launch the browser and run the tests:
npx playwright test --headed
The second one will launch the UI mode of Playwright to have more tools and control on the process:
npx playwright test --ui
Top comments (0)