Welcome!
In today’s post, I’ll talk about one of my favorite patterns for UI testing. I won’t go into detail about what it is and why you should use it. My goal today is to demonstrate the implementation of this pattern when working with Playwright and Javascript/Typescript. If after reading and analyzing the implementation examples you still have questions, I recommend reading more about this pattern here.
If you liked the post, support it with a like and a comment, and also join the Telegram channel, where I post a little more about my tasks and thoughts on the topic of software development and testing.
So, let’s go 🙂
Java first
I really like it when I don’t have to keep in mind what step of the scenario I am at and which page is open at the moment while I’m writing automated test. It seems like a small thing, but actually it complicates development due to so-called information noise. First and foremost, when I am busy with writing a test, I want to focus on the test logic test, not keeping a heap of service information in my head.
This problem can be easily solved in typed programming languages (such as Java, C#). I will show you an example and you will immediately understand that we are talking about the Fluent API pattern:
@Test
void testExample() {
HomePage page = Pages.login.open()
.fillUsernameInput(Config.correctUsername)
.fillUsernamePassword(Confid.correctPass)
.clickLoginButton();
assertTrue(page.isInventoryListVisible());
}
Aside from the code looking super readable, there’s another side effect: I don’t have to think about where I am in the application after a certain action, all the transition logic (essentially the graph of our application) is described within the page objects. Each Page Object method returns the page type that is expected after performing the action.
There is a problem though…
Unlike the synchronous code in the above-mentioned languages, Javascript or Typescript are not synchronous by the nature, which means such code cannot be written because all methods of interacting with the page will return Promise<T>
. Let me show you this with the example of the open()
method of some page (it will be useful to those who are not very familiar with Promise
or those who are just beginning to understand the basics of Javascript):
export class LoginPage {
// locators, constructors etc.
/*
Here we need to return this page as a result as we know we're on current
page after it's open
*/
public async open(): Promise<LoginPage> {
await this.page.goto(this.url);
return this;
}
}
And now I cannot utilize Fluent API patter as of Javascript asynchronousness:
What implementation options do we have?
The simpliest one
The most obvious way is to wait until the action is done and next Page Object is returned step by step:
let loginPage: LoginPage;
test.beforeEach(({page}) => {
loginPage = new LoginPage(page);
});
test('User can login', async () => {
let login = await loginPage.open();
login = await loginPage.setUsername('standard_user');
login = await loginPage.setPassword('secret_sauce');
let home = await loginPage.clickLoginButton();
expect(await home.isInventoryListVisible()).toBeTruthy();
});
To be honest, it doesn’t look so good 😕. Need to mention that such a code doesn’t solve the context problem as well. I still have to remember which page I’m on at any given moment.
Next step — using then()
The Promise class allows us to use the then() method and the return value to build the call chains. Let's tweak our code a bit and see what we've got:
let loginPage: LoginPage;
test.beforeEach(({page}) => {
loginPage = new LoginPage(page);
});
test('User can login', async () => {
await loginPage.open()
.then(async (page) => await loginPage.setUsername('standard_user'))
.then(async (page) => await loginPage.setPassword('secret_sauce'))
.then(async (page) => await loginPage.clickLoginButton())
.then(async (page) => expect(await page.isInventoryListVisible()).toBeTruthy());
});
It’s better now, no need to remember the current page. Basically, you can use it, but it still doesn’t look nice.
The graceful way
First of all, I want to say thank you to Anthony Gore — it was he who introduced me to the wonderful library that helps solve the problem of calling a chain of methods.
So, the first thing to do is to add a new dependency to the project:
npm i proxymise
If you are working with a Javascript project, everything is okay, you can use it.
If the project works with Typescript, additional actions will be required:
Check that the command
tsc -v
works. If not, you need to install an additional dependency:npm i tsc
Then we execute
tsc --init
and look at the result. It will also be written there how to fix errors if there is any. If the command was executed successfully, atsconfig.json
file should appear in your project.
Now it is necessary to connect proxymise in the page object files and wrap the class so that it can be used from tests in the Fluent API format. The full project code looks like this:
// pages/login-page.ts
import {Locator, Page} from "@playwright/test";
import {HomePage} from "./home-page";
import proxymise from "proxymise";
export class LoginPage {
private page: Page;
// Make this field static to use in static method
private static url = 'https://www.saucedemo.com/';
private usernameField: Locator;
private passwordField: Locator;
private loginButton: Locator;
constructor(page: Page) {
this.page = page;
this.usernameField = this.page.locator('#user-name');
this.passwordField = this.page.locator('#password');
this.loginButton = this.page.locator('#login-button');
}
// This method is static now. Necessary for proxymise correct work
public static async open(page: Page): Promise<LoginPage> {
await page.goto(LoginPage.url);
return new LoginPage(page);
}
public async setUsername(name: string): Promise<LoginPage> {
await this.usernameField.fill(name);
return this;
}
public async setPassword(pass: string): Promise<LoginPage> {
await this.passwordField.fill(pass);
return this;
}
public async clickLoginButton(): Promise<HomePage> {
await this.loginButton.click();
return new HomePage(this.page);
}
}
// Wrap this class with proxymise to avoid it in tests code
export default proxymise(LoginPage);
// pages/home-page.ts
import {Locator, Page} from "@playwright/test";
import proxymise from "proxymise";
export class HomePage {
private page: Page;
private static url = 'https://www.saucedemo.com/inventory.html';
private inventoryList: Locator;
constructor(page: Page) {
this.page = page;
this.inventoryList = page.locator('div.inventory_list');
}
public static async open(page: Page): Promise<HomePage> {
await page.goto(HomePage.url);
return new HomePage(page);
}
public async isInventoryListVisible(): Promise<boolean> {
return await this.inventoryList.isVisible();
}
}
export default proxymise(HomePage);
// tests/saucedemo.spec.ts
import {test, expect} from '@playwright/test';
import LoginPage from "../pages/login-page";
test('User can login', async ({page}) => {
const isInventoryShown = await LoginPage.open(page)
.setUsername('standard_user')
.setPassword('secret_sauce')
.clickLoginButton()
.isInventoryListVisible();
expect(isInventoryShown).toBeTruthy();
});
Pay attention to how concise our test has become. This solution is no different from the standard Fluent API in Java or C#.
Conclusions
This example is basic for understanding how to achieve Fluent API in tests written in Playwright and Typescript. But it is not final, it can be further improved by adding new features and concepts to facilitate developer and code maintainance. In any case, doing such an exercise would be useful for the development of coding skills 🙂
Good luck with your projects and don’t stop to automate!
Top comments (0)