From harness-claude
Implements maintainable Playwright E2E tests using page objects, fixtures, accessible locators, user flows, auto-waiting, and dialog handling for reliable testing.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Write maintainable Playwright tests using page objects, fixtures, and parallel execution
Writes E2E tests with Playwright, creates page objects, configures fixtures and reporters, mocks APIs, integrates with CI, debugs flaky tests, and sets up visual regression testing.
Writes Playwright E2E tests using POM and role selectors, configures fixtures and CI, debugs flakiness, mocks APIs, and runs visual regression testing.
Provides Playwright E2E testing best practices: file organization, Page Object Model, accessible locators, authentication with storage state.
Share bugs, ideas, or general feedback.
Write maintainable Playwright tests using page objects, fixtures, and parallel execution
// e2e/pages/login-page.ts
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
constructor(private page: Page) {
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Log in' });
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
}
// Best — accessible
page.getByRole('button', { name: 'Submit' });
page.getByLabel('Email address');
page.getByText('Welcome back');
page.getByPlaceholder('Search...');
// Acceptable — test IDs
page.getByTestId('user-avatar');
// Avoid — brittle
page.locator('.btn-primary');
page.locator('#submit-form');
page.locator('div > span:nth-child(2)');
import { test, expect } from '@playwright/test';
test('user can create and publish a post', async ({ page }) => {
await page.goto('/posts/new');
await page.getByLabel('Title').fill('My First Post');
await page.getByLabel('Content').fill('Hello, world!');
await page.getByRole('button', { name: 'Save draft' }).click();
await expect(page.getByText('Draft saved')).toBeVisible();
await page.getByRole('button', { name: 'Publish' }).click();
await expect(page).toHaveURL(/\/posts\/[\w-]+$/);
await expect(page.getByRole('heading', { name: 'My First Post' })).toBeVisible();
});
// Playwright auto-waits for elements to be actionable
await page.getByRole('button', { name: 'Submit' }).click(); // Waits for button to be enabled
// Wait for navigation
await Promise.all([
page.waitForURL('/dashboard'),
page.getByRole('button', { name: 'Log in' }).click(),
]);
// Wait for API response
const responsePromise = page.waitForResponse('/api/users');
await page.getByRole('button', { name: 'Load users' }).click();
const response = await responsePromise;
expect(response.status()).toBe(200);
page.on('dialog', (dialog) => dialog.accept());
await page.getByRole('button', { name: 'Delete' }).click();
await expect(page.getByText('Item deleted')).toBeVisible();
test('homepage matches snapshot', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixels: 100,
});
});
// Each test gets its own browser context
test('user A sees their data', async ({ page }) => {
/* ... */
});
test('user B sees their data', async ({ page }) => {
/* ... */
});
// These run in parallel without interference
test('critical: checkout flow', { tag: '@critical' }, async ({ page }) => {
// ...
});
// Run only critical tests
// npx playwright test --grep @critical
Playwright tests run against real browsers with full DOM, network, and JavaScript execution. They are the closest thing to manual testing that can be automated.
Auto-waiting: Playwright automatically waits for elements to be visible, enabled, and stable before interacting. This eliminates most explicit waits and reduces flakiness compared to Selenium or Cypress.
Locator best practices:
getByRole — always first choice. Forces accessible markupgetByLabel — for form inputs, anchored to their labelgetByText — for visible text contentgetByTestId — when no accessible name exists. Add data-testid attributesDebugging tools:
npx playwright test --ui — interactive test runner with time-travel debuggingnpx playwright test --debug — step through with browser DevToolsnpx playwright show-trace trace.zip — replay recorded tracesHandling flakiness:
retries: 2 in CI as a safety net, but investigate repeated flakesTrade-offs:
https://playwright.dev/docs/test-patterns