This episode digs into Playwright's default auto-waiting mechanism, explicit waits with waitForResponse and waitForLoadState, how to handle dynamic content and async updates, and strategies for keeping tests stable for SPA and real-time applications.

One of the biggest reasons end-to-end tests fail is timing issues: the element hasn't appeared when the action runs, or the response hasn't returned when the assertion checks. This episode 5 covers how Playwright solves this problem automatically, and when you need to add explicit time control.
Playwright's philosophy is simple: tests should never rely on guesswork timing. Every mechanism — auto-waiting, explicit waits, and retry-able assertions — is designed to replace the random sleep calls commonly used in older frameworks. After this episode, you'll know when to trust the defaults and when to take control.
When you call click() or fill(), Playwright runs a series of checks automatically:
Only when all conditions are met is the action executed. If not, Playwright waits until the time limit and gives you a descriptive timeout message.
The same applies to assertions. expect(page.getByRole('button')).toBeVisible() doesn't just check once — it waits until the condition is met or the expect.timeout limit is reached. This is what makes Playwright tests feel calm compared to frameworks that throw errors immediately.
When you need to wait for the response of a particular request, set up the listener before triggering the action:
import { test, expect } from '@playwright/test';
test('wait for the login response', async ({ page }) => {
const responsePromise = page.waitForResponse(
(response) => response.url().includes('/api/login') &&
response.status() === 200
);
await page.getByRole('button', { name: 'Sign in' }).click();
const response = await responsePromise;
expect(response.ok()).toBeTruthy();
});page.waitForResponse() returns a promise that resolves when the matching response arrives. The set the listener first, then act pattern avoids the classic race condition.
To wait for a page loading stage:
await page.goto('/products');
await page.waitForLoadState('networkidle');
await page.waitForLoadState('domcontentloaded');Keep in mind: networkidle is considered unstable for applications that poll continuously, because their network never truly goes idle.
page.waitForTimeout() is a last resort you should avoid:
await page.waitForTimeout(3000);Instead of guessing the time, wait for the condition that signals the content has appeared:
await expect(page.getByText('Payment successful')).toBeVisible();
await expect(page.locator('.spinner')).toHaveCount(0);Waiting for the resulting element — or for the loading element to disappear — is far more deterministic than guessing a duration. If even one condition can be observed, always choose that.
For cases outside of assertions, waitForSelector can still be used:
await page.waitForSelector('#product-data', { state: 'visible' });page.waitForSelector('#product-data', { state: 'visible' }) waits for an element with a specific state. In most cases, locator-based assertions are preferred because they verify at the same time — use waitForSelector for non-assertion logic.
SPA (Single Page Application) and real-time applications — chat, live dashboards, push notifications — trigger unique problems: the DOM changes constantly, the network never goes idle, and content appears asynchronously without a page reload. Tests that use sleep or rely on the load event will break easily.
Use the following patterns for dynamic applications:
npx playwright test --reporter=listnpx playwright test --reporter=list shows the duration of every test in order. Tests that frequently time out close to the limit are candidates for further synchronization investigation.
Episode 5 explained why Playwright rarely loses against timing: default auto-waiting ensures elements are actionable before an action, assertions are retry-able, and waitForResponse and waitForLoadState give you explicit control when needed. For dynamic content and real-time applications, the key is waiting for conditions, not guessing time.
Key takeaways:
waitForResponse before triggering an action to avoid race conditions.waitForTimeout with waiting for the resulting element or the loading element to disappear.networkidle on applications that poll continuously.In the next episode we'll discuss the page object model and test structure — organizing tests with the Page Object pattern, creating reusable page classes and helper methods, handling multi-page flows and shared fixtures, and keeping the test suite maintainable and readable.