cd /news/developer-tools/end-to-end-e2e-testing-pipeline Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-12424] src=dev.to β†— pub= topic=developer-tools verified=true sentiment=Β· neutral

End-to-End (E2E) testing pipeline

The article provides a guide on building an End-to-End (E2E) testing pipeline using Playwright and GitHub Actions, explaining how to test applications from a user's perspective rather than testing individual functions. It covers setting up the project structure, writing test scripts for user interactions like login and navigation, and configuring the pipeline to run automatically on code pushes or pull requests. The guide also includes advanced features such as auto-capturing screenshots and videos on test failures, running tests against production URLs, and best practices for writing reliable tests.

read3 min views26 publishedMay 23, 2026

Let’s build a real End-to-End (E2E) testing pipeline like teams use in production using Playwright (recommended) and GitHub Actions. I’ll show you: E2E (End-to-End) testing means: β€œTest your app like a real user would use it.” Instead of testing functions, you test: πŸ‘‰ We’ll use Playwright (industry standard in 2025)

npm init playwright@latest
When prompted choose:
* JavaScript or TypeScript (TS recommended)
* Tests folder: `tests`
* GitHub Actions: YES
---
``` plaintext id="pw2"
my-app/
β”œβ”€β”€ tests/
β”‚ β”œβ”€β”€ example.spec.ts
β”œβ”€β”€ playwright.config.ts
β”œβ”€β”€ package.json
``` ts id="test1"
import { test, expect } from '@playwright/test';
test('user can login successfully', async ({ page }) => {
await page.goto('http://localhost:3000/login');
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL(/dashboard/);
});
---
``` bash id="run1"
npx playwright test
Open UI mode (very useful):
``` bash id="ui1"
npx playwright test --ui
---
Playwright automatically captures:
* screenshots
* videos
* traces
Enable in config:
``` ts id="cfg1"
use: {
screenshot: 'only-on-failure',
video: 'retain-on-failure',
trace: 'on-first-retry'
}
.github/workflows/e2e.yml
``` yaml id="ci1"
name: E2E Tests (Playwright)
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm install
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
---
``` plaintext id="flow1"
Push code
↓
GitHub Actions starts
↓
Install dependencies
↓
Install browsers (Chromium, Firefox, WebKit)
↓
Run E2E tests
↓
Pass β†’ allow merge
Fail β†’ block PR + show report
Enable report:
``` ts id="rep1"
reporter: [['html'], ['list']]
Then in CI:
``` yaml id="rep2"
- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report
Instead of localhost:
``` ts id="prod1"
await page.goto('https://your-app.vercel.app/login');
πŸ‘‰ This turns it into **true production E2E testing**
---
---
## 🟒 UI navigation test
``` ts id="nav1"
test('navigate to dashboard', async ({ page }) => {
await page.goto('/');
await page.click('text=Dashboard');
await expect(page).toHaveURL(/dashboard/);
});
``` ts id="form1"
test('shows error for empty email', async ({ page }) => {
await page.goto('/login');
await page.click('button[type="submit"]');
await expect(page.locator('.error')).toContainText('Email is required');
});
---
## πŸ”΅ API + UI combined test
``` ts id="api1"
test('data loads after API call', async ({ page }) => {
await page.goto('/dashboard');
await expect(page.locator('.')).toBeHidden();
await expect(page.locator('.chart')).toBeVisible();
});
``` yaml id="bp1"
``` ts id="rt1"
retries: 2
---
### ❌ Testing implementation instead of behavior
Bad:
``` ts id="bad1"
expect(component.state).toBe(true)
Good:
``` ts id="good1"
expect(page).toHaveText('Welcome')
---
### ❌ No stable selectors
Use:
``` html id="sel1"
data-testid="login-button"
Then:
``` ts id="sel2"
page.getByTestId('login-button')
---
### ❌ Running E2E without CI
Always run in GitHub Actions
---
``` plaintext id="final1"
Push / PR
↓
CI (unit tests)
↓
E2E tests (Playwright)
↓
Build app
↓
Deploy to staging/production
↓
Report + screenshots stored in GitHub
You now have:
── more in #developer-tools 4 stories Β· sorted by recency
── more on @playwright 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/end-to-end-e2e-testi…] indexed:0 read:3min 2026-05-23 Β· β€”