Playwright’s auto-wait is simple and superb

Cerosh Jacob
4 min readMar 19, 2024
Photo by Phil Hearing on Unsplash

Problem Statement: Extracting text from an initial hidden element only becomes visible after a preceding action is completed.

To understand the problem, let’s examine a scenario where this issue occurs.

Dynamically loaded page elements that are hidden.
test('Element on page that is hidden', async ({ page }) => {
await page.goto('https://the-internet.herokuapp.com/dynamic_loading/1');
await page.getByRole('button', { name: 'Start' }).click();
await expect(page.locator('#loading').getByRole('img')).toBeVisible();
const label = await page.locator('#finish >> h4').textContent();
console.log(label);
});

The test navigates to a specific URL, clicks a button, and searches for an image with certain characteristics. It then extracts text from this element and displays it. The problem arises because the h4 element is hidden until the loading process is finished. Yet, the Playwright extracts the text from it before the loading is complete. In our scenario, there is no noticeable difference since the text remains the same before and after loading. However, if we were to filter the result count based on some criteria, the counts before and after filtering could vary, potentially leading to issues.

As indicated in the problem, the solution is to wait until the loading completes before attempting to extract the text, regardless of whether it is available or not. In Playwright, there are several methods to address this issue. This post discusses three possible solutions.

The most straightforward approach involves identifying the element using the text that appears once the loading is complete. The playwright’s auto-wait is smart enough to wait for the loading to finish. Once the text appears, it will extract and display the value.

test.only('Handle elements on a page that are hidden by identifying them through their final text.', async ({ page }) => {
await page.goto('https://the-internet.herokuapp.com/dynamic_loading/1');
await page.getByRole('button', { name: 'Start' }).click();
const label = await page.getByRole('heading', { name: 'Hello World!' }).textContent();
console.log(label);
});

The solution does not work when the text appearing after the loading completion is dynamic or unknown, as it cannot be used to identify the element.

Next, we have the natural solution which involves delaying text extraction until loading is complete. This is achieved through the waitFor method. This method pauses the execution until the element specified by the locator satisfies the state option. state is an optional argument that can take values such as attached/detached and visible/hidden. attached/detached means waiting for an element to be present or not present in the DOM. visible/hidden refers to an element with or without any content, or with display:none. An element with display:none will have an empty bounding box and is not considered visible and and vice versa for hidden. so in our case

test.only('Handle elements on a page that are hidden by waiting for the loading to finish.', async ({ page }) => {
await page.goto('https://the-internet.herokuapp.com/dynamic_loading/1');
await page.getByRole('button', { name: 'Start' }).click();
await expect(page.locator('#loading').getByRole('img')).toBeVisible();
await page.locator('#loading').getByRole('img').waitFor({ state: 'hidden' })
const label = await page.locator('#finish >> h4').textContent();
console.log(label);
});

This example demonstrates the use of the waitFor method for handling issues. However, before waiting for the loading element to disappear, an extra assertion is needed to ensure the loading element appears. Otherwise, the Playwright may assume that the loading element is already hidden in the brief moment between the click and the appearance of the loading element. The waiting time can be adjusted using the optional timeout argument. In our example, we didn’t specify a timeout because it’s optional. If not defined, the timeout set for each test will apply. If there isn’t one, the default timeout of 30 seconds will be used. Of course, these default values can be overridden at the test or individual level.

Managing dynamically loaded page elements that are hidden.

The next approach is more advanced. Rather than waiting for the element, it uses assertions, which will retry until they pass or until the assertion timeout is reached. By default, this timeout is set to 5 seconds. The assertion timeout is not related to the test timeout. It refers to the maximum time the test will wait for the assertion to be validated. In our case, five seconds was insufficient, so we extended the time to ten seconds. Rather than making adjustments for each element, this can also be modified through the expect setting in the configuration.

test.only('Handling elements on the page that are hidden through assertions.', async ({ page }) => {
await page.goto('https://the-internet.herokuapp.com/dynamic_loading/1');
await page.getByRole('button', { name: 'Start' }).click();
await expect(page.locator('#loading').getByRole('img')).toBeVisible();
await expect(page.locator('#loading').getByRole('img')).not.toBeVisible({ timeout: 10000 });
const label = await page.locator('#finish >> h4').textContent();
console.log(label);
});

The retrying assertions are asynchronous, so they require the use of await. However, some assertions can test conditions without auto-retry. These can be used without await. Keep in mind that using non-retrying assertions may result in flaky tests. The example below demonstrates the use of these assertions. Note that they do not yield the same results as retrying assertions.

test.only('Handling elements on the page that are hidden in a faulty way.', async ({ page }) => {
await page.goto('https://the-internet.herokuapp.com/dynamic_loading/1');
await page.getByRole('button', { name: 'Start' }).click();
await expect(page.locator('#loading').getByRole('img')).toBeVisible();
expect(page.locator('#loading').getByRole('img')).toBeFalsy()
const label = await page.locator('#finish >> h4').textContent();
console.log(label);
});

Note: Identifying the locator of a loading icon can often be tricky as it appears on the screen briefly and may disappear when inspecting the elements. The most effective method is to pause script execution in the Sources tab of Chrome DevTools.

--

--