
September 10, 2026
12 min read
By Kokil Thapa | Last reviewed: September 2026
End-to-End Testing with Playwright in CI is how you prove checkout, login, and booking flows work on every merge—not just on your laptop. Unit tests miss broken CSS, stale sessions, and misconfigured environment variables. I've seen Laravel apps pass PHPUnit and still fail when a user clicks "Pay with Khalti" in staging. This guide walks through a practical CI integration testing setup you can ship this week.
What is End-to-End Testing with Playwright in CI?
End-to-end (E2E) tests drive a real browser against your running application. Playwright controls Chromium, Firefox, or WebKit through a stable API. In CI, those tests run automatically when someone opens a pull request or pushes to main.
The goal is simple. Catch UI and workflow bugs that PHPUnit, Pest, or Jest cannot see. A form may validate on the server yet fail because a modal overlay blocks the submit button. Playwright clicks the same path a user would.
On production Laravel projects I maintain, E2E sits above feature tests in the testing pyramid. You do not replace fast unit tests. You add a thin layer of high-value smoke and regression specs around money paths: auth, checkout, document upload, and admin CRUD.
Playwright fits PHP stacks well. Your Laravel app serves HTML. Playwright loads pages, fills forms, and asserts visible text. No React requirement. Blade, Livewire, Alpine, and jQuery UIs all work.
How do you set up Playwright for CI pipelines?
Start in your repo root. Install Playwright as a dev dependency and pin browser versions so CI matches local runs.
Install and configure Playwright
npm init -y
npm install -D @playwright/test@latest
npx playwright install --with-deps chromium
npx playwright init
Create playwright.config.ts with CI-aware settings. The official Playwright CI documentation recommends explicit retries, trace capture, and a fixed base URL.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 2 : undefined,
reporter: [['html'], ['list']],
use: {
baseURL: process.env.APP_URL ?? 'http://127.0.0.1:8000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
});
Add npm scripts in package.json so CI calls one entry point.
{
"scripts": {
"test:e2e": "playwright test",
"test:e2e:report": "playwright show-report"
}
}
Boot the Laravel app inside CI
E2E needs a running server. For Laravel 12 or 13, a typical sequence looks like this:
- Copy
.env.exampleto.envand setAPP_KEY. - Run migrations against SQLite or a service container database.
- Seed minimal test data with a dedicated seeder.
- Build frontend assets with Vite 8.x if your pages require compiled JS.
- Start
php artisan servein the background on port 8000. - Wait until the health URL returns 200.
- Run Playwright with
APP_URL=http://127.0.0.1:8000.
On booking portals like Adventure Third Pole Trek, I seed one admin, one customer, and one published tour. Tests stay fast and deterministic. Heavy factories belong in PHPUnit, not every E2E run.
A sample smoke spec in e2e/smoke.spec.ts:
import { test, expect } from '@playwright/test';
test('homepage loads and nav is visible', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('navigation')).toBeVisible();
await expect(page).toHaveTitle(/Adventure|Home/i);
});
test('login form accepts credentials', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('e2e@example.com');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Log in' }).click();
await expect(page).toHaveURL(/dashboard/);
});
Pair this with Laravel Pest tests in CI for server logic. E2E confirms the wiring between backend and browser.
Which CI platform works best with Playwright?
GitHub Actions and GitLab CI both ship solid Playwright support. Pick the platform your team already uses. Playwright publishes official Docker images that include browsers and OS dependencies.
| Criteria | GitHub Actions | GitLab CI |
|---|---|---|
| Official Playwright image | mcr.microsoft.com/playwright:v1.55.0-jammy | Same image in image: key |
| Browser cache | actions/cache on ~/.cache/ms-playwright | GitLab cache paths |
| Parallel shards | matrix strategy | parallel matrix |
| Artifact upload | actions/upload-artifact | artifacts reports |
| Laravel fit | Strong with GitHub Actions for Laravel | Strong with GitLab CI for Laravel |
For a deeper platform comparison, see GitHub Actions vs GitLab CI. The Playwright steps are nearly identical once the app is running.
GitHub Actions example
name: E2E Playwright
on:
pull_request:
push:
branches: [main]
jobs:
e2e:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.55.0-jammy
services:
mysql:
image: mysql:8.4
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: testing
ports: ['3306:3306']
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3]
env:
APP_URL: http://127.0.0.1:8000
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '26'
cache: npm
- uses: php-actions/composer@v6
with:
php_version: '8.3'
php_extensions: mbstring pdo_mysql
- run: cp .env.example .env && php artisan key:generate
- run: composer install --no-interaction --prefer-dist
- run: npm ci
- run: npm run build
- run: php artisan migrate --force --seed --seeder=E2eSeeder
- run: php artisan serve --host=127.0.0.1 --port=8000 &
- run: npx wait-on "$APP_URL"
- run: npx playwright test --shard=${{ matrix.shard }}/3
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report-${{ matrix.shard }}
path: playwright-report/
GitLab CI example
stages: [test]
e2e:
stage: test
image: mcr.microsoft.com/playwright:v1.55.0-jammy
parallel: 3
variables:
APP_URL: http://127.0.0.1:8000
cache:
key: playwright
paths:
- node_modules/
- ~/.cache/ms-playwright/
before_script:
- apt-get update && apt-get install -y php8.3-cli php8.3-mysql
- cp .env.example .env && php artisan key:generate
- composer install --no-interaction
- npm ci && npm run build
- php artisan migrate --force --seed --seeder=E2eSeeder
- php artisan serve --host=127.0.0.1 --port=8000 &
- npx wait-on "$APP_URL"
script:
- npx playwright test --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL
artifacts:
when: on_failure
paths:
- playwright-report/
- test-results/
Cache browsers and node_modules to cut run time. See build caching in CI for path patterns that actually hit on repeat runs.
How do you make Playwright tests stable in CI?
Flaky E2E erodes trust fast. A red pipeline that passes on re-run teaches teams to ignore failures. Treat stability as architecture, not luck.
Use locators that match accessibility roles
Prefer getByRole, getByLabel, and getByTestId over brittle CSS chains. Roles survive Bootstrap layout tweaks better than .btn-primary:nth-child(3).
Never depend on fixed sleeps
Replace page.waitForTimeout(3000) with auto-waiting assertions. Playwright retries until the timeout you set in config. On slow CI runners, increase the default expect timeout to 10–15 seconds for navigation-heavy flows.
Isolate test data
Parallel workers plus one shared database cause collisions. Options that work in practice:
- Run E2E against SQLite in memory with a fresh migrate per job.
- Prefix records with a UUID per test file in the seeder.
- Stub external APIs with contract-tested mocks instead of live payment gateways.
- Reset state in
beforeEachonly when the operation is cheap.
For legal-tech portals such as Court Marriage In Nepal, I never hit real SMS or payment APIs in E2E. Fake gateways return deterministic success payloads. That keeps CI bills low and tests repeatable.
Gate merges with real quality bars
Wire Playwright into branch protection alongside PHPUnit and static analysis. A failing E2E job should block merge the same way a coverage gate would. Document which specs are required versus nightly-only.
Keep the E2E suite lean. Twenty stable tests beat two hundred flaky ones. Expand coverage after build verification gates are green and trusted.
How do you debug failed Playwright runs in CI?
CI failures without artifacts waste hours. Configure Playwright to capture traces, screenshots, and video on failure. Upload them as pipeline artifacts with a seven-day retention minimum.
Read the HTML report locally
Download the artifact folder from the failed job. Unzip it and run:
npx playwright show-report ./playwright-report
The report lists failing specs, stderr, and links to traces. Share the ZIP in Slack so reviewers reproduce the exact step without re-running the full pipeline.
Inspect traces for timing issues
Traces show DOM snapshots, network calls, and console logs per step. Open a trace file with:
npx playwright show-trace test-results/example-spec/trace.zip
Look for 401 responses, CORS blocks, or JavaScript errors in the console panel. Many "button not clickable" failures trace back to a 500 on an XHR call that PHPUnit never touched.
Reproduce with CI env vars locally
Run the same command CI uses:
CI=true APP_URL=http://127.0.0.1:8000 npx playwright test --shard=1/3
Match PHP 8.3, Node.js 26 LTS, and headless mode. Differences in timezone or locale also cause date-picker mismatches. Set TZ=UTC in both local Docker and CI for consistency.
Store secrets in CI variables, never in specs. Follow CI secrets management and scan repos with tools covered in Gitleaks scanning guides. E2E login passwords belong in masked variables, not Git history.
Validate JSON API responses in failing tests with a local JSON formatter when you log response bodies during investigation. Keep production logs out of artifacts unless redacted.
What should you test first with Playwright in CI?
Start with smoke tests that cover revenue and trust paths. Add depth over sprints, not in one giant PR.
A sensible first batch for a Laravel eCommerce or booking app:
- Guest can view homepage and product or tour listing.
- Registered user can log in and reach dashboard.
- Cart or booking flow reaches confirmation page.
- Admin can create one CRUD record.
- Logout clears session and blocks protected routes.
On Notary Nepal, document upload and appointment booking were higher priority than footer link checks. Prioritise flows that generate leads or filings.
Run heavier suites on a nightly schedule. PR pipelines should finish in under fifteen minutes on three shards. Combine with database restore testing for backup confidence on a separate cron.
For teams without in-house QA bandwidth, testing and optimization services can bootstrap the first Playwright suite and wire it into an existing custom software pipeline. The patterns in this article transfer to WordPress and WooCommerce 11.1 storefronts as well—only the boot command changes.
Security-minded teams often pair E2E with SAST and DAST testing. Playwright is not a replacement for OWASP scans. It confirms that auth walls still stand after deploy.
Key Takeaways
- Install Playwright with pinned browsers and a CI-aware
playwright.config.tsthat enables retries, traces, andforbidOnly. - Boot Laravel with migrate, seed, Vite build, and
wait-onbefore running headless tests againstAPP_URL. - Shard specs across three or more workers to keep PR feedback under fifteen minutes.
- Use role-based locators, mocked payments, and isolated seed data to prevent flaky pipelines.
- Upload HTML reports and trace ZIPs on failure so debugging does not require full re-runs.
- Keep PR smoke tests lean; expand regression coverage on nightly jobs after gates are trusted.
People Also Ask
Does Playwright work with Laravel and PHP projects?
Yes. Playwright tests any app served over HTTP. Laravel Blade, Livewire, and Vite-built assets work without a JavaScript test runner inside PHP. You run Playwright via Node.js while PHP serves the app.
How many E2E tests should run on every pull request?
Most teams aim for ten to thirty smoke tests on PRs. That covers login, checkout or booking, and one admin path. Run larger regression suites nightly or pre-release.
Should Playwright replace PHPUnit or Pest in CI?
No. Keep fast unit and feature tests in PHPUnit or Pest. Playwright adds browser-level confidence on top. All layers should gate merge when failures indicate real regressions.
Which Playwright Docker image should CI use?
Use the official Microsoft image matching your @playwright/test version, such as mcr.microsoft.com/playwright:v1.55.0-jammy. Pin the tag and bump it when you upgrade the npm package.
Ship End-to-End Testing with Playwright in CI on your next sprint
End-to-End Testing with Playwright in CI turns "works on my machine" into a merge gate your whole team trusts. Start with one smoke spec, one GitHub Actions or GitLab job, and artifact upload on failure. Expand shards and coverage once the pipeline stays green for two weeks.
If you want help wiring Playwright into a Laravel deploy pipeline—or reviewing a flaky suite before it blocks releases—contact us for a practical audit. You can also browse the portfolio for live examples of booking and legal-tech flows that benefit most from browser-level CI gates.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

