DEV Community

Cover image for ๐Ÿ—๏ธ Building Playwright Framework Step By Step - Setup Design Pattern
idavidov13
idavidov13

Posted on • Edited on

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

๐ŸŽฏ 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

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.

Top comments (0)