Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 47 additions & 12 deletions e2e/live-ui-payment.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,8 +290,18 @@ async function driveUpgradeToCheckout(page: Page): Promise<UpgradeOutcome> {
try {
await expect(redirecting.or(fallback).or(errorPanel).or(emailGate)).toBeVisible({ timeout: 30_000 })
} catch {
// When the api is ARMED (test keys live), CheckoutPage gets a real short_url
// and calls window.location.assign immediately — our route() aborts that
// top-level nav, which can tear the React tree down before the
// checkout-redirecting testid stabilises, so toBeVisible can time out even
// though the redirect DID happen. If the route captured a Razorpay URL,
// that's a successful 'razorpay' outcome, not 'stuck'.
if (capturedShortUrl) return { kind: 'razorpay', detail: capturedShortUrl }
return { kind: 'stuck', detail: 'no terminal checkout state within 30s (still loading?)' }
}
// Same race even on the success branch: the route may have fired before any
// panel rendered. Prefer the captured URL the instant we have it.
if (capturedShortUrl) return { kind: 'razorpay', detail: capturedShortUrl }

if (await fallback.isVisible().catch(() => false)) {
return { kind: 'fallback', detail: 'billing-not-configured fallback panel' }
Expand Down Expand Up @@ -366,39 +376,63 @@ async function driveRazorpayTestCard(page: Page): Promise<boolean> {
await popup.waitForLoadState('domcontentloaded').catch(() => {})
}

// Razorpay sometimes shows a "Card" payment-method tab first. Try to click it.
// The flow, as observed driving a real TEST-mode INR subscription checkout
// (2026-06-07): the short_url lands on a Razorpay "Subscription Details" page
// with a "Start Subscription" button → that opens the checkout iframe listing
// UPI / Cards / EMandate → pick Cards → fill card → "Continue" → an RBI
// "Save your card as per RBI guidelines?" mandate modal (required for
// recurring) → "Yes, secure my card" → a mock-bank OTP step → enter 1234 →
// "Continue". Every step is best-effort (clickIfPresent never throws) so a
// markup change still soft-fails rather than redding the nightly.

// 0) Subscription details page → "Start Subscription" opens the checkout iframe.
await clickIfPresent(target, [
'button:has-text("Start Subscription")',
'button:has-text("Start subscription")',
])
// Give the checkout iframe a beat to mount before scanning frames.
await target.waitForTimeout(2000).catch(() => {})

// 1) Select the "Cards" payment method (the INR checkout defaults to UPI).
await clickIfPresent(target, [
'text=/^card$/i',
'text=/cards?/i',
'[role="radio"][aria-label*="card" i]',
'text=/^cards?$/i',
'[data-testid="card"]',
'button:has-text("Card")',
])

// Find the card-number field across the page + its frames. Razorpay nests the
// PCI card fields in iframes; we scan candidate frames for a recognisable
// number input.
// 2) Find + fill the card fields across the page + its (cross-origin) frames.
const cardFilled = await fillCardAcrossFrames(target)
if (!cardFilled) {
// eslint-disable-next-line no-console
console.warn('[live-ui-payment] could not locate the Razorpay card-number field in any frame — markup changed?')
return false
}

// Submit (Pay). The button label varies (Pay, Pay Now, ₹…).
// 3) Submit the card. For SUBSCRIPTIONS the button is "Continue" (one-time
// payments say "Pay"); try both.
const submitted = await clickIfPresent(target, [
'button:has-text("Continue")',
'button:has-text("Pay")',
'button:has-text("Subscribe")',
'button[type="submit"]',
'[data-testid="submit"]',
])
if (!submitted) {
// eslint-disable-next-line no-console
console.warn('[live-ui-payment] could not find the Razorpay Pay/Submit button — markup changed?')
console.warn('[live-ui-payment] could not find the Razorpay Continue/Pay button — markup changed?')
return false
}

// Mock-bank / 3DS OTP step. Enter OTP 1234 and click Success/Submit. Both the
// OTP field and the success button are in the bank-simulator frame on TEST.
// 4) RBI save-card mandate modal ("Save your card as per RBI guidelines?") —
// required for recurring. Best-effort; absent on some flows.
await target.waitForTimeout(1000).catch(() => {})
await clickIfPresent(target, [
'button:has-text("Yes, secure my card")',
'button:has-text("secure my card")',
])

// 5) Mock-bank / 3DS OTP step. Enter OTP 1234 and click Continue/Success.
const otpDone = await enterOtpAcrossFrames(target, TEST_OTP)
if (!otpDone) {
// The OTP step may be skipped for some test flows; don't fail solely on it.
Expand Down Expand Up @@ -501,9 +535,10 @@ async function enterOtpAcrossFrames(page: Page, otp: string): Promise<boolean> {
}
}
}
// Click Success / Submit on the bank simulator (TEST mode renders a "Success"
// button; OTP 1234 is also valid).
// Submit the OTP. The TEST bank simulator's button is "Continue" (newer
// checkout) or "Success"/"Submit" (older) — try all.
await clickIfPresent(page, [
'button:has-text("Continue")',
'button:has-text("Success")',
'button:has-text("Submit")',
'button[type="submit"]',
Expand Down
Loading