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"]', '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'
}
⚙️ 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
Top comments (0)