159 lines
5.6 KiB
TypeScript
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");
|
|
};
|