Playwright stories: Navigating Tricky UI Automation Scenarios for Beginners

Kostiantyn Teltov
9 min readJan 24, 2024

Hello everyone,
Not only QA community,
It’s no secret that Playwright is one of the most user-friendly test automation tools for beginners. It is becoming more and more popular in the community with a cool and useful features, regular updates with release demos. Did you know about the Discord channel they have set up? If you haven’t joined yet, it’s open to you: https://discord.com/servers/playwright-807756831384403968

However, sometimes you need to spend some time to find a solution for solving some test automation problem. Today we are going to talk about some of the problems and how you can solve them with the help of Playwright. Will spoil from the beginning, today we will consider the following problems:

  • JavaScript Alerts
  • IFrames
  • Shadow Dom(open)
  • File Download
  • File Upload
  • Tables

If you’re ready, please fasten your seatbelts and let’s go.

Alerts

We will start with Alerts. You have probably seen them many times before.

Alert Box

The first is the simple “Alert Box”. We can just close it and nothing more. How do you do that in Playwright? Quite simply

All we need to do is subscribe to the ‘dialogue’ event and call ‘dismiss’ from it once this event has been triggered. In our case we trigger it with the click.

page.on(‘dialog’, async dialog => await dialog.dismiss());

Confirm Box

The next is “Confirm Box” with “OK” and “Cancel” buttons.

The idea is quite similar. Subscribe on ‘dialog’ and call “accept” or “dismiss” methods

page.on(‘dialog’, async dialog => await dialog.accept());

Prompt Box

The last item on the list is the “Prompt Box”. In addition to the previous box, it contains an “Input” field where you can add a text.

But it is not very difficult. Subscribe again and call an accept method with a text you want to pass inside the input field.

page.on(‘dialog’, async dialog => await dialog.accept(‘Test’));

I think it looks easy now. Does it?

Let’s move on to the next chapter

IFrames

Yes. We all hate it a bit. Some less and some more :)

But we definitely face it and we have to solve it. So we have a page with some frames

  • If we look inside of the HTML markup, we may my two top level Iframes(frame_top and frame_bottom).
  • Inside of the frame_top we also have 3 nested frames (frame_left, frame_middle and frame_right)

Let’s try to get text from each of these frames.

  • Finding top-level frame locators. We need to use a page and a “frameLocator” method. The parameter of this method is the selector of the frame.

this.frameTop = page.frameLocator(‘frame[name=”frame-top”]’);

this.frameBottom = page.frameLocator(‘frame[name=”frame-bottom”]’);

  • Now. We can find nested frames for frame_top frame. We just need to call “frameLocator” method again for the found top level locator. And will chain it.

this.innerTopLeftFrame = this.frameTop.frameLocator('frame[name="frame-left"]');

this.innerTopRightFrame = this.frameTop.frameLocator('frame[name="frame-right"]');

this.innerTopMiddleFrame = this.frameTop.frameLocator('frame[name="frame-middle"]');

  • Finally we create a methods that gets a text using already prepared frame locators

Top level frame

async getFrameBottomText(): Promise<string> {

return await this.frameBottom.locator(‘body’).innerText();

}

Nested frame

async getInnerTopLeftFrameText(): Promise<string> {

return await this.innerTopLeftFrame.locator(‘body’).innerText();

}

We can call these methods in the tests and make sure they pass.

As you can see, it is not that difficult. What I like is that you don’t have to go in and out of the frames as you do with WebDriver.

Let’s take it to the next stage

Shadow Dom

Also one of the most popular features on the list. But we will beat it too.

But first. What is Shadow Dom?

The Shadow DOM is a web standards technology designed to help web developers encapsulate their code. The primary benefit is encapsulation: styles and scripts defined inside the shadow DOM won’t conflict with the styles and scripts in the main document or other shadow DOMs.

  • Here we have a two simple paragraphs. Each of these paragraphs have ‘shadow-root’. In a first case it is just a ‘span’ and in second it is a list ‘ul’ with two items ‘li’.

So let’s try to read these texts. All we need to do is take the component name and the rest of the selector

First paragraph span text:

async getFirstParagraphText(): Promise<string> {

return await this.page.locator(‘my-paragraph span’).textContent() as string;

}

Second paragraph list text:

async getSecondParagraphText(): Promise<string[]> {

return await this.page.locator(‘my-paragraph ul li’).allInnerTexts() as string[];

}

Let’s call this method and check inside the test

As you can see, this is even easier than using IFrames.

Watch the new episode

