Payment Testing Guide

How to Test Apple Pay on Web with Playwright: Complete 2026 Guide

A scenario-by-scenario walkthrough of testing Apple Pay on the web with Playwright. Payment Request API interception, native payment sheet mocking, merchant validation, shipping address updates, error states, and the CI strategies that make Apple Pay testable without a real device.

$6T+

β€œApple Pay has processed over six trillion dollars in transactions globally since launch, according to Apple's 2025 services report, making it the most widely used contactless payment method on the web.”

Apple Services Report, 2025

0%Safari mobile checkout share
0Payment scenarios covered
0sAverage payment sheet latency
0%Fewer lines with Assrt

Apple Pay on Web: End-to-End Flow

BrowserYour AppApple Pay JSApple ServersPayment ProcessorClick Apple Pay buttonnew PaymentRequest()Validate merchantMerchant sessionShow payment sheetUser authorizes (Face ID / Touch ID)Payment token (encrypted)Charge token server-sidePayment confirmedShow success page

1. Why Testing Apple Pay on the Web Is Harder Than It Looks

Apple Pay on the web uses the Payment Request API, a browser-native interface that triggers an operating system level payment sheet. Unlike a standard HTML form that Playwright can fill with locators and clicks, the Apple Pay sheet is rendered by the OS itself, outside of the browser's DOM. Playwright cannot see, click, or interact with that native sheet directly. This single constraint makes Apple Pay fundamentally different from testing any other checkout flow.

The complexity compounds from there. Apple Pay requires HTTPS with a registered domain; it will not activate on localhost or HTTP pages without special configuration. The merchant must complete Apple's domain verification process, which involves hosting a verification file at /.well-known/apple-developer-merchantid-domain-association on the production domain. In sandbox mode, Apple provides test cards and test accounts, but the sandbox environment still requires a real Apple device with Face ID or Touch ID enrolled. CI runners typically run headless Linux browsers, which have no Apple Pay support whatsoever.

There are five structural reasons this flow resists standard end-to-end testing. First, the payment sheet is a native OS overlay, invisible to the browser's accessibility tree. Second, Apple Pay availability depends on the device, browser, and OS version; Safari on macOS and iOS support it, while Chrome and Firefox do not unless a third-party wallet bridges the gap. Third, merchant validation requires a server-to-server call from your backend to Apple's servers, and the resulting session object is opaque and time-limited. Fourth, the encrypted payment token returned after authorization uses Apple's PKI infrastructure and cannot be generated synthetically without Apple's private keys. Fifth, shipping address and contact information updates trigger asynchronous events that your frontend must handle in real time during the payment session.

Apple Pay Web Payment Flow

🌐

Feature Check

canMakePayment()

βš™οΈ

Create Request

new PaymentRequest()

πŸ’³

Show Sheet

request.show()

βš™οΈ

Merchant Validation

Server-to-Apple call

πŸ”’

User Authorizes

Face ID / Touch ID

βœ…

Token Returned

Encrypted payment data

Domain Verification Prerequisite

βš™οΈ

Apple Developer Portal

Register merchant ID

🌐

Download File

Verification file

βš™οΈ

Host at /.well-known/

On your domain

βœ…

Apple Validates

Checks domain ownership

πŸ’³

Merchant ID Active

Apple Pay enabled

The practical solution is to intercept the Payment Request API at the JavaScript level, mock the native payment sheet behavior, and validate your application's handling of the payment lifecycle events. The sections below walk through each scenario with runnable Playwright TypeScript you can paste directly into your test suite.

2. Setting Up a Reliable Test Environment

Since Apple Pay requires Safari on macOS or iOS and a registered domain with HTTPS, your test environment needs a strategy that decouples the browser-level payment flow from the native OS integration. The approach is to use Playwright's page.addInitScript to mock the Payment Request API before your application code runs. This lets you control every aspect of the payment lifecycle: availability checks, payment sheet display, authorization responses, shipping updates, and error conditions.

Apple Pay Test Environment Checklist

  • Create an Apple Developer account with Apple Pay merchant ID configured
  • Set up a Stripe (or Adyen, Braintree) sandbox with Apple Pay enabled
  • Prepare Payment Request API mock scripts for Playwright injection
  • Configure Playwright to use Chromium (mocks bypass Safari requirement)
  • Store test payment tokens in environment variables for server-side validation
  • Set up route interception for merchant validation endpoints
  • Ensure your dev server runs with HTTPS (mkcert for local development)
  • Create fixture files for different payment response scenarios

