DEV Community

Cover image for Playwright: Simplify Tests by Using a Main Page Object Class
Luciano Renzi
Luciano Renzi

Posted on

Playwright: Simplify Tests by Using a Main Page Object Class

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();
});
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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)