top of page

Learn Playwright Automation On a Demo E-Commerce Store Using CSS Selectors


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:



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


  1. Login

  2. Open DemoShop

  3. Search for a keyword (example: “organic”)

  4. Verify search results show products

  5. Apply a max price filter ($25) using the price slider

  6. Verify filtered product prices are within range

  7. Open the first product

  8. Add to cart

  9. Go to cart → checkout → place order

  10. Capture Order ID

  11. Go to My Account → Orders → verify order exists

  12. 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


Never Miss a Post. Subscribe Now!

Thanks for submitting!

©anuradha agarwal

    bottom of page