πŸ—οΈ Building Playwright Framework Step By Step - Setup Design Pattern
idavidov13

idavidov13 @idavidov13

About: Automation QA | Driving Software Quality & Scalable Solutions in Crypto and FinTech | Mentoring QA Talent

Location:
Sofia, Bulgaria
Joined:
Feb 23, 2025

πŸ—οΈ Building Playwright Framework Step By Step - Setup Design Pattern

Publish Date: May 28
6 0

βœ… Prerequisites

This article builds directly on the concepts from previous ones. To get the most out of it, you should have:

🎯 Importance of Design Pattern

The importance of employing design patterns in test automation cannot be overstated! It serves as a blueprint for organizing interaction with the user interface (UI) elements of web pages in a structured and reusable manner. 🎭

πŸ’‘ What is a Design Pattern? A proven solution to common problems in software design that provides a template for how to solve problems in various situations

πŸš€ Why Design Patterns Matter

Design patterns provide several critical benefits:

  • πŸ”§ Enhanced Maintainability - Centralized UI changes management
  • πŸ“– Improved Readability - Cleaner, more efficient code
  • πŸ”„ Reduced Code Duplication - Reusable components
  • πŸ—οΈ Better Structure - Organized and scalable architecture
  • πŸ›‘οΈ Increased Robustness - More reliable test automation

By abstracting the UI structure away from the test scripts, Design Patterns enable testers to write cleaner, more efficient code. Changes to the UI can be managed in a centralized manner, minimizing the impact on tests and improving the robustness of the automation suite.

⚑ Result: More scalable, maintainable, and reliable test automation strategies that align with software development best practices

πŸ€” POM (Page Object Model) vs Functional Helpers

Both Page Object Model (POM) and Functional Helpers are popular design patterns used to enhance test automation frameworks. Let's explore the key differences:

πŸ›οΈ Page Object Model (POM)

Aspect Description Benefits
πŸ—οΈ Structure Organizes web UI elements into objects corresponding to pages/components Clear page-based organization
πŸ”§ Maintenance Centralizes UI changes, ideal for frequently changing applications Easy to update and maintain
πŸ“– Readability Abstracts UI specifics into methods, making tests read like user stories Highly readable test scripts
♻️ Reusability High reusability across different tests for same page/component Maximum code reuse
πŸ“š Learning Curve Steeper due to separate page object layer design Requires architectural planning

βš™οΈ Functional Helpers

Aspect Description Benefits
πŸ—οΈ Structure Uses functions for common tasks without strict page binding Flexible function-based approach
πŸ”§ Maintenance Straightforward for small projects, challenging for large suites Simple for small-scale projects
πŸ“– Readability Abstracts UI specifics into functions for better readability Good readability with functions
♻️ Reusability Moderate reusability, may need adjustments across contexts Limited cross-context reuse
πŸ“š Learning Curve Lower initial setup, more intuitive for simple projects Quick to get started

🎯 Which Should You Choose?

πŸ’‘ Decision Factors:

  • Project Scale: Large/complex β†’ POM, Small/simple β†’ Functional Helpers
  • Team Experience: Experienced β†’ POM, Beginners β†’ Functional Helpers
  • UI Complexity: Complex/changing β†’ POM, Static/simple β†’ Functional Helpers
  • Long-term Maintenance: Long-term β†’ POM, Short-term β†’ Functional Helpers

Decision taken: For this series, we'll implement POM as it's more popular and provides better scalability for real-world applications.

πŸ› οΈ POM Setup

Since POM Design Pattern is more popular and scalable, we will implement it in our project. There are several different implementations, but I'll show you the two most effective approaches.

Step 1: Create Folder Structure

Create a logical folder structure in your project's root directory:

project-root/
β”œβ”€β”€ pages/
β”‚   └── clientSite/
β”‚       β”œβ”€β”€ HomePage.ts
β”‚       β”œβ”€β”€ NavPage.ts
β”‚       └── ArticlePage.ts
β”œβ”€β”€ tests/
└── playwright.config.ts
Enter fullscreen mode Exit fullscreen mode

πŸ—οΈ Why This Structure?: This gives you flexibility to extend with Admin Panel or other application sections later

Step 2: Create Page Object Files

Create and implement page objects for all pages of the application. We'll create page objects for:

  • 🏠 Home Page - Main landing page functionality
  • 🧭 Nav Page - Navigation bar (present on every page, but defined once)
  • πŸ“„ Article Page - Article creation and management

Step 3: Create Page Object Classes

πŸ“š Complete Implementation: The three page objects are fully implemented in the GitHub repository

Before we continue, you can learn more about Classes in TypeScript.
Let's examine the Article Page as our primary example:

import { Page, Locator, expect } from '@playwright/test';

/**
 * This is the page object for Article Page functionality.
 * @export
 * @class ArticlePage
 * @typedef {ArticlePage}
 */
export class ArticlePage {
    constructor(private page: Page) {}

    get articleTitleInput(): Locator {
        return this.page.getByRole('textbox', {
            name: 'Article Title',
        });
    }
    get articleDescriptionInput(): Locator {
        return this.page.getByRole('textbox', {
            name: "What's this article about?",
        });
    }
    get articleBodyInput(): Locator {
        return this.page.getByRole('textbox', {
            name: 'Write your article (in',
        });
    }
    get articleTagInput(): Locator {
        return this.page.getByRole('textbox', {
            name: 'Enter tags',
        });
    }
    get publishArticleButton(): Locator {
        return this.page.getByRole('button', {
            name: 'Publish Article',
        });
    }
    get publishErrorMessage(): Locator {
        return this.page.getByText("title can't be blank");
    }
    get editArticleButton(): Locator {
        return this.page.getByRole('link', { name: ' Edit Article' }).first();
    }
    get deleteArticleButton(): Locator {
        return this.page
            .getByRole('button', { name: 'ο‰’ Delete Article' })
            .first();
    }

    /**
     * Navigates to the edit article page by clicking the edit button.
     * Waits for the page to reach a network idle state after navigation.
     * @returns {Promise<void>}
     */
    async navigateToEditArticlePage(): Promise<void> {
        await this.editArticleButton.click();

        await this.page.waitForResponse(
            (response) =>
                response.url().includes('/api/articles/') &&
                response.request().method() === 'GET'
        );
    }

    /**
     * Publishes an article with the given details.
     * @param {string} title - The title of the article.
     * @param {string} description - A brief description of the article.
     * @param {string} body - The main content of the article.
     * @param {string} [tags] - Optional tags for the article.
     * @returns {Promise<void>}
     */
    async publishArticle(
        title: string,
        description: string,
        body: string,
        tags?: string
    ): Promise<void> {
        await this.articleTitleInput.fill(title);
        await this.articleDescriptionInput.fill(description);
        await this.articleBodyInput.fill(body);

        if (tags) {
            await this.articleTagInput.fill(tags);
        }

        await this.publishArticleButton.click();

        await this.page.waitForResponse(
            (response) =>
                response.url().includes('/api/articles/') &&
                response.request().method() === 'GET'
        );

        await expect(
            this.page.getByRole('heading', { name: title })
        ).toBeVisible();
    }

    /**
     * Edits an existing article with the given details.
     * @param {string} title - The new title of the article.
     * @param {string} description - The new description of the article.
     * @param {string} body - The new content of the article.
     * @param {string} [tags] - Optional new tags for the article.
     * @returns {Promise<void>}
     */
    async editArticle(
        title: string,
        description: string,
        body: string,
        tags?: string
    ): Promise<void> {
        await this.articleTitleInput.fill(title);
        await this.articleDescriptionInput.fill(description);
        await this.articleBodyInput.fill(body);

        if (tags) {
            await this.articleTagInput.fill(tags);
        }

        await this.publishArticleButton.click();

        await this.page.waitForResponse(
            (response) =>
                response.url().includes('/api/articles/') &&
                response.request().method() === 'GET'
        );

        await expect(
            this.page.getByRole('heading', { name: title })
        ).toBeVisible();
    }

    /**
     * Deletes the currently selected article.
     * @returns {Promise<void>}
     */
    async deleteArticle(): Promise<void> {
        await this.deleteArticleButton.click();

        await expect(this.page.getByText('Global Feed')).toBeVisible();
    }
}

Enter fullscreen mode Exit fullscreen mode

It is debatable if using only methods leads to easier implementation. My opinion is to stick with get functions and use them into the methods.

🎯 What's Next?

In the next article we will dive into implementing POM (Page Object Model) as Fixture and creating Auth User Session.

πŸ’¬ Community: Please feel free to initiate discussions on this topic, as every contribution has the potential to drive further refinement.


✨ Ready to supercharge your testing skills? Let's continue this journey together!


πŸ™πŸ» Thank you for reading! Building robust, scalable automation frameworks is a journey best taken together. If you found this article helpful, consider joining a growing community of QA professionals πŸš€ who are passionate about mastering modern testing.

Join the community and get the latest articles and tips by signing up for the newsletter.

Comments 0 total

    Add comment