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.
“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.”
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
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
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.
| Gateway | Success Card | Decline Card |
|---|---|---|
| Bogus Gateway | 1 | 2 |
| Shopify Payments (test) | 4242 4242 4242 4242 | 4000 0000 0000 0002 |
| Shopify Payments (test) | 5555 5555 5555 4444 | 4000 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.
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.
Guest Checkout: Product to Confirmation
ModerateStarting 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 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.
Discount Code Application with Live Price Recalculation
Moderatetest('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.
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.
Logged-In Customer with Saved Address
Moderatetest('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.
Express Checkout Button Visibility and Fallback
Complextest('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.
International Order with Tax, Duty, and Currency
Complextest('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).
Cart Abandonment Creates Recoverable Checkout
Straightforwardtest('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();
});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 subtotalAssrt 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.
Related Guides
How to Test Apple Pay on Web
A practical, scenario-by-scenario guide to testing Apple Pay on the web with Playwright....
How to Test BigCommerce Checkout
A scenario-by-scenario guide to testing BigCommerce checkout with Playwright. Covers...
How to Test Google Pay on Web
Step-by-step guide to testing Google Pay web integration with Playwright. Covers Payment...
Ready to automate your testing?
Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.