--- name: Playwright Enhanced description: Advanced Playwright automation with auto-detection, custom fixtures, trace debugging, visual testing, mobile emulation, and production-grade test architecture. version: 1.0.0 author: thetestingacademy license: MIT tags: [playwright, e2e, advanced, fixtures, trace-viewer, visual-testing, mobile-testing] testingTypes: [e2e, visual] frameworks: [playwright] languages: [typescript, javascript] domains: [web, mobile] agents: [claude-code, cursor, github-copilot, windsurf, codex, aider, continue, cline, zed, bolt] --- # Playwright Enhanced Skill You are an expert QA automation engineer specializing in advanced Playwright patterns. When asked to write or enhance Playwright tests, follow these production-grade instructions. ## Advanced Configuration ### Multi-Environment Setup ```typescript // playwright.config.ts import { defineConfig, devices } from '@playwright/test'; const env = process.env.TEST_ENV || 'local'; const environments = { local: { baseURL: 'http://localhost:3000', apiURL: 'http://localhost:8080', }, staging: { baseURL: 'https://staging.example.com', apiURL: 'https://api-staging.example.com', }, production: { baseURL: 'https://example.com', apiURL: 'https://api.example.com', }, }; 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', outputFolder: 'test-results/html' }], ['json', { outputFile: 'test-results/results.json' }], ['junit', { outputFile: 'test-results/junit.xml' }], process.env.CI ? ['github'] : ['list'], ], use: { baseURL: environments[env].baseURL, trace: 'retain-on-failure', screenshot: 'only-on-failure', video: 'retain-on-failure', actionTimeout: 15000, navigationTimeout: 30000, }, 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'], }, { name: 'tablet', use: { ...devices['iPad Pro'] }, dependencies: ['setup'], }, ], webServer: env === 'local' ? { command: 'npm run dev', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, timeout: 120000, } : undefined, }); ``` ## Advanced Custom Fixtures ```typescript // fixtures/index.ts import { test as base, Page } from '@playwright/test'; import { LoginPage } from '../pages/login.page'; import { DashboardPage } from '../pages/dashboard.page'; type MyFixtures = { loginPage: LoginPage; dashboardPage: DashboardPage; authenticatedPage: Page; adminPage: Page; mockApi: MockApiHelper; testData: TestDataFactory; }; export const test = base.extend({ loginPage: async ({ page }, use) => { await use(new LoginPage(page)); }, dashboardPage: async ({ page }, use) => { await use(new DashboardPage(page)); }, authenticatedPage: async ({ browser }, use) => { const context = await browser.newContext({ storageState: 'playwright/.auth/user.json', }); const page = await context.newPage(); await use(page); await context.close(); }, adminPage: async ({ browser }, use) => { const context = await browser.newContext({ storageState: 'playwright/.auth/admin.json', }); const page = await context.newPage(); await use(page); await context.close(); }, mockApi: async ({ page }, use) => { const helper = new MockApiHelper(page); await use(helper); }, testData: async ({}, use) => { const factory = new TestDataFactory(); await use(factory); await factory.cleanup(); }, }); export { expect } from '@playwright/test'; ``` ### Advanced Page Object Pattern ```typescript // pages/base.page.ts import { Page, Locator, expect } from '@playwright/test'; export abstract class BasePage { protected page: Page; constructor(page: Page) { this.page = page; } async navigate(path: string): Promise { await this.page.goto(path); } async waitForPageLoad(): Promise { await this.page.waitForLoadState('networkidle'); } async waitForElement(selector: string, timeout = 10000): Promise { return this.page.waitForSelector(selector, { timeout }); } async takeScreenshot(name: string): Promise { await this.page.screenshot({ path: `screenshots/${name}.png`, fullPage: true, }); } async scrollToElement(locator: Locator): Promise { await locator.scrollIntoViewIfNeeded(); } async typeWithDelay(locator: Locator, text: string, delay = 100): Promise { await locator.type(text, { delay }); } async selectDropdownOption(locator: Locator, option: string): Promise { await locator.click(); await this.page.getByRole('option', { name: option }).click(); } async uploadFile(locator: Locator, filePath: string): Promise { await locator.setInputFiles(filePath); } async waitForApiResponse(urlPattern: string | RegExp): Promise { await this.page.waitForResponse((response) => (typeof urlPattern === 'string' ? response.url().includes(urlPattern) : urlPattern.test(response.url())) && response.status() === 200 ); } } ``` ```typescript // pages/dashboard.page.ts import { Page, Locator, expect } from '@playwright/test'; import { BasePage } from './base.page'; export class DashboardPage extends BasePage { readonly heading: Locator; readonly userMenu: Locator; readonly notificationBell: Locator; readonly searchInput: Locator; constructor(page: Page) { super(page); this.heading = page.getByRole('heading', { name: 'Dashboard' }); this.userMenu = page.getByRole('button', { name: /user menu/i }); this.notificationBell = page.getByTestId('notification-bell'); this.searchInput = page.getByPlaceholder('Search...'); } async goto(): Promise { await this.navigate('/dashboard'); await this.expectToBeLoaded(); } async expectToBeLoaded(): Promise { await expect(this.heading).toBeVisible(); await this.waitForPageLoad(); } async searchFor(query: string): Promise { await this.searchInput.fill(query); await this.searchInput.press('Enter'); await this.waitForApiResponse('/api/search'); } async openUserMenu(): Promise { await this.userMenu.click(); } async getNotificationCount(): Promise { const badge = this.notificationBell.locator('.badge'); const text = await badge.textContent(); return parseInt(text || '0', 10); } async expectWelcomeMessage(username: string): Promise { const message = this.page.getByText(`Welcome, ${username}`); await expect(message).toBeVisible(); } } ``` ## API Mocking Strategies ```typescript // helpers/mock-api.helper.ts import { Page, Route } from '@playwright/test'; export class MockApiHelper { constructor(private page: Page) {} async mockSuccess(endpoint: string, data: any): Promise { await this.page.route(endpoint, async (route: Route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(data), }); }); } async mockError(endpoint: string, status: number, message: string): Promise { await this.page.route(endpoint, async (route: Route) => { await route.fulfill({ status, contentType: 'application/json', body: JSON.stringify({ error: message }), }); }); } async mockDelay(endpoint: string, delay: number): Promise { await this.page.route(endpoint, async (route: Route) => { await new Promise((resolve) => setTimeout(resolve, delay)); await route.continue(); }); } async interceptAndModify(endpoint: string, modifier: (data: any) => any): Promise { await this.page.route(endpoint, async (route: Route) => { const response = await route.fetch(); const json = await response.json(); const modified = modifier(json); await route.fulfill({ response, body: JSON.stringify(modified), }); }); } } // Usage in tests test('should handle API error gracefully', async ({ page, mockApi }) => { await mockApi.mockError('/api/users', 500, 'Internal Server Error'); await page.goto('/users'); await expect(page.getByText('Something went wrong')).toBeVisible(); }); ``` ## Visual Regression Testing ```typescript test.describe('Visual regression', () => { test('homepage snapshot desktop', async ({ page }) => { await page.goto('/'); await page.waitForLoadState('networkidle'); await expect(page).toHaveScreenshot('homepage-desktop.png', { fullPage: true, animations: 'disabled', maxDiffPixels: 100, }); }); test('homepage snapshot mobile', async ({ page }) => { await page.setViewportSize({ width: 375, height: 667 }); await page.goto('/'); await expect(page).toHaveScreenshot('homepage-mobile.png', { fullPage: true, animations: 'disabled', }); }); test('component snapshot', async ({ page }) => { await page.goto('/components/button'); const button = page.getByRole('button', { name: 'Primary' }); await expect(button).toHaveScreenshot('button-primary.png'); }); test('dark mode snapshot', async ({ page }) => { await page.emulateMedia({ colorScheme: 'dark' }); await page.goto('/'); await expect(page).toHaveScreenshot('homepage-dark.png', { fullPage: true, }); }); }); ``` ## Mobile Testing Patterns ```typescript test.describe('Mobile-specific tests', () => { test.use({ ...devices['iPhone 13'] }); test('should handle mobile navigation', async ({ page }) => { await page.goto('/'); // Open hamburger menu const menuButton = page.getByRole('button', { name: 'Menu' }); await expect(menuButton).toBeVisible(); await menuButton.click(); // Navigate to section await page.getByRole('link', { name: 'Products' }).click(); await expect(page).toHaveURL('/products'); }); test('should handle touch gestures', async ({ page }) => { await page.goto('/gallery'); const image = page.getByTestId('gallery-image'); // Swipe left await image.dragTo(image, { sourcePosition: { x: 200, y: 100 }, targetPosition: { x: 50, y: 100 }, }); await expect(page.getByTestId('image-2')).toBeVisible(); }); test('should adapt to orientation changes', async ({ page }) => { await page.goto('/'); // Portrait await page.setViewportSize({ width: 375, height: 667 }); await expect(page.getByTestId('mobile-menu')).toBeVisible(); // Landscape await page.setViewportSize({ width: 667, height: 375 }); await expect(page.getByTestId('desktop-menu')).toBeVisible(); }); }); ``` ## Network Manipulation ```typescript test.describe('Network conditions', () => { test('should handle slow 3G connection', async ({ page, context }) => { await context.route('**/*', async (route) => { await new Promise((resolve) => setTimeout(resolve, 500)); await route.continue(); }); await page.goto('/'); await expect(page.getByTestId('loading-spinner')).toBeVisible(); await expect(page.getByRole('heading')).toBeVisible({ timeout: 20000 }); }); test('should handle offline mode', async ({ page, context }) => { await page.goto('/'); // Go offline await context.setOffline(true); await page.reload(); await expect(page.getByText('You are offline')).toBeVisible(); // Go back online await context.setOffline(false); await page.reload(); await expect(page.getByRole('heading')).toBeVisible(); }); }); ``` ## Advanced Trace Debugging ```typescript test('with detailed trace', async ({ page }) => { // Start tracing before creating the page await page.context().tracing.start({ screenshots: true, snapshots: true, sources: true, }); await test.step('Navigate to login page', async () => { await page.goto('/login'); }); await test.step('Fill login form', async () => { await page.fill('#email', 'user@example.com'); await page.fill('#password', 'password123'); }); await test.step('Submit form', async () => { await page.click('button[type="submit"]'); }); await test.step('Verify dashboard loaded', async () => { await expect(page.getByRole('heading')).toHaveText('Dashboard'); }); // Stop tracing and save await page.context().tracing.stop({ path: 'trace.zip' }); }); ``` ## Parallel Test Execution Optimization ```typescript // tests/parallel-safe.spec.ts import { test, expect } from './fixtures'; test.describe.configure({ mode: 'parallel' }); test.describe('Parallel safe tests', () => { test('test 1 with isolated data', async ({ page, testData }) => { const user = await testData.createUniqueUser(); await page.goto(`/users/${user.id}`); // Each test gets unique data }); test('test 2 with isolated data', async ({ page, testData }) => { const user = await testData.createUniqueUser(); await page.goto(`/users/${user.id}`); // No conflict with test 1 }); }); // tests/serial-required.spec.ts test.describe.configure({ mode: 'serial' }); test.describe('Serial tests (order matters)', () => { let orderId: string; test('step 1: create order', async ({ page }) => { // ... create order orderId = 'ORDER-123'; }); test('step 2: process order', async ({ page }) => { // Uses orderId from previous test await page.goto(`/orders/${orderId}`); }); }); ``` ## Performance Monitoring ```typescript import { chromium } from '@playwright/test'; test('measure page performance', async ({ page }) => { await page.goto('/'); const metrics = await page.evaluate(() => { const perfData = window.performance.timing; return { domContentLoaded: perfData.domContentLoadedEventEnd - perfData.navigationStart, loadComplete: perfData.loadEventEnd - perfData.navigationStart, firstPaint: performance.getEntriesByType('paint')[0]?.startTime || 0, }; }); console.log('Performance metrics:', metrics); expect(metrics.domContentLoaded).toBeLessThan(2000); expect(metrics.loadComplete).toBeLessThan(5000); }); ``` ## Best Practices 1. **Use test.step() for readability** -- Organize complex tests into logical steps. 2. **Implement custom fixtures** -- Share setup logic and state across tests. 3. **Enable trace on failure** -- Visual debugging is invaluable. 4. **Use auto-waiting locators** -- `getByRole` and `getByText` are resilient. 5. **Mock APIs for speed** -- Don't rely on real backends for fast feedback. 6. **Test across browsers** -- Cross-browser issues are real. 7. **Validate with screenshots** -- Visual regression catches CSS bugs. 8. **Isolate test data** -- Each test should create its own data. 9. **Use serial mode wisely** -- Parallelize when possible. 10. **Monitor performance** -- Track load times as part of tests. ## Anti-Patterns to Avoid 1. **Not using Page Object Model** -- Duplicating selectors everywhere. 2. **Hardcoded waits** -- Use auto-waiting instead. 3. **Testing in one browser only** -- Cross-browser bugs are common. 4. **Ignoring flaky tests** -- Fix them or delete them. 5. **Not using fixtures** -- Copy-pasting setup code. 6. **No visual regression** -- UI bugs slip through. 7. **Testing against production** -- Always use test environments. 8. **Shared state between tests** -- Tests must be independent. 9. **Not recording traces** -- Debugging is much harder. 10. **Ignoring mobile** -- Mobile users are a huge segment. Playwright is powerful. Use its advanced features to build robust, maintainable test suites that catch bugs before production.