E-Commerce Testing Guide

How to Test WooCommerce Checkout with Playwright: Complete 2026 Guide

A scenario-by-scenario walkthrough of testing WooCommerce checkout with Playwright. Form reload cycles, payment gateway iframes, coupon application, shipping calculator quirks, WooCommerce Blocks vs classic checkout, and the pitfalls that break real e-commerce test suites.

6.6M+

WooCommerce powers over 6.6 million live stores worldwide, making it the most widely deployed e-commerce platform on the web according to BuiltWith usage data.

BuiltWith, 2025

0+AJAX reload cycles per order
0Checkout scenarios covered
0Checkout architectures tested
0%Fewer lines with Assrt

WooCommerce Checkout End-to-End Flow

BrowserWordPress/WooCommercePayment GatewayOrder APIAdd product to cartCart updated (AJAX)Navigate to /checkoutRender checkout formFill billing + shippingupdate_order_review (AJAX)Submit payment (iframe)Payment token / chargeCreate orderOrder #12345 confirmedRedirect to /order-received

1. Why Testing WooCommerce Checkout Is Harder Than It Looks

WooCommerce checkout looks like a simple form, but under the hood it is one of the most AJAX-heavy pages in the WordPress ecosystem. Every time a customer changes their billing country, updates a shipping field, or applies a coupon code, WooCommerce fires an AJAX request to the update_order_review endpoint. That request recalculates taxes, updates available shipping methods, and potentially swaps the entire payment method section of the form. The DOM gets partially replaced while the user is still filling in fields, which means any Playwright locators pointing to form elements may suddenly reference detached nodes.

The complexity multiplies when you factor in payment gateways. Stripe for WooCommerce renders its card fields inside a cross-origin iframe hosted on js.stripe.com. PayPal renders its own iframe and sometimes redirects to paypal.com entirely. Square, Authorize.net, and Braintree each have their own iframe strategies with different loading patterns and event models. Your test cannot simply fill a card number input; it must locate the gateway iframe, switch Playwright into that frame context, fill the fields there, then switch back to the parent page to click the Place Order button.

There are five structural reasons WooCommerce checkout is difficult to test reliably. First, the AJAX form reload cycle means the DOM is a moving target during form fill. Second, payment gateways use cross-origin iframes that require explicit frame handling. Third, WooCommerce ships two entirely different checkout architectures (classic shortcode and Gutenberg Blocks) with completely different DOM structures. Fourth, the shipping calculator triggers additional AJAX recalculations that can race with your form fill actions. Fifth, coupon application involves its own AJAX round-trip that replaces the order summary section, potentially invalidating any locators that referenced price elements.

WooCommerce Checkout AJAX Reload Cycle

🌐

User fills field

Billing country, state, postcode

⚙️

AJAX fires

update_order_review

⚙️

Server recalculates

Tax, shipping, totals

🌐

DOM replaced

Payment + shipping sections swap

User continues

Previous locators may be stale

Classic vs Blocks Checkout Architecture

🌐

Classic Shortcode

[woocommerce_checkout]

⚙️

jQuery + AJAX

checkout.js, update_order_review

↪️

Full DOM swap

Sections replaced on each change

🌐

Blocks Checkout

React-based components

⚙️

REST API

wc/store/v1/checkout

Partial re-render

Only changed components update

A good WooCommerce checkout test suite handles both checkout architectures, waits for AJAX cycles to complete before interacting with form elements, and manages gateway iframe contexts correctly. The sections below walk through each scenario you need, with runnable Playwright TypeScript you can copy directly into a real project.

2. Setting Up a Reliable Test Environment

Before you write any checkout scenarios, establish a stable test environment. WooCommerce test automation requires a running WordPress instance with WooCommerce active, at least one published product, a configured payment gateway in test mode, and a set of environment variables your Playwright tests can reference. Using WP-CLI and the WooCommerce REST API, you can script the entire setup so every CI run starts from a known state.

