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.
β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
Apple Pay on Web: End-to-End Flow
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
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.
Playwright Configuration for Apple Pay Testing
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.
Feature Detection: Apple Pay Button Visibility
StraightforwardGoal
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_URLwith HTTPS - Checkout page accessible with items in cart
- Payment Request API mock script ready for injection
Playwright Implementation
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();
});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.
Happy Path: Complete Apple Pay Checkout
ModerateGoal
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
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();
});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.
Merchant Validation: Session Lifecycle
ComplexPlaywright Implementation
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.
Shipping Address Updates During Payment
ComplexPlaywright Implementation
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');
});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.
Declined Payment and Error Handling
ModeratePlaywright Implementation
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.
User Cancellation and Re-try
ModeratePlaywright Implementation
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.
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.
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
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...
How to Test Lemon Squeezy Checkout
A practical, scenario-by-scenario guide to testing Lemon Squeezy checkout with...
Ready to automate your testing?
Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.