Payments Testing Guide
How to Test PayPal Checkout End to End: Complete 2026 Guide
A scenario-by-scenario walkthrough of testing PayPal Checkout with Playwright. Popup window handling, sandbox account setup, balance payments, guest card checkout, Pay Later flows, cancellation, and the cross-origin pitfalls that break real suites.
“PayPal serves over 400 million active accounts globally. Its Checkout popup flow is one of the most common payment integrations on the web, and one of the hardest to test end to end because it relies on a cross-origin popup window.”
1. Why Testing PayPal Checkout Is Different
PayPal Checkout does not behave like Stripe, Braintree, or any card-input integration you have tested before. When your user clicks the PayPal button on your site, a full popup window opens on paypal.com. That popup is an entirely separate browser context on a cross-origin domain, with its own login flow, its own DOM, and its own session. Your main page sits idle in the background until the user approves the payment inside that popup, at which point the popup closes and your page receives an onApprove callback.
This architecture creates four structural testing challenges. First, the PayPal button itself renders inside an iframe on your page, so you need a frameLocator just to click it. Second, the popup is a new window that Playwright must capture via page.waitForEvent('popup') before you can interact with it. Third, the popup domain is paypal.com, which means all your locators inside that window target a page you do not control and that PayPal can redesign at any time. Fourth, the popup must close cleanly for the onApprove callback to fire on your original page. If the popup is blocked, dismissed, or stuck in an error state, your payment flow silently fails.
A complete PayPal Checkout test suite must handle all four surfaces: the iframe button click, the popup lifecycle, the cross-origin login and approval flow, and the callback-driven state transition on your own page. The sections below walk through each scenario with runnable Playwright TypeScript code.
PayPal Checkout Popup Flow
Your Page
User sees PayPal button in iframe
Click Button
frameLocator targets PayPal iframe
Popup Opens
paypal.com in new window
Sandbox Login
Email + password on PayPal
Approve Payment
User clicks Continue
Popup Closes
Window dismissed automatically
onApprove Fires
Callback on your page
PayPal Checkout Sequence (Full Popup Lifecycle)
2. Setting Up Your Test Environment
PayPal provides a full sandbox environment that mirrors production. Before writing a single test, you need three things: a sandbox business account (the merchant), a sandbox personal account (the buyer), and REST API credentials linked to the business account. All three are created in the PayPal Developer Dashboard.
Creating Sandbox Accounts
Navigate to developer.paypal.com, sign in, and go to Testing Tools, then Sandbox Accounts. Create one Business account and one Personal account. The personal account is the buyer your tests will log in as inside the popup. Note the email and password for both accounts. PayPal auto-generates passwords, so copy them immediately.
Sandbox Account Setup Checklist
- Sign in to developer.paypal.com with your real PayPal account
- Go to Testing Tools, then Sandbox Accounts
- Create one Business (merchant) sandbox account
- Create one Personal (buyer) sandbox account
- Copy the auto-generated email and password for both accounts
- Ensure the personal account has a positive sandbox balance
- Store credentials in .env.test (never commit this file)
REST API Credentials
Under Apps & Credentials in the Developer Dashboard, create a new app linked to your sandbox business account. This gives you a Client ID and Secret. Your server uses these to create orders and capture payments via the PayPal REST API. Your frontend uses the Client ID to render the PayPal buttons.
Playwright Configuration for Popups
PayPal Checkout opens a popup window. By default, some Playwright configurations block popups or fail to track them. You need to ensure your browser context allows popups and that you register the popup event listener before clicking the PayPal button.
Helper: Clicking the PayPal Button Inside Its Iframe
The PayPal JavaScript SDK renders its buttons inside an iframe on your page. Every test starts by clicking that button, so extract this into a shared helper that your scenarios can reuse.
// helpers/paypal.ts
import { Page } from '@playwright/test';
export async function clickPayPalButton(page: Page) {
// The PayPal button iframe uses a name that starts with
// "__zoid__paypal_buttons__" or a title containing "PayPal"
const paypalFrame = page.frameLocator(
'iframe[name^="__zoid__paypal_buttons"]'
);
// Inside the iframe, click the primary PayPal button
await paypalFrame
.getByRole('button', { name: /paypal/i })
.first()
.click();
}Test Environment Setup Checklist
Sandbox Accounts
Business + Personal in Developer Dashboard
REST API Creds
Client ID + Secret for sandbox app
Env Variables
.env.test with all credentials
Playwright Config
Popup support, generous timeouts
Shared Helpers
clickPayPalButton utility
3. Scenario: Happy Path PayPal Balance Payment
PayPal Balance Payment
ModerateThe most common PayPal flow: the buyer logs into the PayPal popup with their sandbox credentials, pays with their PayPal balance, and the popup closes. Your page receives the onApprove callback, captures the order server-side, and shows a confirmation.
Goal
Starting from your checkout page, complete a full PayPal payment using sandbox buyer credentials, return to your page after popup closes, and confirm the order is captured.
Preconditions
- App running at
APP_BASE_URLwith PayPal sandbox Client ID - Sandbox personal account has a positive balance
- Server-side order creation endpoint is functional
Playwright vs Assrt Implementation
Happy Path: PayPal Balance Payment
import { test, expect } from '@playwright/test';
import { clickPayPalButton } from '../helpers/paypal';
test('happy path: PayPal balance payment succeeds', async ({ page }) => {
// 1. Navigate to your checkout page
await page.goto('/checkout');
await page.getByRole('button', { name: 'Buy Pro plan' }).click();
// 2. Wait for PayPal buttons to load inside their iframe
const paypalFrame = page.frameLocator(
'iframe[name^="__zoid__paypal_buttons"]'
);
await expect(
paypalFrame.getByRole('button', { name: /paypal/i }).first()
).toBeVisible({ timeout: 15_000 });
// 3. Register the popup listener BEFORE clicking the button.
const popupPromise = page.waitForEvent('popup');
await clickPayPalButton(page);
const popup = await popupPromise;
// 4. Wait for the PayPal login page to load in the popup
await popup.waitForLoadState('domcontentloaded');
// 5. Log in with sandbox buyer credentials
await popup.getByLabel('Email or mobile number').fill(
process.env.PAYPAL_SANDBOX_BUYER_EMAIL!
);
await popup.getByRole('button', { name: 'Next' }).click();
await popup.getByLabel('Password').fill(
process.env.PAYPAL_SANDBOX_BUYER_PASSWORD!
);
await popup.getByRole('button', { name: 'Log In' }).click();
// 6. Wait for the review page and approve the payment
await popup.waitForLoadState('domcontentloaded');
await popup.getByRole('button', { name: /continue/i })
.click({ timeout: 30_000 });
// 7. The popup closes automatically after approval.
await popup.waitForEvent('close', { timeout: 30_000 });
// 8. Your onApprove callback fires. Assert success UI.
await expect(page.getByRole('heading', { name: /thank you/i }))
.toBeVisible({ timeout: 30_000 });
await expect(page.getByText(/order confirmed/i)).toBeVisible();
});4. Scenario: Guest Checkout with Card Inside PayPal
Guest Card Checkout via PayPal Popup
ComplexPayPal offers a guest checkout option where buyers can pay with a credit or debit card without logging into a PayPal account. This flow still happens inside the PayPal popup, but instead of entering PayPal credentials, the buyer fills in card details directly. This is a critical path because many buyers prefer not to create a PayPal account.
The Guest Checkout Flow
Inside the PayPal popup, there is typically a link or tab labeled something like "Pay with Debit or Credit Card" below the login form. Clicking it transitions the popup to a card entry form. This form is rendered by PayPal, not by your application, so the field labels and layout are controlled by PayPal and may vary by region. In sandbox mode, you can use any valid-format card number and it will succeed.
Playwright Implementation
test('guest checkout: card payment without PayPal account', async ({ page }) => {
await page.goto('/checkout');
await page.getByRole('button', { name: 'Buy Pro plan' }).click();
const paypalFrame = page.frameLocator(
'iframe[name^="__zoid__paypal_buttons"]'
);
await expect(
paypalFrame.getByRole('button', { name: /paypal/i }).first()
).toBeVisible({ timeout: 15_000 });
const popupPromise = page.waitForEvent('popup');
await clickPayPalButton(page);
const popup = await popupPromise;
await popup.waitForLoadState('domcontentloaded');
// Click the guest checkout link (below the login form)
await popup.getByRole('link', { name: /debit or credit card/i })
.click({ timeout: 15_000 });
// Fill in card details inside the PayPal popup
await popup.getByLabel(/card number/i).fill('4032039317984658');
await popup.getByLabel(/expiry/i).fill('12/2030');
await popup.getByLabel(/CVV|CVC|security code/i).fill('123');
// Fill billing info
await popup.getByLabel(/first name/i).fill('Test');
await popup.getByLabel(/last name/i).fill('Buyer');
await popup.getByLabel(/email/i).fill('guest@assrt.ai');
await popup.getByLabel(/phone/i).fill('4155551234');
// Address fields
await popup.getByLabel(/address line 1/i).fill('123 Test Street');
await popup.getByLabel(/city/i).fill('San Jose');
await popup.getByLabel(/state/i).selectOption('CA');
await popup.getByLabel(/postal code|zip/i).fill('95131');
// Submit the guest payment
await popup.getByRole('button', { name: /pay now|continue/i })
.click({ timeout: 15_000 });
// Wait for popup to close and assert on the original page
await popup.waitForEvent('close', { timeout: 30_000 });
await expect(page.getByRole('heading', { name: /thank you/i }))
.toBeVisible({ timeout: 30_000 });
});Assrt Plain-English Equivalent
# scenarios/paypal-guest-card.assrt
describe: Guest checkout with card inside PayPal popup
given:
- I am on the checkout page
steps:
- click "Buy Pro plan"
- click the PayPal button inside its iframe
- a popup window opens on paypal.com
- in the popup, click "Pay with Debit or Credit Card"
- fill the card number with 4032039317984658
- fill the expiry with 12/2030
- fill the CVV with 123
- fill billing name, email, phone, and address
- click "Pay Now"
- the popup closes
expect:
- the page shows a thank you heading
- the order API returns "captured" status within 20 seconds5. Scenario: User Cancels the PayPal Popup
PayPal Popup Cancellation
ModerateNot every user completes the PayPal flow. Some close the popup window before approving. Others click a cancel link inside the PayPal UI. Your application must handle both gracefully: the onCancel callback should fire, the order should remain unpaid, and the user should see a clear message explaining they can try again.
There are two cancellation paths to test. The first is the user closing the popup window directly (clicking the X button or the browser dismissing it). The second is the user clicking a "Cancel and return" link inside the PayPal approval page. Both should trigger the same recovery behavior on your page.
Playwright Implementation: Close the Popup Window
test('cancel: user closes the PayPal popup window', async ({ page }) => {
await page.goto('/checkout');
await page.getByRole('button', { name: 'Buy Pro plan' }).click();
const paypalFrame = page.frameLocator(
'iframe[name^="__zoid__paypal_buttons"]'
);
await expect(
paypalFrame.getByRole('button', { name: /paypal/i }).first()
).toBeVisible({ timeout: 15_000 });
const popupPromise = page.waitForEvent('popup');
await clickPayPalButton(page);
const popup = await popupPromise;
await popup.waitForLoadState('domcontentloaded');
// Close the popup without completing the flow
await popup.close();
// Assert your page handles the cancellation
await expect(page.getByText(/payment cancelled|try again/i))
.toBeVisible({ timeout: 15_000 });
// The PayPal button should still be available for retry
await expect(
paypalFrame.getByRole('button', { name: /paypal/i }).first()
).toBeVisible();
// Assert no order was created
const response = await page.request.get('/api/orders/latest');
const body = await response.json();
expect(body.status).not.toBe('captured');
});Playwright Implementation: Cancel Link Inside Popup
test('cancel: user clicks cancel link inside PayPal', async ({ page }) => {
await page.goto('/checkout');
await page.getByRole('button', { name: 'Buy Pro plan' }).click();
const paypalFrame = page.frameLocator(
'iframe[name^="__zoid__paypal_buttons"]'
);
await expect(
paypalFrame.getByRole('button', { name: /paypal/i }).first()
).toBeVisible({ timeout: 15_000 });
const popupPromise = page.waitForEvent('popup');
await clickPayPalButton(page);
const popup = await popupPromise;
await popup.waitForLoadState('domcontentloaded');
// Log in first, then cancel on the approval page
await popup.getByLabel('Email or mobile number').fill(
process.env.PAYPAL_SANDBOX_BUYER_EMAIL!
);
await popup.getByRole('button', { name: 'Next' }).click();
await popup.getByLabel('Password').fill(
process.env.PAYPAL_SANDBOX_BUYER_PASSWORD!
);
await popup.getByRole('button', { name: 'Log In' }).click();
await popup.waitForLoadState('domcontentloaded');
// Click the cancel link on the review page
await popup.getByRole('link', { name: /cancel/i }).click();
// Popup closes after cancel
await popup.waitForEvent('close', { timeout: 15_000 });
// Assert cancellation is handled on your page
await expect(page.getByText(/payment cancelled|try again/i))
.toBeVisible({ timeout: 15_000 });
});Assrt Plain-English Equivalent
# scenarios/paypal-cancel.assrt
describe: User cancels the PayPal popup
given:
- I am on the checkout page
steps:
- click "Buy Pro plan"
- click the PayPal button inside its iframe
- a popup window opens on paypal.com
- close the popup window without completing payment
expect:
- the page shows a cancellation or retry message
- the PayPal button is still available
- no order was captured6. Scenario: Pay Later / Pay in 4 Installments
Pay Later / Pay in 4
ComplexPayPal offers "Pay Later" and "Pay in 4" options that let buyers split their purchase into installments. When enabled, a separate "Pay Later" button renders alongside the standard PayPal button inside the iframe. Clicking it opens the same popup window but routes the buyer through an installment approval flow instead of a single payment.
In sandbox mode, Pay Later is available for US accounts with orders between $30 and $2,000. The sandbox will present the installment plan details and let the test buyer approve. Your server still captures the full order amount; PayPal handles the installment collection from the buyer independently.
Playwright Implementation
test('Pay Later: buyer approves Pay in 4 installments', async ({ page }) => {
// Order must be $30-$2000 for Pay Later eligibility
await page.goto('/checkout?plan=pro-annual');
await page.getByRole('button', { name: 'Buy Pro plan' }).click();
const paypalFrame = page.frameLocator(
'iframe[name^="__zoid__paypal_buttons"]'
);
// The Pay Later button is a separate button inside the iframe
const payLaterButton = paypalFrame
.getByRole('button', { name: /pay later/i })
.first();
await expect(payLaterButton).toBeVisible({ timeout: 15_000 });
const popupPromise = page.waitForEvent('popup');
await payLaterButton.click();
const popup = await popupPromise;
await popup.waitForLoadState('domcontentloaded');
// Log in with sandbox buyer
await popup.getByLabel('Email or mobile number').fill(
process.env.PAYPAL_SANDBOX_BUYER_EMAIL!
);
await popup.getByRole('button', { name: 'Next' }).click();
await popup.getByLabel('Password').fill(
process.env.PAYPAL_SANDBOX_BUYER_PASSWORD!
);
await popup.getByRole('button', { name: 'Log In' }).click();
await popup.waitForLoadState('domcontentloaded');
// The Pay Later flow shows installment details.
// Verify the installment info is displayed.
await expect(popup.getByText(/pay in 4|4 payments/i))
.toBeVisible({ timeout: 20_000 });
// Approve the installment plan
await popup.getByRole('button', { name: /agree.*continue|continue/i })
.click({ timeout: 15_000 });
await popup.waitForEvent('close', { timeout: 30_000 });
// Assert success on your page (same as regular PayPal flow)
await expect(page.getByRole('heading', { name: /thank you/i }))
.toBeVisible({ timeout: 30_000 });
});Assrt Plain-English Equivalent
# scenarios/paypal-pay-later.assrt
describe: Pay Later installment flow via PayPal
given:
- I am on the checkout page with a $99 annual plan
- PayPal sandbox buyer credentials are configured
steps:
- click "Buy Pro plan"
- click the "Pay Later" button inside the PayPal iframe
- a popup window opens on paypal.com
- in the popup, log in with sandbox buyer credentials
- verify installment details show "Pay in 4" or "4 payments"
- click "Agree and Continue" to approve the installment plan
- the popup closes
expect:
- the page shows a thank you heading
- the order API returns "captured" status within 20 seconds7. Scenario: Server-Side Order Capture and Error Handling
With PayPal Checkout, the buyer approves the payment in the popup, but the actual money transfer does not happen until your server calls the PayPal Orders API to capture the order. This is fundamentally different from Stripe Checkout, where Stripe handles the capture automatically. If your server-side capture fails, the buyer approved a payment that never went through.
The onApprove callback on your frontend receives the order ID. Your frontend sends that ID to your server, which calls POST /v2/checkout/orders/{id}/capture on the PayPal API. If that call fails (network error, insufficient buyer funds at capture time, order already captured), your application must show a meaningful error and not leave the buyer thinking they paid successfully.
Server-Side Capture Flow
Buyer Approves
onApprove fires with order ID
Frontend POST
Sends order ID to your server
Server Captures
POST /v2/checkout/orders/{id}/capture
PayPal Responds
COMPLETED or error status
UI Updates
Success or error message
Testing the Capture Failure Path
To test capture failures, intercept the network request from your frontend to your server and simulate a failure response. This verifies that your onApprove handler correctly shows an error state when the capture call does not succeed.
test('capture failure: server returns error after approval', async ({ page }) => {
await page.goto('/checkout');
await page.getByRole('button', { name: 'Buy Pro plan' }).click();
// Intercept your server's capture endpoint to simulate failure
await page.route('**/api/paypal/capture', (route) => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({
error: 'INSTRUMENT_DECLINED',
message: 'The instrument presented was declined.',
}),
});
});
const paypalFrame = page.frameLocator(
'iframe[name^="__zoid__paypal_buttons"]'
);
await expect(
paypalFrame.getByRole('button', { name: /paypal/i }).first()
).toBeVisible({ timeout: 15_000 });
const popupPromise = page.waitForEvent('popup');
await clickPayPalButton(page);
const popup = await popupPromise;
await popup.waitForLoadState('domcontentloaded');
// Complete the PayPal login and approval
await popup.getByLabel('Email or mobile number').fill(
process.env.PAYPAL_SANDBOX_BUYER_EMAIL!
);
await popup.getByRole('button', { name: 'Next' }).click();
await popup.getByLabel('Password').fill(
process.env.PAYPAL_SANDBOX_BUYER_PASSWORD!
);
await popup.getByRole('button', { name: 'Log In' }).click();
await popup.waitForLoadState('domcontentloaded');
await popup.getByRole('button', { name: /continue/i })
.click({ timeout: 30_000 });
await popup.waitForEvent('close', { timeout: 30_000 });
// The capture fails server-side. Assert error UI.
await expect(page.getByText(/payment could not be completed/i))
.toBeVisible({ timeout: 15_000 });
await expect(page.getByRole('button', { name: /try again|retry/i }))
.toBeVisible();
});Testing Idempotent Capture
Another important edge case: the buyer approves, your frontend sends the capture request, the network drops, and the frontend retries. If your server does not handle idempotent capture correctly, you may try to capture the same order twice and get an ORDER_ALREADY_CAPTURED error. Test this by letting the first capture succeed, then verifying that a second capture attempt does not crash your application or show a confusing error to the buyer.
test('idempotent capture: retrying does not break the flow', async ({ page, request }) => {
// Complete a normal PayPal flow and capture successfully
// ... (PayPal popup login and approval as above)
// After the first successful capture, simulate a retry
// by calling the capture endpoint again with the same order ID
const orderId = await page.evaluate(
() => (window as any).__lastPayPalOrderId
);
const retryResponse = await request.post('/api/paypal/capture', {
data: { orderId },
});
const retryBody = await retryResponse.json();
// Your server should handle this gracefully
expect([200, 409]).toContain(retryResponse.status());
expect(retryBody.error).not.toBe('UNHANDLED_EXCEPTION');
});8. Common Pitfalls That Break PayPal Test Suites
Popup Blockers and Missed Popup Events
The single most common failure in PayPal Checkout testing is missing the popup. If you call clickPayPalButton(page) and then call page.waitForEvent('popup'), the popup may have already opened and closed before the listener was registered. Always create the popup promise first, then click. The correct order is: const popupPromise = page.waitForEvent('popup'), then click, then await popupPromise. Getting this order wrong causes tests to hang indefinitely.
Sandbox Account Expiry and Password Resets
PayPal sandbox accounts can expire or have their passwords reset by PayPal without warning. If your tests suddenly start failing at the login step, check the Developer Dashboard to confirm the sandbox personal account still exists and its password has not changed. Consider creating fresh sandbox accounts at the start of each CI run using the PayPal Sandbox Accounts API.
// Create a fresh sandbox buyer before each test run
const token = await getPayPalAccessToken();
const res = await fetch(
'https://api-m.sandbox.paypal.com/v1/customer/partners/merchant-accounts',
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
// Use PayPal's Sandbox Accounts API to generate
// a fresh personal account for each test run
}),
}
);PayPal Button Iframe Selectors Are Unstable
The PayPal JavaScript SDK generates iframe names dynamically. The name attribute includes a hash or session identifier that changes on every page load. Never match on the full iframe name. Use a prefix match like iframe[name^="__zoid__paypal_buttons"] and avoid matching on any suffix characters. Similarly, the button inside the iframe may be labeled differently depending on the buyer's locale, so use a case-insensitive regex like /paypal/i rather than an exact string match.
Sandbox Environment Slowness
The PayPal sandbox is notoriously slower than production. Pages inside the popup can take 10 to 20 seconds to load, especially the login page and the review page. If your tests use the default Playwright timeout of 5 seconds, they will fail intermittently. Set a generous timeout on every interaction inside the popup. A timeout of 30 seconds is reasonable for sandbox testing. This is not a waitForTimeout sleep; it is a maximum wait on an auto-retrying assertion or action.
PayPal Login Flow Changes
PayPal periodically updates its login UI inside the popup. The email and password fields may be on separate pages (two-step login) or on the same page (single-step login). Your tests must handle both. Check whether a "Next" button exists after filling the email; if it does, click it and then fill the password on the next page. If it does not, both fields are on the same page and you can fill them in sequence.
Not Cleaning Up Sandbox Orders
Every test run creates real orders in your PayPal sandbox. Unlike Stripe, PayPal does not offer a convenient test mode cleanup. Over time, the sandbox accumulates thousands of orders that make debugging painful. Add a teardown step that voids uncaptured orders and records captured order IDs for manual review if needed.
9. Writing These Scenarios in Plain English with Assrt
Every scenario above involves managing popups, cross-origin navigation, iframe frame locators, and timing-sensitive assertions. A single PayPal test is 50 to 90 lines of Playwright TypeScript. Multiply that by the five or six scenarios you need for reasonable coverage, and you have a 400-line file that breaks the moment PayPal renames a button label or restructures its login flow.
Assrt lets you describe each scenario in plain English. It generates the Playwright code, handles the popup lifecycle, and regenerates selectors automatically when PayPal changes its UI. The happy path from Section 3 looks like this:
# scenarios/paypal-full-suite.assrt
describe: Complete PayPal Checkout test suite
---
scenario: Happy path PayPal balance payment
given:
- I am on the checkout page
steps:
- click "Buy Pro plan"
- click the PayPal button
- in the PayPal popup, log in with sandbox credentials
- approve the payment
expect:
- the page shows "order confirmed"
- the order is captured in the database
---
scenario: Guest card checkout
given:
- I am on the checkout page
steps:
- click "Buy Pro plan"
- click the PayPal button
- in the PayPal popup, choose "Pay with Debit or Credit Card"
- fill in card 4032039317984658, expiry 12/2030, CVV 123
- fill in billing details and submit
expect:
- the page shows "order confirmed"
---
scenario: User cancels PayPal popup
given:
- I am on the checkout page
steps:
- click "Buy Pro plan"
- click the PayPal button
- close the PayPal popup without completing
expect:
- the page shows a cancellation message
- the PayPal button is still available for retry
---
scenario: Pay Later installments
given:
- I am on the checkout page with a $99 plan
steps:
- click "Buy Pro plan"
- click the "Pay Later" button
- in the popup, log in and approve the installment plan
expect:
- the page shows "order confirmed"
- the order is captured at the full amountAssrt compiles each scenario block into the same Playwright TypeScript you saw in the sections above, committed to your repo as a real test file you can inspect, run, and debug. When PayPal changes its popup login from two-step to single-step, or renames the "Continue" button to "Agree and Pay," Assrt detects the failure, analyzes the new popup DOM, and opens a pull request with the updated locators. Your scenario file stays unchanged.
Start with the happy path scenario. Once it is green in your CI, add guest checkout, then cancellation, then Pay Later, then the capture failure test. In a single afternoon you can build the full PayPal Checkout coverage that most teams 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.