Transcripted Summary

By the end of this video, you will be able to use the Page Object Model with Playwright.

At this point, we have learned all the basics about Playwright, so now let's review how to use the page object model with this tool.

NOTE

As a side note, I will not go into detail for the Page Object Model because there is plenty of material about this topic already created on the Internet. And some of it, you can even find it in Test Automation University. So, in the end, it really doesn't matter what language you're using to code. The page object model concept is always the same.

Check out the Test Automation University course, From Scripting to Framework with Selenium and C# - Chapter 3 by Carlos Kidman.

Let's take this website as our example for the Page Object Model.



We will create two Page Models. The first one is the login form.

And after it, if we login, we're going to create another page model for this homepage that we have here.



The scenarios that we will cover are the following:

  • First, we're going to have to login.
  • And then once we land on this page, we're going to verify that this is the name and the total balance, the credit available, and the due today.

Let's go back to visual studio code and let's create some new folders.

  • For this one it's going to be named pom.

  • Inside “pom” I'm going to create another one which is called models. This one is going to have all of our pages

  • Let's also create another folder called spec. And here is where we're going to have our test.


Let's also go to the package.json to exclude the files that we don't want just to take a look into for this example.

How do we do that?

We just have to add a new key here, which is going to be “jest” and inside of it, we have “testPathIgnorePatterns”.


  "jest":{
    "testPathIgnorePatterns":["/tests/"]
  },

This is an array, and what we have to type here is the directory of those tests that we don't want to include in our execution. In this case, I want to ignore this “tests” folder.


# All right now, inside the “models” folder let's create our first base page (Base.page.js)

We have to start by creating a class, and its constructor. Here, we're going to pass a page and we're going to initialize it like this.

Next, let's create a navigate method like this. We're going to take a path, and we're going to do just await this.page.goto(). In here goes the URL. In this case, let's retrieve it from the website we were taking.



I'm using path because I want to send this extra part of the URL as path, and this is going to be the main URL that we're going to be using.

Now, using interpolation, we add that path here. And in this case is “index” but if you log in, you can see here that it is an “app”.


If we have more pages here, it will usually have different paths in this URL; that's why we're creating this navigate method here.

And finally, let's export this module to make it accessible for the other files.


// https://playwright.dev/docs/pom
class BasePage{
    constructor(page){
        this.page = page;
    }
    /**
     * Method to navigate to path passed
     * @param {string} path 
     */
    async navigate(path){
        await this.page.goto(`https://demo.applitools.com/${path}`)
    }
}
module.exports = BasePage;

# Then, let's create our login model, “Login.page.js”.

We need to require the base page.

In order to do that, we just have to do const then BasePage = require and here goes our module, which is “./Base.page”.

And again, we need to create a class.

We're extending because we are using inheritance here from the BasePage.


We also need our constructor here.

We have the page again and this time we're going to call it super because it's the parent class, and we're going to pass the page again.

Then, we're going to have all of our locators for the elements that are in the login page. So, let's go to the website again and let's retrieve those selectors.

Let me just go back to the index page, which is the login one. And here, let's see what are the selectors for username and password.

  • So, we have an id which is “username”.

  • And for the other one, we also have “password” and that's the id.

  • And lastly, the sign in button is also an id. It's just “log-in”.


Let's go back here and let's create those selectors.

  • It's going to be this.userNameTxt. In this case, it reached the id which is “#username”.

  • Then this.passwordTxt; this one is “#password”.

  • Now, this.loginBtn for the login button, right? So, the login button is “loginBtn” and its selector was also an id and it was just “#log-in”.

Now what we can also create here is the async navigate, which is similar to the one that we have in our base class. But in this case, what we're going to do is await super in order to call one of the parent class, and we're going to do .navigate. And here, we're going to pass the “index.html”, which is this part of the path.


So now, the other thing that we can put in this login page is the login action, right?