Environment Variables

.env.test

The Core Payment Request API Mock

The most important piece of infrastructure is the Payment Request API mock. This script replaces the native PaymentRequest constructor with a controlled implementation that simulates Apple Pay behavior without requiring a real device. You inject this script before every test that touches the payment flow.

test/fixtures/apple-pay-mock.ts
Install Dependencies

Playwright Configuration for Apple Pay Testing

playwright.config.ts

3. Scenario: Feature Detection and Availability

Before showing the Apple Pay button, every well-built checkout page checks whether Apple Pay is available. This typically involves calling ApplePaySession.canMakePayments() for Safari-specific implementations or PaymentRequest.canMakePayment() for Payment Request API implementations. Your test needs to verify that the button appears when Apple Pay is available and stays hidden when it is not. This is the foundation for all other Apple Pay tests.

1

Feature Detection: Apple Pay Button Visibility

Straightforward

Goal

Verify that the Apple Pay button appears when the Payment Request API indicates Apple Pay is available, and confirm the button is hidden when Apple Pay is not supported.

Preconditions

  • App running at APP_BASE_URL with HTTPS
  • Checkout page accessible with items in cart
  • Payment Request API mock script ready for injection

Playwright Implementation

apple-pay.spec.ts

Feature Detection: Playwright vs Assrt

import { test, expect } from '@playwright/test';
import { applePayMockScript } from '../fixtures/apple-pay-mock';

test('Apple Pay button visible when supported', async ({ page }) => {
  await page.addInitScript({ content: applePayMockScript });
  await page.goto('/checkout');

  const applePayButton = page.locator('[data-testid="apple-pay-button"]');
  await expect(applePayButton).toBeVisible({ timeout: 10_000 });
  await expect(applePayButton).toHaveAttribute('aria-label', /apple pay/i);
});

test('Apple Pay button hidden when not supported', async ({ page }) => {
  await page.addInitScript({
    content: `
      window.ApplePaySession = undefined;
      window.PaymentRequest = class MockPaymentRequest {
        constructor() {}
        async canMakePayment() { return false; }
        async show() { throw new Error('Not supported'); }
      };
    `,
  });
  await page.goto('/checkout');
  const applePayButton = page.locator('[data-testid="apple-pay-button"]');
  await expect(applePayButton).not.toBeVisible({ timeout: 5_000 });
  await expect(page.getByLabel(/card number/i)).toBeVisible();
});
53% fewer lines

4. Scenario: Happy Path Payment Flow

The happy path for Apple Pay on the web follows a precise sequence. The user clicks the Apple Pay button, which calls PaymentRequest.show() (or ApplePaySession.begin() for Safari-native implementations). The browser displays the native payment sheet. The user authenticates with Face ID or Touch ID. The payment sheet returns an encrypted payment token. Your frontend sends that token to your backend, which submits it to your payment processor (Stripe, Adyen, Braintree, or direct Apple Pay integration). The processor decrypts the token, charges the card, and returns a confirmation. Your frontend displays a success page.

In Playwright, you mock the entire native payment sheet portion of this flow. When your application calls show(), the mock immediately resolves with a synthetic payment response containing a test token. You also intercept the server-side payment endpoint to return a successful charge response. This lets you test the complete end-to-end flow from button click to confirmation page without ever touching a real Apple device.

2

Happy Path: Complete Apple Pay Checkout

Moderate

Goal

Starting from the checkout page, click the Apple Pay button, verify the mocked payment sheet resolves with a token, confirm the server-side charge succeeds, and land on the order confirmation page.

Preconditions

  • App running with items in cart totaling $99.99
  • Payment Request API mock injected
  • Server-side payment endpoint intercepted to return success

Playwright Implementation

apple-pay.spec.ts

What to Assert Beyond the UI

Happy Path Verification Checklist

  • Payment token was forwarded to the server endpoint
  • Token contains required paymentData and paymentMethod fields
  • Server responded with a valid order ID
  • Order confirmation page shows the correct total
  • PaymentResponse.complete('success') was called
  • No JavaScript errors in the console during the flow

Happy Path Checkout: Playwright vs Assrt

import { test, expect } from '@playwright/test';
import { applePayMockScript } from '../fixtures/apple-pay-mock';

