E2E Playwright
Selector Priority
getByRole('button', { name: 'Submit' })— best for accessibilitygetByLabelText('Email')— formsgetByTestId('submit-btn')— adddata-testidwhen neededgetByPlaceholderText,getByTitle— fallbacks- Avoid: deep CSS,
:nth-child, XPath for layout
Waiting
- Use Playwright auto-waiting; avoid
page.waitForTimeout() - Prefer:
expect(locator).toBeVisible(),locator.click()(auto-waits) - For network:
page.waitForResponse(),page.waitForRequest()
Page Object Pattern
// pages/LoginPage.ts
export class LoginPage {
constructor(private page: Page) {}
email = this.page.getByLabel('Email');
password = this.page.getByLabel('Password');
submit = this.page.getByRole('button', { name: 'Sign in' });
async login(email: string, password: string) {
await this.email.fill(email);
await this.password.fill(password);
await this.submit.click();
}
}
Test Structure
test.describe('Checkout flow', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
// Auth if needed
});
test('completes purchase', async ({ page }) => {
const cart = new CartPage(page);
await cart.addItem('Product A');
await cart.goToCheckout();
// ...
});
});
Config (playwright.config.ts)
retries: 1for CI flakinesstrace: 'on-first-retry'for debuggingscreenshot: 'only-on-failure'- Parallel workers for speed
Isolation
- Each test independent; no shared mutable state
- Seed/reset data in
beforeEachif needed - Use unique test data (timestamps, UUIDs) to avoid conflicts
Source: iamjcabalejo/payoys-cursor-sub-agents — distributed by TomeVault.