Like we passed the username and password, and now we want to log in with those credentials. The way to do it would be, maybe we can call this async login(), and we're going to pass the username and password values to this method here.

And what do we need to do?

  • We need to fill the username text box

  • we need to fill the password text box

  • We need to click on the login button

So let's do that, await this.page.fill.

  • We're going to use this.userNameTxt that is here. And we're passing the username that we got here as part of this method.

  • It's going to be something similar for the password, but instead of using “usernameTxt box”, we're going to be using this.passwordTxt here. And we're going to pass the password value that we're receiving in this method.

  • Finally, we just need to click on that submit button, right? So this is going to be this.loginBtn, but this is just a selector, so we have to do this.page.click.


We also have to export this module in order to make it available for the other files the same way that we did with the base page here.

The only difference is that this is not the BasePage. It's going to be the LoginPage.


// https://playwright.dev/docs/pom
const BasePage = require('./Base.page');
class LoginPage extends BasePage {
    constructor(page){
        super(page);

        // selectors
        this.userNameTxt =  '#username';
        this.passwordTxt =  '#password';
        this.loginBtn = '#log-in';
    }
    /**
     * Method to navigate to Login page using parent's method
     */
    async navigate(){
       await super.navigate('index.html'); 
    }
    /**
     * Method to login using username and password
     * @param {string} username 
     * @param {string} password 
     */
    async login(username, password){
        await this.page.fill(this.userNameTxt,username);
        await this.page.fill(this.passwordTxt,password);
        await this.page.click(this.loginBtn);
    }
}
module.exports = LoginPage;

# Now, we can create our home model; it's just going to be “Home.page.js”.

Again, we need the similar things that we have in the other one, just in this case it's not named “LoginPage”, this can be named “HomePage”.

We also need to have our constructor here. And this one is going to be looking pretty similar to the one of the LoginPage, but the selectors obviously are different.

We just need to do super(page), we initialize the page and here comes the locators or selectors.


Let's go back to this website and let's login, and let's retrieve the elements that we want to validate, right?

We said that we wanted to validate the name and these balances.

Let's take a look to see what are the selectors for the logged in user. In this case, we have a class which we can use, which is “logged-user-name”. So, let's give it a try and create that. This is going to be this.loggedUser and this is a class.

And how about these balances, right?

Let's take a look and see what we have here?



So, we have “balance-value” we can perhaps use.

Let's just filter as the class. And yes, we have these...

There is, however, a minor difference here between the first one and the second ones. The first one seems to be having the value of the $350 in a span. There's actually 2 spans here and the other ones do not have a span.



So, we may have to take that into account where we do our logic for retrieving the inner text in these elements.


Anyway, let's just go back and we're going to create a new variable for the selector for this.balances.

In this case, we got it and it's a “balance-value” class.

And we can create an async function, which is going to help us to retrieve the balance depending on what we pass on it.

The way to do it will be to create this asynchronous function, which is going to be named getBalance(). And here, we can pass balance type (balType).

In this case, if it's the credit or if it's the total balance, and I think the last one was something like Due Today, so that's what we're going to pass here.

We can do let balArrray to generate this array with all of these element handles that are with this type of selector.



So we're going to just do await this.page.$$(this.balances). This is the selector for these balances.


We're going to have in this array all of those balances as element handles.

In this case, we can access every single one of those by doing at position 0, 1, and 2, and then just get the innerText inside it and retrieve the value that we need.

So, let's just do if(balType == 'total').

We're going to have the logic there — else if(balType == 'credit') — and else for the last one which is due today or something similar to that.

So now, we noticed that the “total” is different from the “credit” because there is a span inside that element handle. So, let's just get that span in order to get the innerText of that span.

We can do it this way, return, here we just have to do await balArray[0] in the position zero because this is the first balance on the screen.

Here, we have to look inside of it to get the span. Again, I will show you what I'm talking about.



