End-to-End (E2E) testing pipeline

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:

  • πŸ§ͺ What E2E testing is
  • βš™οΈ Playwright setup (React app example)
  • πŸš€ GitHub Actions CI pipeline
  • πŸ“Έ Test reports + screenshots on failure
  • 🧠 Production best practices

🧠 0. What is E2E testing?

E2E (End-to-End) testing means:

β€œTest your app like a real user would use it.”

Instead of testing functions, you test:

  • Clicking buttons
  • Filling forms
  • Navigation
  • API + UI together

βš”οΈ Playwright vs Cypress (quick decision)

Feature Playwright Cypress
Speed πŸš€ Faster Ω…Ψͺوسط
Multi-browser βœ… Yes Limited
CI friendly ⭐ Excellent Good
Modern apps ⭐ Best choice Good

πŸ‘‰ We’ll use Playwright (industry standard in 2025)

🧱 1. Install Playwright (React project)

“`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

πŸ§ͺ 3. First E2E test (real user flow)

Example: Login flow test

“`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”]’, ‘[email protected]‘);
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'
}

βš™οΈ 6. GitHub Actions CI pipeline (E2E automation)

πŸ“ .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

πŸ“Š 8. Add HTML test 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

🌐 9. Test real deployed app (PRO setup)

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/);
});

🟑 Form validation test

“`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();
});

πŸ” 11. CI best practices (IMPORTANT)

βœ” Run E2E after build

“`yaml id=”bp1″

  • run: npm run build
  • run: npm start &
    “`

βœ” Use staging environment

  • staging.example.com
  • production.example.com

βœ” Retry flaky tests

“`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

πŸš€ What you just built

You now have:

  • πŸ§ͺ Real user simulation testing
  • πŸš€ CI/CD integrated E2E pipeline
  • πŸ“Έ Failure screenshots + videos
  • 🌍 Production-ready test system
  • πŸ”’ Safe deployment gating system