Modern front-ends often load data asynchronously, animate elements, or replace parts of the DOM on interaction. Your selectors must work with this dynamic behaviour rather than fighting it.
Working with Dynamic and Async UI
Playwrightβs auto-waiting and locator model already handle many async cases, but you still need to design selectors that target stable anchors in the UI. Combining good selectors with appropriate expectations avoids flaky tests.
// dynamic-async-ui.spec.ts
import { test, expect } from '@playwright/test';
test('loads search results dynamically', async ({ page }) => {
await page.goto('https://demo.myshop.com/search');
await page.getByRole('textbox', { name: 'Search products' }).fill('headphones');
await page.getByRole('button', { name: 'Search' }).click();
const results = page.getByTestId('search-results');
await expect(results).toBeVisible();
await expect(results.getByRole('article')).toHaveCountGreaterThan(0);
});
toBeVisible and toHaveText will automatically wait up to the configured timeout for the condition to become true.waitForTimeout calls to “fix” flakiness usually hides deeper problems; rely on conditions and locators instead.If elements appear only after network calls complete, you can also assert on network behaviour or use page.waitForResponse combined with selectors to check that UI and API results are aligned.
Common Mistakes
Mistake 1 β Using fixed sleeps instead of conditions
This wastes time and is unreliable.
β Wrong: Calling page.waitForTimeout(5000) after every action.
β Correct: Wait for specific selectors or expectations that represent readiness.
Mistake 2 β Selecting transient elements instead of stable containers
Animations can break these tests.
β Wrong: Targeting temporary loading spinners as your main selector.
β Correct: Use containers or final content as the primary target for assertions.