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