WooCommerce Test Environment Checklist

  • WordPress instance running with WooCommerce active (use wp-env or Docker)
  • At least one Simple product published with stock enabled
  • Stripe for WooCommerce plugin installed in test mode
  • WooCommerce Stripe test keys configured (pk_test_ / sk_test_)
  • Flat rate + free shipping zones configured for testing
  • At least one coupon code created (e.g., TESTCOUPON10 for 10% off)
  • Guest checkout enabled (WooCommerce > Settings > Accounts)
  • Classic checkout page AND Blocks checkout page both created for dual testing

Environment Variables

.env.test

Seeding Products and Coupons via WooCommerce REST API

Every test run should start from a known product catalog and coupon state. Use the WooCommerce REST API in your global setup to ensure the test product exists, has inventory, and the coupon is active with the correct discount amount.

test/helpers/woo-setup.ts

Playwright Configuration for WooCommerce

WooCommerce checkout is AJAX-heavy and payment gateways load cross-origin iframes. Configure Playwright with generous timeouts and ensure your global setup seeds the required test data before any scenario runs.

playwright.config.ts
WooCommerce Test Environment Setup

3. Scenario: Add to Cart and Reach Checkout

Before testing the checkout form itself, your test needs to get a product into the cart. WooCommerce supports AJAX add to cart on archive pages and a traditional form POST on single product pages. The AJAX path is more common in modern themes but introduces a timing challenge: the cart widget updates asynchronously after the button click, and your test must wait for the cart count to increment before navigating to checkout. Navigating too early results in an empty cart page and a confusing test failure.

1

Add to Cart and Navigate to Checkout

Straightforward

Goal

Starting from the shop page, add a product to the cart via AJAX, verify the cart contents, and navigate to the checkout page with the product present in the order summary.

Preconditions

  • WordPress running with WooCommerce active
  • At least one published Simple product
  • AJAX add to cart enabled (default for Simple products)

Playwright Implementation

add-to-cart.spec.ts

What to Assert Beyond the UI

  • The WooCommerce cart session cookie (wp_woocommerce_session_*) is set
  • The cart AJAX response returns fragments with the updated cart HTML
  • The checkout page order review table matches the expected product and price

4. Scenario: Classic Checkout with Stripe Gateway

The classic WooCommerce checkout uses the [woocommerce_checkout] shortcode. It renders a jQuery-driven form that fires AJAX requests on nearly every field change. The Stripe payment gateway renders card input fields inside a cross-origin iframe from js.stripe.com. This is the most common checkout configuration in the WooCommerce ecosystem, and it is also the trickiest to automate because you must handle both the AJAX reload cycles and the iframe context switching.

The critical sequencing issue is that WooCommerce firesupdate_order_review when billing fields change. If your test fills the billing country and immediately tries to interact with the Stripe iframe, the AJAX response may arrive mid-interaction and replace the payment section, destroying the iframe your test just located. The solution is to fill all billing fields first, wait for the final AJAX cycle to complete, and only then interact with the payment gateway iframe.

2

Classic Checkout with Stripe Payment

Complex

Goal

Complete a full checkout using the classic shortcode form with Stripe as the payment gateway. Fill billing details, enter a Stripe test card in the iframe, place the order, and verify the order confirmation page.

Preconditions

  • Product already in cart (from Scenario 1)
  • Stripe for WooCommerce plugin active in test mode
  • Classic checkout page using [woocommerce_checkout] shortcode

Playwright Implementation

classic-stripe-checkout.spec.ts

What to Assert Beyond the UI

  • The order status in WooCommerce is “Processing” (verify via REST API)
  • The Stripe charge was created in test mode (check Stripe Dashboard or API)
  • The order total matches the expected product price plus tax
  • The order confirmation email was triggered (check WooCommerce email logs)

Classic Stripe Checkout: Playwright vs Assrt

import { test, expect } from '@playwright/test';

