How to Handle Dynamic Waits in Selenium and Playwright
Practice dynamic waits — waitForSelector, explicit waits, polling, network idle, toast messages, skeleton loaders, and async content in Playwright, Selenium & Cypress.
Interactive Scenarios
Button becomes enabled 3 seconds after arming. Wait for state: 'enabled'.
Locate via: //div[@data-testid="dw-status-panel"]//span[contains(@class,"status-value")]
Element appears at a random delay (0.5–3.5 s) and vanishes after 800 ms. Catch it with waitForSelector.
| ID | Scenario | Type | Priority | |
|---|---|---|---|---|
| DW_001 | Verify waitForSelector resolves when delayed element appears Expected: Element is visible within the wait timeout. ✅ positivehigh | positive | high | |
| DW_002 | Verify waitForSelector times out when element never appears Expected: A TimeoutError is thrown after the configured timeout. ❌ negativehigh | negative | high | |
| DW_003 | Verify spinner appears immediately after trigger click Expected: dw-spinner is visible before the data loads. ✅ positivemedium | positive | medium | |
| DW_004 | Verify spinner disappears and content appears after loading Expected: dw-spinner hidden; dw-spinner-content visible. ✅ positivehigh | positive | high | |
| DW_005 | Verify toast message appears and contains expected text Expected: dw-toast is visible with correct message text. ✅ positivehigh | positive | high | |
| DW_006 | Verify toast auto-dismisses within 4 seconds Expected: dw-toast is no longer in DOM after dismiss delay. ✅ positivemedium | positive | medium | |
| DW_007 | Verify waitForFunction resolves when counter reaches target Expected: waitForFunction returns when dw-poll-count text equals '5'. ✅ positivehigh | positive | high | |
| DW_008 | Verify disabled button becomes enabled after delay Expected: dw-submit-btn transitions from disabled to enabled. ✅ positivehigh | positive | high | |
| DW_009 | Verify text change is detected via waitForFunction Expected: Status span text changes from 'Idle' to 'Done'. ✅ positivemedium | positive | medium | |
| DW_010 | Verify simulated fetch result appears after network wait Expected: dw-fetch-result is visible with non-empty text. ✅ positivemedium | positive | medium | |
| DW_011 | Verify race-condition element can be caught within timeout Expected: Element is found before it disappears when timeout is sufficient. ✅ positivemedium | positive | medium | |
| DW_012 | Verify Cypress timeout override works for slow elements Expected: .should() assertion passes when custom timeout is large enough. ✅ positivemedium | positive | medium | |
| DW_013 | Verify Selenium WebDriverWait with custom polling interval Expected: Element is found without excessive polling overhead. ✅ positivelow | positive | low | |
| DW_014 | Verify no hard sleep (Thread.sleep / page.waitForTimeout) is used Expected: Tests complete faster using condition-based waits rather than fixed delays. ⚠️ edgelow | edge | low |
Dynamic waits pause test execution until a specific condition becomes true rather than sleeping for a fixed duration. They are faster than hard-coded sleeps and far more reliable in CI environments where timing is unpredictable. Every modern framework provides condition-based waiting — use it.
waitForSelector waits until an element matching the selector is in the given state: 'attached', 'detached', 'visible', or 'hidden'. It is the primary wait primitive in Playwright and works for both appearing and disappearing elements.
// Wait for element to appear
await page.waitForSelector('[data-testid="dw-delayed-result"]', {
state: 'visible',
timeout: 5000,
});
// Wait for spinner to disappear
await page.waitForSelector('[data-testid="dw-spinner"]', {
state: 'hidden',
});
// Using locator waitFor
await page.getByTestId('dw-delayed-result').waitFor({ state: 'visible' });waitForFunction evaluates a JavaScript expression in the page context repeatedly until it returns a truthy value. It is ideal for asserting that text content matches a specific value, a counter reaches a threshold, or any computed DOM state becomes true.
// Wait until counter text equals "5"
await page.waitForFunction(() => {
const el = document.querySelector('[data-testid="dw-poll-count"]');
return el?.textContent?.trim() === '5';
});
// Wait until button is no longer disabled
await page.waitForFunction(() => {
const btn = document.querySelector('[data-testid="dw-submit-btn"]') as HTMLButtonElement;
return btn && !btn.disabled;
});Individual Playwright locators expose a waitFor({ state }) method that waits for that specific locator to reach a given state: visible, hidden, attached, or detached. For waiting until a button is clickable, use waitFor({ state: 'visible' }) combined with checking isEnabled().
// Wait for button to become enabled
const btn = page.getByTestId('dw-submit-btn');
await btn.waitFor({ state: 'visible' });
await expect(btn).toBeEnabled();
await btn.click();
// Wait for toast to appear then vanish
const toast = page.getByTestId('dw-toast');
await toast.waitFor({ state: 'visible' });
await expect(toast).toContainText('Success');
await toast.waitFor({ state: 'hidden' });In Selenium, WebDriverWait combined with ExpectedConditions is the standard pattern. Common conditions include visibilityOfElementLocated, invisibilityOf, elementToBeClickable, and textToBePresentInElement. Always set a reasonable timeout and optionally a polling interval.
// Playwright equivalent of Selenium's polling wait
await expect(page.getByTestId('dw-poll-count')).toHaveText('5', {
timeout: 10_000,
});
// With custom polling (via waitForFunction)
await page.waitForFunction(
() => document.querySelector('[data-testid="dw-poll-count"]')?.textContent === '5',
{ polling: 500, timeout: 10_000 },
);| Action | Selenium | Playwright JS | Playwright PY | Cypress |
|---|---|---|---|---|
| Wait for visible | visibilityOfElementLocated() | waitForSelector({ state: 'visible' }) | wait_for_selector(state='visible') | .should('be.visible') |
| Wait for hidden | invisibilityOf(element) | waitForSelector({ state: 'hidden' }) | wait_for_selector(state='hidden') | .should('not.exist') |
| Wait for enabled | elementToBeClickable() | waitFor({ state: 'visible' }) + toBeEnabled() | wait_for(state='visible') + to_be_enabled() | .should('not.be.disabled') |
| Wait for text | textToBePresentInElement() | waitForFunction(() => el.textContent === 'x') | wait_for_function('...') | .should('have.text', 'x') |
| Custom polling | FluentWait.pollingEvery() | waitForFunction({ polling: N }) | wait_for_function(polling=N) | { timeout: N } option |