top of page

Playwright API Testing Tutorial: From Basics to UI + API Integration with Real Framework Examples

When we start learning Playwright, most of our focus naturally goes toward UI automation.

We open the browser. We locate elements. We click buttons. We verify text on screen.

That is absolutely the right way to begin.


But not everything needs to be tested through the UI.

Many validations are faster, simpler, and more reliable when we interact directly with the system via APIs.


This is where API testing in Playwright becomes extremely useful.

Instead of always interacting with the application through the browser, we can:


  • Create data directly

  • Update records instantly

  • Validate backend logic

  • Clean up test data

  • Prepare the system state before UI tests

  • Combine API and UI workflows in a single framework


This makes our automation smarter and more practical.


In this post, we will take a step-by-step journey:

  1. Understand API testing fundamentals

  2. Learn how Playwright supports API automation

  3. Practice using freely available public APIs

  4. Build complete API workflows

  5. Integrate API testing into a real-world e-commerce framework

  6. Use APIs for test data cleanup with global teardown

  7. Combine UI + API validation for stronger test coverage


The goal is not just to send API requests. The goal is to understand how API testing fits into practical automation frameworks.


Before Writing Code – Let Us Understand Why API Testing Matters


Suppose we are testing an e-commerce application. We want to verify that an order is created correctly. One way is to perform everything through the UI:

login → search product → add to cart → checkout → place order → verify confirmation → open orders page → validate order details

This is useful. But now think from an automation design perspective. If the purpose is only to verify backend logic, repeating the entire UI journey every time may not be necessary. Sometimes we simply need to:

create order → verify response → update order → delete order

This can be done much faster through APIs.That is why mature automation frameworks always include both:

UI testing + API testing


What is API Testing?


API testing means sending requests directly to application endpoints and validating the response. API stands for Application Programming Interface.

Instead of clicking through UI screens, we validate the system behaviour directly.

This makes API testing: faster, more stable, very useful for setup and teardown, and very suitable for CI/CD pipelines


API Testing Fundamentals


Before we jump into Playwright code, we need to understand a few basic terms.


Endpoint

An endpoint is simply the URL where an API is available.

Example:

This endpoint returns sample post data.


HTTP Methods

Each request method tells the server what action we want to perform.


GET → retrieve data

POST → create data

PUT → update full data

PATCH → update partial data

DELETE → remove data


Example:

GET /products  
POST /orders PUT /customers/5 
DELETE  /orders/20

Status Code

Status codes tell us whether the request was successful.

200 → success
201 → resource created
204 → success with no content
400 → bad request
401 → unauthorized
403 → forbidden
404 → not found
500 → server error

Request Headers


Headers send additional information with the request.

Content-Type: application/json
Authorization: Bearer token
x-api-key: API key value

Request Body


Used mostly in POST, PUT, and PATCH requests.


Example:

{"name": "Anuradha","role": "QA Engineer"}

Response Body


The server sends data back as a response.

Example:

{"id": 101,"name": "Anuradha","role": "QA Engineer"}

Authentication


Some APIs are public. Some require credentials.

Common types:

API key, Bearer token, Basic authentication


For learning, we will start with public APIs so we can focus on understanding the workflow.


Why Learn API Testing in Playwright?




Our First API Test Using a Free Public API


We will start with JSONPlaceholder. It is a free public API designed for learning and testing.


Example endpoint:


Let us write our first Playwright API test.


import { test, expect } from '@playwright/test';
test('GET posts from public API', async ({ request }) => {
    const response = await request.get('https://jsonplaceholder.typicode.com/posts');
    expect(response.status()).toBe(200);
    const body = await response.json();
    expect(body.length).toBeGreaterThan(0);
    expect(body[0]).toHaveProperty('title');
});

What we are learning here:


  • How to send GET request

  • How to check the status code

  • How to parse a JSON response

  • How to validate response structure


Notice that we did not open any browser. This is pure backend validation.

Playwright report of test run
Playwright report of test run
Step-by-Step Understanding of the above flow

