๐Ÿ—๏ธ Building Playwright Framework Step By Step - Setup Design Pattern

Ivan DavidovIvan Davidov
6 min read

โœ… 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)

AspectDescriptionBenefits
๐Ÿ—๏ธ StructureOrganizes web UI elements into objects corresponding to pages/componentsClear page-based organization
๐Ÿ”ง MaintenanceCentralizes UI changes, ideal for frequently changing applicationsEasy to update and maintain
๐Ÿ“– ReadabilityAbstracts UI specifics into methods, making tests read like user storiesHighly readable test scripts
โ™ป๏ธ ReusabilityHigh reusability across different tests for same page/componentMaximum code reuse
๐Ÿ“š Learning CurveSteeper due to separate page object layer designRequires architectural planning

โš™๏ธ Functional Helpers

AspectDescriptionBenefits
๐Ÿ—๏ธ StructureUses functions for common tasks without strict page bindingFlexible function-based approach
๐Ÿ”ง MaintenanceStraightforward for small projects, challenging for large suitesSimple for small-scale projects
๐Ÿ“– ReadabilityAbstracts UI specifics into functions for better readabilityGood readability with functions
โ™ป๏ธ ReusabilityModerate reusability, may need adjustments across contextsLimited cross-context reuse
๐Ÿ“š Learning CurveLower initial setup, more intuitive for simple projectsQuick 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

๐Ÿ—๏ธ 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();
    }
}

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.

0
Subscribe to my newsletter

Read articles from Ivan Davidov directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ivan Davidov
Ivan Davidov

Automation QA Engineer, ISTQB CTFL, PSM I, helping teams improve the quality of the product they deliver to their customers. โ€ข Led the development of end-to-end (E2E) and API testing frameworks from scratch using Playwright and TypeScript, ensuring robust and scalable test automation solutions. โ€ข Integrated automated tests into CI/CD pipelines to enhance continuous integration and delivery. โ€ข Created comprehensive test strategies and plans to improve test coverage and effectiveness. โ€ข Designed performance testing frameworks using k6 to optimize system scalability and reliability. โ€ข Provided accurate project estimations for QA activities, aiding effective project planning. โ€ข Worked with development and product teams to align testing efforts with business and technical requirements. โ€ข Improved QA processes, tools, and methodologies for increased testing efficiency. โ€ข Domain experience: banking, pharmaceutical and civil engineering. Bringing over 3 year of experience in Software Engineering, 7 years of experience in Civil engineering project management, team leadership and project design, to the table, I champion a disciplined, results-driven approach, boasting a record of more than 35 successful projects.