Qaskills Playwright Enhanced
Advanced Playwright automation with auto-detection, custom fixtures, trace debugging, visual testing, mobile emulation, and production-grade test architecture.
install
source · Clone the upstream repo
git clone https://github.com/PramodDutta/qaskills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/PramodDutta/qaskills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/seed-skills/playwright-skill-enhanced" ~/.claude/skills/pramoddutta-qaskills-playwright-enhanced && rm -rf "$T"
manifest:
seed-skills/playwright-skill-enhanced/SKILL.mdsource content
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
// 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
// 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<MyFixtures>({ 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
// 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<void> { await this.page.goto(path); } async waitForPageLoad(): Promise<void> { await this.page.waitForLoadState('networkidle'); } async waitForElement(selector: string, timeout = 10000): Promise<Locator> { return this.page.waitForSelector(selector, { timeout }); } async takeScreenshot(name: string): Promise<void> { await this.page.screenshot({ path: `screenshots/${name}.png`, fullPage: true, }); } async scrollToElement(locator: Locator): Promise<void> { await locator.scrollIntoViewIfNeeded(); } async typeWithDelay(locator: Locator, text: string, delay = 100): Promise<void> { await locator.type(text, { delay }); } async selectDropdownOption(locator: Locator, option: string): Promise<void> { await locator.click(); await this.page.getByRole('option', { name: option }).click(); } async uploadFile(locator: Locator, filePath: string): Promise<void> { await locator.setInputFiles(filePath); } async waitForApiResponse(urlPattern: string | RegExp): Promise<void> { await this.page.waitForResponse((response) => (typeof urlPattern === 'string' ? response.url().includes(urlPattern) : urlPattern.test(response.url())) && response.status() === 200 ); } }
// 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<void> { await this.navigate('/dashboard'); await this.expectToBeLoaded(); } async expectToBeLoaded(): Promise<void> { await expect(this.heading).toBeVisible(); await this.waitForPageLoad(); } async searchFor(query: string): Promise<void> { await this.searchInput.fill(query); await this.searchInput.press('Enter'); await this.waitForApiResponse('/api/search'); } async openUserMenu(): Promise<void> { await this.userMenu.click(); } async getNotificationCount(): Promise<number> { const badge = this.notificationBell.locator('.badge'); const text = await badge.textContent(); return parseInt(text || '0', 10); } async expectWelcomeMessage(username: string): Promise<void> { const message = this.page.getByText(`Welcome, ${username}`); await expect(message).toBeVisible(); } }
API Mocking Strategies
// 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<void> { 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<void> { 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<void> { 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<void> { 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
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
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
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
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
// 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
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
- Use test.step() for readability -- Organize complex tests into logical steps.
- Implement custom fixtures -- Share setup logic and state across tests.
- Enable trace on failure -- Visual debugging is invaluable.
- Use auto-waiting locators --
andgetByRole
are resilient.getByText - Mock APIs for speed -- Don't rely on real backends for fast feedback.
- Test across browsers -- Cross-browser issues are real.
- Validate with screenshots -- Visual regression catches CSS bugs.
- Isolate test data -- Each test should create its own data.
- Use serial mode wisely -- Parallelize when possible.
- Monitor performance -- Track load times as part of tests.
Anti-Patterns to Avoid
- Not using Page Object Model -- Duplicating selectors everywhere.
- Hardcoded waits -- Use auto-waiting instead.
- Testing in one browser only -- Cross-browser bugs are common.
- Ignoring flaky tests -- Fix them or delete them.
- Not using fixtures -- Copy-pasting setup code.
- No visual regression -- UI bugs slip through.
- Testing against production -- Always use test environments.
- Shared state between tests -- Tests must be independent.
- Not recording traces -- Debugging is much harder.
- 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.