15 Days of Playwright - Day 3

Smart Waits and Page States are essential concepts for writing reliable tests. In this lesson, I will explore how to use smart waits and manage page states effectively in Playwright tests.

15 Days of Playwright - Day 3
Jhonatas Matos

Jhonatas Matos

Smart Waits and Page States are essential concepts for writing reliable and non-flaky tests. In this lesson, I will explore how to use smart waits and manage page states effectively in Playwright tests.

Smart Waits

Smart waits ensure your tests wait for the right conditions BEFORE proceeding, adapting to your application's actual behavior instead of using fixed timeouts.

For example, you can wait for an element to be visible before interacting with it:

await page.locator('button').waitFor({ state: 'visible' });
await page.locator('button').click();

You can also wait for a specific text to appear on the page:

await page.locator('h1').waitFor({ state: 'attached' });
await expect(page.locator('h1')).toHaveText('Welcome to BugBank');

Page States

Managing page states is crucial for writing reliable tests. You need to ensure that your tests account for different states of the page, such as loading, error, and success states.

You can use Playwright's built-in methods to check the state of the page:

// wait for the `load` event to be fired.
await frame.waitForLoadState('load');

// wait for the `DOMContentLoaded` event to be fired.
await frame.waitForLoadState('domcontentloaded');

// Exists but not recommended - can be unpredictable in modern SPAs
await frame.waitForLoadState('networkidle');

By using smart waits and managing page states effectively, you can write more reliable and robust tests with Playwright.

Bad Practices

Using Fixed Timeouts: Avoid using fixed timeouts like await page.waitForTimeout(1000);. This can lead to flaky tests that fail intermittently.

Ignoring Page States: Always check the state of the page before interacting with elements. This helps prevent errors caused by elements not being ready.

Not Using Assertions: Make sure to use assertions to verify the state of the page after actions are performed. This ensures that your tests are validating the expected behavior.

Never use await page.waitForTimeout();

// ❌ Ant-pattern (NOT RECOMMENDED)
await page.waitForTimeout(5000); // Waits 5s ALWAYS, even if the element is already ready

Tests can be 60% slower if you use fixed timeouts instead of smart waits.
Random failures if the element takes longer than the timeout.

Ignore Auto-Waiting

Playwright already has automatic waits in interactions, but many override them:

// ❌ Redundant (actions like click() already have auto-wait)
await page.locator('button').waitFor({ state: 'visible' }); // Unnecessary
await page.locator('button').click();

// ✅ Better (DOC says that click() already waits)
await page.locator('button').click(); // Auto-wait included

Don't Wait for Navigation States

Interact before the page fully loads.

// ❌ Flaky (can fail if the page takes time to load)
await page.goto('https://bugbank.netlify.app');
await page.locator('form').fill('data'); // Risk: form not loaded yet

// ✅ Resilient (DOC recommends waitForLoadState)
await page.goto('https://bugbank.netlify.app');
await page.waitForLoadState('networkidle'); // Wait for requests to finish
await page.locator('form').fill('data');

Good Practices

There are some best practices to follow when using Playwright.

waitFor() for Dynamic Elements

  • Elements appear dynamically after actions
  • CSS animations end
  • Content loads via AJAX
// ✅ Good (wait for element to be visible)
await page.locator('button').waitFor({ state: 'visible' });
await page.locator('button').click();

// ✅ Good (wait for element to be visible)
await page.locator('#modal').waitFor(); 
await expect(page.locator('#modal')).toHaveText('Success!');

Wait for URLs/Redirects

  • Login redirects to dashboard
  • Forms submit and redirect
  • Navigation between pages
// ✅ Good (wait for URL to change)
await page.getByText('Login').click();
await page.waitForURL(/dashboard/);

Waiting for Hover-Activated Popups

When testing UI elements that appear on hover (like tooltips or menus), you need to:

  • Needs to check complex states (CSS + content)
  • Custom conditions not covered by built-in methods
  1. Trigger the hover action
  2. Wait for the popup to fully render
  3. Verify its contents

Example testing a tooltip:

test('Verify dynamic tooltip content', async ({ page }) => {
  // 1. Hover the target element
  await page.locator('.balance-indicator').hover();

  // 2. Wait for tooltip to be fully interactive
  await page.waitForFunction(() => {
    const tooltip = document.querySelector('.tooltip');
    if (!tooltip) return false;
    
    // Check both visibility AND content
    const isVisible = tooltip.style.opacity === '1' && 
                     tooltip.style.visibility === 'visible';
    const hasContent = tooltip.textContent.includes('Updated balance');
    
    return isVisible && hasContent;
  }, { timeout: 5000 }); // 5 second timeout

  // 3. Additional assertions (optional)
  await expect(page.locator('.tooltip')).toHaveCSS('opacity', '1');
});

Bad practices vs Good Practices

There are some common pitfalls to avoid when using Playwright, as well as best practices to follow.

ScenarioBad Practice (Flaky)Good Practice (Official DOC)Why It Matters
Dynamic ElementwaitForTimeout(5000)locator.waitFor()Adapts to actual load time instead of fixed delay
Page NavigationNo explicit waitpage.waitForURL()Ensures page fully loaded before interactions
Slow APIFixed waitwaitForResponse()Waits specifically for API calls to complete
Element StateManual retriesNative auto-wait (click())Built-in waiting makes tests more reliable

Follow the update in the code in the link below to see the complete code for this lesson. If the repository is useful, please give it a star!

Link to GitHub projectGitHub Octocat

Conclusion

Forget waitForTimeout() !

Mastering smart waits is the difference between flaky tests and reliable tests. Remember:

  • NEVER use waitForTimeout() - it is the main cause of flakiness
  • Trust Playwright's native auto-waiting
  • Use explicit waits only for complex cases
  • Combine methods to cover all scenarios

Thank you for reading and see you in the next lesson! ☕💜