test('Apple Pay happy path: complete checkout', async ({ page }) => {
  await page.addInitScript({ content: applePayMockScript });
  await page.route('**/api/payments/apple-pay', async (route) => {
    const body = route.request().postDataJSON();
    expect(body.token).toBeDefined();
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        success: true, orderId: 'ORD-TEST-001', amount: 9999, currency: 'USD'
      }),
    });
  });

  await page.goto('/checkout');
  await expect(page.getByText('$99.99')).toBeVisible();
  await page.locator('[data-testid="apple-pay-button"]').click();

  const sheetShown = await page.evaluate(() => window.__paymentSheetShown);
  expect(sheetShown).toBe(true);

  await page.waitForURL(/\/order-confirmation/, { timeout: 15_000 });
  await expect(page.getByRole('heading', { name: /order confirmed/i }))
    .toBeVisible();
  await expect(page.getByText('$99.99')).toBeVisible();
});
57% fewer lines

Try Assrt for free

Open-source AI testing framework. No signup required.

Get Started β†’

5. Scenario: Merchant Validation and Session Creation

When using the Safari-native ApplePaySession API, Apple requires merchant validation before the payment sheet becomes interactive. After calling session.begin(), Apple fires the onvalidatemerchantevent with a validation URL. Your frontend must send this URL to your backend, which makes a server-to-server POST request to Apple's payment gateway with your merchant certificate. Apple responds with an opaque merchant session object, which your backend returns to the frontend. The frontend then calls session.completeMerchantValidation(merchantSession) to unlock the payment sheet.

This validation step is where most Apple Pay integrations fail silently. If your merchant certificate is expired, the domain is not registered, or the server-to-server call times out, the payment sheet closes without any visible error. The user sees the sheet appear and then vanish. Testing this properly means intercepting the merchant validation endpoint and verifying your application handles both success and failure cases.

3

Merchant Validation: Session Lifecycle

Complex

Playwright Implementation

apple-pay-merchant.spec.ts

6. Scenario: Shipping Address and Method Updates

Apple Pay allows users to select shipping addresses and shipping methods directly within the payment sheet. When the user changes their shipping address, Apple fires the onshippingaddresschange event (or onshippingcontactselected in the ApplePaySession API). Your application must respond with updated shipping costs, tax calculations, and available shipping methods for the new address. If the address is in an unsupported region, you must return an error status that Apple displays in the payment sheet.

This is one of the trickiest flows to test because it involves asynchronous event handling within the payment session. Your mock needs to simulate the user changing their shipping address and verify that your application responds with the correct updated totals.

4

Shipping Address Updates During Payment

Complex

Playwright Implementation

apple-pay-shipping.spec.ts

Shipping Updates: Playwright vs Assrt

test('shipping address change updates totals', async ({ page }) => {
  // ... 60+ lines of mock setup and event simulation ...
  await page.addInitScript({ content: shippingMockScript });
  await page.route('**/api/shipping/calculate', async (route) => {
    const body = route.request().postDataJSON();
    await route.fulfill({
      status: 200,
      body: JSON.stringify({
        shippingMethods: [
          { label: 'Standard', amount: '5.99', identifier: 'standard' },
          { label: 'Express', amount: '12.99', identifier: 'express' },
        ],
        newTotal: { label: 'Your App', amount: '105.98' },
      }),
    });
  });

  await page.goto('/checkout');
  await page.locator('[data-testid="apple-pay-button"]').click();

  const updates = await page.evaluate(() => window.__shippingUpdates);
  expect(updates[0].newTotal.amount).toBe('105.98');
});
76% fewer lines

7. Scenario: Error States and Declined Payments

Payment failures with Apple Pay are harder to surface than with traditional card forms. When a payment is declined, your backend receives an error from the payment processor, but the Apple Pay sheet has already closed. Your application must handle the PaymentResponse.complete('fail') signal and display an appropriate error message in your own UI. If you use the ApplePaySession API, you call completePayment(ApplePaySession.STATUS_FAILURE) to dismiss the sheet with a failure animation.

There are several distinct failure modes to test: processor declines (insufficient funds, expired card), network timeouts during token submission, invalid billing address, and your own backend returning an error. Each of these should result in a clear, user-facing message that guides the customer to try a different payment method.

5

Declined Payment and Error Handling

Moderate

Playwright Implementation

apple-pay-errors.spec.ts
Error Handling Test Run

8. Scenario: User Cancellation and Timeout Handling

Users can dismiss the Apple Pay sheet at any time by tapping Cancel, pressing Escape, or simply ignoring the sheet until it times out. When this happens, the PaymentRequest.show() promise rejects with an AbortError, or the ApplePaySession fires the oncancel event. Your application must handle this gracefully: reset the checkout state, keep the user on the checkout page, and allow them to try again or choose a different payment method.