Here, you can see that there is the span, and this is the one that we want to get the inner text of.

And if it's not the total, we can just pass the balance array inner text because there is no span there, so what we can do is return (await balArray[1]).innerText().

And if it's not credit, it's going to be the other one — return (await balArray[2]).innerText() — which is just the second one.

That's pretty much all that we will have to do.


We can also create an asynchronous method for the navigate, in this case, the homepage.

This is similar to what we did with the other one with the login one. Instead of passing the “index,” what we can pass here is this “app”. Let's do that.


And I almost forgot that we also wanted to validate the name on the screen, right?

So basically, this one, which is why we retrieved this selector here.

Let's also create an asynchronous function to send it back.


    async getUserName(){
        let user = await this.page.$(this.loggedUser);
        return await user.innerText();
    }

With this, we have our element handle.


At the very end, let’s also export these in order to make it accessible for the other files, just like we did with the login page and the base page.


// https://playwright.dev/docs/pom
const BasePage = require('./Base.page');
class HomePage extends BasePage {
    constructor(page){
        // calling the parent class BasePage constructor - inheritance
        super(page);
        //selectors
        this.loggedUser = '.logged-user-name';
        this.balances = '.balance-value';
    }
    /**
     * Method to retrieve the username 
     * @returns {string} username logged
     */
    async getUserName(){
        let user = await this.page.$(this.loggedUser);
        return await user.innerText();
    }
    /**
     * Method to retrieve the balance type
     * @param {string} balType : 'total', 'credit', 'due today'
     * @returns string
     */
    async getBalance(balType){
        let balArray  = await this.page.$$(this.balances);
        if(balType == 'total'){
            // according to the DOM the first balance has an extra span
            return (await balArray[0].$('span')).innerText();
        }
        else if(balType ==  'credit'){
            return (await balArray[1]).innerText();
        }
        else {
            return (await balArray[2]).innerText();
        }
    }
    /**
     * Method to navigate to home page using parent's method
     */
    async navigate(){
        await super.navigate('app.html'); 
    }
}
module.exports = HomePage;

# And now finally, we can start creating our test file.

Let's come here under “specs” and let's create our test, which is going to be named demo test.js.

So, what do we need?

We need to require Playwright like we have done all this time, so let's just do that — const { chromium } = require('playwright').

Just to save some time, I had already created this template, but this is nothing that we haven't seen before.



The only difference is that I am using it instead of test like we did before.

But in Jest, it and test are an alias, so they look similar. In this case, we are going to see that we're going to be able to run our tests also using this.

As you can see, we have already created our browser object, context, and the page, and we are having everything that we need in order to start coding ourselves. Now, how can we bring everything together, right?

We need those page objects to come into the picture in order to use them. So, how do we do that?


We have to create a variable here for every single one of them.

In this case, we can start with the HomePage and then we use require, just like we are required chromium here, but instead of using “playwright”, we're going to do something like this, “../models/Home.page”, right?

And we also want the same for the login page.


const HomePage = require('../models/Home.page');
const LoginPage = require('../models/Login.page');

Now, we can create a couple of variables for these 2 pages in our describe block.


    let homePage  = null;
    let loginPage  = null;

Here in our beforeAll block we need to add some code.

What we have is basically homePage = new HomePage() and then we pass the page. I think if you're familiar with Java, this would look something similar to what you have seen before.

We will do the same for the login page.

And at the end of the beforeAll what we want is to navigate to have this as our precondition to start our test. So, we're going to navigate by using the loginPage.navigate.


        homePage = new HomePage(page);
        loginPage = new LoginPage(page);
        await loginPage.navigate();

And if you notice the method that we have here is the one that navigates to the index.html.


Now, once we navigate to that page, what we can do is to login, right?

So, here's our test. What we can do is something like this — await loginPage.login — because we have already created that login method, and we are only needing to pass the username and password.

