From c839b31df9994b94e11d8c66bc4b7f61f98a188d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 7 Jun 2026 11:53:07 +0530 Subject: [PATCH 1/2] =?UTF-8?q?test(e2e):=20drive=20the=20full=20Razorpay?= =?UTF-8?q?=20subscription=20checkout=20(Start=E2=86=92Cards=E2=86=92RBI?= =?UTF-8?q?=E2=86=92OTP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- e2e/live-ui-payment.spec.ts | 49 ++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/e2e/live-ui-payment.spec.ts b/e2e/live-ui-payment.spec.ts index 97b7d60..87c23f2 100644 --- a/e2e/live-ui-payment.spec.ts +++ b/e2e/live-ui-payment.spec.ts @@ -366,17 +366,32 @@ async function driveRazorpayTestCard(page: Page): Promise { 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, [ - 'text=/^card$/i', - 'text=/cards?/i', + '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, [ + '[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 @@ -384,8 +399,10 @@ async function driveRazorpayTestCard(page: Page): Promise { 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"]', @@ -393,12 +410,19 @@ async function driveRazorpayTestCard(page: Page): Promise { ]) 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. @@ -501,9 +525,10 @@ async function enterOtpAcrossFrames(page: Page, otp: string): Promise { } } } - // 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"]', From 42ec8c58dbc83104e34e3f5fbff8b5838e572973 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 7 Jun 2026 12:09:57 +0530 Subject: [PATCH 2/2] test(e2e): contract-only payment leg accepts the armed real-redirect (capturedShortUrl) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- e2e/live-ui-payment.spec.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/e2e/live-ui-payment.spec.ts b/e2e/live-ui-payment.spec.ts index 87c23f2..92ebbee 100644 --- a/e2e/live-ui-payment.spec.ts +++ b/e2e/live-ui-payment.spec.ts @@ -290,8 +290,18 @@ async function driveUpgradeToCheckout(page: Page): Promise { 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' }