A common bug is failing to re-enable the Apple Pay button after a cancellation. Many implementations disable the button when clicked to prevent double submissions, but forget to re-enable it when the payment is cancelled. Another subtle issue is the Apple Pay session timeout: if the user leaves the payment sheet open for too long, Apple closes it automatically, and your application must detect this as a cancellation rather than a payment error.

6

User Cancellation and Re-try

Moderate

Playwright Implementation

apple-pay-cancel.spec.ts

9. Common Pitfalls That Break Apple Pay Test Suites

Testing on Non-Safari Browsers Without Mocks

The most common mistake is attempting to run Apple Pay tests in Chrome or Firefox without mocking the Payment Request API. Apple Pay is only natively available in Safari on macOS and iOS. Chrome and Firefox do not expose ApplePaySession at all. If your tests rely on the native API being present, they will fail immediately in CI environments that use headless Chromium. Always inject your Payment Request API mock via page.addInitScript() before navigating to the page. This decouples your tests from the browser vendor.

Domain Verification File Missing or Stale

Apple requires a verification file at /.well-known/apple-developer-merchantid-domain-association. This file must be accessible without authentication, must return the exact content Apple provided (no trailing newline, no HTML wrapper), and must be served with the correct content type. When you deploy to a new environment (staging, preview), the verification file may not be present, causing merchant validation to fail silently. Add a health check test that fetches this endpoint and verifies the response matches your expected merchant ID. A GitHub issue (apple-pay-js #287) documents how CDN caching of stale verification files causes intermittent failures in production.

Merchant Certificate Expiration

Apple Pay merchant identity certificates expire every 25 months. When the certificate expires, the server-to-server merchant validation call fails, and the payment sheet silently dismisses. There is no visible error in the browser console. Many teams discover this only when customers report that Apple Pay β€œjust stopped working.” Add a monitoring check that validates the certificate expiration date and alerts at least 30 days before expiry. In your test suite, include a test that verifies the merchant validation endpoint responds with a valid session.

Forgetting to Call complete() on PaymentResponse

After processing the payment on your backend, you must call PaymentResponse.complete('success') or complete('fail') to dismiss the payment sheet. If you forget this call, the sheet remains open indefinitely on iOS, blocking the entire browser. The user cannot interact with your page until they force-close the browser. This is one of the most reported issues in Apple Pay integrations on Stack Overflow. In your mock, track whether complete() was called by storing the result in a window variable, and assert against it in every payment test.

Race Conditions in Shipping Address Events

When the user rapidly changes shipping addresses in the payment sheet, your application receives multiple onshippingaddresschange events in quick succession. If your handler makes an API call for each event without debouncing or cancelling the previous request, you can end up with stale shipping costs displayed for the wrong address. The last response wins, but it may not correspond to the last address selected. Test this by triggering two rapid shipping address changes in your mock and verifying that only the final calculation is applied.

Apple Pay Full Test Suite

10. Writing These Scenarios in Plain English with Assrt

The Apple Pay test suite above contains over 300 lines of mock setup code, event simulation logic, and route interception configuration. The Payment Request API mock alone is 100 lines of JavaScript that you must maintain alongside the actual tests. Every time Apple updates the ApplePaySession API version, adds new events, or changes the payment token structure, you need to update the mock, update the assertions, and re-verify every scenario. This maintenance burden is why most teams stop at testing the happy path and skip error states, shipping updates, and cancellation flows entirely.

Assrt eliminates this maintenance by letting you describe what you want to test in plain English. It handles the mock infrastructure, event simulation, and API interception internally. When Apple releases a new ApplePaySession version or your payment processor changes their API, Assrt regenerates the underlying Playwright code while your scenario files remain unchanged. Here is the complete happy path scenario from Section 4, written as an Assrt file.

scenarios/apple-pay-full-suite.assrt

Assrt compiles each scenario block into the same Playwright TypeScript you saw in the preceding sections, including the Payment Request API mock, route interception, and all assertions. The generated code is committed to your repository as real tests you can read, run, and modify. When Apple changes the payment sheet behavior or your payment processor updates their API response format, Assrt detects the failure, analyzes the new behavior, and opens a pull request with updated test code. Your scenario files stay untouched.

Start with the feature detection scenario. Once it passes in CI, add the happy path, then the error handling, then shipping address updates, then the cancellation flow. In a single afternoon you can have complete Apple Pay coverage that most production e-commerce applications never achieve by hand.

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