File Download

This is a more interesting case. Maybe for me :) We want to download a file using a href link.

What we need to do is to break this logic down into a few parts

  • First we need to initialize the download and wait for the download event.

const [download] = await Promise.all([

this.page.waitForEvent(‘download’), // Wait for the download event

this.page.click(`a[href=”download/${expectedFileName}”]`) // Click the download link

]);

  • Secondly, we need to save our file

await download.saveAs(savePath);

The method is ready. Now let’s call it in the test.

  • Within the test, we have prepared a filename. Because we will use it in our selector.
  • And then we need to provide a save path

const expectedFileName = ‘USA.png’;

const downloadFolderPath = path.resolve(__dirname, `../test-data`); // Update this path

const savePath = path.join(downloadFolderPath, expectedFileName);

  • The next step is simply to call the prepared method and assert

await downloadFilePage.downloadFile(expectedFileName, savePath);

expect(fs.existsSync(savePath)).toBeTruthy();

  • And of course it is good practice to clean up after yourself :)

// Clean up: remove downloaded file

fs.unlinkSync(savePath);

To summarize:

  1. Click and wait for event
  2. Save event to real location

We still have two topics to cover. Let’s move

File Upload

Now we want to do the opposite. We want to upload a file

How to do it? In the case of this training site implementation, we need to perform 2 operations:

  1. Upload a file from the specified location using the setInputFiles method. As you can see, it takes the selector and the local file location path.

await this.page.setInputFiles(‘#file-upload’, filePath);

2. We need to click the Upload button to confirm the operation

await this.page.getByRole(‘button’, { name: ‘Upload’ }).click();

Additionally: I created a method that takes the uploaded file name on the file upload screen.

public async getUploadedFileName(): Promise<string> {

return await this.page.textContent(‘#uploaded-files’).then(text => text?.trim()) as string;

}

As usual, we need to call it from within the test.

  1. Before you do this, you need to create a directory and put a file in it. You could potentially create this file during the test run. But I decided not to make it too complicated for our example
  2. Next we need to resolve the file path. I have also defined ‘fileName’ because I want to check that it is at the end.

const fileName = ‘upload_file.txt’;

const filePath = path.resolve(__dirname, `../test-data/${fileName}`);

3. Finally, we can call an upload method that we prepared earlier

await uploadFilePage.uploadFile(filePath);

4. And check that the file has been uploaded

expect(await uploadFilePage.getUploadedFileName()).toBe(fileName);

Nor space technology. Do you agree?

We are almost there. We have to hurry to catch the last train.

Tables

And last but not least. Yes, tables. I have heard questions about how to parse them, even from experienced people. Today we will only look at a table with static fields. Yes, they are easier to parse. You always know which header is coming next.

There are several ways to do it. But I still prefer the method I use for WebDriver.

  • First, we will create a model (interface with a table row names)

export default interface ExampleOneTableModel {

lastName: string | null;

firstName: string | null;

email: string | null;

due: string | null;

webSite: string | null;

}

  • The second step is the implementation of the method logic. In short, we can divide it into two parts
  1. We need to find a line. Just do what we normally do. Not nothing:) Find a locator.

const tableRows = this.page.locator(this.tableRowsSelector);

const rowCount = await tableRows.count();

2. We want to use the for operator to fill cells for each row.

a) So we take the cells for the row using the “td” selector.

b) We know exactly how many cells we have and what each cell number belongs to. So we use indexes to get values and assign them to the appropriate cell.

What to do next? Test it!

We want to call a method we have prepared.

const tableResults: ExampleOneTableModel[] = await (await dataTablesPage.tableOne.sortTableByField(TableHeaderNames.Email)).getTableData();

Note: I also have a method that sorts the results by a specified field. This is more related to the random results returned by the table in it’s default state. I will provide a repo at the end of this article and you may find this implementation too :)

So we did it. We met all our challenges and survived.

Before we go, here’s the epilogue

Epilog

As you can see from this article, I really like movies and pop culture :)

You’ve already seen that you don’t have to write a lot of code to solve UI test automation challenges with Playwright. Even if some cases look tricky, you can give it a try and see that it is easier than you think. I wish you an easy start and enjoy the journey. I hope this article was useful and opened up some possibilities for you.

Thanks a lot and may power will be with you!

--

--

Kostiantyn Teltov

From Ukraine with NLAW. QA Tech Lead/SDET/QA Architect (C#, JS/TS, Java). Like to build testing processes and help people learn. Dream about making indie games