Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

End-to-End Testing with Playwright in CI

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.

Git PushPR or mainBuild AppComposer + ViteServe Appphp artisan servePlaywright CIHeadless browserParallel shardsQuality GatePass or blockArtifacts on FailureTrace ZIP, screenshot, video, HTML reportDownload from CI job logs
End-to-End Testing with Playwright in CI: commit triggers build, app boot, browser tests, then deploy gate.

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:

  1. Copy .env.example to .env and set APP_KEY.
  2. Run migrations against SQLite or a service container database.
  3. Seed minimal test data with a dedicated seeder.
  4. Build frontend assets with Vite 8.x if your pages require compiled JS.
  5. Start php artisan serve in the background on port 8000.
  6. Wait until the health URL returns 200.
  7. 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.

CriteriaGitHub ActionsGitLab CI
Official Playwright imagemcr.microsoft.com/playwright:v1.55.0-jammySame image in image: key
Browser cacheactions/cache on ~/.cache/ms-playwrightGitLab cache paths
Parallel shardsmatrix strategyparallel matrix
Artifact uploadactions/upload-artifactartifacts reports
Laravel fitStrong with GitHub Actions for LaravelStrong 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.

Parallel ShardingWorker 1npx playwright test--shard=1/3Worker 2npx playwright test--shard=2/3Worker 3npx playwright test--shard=3/3Shared Laravel AppSingle seeded DB per pipeline runMerge shard results — any failure blocks deploy
Sharding splits Playwright specs across CI workers while one app instance serves all shards.

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 beforeEach only 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.

Stable vs Flaky E2EStable Pattern• getByRole locators• Seeded test users• Mocked payment APIs• Trace on first retry• 10 smoke specs in PR• SQLite per job• wait-on health checkFlaky Pattern• CSS nth-child selectors• Shared live database• Real Khalti in CI• No screenshots• 200 specs on every PR• Fixed sleep(5000)• No server wait stepavoid
Stable End-to-End Testing with Playwright in CI uses role locators, mocks, and lean PR suites—not live payments and sleep calls.

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.

CI Job FailsE2E red statusDownloadArtifacts ZIPshow-reportHTML timeline viewscreenshots attachedOpen Tracenpx playwrightshow-trace fileFind root causeNetwork 500, overlay, or missing seed data
Debug failed End-to-End Testing with Playwright in CI by downloading artifacts, opening the HTML report, then inspecting traces.

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.ts that enables retries, traces, and forbidOnly.
  • Boot Laravel with migrate, seed, Vite build, and wait-on before running headless tests against APP_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

End-to-end testing with Playwright in CI runs real browser tests on each pipeline build using headless Chromium, cached dependencies, parallel shards, and uploaded traces on failure—so UI and workflow regressions block deploy before users hit them.

Install @playwright/test as a dev dependency, pin browsers with npx playwright install --with-deps chromium, and create playwright.config.ts with CI-aware retries, trace capture, forbidOnly, and a fixed baseURL from APP_URL. Add npm scripts test:e2e and test:e2e:report. Boot Laravel by copying .env.example, running migrations and E2eSeeder, building Vite 8.x assets, starting php artisan serve on port 8000, waiting with wait-on, then running Playwright against http://127.0.0.1:8000. Pair smoke specs in e2e/ with PHPUnit or Pest for server logic.

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.

GitHub Actions and GitLab CI both ship solid Playwright support—pick whichever your team already uses. Playwright publishes official Docker images with browsers and OS dependencies preinstalled. GitHub Actions uses actions/cache on ~/.cache/ms-playwright, matrix sharding, and actions/upload-artifact. GitLab CI uses cache paths, parallel matrix jobs, and artifacts reports. The Playwright steps are nearly identical once the Laravel app is running. Both platforms fit Laravel projects well when paired with php-actions/composer or apt-installed PHP 8.3-cli.

For Laravel 12 or 13, copy .env.example to .env, generate APP_KEY, run composer install, npm ci, and npm run build if pages need compiled JS. Migrate against SQLite or a MySQL 8.4 service container, then seed minimal deterministic data with a dedicated E2eSeeder—one admin, one customer, one published tour keeps specs fast. Start php artisan serve --host=127.0.0.1 --port=8000 in the background, wait until the health URL returns 200 with wait-on, then run Playwright with APP_URL=http://127.0.0.1:8000.

