DEV Community

Kyle Y. Parsotan
Kyle Y. Parsotan

Posted on

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
Enter fullscreen mode Exit fullscreen mode

🧪 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"]', '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
Enter fullscreen mode Exit fullscreen mode

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'
}
Enter fullscreen mode Exit fullscreen mode

⚙️ 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
Enter fullscreen mode Exit fullscreen mode



---

# 🧠 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
Enter fullscreen mode Exit fullscreen mode

📊 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
Enter fullscreen mode Exit fullscreen mode

🌐 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/);
});
Enter fullscreen mode Exit fullscreen mode

🟡 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();
});
Enter fullscreen mode Exit fullscreen mode

🔐 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)
Enter fullscreen mode Exit fullscreen mode

Good:

```ts id="good1"
expect(page).toHaveText('Welcome')




---

### ❌ No stable selectors

Use:



```html id="sel1"
data-testid="login-button"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

🚀 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

Top comments (0)