Playwright Skill
Expert assistance for building comprehensive E2E test suites with Playwright, including page objects, fixtures, visual regression, and CI/CD integration.
Capabilities
- Generate Playwright test project structure
- Create page object models for maintainable tests
- Implement custom fixtures and test utilities
- Configure visual regression testing
- Set up accessibility testing with axe-core
- Integrate with CI/CD pipelines (GitHub Actions, etc.)
- Generate API testing alongside UI tests
Usage
Invoke this skill when you need to:
- Set up Playwright testing for a web application
- Create page object patterns for test organization
- Implement visual regression testing
- Configure cross-browser testing
- Set up CI/CD test automation
Inputs
| Parameter | Type | Required | Description | |-----------|------|----------|-------------| | projectType | string | No | web, api, component (default: web) | | framework | string | No | react, nextjs, vue, angular | | browsers | array | No | chromium, firefox, webkit (default: all) | | features | array | No | visual, a11y, api, component | | ci | string | No | github, gitlab, jenkins |
Test Configuration
{
"projectType": "web",
"framework": "nextjs",
"browsers": ["chromium", "firefox"],
"features": ["visual", "a11y", "api"],
"ci": "github",
"baseUrl": "http://localhost:3000"
}
Output Structure
tests/
├── playwright.config.ts # Playwright configuration
├── fixtures/
│ ├── base.ts # Base test fixture
│ ├── auth.ts # Authentication fixture
│ └── api.ts # API helper fixture
├── pages/
│ ├── BasePage.ts # Base page object
│ ├── LoginPage.ts # Login page object
│ └── DashboardPage.ts # Dashboard page object
├── e2e/
│ ├── auth/
│ │ ├── login.spec.ts
│ │ └── logout.spec.ts
│ ├── dashboard/
│ │ └── dashboard.spec.ts
│ └── api/
│ └── users.api.spec.ts
├── visual/
│ ├── homepage.visual.spec.ts
│ └── screenshots/ # Baseline screenshots
├── a11y/
│ └── accessibility.spec.ts
├── utils/
│ ├── helpers.ts
│ └── test-data.ts
└── .github/
└── workflows/
└── playwright.yml # CI workflow
Generated Code Patterns
Playwright Configuration
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html', { open: 'never' }],
['json', { outputFile: 'test-results/results.json' }],
['junit', { outputFile: 'test-results/junit.xml' }],
],
use: {
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
dependencies: ['setup'],
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
dependencies: ['setup'],
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
dependencies: ['setup'],
},
{
name: 'mobile-chrome',
use: { ...devices['Pixel 5'] },
dependencies: ['setup'],
},
{
name: 'mobile-safari',
use: { ...devices['iPhone 13'] },
dependencies: ['setup'],
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
Base Page Object
// tests/pages/BasePage.ts
import { Page, Locator, expect } from '@playwright/test';
export abstract class BasePage {
readonly page: Page;
readonly header: Locator;
readonly footer: Locator;
readonly loadingSpinner: Locator;
constructor(page: Page) {
this.page = page;
this.header = page.locator('header');
this.footer = page.locator('footer');
this.loadingSpinner = page.locator('[data-testid="loading"]');
}
abstract get url(): string;
async goto() {
await this.page.goto(this.url);
await this.waitForPageLoad();
}
async waitForPageLoad() {
await this.loadingSpinner.waitFor({ state: 'hidden' });
}
async expectToBeVisible() {
await expect(this.page).toHaveURL(new RegExp(this.url));
}
async getToastMessage(): Promise<string | null> {
const toast = this.page.locator('[role="alert"]');
if (await toast.isVisible()) {
return toast.textContent();
}
return null;
}
}
Login Page Object
// tests/pages/LoginPage.ts
import { Page, Locator, expect } from '@playwright/test';
import { BasePage } from './BasePage';
export class LoginPage extends BasePage {
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
readonly forgotPasswordLink: Locator;
constructor(page: Page) {
super(page);
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Sign in' });
this.errorMessage = page.locator('[role="alert"]');
this.forgotPasswordLink = page.getByRole('link', { name: 'Forgot password?' });
}
get url() {
return '/login';
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectErrorMessage(message: string) {
await expect(this.errorMessage).toContainText(message);
}
async expectLoginSuccess() {
await expect(this.page).toHaveURL(/\/dashboard/);
}
}
Custom Fixtures
// tests/fixtures/base.ts
import { test as base, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
interface TestFixtures {
loginPage: LoginPage;
dashboardPage: DashboardPage;
}
interface WorkerFixtures {
authenticatedPage: void;
}
export const test = base.extend<TestFixtures, WorkerFixtures>({
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await use(loginPage);
},
dashboardPage: async ({ page }, use) => {
const dashboardPage = new DashboardPage(page);
await use(dashboardPage);
},
authenticatedPage: [
async ({ browser }, use) => {
const context = await browser.newContext({
storageState: 'tests/.auth/user.json',
});
await use();
await context.close();
},
{ scope: 'worker' },
],
});
export { expect };
Authentication Setup
// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
import path from 'path';
const authFile = path.join(__dirname, '.auth/user.json');
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL!);
await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL(/\/dashboard/);
await page.context().storageState({ path: authFile });
});
E2E Test Example
// tests/e2e/auth/login.spec.ts
import { test, expect } from '../../fixtures/base';
test.describe('Login', () => {
test.beforeEach(async ({ loginPage }) => {
await loginPage.goto();
});
test('should login with valid credentials', async ({ loginPage }) => {
await loginPage.login('user@example.com', 'password123');
await loginPage.expectLoginSuccess();
});
test('should show error with invalid credentials', async ({ loginPage }) => {
await loginPage.login('invalid@example.com', 'w