Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Automatically activated when user works with Playwright tests, mentions Playwright configuration, asks about selectors/locators/page objects, or has files matching *.spec.ts in e2e or tests directories. Provides Playwright-specific expertise for E2E and integration testing.
.claude/skills/aiskillstore-playwright-testing/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 130% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 167% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 207% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 340% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 179% | 0% |
You are an expert in Playwright testing framework with deep knowledge of browser automation, selectors, page objects, and best practices for end-to-end testing.
Claude should automatically invoke this skill when:
*.spec.ts in e2e, tests, or playwright directories are encounteredUse {baseDir} to reference files in this skill directory:
{baseDir}/scripts/{baseDir}/references/{baseDir}/assets/This skill includes ready-to-use resources in {baseDir}:
typescriptimport { test, expect } from '@playwright/test'; test.describe('Contact Form', () => { test.beforeEach(async ({ page }) => { await page.goto('/contact'); }); test('should show success message after form submission', async ({ page }) => { // Arrange await page.getByLabel('Name').fill('Test User'); await page.getByLabel('Email').fill('test@example.com'); await page.getByLabel('Message').fill('Hello, this is a test message.'); // Act await page.getByRole('button', { name: 'Submit' }).click(); // Assert await expect(page.getByText('Thank you for your message')).toBeVisible(); await expect(page.getByLabel('Name')).toBeEmpty(); }); });
typescript// Role-based (best) page.getByRole('button', { name: 'Submit' }); page.getByRole('textbox', { name: 'Email' }); page.getByRole('heading', { level: 1 }); // Label-based page.getByLabel('Email address'); page.getByPlaceholder('Enter your email'); // Text-based page.getByText('Welcome'); page.getByTitle('Close');
typescriptpage.getByRole('listitem') .filter({ hasText: 'Product 1' }) .getByRole('button', { name: 'Add' });
typescriptpage.getByTestId('submit-button');
typescript// pages/login.page.ts import { Page, Locator, expect } from '@playwright/test'; export class LoginPage { private readonly page: Page; readonly emailInput: Locator; readonly passwordInput: Locator; readonly submitButton: Locator; readonly errorMessage: Locator; constructor(page: Page) { this.page = page; this.emailInput = page.getByLabel('Email'); this.passwordInput = page.getByLabel('Password'); this.submitButton = page.getByRole('button', { name: 'Sign in' }); this.errorMessage = page.getByRole('alert'); } async goto() { await this.page.goto('/login'); await expect(this.emailInput).toBeVisible(); } async login(email: string, password: string) { await this.emailInput.fill(email); await this.passwordInput.fill(password); await this.submitButton.click(); } async getError(): Promise<string | null> { if (await this.errorMessage.isVisible()) { return this.errorMessage.textContent(); } return null; } } // Usage in test import { test, expect } from '@playwright/test'; import { LoginPage } from './pages/login.page'; test('should login successfully', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('user@test.com', 'password'); await expect(page).toHaveURL('/dashboard'); }); test('should show error for invalid credentials', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.goto(); await loginPage.login('invalid@test.com', 'wrongpassword'); const error = await loginPage.getError(); expect(error).toContain('Invalid credentials'); });
typescript// Auto-waits for element await expect(page.getByRole('alert')).toBeVisible(); await expect(page.getByRole('button')).toBeEnabled(); await expect(page.getByText('Count: 5')).toBeVisible(); // Negative assertions await expect(page.getByRole('dialog')).toBeHidden(); await expect(page.getByText('Error')).not.toBeVisible(); // With custom timeout await expect(page.getByText('Loaded')).toBeVisible({ timeout: 10000 });
typescript// fixtures.ts import { test as base } from '@playwright/test'; export const test = base.extend<{ authenticatedPage: Page; }>({ authenticatedPage: async ({ page }, use) => { await page.goto('/login'); await page.getByLabel('Email').fill('test@test.com'); await page.getByLabel('Password').fill('password'); await page.getByRole('button', { name: 'Login' }).click(); await page.waitForURL('/dashboard'); await use(page); }, });
For efficient authentication without UI login each time:
typescript// Setup: Save auth state after login (run once) // auth.setup.ts import { test as setup, expect } from '@playwright/test'; 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 expect(page).toHaveURL('/dashboard'); // Save storage state (cookies, localStorage) await page.context().storageState({ path: '.auth/user.json' }); }); // playwright.config.ts export default defineConfig({ projects: [ { name: 'setup', testMatch: /.*\.setup\.ts/ }, { name: 'chromium', use: { storageState: '.auth/user.json' }, dependencies: ['setup'], }, ], }); // Tests automatically have auth state test('dashboard loads for authenticated user', async ({ page }) => { await page.goto('/dashboard'); await expect(page.getByText('Welcome back')).toBeVisible(); });
Mock API responses for reliable, fast tests:
typescriptimport { test, expect } from '@playwright/test'; test('should display mocked user data', async ({ page }) => { // Mock API response await page.route('**/api/users', route => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([ { id: 1, name: 'Test User', email: 'test@example.com' } ]), }); }); await page.goto('/users'); await expect(page.getByText('Test User')).toBeVisible(); }); test('should handle API errors gracefully', async ({ page }) => { // Mock error response await page.route('**/api/users', route => { route.fulfill({ status: 500, body: JSON.stringify({ error: 'Internal Server Error' }), }); }); await page.goto('/users'); await expect(page.getByText('Failed to load users')).toBeVisible(); }); test('should handle network failure', async ({ page }) => { // Abort network request await page.route('**/api/data', route => route.abort()); await page.goto('/data'); await expect(page.getByText('Network error')).toBeVisible(); }); test('should handle slow responses', async ({ page }) => { // Simulate slow API await page.route('**/api/slow', async route => { await new Promise(resolve => setTimeout(resolve, 3000)); await route.continue(); }); await page.goto('/slow-page'); await expect(page.getByText('Loading...')).toBeVisible(); }); // Modify request/response test('should modify request headers', async ({ page }) => { await page.route('**/api/**', route => { route.continue({ headers: { ...route.request().headers(), 'X-Test-Header': 'test-value', }, }); }); });
Integrate accessibility audits with @axe-core/playwright:
typescript// Install: npm install @axe-core/playwright import { test, expect } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright'; test('should pass accessibility audit', async ({ page }) => { await page.goto('/'); const results = await new AxeBuilder({ page }).analyze(); expect(results.violations).toEqual([]); }); test('should pass accessibility audit for specific section', async ({ page }) => { await page.goto('/dashboard'); const results = await new AxeBuilder({ page }) .include('#main-content') .exclude('#third-party-widget') .withTags(['wcag2a', 'wcag2aa']) .analyze(); expect(results.violations).toEqual([]); }); // Check specific rules test('should have proper color contrast', async ({ page }) => { await page.goto('/'); const results = await new AxeBuilder({ page }) .withRules(['color-contrast']) .analyze(); expect(results.violations).toEqual([]); }); // Detailed violation reporting test('accessibility check with detailed report', async ({ page }) => { await page.goto('/'); const results = await new AxeBuilder({ page }).analyze(); if (results.violations.length > 0) { console.log('Accessibility violations:'); results.violations.forEach(violation => { console.log(`- ${violation.id}: ${violation.description}`); violation.nodes.forEach(node => { console.log(` Element: ${node.html}`); console.log(` Fix: ${node.failureSummary}`); }); }); } expect(results.violations).toEqual([]); });
Compare screenshots to detect visual changes:
typescriptimport { test, expect } from '@playwright/test'; test('homepage visual regression', async ({ page }) => { await page.goto('/'); // Full page screenshot comparison await expect(page).toHaveScreenshot('homepage.png'); }); test('component visual regression', async ({ page }) => { await page.goto('/components'); // Element-specific screenshot const button = page.getByRole('button', { name: 'Submit' }); await expect(button).toHaveScreenshot('submit-button.png'); }); test('visual with threshold', async ({ page }) => { await page.goto('/'); // Allow small differences await expect(page).toHaveScreenshot('homepage.png', { maxDiffPixels: 100, threshold: 0.2, }); }); // Update snapshots: npx playwright test --update-snapshots
typescript// playwright.config.ts import { defineConfig, devices } from '@playwright/test'; export default 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:3000', 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', use: { ...devices['iPhone 13'] } }, ], webServer: { command: 'npm run start', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, }, });
bashnpx playwright test --debug npx playwright test --ui
typescript// Capture trace on failure use: { trace: 'on-first-retry', } // View trace npx playwright show-trace trace.zip
typescriptawait page.screenshot({ path: 'screenshot.png', fullPage: true });
networkidle (which fails with WebSockets, long-polling, analytics):typescript // Bad: networkidle is unreliable await page.waitForLoadState('networkidle');
// Good: wait for specific content await expect(page.getByRole('main')).toBeVisible(); await expect(page.getByTestId('data-loaded')).toBeAttached();
test.describe.parallel()When testing forms:
When testing tables/lists:
.filter()The patterns in this skill require the following minimum versions:
| Feature | Minimum Version | Notes | |---------|----------------|-------| | getByRole with name | 1.27+ | Role-based locators with accessible name | | toHaveScreenshot | 1.22+ | Visual regression testing | | storageState | 1.13+ | Authentication state persistence | | @axe-core/playwright | 4.7+ | Accessibility testing integration | | route.fulfill | 1.0+ | Network mocking (stable) | | test.describe.configure | 1.24+ | Parallel/serial test configuration |
Check your Playwright version:
bashnpx playwright --version
bash# Update Playwright npm install -D @playwright/test@latest # Update browsers npx playwright install
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-03 | pass→pass | 16,816 | 10,498 | -38% | 1 | 1 | 0% | 2,040 | 5,692 | +179% | 0 | 0 | — |
case-04 | pass→pass | 5,584 | 10,374 | +86% | 1 | 1 | 0% | 939 | 4,808 | +412% | 0 | 0 | — |
case-01 | fail→pass | 18,189 | 14,853 | -18% | 1 | 1 | 0% | 2,504 | 5,756 | +130% | 0 | 0 | — |
case-02 | fail→fail | 18,113 | 19,069 | +5% | 1 | 1 | 0% | 2,487 | 6,733 | +171% | 0 | 0 | — |
case-05 | pass→pass | 9,664 | 8,585 | -11% | 1 | 1 | 0% | 877 | 4,559 | +420% | 0 | 0 | — |
case-06 | fail→pass | 17,248 | 13,955 | -19% | 1 | 1 | 0% | 2,071 | 5,537 | +167% | 0 | 0 | — |
case-07 | pass→pass | 6,696 | 7,434 | +11% | 1 | 1 | 0% | 1,277 | 4,956 | +288% | 0 | 0 | — |
case-08 | pass→pass | 8,714 | 9,756 | +12% | 1 | 1 | 0% | 1,587 | 4,837 | +205% | 0 | 0 | — |
case-09 | pass→pass | 9,790 | 3,781 | -61% | 1 | 1 | 0% | 762 | 4,634 | +508% | 0 | 0 | — |
case-10 | pass→pass | 13,853 | 9,985 | -28% | 1 | 1 | 0% | 1,585 | 4,802 | +203% | 0 | 0 | — |
case-11 | pass→pass | 9,375 | 8,943 | -5% | 1 | 1 | 0% | 1,380 | 4,571 | +231% | 0 | 0 | — |
case-12 | pass→pass | 15,320 | 16,970 | +11% | 1 | 1 | 0% | 1,747 | 5,590 | +220% | 0 | 0 | — |
case-13 | pass→pass | 12,769 | 6,485 | -49% | 1 | 1 | 0% | 1,340 | 5,036 | +276% | 0 | 0 | — |
case-14 | fail→pass | 8,899 | 11,370 | +28% | 1 | 1 | 0% | 1,668 | 5,119 | +207% | 0 | 0 | — |
case-15 | pass→pass | 13,402 | 12,750 | -5% | 1 | 1 | 0% | 1,591 | 5,329 | +235% | 0 | 0 | — |
case-16 | pass→pass | 12,181 | 11,272 | -7% | 1 | 1 | 0% | 1,305 | 5,039 | +286% | 0 | 0 | — |
case-21 | pass→pass | 7,547 | 14,161 | +88% | 1 | 1 | 0% | 1,568 | 5,609 | +258% | 0 | 0 | — |
case-17 | pass→pass | 11,311 | 13,401 | +18% | 1 | 1 | 0% | 1,154 | 5,159 | +347% | 0 | 0 | — |
case-18 | fail→pass | 10,759 | 9,881 | -8% | 1 | 1 | 0% | 1,085 | 4,772 | +340% | 0 | 0 | — |
case-19 | pass→pass | 14,407 | 7,901 | -45% | 1 | 1 | 0% | 1,724 | 5,427 | +215% | 0 | 0 | — |
case-20 | pass→pass | 8,579 | 4,152 | -52% | 1 | 1 | 0% | 565 | 4,587 | +712% | 0 | 0 | — |
case-22 | pass→pass | 13,142 | 9,977 | -24% | 1 | 1 | 0% | 1,313 | 4,780 | +264% | 0 | 0 | — |
case-23 | pass→pass | 8,892 | 14,097 | +59% | 1 | 1 | 0% | 1,719 | 5,703 | +232% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +17 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.