Skip to content
SDETRole
SDETRole

Playwright interview question

What are web-first assertions, and why is expect(await locator.textContent()).toBe() an anti-pattern?

intermediate 2–5 yrs #assertions#web-first#expect#flaky-tests

Short answer

Web-first assertions such as expect(locator).toHaveText() take a locator and retry until the condition is met or the expect timeout (5 seconds by default) expires. Reading a value first with await locator.textContent() takes one snapshot and asserts immediately, so it fails on content that is still loading.

Answer

Web-first assertions are the async matchers on expect(locator) (and on expect(page)), such as toBeVisible, toHaveText and toHaveURL. They re-resolve the locator and re-check the condition repeatedly until it passes or the expect timeout expires (5 seconds by default). Reading a value yourself and then asserting on it uses a generic matcher on a single snapshot, so if the UI has not finished updating, the test fails even though it would have been correct a moment later.

The anti-pattern

// Snapshot: reads the text once, right now
expect(await page.getByTestId('cart-count').textContent()).toBe('3');

// Snapshot: isVisible() returns immediately and does not wait
expect(await page.getByText('Saved').isVisible()).toBe(true);

After clicking “Add to cart”, the count may update after an API call. The first assertion races that update; on a fast laptop it passes, in CI it fails sometimes. That is a textbook flaky test.

The web-first version

await expect(page.getByTestId('cart-count')).toHaveText('3');
await expect(page.getByText('Saved')).toBeVisible();

These retry until the text is 3 and the message is visible, up to the timeout. Note the await: web-first assertions are asynchronous, and forgetting await means the assertion is never really checked before the test moves on. The @typescript-eslint/no-floating-promises lint rule catches this.

Common matchers

await expect(page).toHaveURL(/\/orders\/\d+$/);
await expect(page).toHaveTitle('Checkout');
await expect(locator).toHaveText('Paid');          // full text (normalised whitespace); regex allowed
await expect(locator).toContainText('Paid');
await expect(locator).toHaveValue('qa@example.com');
await expect(locator).toBeEnabled();
await expect(locator).toBeChecked();
await expect(locator).toHaveAttribute('aria-expanded', 'true');
await expect(page.getByRole('listitem')).toHaveCount(5);
await expect(page.getByRole('listitem')).toHaveText(['Apples', 'Bananas', 'Cherries']);

toHaveText with an array checks the whole list in order, in one retrying assertion, which is much more reliable than looping over items.

Negation waits too

await expect(page.getByRole('progressbar')).not.toBeVisible();  // or toBeHidden()

This waits for the spinner to disappear, rather than checking once.

Timeouts

// playwright.config.ts
export default defineConfig({ expect: { timeout: 10_000 } });

// per assertion
await expect(page.getByText('Report ready')).toBeVisible({ timeout: 30_000 });

Raise the timeout for genuinely slow operations, not to paper over a wrong expectation.

Soft assertions

expect.soft(...) records a failure but lets the test continue, useful for checking several independent fields on one page and seeing all mismatches in one run. The test is still marked failed at the end.

Values that are not in the DOM

Generic matchers like toBe do not retry. If you need to wait for an API value or any computed result, use expect.poll:

await expect.poll(async () => {
  const res = await request.get('/api/orders/42');
  return (await res.json()).status;
}, { timeout: 15_000 }).toBe('SHIPPED');

or await expect(async () => { ... }).toPass() for a block of several assertions that should eventually all pass.

What interviewers look for

  • The retrying behaviour and the default timeout.
  • A clear explanation of why snapshot reads cause flakiness, with isVisible() as an example.
  • Fluency with the main matchers, including array toHaveText and negation.
  • Knowing expect.poll and toPass for non-DOM conditions.

A strong answer covers

  • Web-first assertions take a locator and retry until timeout
  • Snapshot reads like textContent/isVisible do not wait
  • Names common matchers: toBeVisible, toHaveText, toHaveURL, toHaveCount
  • Knows the default 5s expect timeout and how to configure it
  • Mentions soft assertions, negation or expect.poll/toPass for non-DOM values
Practise answering under time pressure

Type to search across SDETRole.

Full-text search by Pagefind · runs in your browser Open search page →