In this case, I'm just going to pass something by default. It doesn't really matter.

We need to do some certain validation here because if we don't do a validation, what is the purpose of this test, right? So, let's just check that the title is not null.


    it('Should be able to login', async() => {
       await loginPage.login('username','password');
       expect(await page.title()).not.toBeNull();
    }) 

# Creating the Validations for Our Tests

And to validate the name is Jack Gomez, what we have to do is expect(await homePage.getUserName()). In this case, because we're no longer in the login, then getUserName. I think we call it “UserName”.

Let's see what's the name of our homepage here. Yeah, it's getUserName. And we'd want to use Jack Gomez, right? Jack Gomez.

Now, the total balance is expect(await homePage.getBalance('total')). Here, we have to pass the balance type. In this case, let's pass “total”, and this one has toBe $350.

And for the other 2, it's going to be pretty much the same, which needs to pass different values here.

Instead of “total”, it's “credit”. And instead of “total” here, it's “due today”. It doesn't matter because this is going to fall in the else one.

So here in credit available, it's going to be $17,800 and here in due today is just base value.


# Example Code - demo.test.js


// https://jestjs.io/docs/expect
const { chromium } = require('playwright');
const HomePage = require('../models/Home.page');
const LoginPage = require('../models/Login.page');

describe('Applitools demo page', () => {
    jest.setTimeout(30000);
    let browser = null;
    let context = null;
    let page = null;
    let homePage  = null;
    let loginPage  = null;

    beforeAll( async ()=>{
        // we launch browser and navigate to the loginpage
        browser = await chromium.launch({ headless: false });
        context = await browser.newContext();
        page = await context.newPage();
        homePage = new HomePage(page);
        loginPage = new LoginPage(page);
        await loginPage.navigate();
    });

    afterAll( async ()=>{
        // closing browser
        await context.close();
        await browser.close();
    });


    it('Should be able to login', async() => {
       await loginPage.login('username','password');
       expect(await page.title()).not.toBeNull();
    })

    it('Should be logged in as Jack Gomez', async() => {
       expect(await homePage.getUserName()).toBe('Jack Gomez');
    })

    it('Should have total balance of $350',  async() => {
       expect(await homePage.getBalance('total')).toBe('$300');
    })

    it('Should have credit available of $17800',  async() => {
        expect(await homePage.getBalance('credit')).toBe('$17800');
    })

    it('Should have due today of $180',  async() => {
        expect(await homePage.getBalance('due')).toBe('$180');
    })
});

Now, let's run this and see what happens.

Let's open a new terminal and let's do npm test.

We can see Jest is starting the execution, and it's launching the browsers, doing the login. And it seems like we have an issue. So let's take a look and see what's going on.

Here, it seems like we have 4 tests that are passing and 1 is failing. Why is one failing?

Page is not defined. Oops, I think I know why. Let's go to this page. In this case, it's for Jack Gomez.

So, it’s in this method, getUserName in the homePage here I am missing this in order to make it work.



Let's clear this, and let's try this again.

And this time, it ran super-fast, and it passed.



Let's try something different.


Let's make it fail on purpose, let's change this to something like this to see what happens.



And you see it failed, and it's telling us 2 failed. Let's scroll up and see why it's saying it's failed.

Here it’s saying it's because we're missing a comma, so we actually have to pass a comma in order to match with what is in the inner text.



The second test that is failed is this one because it's not $350. It is expecting $300.

And that's it; this is how you can create a simple page object model in Playwright.


# And with this, we have reached the end of this course.

I hope this was useful for you. If you want to dive deeper in Playwright, because this is just the tip of the iceberg, I will recommend you to go and check the official documentation. It is actually quite good.

You'll see that there's so much more you can do with Playwright.

Also, if you want to connect with me on Twitter, my DMs are open. Thank you.



Resources



© 2024 Applitools. All rights reserved. Terms and Conditions Privacy Policy GDPR