import { test, expect } from '@playwright/test'; import { execSync } from 'child_process'; const LOGIN_EMAIL = 'checkout-e2e@example.com'; const LOGIN_PASSWORD = 'Password123!'; test.describe('Checkout Payment Step – Paddle flow', () => { test.beforeAll(async () => { execSync( `php artisan tenant:add-dummy --email=${LOGIN_EMAIL} --password=${LOGIN_PASSWORD} --first_name=Checkout --last_name=Tester --address="Playwrightstr. 1" --phone="+4912345678"` ); execSync( `php artisan tinker --execute="App\\\\Models\\\\User::where('email', '${LOGIN_EMAIL}')->update(['email_verified_at' => now()]);"` ); }); test.beforeEach(async ({ page }) => { await page.goto('/login'); await page.fill('input[name="email"]', LOGIN_EMAIL); await page.fill('input[name="password"]', LOGIN_PASSWORD); await page.getByRole('button', { name: /Anmelden|Login/ }).click(); await expect(page).toHaveURL(/dashboard/); }); test('opens Paddle checkout and shows success notice', async ({ page }) => { await page.route('**/paddle/create-checkout', async (route) => { const request = route.request(); const postData = request.postDataJSON() as { inline?: boolean } | null; const inline = Boolean(postData?.inline); if (inline) { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ mode: 'inline', items: [ { priceId: 'pri_123', quantity: 1 }, ], custom_data: { tenant_id: '1', package_id: '2', checkout_session_id: 'cs_123', }, customer: { email: LOGIN_EMAIL, }, }), }); return; } await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ checkout_url: 'https://paddle.test/checkout/success', }), }); }); await page.route('https://cdn.paddle.com/paddle/v2/paddle.js', async (route) => { await route.fulfill({ status: 200, contentType: 'application/javascript', body: ` window.Paddle = { Environment: { set: function(env) { window.__paddleEnv = env; } }, Initialize: function(opts) { window.__paddleInit = opts; }, Checkout: { open: function(config) { window.__paddleOpenConfig = config; } } }; `, }); }); await openCheckoutPaymentStep(page); await page.evaluate(() => { window.__openedUrls = []; window.open = (url: string, target?: string | null, features?: string | null) => { window.__openedUrls.push({ url, target: target ?? null, features: features ?? null }); return null; }; }); await page.getByRole('button', { name: /Continue with Paddle|Weiter mit Paddle/ }).click(); await expect( page.locator( 'text=/Paddle checkout is running in a secure overlay|Der Paddle-Checkout läuft jetzt in einem Overlay/' ) ).toBeVisible(); await expect.poll(async () => { return page.evaluate(() => window.__paddleOpenConfig?.items?.[0]?.priceId ?? null); }).toBe('pri_123'); await expect.poll(async () => { return page.evaluate(() => window.__openedUrls?.length ?? 0); }).toBe(0); }); test('shows error state when Paddle checkout creation fails', async ({ page }) => { await page.route('**/paddle/create-checkout', async (route) => { await route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ message: 'test-error' }), }); }); await openCheckoutPaymentStep(page); await page.getByRole('button', { name: /Continue with Paddle|Weiter mit Paddle/ }).click(); await expect( page.locator('text=/Paddle checkout could not be started|Paddle-Checkout konnte nicht gestartet werden/') ).toBeVisible(); }); }); async function openCheckoutPaymentStep(page: import('@playwright/test').Page) { await page.goto('/packages'); const checkoutLink = page.locator('a[href^="/checkout/"]').first(); const href = await checkoutLink.getAttribute('href'); if (!href) { throw new Error('No checkout link found on packages page.'); } await page.goto(href); const nextButton = page.getByRole('button', { name: /Weiter zum Zahlungsschritt|Continue to Payment/, }); if (await nextButton.isVisible()) { await nextButton.click(); } await page.waitForSelector('text=/Zahlung|Payment/'); } declare global { interface Window { __openedUrls?: Array<{ url: string; target?: string | null; features?: string | null }>; __paddleOpenConfig?: { url?: string; items?: Array<{ priceId: string; quantity: number }>; settings?: { displayMode?: string } }; __paddleEnv?: string; __paddleInit?: Record; } }