In this tutorial, we will build a Playwright End to End Testing Framework and prepare the architecture required for executing real-world automation workflows. We will implement and execute a complete end-to-end test case that validates the product purchase workflow in our automation demo application.
Test Case We Are Going to Execute
Before jumping into framework code, let’s clearly understand the scenario.
✅ Test Scenario: Verify User Can Add Products to Cart and Place an Order
📌 Test Steps:
- Navigate to the application
- Login with valid credentials
- Go to Products page
- Add multiple products to cart
- Navigate to View Cart page
- Validate selected products
- Proceed to place order
- Verify order confirmation
This is a classic E2E workflow test covering:
- UI interactions
- Page navigation
- Data validation
- Business flow validation
Framework Structure Used
To implement this test cleanly, we are following the Page Object Model (POM) approach.
We have:
Products.ts→ Handles product-related actionsViewCart.ts→ Handles cart page operationsorders.spec.ts→ Contains the actual test case
This keeps our test:
✔ Maintainable
✔ Reusable
✔ Clean
✔ Scalable
Step 1: Products Page Object (Products.ts)
This file contains everything related to the Products page.
Instead of writing page.locator() in the test file, we move that logic here. It contains methods like:
async addToCart(productName : string){
const product = this.page.locator('.product', {
has: this.page.getByRole('heading', { name: productName })
});
await product.getByRole('button', { name: 'Add to Cart' }).click();
}
async openCart(){
await this.cartLink.click();
}
SInce we have multiple products , we need the code to be able to select any product and therefore we must provide a filtering mechanism while finding product name.
Inside add to cart method :
productName : string → Accepts dynamic product name (For Eg : ‘Playwright Book’);
It finds:
- A
.productcontainer - That contains a heading
- With text matching
productName
Once the product is selected , we find a button ‘Add to Cart’ corresponding to the product and click on it.
openCart method is used to click on the View Cart menu
Step 2: ViewCart.ts – Handles Cart Page Actions
In View Cart, we will view the products added, we will increase or decrease the quantity of product , verify the total ordering cost and buy or reset the products. Let’s have a look at the code
/ Row anchor
getProduct(product: string): Locator {
return this.table.getByRole('row', {
name: new RegExp(product),
});
}
getProductQuantity(product: string): Locator {
return this.getProduct(product)
.locator('span[data-testid^="qty-"]');
}
getProductPrice(product: string): Locator {
return this.getProduct(product).locator('td').nth(3);
}
async increaseQuantity(product: string) {
await this.getProduct(product)
.getByRole('button', { name: '+' })
.click();
}
async decreaseQuantity(product: string) {
await this.getProduct(product)
.getByRole('button', { name: '-' })
.click();
}
async buyProduct() {
await this.buy.click();
}
async resetCart() {
await this.reset.click();
}
getContinueCta(): Locator {
return this.continueCta;
}
Here getProduct method will work as row anchor, this method finds and returns a specific product row from a table based on the product name. We will use this method in the subsequent methods to fetch the required products from the table in View Cart.
new RegExp(product) allows filtering rows by accessible name. It finds the row whose text matches the product name. This allows:
- Case-insensitive option (if added)
- Partial match
- Flexible matching
if table row contains ‘Playwright Book – 999’ and we pass ‘Playwright Book’ to test case , regular expression will pass it, without it might fail.
Other methods we have in this page to increase or decrease quantity , view qantity and price for validation purpose, buy or reset carts . we will use these methods in orders.spec.ts where we will write the whole test case which will automate the whole flow.
You can find the full code for product and cart below :
Products.ts
import { Locator, Page } from '@playwright/test';
export class Products{
readonly page : Page ;
readonly cartLink : Locator;
constructor(page : Page){
this.page = page ;
this.cartLink = page.getByTestId('menu-cart');
}
async addToCart(productName : string){
const product = this.page.locator('.product', {
has: this.page.getByRole('heading', { name: productName })
});
await product.getByRole('button', { name: 'Add to Cart' }).click();
}
async openCart(){
await this.cartLink.click();
}
}
ViewCart.ts
import { Locator, Page } from '@playwright/test';
export class ViewCart {
readonly page: Page;
readonly table: Locator;
readonly buy: Locator;
readonly reset: Locator;
readonly continueCta: Locator;
constructor(page: Page) {
this.page = page;
this.table = page.getByTestId('cart-table');
this.buy = page.getByRole('button', { name: 'Buy Now' });
this.reset = page.getByRole('button', { name: 'Reset Cart' });
this.continueCta = page.getByRole('link', { name: /Continue Shopping/ });
}
// Row anchor
getProduct(product: string): Locator {
return this.table.getByRole('row', {
name: new RegExp(product),
});
}
getProductQuantity(product: string): Locator {
return this.getProduct(product)
.locator('span[data-testid^="qty-"]');
}
getProductPrice(product: string): Locator {
return this.getProduct(product).locator('td').nth(3);
}
async increaseQuantity(product: string) {
await this.getProduct(product)
.getByRole('button', { name: '+' })
.click();
}
async decreaseQuantity(product: string) {
await this.getProduct(product)
.getByRole('button', { name: '-' })
.click();
}
async buyProduct() {
await this.buy.click();
}
async resetCart() {
await this.reset.click();
}
getContinueCta(): Locator {
return this.continueCta;
}
}
In the next chapter, we will use these methods in the orders.spec.ts file, where we will write the whole test case which will automate the whole flow.
