This recipe is the smallest end-to-end Playwright test that is recognizably an e-commerce checkout test.
It walks the QA Shop guest cart flow from a product detail page through the cart and into the checkout auth gate. Run it on your laptop in about five seconds.
It is the canonical first test to write against any e-commerce site, and on QA Shop it is a copy-paste away thanks to the testid contract on every interactive element.
§ 01 · PREREQUISITES
Prerequisites
You need a project with @playwright/test installed and a Playwright config that points at QA Shop. If you have not done either, run npm init playwright@latest in a fresh directory and accept the defaults.
Bootstrap with npm init playwright@latest if you don't already have a project — it scaffolds the runner, the config, and an example spec.
Set to http://localhost:3002 for a local checkout, or https://automationtestingplatform.com for the hosted demo.
The recipe assumes the server is already up. The test does not start it for you.
§ 02 · THE SPEC
The full spec
Save this file as tests/recipes/playwright/add-to-cart-checkout.spec.ts and run it with npx playwright test --grep add-to-cart-checkout.
import { test, expect } from "@playwright/test";
const PRODUCT_SLUG = "wooden-train-set-100-piece";
test("guest can add a product to the cart and reach the checkout gate", async ({
page,
}) => {
// 1. Land on the product detail page.
await page.goto(`/products/${PRODUCT_SLUG}`);
await expect(page.getByTestId("page-pdp")).toBeVisible();
// 2. Add to cart. The buy box optimistically updates the header badge.
await page.getByTestId("pdp-add-to-cart").click();
await expect(page.getByTestId("cart-count")).toBeVisible();
await expect(page.getByTestId("cart-count")).toHaveText("1");
// 3. Open the cart and confirm the line item is present.
await page.goto("/cart");
await expect(page.getByTestId("page-cart")).toBeVisible();
await expect(
page.locator("[data-testid^='cart-item-qty-value-']").first()
).toHaveText("1");
// 4. Confirm the editorial cart summary shows the totals + checkout CTA.
await expect(page.getByTestId("cart-summary-subtotal")).toBeVisible();
await expect(page.getByTestId("cart-summary-total")).toBeVisible();
const checkoutBtn = page.getByTestId("cart-summary-checkout");
await expect(checkoutBtn).toBeVisible();
// 5. Click checkout. QA Shop guards /checkout behind auth — guests are
// redirected to /auth/login with a redirect param. This is the realistic
// stopping point for a guest E2E run; auth fixtures unlock the next leg.
await checkoutBtn.click();
await page.waitForURL(/\/auth\/login\?redirect=%2Fcheckout/);
await expect(page).toHaveURL(/\/auth\/login\?redirect=%2Fcheckout/);
});§ 03 · WALKTHROUGH
Walkthrough
The first navigation to /products/wooden-train-set-100-piece is plain page.goto — no readiness flag yet.
The first assertion is on data-testid="page-pdp", the wrapper at the top of the product detail page. Asserting on the wrapper rather than the title or price is deliberate: when the wrapper is visible, the buy box has hydrated and the add-to-cart button is wired.
The add-to-cart click is followed by two assertions on data-testid="cart-count". The first is toBeVisible — the badge only renders when the count is greater than zero, so visibility doubles as a count-greater-than-zero assertion. The second is toHaveText("1") — exactly one item.
Splitting the assertions is intentional. Playwright's locator auto-waits past actionability, so toHaveText will retry until the badge text matches1; if the optimistic update lags, the retry loop absorbs the latency without a manual sleep.
The navigation to /cart is direct rather than via the header cart icon. A direct page.goto skips a header-mount race and is the right call for a recipe whose point is the cart-and-checkout boundary.
The line item assertion uses a [data-testid^='cart-item-qty-value-'] prefix selector because each cart line carries an order-stable suffix; the prefix is the contract, the suffix is implementation detail.
The summary assertions are the contract every checkout test depends on. cart-summary-subtotal, cart-summary-total, and cart-summary-checkout are the three testids worth checking at this checkpoint.
The checkout button click triggers the protected-route redirect; page.waitForURL with a regex pattern is the idiomatic way to wait for an SPA navigation, and the trailing expect(page).toHaveURL makes the intent of the wait explicit at the assertion layer2.
§ 04 · EXTEND IT
Things to extend
Three obvious variants build on this skeleton.
Swap PRODUCT_SLUG for any other catalog slug — the rest of the spec is product-agnostic.
Click add-to-cart twice (or click data-testid="pdp-qty-inc" first to bump the quantity), then assert cart-count reads the expected total.
With a logged-in fixture, the checkout click lands on /checkout instead of /auth/login — unlocking the shipping form, the test card panel, and the order confirmation page.
A fourth variant worth scripting eventually is the declined card path — the test card 4000 0000 0000 0002 produces a checkout error at data-testid="checkout-error" instead of an order. That spec doubles as a regression net for the error-handling layer and is small enough to write once the auth fixture exists.
Related
For the full testid catalog and the checkpoint-by-checkpoint tour of the e-commerce flow, see the Walking the e-commerce flow guide. For the broader testing philosophy and the framework-by-framework comparison, see the QA Shop testing guide.
The Selenium, Cypress, and WebdriverIO add-to-cart counterparts are coming soon — placeholder slugs are /recipes/selenium/add-to-cart-checkout, /recipes/cypress/add-to-cart-checkout, and /recipes/webdriverio/add-to-cart-checkout. The /recipes index lists them as they ship.
Footnotes
-
Playwright Auto-waiting and actionability — https://playwright.dev/docs/actionability (verified 2026-04-28, Playwright 1.58) ↩
-
Playwright page.waitForURL — https://playwright.dev/docs/api/class-page#page-wait-for-url (verified 2026-04-28, Playwright 1.58) ↩
Frequently asked questions
Last verified: