Tests often need to use many page objects. Instantiating all required page objects inside each test creates a lot of boilerplate. Let’s review a pattern for instantiating all the application page objects inside a main class or container and fixtures to reduce verboseness in the tests using Playwright in Typescript.
Let’s start with a basic test that uses many page objects:
tests/checkoutProduct.spec.ts
import { test, expect } from '@playwright/test';
import Home from '../pages/home.page';
import SignIn from '../pages/signIn.page';
import Checkout from '../pages/checkout.page';
import Confirmation from '../pages/confirmation.page';
test('Checkout Product', async ({ page }) => {
await page.goto('https://www.bstackdemo.com/');
const home = new Home(page);
const signIn = new SignIn(page);
const checkout = new Checkout(page);
const confirmation = new Confirmation(page);
await home.addProductToCart();
await signIn.doSignIn();
await checkout.submit('John', 'Doe');
await expect(confirmation.confirmationMessage).toBeVisible();
});
And, for reference, a page object class looks like this:
pages/checkout.page.ts
import { type Locator, type Page } from '@playwright/test';
export default class Checkout {
readonly page: Page;
readonly firstName: Locator;
readonly lastName: Locator;
readonly submitButton: Locator;
constructor(page: Page) {
this.firstName = page.locator('#firstNameInput');
this.lastName = page.locator('#lastNameInput');
this.submitButton = page.getByRole('button', { name: 'Submit' });
}
async submit(firstName: string, lastName: string) {
await this.firstName.fill(firstName);
await this.lastName.fill(lastName);
await this.submitButton.click();
}
}
Main Page Object
To reduce the verboseness, a better pattern is to create a main class that instantiates every other page object we use for our application inside of it. Then, we need to instantiate only one per test.
First, we create a main class that receives the page and passes it to each page object class.
pages/app.page.ts
import { type Page } from '@playwright/test';
import Home from './home.page';
import SignIn from './signIn.page';
import Checkout from './checkout.page';
import Confirmation from './confirmation.page';
export default class App {
readonly page: Page;
readonly home: Home;
readonly signIn: SignIn;
readonly checkout: Checkout;
readonly confirmation: Confirmation;
constructor(page: Page) {
this.page = page;
this.home = new Home(page);
this.signIn = new SignIn(page);
this.checkout = new Checkout(page);
this.confirmation = new Confirmation(page);
}
}
We pass the page fixture to this class and through it, we can access the page objects we have defined for our application.
tests/checkoutProduct.spec.ts
import { test, expect } from '@playwright/test';
import App from '../pages/app.page';
test('Checkout Product', async ({ page }) => {
await page.goto('https://www.bstackdemo.com/');
const app = new App(page); // <-- instantiate main class
await app.home.addProductToCart();
await app.signIn.doSignIn();
await app.checkout.submit('John', 'Doe');
await expect(app.confirmation.confirmationMessage).toBeVisible();
});
Using Playwright Fixtures
Another improvement we can do is to use dependency injection and use this “app” class directly in our tests without needing to instantiate it manually in every test, thus reducing even more the verboseness.
We add a fixture for the app object.
config/fixtures.ts
import { test as base } from '@playwright/test';
import App from '../pages/app.page';
type Fixtures = {
app: App;
};
export const test = base.extend<Fixtures>({
app: async ({ page }, use) => {
const myApp = new App(page);
await page.goto('https://www.bstackdemo.com/');
await use(myApp);
},
});
export { expect } from '@playwright/test';
This fixture returns the instantiated master app object.
Now in our tests, we can pass directly the app fixture as a parameter, leaving the final result like this:
tests/checkoutProduct.spec.ts
import { test, expect } from '../config/fixtures';
test('Checkout Product', async ({ app }) => {
await app.home.addProductToCart();
await app.signIn.doSignIn();
await app.checkout.submit('John', 'Doe');
await expect(app.confirmation.confirmationMessage).toBeVisible();
});
If we need to access the page inside the test, it’s in app.page
Following this pattern, we greatly reduce boilerplate code and the test is much more readable and concise. The final result has 355 fewer characters and 9 fewer lines of code.
Top comments (0)