Test Organization
When to Use
Use
describeto group related tests,tagsfor selective runs,test.step()to annotate long flows in the trace, and fixtures over hooks whenever the setup produces state.
Decision
| Concurrency mode | When |
|---|---|
fullyParallel: true (global default) |
Independent tests — the normal case |
test.describe.serial |
Multi-step wizard where step N depends on step N-1 state |
test.describe.parallel |
Override parallel on a file that would otherwise be serial |
Pattern
// Grouping
test.describe('checkout flow', () => { /* sequential within file by default */ });
test.describe.serial('multi-step wizard', () => { /* first failure skips rest; retries whole group */ });
test.describe.parallel('independent CRUD', () => { /* force parallel within file */ });
test.describe.skip('flaky vendor area, ticket #1234', () => { /* ... */ });
Hooks
test.beforeAll(async ({ browser }) => { /* once per worker */ });
test.afterAll(async () => { /* once per worker */ });
test.beforeEach(async ({ page }) => { /* before every test */ });
test.afterEach(async ({ page }, testInfo) => { /* after every test */ });
Skipping and conditional execution
test('only on chromium', async ({ browserName }) => {
test.skip(browserName !== 'chromium', 'WebKit lacks feature X');
});
test.fixme('broken, see #4567', async () => { /* not executed, marked failing-known */ });
test.slow(); // multiplies timeouts by 3 for this test
Tags and annotations
// Tags — for grep-based filtering
test('homepage smoke', { tag: ['@smoke', '@critical'] }, async ({ page }) => { /* ... */ });
// npx playwright test --grep @smoke
// Annotations — surface in HTML report
test('checkout', {
annotation: [{ type: 'issue', description: 'https://jira/PROJ-123' }],
}, async ({ page }) => { /* ... */ });
Parameterization
const roles = ['admin', 'editor', 'anonymous'];
for (const role of roles) {
test.describe(`as ${role}`, () => {
test.use({ storageState: `playwright/.auth/${role}.json` });
test('can view homepage', async ({ page }) => { /* ... */ });
});
}
test.step — nesting actions in the trace
test('purchase', async ({ page }) => {
await test.step('login', async () => {
await page.goto('/login');
await page.getByLabel('Email').fill('a@b');
});
await test.step('add to cart', async () => { /* ... */ });
await test.step('checkout', async () => { /* ... */ });
});
Common Mistakes
- Wrong:
test.describe.serialfor "convenience" — hides test coupling. Right: Refactor to fixtures - Wrong:
test.onlycommitted — CI doesn't catch it withoutforbidOnly: true - Wrong: Test files >500 lines. Right: Split per user journey
See Also
- Fixtures — typed, composable alternative to
beforeEachfor state - Debugging —
test.stepentries in Trace Viewer - CI Patterns —
forbidOnly,fullyParallel, retries config