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

Search for a command to run...

No comments yet. Be the first to comment.
Master Playwright automation! This step-by-step series explains 'how' & 'why' to build robust, scalable test frameworks. A practical, educational articles providing a solid foundation for all skill levels.
π― Introduction Fixtures in Playwright provide a powerful mechanism to set up the environment for your tests, manage resources, and share common objects or context across multiple tests! π These reusable components allow you to define custom setup a...
Structure your CLAUDE.md and skill files like a technical book, and your AI agent will read them like one.

Meet the meta skill that ties every other skill together and tells you how to actually drive the scaffold

A practical guide to HTTP methods, status codes, payload testing, and schema validation for API testers

After 30+ articles, the missing piece was always the deeper explanation. Now it's on video.

Watch an AI agent go from a single prompt to page objects, factories, and passing Playwright tests

This article builds directly on the concepts from previous ones. To get the most out of it, you should have:
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
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
Both Page Object Model (POM) and Functional Helpers are popular design patterns used to enhance test automation frameworks. Let's explore the key differences:
| 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 |
| 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 |
π‘ 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.
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.
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
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
π 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.
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.