Files
cfw-autumn/server/tests/utils/browserPool/playwright/stripeCheckout.ts
2026-03-02 21:06:47 +00:00

159 lines
5.6 KiB
TypeScript

import type { Page } from "playwright-core";
/**
* Self-contained Playwright function for completing a Stripe Checkout session page.
* NO external imports — this function must be serializable via fn.toString()
* for Kernel Playwright Execution. (type imports are fine — Bun strips them)
*
* Handles checkout.stripe.com pages which have:
* 1. Direct DOM selectors (#cardNumber, #cardExpiry, #cardCvc, #billingName)
* 2. Optional adjustable quantity (.AdjustableQuantitySelector)
* 3. Optional promo code (#promotionCode)
* 4. Submit button (.SubmitButton-TextContainer)
*/
export const stripeCheckout = async ({
page,
url,
overrideQuantity,
promoCode,
}: {
page: Page;
url: string;
overrideQuantity?: number;
promoCode?: string;
}) => {
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 60000 });
console.log("[stripeCheckout] Page loaded");
// Wait for the checkout page to render
await page.waitForTimeout(3000);
// Select the Card payment method. The radio input is hidden behind an
// AccordionButton overlay with expandedClickArea. Use JS click on the
// data-testid button, with a fallback to the radio input directly.
try {
const cardBtn = page.locator('[data-testid="card-accordion-item-button"]');
if ((await cardBtn.count()) > 0) {
await cardBtn.evaluate((el) => (el as HTMLElement).click());
console.log("[stripeCheckout] Card selected via accordion button");
await page.waitForTimeout(1000);
} else {
await page.evaluate(() => {
const radio = document.getElementById(
"payment-method-accordion-item-title-card",
);
if (radio) radio.click();
});
console.log("[stripeCheckout] Card selected via radio fallback");
await page.waitForTimeout(500);
}
} catch {
// Card might already be selected or not present
}
// Stripe's card inputs are custom components that require individual key events.
// .fill() sets the value programmatically and skips keydown/keypress/keyup,
// so Stripe never registers the input. Use .pressSequentially() instead.
const cardNumber = page.locator("#cardNumber");
await cardNumber.waitFor({ timeout: 120000 });
await cardNumber.pressSequentially("4242424242424242");
console.log("[stripeCheckout] Card number filled");
const cardExpiry = page.locator("#cardExpiry");
await cardExpiry.waitFor({ timeout: 60000 });
await cardExpiry.pressSequentially("1234");
console.log("[stripeCheckout] Expiry filled");
const cardCvc = page.locator("#cardCvc");
await cardCvc.waitFor({ timeout: 60000 });
await cardCvc.pressSequentially("123");
console.log("[stripeCheckout] CVC filled");
const billingName = page.locator("#billingName");
await billingName.waitFor({ timeout: 60000 });
await billingName.pressSequentially("Test Customer");
console.log("[stripeCheckout] Billing name filled");
try {
const emailField = page.locator("#email");
if ((await emailField.count()) > 0) {
await emailField.pressSequentially("test@example.com");
console.log("[stripeCheckout] Email filled");
}
} catch {
// Email field not present
}
// Force country to US so a postal code input is always shown
try {
const countrySelect = page.locator("#billingCountry");
if ((await countrySelect.count()) > 0) {
await countrySelect.selectOption("US");
console.log("[stripeCheckout] Country set to US");
await page.waitForTimeout(500);
}
} catch {
// Country selector not present
}
try {
const postalCode = page.locator("#billingPostalCode");
if ((await postalCode.count()) > 0) {
await postalCode.pressSequentially("10001");
console.log("[stripeCheckout] Postal code filled");
}
} catch {
// Postal code field not present
}
// Handle adjustable quantity if requested
if (overrideQuantity !== undefined && overrideQuantity !== null) {
const quantityBtn = page.locator(".AdjustableQuantitySelector").first();
if ((await quantityBtn.count()) === 0) {
throw new Error(
".AdjustableQuantitySelector not found - did you pass adjustable_quantity: true in attach params?",
);
}
await quantityBtn.click();
console.log("[stripeCheckout] Quantity selector clicked");
const quantityInput = page.locator("#adjustQuantity");
await quantityInput.waitFor({ timeout: 60000 });
await quantityInput.click({ clickCount: 3 });
await page.keyboard.press("Backspace");
await quantityInput.pressSequentially(overrideQuantity.toString());
const updateBtn = page.locator(".AdjustQuantityFooter-btn").first();
if ((await updateBtn.count()) === 0) {
throw new Error(".AdjustQuantityFooter-btn not found");
}
await updateBtn.click();
console.log(`[stripeCheckout] Quantity set to ${overrideQuantity}`);
await page.waitForTimeout(1000);
}
// Handle promo code if provided
if (promoCode) {
const promoInput = page.locator("#promotionCode");
await promoInput.waitFor({ timeout: 60000 });
await promoInput.click();
await promoInput.pressSequentially(promoCode);
await page.keyboard.press("Enter");
console.log(`[stripeCheckout] Promo code "${promoCode}" applied`);
await page.waitForTimeout(5000);
}
// Click submit button — use force:true because Stripe overlays (Link, phone number)
// can obscure the button and cause Playwright's actionability checks to time out.
const submitBtn = page.locator(".SubmitButton-TextContainer").first();
if ((await submitBtn.count()) === 0) {
throw new Error(".SubmitButton-TextContainer not found");
}
await submitBtn.evaluate((el) => (el as HTMLElement).click());
console.log("[stripeCheckout] Submit clicked");
// Wait for checkout to process + webhook delivery
await page.waitForTimeout(20000);
console.log("[stripeCheckout] Checkout complete");
};