👉Playwright starts the test

test('GET posts from public API', async ({ request }) => {

Playwright provides a built-in fixture called request. This acts like a client that can directly call APIs.We are not opening a browser here. We are directly interacting with the backend.


👉Sending request to API endpoint

const response = await request.get('https://jsonplaceholder.typicode.com/posts');

Here, Playwright sends an HTTP GET request. Think of it as:


Client → API server → response comes back


The API endpoint returns a list of posts in JSON format.

Example response:

[
  {
    "userId": 1,
    "id": 1,
    "title": "sample title",
    "body": "sample body"
  }
]

👉 Validate status code

expect(response.status()).toBe(200);

200 means success. This confirms:

Request reached the server successfully. Server processed request successfully.If the status were 404 or 500, the test would fail.


👉 Convert response into JSON

const body = await response.json();

API responses usually come as JSON text. We convert that JSON into a JavaScript object so we can validate fields. Now, the body becomes an array of objects.


👉 Validate response data

expect(body.length).toBeGreaterThan(0);

We check:

API returned some data, the array is not empty


👉Validate specific field

expect(body[0]).toHaveProperty('title');

We confirm:

Each post contains a title field. This ensures the API response structure is correct.


Testing an E-commerce Style API using Playwright


Now that we understand the basic flow of an API test, let us move one step closer to a real-world scenario. Instead of working with generic sample data, we will now test an API that behaves like an e-commerce system.

For learning purposes, we will keep things simple and use a freely available public API.


Fake Store API


Fake Store API simulates an online shopping system. It provides endpoints for:

  • products

  • carts

  • users

  • categories


This makes it a very good practice API because it resembles real e-commerce applications.


Example endpoint:

This endpoint returns a list of products similar to what we see in online stores.


Writing our Playwright Test for Product Data


We will now create another Playwright test to fetch product information.

import { test, expect } from '@playwright/test';
test('Get product list from fake store api', async ({ request }) => {    
const response = await request.get(
        'https://fakestoreapi.com/products'
    );
    expect(response.status()).toBe(200);
    const products = await response.json();
    expect(products.length).toBeGreaterThan(0);
    expect(products[0]).toHaveProperty('title');
});

In this test:

Playwright sends a GET request to the Fake Store API. The API returns a list of product objects. We validate that:

  • The request was successful

  • API returned data

  • The product contains expected fields

expect(response.status()).toBe(200);

This confirms that the request was successful. But this alone is usually not sufficient.

A good API test validates more aspects of the response. For example:


  • Response structure

  • Required fields

  • Data types

  • Business logic

  • Headers

  • Behaviour when incorrect input is provided


Example: Validating Response Structure


Let us validate a specific product and check whether the API returns the correct data structure.

import { test, expect } from '@playwright/test';
test('validate product response structure', async ({ request }) => {
    const response = await request.get(
        'https://fakestoreapi.com/products/1'
    );
    expect(response.status()).toBe(200);
    const product = await response.json();
    expect(product).toHaveProperty('id');
    expect(product).toHaveProperty('title');
    expect(product).toHaveProperty('price');
    expect(typeof product.price).toBe('number');
});

In this test, we are checking:

API returns expected fields
id exists
title exists
price exists
price is numeric

This makes the test more meaningful because we are validating the actual structure of the response.

We are no longer checking only status code.
We are checking correctness of returned data.

Negative Testing Example


Good API testing also includes negative scenarios. We should verify how the system behaves when invalid input is provided.

Example:

import { test, expect } from '@playwright/test';
test('invalid product id should return error', async ({ request }) => {
    const response = await request.get(
        'https://fakestoreapi.com/products/999999'
    );
    expect(response.status()).toBe(404);
});
  • Negative testing helps us understand:

  • How the system handles invalid input

  • How errors are returned

  • whether proper status codes are used

  • how stable the API behaviour is


So far, we have worked with freely available public APIs that did not require authentication.

That helped us focus on understanding the basic flow:


send request → receive response → validate result


But in real automation projects, APIs are usually protected.


They often require:

  • API key

  • token

  • username/password

  • OAuth authentication


So, before we move into framework design and reusable helpers, it is important to understand how authentication works in API testing. Many APIs require authentication before they return data.


We will use the OpenWeather API :


Example working URL:


• API key must be generated first

• activation may take some time

• Once activated, the API starts returning a JSON response




Playwright Test Using API Key


import { test, expect } from '@playwright/test';test('Get weather data using API key', async ({ request }) => {
    const apiKey = 'YOUR_API_KEY';
    const response = await request.get(
        `https://api.openweathermap.org/data/2.5/weather?q=London&appid=${apiKey}`
    );
    expect(response.status()).toBe(200);
    const weatherData = await response.json();
    expect(weatherData).toHaveProperty('weather');
});

In automation projects, credentials should not be written directly in test files. Instead, we store them in .env.

Example:

WEATHER_API_KEY=your_actual_key
BASE_WEATHER_URL=https://api.openweathermap.org/data/2.5

Updated Playwright Test Using Environment Variable


First, install dotenv if it is not already installed:

npm install dotenv

Make sure the entry is in playwright.config.ts


import { test, expect } from '@playwright/test';
import dotenv from 'dotenv';
dotenv.config();
test('Get weather data using env api key', async ({ request }) => {
    const response = await request.get(
        `${process.env.BASE_WEATHER_URL}/weather`,
        {
            params: {
                q: 'London',
                appid: process.env.WEATHER_API_KEY,
                units: 'metric'
            }
        }
    );
    expect(response.status()).toBe(200);
    const data = await response.json();
    expect(data).toHaveProperty('weather');
    console.log(data.main.temp);
}
);

Using Environment Variables in CI/CD Pipelines


When tests run in CI/CD pipelines like GitHub Actions, we should not upload the .env file.

Instead, secrets are stored securely in repository settings.

Example:

GitHub → Settings → Secrets → Actions

Add:

WEATHER_API_KEYWC_KEY
WC_SECRET

Then reference them inside the workflow YAML.

Example:

env:  WEATHER_API_KEY: ${{ secrets.WEATHER_API_KEY }}

This keeps credentials secure while still allowing automation to run.Refer for more details:


Moving Beyond Single Calls


Up to this point, we have mostly tested individual API calls.

But systems usually do not work as isolated single calls.

They work through flows.

A customer gets created.->A record is fetched.->Something is updated.->Then something may be deleted or cleaned up.

That means API testing also needs to move from isolated requests to complete workflows.


Complete API Workflow Example


A common API workflow looks like this:


create data → retrieve it → update it → delete it

import { test, expect } from '@playwright/test';
test('complete api workflow example', async ({ request }) => {    
const createResponse = await request.post(
        'https://jsonplaceholder.typicode.com/posts',
        {
            data: {
                title: 'workflow',
                body: 'step by step',
                userId: 1
            }
        }
    );
    expect(createResponse.status()).toBe(201);
    const created = await createResponse.json();
    const id = created.id;
    const getResponse = await request.get(
        `https://jsonplaceholder.typicode.com/posts/${id}`
    );
    expect(getResponse.status()).toBe(200);
    const deleteResponse = await request.delete(
        `https://jsonplaceholder.typicode.com/posts/${id}`
    );
    expect(deleteResponse.status()).toBe(200);
}
);

Using API for Test Data Cleanup in Frameworks



Suppose UI tests are creating orders repeatedly in the demo store.



After some time, the environment becomes cluttered with test data. If we keep deleting those orders manually through the admin UI, it becomes slow and repetitive. This is exactly the kind of situation where API testing becomes very practical.API can help us clean up the environment quickly. Our test demo store is a WordPress-based WooCommerce store that typically exposes endpoints like these:

GET    /wp-json/wc/v3/orders
POST   /wp-json/wc/v3/products
DELETE /wp-json/wc/v3/orders/{id}

Authentication is usually handled using(can't be shared publicly here):

  • consumer key

  • consumer secret


These should be stored safely in .env.


Example:


So far in previous work, we have focused on authentication, test grouping, regression vs smoke workflows, and parallel execution.


Now let’s talk about another very important part of a practical automation framework:

test data cleanup. When we automate an end-to-end purchase workflow, the test creates real data in the application. In our case, that means a successful checkout creates a new order.

If we keep running the same workflow repeatedly, the environment starts getting cluttered with old test orders. That is why a good framework should not only create test data — it should also know how to clean it up after execution.

In Playwright, the best place to do this kind of once-per-run cleanup is:

globalTeardown.ts.


Global Teardown runs once after all tests finish.

Execution flow:

Setup project
↓Test execution
↓Orders created
↓Global teardown removes test data

This keeps the test environment clean for future runs.


In this section, we will first understand the cleanup API call manually in PowerShell, and then we will move the same logic into Playwright global teardown using TypeScript.

In the tutorials, I will use placeholders for API credentials for security reasons

Understand the WooCommerce REST API endpoint


To delete an order permanently, the endpoint looks like this:

DELETE /wp-json/wc/v3/orders/{orderId}?force=true

Example:

Here:

  • 5646 is the order ID

  • force=true means permanently delete the order instead of sending it to the trash


Let's first test it manually in PowerShell


Before automating cleanup in Playwright, it is always a good idea to verify the API manually.


Important note for Windows PowerShell


In Windows PowerShell, curl may behave differently because it can map to PowerShell’s own command behaviour.

To avoid confusion, use:

curl.exe

instead of just:

curl

If you encounter SSL revocation issues on Windows, you can use:

--ssl-no-revoke

List orders first

This helps confirm that the order exists before deleting it.

curl.exe --ssl-no-revoke -u "ck_yourKey:cs_yourSecret" "https://qa-cart.com/wp-json/wc/v3/orders?per_page=5"

This returns JSON containing recent orders. You can identify the order ID from the response.

Example order IDs:

5646
5645
5644

Step 2.2: Delete one order manually

Now let’s delete a single order.

curl.exe --ssl-no-revoke -u "ck_yourKey:cs_yourSecret" -X DELETE "https://qa-cart.com/wp-json/wc/v3/orders/5646?force=true"

If the delete is successful, WooCommerce returns the deleted order object as confirmation.

That means the request worked.


Step 2.3: Verify the order is actually deleted

Now try fetching the same order again:

curl.exe --ssl-no-revoke -u "ck_yourKey:cs_yourSecret" "https://qa-cart.com/wp-json/wc/v3/orders/5646"

If the order is permanently deleted, the response will look like this:

{  
"code": "woocommerce_rest_shop_order_invalid_id",  
"message": "Invalid ID.",
"data": 
{    
"status": 404
  }
}

This is the confirmation that the order no longer exists.


Move from manual cleanup to framework cleanup


Now let’s connect this idea to Playwright.


In a practical scenario, we do not want to delete orders after every run manually. Instead, we want the cleanup to happen automatically after the full test execution completes.

This is exactly what globalTeardown.ts is meant for.


Global Teardown is a step executed after all tests finish to clean test data.

Example:

Test Execution
      ↓Test creates orders
      ↓Orders remain in system
      ↓Global teardown deletes test orders

API Context Design Options in Playwright


Before we implement Global Teardown, it is important to understand how API request context is created and reused in Playwright.


There are three common ways to create API context:


Option 1 – Create context directly in the test

Simple, but leads to repeated code.


Option 2 – Helper function (our approach)

Centralised authentication logic, reusable across tests, clean structure


Option 3 – Custom fixture (advanced approach)

Automatically injected into tests. Highly scalable framework design.We implement the helper approach first, then evolve into fixtures.This mirrors how real frameworks grow gradually





Helper Function for API Context


We create a reusable helper:

helpers/api_helper.ts


Responsibilities:

• load environment variables

• configure authentication

• create reusable API context


This avoids repeating authentication logic across tests.

import { request, APIRequestContext } from "@playwright/test";
import * as dotenv from "dotenv";

dotenv.config({ quiet: true });

export async function createApiContext(): Promise<APIRequestContext> {
  const baseURL = process.env.WC_BASE_URL;
  const consumerKey = process.env.WC_CONSUMER_KEY;
  const consumerSecret = process.env.WC_CONSUMER_SECRET;

  if (!baseURL || !consumerKey || !consumerSecret) {
    throw new Error("Missing WooCommerce API environment variables.");
  }

  const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString("base64");

  return await request.newContext({
    baseURL,
    extraHTTPHeaders: {
      Authorization: `Basic ${auth}`,
    },
  });
}

This helper function creates a reusable authenticated API client for our WooCommerce store.Instead of writing authentication logic in a repeated manner inside every test, we centralize it in one place.


What this helper does

1. Loads environment variables securely

dotenv.config({ quiet: true });

Reads sensitive values from the .env file instead of hardcoding credentials in test files.

Example values stored in .env:

This improves security and allows different environments (local, CI/CD, staging).


2. Reads WooCommerce credentials

const baseURL = process.env.WC_BASE_URL;
const consumerKey = process.env.WC_CONSUMER_KEY;
const consumerSecret = process.env.WC_CONSUMER_SECRET;

These values are required to authenticate API requests.


3. Validates required configuration

if (!baseURL || !consumerKey || !consumerSecret) {
  throw new Error("Missing WooCommerce API environment variables.");
}

Prevents test failures caused by missing credentials.


4. Creates Basic Authentication header

const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString("base64");

WooCommerce REST API uses Basic Authentication.Credentials are encoded into Base64 format and sent in the Authorization header.

Format:

Authorization: Basic base64_encoded_key_secret

5. Creates reusable API context

return await request.newContext({
  baseURL,  extraHTTPHeaders: {
    Authorization: `Basic ${auth}`,
  },
});

This creates an authenticated API client that can:

• send GET requests

• create orders

• retrieve order details

• delete test data

• interact with WooCommerce endpoints


How it is used in tests

import { test, expect } from "@playwright/test";
import { createApiContext } from "../helpers/api_helper";

test("global teardown: delete test orders", async () => {
  const apiContext = await createApiContext();

  try {
    const response = await apiContext.get("/wp-json/wc/v3/orders");

The helper automatically attaches authentication headers, so tests remain simple and readable.


Implementing Global Teardown


Global teardown deletes test orders automatically.


Steps:

  1. fetch existing orders

  2. loop through orders

  3. delete each order using API

  4. close API context


import { test, expect } from "@playwright/test";
import { createApiContext } from "../helpers/api_helper";

test("global teardown: delete test orders", async () => {
  const apiContext = await createApiContext();

  try {
    const response = await apiContext.get("/wp-json/wc/v3/orders");
    expect(response.ok()).toBeTruthy();

    const orders = await response.json();

    for (const order of orders) {
      const deleteResponse = await apiContext.delete(`/wp-json/wc/v3/orders/${order.id}`, {
        params: { force: true },
      });

      const deleteBody = await deleteResponse.text();
      console.log(`DELETE ${order.id}: ${deleteResponse.status()} ${deleteBody}`);

      expect(deleteResponse.ok(), `Failed to delete order ${order.id}: ${deleteBody}`).toBeTruthy();
    }
  } finally {
    await apiContext.dispose();
  }
});

This global teardown script automatically removes test-created orders from the WooCommerce demo store after all tests finish.


What this script does


1. Creates authenticated API client

const apiContext = await createApiContext();

Uses the helper function to create a reusable WooCommerce API client with authentication.


2. Fetches existing orders

const response = await apiContext.get("/wp-json/wc/v3/orders");

Retrieves list of orders currently present in the system.


3. Loops through each order

for (const order of orders)

Processes orders one by one.


4. Permanently deletes each order

await apiContext.delete(`/wp-json/wc/v3/orders/${order.id}`,
 {
  params: { force: true },
});

force=true ensures orders are deleted permanently instead of moving to trash.


5. Logs cleanup activity

console.log(`DELETE ${order.id}: ${deleteResponse.status()}`);

Helps debug cleanup execution.


6. Disposes API context

await apiContext.dispose();

Closes connection properly after cleanup finishes.



Register Global Teardown Using Project Dependency Approach


Instead of using the traditional global Teardown config property, Playwright recommends using project dependencies.


This gives better reporting visibility and better integration with fixtures and traces.


Step 1 – Create a cleanup project

File:

tests/global.teardown.ts

(contains the cleanup test shown above)


Step 2 – Register in playwright.config.ts

Complete configuration:

import { defineConfig, devices } from '@playwright/test';
import dotenv from "dotenv";

// dotenv.config();
dotenv.config({ quiet: true });
/**
 * See https://playwright.dev/docs/test-configuration.
 */
export default defineConfig({
  workers: process.env.CI ? 1 : undefined,
  testDir: './tests',
  retries: 2,
  timeout: 40 * 1000,
  expect: {
    timeout: 40 * 1000,
  },
  // reporter:'html',
  reporter: [
    ['line'],                 // console reporting
    ['html', { open: 'never' }],  // html reporting
    ["allure-playwright"],
  ],
  use: {
    baseURL: "https://qa-cart.com",   // 👈 global baseURL
    screenshot: "only-on-failure",
    video: "retain-on-failure",
    trace: "retain-on-failure",
    headless: process.env.CI ? true : false,
  },
  projects: [
    // 1) Setup project runs first
    {
      name: "setup",
      testMatch: /global\.setup\.ts/,
      teardown: 'cleanup',
    },
    {
      name: "cleanup",
      testMatch: /global\.teardown\.ts/,
    },

    // 2) Your main tests depend on setup
    {
      name: "chromium",
      testIgnore: [/global\.setup\.ts/, /global\.teardown\.ts/],
      use: {
        // baseURL: "https://qa-cart.com",
        storageState: "state.json",
      },
      dependencies: ["setup"],
    },

    // Optional: add other browsers later
    // {
    //   name: "firefox",
    //   use: { baseURL: "https://qa-cart.com", storageState: "state.json" },
    //   dependencies: ["setup"],
    // },
  ],

  // use: {
  //   browserName:"chromium",
  //   headless:false,
  //   baseURL: "https://qa-cart.com",
  //   storageState: "state.json",
  // },
  //  globalSetup: require.resolve("./helpers/global-setup.ts"),

});

Updating Playwright Config for Global Teardown



In our earlier setup, we already configured a setup project to handle tasks like login and saving storageState.


Now we are extending that same project-based approach to also support global teardown, so cleanup runs automatically after test execution finishes.This keeps the framework consistent:

setup → main tests → cleanup


The main modification is inside the projects section.

We already had:

  • a setup project for login

  • a chromium project for running actual tests


Now we add one more project:

  • a cleanup project for teardown


And then we connect it to the setup using:

teardown: 'cleanup'

This tells Playwright:

  • Run the setup project first

  • Then run dependent test projects like chromium

  • After that, run the cleanup project automatically



setup project

This was already introduced earlier for login and initial preparation.

{
  name: "setup",
  testMatch: /global\.setup\.ts/,
  teardown: "cleanup",
}

New addition here is:

teardown: "cleanup"

This links the setup flow to the cleanup project.


cleanup project

This project points to:

testMatch: /global\.teardown\.ts/

So Playwright knows which file should run as the teardown step.


chromium project

This remains your main execution project.

dependencies: ["setup"]

This means Chromium tests will only start after setup is completed.

And this line prevents setup/teardown files from running as normal tests:

testIgnore: [/global\.setup\.ts/, /global\.teardown\.ts/]

Execution flow now

Your framework now works like this:

global.setup.ts
      ↓actual UI/API test execution
      ↓global.teardown.ts

So:

  • setup prepares the environment

  • tests execute normally

  • teardown cleans the environment afterwards



This project-based structure keeps setup and cleanup inside Playwright’s normal execution model.

That means:

  • cleaner execution flow

  • easier maintenance

  • better separation of responsibility

  • setup and teardown remain visible as dedicated projects

It also fits very nicely with the framework design we already started earlier with project dependency setup.



Combining UI and API in One Test


So far, our examples have focused mainly on UI automation:


  • open the shop

  • add product to cart

  • checkout

  • place order

  • verify confirmation in UI


That is already a strong end-to-end workflow. But in automation, UI validation alone is often not enough.A success message in the browser tells us that the user flow has been completed. But how do we confirm that the backend actually stored the correct data?

This is where API validation becomes very useful.


Instead of relying only on the UI, we can combine:

  • UI automation to simulate real user actions

  • API validation to confirm backend data


This gives us much stronger confidence in the application.


In this example, we will:

  1. place an order through the UI

  2. Capture the generated order ID from the confirmation page

  3. Use the API to fetch that order

  4. validate that the backend contains the expected order data


Write the combined UI + API test



tests/order-ui-api.spec.ts


Improving with POM Model


import { test, expect } from "../fixtures/api-fixture";
import { HomePage } from "../pages/HomePage";
import { DemoShopPage } from "../pages/DemoShopPage";
import { CartPage } from "../pages/CartPage";
import { CheckoutPage } from "../pages/CheckoutPage";
import { OrdersPage } from "../pages/OrdersPage";
import { createApiContext } from "../helpers/api_helper";

const dataSet = JSON.parse(
  JSON.stringify(require("../data/demostore_purchase_data.json"))
);

const maxPrice = dataSet[0].maxPrice;
const searchKeyword = dataSet[0].searchKeyword;

test("@regression Shop: DemoShop opens and search returns results", async ({ page }) => {
  const homePage = new HomePage(page);
  const demoShopPage = new DemoShopPage(page);

  await homePage.goto();
  await homePage.openDemoShop();
  await demoShopPage.verifyPageLoaded();
  await demoShopPage.searchForProduct(searchKeyword);
  await demoShopPage.verifySearchResultsFor(searchKeyword);
});

test(`@regression Filter: max price (${maxPrice}) limits product prices`, async ({ page }) => {
  const demoShopPage = new DemoShopPage(page);

  await demoShopPage.goto();
  await demoShopPage.searchForProduct(searchKeyword);
  await demoShopPage.verifySearchResultsFor(searchKeyword);
  await demoShopPage.applyMaxPriceFilter(maxPrice);
  await demoShopPage.verifyPriceFilterApplied();
  await demoShopPage.verifyResultsCountVisible();
  await demoShopPage.verifyProductGridVisible();
  await demoShopPage.verifyProductsDisplayed();
  await demoShopPage.verifyAllDisplayedPricesAreAtMost(maxPrice);
});

test("@regression Cart: add first product and verify it appears in cart", async ({ page }) => {
  const demoShopPage = new DemoShopPage(page);
  const cartPage = new CartPage(page);

  await demoShopPage.goto();
  await demoShopPage.verifyProductGridVisible();

  const productName = await demoShopPage.addFirstProductToCart();

  await cartPage.goto();
  await cartPage.verifyPageLoaded();
  await cartPage.verifyCartHasItems();
  await cartPage.verifyProductPresent(productName);
});

test("@smoke @regression E2E: Shop → Cart → Checkout → Verify Order in UI and API", async ({ page}) => {
  const demoShopPage = new DemoShopPage(page);
  const cartPage = new CartPage(page);
  const checkoutPage = new CheckoutPage(page);
  const homePage = new HomePage(page);
  const ordersPage = new OrdersPage(page);

  let productName = "";
  let orderId = "";

  await test.step("Open shop and add first product to cart", async () => {
    await demoShopPage.goto();
    await demoShopPage.verifyProductGridVisible();
    productName = await demoShopPage.addFirstProductToCart();
    expect(productName).toBeTruthy();
  });

  await test.step("Validate product exists in cart", async () => {
    await cartPage.goto();
    await cartPage.verifyPageLoaded();
    await cartPage.verifyCartHasItems();
    await cartPage.verifyProductPresent(productName);
  });

  await test.step("Checkout and place the order", async () => {
    await cartPage.proceedToCheckout();
    await checkoutPage.verifyPageLoaded();
    orderId = await checkoutPage.placeOrder();
    expect(orderId).toBeTruthy();
    console.log("Created order ID:", orderId);
  });

  await test.step("Verify order appears in My Account → Orders", async () => {
    await homePage.goto();
    await homePage.verifyMyAccountPageLoaded();
    await homePage.openOrders();

    await ordersPage.verifyPageLoaded();
    await ordersPage.openOrderById(orderId);
    await ordersPage.verifyOrderDetailsPage(orderId);
  });

  await test.step("Verify order details through API", async () => {
    const apiContext = await createApiContext();
    const response = await apiContext.get(`/wp-json/wc/v3/orders/${orderId}`);
    expect(response.status()).toBe(200);

    const orderData = await response.json();

    expect(orderData.id).toBe(Number(orderId));
    expect(orderData.status).toBe("processing");
    expect(parseFloat(orderData.total)).toBeGreaterThan(0);
    expect(orderData.line_items.length).toBeGreaterThan(0);

    const apiProductName = orderData.line_items[0].name;
    const apiQuantity = orderData.line_items[0].quantity;

    expect(apiProductName).toContain(productName);
    expect(apiQuantity).toBe(1);
  });
});

What this test suite is doing


This test file contains 4 key scenarios covering UI + API validation:


1. Shop Navigation & Search

  • Opens homepage

  • Navigates to demo shop

  • Searches using keyword from test data

  • Verifies results are displayed

👉 Focus: basic UI navigation + search validation


2. Price Filter Validation

  • Searches for products

  • Applies max price filter

  • Validates:

    • Filter applied

    • Products visible

    • All prices ≤ maxPrice

👉 Focus: UI filtering logic + assertions


3. Cart Functionality

  • Adds first product to cart

  • Navigates to cart page

  • Verifies:

    • Cart has items

    • Correct product exists

👉 Focus: cart behavior validation


The above tests were achieved as part of the previous post


4. End-to-End (UI + API)


Flow:

  1. Add product to cart (UI)

  2. Validate cart (UI)

  3. Place order (UI)

  4. Verify order in "My Account" (UI)

  5. Validate order via API

const response = await apiContext.get(`/wp-json/wc/v3/orders/${orderId}`);

During the run, we observed that there is a Flaky Assertion

expect(apiQuantity).toBe(1);

Failure:

Expected: 1
Received: 2

Observation: Flaky Cart Quantity


This behaviour shows that the cart quantity is inconsistent and not always 1.


Why is this happening?


The primary reason is that the cart is not reset before the test runs.


Because of this:


  • Previous tests may have already added items to the cart

  • The current test adds another item on top of the existing ones

  • This results in unexpected quantities during order validation


Key Testing Problem Identified


The test is not isolated.

It depends on:

  • Previous test execution

  • Existing cart state


Impact

  • Inconsistent test results

  • Flaky failures (tests may pass or fail unpredictably)

  • Reduced confidence in test reliability


What this means


A good test should always run in a clean and predictable state.

What’s next


In the next step, we will make our tests independent and reliable by:


  • Clearing the cart using UI

  • Moving this logic into beforeEach for proper test isolation

  • Understanding session management and why the cart persists

  • Optimizing the process using the Cart API for faster execution


Further improvements


We will also enhance the current tests by:

  • Tracking orders created during the test session

  • Ensuring we only delete orders created by the tests

  • Avoiding accidental deletion of other users’ data

Comments


Never Miss a Post. Subscribe Now!

Thanks for submitting!

©anuradha agarwal

    bottom of page