Playwright API Testing Tutorial: From Basics to UI + API Integration with Real Framework Examples
- Anuradha Agarwal
- 12 minutes ago
- 17 min read
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:
Understand API testing fundamentals
Learn how Playwright supports API automation
Practice using freely available public APIs
Build complete API workflows
Integrate API testing into a real-world e-commerce framework
Use APIs for test data cleanup with global teardown
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 detailsThis 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 orderThis 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/20Status 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 errorRequest Headers
Headers send additional information with the request.
Content-Type: application/json
Authorization: Bearer token
x-api-key: API key valueRequest 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.

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 numericThis 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 dotenvMake 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_SECRETThen 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 dataThis 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=trueExample:
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.exeinstead of just:
curlIf 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
5644Step 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 ordersAPI 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_secret5. 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:
fetch existing orders
loop through orders
delete each order using API
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.tsSo:
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:
place an order through the UI
Capture the generated order ID from the confirmation page
Use the API to fetch that order
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:
Add product to cart (UI)
Validate cart (UI)
Place order (UI)
Verify order in "My Account" (UI)
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: 2Observation: 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