test('classic checkout with Stripe', async ({ page }) => {
  await page.goto('/?add-to-cart=15');
  await page.goto('/checkout/');

  await page.selectOption('#billing_country', 'US');
  await page.selectOption('#billing_state', 'CA');
  await page.waitForResponse(
    (res) => res.url().includes('wc-ajax=update_order_review')
  );

  await page.fill('#billing_first_name', 'Test');
  await page.fill('#billing_last_name', 'Buyer');
  await page.fill('#billing_address_1', '123 Test Street');
  await page.fill('#billing_city', 'San Francisco');
  await page.fill('#billing_postcode', '94102');
  await page.fill('#billing_phone', '5551234567');
  await page.fill('#billing_email', 'test@example.com');
  await page.waitForLoadState('networkidle');

  const stripeFrame = page
    .frameLocator('iframe[name^="__privateStripeFrame"]')
    .first();
  await stripeFrame.getByPlaceholder('Card number')
    .fill('4242424242424242');
  await stripeFrame.getByPlaceholder('MM / YY').fill('12/30');
  await stripeFrame.getByPlaceholder('CVC').fill('123');

  await page.getByRole('button', { name: /place order/i }).click();
  await page.waitForURL(/order-received/, { timeout: 30_000 });
  await expect(page.locator('.woocommerce-thankyou-order-received'))
    .toContainText('Thank you');
});
42% fewer lines

Try Assrt for free

Open-source AI testing framework. No signup required.

Get Started

5. Scenario: WooCommerce Blocks Checkout

WooCommerce Blocks checkout is the React-based replacement for the classic shortcode checkout. Since WooCommerce 8.3, new installations default to the Blocks checkout. It uses the WooCommerce Store API (wc/store/v1) instead of the legacy AJAX endpoints, renders with React components instead of jQuery, and updates the DOM through React state management instead of replacing HTML fragments wholesale.

For test automation, Blocks checkout is both easier and harder than classic. Easier because the React-driven updates are more predictable (no full DOM swap mid-fill). Harder because the input selectors are completely different, the Stripe integration uses the Payment Element (not the legacy Card Element), and the form validation is inline rather than server-side. A test suite that works perfectly on classic checkout will fail completely on Blocks checkout without significant selector changes.

3

WooCommerce Blocks Checkout

Complex

Goal

Complete a full order using the WooCommerce Blocks checkout page with Stripe Payment Element. Fill billing details using the Blocks form inputs, complete payment in the Stripe iframe, and verify the order confirmation.

Playwright Implementation

blocks-checkout.spec.ts

Key Differences from Classic Checkout

  • Input IDs use billing-first_name instead of billing_first_name (note the hyphen vs underscore prefix)
  • Country and state fields are combobox elements, not standard select dropdowns
  • The email field is at the top of the form, not inside the billing section
  • Stripe uses the Payment Element (unified card form) instead of separate Card Element
  • Store API calls replace the legacy wc-ajax endpoints

6. Scenario: Coupon Application and Price Recalculation

Coupon application is one of the most AJAX-intensive operations in WooCommerce checkout. When a customer enters a coupon code and clicks “Apply coupon,” WooCommerce sends an AJAX request to ?wc-ajax=apply_coupon, validates the code server-side, recalculates all totals, and replaces the entire order review section with updated HTML. In classic checkout, this DOM replacement can invalidate any locators your test holds that reference price elements. In Blocks checkout, the React state update is smoother but the Store API call still needs to complete before assertions are valid.

4

Coupon Application and Price Verification

Moderate

Playwright Implementation

coupon-application.spec.ts

Coupon Application: Playwright vs Assrt

