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.


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
- Trigger the hover action
- Wait for the popup to fully render
- 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.
Scenario | Bad Practice (Flaky) | Good Practice (Official DOC) | Why It Matters |
---|---|---|---|
Dynamic Element | waitForTimeout(5000) | locator.waitFor() | Adapts to actual load time instead of fixed delay |
Page Navigation | No explicit wait | page.waitForURL() | Ensures page fully loaded before interactions |
Slow API | Fixed wait | waitForResponse() | Waits specifically for API calls to complete |
Element State | Manual retries | Native 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 project
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! ☕💜