In this chapter, we'll learn about Dynamic page objects and Fixtures, ways to speed up our implementation, and to create flows to be reused in our application.
We'll see how the hooks file works and how to reuse it for the Page Object Model creation, how to build the URLs dynamically so you can speed up your application tests, and lastly, how to work with Fixtures and create reusable flows.
I hope you like it.
For this chapter, in our playwright.config.ts
, let's make sure we have the globalSetup
and the storageSetup
reverted because we are going to use the reused sign in state, as we just learned.
Let's open the profile-with-dynamic-pom.spec.ts
, which is under tests
> ui
> specs
.
This file is a regular spec file with its imports, and since we are testing the profile page, we created a new variable profilePage
here with the type of the page object ProfilePage
.
Usually, we would have a goto
and a new page object creation inside the beforeEach
method.
Instead, we are doing it via hooks.beforeEach
.
We are going to take a look at that in a minute.
Inside our describe
test method, we are performing a checkLoggedIn
that comes from the profilePage
.
And of course, you could have as many scenarios or as many steps as you would like inside the test.
Just because the focus here is the dynamic page object creation, we are not really concerned about the scenarios right now.
Let's understand what hooks.beforeEach
does.
You can see here that it's passing three parameters.
If we open the hooks.ts
file, which is under tests
> utils
, you will see the imports in a function called beforeEach
.
This function can be named as anything, and it has the first parameter page
, which is the playwright driver.
The second parameter is the PageObjectParam
that can be of type LoginPage
, BookPage
, or ProfilePage
, which are the page objects that we have in this application. You can have as many as you want.
The third parameter targetPage
is a string, and lastly, extra params
, which is not required but comes in a Record<any, any>
.
Inside this method, then we will finally do the page.goto
and a new thing that is the buildUrl
that we will understand.
We are finally creating the pageObject
in this dynamic way.
So it doesn't matter necessarily what exactly you are creating, as long as you pass the right PageObjectParam
, then you'll be able to create that.
And we return
the pageObject
to be consumed in the test file.
The benefit of using this is that if you need to create a new page object, you can replace these two lines with one single line, and it will create and go to the page automatically for you for each test.
Again, less lines of code, and of course, faster development of the tests.
Once we are back to the hooks
file, we see that inside this beforeEach
method we can have any function or command that is similar to more than one page object.
So, whenever you go to a page, the first step is to open it usually.
You can do that here , and right after that, you create the pageObject.
If you have more steps or more functions to be executed, we can combine them inside this method or even create other methods for different page objects.
The idea here is just to simplify our coding experience and make it faster.
Speaking of buildUrl
, let's see what it actually does.
Because the idea here is to create things dynamically, you need to understand the pattern of your application to implement this method.
For our demo QA, usually it's just the path of the page - for example, '/profile' - or sometimes at the book page, we do have a parameter which is similar to this one here.
So what this method does is it receives two parameters - one is a page
name as a string
, and the second is the params
as a Record
.
These will be used here as the search url parameters.
The page name will be used to build the path.
The uiPath
will receive the page
, which is basically the URL of the page, and if it does have any parameters, we will create it using the URLSearchParams
.
Here you can see this ternary where it concats the query string parameters if it exists.
Otherwise, it will just use the uiPath
.
You can see here an example of each of the variables for the books page.
In case it's profile or any other, it will just have the uiPath
with the parameters empty.
In this case, the URL would be something like this - '/books`.
Then it is returning the URL to our hooks
file and returning the pageObject
model to our test file.
This way, we can continue with the test and call the functions whenever we need them.
Another example is the books-with-dynamic-pom.specs.ts
file that can be found under the specs
folder, and it does the same thing.
We create a variable bookPage
with the BookPage
object type.
We create the bookPage
dynamically using hooks.beforeEach
, and you can see here that we pass pages.bookStorePage
, which is exactly the page that will be identified inside the path later on, and the test
with the methods from the page object.
So, it's pretty simple to create a new test file because you already have the steps defined inside the hooks.beforeEach
.
Let's learn now about fixtures.
I consider a fixture an evolution of a hook file.
With a fixture, you can group your test based on their meanings instead of their setup.
So, in this case here, we have the file book-with-fixture-and-api.spec.ts
, which is under specs
.
It's pretty similar to any other tests.
The first difference we note is the import
of test
- instead of coming from @playwright/test
, it comes from fixtures/books-fixture
. We'll see that file in a minute.
We do have the first definitions here, we have a "beforeAll" that we won't look at right now. We'll see it in a minute.
Inside of the test.describe
, we do have bookPage
passed as a parameter.
This will mean that this test will use a fixture instead of just playwright.
We do have a method here to cleanBooks
and then go to a new books page and then do something else.
We also see here a test.use
with isDupe: false
.
Let's take a look inside the books fixture.
Here, we can import
the test
from '@playwright/test' because we will need to use it.
We need to define a type
that can be called MyFixture
passing the bookPage
page object. You could also use something else here.
Another thing is the type Duplicate
that is used to store the isDupe
parameter that will be passed to this fixture.
Then, you finally create a new test
variable extending the base
, passing the MyFixtures
and Duplicate
types that we just created.
This will allow this file to communicate with the test file.
I'm setting here isDupe
as false
, in case it's not defined by the test.
We create a function bookPage
with page
and with the variable that we want to pass.
Here, we finally create a new page object bookPage
using our hooks
file that we already learned, and we see this - await use(bookPage)
.
After that, we see that we have a command bookPage.addToYourCollection
, and passing isDupe
.
Finally, we export this expect
, and then we can use it in our test, as we saw previously.
Back to the test file, the trick here is that the first thing the test will do when it sees a fixture is to call it.
So, instead of executing cleanBooks
right away, it will go to the fixture and execute what is inside it, and it will execute this until it sees the method use
.
As soon as it sees the method use
, it will go back to the test, execute the commands, and as soon as it finishes, it goes back to the fixture and continues the flow.
So it's very important that you use it with moderation and awareness because it can get really confusing, but it's an amazing feature when you have to combine flows or scenarios within the same context.
To help with our understanding, I created a list of steps here so we can go over them one more time.
bookPage
as a param of the test, the fixture will be called. So, right here in our test, we have bookPage
, which causes the fixture to be called.hooks.beforeEach
.use
, so it goes back to the test file. Inside the fixture, we can see that right after creating the object model, we can see that use
is called. That means that it will go back to the test and execute the first step.cleanBooks
, it will do await bookPage.goto
, as mentioned here.use
. So, it goes back to the fixture and the next command is bookPage.addToYourCollection
. We can see here isDupe
is being used, and it's passing that because we were able to define it here, here, and here. Back to our test, we can see that it is also being defined here before the test itself.bookPage.addToYourCollection
, passing the param defined in the describe
.To exemplify how important this feature is, here is another example.
This is the file book-with-fixture-and-api-isolated-auth.spec.ts
, which is also under specs
.
We can see that we also import the fixture here.
For this test, we are not reusing the signed in state, so we are doing the sign in manually, intentionally.
Inside our test file, we define our isDupe
as true
, as opposed to false in the other test.
Of course, we could have this test inside the other file too, but I wanted to give you a few more examples.
In our test file, we are calling the bookPage
fixture.
If we remember, the first thing it will do is create the page object and it will go back right to the test.
In this case, we are adding books and going to a page with a different parameter.
Compared to this one, we were cleaning books and going to books.new
instead of books.duplicate
.
As soon as it finishes here, it goes back to the fixture and then calls addToYourCollection
passing isDupe
.
Finally, if we go to addToYourCollection
inside book-page.ts
, we'll see that isDupe
is being used.
If it's true, it has a series of commands that will execute only in that case.
With that, we complete Chapter 2. Congratulations.
Good luck with the quiz below, and a friendly reminder to take a look at the links in the Resources section. I'll see you in Chapter 3. Happy testing.