test('apply coupon and verify discount', async ({ page }) => {
  await page.goto('/?add-to-cart=15');
  await page.goto('/checkout/');

  await expect(page.locator('.order-total .woocommerce-Price-amount'))
    .toContainText('45.00');

  await page.getByText(/have a coupon/i).click();
  await page.fill('#coupon_code', 'TESTCOUPON10');

  const couponRes = page.waitForResponse(
    (res) => res.url().includes('wc-ajax=apply_coupon')
  );
  await page.getByRole('button', { name: /apply coupon/i }).click();
  await couponRes;
  await page.waitForResponse(
    (res) => res.url().includes('wc-ajax=update_order_review')
  );

  await expect(page.locator('.cart-discount'))
    .toContainText('4.50');
  await expect(page.locator('.order-total .woocommerce-Price-amount'))
    .toContainText('40.50');
});
50% fewer lines

7. Scenario: Shipping Calculator and Method Selection

WooCommerce shipping calculation is tightly coupled to the billing and shipping address fields. Every time the customer changes their country, state, or postcode, the checkout fires update_order_review to fetch available shipping methods for that address. The available methods can change completely between addresses (for example, free shipping may only be available domestically, while international orders show only flat rate). The shipping method selection itself triggers another AJAX round-trip that recalculates the order total.

The testing challenge is sequencing. Your test must fill the address fields that determine shipping availability, wait for the AJAX cycle that returns the shipping options, select a shipping method, wait for the second AJAX cycle that recalculates the total, and only then assert the final price. Filling the payment fields before shipping has settled can cause the Stripe iframe to be destroyed and re-created by the AJAX response.

5

Shipping Method Selection and Total Update

Moderate

Playwright Implementation

shipping-calculator.spec.ts

What to Assert Beyond the UI

  • The AJAX response payload includes the correct shipping method IDs and costs
  • Tax recalculation reflects the correct rate for the selected shipping destination
  • Changing the country to a zone with no shipping methods shows a “no shipping available” notice

8. Scenario: Payment Decline and Error Recovery

Testing the happy path is necessary but not sufficient. Real checkout flows must handle payment declines gracefully: the customer sees a clear error message, the form remains filled with their billing details, and they can correct their payment method and try again without re-entering everything. Stripe provides specific test card numbers that trigger different decline reasons, letting you test each error scenario deterministically.

6

Payment Decline and Retry

Moderate

Playwright Implementation

payment-decline.spec.ts

Stripe Test Cards for Decline Scenarios

Card NumberDecline Reason
4000000000000002Generic decline
4000000000009995Insufficient funds
4000000000009987Lost card
4000000000000069Expired card

Payment Decline Recovery: Playwright vs Assrt

test('declined card shows error and allows retry', async ({ page }) => {
  await page.goto('/?add-to-cart=15');
  await page.goto('/checkout/');

  // ... fill billing details (12 lines) ...

  const stripeFrame = page.frameLocator(
    'iframe[name^="__privateStripeFrame"]'
  ).first();
  await stripeFrame.getByPlaceholder('Card number')
    .fill('4000000000000002');
  await stripeFrame.getByPlaceholder('MM / YY').fill('12/30');
  await stripeFrame.getByPlaceholder('CVC').fill('123');

  await page.getByRole('button', { name: /place order/i }).click();

  await expect(page.locator('.woocommerce-error'))
    .toContainText(/declined/i);

  // Verify form data preserved
  await expect(page.locator('#billing_first_name'))
    .toHaveValue('Test');

  // Retry with valid card
  const retryFrame = page.frameLocator(
    'iframe[name^="__privateStripeFrame"]'
  ).first();
  await retryFrame.getByPlaceholder('Card number')
    .fill('4242424242424242');
  // ... fill remaining fields ...

  await page.getByRole('button', { name: /place order/i }).click();
  await page.waitForURL(/order-received/);
});
56% fewer lines

9. Common Pitfalls That Break WooCommerce Test Suites

These are real failure modes pulled from WooCommerce plugin issue trackers, Playwright GitHub discussions, and Stack Overflow threads about WooCommerce test automation. Each one has caused flaky or broken test suites in production e-commerce projects.

