.agents/skills/playwright-best-practices/core/annotations.md
// Skip unconditionally
test.skip('feature not implemented', async ({page}) => {
// This test won't run
})
// Skip with reason
test('payment flow', async ({page}) => {
test.skip(true, 'Payment gateway in maintenance')
// Test body won't execute
})
test('webkit-specific feature', async ({page, browserName}) => {
test.skip(browserName !== 'webkit', 'This feature only works in WebKit')
await page.goto('/webkit-feature')
})
test('production only', async ({page}) => {
test.skip(process.env.ENV !== 'production', 'Only runs against production')
await page.goto('/prod-feature')
})
test('windows-specific', async ({page}) => {
test.skip(process.platform !== 'win32', 'Windows only')
})
test('not on CI', async ({page}) => {
test.skip(!!process.env.CI, 'Skipped in CI environment')
})
test.describe('Admin features', () => {
test.skip(({browserName}) => browserName === 'firefox', 'Firefox admin bug')
test('admin dashboard', async ({page}) => {
// Skipped in Firefox
})
test('admin settings', async ({page}) => {
// Skipped in Firefox
})
})
// Mark test as needing fix (skips the test)
test.fixme('broken after refactor', async ({page}) => {
// Test won't run but is tracked
})
// Conditional fixme
test('flaky on CI', async ({page}) => {
test.fixme(!!process.env.CI, 'Investigate CI flakiness - ticket #123')
await page.goto('/flaky-feature')
})
// Test is expected to fail (runs but expects failure)
test('known bug', async ({page}) => {
test.fail()
await page.goto('/buggy-page')
// If this passes, the test fails (bug was fixed!)
await expect(page.getByText('Working')).toBeVisible()
})
// Conditional fail
test('fails on webkit', async ({page, browserName}) => {
test.fail(browserName === 'webkit', 'WebKit rendering bug #456')
await page.goto('/render-test')
await expect(page.getByTestId('element')).toHaveCSS('width', '100px')
})
| Annotation | Runs? | Use Case |
|---|---|---|
test.skip() | No | Feature not applicable |
test.fixme() | No | Known bug, needs investigation |
test.fail() | Yes | Expected to fail, tracking a bug |
// Triple the default timeout
test('large data import', async ({page}) => {
test.slow()
await page.goto('/import')
await page.setInputFiles('#file', 'large-file.csv')
await page.getByRole('button', {name: 'Import'}).click()
await expect(page.getByText('Import complete')).toBeVisible()
})
// Conditional slow
test('video processing', async ({page, browserName}) => {
test.slow(browserName === 'webkit', 'WebKit video processing is slow')
await page.goto('/video-editor')
})
test('very long operation', async ({page}) => {
// Set specific timeout (in milliseconds)
test.setTimeout(120000) // 2 minutes
await page.goto('/long-operation')
})
// Timeout for describe block
test.describe('Integration tests', () => {
test.describe.configure({timeout: 60000})
test('test 1', async ({page}) => {
// Has 60 second timeout
})
})
test('checkout flow', async ({page}) => {
await test.step('Add item to cart', async () => {
await page.goto('/products')
await page.getByRole('button', {name: 'Add to Cart'}).click()
})
await test.step('Go to checkout', async () => {
await page.getByRole('link', {name: 'Cart'}).click()
await page.getByRole('button', {name: 'Checkout'}).click()
})
await test.step('Fill shipping info', async () => {
await page.getByLabel('Address').fill('123 Test St')
await page.getByLabel('City').fill('Test City')
})
await test.step('Complete payment', async () => {
await page.getByLabel('Card').fill('4242424242424242')
await page.getByRole('button', {name: 'Pay'}).click()
})
await expect(page.getByText('Order confirmed')).toBeVisible()
})
test('user registration', async ({page}) => {
await test.step('Fill registration form', async () => {
await page.goto('/register')
await test.step('Personal info', async () => {
await page.getByLabel('Name').fill('John Doe')
await page.getByLabel('Email').fill('[email protected]')
})
await test.step('Security', async () => {
await page.getByLabel('Password').fill('SecurePass123')
await page.getByLabel('Confirm Password').fill('SecurePass123')
})
})
await test.step('Submit and verify', async () => {
await page.getByRole('button', {name: 'Register'}).click()
await expect(page.getByText('Welcome')).toBeVisible()
})
})
test('verify order', async ({page}) => {
const orderId = await test.step('Create order', async () => {
await page.goto('/checkout')
await page.getByRole('button', {name: 'Place Order'}).click()
// Return value from step
return await page.getByTestId('order-id').textContent()
})
await test.step('Verify order details', async () => {
await page.goto(`/orders/${orderId}`)
await expect(page.getByText(`Order #${orderId}`)).toBeVisible()
})
})
// pages/checkout.page.ts
export class CheckoutPage {
async fillShippingInfo(address: string, city: string) {
await test.step('Fill shipping information', async () => {
await this.page.getByLabel('Address').fill(address)
await this.page.getByLabel('City').fill(city)
})
}
async completePayment(cardNumber: string) {
await test.step('Complete payment', async () => {
await this.page.getByLabel('Card').fill(cardNumber)
await this.page.getByRole('button', {name: 'Pay'}).click()
})
}
}
test('important feature', async ({page}, testInfo) => {
// Add custom annotation
testInfo.annotations.push({
type: 'priority',
description: 'high',
})
testInfo.annotations.push({
type: 'ticket',
description: 'JIRA-123',
})
await page.goto('/feature')
})
// fixtures/annotations.fixture.ts
import {test as base, TestInfo} from '@playwright/test'
type AnnotationFixtures = {
annotate: {
ticket: (id: string) => void
priority: (level: 'low' | 'medium' | 'high') => void
owner: (name: string) => void
}
}
export const test = base.extend<AnnotationFixtures>({
annotate: async ({}, use, testInfo) => {
await use({
ticket: (id) => {
testInfo.annotations.push({type: 'ticket', description: id})
},
priority: (level) => {
testInfo.annotations.push({type: 'priority', description: level})
},
owner: (name) => {
testInfo.annotations.push({type: 'owner', description: name})
},
})
},
})
// Usage
test('critical feature', async ({page, annotate}) => {
annotate.ticket('JIRA-456')
annotate.priority('high')
annotate.owner('Alice')
await page.goto('/critical')
})
// reporters/annotation-reporter.ts
import {Reporter, TestCase, TestResult} from '@playwright/test/reporter'
class AnnotationReporter implements Reporter {
onTestEnd(test: TestCase, result: TestResult) {
const ticket = test.annotations.find((a) => a.type === 'ticket')
const priority = test.annotations.find((a) => a.type === 'priority')
if (ticket) {
console.log(`Test linked to: ${ticket.description}`)
}
if (priority?.description === 'high' && result.status === 'failed') {
console.log(`HIGH PRIORITY FAILURE: ${test.title}`)
}
}
}
export default AnnotationReporter
// helpers/test-annotations.ts
import {test} from '@playwright/test'
export function skipInCI(reason = 'Skipped in CI') {
test.skip(!!process.env.CI, reason)
}
export function skipInBrowser(browser: string, reason: string) {
test.beforeEach(({browserName}) => {
test.skip(browserName === browser, reason)
})
}
export function onlyInEnv(env: string) {
test.skip(process.env.ENV !== env, `Only runs in ${env}`)
}
// tests/feature.spec.ts
import {skipInCI, onlyInEnv} from '../helpers/test-annotations'
test('local only feature', async ({page}) => {
skipInCI('Uses local resources')
await page.goto('/local-feature')
})
test('production check', async ({page}) => {
onlyInEnv('production')
await page.goto('/prod-only')
})
test.describe('Mobile features', () => {
test.beforeEach(({isMobile}) => {
test.skip(!isMobile, 'Mobile only tests')
})
test('touch gestures', async ({page}) => {
// Only runs on mobile
})
})
test.describe('Desktop features', () => {
test.beforeEach(({isMobile}) => {
test.skip(isMobile, 'Desktop only tests')
})
test('hover interactions', async ({page}) => {
// Only runs on desktop
})
})
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Skipping without reason | Hard to track why | Always provide description |
| Too many skipped tests | Test debt accumulates | Review and clean up regularly |
| Using skip instead of fixme | Loses intent | Use fixme for bugs, skip for N/A |
| Not using steps | Hard to debug failures | Group logical actions in steps |
--grep