DEV Community

Cover image for Advanced Playwright Testing: Leveraging the Page Object Model (Part 2)
Samuel Kinuthia
Samuel Kinuthia

Posted on

Advanced Playwright Testing: Leveraging the Page Object Model (Part 2)

In my previous post; End-to-End Testing React Components with Playwright: (Part 1), we explored the fundamentals of testing React components using Playwright, focusing on setting up your environment and writing basic tests. Now, let's dive deeper into Playwright by implementing the Page Object Model (POM), a powerful design pattern that simplifies the maintenance and scalability of your end-to-end tests.

The Page Object Model (POM) abstracts away the complexities of interacting with UI elements, enabling you to write clean, maintainable, and reusable test code. In this post, we’ll cover how to set up POM in your Playwright tests, advanced usage scenarios, and best practices for managing large-scale applications.


1. Recap: What is the Page Object Model (POM)?

The Page Object Model (POM) is a design pattern commonly used in automated testing. It promotes the separation of the test logic from the UI interactions by encapsulating the elements and actions of a web page into a class. This not only enhances test readability but also makes it easier to maintain tests as your application evolves.

Key Advantages of POM:

  • Maintainability: UI changes require updates only in the page objects, not in the test cases.
  • Reusability: Page objects can be used across multiple test cases, reducing redundancy.
  • Readability: Tests focus on the "what" rather than the "how," making them easier to understand.

2. Implementing the Page Object Model in Playwright

a. Setting Up Your Project Structure

A well-organized project structure is crucial for implementing POM effectively. Here's an example of how you might structure your Playwright project:

/tests
  /pages
    homePage.ts
    loginPage.ts
  home.spec.ts
  login.spec.ts
/playwright.config.ts
Enter fullscreen mode Exit fullscreen mode
  • /pages: This directory contains your page objects, each representing a different page or component of your application.
  • /tests: This directory holds the test files that leverage the page objects.

b. Creating a Page Object Class

Let’s create a loginPage.ts file that encapsulates the logic for interacting with the login page of your application:

import { Page } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly usernameInput: string;
  readonly passwordInput: string;
  readonly loginButton: string;

  constructor(page: Page) {
    this.page = page;
    this.usernameInput = 'input[name="username"]';
    this.passwordInput = 'input[name="password"]';
    this.loginButton = 'button[type="submit"]';
  }

  async navigate() {
    await this.page.goto('/login');
  }

  async login(username: string, password: string) {
    await this.page.fill(this.usernameInput, username);
    await this.page.fill(this.passwordInput, password);
    await this.page.click(this.loginButton);
  }

  async isLoginSuccessful(): Promise<boolean> {
    return this.page.locator('text="Welcome back!"').isVisible();
  }
}
Enter fullscreen mode Exit fullscreen mode

This LoginPage class encapsulates the interactions with the login page, such as navigating to the page, entering credentials, and checking for a successful login.

c. Writing Tests with Page Objects

Once your page object is ready, you can use it in your tests:

import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/loginPage';

test('should log in successfully', async ({ page }) => {
  const loginPage = new LoginPage(page);

  await loginPage.navigate();
  await loginPage.login('user', 'password');

  const isSuccess = await loginPage.isLoginSuccessful();
  expect(isSuccess).toBe(true);
});
Enter fullscreen mode Exit fullscreen mode

This test scenario is now clean, focused, and easy to maintain, thanks to the encapsulation provided by POM.


3. Diving Deeper: Advanced Concepts in Playwright with POM

a. Composition of Page Objects

For larger applications, you may have pages composed of multiple components. You can break down these components into smaller page objects and then compose them into a single page object.

Example:

// components/header.ts
import { Page } from '@playwright/test';

export class Header {
  readonly page: Page;
  readonly profileMenu: string;

  constructor(page: Page) {
    this.page = page;
    this.profileMenu = '#profile-menu';
  }

  async openProfile() {
    await this.page.click(this.profileMenu);
  }
}
Enter fullscreen mode Exit fullscreen mode
// pages/dashboardPage.ts
import { Page } from '@playwright/test';
import { Header } from '../components/header';

export class DashboardPage {
  readonly page: Page;
  readonly header: Header;

  constructor(page: Page) {
    this.page = page;
    this.header = new Header(page);
  }

  async navigate() {
    await this.page.goto('/dashboard');
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, the DashboardPage class encapsulates the Header component, demonstrating how you can modularize and compose page objects.

b. Handling Dynamic Content

Dynamic content, such as search results or user-generated content, can make testing more complex. Playwright provides powerful tools to handle such scenarios effectively.

Example:

export class SearchResultsPage {
  readonly page: Page;

  constructor(page: Page) {
    this.page = page;
  }

  async getResultCount(): Promise<number> {
    await this.page.waitForSelector('.search-result');
    return this.page.locator('.search-result').count();
  }
}
Enter fullscreen mode Exit fullscreen mode

This page object handles dynamic content by waiting for elements to appear before interacting with them, ensuring that your tests are robust and reliable.

c. Extending Page Objects for Different User Roles

In some applications, different user roles might interact with the same page differently. You can extend your page objects to handle these variations.

Example:

export class AdminDashboardPage extends DashboardPage {
  async deleteUser(userId: string) {
    await this.page.click(`button[data-user-id="${userId}"]`);
  }
}
Enter fullscreen mode Exit fullscreen mode

By extending the DashboardPage, the AdminDashboardPage adds admin-specific functionality, making your tests more modular and maintainable.


4. Best Practices for POM in Playwright

a. Keep Page Objects Focused

Page objects should be responsible only for interactions with the UI. Avoid adding business logic or assertions to them.

b. Reuse Page Objects

Reuse page objects across tests to minimize duplication and ensure consistency.

c. Modularize for Scalability

As your application grows, break down large page objects into smaller, reusable components. This will make your tests easier to maintain and scale.

d. Consistent Naming Conventions

Adopt consistent naming conventions for selectors and methods within your page objects to improve readability and ease of use.


5. Conclusion

The Page Object Model is a powerful pattern for organizing and maintaining your Playwright tests, especially as your application grows in complexity. By encapsulating page interactions, you can write more modular, reusable, and maintainable tests.

Incorporating advanced concepts like composition, dynamic content handling, and role-specific page objects will help you get the most out of POM in your Playwright setup. As you continue to develop your testing strategy, keep these best practices in mind to ensure your tests remain robust and scalable.

For further insights into Playwright and testing strategies, revisit my previous post, and continue exploring the full capabilities of this powerful testing tool.


Happy Coding 👨‍💻
Enter fullscreen mode Exit fullscreen mode

Top comments (0)