E-Commerce Testing Guide

How to Test Shopify Checkout End to End: Complete 2026 Guide

A scenario-by-scenario walkthrough of testing Shopify Checkout with Playwright. Guest checkout, discount codes, account checkout, Shop Pay, international taxes and duties, cart abandonment, and the hosted checkout pitfalls that break real test suites.

Millions

Shopify powers millions of merchants worldwide, and its hosted checkout handles the multi-step flow of information collection, shipping rate calculation, payment processing, and order confirmation for every storefront.

0 stepsCheckout flow depth
0Test scenarios covered
0%Fewer lines with Assrt
< 0sPer test runtime

1. Why Testing Shopify Checkout Is Complex

Shopify Checkout is a hosted, multi-step flow. When a customer clicks your storefront's checkout button, the browser navigates to checkout.shopify.com (or your custom domain's /checkouts/ path) and walks through a sequence of pages: information, shipping, payment, and confirmation. Each step is rendered server-side by Shopify, not by your theme code. That means your Playwright tests are driving a page you do not control and cannot modify at the DOM level.

There are five structural reasons this flow is harder to test than a typical form. First, the checkout is hosted on a separate domain, so your test crosses an origin boundary on every run. Second, the multi-step flow means each assertion depends on the previous step completing and the next page loading, which introduces timing variability that fixed waits cannot handle. Third, the payment step renders card inputs inside iframes for PCI compliance, just like Stripe. Fourth, Shopify's checkout UI varies by market: a Canadian customer sees tax included, a US customer sees tax calculated at the shipping step, and an EU customer may see duties and import fees. Fifth, Shopify frequently updates the checkout experience through its Checkout Extensibility platform, which means selectors that worked last quarter may silently break.

Shopify Checkout Flow

🌐

Storefront

Product + Cart

📧

Information

Email + Address

⚙️

Shipping

Rate Selection

💳

Payment

Card / Wallet

Confirmation

Order Complete

Checkout Interaction Sequence

BrowserStoreCartCheckout InfoShippingPaymentConfirmationVisit product pageAdd to cartProceed to checkoutRender email + address formSubmit info, continueShow shipping ratesSelect rate, continueRender payment iframeSubmit card detailsProcess paymentOrder confirmed

A good Shopify Checkout test suite covers the full multi-step flow, validates price calculations at every step, and handles the market variations your store actually serves. The sections below walk through each scenario you need, with runnable Playwright TypeScript code you can copy directly.

2. Setting Up Your Test Environment

Before you write a single scenario, configure a proper test environment. Shopify provides development stores through the Partner Dashboard that are free, fully functional, and designed for exactly this purpose. Never run end-to-end checkout tests against a live production store.

Development Store Setup

Create a development store from your Shopify Partner account. Enable the bogus gateway under Settings, then Payments, then choose the Bogus Gateway option. This gateway accepts specific test values without contacting any real payment processor. You can also enable Shopify Payments in test mode if your store is in a supported region, which lets you use standard test card numbers.

Dev Store Setup Checklist

  • Create a development store from the Shopify Partner Dashboard
  • Enable the Bogus Gateway under Settings > Payments
  • Or enable Shopify Payments in test mode (supported regions)
  • Publish at least one product with available inventory
  • Configure shipping rates for your test address regions
  • Set a storefront password and store it in .env.test
  • Create an Admin API access token (shpat_...) for API verification
  • Install Playwright and configure the global setup fixture

Environment Variables

.env.test

The Bogus Gateway vs Shopify Payments Test Mode

Shopify offers two test payment approaches. The bogus gateway accepts three magic values: enter "Bogus Gateway" as the card name, "1" as the card number for success, "2" for failure, and "3" for an exception. The expiry and CVV can be anything. Shopify Payments test mode, on the other hand, accepts standard test cards like 4242 4242 4242 4242 for success and 4000 0000 0000 0002 for decline.

GatewaySuccess CardDecline Card
Bogus Gateway12
Shopify Payments (test)4242 4242 4242 42424000 0000 0000 0002
Shopify Payments (test)5555 5555 5555 44444000 0000 0000 9995

Storefront Password Handling

Development stores are password-protected by default. Your Playwright tests need to bypass this gate before they can reach any product page. The simplest approach is to handle the password page at the start of each test or in a global setup fixture that stores the authenticated session.

global-setup.ts

3. Scenario: Guest Checkout Happy Path

The first scenario every Shopify store needs is the guest checkout that walks through every step and reaches the order confirmation page. This is your smoke test. If this breaks, your store is broken and you want to know immediately.

