diff --git a/tests/ui/purchase/paddle-sandbox-full.test.ts b/tests/ui/purchase/paddle-sandbox-full.test.ts index 19bb928..2783e45 100644 --- a/tests/ui/purchase/paddle-sandbox-full.test.ts +++ b/tests/ui/purchase/paddle-sandbox-full.test.ts @@ -1,4 +1,5 @@ import type { Page } from '@playwright/test'; +import fs from 'node:fs/promises'; import { dismissConsentBanner, expectFixture as expect, test } from '../helpers/test-fixtures'; @@ -30,93 +31,130 @@ test.describe('Paddle sandbox full flow (staging)', () => { test.skip(!shouldRun, 'Set E2E_PADDLE_SANDBOX=1 to run live sandbox checkout on staging.'); test.skip(!tenantEmail || !tenantPassword, 'Set E2E_TENANT_EMAIL and E2E_TENANT_PASSWORD for sandbox flow.'); - test('register, pay via Paddle sandbox, and login to event admin', async ({ page, request }) => { + test('register, pay via Paddle sandbox, and login to event admin', async ({ page, request }, testInfo) => { + const paddleNetworkLog: string[] = []; + + page.on('response', async (response) => { + const url = response.url(); + if (!/paddle/i.test(url)) { + return; + } + + const status = response.status(); + let bodySnippet = ''; + if (status >= 400 || /checkout|error/i.test(url)) { + try { + const text = await response.text(); + bodySnippet = text.trim().slice(0, 2000); + } catch { + bodySnippet = ''; + } + } + + const entry = [`[${status}] ${url}`, bodySnippet].filter(Boolean).join('\n'); + paddleNetworkLog.push(entry); + if (paddleNetworkLog.length > 40) { + paddleNetworkLog.shift(); + } + }); + await page.addInitScript(() => { Object.defineProperty(navigator, 'webdriver', { get: () => undefined, }); }); - // Jump directly into wizard for Standard package (2) - await page.goto(`${baseUrl}/${locale}/${checkoutSlug}/2`); - - await dismissConsentBanner(page); - - await proceedToAccountStep(page); - - await completeRegistrationOrLogin(page, { - email: tenantEmail!, - password: tenantPassword!, - }); - - const termsCheckbox = page.locator('#checkout-terms-hero'); - await expect(termsCheckbox).toBeVisible(); - await termsCheckbox.click(); - - const checkoutCta = page.getByRole('button', { name: /Weiter mit Paddle|Continue with Paddle/i }).first(); - await expect(checkoutCta).toBeVisible({ timeout: 20000 }); - - const [apiResponse] = await Promise.all([ - page.waitForResponse((resp) => resp.url().includes('/paddle/create-checkout') && resp.status() < 500), - checkoutCta.click(), - ]); - - const rawBody = await apiResponse.text(); - let checkoutPayload: Record | null = null; try { - checkoutPayload = rawBody && rawBody.trim().startsWith('{') ? JSON.parse(rawBody) : null; - } catch { - checkoutPayload = null; + // Jump directly into wizard for Standard package (2) + await page.goto(`${baseUrl}/${locale}/${checkoutSlug}/2`); + + await dismissConsentBanner(page); + + await proceedToAccountStep(page); + + await completeRegistrationOrLogin(page, { + email: tenantEmail!, + password: tenantPassword!, + }); + + const termsCheckbox = page.locator('#checkout-terms-hero'); + await expect(termsCheckbox).toBeVisible(); + await termsCheckbox.click(); + + const checkoutCta = page.getByRole('button', { name: /Weiter mit Paddle|Continue with Paddle/i }).first(); + await expect(checkoutCta).toBeVisible({ timeout: 20000 }); + + const [apiResponse] = await Promise.all([ + page.waitForResponse((resp) => resp.url().includes('/paddle/create-checkout') && resp.status() < 500), + checkoutCta.click(), + ]); + + const rawBody = await apiResponse.text(); + let checkoutPayload: Record | null = null; + try { + checkoutPayload = rawBody && rawBody.trim().startsWith('{') ? JSON.parse(rawBody) : null; + } catch { + checkoutPayload = null; + } + + const inlineMode = checkoutPayload?.mode === 'inline' || checkoutPayload?.inline === true; + const checkoutUrl = extractCheckoutUrl(checkoutPayload, rawBody); + + if (!inlineMode) { + expect(checkoutUrl).toContain('paddle'); + } + + // Navigate to Paddle hosted checkout and complete payment. + if (inlineMode) { + await expect( + page.getByText(/Checkout geöffnet|Checkout opened|Paddle-Checkout/i).first() + ).toBeVisible({ timeout: 20_000 }); + await waitForPaddleCardInputs(page, ['input[autocomplete="cc-number"]', 'input[name="cardnumber"]', 'input[name="card_number"]']); + } else if (checkoutUrl) { + await page.goto(checkoutUrl); + await expect(page).toHaveURL(/paddle/); + } else { + throw new Error(`Missing Paddle checkout URL. Response: ${rawBody}`); + } + + await completeHostedPaddleCheckout(page, sandboxCard); + + await expect + .poll( + async () => { + const latestCompleted = await request.get('/api/_testing/checkout/sessions/latest', { + params: { status: 'completed', email: tenantEmail }, + }); + if (!latestCompleted.ok()) { + return null; + } + + const json = await latestCompleted.json(); + return json?.data?.status ?? null; + }, + { timeout: 120_000 } + ) + .toBe('completed'); + + await page.goto(`${baseUrl}/event-admin/login`); + await dismissConsentBanner(page); + + await page.locator('input[name="identifier"]').fill(tenantEmail!); + await page.locator('input[name="password"]').fill(tenantPassword!); + await page.getByRole('button', { name: /Anmelden|Login/i }).click(); + + await expect(page).toHaveURL(/\/event-admin\/mobile\/(dashboard|welcome)/i, { timeout: 30_000 }); + await expect(page.getByText(/Dashboard|Willkommen/i)).toBeVisible(); + } finally { + if (paddleNetworkLog.length > 0) { + const logPath = testInfo.outputPath('paddle-network-log.txt'); + await fs.writeFile(logPath, paddleNetworkLog.join('\n\n'), 'utf8'); + await testInfo.attach('paddle-network-log', { + path: logPath, + contentType: 'text/plain', + }); + } } - - const inlineMode = checkoutPayload?.mode === 'inline' || checkoutPayload?.inline === true; - const checkoutUrl = extractCheckoutUrl(checkoutPayload, rawBody); - - if (!inlineMode) { - expect(checkoutUrl).toContain('paddle'); - } - - // Navigate to Paddle hosted checkout and complete payment. - if (inlineMode) { - await expect( - page.getByText(/Checkout geöffnet|Checkout opened|Paddle-Checkout/i).first() - ).toBeVisible({ timeout: 20_000 }); - await waitForPaddleCardInputs(page, ['input[autocomplete="cc-number"]', 'input[name="cardnumber"]', 'input[name="card_number"]']); - } else if (checkoutUrl) { - await page.goto(checkoutUrl); - await expect(page).toHaveURL(/paddle/); - } else { - throw new Error(`Missing Paddle checkout URL. Response: ${rawBody}`); - } - - await completeHostedPaddleCheckout(page, sandboxCard); - - await expect - .poll( - async () => { - const latestCompleted = await request.get('/api/_testing/checkout/sessions/latest', { - params: { status: 'completed', email: tenantEmail }, - }); - if (!latestCompleted.ok()) { - return null; - } - - const json = await latestCompleted.json(); - return json?.data?.status ?? null; - }, - { timeout: 120_000 } - ) - .toBe('completed'); - - await page.goto(`${baseUrl}/event-admin/login`); - await dismissConsentBanner(page); - - await page.locator('input[name="identifier"]').fill(tenantEmail!); - await page.locator('input[name="password"]').fill(tenantPassword!); - await page.getByRole('button', { name: /Anmelden|Login/i }).click(); - - await expect(page).toHaveURL(/\/event-admin\/mobile\/(dashboard|welcome)/i, { timeout: 30_000 }); - await expect(page.getByText(/Dashboard|Willkommen/i)).toBeVisible(); }); });