# End-to-End (E2E) testing pipeline

> Source: <https://dev.to/kyl67899/end-to-end-e2e-testing-pipeline-dl8>
> Published: 2026-05-23 21:02:29+00:00

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)
``` bash id="pw1"
npm init playwright@latest
When prompted choose:
* JavaScript or TypeScript (TS recommended)
* Tests folder: `tests`
* GitHub Actions: YES
---
# 📁 2. Project structure
``` 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/);
});
---
# 🚀 4. Run tests locally
``` bash id="run1"
npx playwright test
Open UI mode (very useful):
``` bash id="ui1"
npx playwright test --ui
---
# 📸 5. Auto screenshots on failure
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
---
# 🧠 7. What happens in CI
``` 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**
---
# 🧪 10. Advanced real-world test examples
---
## 🟢 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('.loading')).toBeHidden();
await expect(page.locator('.chart')).toBeVisible();
});
``` yaml id="bp1"
``` ts id="rt1"
retries: 2
---
# ⚠️ 12. Common mistakes
### ❌ 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
---
# 🧠 Final architecture
``` 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:
