Locators And Waits
1 min readRapid overview
Locators + Waiting (Best Practices)
The goal: stable selectors + web-first assertions. If you have to “sleep”, something is missing in the test design.
Core principles
Trust Playwright’s auto-waiting
// GOOD: actions auto-wait for actionability
await page.getByTestId('submit').click();
// BAD: redundant manual waits (often hides real problems)
await page.waitForSelector('[data-testid="submit"]');
await page.click('[data-testid="submit"]');
Prefer web-first assertions
// GOOD: retries until timeout
await expect(page.getByTestId('success')).toBeVisible();
// BAD: single snapshot check
expect(await page.getByTestId('success').isVisible()).toBe(true);
Locator strategy (priority order)
getByTestId/[data-testid="..."](fast + stable)getByRole(semantic + accessibility-friendly)getByLabel,getByPlaceholder(forms)getByText(only if unique and stable)- Complex CSS selectors (last resort)
Recommended:
page.getByTestId('login-button')
page.getByRole('button', { name: 'Sign in' })
page.getByLabel('Email')
Avoid:
// brittle: DOM structure coupling
page.locator('.form > div:nth-child(2) button.primary')
// brittle: index-based selection
page.locator('button').nth(2)
What to wait for (instead of sleeping)
| Scenario | Best wait |
|---|---|
| element appears | await expect(locator).toBeVisible() |
| element disappears | await expect(locator).not.toBeVisible() |
| text changes | await expect(locator).toHaveText(...) |
| URL changes | await expect(page).toHaveURL(...) |
| request finishes | await page.waitForResponse(...) |
Parallel wait pattern
await Promise.all([
page.waitForURL(/\\/dashboard/),
page.getByTestId('login-submit').click(),
]);
Navigation
Fast default:
await page.goto('/dashboard', { waitUntil: 'commit' });
await expect(page.getByTestId('dashboard-root')).toBeVisible();
Avoid networkidle unless you fully understand why it’s needed.
Anti-patterns
page.waitForTimeout(...)waitUntil: 'networkidle'as a default navigation policy.or(...)locator chains (slow + hides selector problems)- “check then assert” (double work):
if (await locator.isVisible()) expect(locator).toBeVisible()