Day 10: CI/CD and Best Practices
What You Will Learn Today
- Why run Playwright in CI/CD
- Setting up GitHub Actions
- Test artifacts (reports, screenshots, videos, traces)
- Running Playwright in Docker
- Authentication state reuse (storageState)
- Test design best practices
- Dealing with flaky tests
- Test strategy: E2E vs unit/integration
- 10-day journey summary
Why Run Playwright in CI/CD
Running tests manually on your local machine is not enough to maintain quality over time.
flowchart LR
subgraph Manual["Manual Testing"]
M1["Easy to forget"]
M2["Environment differences"]
M3["Time-consuming"]
end
subgraph CICD["CI/CD Automated Testing"]
C1["Runs every time"]
C2["Consistent environment"]
C3["Fast feedback"]
end
style Manual fill:#ef4444,color:#fff
style CICD fill:#22c55e,color:#fff
By integrating Playwright into your CI/CD pipeline, you can automatically verify that code changes do not break existing functionality.
Setting Up GitHub Actions
Basic Workflow
# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- name: Upload HTML report
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30
Workflow Overview
flowchart TB
subgraph CI["GitHub Actions Workflow"]
A["Checkout code"] --> B["Set up Node.js"]
B --> C["Install dependencies"]
C --> D["Install browsers"]
D --> E["Run tests (headless)"]
E --> F{"Pass?"}
F -->|"Yes"| G["Done"]
F -->|"No"| H["Save report & traces"]
H --> I["Report failure"]
end
style CI fill:#8b5cf6,color:#fff
style G fill:#22c55e,color:#fff
style I fill:#ef4444,color:#fff
Caching Browsers
Browser installation takes time. Using a cache can speed up your workflow significantly.
- name: Cache Playwright browsers
uses: actions/cache@v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
- name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: npx playwright install --with-deps
- name: Install system dependencies
if: steps.playwright-cache.outputs.cache-hit == 'true'
run: npx playwright install-deps
Test Artifacts
When tests fail in CI, artifacts are essential for investigating the cause.
Configuration in playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [
['html', { open: 'never' }],
['junit', { outputFile: 'results.xml' }],
],
use: {
screenshot: 'only-on-failure',
video: 'retain-on-failure',
trace: 'retain-on-failure',
},
});
Types of Artifacts
| Artifact | Configuration | Purpose |
|---|---|---|
| HTML Report | reporter: 'html' |
Visualize test results |
| Screenshots | screenshot: 'only-on-failure' |
Capture screen on failure |
| Videos | video: 'retain-on-failure' |
Replay failed tests |
| Traces | trace: 'retain-on-failure' |
Detailed debugging information |
| JUnit Report | reporter: 'junit' |
Integration with CI/CD tools |
Uploading in GitHub Actions
- name: Upload test results
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: |
playwright-report/
test-results/
retention-days: 30
The test-results/ directory contains screenshots, videos, and trace files.
Running Playwright in Docker
Playwright provides official Docker images with all browsers pre-installed.
Using the Official Image
FROM mcr.microsoft.com/playwright:v1.50.0-noble
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npx", "playwright", "test"]
Using Docker in GitHub Actions
jobs:
test:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.50.0-noble
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright test
env:
HOME: /root
Using the container image eliminates the need for browser installation steps and guarantees environment consistency.
Authentication State Reuse (storageState)
When many tests require login, performing UI login for each test is inefficient. Playwright's storageState allows you to save and reuse authentication state.
Defining a Setup Project
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
{
name: 'chromium',
use: {
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});
Authentication Setup File
// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('/dashboard');
await expect(page.getByText('Welcome')).toBeVisible();
// Save authentication state
await page.context().storageState({ path: authFile });
});
flowchart TB
subgraph Auth["Authentication State Reuse"]
A["Setup project"] --> B["Perform login"]
B --> C["Save storageState"]
C --> D["Chromium project"]
D --> E["Load saved state"]
E --> F["Run tests as logged-in user"]
end
style Auth fill:#3b82f6,color:#fff
This approach eliminates the need to log in for each test, significantly reducing overall execution time.
Test Design Best Practices
Project Structure
project-root/
βββ tests/
β βββ auth.setup.ts # Authentication setup
β βββ auth/
β β βββ login.spec.ts
β β βββ register.spec.ts
β βββ dashboard/
β β βββ overview.spec.ts
β β βββ settings.spec.ts
β βββ products/
β βββ list.spec.ts
β βββ detail.spec.ts
βββ pages/ # Page Object Model
β βββ LoginPage.ts
β βββ DashboardPage.ts
β βββ ProductPage.ts
βββ fixtures/ # Test data
β βββ test-data.json
βββ playwright.config.ts
βββ package.json
Writing Maintainable Tests
// BAD: Fragile selectors
await page.locator('.btn-primary.mt-4.px-6').click();
await page.locator('#root > div > div:nth-child(3) > button').click();
// GOOD: Role-based locators
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel('Email').fill('test@example.com');
await page.getByTestId('checkout-button').click();
Locator Priority
| Priority | Locator | Reason |
|---|---|---|
| 1 | getByRole() |
Based on accessibility, most stable |
| 2 | getByLabel() |
Best for form elements |
| 3 | getByText() |
Identify by visible text |
| 4 | getByTestId() |
Test-specific attribute |
| 5 | locator('.class') |
Last resort |
Test Isolation
// BAD: Tests depend on each other
test('create item', async ({ page }) => {
// ...create item...
});
test('edit item', async ({ page }) => {
// Assumes 'create item' test ran first
});
// GOOD: Each test is independent
test('edit item', async ({ page }) => {
// Prepare data via API
const item = await createTestItem(page);
await page.goto(`/items/${item.id}/edit`);
// ...
});
Dealing with Flaky Tests
A flaky test is an unstable test that sometimes passes and sometimes fails with the same code.
flowchart TB
subgraph Causes["Causes of Flakiness"]
C1["Timing dependencies"]
C2["Shared state between tests"]
C3["External service dependencies"]
C4["Animations"]
C5["Dynamic content"]
end
subgraph Solutions["Solutions"]
S1["Trust auto-waiting"]
S2["Ensure test isolation"]
S3["Mock APIs"]
S4["Disable animations"]
S5["Explicit waits"]
end
C1 --> S1
C2 --> S2
C3 --> S3
C4 --> S4
C5 --> S5
style Causes fill:#ef4444,color:#fff
style Solutions fill:#22c55e,color:#fff
Configuring Retries
// playwright.config.ts
export default defineConfig({
retries: process.env.CI ? 2 : 0, // Retry twice in CI
});
Detecting Flaky Tests
# Repeat the same test 10 times to check stability
npx playwright test --repeat-each=10 tests/checkout.spec.ts
Troubleshooting Checklist
| Check | What to Look For |
|---|---|
Fixed waitForTimeout() |
Can auto-waiting replace it? |
| Test order dependency | Does the test pass when run in isolation? |
| External API dependency | Are you using route.fulfill() to mock? |
| Date/time dependency | Are you using page.clock? |
| Random data | Are you using fixed test data? |
Test Strategy: What to Test with E2E
You do not need to cover everything with E2E tests. Keep the testing pyramid in mind.
flowchart TB
subgraph Pyramid["Testing Pyramid"]
E2E["E2E Tests<br/>Few, critical flows"]
INT["Integration Tests<br/>Component interactions"]
UNIT["Unit Tests<br/>Many, fast"]
end
E2E --> INT --> UNIT
style E2E fill:#ef4444,color:#fff
style INT fill:#f59e0b,color:#000
style UNIT fill:#22c55e,color:#fff
Choosing the Right Test Type
| Test Type | Scope | Examples |
|---|---|---|
| Unit tests | Individual functions/classes | Validation logic, calculations |
| Integration | Component interactions | API + DB integration, UI components |
| E2E tests | Full user flows | Login -> purchase -> checkout |
Scenarios to Prioritize for E2E
- Critical paths: Login, checkout, registration
- Cross-browser verification: Layout issues, compatibility
- User journeys: End-to-end operation flows
- Regression prevention: Preventing recurrence of past bugs
10-Day Journey Summary
flowchart TB
subgraph Journey["Your 10-Day Learning Journey"]
D1["Day 1<br/>What is Playwright"]
D2["Day 2<br/>Setup"]
D3["Day 3<br/>Locators"]
D4["Day 4<br/>Assertions"]
D5["Day 5<br/>Page Interactions"]
D6["Day 6<br/>Networking"]
D7["Day 7<br/>Page Objects"]
D8["Day 8<br/>Fixtures"]
D9["Day 9<br/>Debugging"]
D10["Day 10<br/>CI/CD"]
D1 --> D2 --> D3 --> D4 --> D5
D5 --> D6 --> D7 --> D8 --> D9 --> D10
end
style D1 fill:#3b82f6,color:#fff
style D2 fill:#3b82f6,color:#fff
style D3 fill:#8b5cf6,color:#fff
style D4 fill:#8b5cf6,color:#fff
style D5 fill:#8b5cf6,color:#fff
style D6 fill:#f59e0b,color:#000
style D7 fill:#f59e0b,color:#000
style D8 fill:#f59e0b,color:#000
style D9 fill:#22c55e,color:#fff
style D10 fill:#22c55e,color:#fff
| Day | Topic | What You Learned |
|---|---|---|
| 1 | What is Playwright | Overview of E2E testing, Playwright features and architecture |
| 2 | Setup | Installation, configuration, writing your first test |
| 3 | Locators and DOM | getByRole, getByText, locator usage |
| 4 | Assertions | expect, toBeVisible, toHaveText |
| 5 | Page Interactions | Form handling, file uploads, dialogs |
| 6 | Network Control | route.fulfill, waitForResponse, API mocking |
| 7 | Page Object Model | Designing reusable page classes |
| 8 | Fixtures and Data | Custom fixtures, test data management |
| 9 | Debugging and Traces | Trace Viewer, UI Mode, debugging techniques |
| 10 | CI/CD and Best Practices | GitHub Actions, Docker, test strategy |
Summary
| Concept | Description |
|---|---|
| GitHub Actions | Automate test execution in CI/CD |
| Browser caching | Speed up CI execution time |
| Artifacts | Reports, screenshots, videos, traces |
| Docker | Consistent environments with official images |
| storageState | Save and reuse authentication state |
| Retries | Handle flaky tests in CI |
| Testing pyramid | Balance E2E, integration, and unit tests |
Key Takeaways
- CI/CD automates testing to safeguard quality
- storageState makes authentication efficient
- Flaky tests should be fixed at the root cause, not ignored
- Testing pyramid helps keep E2E test scope appropriate
- Role-based locators produce resilient, maintainable tests
Exercises
Basics
- Create a GitHub Actions workflow file that automatically runs Playwright tests on push.
- Configure screenshots, videos, and traces in
playwright.config.ts. - Add settings to upload the HTML report as an artifact.
Intermediate
- Create an authentication setup project using
storageStateto reduce test execution time. - Run Playwright tests using the official Docker image.
- Use
--repeat-each=10to check existing tests for flakiness.
Challenge
- Design a GitHub Actions workflow that runs tests in parallel across multiple browsers (Chromium, Firefox, WebKit). Include configuration to upload trace files on failure.
References
- Playwright CI/CD Guide
- Playwright GitHub Actions
- Playwright Docker
- Playwright Authentication
- Playwright Best Practices
Congratulations!
You have completed 10 days of learning Playwright!
What You Learned
- Day 1: What Playwright is and why E2E testing matters
- Day 2: Setting up your environment and writing your first test
- Day 3: Locators and DOM interaction
- Day 4: Validating behavior with assertions
- Day 5: Page interactions and navigation
- Day 6: Controlling network requests
- Day 7: Page Object Model for reusability
- Day 8: Fixtures and test data management
- Day 9: Debugging techniques and traces
- Day 10: CI/CD and best practices
The Road Ahead
You now have a solid grasp of Playwright fundamentals. From here, the most important thing is to apply what you have learned in real projects and build experience through practice.
Playwright is an actively developed tool with new features added in every release. Check the official documentation regularly to stay up to date with the latest capabilities and best practices.
Writing tests does more than protect software quality -- it gives you the confidence to make changes and ship faster. Take the CI/CD knowledge you gained today and integrate Playwright into your team's development workflow.
Your testing journey starts here. Keep learning, and build web applications you can trust!