In my journey through web automation and testing, I’ve faced the unpredictability of slow-loading pages, dynamic content, and complex user interactions. These challenges led to frustrating issues like timeouts and race conditions, making tests feel unreliable and hard to maintain. Through trial and error, I found Playwright to be a powerful ally in addressing these challenges. By embracing asynchronous methods, Playwright offered solutions to some of the biggest headaches in web & mobile testing.

In this blog, I’ll share my experiences using synchronous and asynchronous approaches, focusing on how modern frameworks like Playwright tackle common issues in a more reliable way. While there are other frameworks that also use asynchronous approaches, I’ll be focusing specifically on how Playwright addresses these challenges.

Synchronous Methods: The Old Way

When I first started, synchronous programming was my go-to approach. Each line of code would wait for the previous one to finish, keeping things simple and sequential. However, this straightforward approach came with its own set of problems in web automation:

  1. Timeouts: When a page loads slower than expected, the next line of code might fail since the page is not ready.
  2. Race Conditions: If elements appear on a page in an unpredictable order, synchronous code may attempt to interact with elements before they’re available.

Let’s examine a hypothetical synchronous code example:

// HYPOTHETICAL: This is NOT actual Playwright code and will not work

// Import 'chromium' from Playwright, define 'runTest' to launch a Chromium browser and create a new page synchronously.
const { chromium } = require('playwright');
function runTest() {
const browser = chromium.launchSync();
const page = browser.newPageSync();

// Navigate to the Playwright website
page.gotoSync('<https://playwright.dev/>');

// Click on the "Get started" button
page.clickSync('a:text("Get started")');

// Wait for the new page to load and get its title
page.waitForSelectorSync('h1:text("Installation")');
const result = page.textContentSync('h1');
console.log('Page title:', result);

// Click on the "Node.js" tab in the installation instructions
page.clickSync('button:text("Node.js")');

// Get the installation command
const installCommand = page.textContentSync('code:has-text("npm init playwright@latest")');
console.log('Installation command:', installCommand);
browser.closeSync();
}
runTest();

This code assumes each operation completes instantly. In practice, this approach often fails when pages load slowly or elements load in unpredictable sequences, leading to timeout and race conditions.

Asynchronous Methods: Playwright’s Solution

To overcome these limitations, I shifted to using Playwright’s asynchronous programming approach, which brought a new level of stability and reliability to my tests. Asynchronous code lets each operation occur independently, allowing for conditions to be met without blocking the execution flow. Here’s how I’ve found it solves common issues:

  1. Timeouts: Asynchronous methods can wait for elements or conditions, automatically retrying until a timeout is reached.
  2. Race Conditions: By waiting for specific elements or states, asynchronous code ensures operations occur in the correct order, regardless of load times.

Below is a practical asynchronous code example I’ve used to address timeouts and race conditions in Playwright:

// Import 'chromium' from Playwright, define an async IIFE to launch a Chromium browser and create a new page.
const { chromium } = require('playwright');

(async () => {
  try {
    const browser = await chromium.launch();
    const page = await browser.newPage();

    // Navigate to the Playwright website
    await page.goto('<https://playwright.dev/>');

    // Click on the "Get started" button
    await page.click('a:text("Get started")');

    // Wait for the new page to load and get its title
    await page.waitForSelector('h1:text("Installation")');
    const result = await page.textContent('h1');
    console.log('Page title:', result);

    // Click on the "Node.js" tab in the installation instructions
    await page.click('button:text("Node.js")');

    // Get the installation command
    const installCommand = await page.textContent('code:has-text("npm init playwright@latest")');
    console.log('Installation command:', installCommand);

    await browser.close();
  } catch (error) {
    console.error('An error occurred:', error);
  }
})();

In this asynchronous version:

  • page.goto()waits for the page to load before proceeding, preventing the race conditions.
  • waitForSelector() ensures elements are present before interacting with them.
  • Waits for content to be available before extracting page.textContent().
  • Handles dynamic content loading with explicit waits.

By leveraging asynchronous methods, I found that Playwright allowed my tests to handle varying load times and dynamic content more effectively, significantly reducing timeouts and eliminating race conditions in my projects.

In the following sections, I’ll share real-world scenarios from my experience that showcase how Playwright’s asynchronous approach revolutionized my web automation efforts. These examples demonstrate how this method significantly improved the reliability and robustness of my tests. Whether you’re just starting with Playwright or looking to enhance existing test suites, understanding the power of asynchronous methods is key to mastering modern automation. Let’s dive into the asynchronous world of Playwright and see how it can revolutionize your testing experience, turning those flaky tests into dependable safeguards for your web and mobile applications

Navigating Timeouts and Race Conditions: Real-Life Use Cases in Playwright

Scenario 1 : E-commerce Product Search

Challenge: Slow-loading Search Results

Imagine an e-commerce site where search results take varying amounts of time to load, depending on the complexity of the search query and server load.

// Import 'chromium' from Playwright, define an async IIFE to launch a Chromium browser and create a new page.
const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();

  await page.goto('<https://example-ecommerce.com>');

  await page.fill('#search-input', 'limited edition sneakers');
  await page.click('#search-button');

  // Wait for search results to appear
  await page.waitForSelector('.search-results', { timeout: 10000 });

  const resultCount = await page.$$eval('.product-card', (cards) => cards.length);
  console.log(`Found ${resultCount} products`);

  await browser.close();
})();

How it conquers timeouts:

  • page.waitForSelector() waits up to 10 seconds for the search results to appear.
  • If results load quickly, the script continues immediately; if they’re slow, it waits patiently.
  • This approach prevents premature failures due to varying load times.

Scenario 2 : Single-Page Application (SPA) Navigation

Challenge: Unpredictable route changes

In a Single-Page Application, route changes can happen quickly but with variable timing.

// Import 'chromium' from Playwright, define an async IIFE to launch a Chromium browser and create a new page.
const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();

  await page.goto('<https://example-spa.com>');

  await page.click('a[href="/dashboard"]');

  // Wait for URL to change and new content to load
  await page.waitForURL('**/dashboard');
  await page.waitForSelector('#dashboard-content', { state: 'visible' });

  const dashboardTitle = await page.textContent('#dashboard-title');
  console.log('Dashboard loaded:', dashboardTitle);

  await browser.close();
})();

How it prevents race conditions:

  • page.waitForURL() ensures the URL has changed before proceeding.
  • page.waitForSelector() with state: 'visible' confirms the new content has loaded and is visible.

Conclusion

Playwright’s asynchronous methods have been transformative in my web automation journey, offering powerful solutions to handle the unpredictable nature of modern web applications. Leveraging methods like page.waitForSelector() and page.waitForURL() has enabled me to build robust tests that adapt to dynamic content and variable load times.
Through these real-world scenarios, I’ve seen how Playwright can help reduce timeouts and eliminate race conditions, making tests more maintainable and reliable. Embracing asynchronous programming in web automation has turned flaky tests into dependable assets, ensuring that applications run smoothly even in the most challenging environments.

Happy Automation!