1

Guest Checkout: Product to Confirmation

Moderate

Starting from a product page, add the item to cart, proceed through information, shipping, and payment steps, and land on the order confirmation page. This exercises the full multi-step flow that real customers experience.

Guest Checkout Happy Path

🌐

Product Page

Add to cart

↪️

Cart

Proceed to checkout

📧

Information

Email + address

⚙️

Shipping

Select rate

💳

Payment

Enter card

Confirmation

Order placed

Preconditions

  • Development store running with bogus gateway or Shopify Payments test mode enabled
  • At least one published product with inventory available
  • At least one shipping rate configured for the test address region
  • Storefront password bypassed via global setup

Playwright Implementation

shopify-checkout.spec.ts

Shopify Payments Test Mode Variant

If your development store uses Shopify Payments in test mode instead of the bogus gateway, the payment fields are rendered inside iframes similar to Stripe. You need to use frameLocator to reach the card inputs.

// Shopify Payments test mode: card fields are in iframes
const cardFrame = page.frameLocator(
  'iframe[id*="card-fields-number"]'
);
await cardFrame.getByPlaceholder(/card number/i)
  .fill('4242 4242 4242 4242');

const expiryFrame = page.frameLocator(
  'iframe[id*="card-fields-expiry"]'
);
await expiryFrame.getByPlaceholder(/expiration/i).fill('12/34');

const cvvFrame = page.frameLocator(
  'iframe[id*="card-fields-verification"]'
);
await cvvFrame.getByPlaceholder(/security code/i).fill('123');

const nameField = page.getByLabel(/name on card/i);
if (await nameField.isVisible().catch(() => false)) {
  await nameField.fill('Jane Tester');
}

4. Scenario: Discount Code with Price Assertion

Discount codes are one of the most error-prone areas of e-commerce checkout. The code must be applied, the discount must appear as a line item, the total must recalculate correctly, and the discount must survive the transition between checkout steps. Testing this end to end catches the silent regression where a discount code "applies" visually but the actual charge ignores it.

2

Discount Code Application with Live Price Recalculation

