Skip to content

Commit c6776c2

Browse files
test(e2e): drive the full Razorpay subscription checkout (Start→Cards→RBI→OTP) (#203)
* test(e2e): drive the full Razorpay subscription checkout (Start→Cards→RBI→OTP) The card-entry leg now drives the real TEST-mode subscription flow observed live: Start Subscription → select Cards (INR checkout defaults to UPI) → keystroke card entry → Continue → RBI 'Yes, secure my card' mandate modal → OTP 1234 → Continue. Previously it assumed a one-shot 'Pay' card form and soft-skipped on the subscription DOM. Still fully resilient (every step best-effort; markup change soft-fails, never reds the nightly). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(e2e): contract-only payment leg accepts the armed real-redirect (capturedShortUrl) When prod is ARMED (test keys live) a cohort checkout returns a real short_url and CheckoutPage window.location.assign's immediately; our route() aborts that nav, which can tear the React tree down before the checkout-redirecting testid stabilises → the @pr-smoke test timed out as 'stuck' (seen in the 2026-06-07 e2e-prod run). If the route captured a Razorpay URL, that IS the 'razorpay' success outcome — check capturedShortUrl before falling back to 'stuck'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b47d601 commit c6776c2

1 file changed

Lines changed: 47 additions & 12 deletions

File tree

e2e/live-ui-payment.spec.ts

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -290,8 +290,18 @@ async function driveUpgradeToCheckout(page: Page): Promise<UpgradeOutcome> {
290290
try {
291291
await expect(redirecting.or(fallback).or(errorPanel).or(emailGate)).toBeVisible({ timeout: 30_000 })
292292
} catch {
293+
// When the api is ARMED (test keys live), CheckoutPage gets a real short_url
294+
// and calls window.location.assign immediately — our route() aborts that
295+
// top-level nav, which can tear the React tree down before the
296+
// checkout-redirecting testid stabilises, so toBeVisible can time out even
297+
// though the redirect DID happen. If the route captured a Razorpay URL,
298+
// that's a successful 'razorpay' outcome, not 'stuck'.
299+
if (capturedShortUrl) return { kind: 'razorpay', detail: capturedShortUrl }
293300
return { kind: 'stuck', detail: 'no terminal checkout state within 30s (still loading?)' }
294301
}
302+
// Same race even on the success branch: the route may have fired before any
303+
// panel rendered. Prefer the captured URL the instant we have it.
304+
if (capturedShortUrl) return { kind: 'razorpay', detail: capturedShortUrl }
295305

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

369-
// Razorpay sometimes shows a "Card" payment-method tab first. Try to click it.
379+
// The flow, as observed driving a real TEST-mode INR subscription checkout
380+
// (2026-06-07): the short_url lands on a Razorpay "Subscription Details" page
381+
// with a "Start Subscription" button → that opens the checkout iframe listing
382+
// UPI / Cards / EMandate → pick Cards → fill card → "Continue" → an RBI
383+
// "Save your card as per RBI guidelines?" mandate modal (required for
384+
// recurring) → "Yes, secure my card" → a mock-bank OTP step → enter 1234 →
385+
// "Continue". Every step is best-effort (clickIfPresent never throws) so a
386+
// markup change still soft-fails rather than redding the nightly.
387+
388+
// 0) Subscription details page → "Start Subscription" opens the checkout iframe.
389+
await clickIfPresent(target, [
390+
'button:has-text("Start Subscription")',
391+
'button:has-text("Start subscription")',
392+
])
393+
// Give the checkout iframe a beat to mount before scanning frames.
394+
await target.waitForTimeout(2000).catch(() => {})
395+
396+
// 1) Select the "Cards" payment method (the INR checkout defaults to UPI).
370397
await clickIfPresent(target, [
371-
'text=/^card$/i',
372-
'text=/cards?/i',
398+
'[role="radio"][aria-label*="card" i]',
399+
'text=/^cards?$/i',
373400
'[data-testid="card"]',
374401
'button:has-text("Card")',
375402
])
376403

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

387-
// Submit (Pay). The button label varies (Pay, Pay Now, ₹…).
412+
// 3) Submit the card. For SUBSCRIPTIONS the button is "Continue" (one-time
413+
// payments say "Pay"); try both.
388414
const submitted = await clickIfPresent(target, [
415+
'button:has-text("Continue")',
389416
'button:has-text("Pay")',
390417
'button:has-text("Subscribe")',
391418
'button[type="submit"]',
392419
'[data-testid="submit"]',
393420
])
394421
if (!submitted) {
395422
// eslint-disable-next-line no-console
396-
console.warn('[live-ui-payment] could not find the Razorpay Pay/Submit button — markup changed?')
423+
console.warn('[live-ui-payment] could not find the Razorpay Continue/Pay button — markup changed?')
397424
return false
398425
}
399426

400-
// Mock-bank / 3DS OTP step. Enter OTP 1234 and click Success/Submit. Both the
401-
// OTP field and the success button are in the bank-simulator frame on TEST.
427+
// 4) RBI save-card mandate modal ("Save your card as per RBI guidelines?") —
428+
// required for recurring. Best-effort; absent on some flows.
429+
await target.waitForTimeout(1000).catch(() => {})
430+
await clickIfPresent(target, [
431+
'button:has-text("Yes, secure my card")',
432+
'button:has-text("secure my card")',
433+
])
434+
435+
// 5) Mock-bank / 3DS OTP step. Enter OTP 1234 and click Continue/Success.
402436
const otpDone = await enterOtpAcrossFrames(target, TEST_OTP)
403437
if (!otpDone) {
404438
// The OTP step may be skipped for some test flows; don't fail solely on it.
@@ -501,9 +535,10 @@ async function enterOtpAcrossFrames(page: Page, otp: string): Promise<boolean> {
501535
}
502536
}
503537
}
504-
// Click Success / Submit on the bank simulator (TEST mode renders a "Success"
505-
// button; OTP 1234 is also valid).
538+
// Submit the OTP. The TEST bank simulator's button is "Continue" (newer
539+
// checkout) or "Success"/"Submit" (older) — try all.
506540
await clickIfPresent(page, [
541+
'button:has-text("Continue")',
507542
'button:has-text("Success")',
508543
'button:has-text("Submit")',
509544
'button[type="submit"]',

0 commit comments

Comments
 (0)