Learn Playwright Automation On a Demo E-Commerce Store Using CSS Selectors
- Anuradha Agarwal
- Feb 19
- 10 min read
Introduction
Modern web applications are dynamic, interactive, and driven by JavaScript frameworks. To automate such applications reliably, it is essential to learn how automation tools interact with real user interface elements such as forms, authentication workflows, navigation menus, filters, sliders, and checkout processes.
If you are learning Playwright test automation using JavaScript or TypeScript, the most effective way to build real confidence is by practising on a real-world application that closely resembles a production environment. This includes workflows such as login, updating account settings, searching for products, applying filters, completing checkout, and validating data such as order IDs.
In this tutorial, we will perform end-to-end automation testing using Playwright on a live demo e-commerce store:
Demo application:https://qa-cart.com
This application includes realistic user workflows and modern UI components, making it an ideal practice environment for learning Playwright automation.
Make sure you have manually registered your username in the demo store.
Learning Strategy: Why We Start with CSS Selectors First
Playwright provides powerful built-in locator methods such as:
getByRole()
getByText()
getByLabel()
getByTestId()
These are considered best practice for building stable automation tests.
However, in real automation projects, engineers must also understand how to work directly with the DOM using CSS selectors. Therefore, in this first tutorial, we will intentionally use CSS selectors instead of Playwright locator shortcuts.
This approach helps you:
Understand how Playwright interacts with DOM elements
Build strong selector identification and debugging skills
Learn how to automate complex and dynamic UI components
Improve troubleshooting ability in real-world automation projects
Develop a solid foundation before moving to advanced locator strategies
Once you understand the fundamentals, upgrading to Playwright locators becomes much easier and more meaningful. Here is the plan:
Phase 1 — Foundation (This Tutorial)
You will learn to automate complete user workflows using CSS selectors, including:
Login and logout workflows
Form automation using radio buttons, checkboxes, dropdowns, and date pickers
Product search and filtering
Slider automation using JavaScript evaluation
Cart and checkout automation
Capturing and validating dynamic order IDs
Verifying order persistence in account history
Phase 2 — Modern Playwright Best Practices (Next Tutorial)
We will upgrade the same scripts using:
Playwright built-in locators (getByRole, getByLabel, etc.)
Locator chaining and scoped locators
Improved readability and stability
fixtures
using productivity tool - codegen
In This Tutorial
We will automate two complete real-world workflows.
Workflow 1: Update Account Details
This workflow focuses on form automation and data persistence validation.
You will learn how to automate:
User login
Navigation to account settings
Selecting radio buttons, checkboxes, and preferences
Saving account changes
Verifying success messages
Validating persistence of selected values
Logout
This workflow helps you understand how Playwright interacts with form elements and validates the application state.
Workflow 2: Buy a Product (End-to-End E-commerce Checkout)
This workflow simulates a real customer purchase.
You will learn how to automate:
Login and navigation
Product search and result validation
Filtering using a price slider
Adding products to the cart
Completing checkout
Placing an order
Capturing dynamic Order ID
Verifying order in the account order history
Logout
This workflow introduces advanced automation concepts such as:
Automating dynamic UI components
Handling JavaScript-driven elements
Working with network-triggered UI updates
Capturing and validating runtime data
Although we could have directly added a product to the cart, we intentionally included search and filtering steps to help you learn how to automate real-world application interactions.
What You Will Learn
In this tutorial, you will learn how to automate real-world user workflows, including:
Workflow 1 – Update Account Details
Login
Navigate to Account Details
Update preferences using radio buttons, checkboxes - multiple choice, single choice, date picker, dropdown select.
Verify changes are saved
Logout
Workflow 2 – Buy a Product
Login
Browse products
Add product to cart
Complete checkout
Fetch Order Id
View Order Table and Verify order
Logout
Note: Each workflow includes login and logout to keep tests independent. We are not introducing fixtures yet—fixtures come later to optimize login/logout and speed up the suite.
Prerequisites
Before starting, ensure you have:
Node.js installed
Visual Studio Code installed
Playwright installed
Create a test file:
tests/demo-store-workflows.spec.ts
Run tests using:
npx playwright test
If you haven’t set up Playwright yet, follow these guides:
Beginner’s Guide to Playwright Setup:https://www.anuradhaagarwal.com/post/beginner-s-guide-to-setting-up-playwright-with-node-js-and-vs-code-for-javascript-and-typescript-aut
Playwright Installation and Hands-On Overview:https://www.anuradhaagarwal.com/post/playwright-tutorial-for-beginners-installation-and-hands-on-overview
If you’re new to DOM and selectors, start here:https://www.anuradhaagarwal.com/post/understanding-dom-and-locators-in-playwright-a-beginner-s-guide-to-stable-test-automation
Workflow 1: Update Account Details
This workflow validates the ability of a user to update and persist account preferences.
Step 1: Login
import { test, expect } from '@playwright/test';
test("Workflow 1: Update Account Details", async ({ page }) => {
await page.goto("https://qa-cart.com/");
await page.locator("input[name='username']")
.fill("anuradha.learn@gmail.com");
await page.locator("input[name='password']")
.fill("*********");
await page.locator("button[name='login']").click();
await expect(
page.locator("a[href*='customer-logout']")
).toBeVisible();
Step 2: Navigate to Account Details
await page.locator(
".woocommerce-MyAccount-navigation-link--edit-account a"
).click();
await expect(
page.locator("h1.entry-title")
).toHaveText("Account details");
Step 3: Update Preferences
await page.locator(
"input[type='checkbox'][name='qa_interests[]'][value='playwright']"
).check();
await page.locator(
"input[type='checkbox'][name='qa_interests[]'][value='ai_testing']"
).check();
Step 4: Save Changes
await page.locator(
"button[name='save_account_details']"
).click();
Step 5: Verify Success Message
const successMessage = page.locator(
".woocommerce-message"
);
await expect(successMessage).toBeVisible();
await expect(successMessage)
.toContainText("Account details changed successfully");
Step 6: Verify Persistence
await page.locator(
".woocommerce-MyAccount-navigation-link--edit-account a"
).click();
await expect(
page.locator(
"input[value='beginner']"
)
).toBeChecked();
await expect(
page.locator(
"input[value='playwright']"
)
).toBeChecked();
await expect(
page.locator(
"input[value='ai_testing']"
)
).toBeChecked();
Step 7: Logout
await page.locator(
"a[href*='customer-logout']"
).click();
await expect(
page.locator("button[name='login']")
).toBeVisible();
});Workflow 2 – Buy a Product (End-to-End Checkout Flow on DemoShop)
In Workflow 1 (Update Account Details), our focus was on an account settings workflow: login → update preferences → verify success message → logout. Workflow 2 is different because it covers a core e-commerce transaction: browsing/searching → filtering results → selecting a product → adding to cart → checkout → placing order → verifying the order appears in “Orders”.
This workflow helps you practice:
Search + results assertions
Filtering (price slider) + network-based debugging
Cart and checkout flows
Capturing and validating dynamic data (Order ID)
Verifying that the order is stored in the account history
What we will automate
Steps
Login
Open DemoShop
Search for a keyword (example: “organic”)
Verify search results show products
Apply a max price filter ($25) using the price slider
Verify filtered product prices are within range
Open the first product
Add to cart
Go to cart → checkout → place order
Capture Order ID
Go to My Account → Orders → verify order exists
Logout
Why is this workflow different from the Update Account workflow?
Update Account workflow
Mostly form field interactions (radio/checkbox/date/select)
Success verification is usually a message like “Account details changed successfully”
Persistence validation means reopening the page and checking fields are still selected
Buy Product workflow
Includes data-driven steps (search keyword, price thresholds)
Requires multi-page navigation (shop → product → cart → checkout → confirmation → orders)
Generates a dynamic ID (Order ID) that must be captured and reused
Includes filtering that may depend on JavaScript and network calls
Although we could have skipped the search and price filter steps and directly added a product to the cart, I included them intentionally to demonstrate how to automate different types of elements. This allows us to learn how Playwright handles search fields, dynamic content updates, sliders, and filtered results—common interactions in modern web applications.
Scenario: Buy a Product (CSS selector version)
Create a test file:
tests/demo-store-practice_css.spec.ts
Step 0: Test Skeleton
import { test, expect } from "@playwright/test";
test("Workflow 2: Buy a Product", async ({ page }) => {
// steps will go here
});
Step 1: Login
Goal: Authenticate the user so we can complete shopping actions under a logged-in account.
await page.goto("https://qa-cart.com/");
await page.locator("input[name='username']").fill("anuradha.learn@gmail.com");
await page.locator("input[name='password']").fill("Play@1234#$");
await page.locator("button[name='login']").click();
// Assertion: Logout link should be visible (means login success)
await expect(page.locator("a[href*='customer-logout']")).toBeVisible();
Step 2: Open DemoShop (and handle multiple matching links)
You noticed: a[href*="demoshop"] returns multiple elements, so we select only the visible one.
await page.locator('a[href*="demoshop"]').filter({ visible: true }).click();
await expect(page.locator(".woocommerce-products-header__title")).toHaveText("DemoShop");
Optional debugging
To prove which links are visible:
const links = page.locator('a[href*="demoshop"]');
for (let i = 0; i < await links.count(); i++) {
console.log(i, await links.nth(i).isVisible());
}
Step 3: Search for a keyword (“organic”)
await page.locator('input[type="search"]').fill("organic");
await page.keyboard.press("Enter");
Step 4: Verify search results loaded
Verify search heading contains keyword (case-insensitive)
await expect(page.locator("h1.page-title")).toContainText(/organic/i);
/organic/i makes it case-insensitive, so it matches:
organic
Organic
ORGANIC
Step 5: Verify products exist + validate first product title
Why we use: ul.products > li.product
The > is a child combinator. It selects only li.product that are direct children of ul.products, which makes the selector more precise.
const products = page.locator("ul.products > li.product");
expect(await products.count()).toBeGreaterThan(0);
First product title check
const firstProduct = products.first();
const firstProductTitle = firstProduct.locator("h2.woocommerce-loop-product__title");
await expect(firstProductTitle).toContainText(/organic/i);
Step 6: Apply Price Filter (Slider) — MAX $25-Using evaluate()
This is the most important learning part of the workflow.
When automating the price filter slider, our first instinct was to use standard Playwright methods like .fill():
await maxRange.fill("2500");
However, this did not work. Playwright reported that the element was not visible or editable, and even when the value visually changed, the product list did not update.
So instead of guessing, we debugged step by step using Chrome DevTools.
//Not working
const maxRange = page.locator("input[type='range'].wc-block-price-filter__range-input--max");
await maxRange.fill("2500");
await minRange.fill("1500")Why .fill() did not work
The max slider input exists, but Playwright may treat it as not visible / not editable, because the slider UI is controlled by styling and overlays.
So we use evaluate() to run JavaScript inside the browser and simulate real slider movement.
Inspect the slider element in Chrome DevTools
Open the page and press F12 → Elements tab, then inspect the max slider.
We found:
<input type="range"
class="wc-block-price-filter__range-input--max"
min="1500"
max="3500"
value="3500"
aria-valuetext="35">
This revealed two important facts:
This is a <input type="range">, not a normal text input
The value is stored in cents, not dollars
Example:
Displayed Price | Actual value |
$35 | 3500 |
$25 | 2500 |
$15 | 1500 |
Test manually in Chrome Console
Before writing Playwright code, we tested directly in the browser.
In DevTools Console, run:
const max = document.querySelector(".wc-block-price-filter__range-input--max");
max.value = "2500";
max.dispatchEvent(new InputEvent("input", {
bubbles: true,
cancelable: true
}));
Immediately, the product list refreshed.
This proved:
The slider listens to the input event
Changing value alone is not enough — the event must fire
Confirm behaviour usingthe Network tab
Next, open:
DevTools → Network tab → Fetch/XHR
Then move the slider manually.
You will see a request like:
collection-data?max_price=2500&calculate_price_range=true&_locale=user
This confirms:
The slider triggers a fetch request
That request refreshes the product list
Therefore, automation must trigger the same event
Convert Chrome Console logic into Playwright
Chrome Console version:
max.value = "2500";
max.dispatchEvent(new InputEvent("input", { bubbles: true }));
Playwright equivalent:
await maxRange.evaluate((el, value) => {
const input = el as HTMLInputElement;
input.value = String(value);
input.dispatchEvent(
new InputEvent("input", { bubbles: true })
);
}, 2500);
However, this still sometimes failed in modern JavaScript frameworks like WooCommerce Blocks.
Use native value setter (framework-friendly approach)
Modern frameworks often track input changes internally. Direct assignment like:
input.value = "2500";
may not trigger internal watchers.
So we use the browser’s native setter:
const setter = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
"value"
)!.set!;
This retrieves the real browser setter for the value property.
Then we call it explicitly:
setter.call(input, String(value));
This ensures the value change is fully recognised.
Dispatch the correct event: InputEvent("input")
This line simulates the exact event fired when a real user moves the slider:
input.dispatchEvent(
new InputEvent("input", {
bubbles: true,
cancelable: true
})
);
Why this works:
WooCommerce Blocks listens specifically to the "input" event
This event triggers the fetch request seen in the Network tab
This refreshes the product list
Wait for the filter to complete
Since we confirmed in the Network tab that filtering triggers a request to collection-data, we wait for that specific response:
await page.waitForResponse(res =>
res.url().includes("collection-data") &&
res.status() === 200
);
This ensures the product list has refreshed.
Sliders and modern UI components often require:
updating the value property
triggering the correct event
waiting for network response
Standard methods like .fill() or .click() may not work because the UI is controlled by JavaScript, not just HTML.
Using evaluate() allows us to interact directly with the browser DOM and simulate real user behaviour reliably.
Step 7: Verify all displayed prices are <= $25
const prices = page.locator("ul.products li.product span.price");
const count = await prices.count();
for (let i = 0; i < count; i++) {
const text = await prices.nth(i).textContent();
const price = parseFloat(text!.replace("$", ""));
expect(price).toBeLessThanOrEqual(25);
}
Step 8: Add the filtered first product to the cart
const addCartButton = page.locator('ul.products > li.product a[role="button"]');
await addCartButton.first().click();Step 9: Go To My Cart Page
Verify the product exists in the table
// go to cart page
const cartIcon = page.locator("a.cart-container").filter({ has: page.locator(":visible") });
await cartIcon.click();
await expect(page).toHaveURL(/mycart/);
//verify the product exists in the table
const cartProductNames = page.locator("td.product-name");
await expect(cartProductNames).toContainText(selectedProductName);While running multiple times, you will notice that the cart has a product from before because of the earlier test run. In that case, adding the same product again may increase the quantity instead of creating a new row.
For a completely clean start, we can either clear the cart via UI or use a faster API-based reset (we will cover the API approach in a later post).
To keep this workflow simple, we will only verify that the product exists in the cart.
Step 10: Proceed To Checkout and place an order
At this stage, we have already verified that our selected product exists in the cart. The next step is to move forward in the purchase workflow by clicking the Proceed to checkout button.
From the cart page HTML, WooCommerce provides a stable class for this button:
<a href=".../checkout" class="checkout-button button alt wc-forward">
This makes .checkout-button a reliable selector.
Automation step:
// Proceed to checkout
await page.locator(".checkout-button").click();Once the checkout button is clicked, WooCommerce loads the checkout page containing billing details, payment method, and order review section.
A reliable element to confirm the checkout page is loaded is the Order review heading:
<h3 id="order_review_heading">Your order</h3>
Assertion:
await expect(
page.locator('h3[id="order_review_heading"]')
).toBeVisible();
This confirms that navigation to the checkout page was successful.
WooCommerce provides the final submission button with attributes:
<button type="submit" value="Place order">
This is a stable and unique selector.
Automation step:
await page.locator(
'button[type="submit"][value="Place order"]'
).click();
When this button is clicked, WooCommerce processes the order and redirects to the order confirmation page.
Step 11: Verify Order Confirmation (Thank You Page)
After successful order placement, WooCommerce displays a confirmation message:
<p class="woocommerce-thankyou-order-received">
Thank you. Your order has been received.
</p>
This element is specifically designed to confirm order success.
Assertion:
const thankYouMessage = page.locator(
".woocommerce-thankyou-order-received"
);
await expect(thankYouMessage).toBeVisible();
await expect(thankYouMessage)
.toHaveText("Thank you. Your order has been received.");
This ensures the order was successfully placed.
Fetch the Order ID
WooCommerce assigns a unique order number and displays it inside:
<li class="woocommerce-order-overview__order order">
<strong>5610</strong>
</li>
We extract and store this value for later verification.
Automation step:
const orderId = await page.locator(
".woocommerce-order-overview__order strong"
).innerText();
console.log("Order ID:", orderId);
Example output:
Order ID: 5610
This order ID becomes important for validating order history.
To verify the order appears in the user's order history, we navigate to the My Account page.
Since Astra theme renders multiple menu versions (desktop and mobile), we click the visible menu item to avoid strict mode conflicts.
Automation step:
await page.locator(
"a.menu-link[href*='qa-cart.com']:visible"
)
.filter({ hasText: "My account" })
.click();
Next, click the Orders link:
await page.locator(
'.woocommerce-MyAccount-navigation-link a[href*="/orders"]'
).click();
WooCommerce displays all orders in a table:
<table class="woocommerce-orders-table">
Each order is represented by a row containing the order ID.
We locate the row containing our extracted order ID:
const orderRow = page.locator(
'.woocommerce-orders-table .woocommerce-orders-table__row',
{ hasText: orderId }
);
await expect(orderRow).toBeVisible();
This confirms:
The order was successfully placed
The order appears correctly in the user’s order history
Bonus — Access Elements Inside the Order Row
Once the row is located, you can interact with elements inside it, such as the View button:
await orderRow.locator("a.button").click();
This demonstrates Playwright’s powerful concept of scoped locators, where actions are performed only within a specific row instead of the entire page.
Finally Logout
// Logout
await page.locator(
"a[href*='customer-logout']"
).click();
await expect(
page.locator("button[name='login']")
).toBeVisible();Next Steps
In this tutorial, you learned how to automate complete real-world user workflows using Playwright and CSS selectors. You practiced automation across multiple types of UI elements, including authentication forms, account settings, search functionality, dynamic filters, cart workflows, checkout flows, and order validation.
More importantly, you learned how modern web applications behave internally — especially how JavaScript-driven components such as sliders and filters respond to events and network calls. This understanding is critical for building reliable automation tests.
By implementing these workflows step-by-step, you have built a strong foundation in:
Playwright automation fundamentals
DOM interaction and selector strategy
Handling dynamic UI elements
Validating backend-driven data such as order IDs
Writing reliable end-to-end automation tests
What’s Coming Next: Upgrade to Playwright Locators and Codegen
In the next tutorial, we will upgrade these exact same workflows using modern Playwright best practices.
You will learn:
How to replace CSS selectors with Playwright locators such as:
getByRole()
getByLabel()
getByText()
getByTestId()
How Playwright locators improve:
Test stability
Readability
Maintainability




Comments