Sharding splits Playwright specs across three or more CI workers using --shard=1/3 style flags while one app instance serves all shards. GitHub Actions uses a matrix with fail-fast: false; GitLab CI uses parallel: 3 with CI_NODE_INDEX and CI_NODE_TOTAL. Cache node_modules and ~/.cache/ms-playwright so repeat runs skip reinstalling browsers. Most teams target PR feedback under fifteen minutes on three shards, reserving heavier regression suites for nightly schedules.

Treat stability as architecture, not luck. Prefer getByRole, getByLabel, and getByTestId over brittle CSS chains. Replace fixed sleeps with auto-waiting assertions and raise expect timeouts to 10–15 seconds on slow runners. Isolate test data with SQLite per job, UUID-prefixed seeders, or cheap beforeEach resets. Stub external APIs and payment gateways instead of hitting live Khalti or SMS services. Wire Playwright into branch protection alongside PHPUnit so failures block merge. Keep PR suites lean—twenty stable tests beat two hundred flaky ones.

Configure trace on-first-retry, screenshot only-on-failure, and video retain-on-failure, then upload playwright-report/ and test-results/ as pipeline artifacts with at least seven-day retention. Download the artifact, unzip it, and run npx playwright show-report ./playwright-report locally. For timing issues, open traces with npx playwright show-trace and check for 401 responses, CORS blocks, or JavaScript console errors. Reproduce CI locally with CI=true APP_URL=http://127.0.0.1:8000 npx playwright test --shard=1/3, matching PHP 8.3, Node.js 26 LTS, headless mode, and TZ=UTC.

Start with smoke tests on revenue and trust paths, then expand over sprints. A sensible first batch for Laravel eCommerce or booking apps: guest views homepage and listing, registered user logs in and reaches dashboard, cart or booking reaches confirmation, admin creates one CRUD record, and logout blocks protected routes. On legal-tech portals, prioritise document upload and appointment booking over footer links. PR pipelines should finish under fifteen minutes; run heavier suites nightly. Playwright confirms auth walls still stand after deploy but does not replace OWASP SAST or DAST scans.

Yes. Playwright tests any application served over HTTP, so Laravel Blade, Livewire, Alpine, jQuery, and Vite-built assets all work without a JavaScript test runner inside PHP. You run Playwright via Node.js while PHP serves the app through php artisan serve. I've seen Laravel apps pass PHPUnit yet fail when a user clicks Pay with Khalti in staging—E2E catches that wiring gap. The same patterns transfer to WordPress and WooCommerce 11.1 storefronts; only the boot command changes.

Most teams aim for ten to thirty smoke tests on PRs covering login, checkout or booking, and one admin path.

No. Keep PHPUnit or Pest for server logic. Playwright adds browser-level checks on top. All layers should gate merge when failures indicate real regressions.

Never hit live payment or SMS APIs in CI. On legal-tech portals and eCommerce flows I've maintained, stub external gateways to return deterministic success payloads instead of calling real Khalti or similar services. That keeps CI bills low, avoids flaky third-party timeouts, and makes assertions repeatable. PHPUnit should still validate server-side payment logic; Playwright confirms the browser wiring—modal overlays, button clicks, and confirmation pages—works end to end with mocked responses.

Store E2E login passwords and other credentials in masked CI variables, never in spec files or Git history. Follow CI secrets management practices and scan repos with Gitleaks-style tooling. Keep production logs out of uploaded artifacts unless redacted. Playwright confirms auth walls still block protected routes after deploy, but it is not a replacement for OWASP-focused SAST or DAST testing. Security-minded teams run both: browser-level smoke gates on every merge and deeper vulnerability scans on a separate schedule.

Common causes include mismatched environment variables, PHP or Node versions, headless-only timing, and timezone differences affecting date pickers. CI runs with CI=true, fixed APP_URL, PHP 8.3, Node.js 26 LTS, and headless Chromium inside the pinned Playwright Docker image. Locally you may skip Vite builds, use a different database, or run with a visible browser that masks race conditions. Set TZ=UTC in both local Docker and CI. Reproduce the exact shard command CI uses before blaming flakiness on the runner.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: