| name | webapp-testing |
| description | Set up and run end-to-end (E2E) tests for web applications using Playwright. Invoke when user wants to create E2E tests, set up testing framework, or run automated browser tests. |
Web App Testing
This skill helps you set up and run end-to-end (E2E) tests for web applications using Playwright.
When to Use
- User wants to create E2E tests for their web application
- User needs to set up automated browser testing
- User asks to test user flows and interactions
- User wants to add visual regression testing
- User needs to verify critical user journeys work correctly
Setup Playwright
If Playwright is not already installed:
npm init playwright@latest
Or for existing projects:
npm install --save-dev @playwright/test
npx playwright install
Configuration
Create playwright.config.js in project root:
const { defineConfig, devices } = require('@playwright/test');
module.exports = defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
},
});
Test Structure
Create test files in e2e/ directory:
const { test, expect } = require('@playwright/test');
test('homepage has title and links', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/My App/);
await page.getByRole('link', { name: 'Get started' }).click();
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});
Best Practices
Use Role-Based Selectors
✅ Good:
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByLabel('Username').fill('john');
await page.getByTestId('submit-button').click();
❌ Avoid:
await page.click('.btn-primary');
await page.fill('#username', 'john');
Test User Flows
test('user can complete checkout', async ({ page }) => {
await page.goto('/products');
await page.getByRole('button', { name: 'Add to cart' }).first().click();
await page.getByRole('link', { name: 'Cart' }).click();
await page.getByRole('button', { name: 'Checkout' }).click();
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Card number').fill('4242424242424242');
await page.getByRole('button', { name: 'Pay' }).click();
await expect(page.getByText('Payment successful')).toBeVisible();
});
Test Authentication
test('authenticated user can access dashboard', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Username').fill('admin');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page).toHaveURL('/dashboard');
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
Visual Regression Testing
test('homepage visual regression', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
threshold: 0.2,
});
});
API Mocking
test('handles API errors gracefully', async ({ page }) => {
await page.route('**/api/users', route => {
route.fulfill({
status: 500,
body: JSON.stringify({ error: 'Server error' }),
});
});
await page.goto('/users');
await expect(page.getByText('Failed to load users')).toBeVisible();
});
Running Tests
npx playwright test
npx playwright test --headed
npx playwright test example.spec.js
npx playwright test --ui
npx playwright test --debug
npx playwright test --last-failed
npx playwright show-report
Common Patterns
Page Object Model
class LoginPage {
constructor(page) {
this.page = page;
this.usernameInput = page.getByLabel('Username');
this.passwordInput = page.getByLabel('Password');
this.loginButton = page.getByRole('button', { name: 'Login' });
}
async goto() {
await this.page.goto('/login');
}
async login(username, password) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
}
module.exports = { LoginPage };
Test Fixtures
const { test: base } = require('@playwright/test');
const { LoginPage } = require('./pages/LoginPage');
const test = base.extend({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
authenticatedPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('admin', 'password');
await use(page);
},
});
module.exports = { test, expect };
CI/CD Integration
Add to .github/workflows/playwright.yml:
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
Output Format
When creating tests, output:
- Test file path
- Test scenarios covered
- Key selectors used
- Any mocks or setup needed