Moderate
test('discount code applies and reduces total correctly', async ({ page }) => {
  const storeUrl = process.env.SHOPIFY_STORE_URL!;

  // Navigate to product and add to cart
  await page.goto(`${storeUrl}/products/test-product`);
  await page.getByRole('button', { name: /add to cart/i }).click();
  await page.goto(`${storeUrl}/cart`);
  await page.getByRole('button', { name: /check out/i }).click();
  await page.waitForURL(/\/checkouts\//);

  // Capture the original subtotal before applying the discount
  const subtotalBefore = await page
    .locator('[data-checkout-subtotal-price-target]')
    .textContent()
    .catch(() => null);

  // Apply discount code
  await page.getByLabel(/discount/i).fill('SAVE20');
  await page.getByRole('button', { name: /apply/i }).click();

  // Wait for discount to appear in the order summary
  await expect(page.getByText(/SAVE20/i)).toBeVisible({ timeout: 10_000 });
  await expect(page.getByText(/-\$/)).toBeVisible();

  // Verify the total has decreased
  const totalAfterDiscount = await page
    .locator('[data-checkout-total-price-target]')
    .textContent();
  expect(totalAfterDiscount).toBeTruthy();

  // Fill information and proceed through remaining steps
  await page.getByLabel(/email/i).fill(`discount+${Date.now()}@assrt.ai`);
  await page.getByLabel(/first name/i).fill('Discount');
  await page.getByLabel(/last name/i).fill('Tester');
  await page.getByLabel(/address/i).first().fill('456 Coupon Lane');
  await page.getByLabel(/city/i).fill('San Francisco');
  await page.getByLabel(/state/i).selectOption('California');
  await page.getByLabel(/zip/i).fill('94102');

  await page.getByRole('button', { name: /continue to shipping/i }).click();
  await expect(page.getByText(/shipping method/i)).toBeVisible({
    timeout: 15_000,
  });

  // Verify discount is still visible after step transition
  await expect(page.getByText(/SAVE20/i)).toBeVisible();

  await page.getByRole('button', { name: /continue to payment/i }).click();

  // Verify discount persists on the payment step
  await expect(page.getByText(/SAVE20/i)).toBeVisible();

  // Complete payment and verify order
  await page.getByLabel(/card number/i).fill('1');
  await page.getByLabel(/name on card/i).fill('Bogus Gateway');
  await page.getByLabel(/expiration/i).fill('12/34');
  await page.getByLabel(/security code/i).fill('123');

  await page.getByRole('button', { name: /pay now|complete order/i }).click();
  await expect(page.getByText(/order confirmed|thank you/i)).toBeVisible({
    timeout: 30_000,
  });

  // Verify the discount appears on the confirmation page
  await expect(page.getByText(/SAVE20/i)).toBeVisible();
});

Also Test Invalid and Expired Codes

Create tests for discount codes that have expired, codes that have hit their usage limit, and codes that do not apply to the products in the cart. Shopify renders inline error messages for each case, and your test should assert the specific error text and confirm that the total remains unchanged.

Try Assrt for free

Open-source AI testing framework. No signup required.

Get Started

5. Scenario: Account Checkout with Saved Addresses

Returning customers who are logged into their Shopify account can select a saved shipping address, which skips the manual address entry. This is a different code path from guest checkout, and it introduces the risk that saved address data displays correctly but ships to the wrong location if the address ID mapping breaks.

3

Logged-In Customer with Saved Address

Moderate
test('account checkout with saved address', async ({ page }) => {
  const storeUrl = process.env.SHOPIFY_STORE_URL!;

  // Log in as the test customer
  await page.goto(`${storeUrl}/account/login`);
  await page.getByLabel(/email/i).fill('returning-customer@assrt.ai');
  await page.getByLabel(/password/i).fill(process.env.TEST_CUSTOMER_PASSWORD!);
  await page.getByRole('button', { name: /sign in|log in/i }).click();
  await page.waitForURL(/\/account/);

  // Add product and proceed to checkout
  await page.goto(`${storeUrl}/products/test-product`);
  await page.getByRole('button', { name: /add to cart/i }).click();
  await page.goto(`${storeUrl}/cart`);
  await page.getByRole('button', { name: /check out/i }).click();
  await page.waitForURL(/\/checkouts\//);

  // Verify saved address is pre-populated
  await expect(page.getByText(/123 Test Street/i)).toBeVisible({
    timeout: 10_000,
  });

  // If multiple addresses exist, select the primary one
  const addressSelector = page.getByLabel(/saved addresses/i);
  if (await addressSelector.isVisible().catch(() => false)) {
    await addressSelector.selectOption({ index: 0 });
  }

  // Continue through shipping and payment
  await page.getByRole('button', { name: /continue to shipping/i }).click();
  await expect(page.getByText(/shipping method/i)).toBeVisible({
    timeout: 15_000,
  });

  await page.getByRole('button', { name: /continue to payment/i }).click();

  // Use bogus gateway to pay
  await page.getByLabel(/card number/i).fill('1');
  await page.getByLabel(/name on card/i).fill('Bogus Gateway');
  await page.getByLabel(/expiration/i).fill('12/34');
  await page.getByLabel(/security code/i).fill('123');

  await page.getByRole('button', { name: /pay now|complete order/i }).click();
  await expect(page.getByText(/order confirmed|thank you/i)).toBeVisible({
    timeout: 30_000,
  });

  // Verify the shipping address on the confirmation matches the saved address
  await expect(page.getByText(/123 Test Street/i)).toBeVisible();
});

The key assertion here is that the saved address appears on the confirmation page, not just on the information step. This catches the bug where the address selector displays one address but submits a different address ID to the checkout API.

6. Scenario: Express Checkout and Shop Pay

Shopify surfaces express checkout buttons (Shop Pay, Apple Pay, Google Pay) at the top of the checkout page and on the cart page. These buttons bypass the information and shipping steps entirely, pulling address and payment data from the wallet. Testing the full wallet flow end to end is not possible in Playwright because the wallet sheets are rendered by the operating system or a third-party app. However, you can and should test three things: that the buttons render, that the fallback card form remains functional, and that Shop Pay redirects to the correct domain.

4

Express Checkout Button Visibility and Fallback

Complex
test('express checkout buttons render and card fallback works', async ({ page }) => {
  const storeUrl = process.env.SHOPIFY_STORE_URL!;

  await page.goto(`${storeUrl}/products/test-product`);
  await page.getByRole('button', { name: /add to cart/i }).click();
  await page.goto(`${storeUrl}/cart`);
  await page.getByRole('button', { name: /check out/i }).click();
  await page.waitForURL(/\/checkouts\//);

  // Assert that express checkout section exists
  const expressSection = page.locator(
    '[data-shopify-buy-now], [data-express-checkout]'
  );

  // Shop Pay button should be visible when enabled
  const shopPayButton = page.locator(
    'iframe[src*="shop-pay"], [data-testid="ShopPay"]'
  );
  const shopPayVisible = await shopPayButton
    .first()
    .isVisible({ timeout: 5_000 })
    .catch(() => false);

  if (shopPayVisible) {
    // Verify Shop Pay iframe loads without errors
    await expect(shopPayButton.first()).toBeAttached();
  }

  // Regardless of express buttons, the standard form must work
  await page.getByLabel(/email/i).fill(`express+${Date.now()}@assrt.ai`);
  await page.getByLabel(/first name/i).fill('Express');
  await page.getByLabel(/last name/i).fill('Tester');
  await page.getByLabel(/address/i).first().fill('789 Wallet Ave');
  await page.getByLabel(/city/i).fill('San Francisco');
  await page.getByLabel(/state/i).selectOption('California');
  await page.getByLabel(/zip/i).fill('94102');

  await page.getByRole('button', { name: /continue to shipping/i }).click();
  await expect(page.getByText(/shipping method/i)).toBeVisible({
    timeout: 15_000,
  });

  await page.getByRole('button', { name: /continue to payment/i }).click();

  await page.getByLabel(/card number/i).fill('1');
  await page.getByLabel(/name on card/i).fill('Bogus Gateway');
  await page.getByLabel(/expiration/i).fill('12/34');
  await page.getByLabel(/security code/i).fill('123');

  await page.getByRole('button', { name: /pay now|complete order/i }).click();
  await expect(page.getByText(/order confirmed|thank you/i)).toBeVisible({
    timeout: 30_000,
  });
});

Shop Pay Redirect Validation

If you need to validate that clicking Shop Pay initiates the correct redirect, you can intercept the navigation and assert the URL without completing the wallet flow. Use page.waitForURL(/shop\\.app/) or check the request target with page.on('request') to verify the redirect points to the correct Shop Pay domain. This confirms your integration configuration is correct even though Playwright cannot complete the wallet authentication.

7. Scenario: International Checkout with Tax and Duty

International orders are where Shopify Checkout becomes significantly more complex. Depending on your Shopify Markets configuration, the checkout may display prices in the local currency, calculate import duties and taxes, show a different set of shipping options, and require additional fields like a phone number or tax ID. Testing this scenario catches the silent bug where duties are calculated but not displayed, or where the currency conversion is applied to the subtotal but not to the shipping cost.

5

International Order with Tax, Duty, and Currency

Complex
test('international checkout displays duties and correct currency', async ({ page }) => {
  const storeUrl = process.env.SHOPIFY_STORE_URL!;

  await page.goto(`${storeUrl}/products/test-product`);
  await page.getByRole('button', { name: /add to cart/i }).click();
  await page.goto(`${storeUrl}/cart`);
  await page.getByRole('button', { name: /check out/i }).click();
  await page.waitForURL(/\/checkouts\//);

  // Fill information with a UK address
  await page.getByLabel(/email/i).fill(`intl+${Date.now()}@assrt.ai`);
  await page.getByLabel(/country/i).selectOption('United Kingdom');
  await page.getByLabel(/first name/i).fill('International');
  await page.getByLabel(/last name/i).fill('Buyer');
  await page.getByLabel(/address/i).first().fill('10 Downing Street');
  await page.getByLabel(/city/i).fill('London');
  await page.getByLabel(/postal code|zip/i).fill('SW1A 2AA');

  // Phone field is often required for international orders
  const phoneField = page.getByLabel(/phone/i);
  if (await phoneField.isVisible().catch(() => false)) {
    await phoneField.fill('+44 20 7946 0958');
  }

  await page.getByRole('button', { name: /continue to shipping/i }).click();
  await expect(page.getByText(/shipping method/i)).toBeVisible({
    timeout: 15_000,
  });

  // Assert that international shipping options are shown
  await expect(
    page.getByText(/international|worldwide|standard shipping/i)
  ).toBeVisible();

  await page.getByRole('button', { name: /continue to payment/i }).click();

  // If Shopify Markets is configured, duties should appear
  const dutiesLine = page.getByText(/duties|import tax/i);
  const dutiesVisible = await dutiesLine
    .isVisible({ timeout: 5_000 })
    .catch(() => false);

  if (dutiesVisible) {
    // Assert duties are a positive value, not zero
    const dutiesText = await dutiesLine.textContent();
    expect(dutiesText).toMatch(/[£€]\d+\.\d{2}/);
  }

  // Assert currency matches the market (GBP for UK)
  const totalText = await page
    .locator('[data-checkout-total-price-target]')
    .textContent()
    .catch(() => '');
  // Total should show GBP or the store's configured currency
  expect(totalText).toBeTruthy();

  // Complete payment
  await page.getByLabel(/card number/i).fill('1');
  await page.getByLabel(/name on card/i).fill('Bogus Gateway');
  await page.getByLabel(/expiration/i).fill('12/34');
  await page.getByLabel(/security code/i).fill('123');

  await page.getByRole('button', { name: /pay now|complete order/i }).click();
  await expect(page.getByText(/order confirmed|thank you/i)).toBeVisible({
    timeout: 30_000,
  });
});

Run this test with multiple country addresses to cover your primary markets. Each market may have different tax rules, duty calculation providers, and currency rounding behavior. Parameterize the test with an array of address fixtures so you can add new markets without duplicating the test body.

8. Scenario: Cart Abandonment and Recovery

Shopify automatically tracks abandoned checkouts and can send recovery emails. Testing this flow end to end verifies that an abandoned checkout is recorded correctly and that the recovery URL works. The key insight is that an abandoned checkout is simply a checkout session where the customer entered their email at the information step but never completed the order. Shopify marks these as abandoned after a configurable timeout (default: 10 hours in production, but you can verify the record via the Admin API immediately).

6

Cart Abandonment Creates Recoverable Checkout

Straightforward
test('abandoned checkout is recorded and recovery URL works', async ({
  page,
  request,
}) => {
  const storeUrl = process.env.SHOPIFY_STORE_URL!;
  const email = `abandon+${Date.now()}@assrt.ai`;

  // Start checkout but do not complete it
  await page.goto(`${storeUrl}/products/test-product`);
  await page.getByRole('button', { name: /add to cart/i }).click();
  await page.goto(`${storeUrl}/cart`);
  await page.getByRole('button', { name: /check out/i }).click();
  await page.waitForURL(/\/checkouts\//);

  // Capture the checkout URL for later
  const checkoutUrl = page.url();

  // Enter email (this is what triggers abandoned checkout tracking)
  await page.getByLabel(/email/i).fill(email);
  await page.getByLabel(/first name/i).fill('Abandoned');
  await page.getByLabel(/last name/i).fill('Cart');
  await page.getByLabel(/address/i).first().fill('999 Forgot Lane');
  await page.getByLabel(/city/i).fill('San Francisco');
  await page.getByLabel(/state/i).selectOption('California');
  await page.getByLabel(/zip/i).fill('94102');

  // Navigate away without completing the order
  await page.goto(`${storeUrl}/`);

  // Verify abandoned checkout via Shopify Admin API
  const adminUrl = `${storeUrl}/admin/api/2024-01/checkouts.json?status=open`;
  const res = await request.get(adminUrl, {
    headers: {
      'X-Shopify-Access-Token': process.env.SHOPIFY_ADMIN_API_TOKEN!,
    },
  });
  const body = await res.json();
  const abandoned = body.checkouts?.find(
    (c: any) => c.email === email
  );
  expect(abandoned).toBeTruthy();
  expect(abandoned.abandoned_checkout_url).toBeTruthy();

  // Verify the recovery URL loads the checkout with pre-filled data
  await page.goto(abandoned.abandoned_checkout_url);
  await expect(page.getByLabel(/email/i)).toHaveValue(email);
});

In CI, you may not want to wait for the 10-hour abandonment window. Instead, query the Admin API immediately after the browser leaves the checkout. The checkout record exists as soon as the email is submitted, even though Shopify will not flag it as "abandoned" until the timeout elapses. For testing purposes, confirming the record exists and the recovery URL is valid is sufficient.

9. Common Pitfalls That Break Real Test Suites

Checkout.liquid Deprecation

Shopify has deprecated checkout.liquid in favor of Checkout Extensibility (checkout UI extensions). If your store still uses checkout.liquid customizations, the selectors in your tests target Liquid template markup that Shopify is actively phasing out. Migrate to Checkout Extensibility and update your selectors to target the new web component based structure before the deprecation deadline hits your store.

Checkout Extensibility and Custom Web Components

Stores using Checkout Extensibility render custom sections as web components inside the checkout. These components have shadow DOMs, which means standard Playwright locators like getByLabel or getByRole cannot reach inside them. Use page.locator('custom-element').locator('internal-selector') or enable Playwright's piercing mode for shadow DOM to target elements inside checkout extensions.

Rate Limiting on Development Stores

Shopify rate-limits checkout creation, even on development stores. If your CI runs 50 checkout tests in parallel, you will start seeing 429 responses. Serialize checkout tests or limit parallelism to 3 to 5 workers. Better yet, use Playwright's test.describe.serial for the checkout suite while keeping the rest of your tests parallel.

Flaky Selectors on Multi-Step Navigation

The transition between checkout steps (information to shipping, shipping to payment) is a server-side navigation, not a client-side transition. This means the page fully reloads between steps. Do not assert on elements from the previous step after clicking "Continue." Wait for a selector that is unique to the new step before making any assertions.

Address Autocomplete Interference

Shopify Checkout includes Google-powered address autocomplete. When your test types into the address field, the autocomplete dropdown may intercept focus and fill fields with unexpected values. Disable autocomplete in your test by clicking away from the suggestion dropdown after filling each field, or fill fields in reverse order (ZIP, then city, then address) to minimize autocomplete triggers.

10. Writing These Scenarios in Plain English with Assrt

Every scenario above is 30 to 70 lines of Playwright. Multiply that by the six scenarios you actually need across multiple markets, and you have a 500-line file that breaks the first time Shopify updates a checkout label or rearranges the step order. Assrt lets you describe the scenario in plain English, generates the equivalent Playwright code, and regenerates the selectors automatically when the underlying checkout changes.

The guest checkout happy path from Section 3 looks like this in Assrt:

Guest Checkout: Playwright vs Assrt

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

test.use({ storageState: './test-state.json' });

test('guest checkout: full happy path', async ({ page }) => {
  const email = `test+${Date.now()}@assrt.ai`;
  const storeUrl = process.env.SHOPIFY_STORE_URL!;

  await page.goto(`${storeUrl}/products/test-product`);
  await page.getByRole('button', { name: /add to cart/i }).click();
  await page.goto(`${storeUrl}/cart`);
  await page.getByRole('button', { name: /check out/i }).click();
  await page.waitForURL(/\\/checkouts\\//);

  await page.getByLabel(/email/i).fill(email);
  await page.getByLabel(/first name/i).fill('Jane');
  await page.getByLabel(/last name/i).fill('Tester');
  await page.getByLabel(/address/i).first().fill('123 Test Street');
  await page.getByLabel(/city/i).fill('San Francisco');
  await page.getByLabel(/state/i).selectOption('California');
  await page.getByLabel(/zip/i).fill('94102');

  await page.getByRole('button', { name: /continue to shipping/i }).click();
  await expect(page.getByText(/shipping method/i)).toBeVisible({ timeout: 15_000 });
  await page.getByRole('button', { name: /continue to payment/i }).click();

  await page.getByLabel(/card number/i).fill('1');
  await page.getByLabel(/name on card/i).fill('Bogus Gateway');
  await page.getByLabel(/expiration/i).fill('12/34');
  await page.getByLabel(/security code/i).fill('123');

  await page.getByRole('button', { name: /pay now|complete order/i }).click();
  await expect(page.getByText(/order confirmed|thank you/i)).toBeVisible({ timeout: 30_000 });
  await expect(page.getByText(email)).toBeVisible();
});
45% fewer lines

The discount code scenario from Section 4 looks like this:

# scenarios/shopify-discount-code.assrt
describe: Discount code SAVE20 reduces total on Shopify checkout

given:
  - I am on the product page for "test-product"

steps:
  - click "Add to cart"
  - go to the cart page
  - click "Check out"
  - fill the discount field with "SAVE20"
  - click "Apply"

expect:
  - the text "SAVE20" is visible in the order summary
  - a negative dollar amount appears as the discount line item
  - the total is lower than the subtotal

Assrt compiles these files into the same Playwright TypeScript you saw in the sections above, committed to your repo as real tests you can read, run, and modify. When Shopify renames a button label, changes a field's accessible name, or restructures a checkout step, Assrt detects the failure, analyzes the new DOM, and opens a pull request with the updated locator. Your scenario file stays untouched. This is the failure mode the rest of this guide exists to prevent, and it is the reason we built Assrt.

Start with the guest checkout happy path. Once it is green in your CI, add the discount code test, then account checkout, then international, then cart abandonment. In a single afternoon you can have the full Shopify Checkout coverage that most production e-commerce stores never manage to ship by hand.

Assrt Test Run

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