Interacting During AJAX Reload Cycles

The single most common WooCommerce test failure: your test fills a form field, which triggers update_order_review, and before the AJAX response arrives, the test interacts with another element. The AJAX response replaces the DOM section, and the element your test just clicked no longer exists. The fix is to always waitForResponse after any field change that triggers AJAX (country, state, postcode, shipping method, coupon) before interacting with elements in the payment or order review sections.

Stale Stripe Iframe References

When an AJAX reload replaces the payment method section, the Stripe iframe is destroyed and a new one is created. If your test stored a reference to the old iframe viaframeLocator, that reference now points to a detached frame. Always re-query the Stripe iframe after any AJAX cycle that could affect the payment section. GitHub issue woocommerce/woocommerce#38245 documents this exact race condition.

Classic vs Blocks Selector Mismatch

A store that upgrades from classic to Blocks checkout (or vice versa) will break every selector in the test suite. Classic uses #billing_first_name while Blocks uses #billing-first_name. Classic wraps the form in .woocommerce-checkout while Blocks uses.wc-block-checkout. Maintain separate test files or use a detection helper that checks which architecture is active before choosing selectors.

Cart Session Expiration

WooCommerce cart sessions expire after 48 hours by default, but in high-traffic test environments with aggressive cleanup cron jobs, sessions may expire faster. If your test adds a product to the cart in one step and navigates to checkout in a later step with a significant delay between them, the cart may be empty. Always add to cart and navigate to checkout within the same test or use a fresh browser context.

WooCommerce Fragment Caching

Some hosting providers and caching plugins aggressively cache WooCommerce AJAX fragments, which means your test may see stale cart data or outdated order totals. In your test environment, disable all page caching and object caching plugins, and ensure the WC_CACHE_SKIP constant is not interfering with test data freshness.

WooCommerce Checkout Test Anti-Patterns

  • Filling payment fields before AJAX reload cycle completes
  • Storing Stripe iframe references across AJAX cycles
  • Using classic checkout selectors on a Blocks checkout page
  • Hardcoding product IDs instead of using slugs or REST API lookups
  • Not waiting for update_order_review after country/state/postcode changes
  • Assuming coupon application is synchronous
  • Running tests against a cached checkout page
  • Not cleaning up test orders between runs (order number conflicts)
WooCommerce Checkout Test Suite Run

10. Writing These Scenarios in Plain English with Assrt

Every scenario above requires careful handling of AJAX wait cycles, iframe context switching, and architecture-specific selectors. The classic checkout Stripe scenario alone is over 40 lines of Playwright TypeScript, and most of those lines are implementation details (iframe names, CSS selectors, AJAX endpoint patterns) rather than test intent. When WooCommerce updates its checkout markup, renames a CSS class, or the Stripe plugin changes its iframe naming convention, those 40 lines break silently.

Assrt lets you describe the checkout scenario in plain English and handles the selector resolution, AJAX synchronization, and iframe management at runtime. When WooCommerce ships a new version that changes the checkout DOM structure, Assrt detects the selector failures, analyzes the new DOM, and opens a pull request with updated locators. Your scenario files remain unchanged.

scenarios/woo-checkout-full-suite.assrt

Assrt compiles each scenario block into the same Playwright TypeScript you saw in the preceding sections, committed to your repo as real test files you can read, run, and debug. The AJAX wait logic, iframe frame-switching, and architecture detection (classic vs Blocks) are handled by the Assrt runtime, not hardcoded into your scenario files.

Start with the add-to-cart and classic checkout happy path. Once those are green in CI, add coupon application, shipping calculation, and payment decline scenarios. Then duplicate the suite for Blocks checkout. In a single afternoon you can achieve comprehensive WooCommerce checkout coverage that most e-commerce teams never manage to build by hand, and it stays working across WooCommerce version upgrades.

Related Guides

Ready to automate your testing?

Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.

$npm install @assrt/sdk