Mastering Auto-Suggestion: Playwright

Cerosh Jacob
7 min readApr 8, 2024

--

Photo by Derick McKinney on Unsplash

Problem statement

Access auto-suggested addresses from a dropdown through test automation. The text box for address input and the auto-suggestion dropdown are located within two separate iframes. Each iframe is identified by a dynamic name.

A question has been raised by a member of a playwright group on how to access auto-suggested addresses from a dropdown when only part of the address is entered. At first glance, this question seems basic, but upon closer inspection of this particular scenario, it becomes apparent that the text box for entering the address and the auto-suggestion results are located in two separate iframes. Furthermore, the iframe name is dynamic and changes with each page reload, adding complexity to the issue. The src URL of the iframe is also dynamic and complex, making the identification of the iframe a significant challenge. Also, the test begins on one page, then another tab opens, and further testing must be conducted in that tab. This post presents a solution to the problem by utilizing the features of Playwright.

Opening the URL in a new tab.

The first step involves accessing the website, opening a separate tab, and shifting focus to input the contact information in the new tab. given below is the code for this.

await page.goto('<https://www.voma.ai/>');
const page1Promise = page.waitForEvent('popup');
await page.getByRole('link', { name: 'Try a demo' }).click();
const page1 = await page1Promise;
await page1.getByTestId('email').click();
await page1.getByTestId('email').fill('cerosh@cerosh.com');
await page1.getByTestId('phone').fill('0470225569');
await page1.locator('#StripeAddressField').waitFor();

The playwright initiates a promise (page1Promise) to wait for a popup event. It listens for new page creation. This step must start before clicking the link that triggers the popup. After clicking the ‘Try a demo’ link, the script waits for page1Promise to resolve, signifying the new tab has opened. It then assigns the new tab's page object to the page1 variable. Finally, it locates and clicks an element with the data-testid attribute in the new tab, After obtaining the new page object, we have to shift focus to it for further interaction. This can be done by clicking on the input field or by using a waitForSelector() method for the input field in the new tab.

Locate the iframe to input billing information.

In the next section, you’ll need to get the iframe locator to enter the billing information. Initially, the billing section won’t be visible upon page load. It will only appear after you’ve entered the contact information.

let billingInfoFrame: string = "";
await page1.locator('#StripeAddressField').waitFor();
try {
const billingInfoFrameElement = await page1.waitForSelector('#StripeAddressField iframe');
billingInfoFrame = await billingInfoFrameElement.evaluate(iframe => iframe.getAttribute('name')) || "";
} catch (error) {
console.error('Could not find first iFrame".', error);
}

The Playwright script is set to wait for a specific element to appear on the page as the billing information only shows up after entering the contact data. This approach ensures that the script pauses until the prospective parent element for the iframe is available.

A variable is declared and initialized as an empty string, ready to store the name of the first iframe found. The code is enclosed within a try-catch block to handle potential errors. It tries to locate an iframe element and, if successful, retrieves its name using the evaluate function. This function extracts the name attribute from the context of the iframe element. The result is then assigned to the previously declared variable.

The || "" segment serves as a fallback for the first iframe name, ensuring it always contains a string value. The logical OR operator examines the outcome of the evaluate function. If the result is a truthy value, it is used as it is. If the result is a falsy value, an empty string is used instead.

Finally, if an error occurs while locating the iframe element (for instance, if the iframe does not exist), the catch block captures the exception and logs an error message to the console to signal the problem.

Enter Billing details

This section details the process of entering billing information, including the user’s first and last name, country, and address.

const locator = page1.locator(`iframe[name="${billingInfoFrame}"]`);
const billingInfoFrameLocator = locator.contentFrame();
await billingInfoFrameLocator.getByLabel('First name').fill('Cerosh');

await page1.frame(billingInfoFrame)?.fill('#Field-lastNameInput', 'jacob')

await billingInfoFrameLocator.getByLabel('Country or region').selectOption('AU');
await billingInfoFrameLocator.getByPlaceholder('Street address').pressSequentially('123 Pitt Street', { delay: 500 })

In Playwright, you can access frames using either a Frame object or a FrameLocator. In our example, we enter the last name using a Frame object. This method retrieves a frame that matches specific criteria — either the name or the url. In this instance, we opted not to use the url due to its dynamic and complex nature. Despite the name also being dynamic, we were able to identify it and thus chose to use it.

As its name suggests, FrameLocator facilitates the creation of a frame locator that enters an iframe, enabling the selection of elements within it. In the latest release of Playwright, Version 1.43, a new method locator.contentFrame() has been introduced. This method transforms a Locator object into a FrameLocator, which proves handy. For instance, the Locator object for the billing information iframe is necessary when inputting remaining billing details. Previously, we needed to repeat the billing information iframe locator each time we wanted to access elements within that iframe. Now, a simple reference to the iframe suffices.

In Playwright, await FrameLocator.locator().action() is generally preferred over await frame.action(selector). The exception is when entering the last name to demonstrate the usage of the Frame object, but in all other cases, FrameLocator is used. The FrameLocator object targets a specific iframe and creates a locator within that frame using .locator(). This approach is explicit about the context (the iframe) where the action is taking place.

While using frame.click or a similar action might seem simpler, it can lead to ambiguity especially when the application contains multiple iframes. This method relies on the current frame context, which may not always be the intended one, leading to unexpected behaviour if the context isn't clear.

The FrameLocator improves code readability and maintainability by separating the frame identification logic from the action. This has been especially beneficial since the introduction of the locator.contentFrame() method. By explicitly locating the element within the frame, Playwright can provide a specific error if the element isn't found in the targeted iframe. This is more effective than using the frame() object, which might only state that the element wasn't found, making troubleshooting more difficult.

Choosing the billing address

The final section involves selecting a billing address from the dropdown list. As the dropdown list is located in a separate iframe, you need to find the name of that iframe. Then use that name to access the iframe and the elements within it.

const frames = page1.frames();
let visibleFrameNames: string[] = []
for (const frame of frames) {
const frameName = frame.name();
const visibleFrames = await page1.frameLocator(`iframe[name="${frameName}"]`).locator('.p-Fade-item ul').isVisible()
if (visibleFrames) {
visibleFrameNames.push(frameName)
}
}
const addressFrameName = visibleFrameNames[1];
const options = await page1.frameLocator(`iframe[name="${addressFrameName}"]`).locator('.p-Fade-item ul li').all();
for (const option of options) {
await option.dispatchEvent('click')
break;
}

Since the dropdown iframe only appears after inputting an address, it’s necessary to compile a list of all iframes present on the current page. This is because we need to identify the specific one containing the dropdown, among multiple iframes.

Begin by declaring an empty array to store the names of visible frames. Then, iterate through each frame. If a frame is visible, add its name to the array.

Upon inspection, it becomes clear that there are two visible iframes, and the dropdown list is located within the second one. So, we target the second visible frame, which is at index 1 in the array.

Within this selected iframe, iterate over each li element. Dispatch a click event on the first li element encountered, and then exit the loop. This action simulates the user behavior of selecting the first option in the dropdown.

In the given scenario, dispatchEvent('click') is used instead of .click(). This choice was made because .click() did not automatically populate the city, state, and zip code of the chosen address. Both dispatchEvent('click') and .click() in Playwright are used to simulate click events on a DOM element.

The method .click() checks if the element is visible and actionable, scrolls if needed, dispatches a click event, and waits for navigation if triggered. On the other hand, dispatchEvent('click') dispatches a click event directly, without checking visibility or scrolling.

await page1.keyboard.press('ArrowDown');
await page1.keyboard.press('Enter');

Usually, .click() is used for natural interaction, while dispatchEvent('click') allows more control over the event. The same outcome could have been achieved using keyboard.press(), but dispatchEvent() was chosen because it targets a specific locator within the iframe to perform the action. Meanwhile, keyboard.press() relies on the current context, which might not always be the intended one, leading to unpredictable behaviour.

This test is kept simple to demonstrate how to access autosuggestions within an iframe without adjusting any models or adding comments, and without considering reusability. Ideally, assertions should be included at each stage before proceeding. However, to maintain focus, the test is kept concise. If you intend to adapt this to your current project, use your discretion.

Access auto-suggested addresses from a dropdown menu involved with multiple iframes.

https://github.com/Cerosh/articles/blob/master/tests/auto-suggested%20addresses%20from%20a%20dropdown.spec.ts

Pro Tip: This example requires the latest Playwright features. Update using the commands below to run this script successfully.

npm install -D @playwright/test@latest
npx playwright install --with-deps

If you’re interested in understanding what these commands do, along with other similar commands, refer to my previous post.https://ceroshjacob.medium.com/common-playwright-commands-f640e4e1b989

My recent publication compiled a comprehensive collection of 100 similar typescript programs. Each program not only elucidates essential Typescript concepts and expounds upon the significance of test automation but also provides practical guidance on its implementation using Playwright. This resource will undoubtedly be valuable if you look deeper into similar topics.

--

--

No responses yet