Jenkins Integration for Playwright Tests
Your Playwright suite passes flawlessly on your laptop, then someone merges a regression on a Friday afternoon and nobody notices until production breaks. The fix is automation: a continuous integration server that runs your tests on every push. In this guide you will learn how to set up a complete Playwright Jenkins integration step by step, run tests headlessly in a reproducible container, publish the HTML report and JUnit results, shard for speed, and avoid the flaky-pipeline traps that waste engineering hours.
🎭 Want to master this with real projects? Join the Playwright Automation Mastery course at The Testing Academy.
Contents
Why run Playwright on Jenkins at all?
Jenkins remains one of the most widely deployed CI servers in enterprise QA teams because it is self-hosted, plugin-rich, and integrates with virtually any source control, artifact store, or notification system. A solid Playwright Jenkins integration gives you a single source of truth: every pull request and every nightly build runs the same browsers, the same Node version, and the same config. That consistency is exactly what kills the “works on my machine” class of bugs.
- Tests execute on a clean, predictable agent instead of a developer’s machine.
- Browser binaries and OS dependencies are pinned via the official Playwright Docker image.
- HTML reports, traces, videos, and screenshots become downloadable build artifacts.
- JUnit XML feeds Jenkins’ native test trend graphs and failure history.
- Failures can block a merge or fire a Slack alert before regressions reach main.
Prerequisites and project setup
You need a Jenkins controller (2.4xx LTS or newer) with at least one agent that can run Docker, plus a Playwright + TypeScript project committed to Git. If you are starting fresh, scaffold one with the official initializer, which generates a sensible playwright.config.ts and an example spec.
// Scaffold a new project (run locally, then commit to Git)
// npm init playwright@latest
// package.json — the scripts Jenkins will call
{
"name": "pw-jenkins-demo",
"scripts": {
"test": "playwright test",
"test:ci": "playwright test --reporter=line,html,junit",
"report": "playwright show-report"
},
"devDependencies": {
"@playwright/test": "^1.49.0",
"typescript": "^5.6.0"
}
}
The config below is tuned for CI. The process.env.CI flag enables retries and forbids accidentally committed test.only calls. We emit three reporters at once so humans get an HTML report and Jenkins gets machine-readable JUnit XML.
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
// Fail the build if test.only is left in the source on CI.
forbidOnly: !!process.env.CI,
// Retry flaky tests twice on CI, never locally.
retries: process.env.CI ? 2 : 0,
// One worker per shard avoids oversubscribing CI CPUs; tune to your agent.
workers: process.env.CI ? 2 : undefined,
reporter: [
['line'],
['html', { open: 'never', outputFolder: 'playwright-report' }],
['junit', { outputFile: 'results/junit.xml' }],
],
use: {
baseURL: process.env.BASE_URL ?? 'https://playwright.dev',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});
Here is a tiny spec to prove the pipeline end to end. It uses real Playwright APIs only — web-first assertions that auto-wait, so you do not need manual sleeps.
// tests/home.spec.ts
import { test, expect } from '@playwright/test';
test('homepage has the correct title and CTA', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/Playwright/);
const getStarted = page.getByRole('link', { name: 'Get started' });
await expect(getStarted).toBeVisible();
await getStarted.click();
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});
Choosing how Jenkins runs the browsers
There are three common execution strategies. The Docker image is almost always the right default because Microsoft ships it with the exact browser binaries and system libraries each Playwright version needs, so you never debug a missing libnss3 on a bare agent again.
| Strategy | How it runs | Best for | Watch out for |
|---|---|---|---|
| Official Docker image | mcr.microsoft.com/playwright:v1.49.0-jammy as the agent | Most teams — reproducible, zero dependency drift | Image tag must match your Playwright version exactly |
| Node tool + install deps | NodeJS plugin, then npx playwright install --with-deps | Agents where Docker is unavailable | Slower; needs sudo/apt for OS libraries |
| Kubernetes pod agent | Jenkins spins a pod from the Playwright image per build | Large suites needing elastic, parallel agents | Requires the Kubernetes plugin and cluster access |
One rule beats every other tip here: keep the Docker image tag and the @playwright/test version in lockstep. If package.json says 1.49.0, the image must be v1.49.0-jammy. A mismatch produces the dreaded “Executable doesn’t exist” error at runtime.
The declarative Jenkinsfile
Commit this Jenkinsfile to the repo root. A declarative pipeline that runs inside the Playwright container is the cleanest approach: Jenkins pulls the image, mounts your workspace, and runs every stage with browsers already installed. The --ipc=host arg prevents Chromium from crashing on the default 64 MB shared-memory limit.
// Jenkinsfile (Groovy declarative pipeline)
pipeline {
agent {
docker {
image 'mcr.microsoft.com/playwright:v1.49.0-jammy'
// --ipc=host fixes Chromium crashes from low /dev/shm size.
args '--ipc=host'
}
}
options {
timeout(time: 30, unit: 'MINUTES')
disableConcurrentBuilds()
ansiColor('xterm')
}
environment {
CI = 'true'
BASE_URL = 'https://playwright.dev'
}
stages {
stage('Install') {
steps {
// npm ci is deterministic and faster than npm install in CI.
sh 'npm ci'
}
}
stage('Test') {
steps {
sh 'npm run test:ci'
}
}
}
post {
always {
// Feed Jenkins' native test trend with JUnit results.
junit testResults: 'results/junit.xml', allowEmptyResults: true
// Keep the HTML report, traces, videos and screenshots as artifacts.
archiveArtifacts artifacts: 'playwright-report/**', allowEmptyArchive: true
archiveArtifacts artifacts: 'test-results/**', allowEmptyArchive: true
}
}
}
Point a Multibranch Pipeline or a plain Pipeline job at your repository, and Jenkins discovers the Jenkinsfile automatically. On the first run it pulls the image (a minute or two), and subsequent builds reuse the cached layers.
Publishing the HTML report so it is actually viewable
Archiving playwright-report stores the files, but Jenkins’ Content-Security-Policy blocks the report’s inline scripts by default, so the archived HTML looks broken. The clean fix is the HTML Publisher plugin, which serves the report from a relaxed sandbox and adds a sidebar link to every build.
// Add this to the post { always { ... } } block of the Jenkinsfile.
// Requires the "HTML Publisher" plugin to be installed.
publishHTML(target: [
reportName: 'Playwright HTML Report',
reportDir: 'playwright-report',
reportFiles: 'index.html',
keepAll: true, // keep a report for every build, not just latest
alwaysLinkToLastBuild: true,
allowMissing: false
])
Sharding for speed across parallel stages
Once your suite grows past a few hundred tests, run it in parallel shards. Playwright’s built-in --shard flag splits tests deterministically; Jenkins runs each shard as a parallel branch, then a final stage merges the blob reports into one combined HTML report using playwright merge-reports.
// Jenkinsfile — parallel sharding pattern
stage('Test (sharded)') {
parallel {
stage('Shard 1') {
steps {
sh 'npx playwright test --shard=1/3 --reporter=blob'
// Name the blob so the merge stage can find all of them.
sh 'mv blob-report/report.zip blob-report/report-1.zip'
}
post { always { stash name: 'blob-1', includes: 'blob-report/report-1.zip' } }
}
stage('Shard 2') {
steps {
sh 'npx playwright test --shard=2/3 --reporter=blob'
sh 'mv blob-report/report.zip blob-report/report-2.zip'
}
post { always { stash name: 'blob-2', includes: 'blob-report/report-2.zip' } }
}
stage('Shard 3') {
steps {
sh 'npx playwright test --shard=3/3 --reporter=blob'
sh 'mv blob-report/report.zip blob-report/report-3.zip'
}
post { always { stash name: 'blob-3', includes: 'blob-report/report-3.zip' } }
}
}
}
stage('Merge reports') {
steps {
unstash 'blob-1'; unstash 'blob-2'; unstash 'blob-3'
// Combine all shard blobs into one HTML report.
sh 'npx playwright merge-reports --reporter=html ./blob-report'
}
}
Three shards roughly cut wall-clock time to a third, minus a small merge overhead. Scale the shard count to the number of CPUs your agents provide, and keep workers modest inside each shard so you do not oversubscribe.
Handling secrets, environments, and authentication
Never hardcode credentials in a spec. Store them as Jenkins credentials and inject them with the withCredentials block, then read them via process.env in a global setup that signs in once and saves storageState for reuse — a real Playwright pattern that avoids logging in on every test.
// global-setup.ts — log in once, persist the session for all tests
import { chromium, type FullConfig } from '@playwright/test';
async function globalSetup(config: FullConfig) {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(`${process.env.BASE_URL}/login`);
await page.getByLabel('Email').fill(process.env.APP_USER!);
await page.getByLabel('Password').fill(process.env.APP_PASS!);
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('**/dashboard');
// Persist cookies + localStorage so specs start authenticated.
await page.context().storageState({ path: 'storage/state.json' });
await browser.close();
}
export default globalSetup;
Wire it into the config with globalSetup: './global-setup.ts' and add storageState: 'storage/state.json' under use. On the Jenkins side, the secrets stay out of the logs:
// Jenkinsfile — inject credentials into the Test stage only
stage('Test') {
steps {
withCredentials([
usernamePassword(
credentialsId: 'app-login',
usernameVariable: 'APP_USER',
passwordVariable: 'APP_PASS'
)
]) {
sh 'npm run test:ci'
}
}
}
🚀 Level Up Your Playwright
From locators to CI pipelines — build a production-grade Playwright + TypeScript framework step by step.
Taming flaky pipelines
Most “Jenkins flakiness” is really test flakiness exposed by a slower, headless environment. Lean on Playwright’s real tooling rather than blanket retries.
- Use web-first assertions like
expect(locator).toBeVisible()that auto-wait; never add fixedpage.waitForTimeoutsleeps. - Set
trace: 'on-first-retry'so you can open the exact failure in the Trace Viewer from the archived artifacts. - Block third-party noise with
page.routeto abort analytics and ad requests that slow or destabilize CI. - Dismiss recurring cookie or promo overlays once with
page.addLocatorHandlerinstead of guarding every test. - Pin time-dependent UI with
page.clockso “expired” tokens or relative timestamps behave identically on every run.
// fixtures or test setup — real Playwright stability APIs
import { test } from '@playwright/test';
test.beforeEach(async ({ page }) => {
// 1. Kill flaky third-party requests in CI.
await page.route(/google-analytics\.com|doubleclick\.net/, (route) => route.abort());
// 2. Auto-dismiss a cookie banner whenever it appears, no per-test code.
await page.addLocatorHandler(
page.getByRole('button', { name: 'Accept cookies' }),
async (button) => { await button.click(); }
);
// 3. Freeze time so relative dates are deterministic on every build.
await page.clock.setFixedTime(new Date('2026-06-27T10:00:00Z'));
});
Triggering builds automatically
A pipeline only earns its keep when it runs without a human clicking “Build now.” Configure triggers in the job or directly in the Jenkinsfile so tests run on every push and on a nightly schedule for slower full-suite runs.
- On push: add a GitHub or GitLab webhook to your repo pointing at
JENKINS_URL/github-webhook/so merges to feature branches trigger a Multibranch build. - Nightly: add
triggers { cron('H 2 * * *') }to run the cross-browser full suite at ~2 AM when agents are idle. - Pull-request gating: with the GitHub Branch Source plugin, Jenkins reports the build status back to the PR so a red suite blocks the merge.
Conclusion
A reliable Playwright Jenkins integration comes down to a handful of disciplined choices: run inside the version-matched official Docker image, emit JUnit plus an HTML report, publish artifacts on every build, shard for speed, keep secrets in Jenkins credentials, and fix flakiness with real Playwright APIs instead of brute-force retries. Commit the Jenkinsfile alongside your tests, point a Multibranch job at the repo, and you have a green-or-red signal on every push that catches regressions long before they reach production. Start with the single-stage pipeline above, get it green, then layer in sharding and report publishing as your suite grows.
FAQ
Do I need to install browsers manually on the Jenkins agent?
No, not if you run inside the official Playwright Docker image (for example mcr.microsoft.com/playwright:v1.49.0-jammy), which already ships the matching browser binaries and OS dependencies. If you cannot use Docker, install Node via the NodeJS plugin and run npx playwright install --with-deps in an early stage, but make sure the agent allows the package installs that --with-deps performs.
How do I view the Playwright HTML report inside Jenkins?
Install the HTML Publisher plugin and call publishHTML in your post block, pointing reportDir at playwright-report and reportFiles at index.html. Plain archiveArtifacts stores the files, but Jenkins’ Content-Security-Policy strips the report’s scripts, so the page renders blank — HTML Publisher serves it correctly and adds a per-build link.
Why do my tests pass locally but fail or flake on Jenkins?
The CI environment is headless, slower, and cleaner, which exposes timing assumptions. Replace fixed waitForTimeout calls with auto-waiting web-first assertions, enable trace: 'on-first-retry' to debug the exact failure in the Trace Viewer, add --ipc=host to stop Chromium shared-memory crashes, and stabilize external factors with page.route, page.clock, and page.addLocatorHandler.
🎓 Master Playwright End to End
Join hundreds of SDETs building real automation frameworks. Lifetime access, hands-on projects, and a job-ready portfolio.
