import { test, expect, Page } from '@playwright/test'; /** * Stripe Connect Onboarding E2E Tests * * Tests the complete Stripe Connect onboarding flow including: * - Payment settings page display * - Starting embedded onboarding * - Completing Stripe test account setup * - Status refresh after onboarding * - Active account display * * Note: These tests interact with Stripe's test mode sandbox. * Test credentials use Stripe's test data (000000000 for EIN, etc.) */ // Helper to login as business owner async function loginAsBusinessOwner(page: Page, subdomain: string = 'acme') { await page.goto(`http://${subdomain}.lvh.me:5173`); await page.waitForLoadState('networkidle'); // Check if login is needed const loginHeading = page.getByRole('heading', { name: /sign in/i }); if (await loginHeading.isVisible({ timeout: 3000 }).catch(() => false)) { await page.getByPlaceholder(/username/i).fill(`${subdomain}_owner`); await page.getByPlaceholder(/password/i).fill('testpass123'); await page.getByRole('button', { name: /sign in/i }).click(); await page.waitForLoadState('networkidle'); } } // Helper to navigate to payments settings async function navigateToPayments(page: Page) { // Look for payments/billing link in sidebar or navigation const paymentsLink = page.getByRole('link', { name: /payments|billing|plan/i }); if (await paymentsLink.isVisible({ timeout: 3000 }).catch(() => false)) { await paymentsLink.click(); await page.waitForLoadState('networkidle'); } else { // Try direct navigation await page.goto(`http://acme.lvh.me:5173/payments`); await page.waitForLoadState('networkidle'); } } test.describe('Stripe Connect Onboarding Flow', () => { test.describe.configure({ mode: 'serial' }); test.beforeEach(async ({ page }) => { // Clear storage before each test await page.context().clearCookies(); await page.evaluate(() => { localStorage.clear(); sessionStorage.clear(); }); }); test('should display payment settings page for business owner', async ({ page }) => { await loginAsBusinessOwner(page); await navigateToPayments(page); // Should show the Plan & Billing heading await expect(page.getByRole('heading', { name: /plan.*billing/i })).toBeVisible(); // Should show payment configuration section const paymentSection = page.getByText(/payment.*config|stripe.*connect|set up payments/i); await expect(paymentSection.first()).toBeVisible(); }); test('should show Connect setup for paid tier business', async ({ page }) => { await loginAsBusinessOwner(page); await navigateToPayments(page); // For paid tier, should show Stripe Connect setup option // Look for setup button or connected status const setupButton = page.getByRole('button', { name: /start payment setup|connect.*stripe|set up/i }); const connectedStatus = page.getByText(/stripe connected|payments.*enabled/i); const hasSetupButton = await setupButton.isVisible({ timeout: 5000 }).catch(() => false); const hasConnectedStatus = await connectedStatus.isVisible({ timeout: 5000 }).catch(() => false); // Should show either setup option or connected status expect(hasSetupButton || hasConnectedStatus).toBeTruthy(); }); test('should initialize embedded onboarding when clicking setup', async ({ page }) => { await loginAsBusinessOwner(page); await navigateToPayments(page); // Find and click setup button const setupButton = page.getByRole('button', { name: /start payment setup/i }); if (await setupButton.isVisible({ timeout: 5000 }).catch(() => false)) { await setupButton.click(); // Should show loading state const loadingIndicator = page.getByText(/initializing|loading/i); await expect(loadingIndicator).toBeVisible({ timeout: 10000 }); // Wait for Stripe component to load (may take time) // The Stripe iframe should appear await page.waitForTimeout(5000); // Allow time for Stripe to load // Look for Stripe onboarding content or container const stripeFrame = page.frameLocator('iframe[name*="stripe"]').first(); const onboardingContainer = page.getByText(/complete.*account.*setup|fill out.*information/i); const hasStripeFrame = await stripeFrame.locator('body').isVisible({ timeout: 10000 }).catch(() => false); const hasOnboardingText = await onboardingContainer.isVisible({ timeout: 5000 }).catch(() => false); expect(hasStripeFrame || hasOnboardingText).toBeTruthy(); } }); test('should display account details when Connect is already active', async ({ page }) => { await loginAsBusinessOwner(page); await navigateToPayments(page); // If account is already connected, should show status details const connectedBadge = page.getByText(/stripe connected/i); if (await connectedBadge.isVisible({ timeout: 5000 }).catch(() => false)) { // Should show account type await expect(page.getByText(/standard connect|express connect|custom connect/i)).toBeVisible(); // Should show charges enabled status await expect(page.getByText(/charges.*enabled|enabled/i).first()).toBeVisible(); // Should show payouts status await expect(page.getByText(/payouts/i)).toBeVisible(); } }); test('should handle onboarding errors gracefully', async ({ page }) => { await loginAsBusinessOwner(page); await navigateToPayments(page); // Mock a failed API response await page.route('**/api/payments/connect/account-session/**', (route) => { route.fulfill({ status: 500, body: JSON.stringify({ error: 'Failed to create session' }), }); }); const setupButton = page.getByRole('button', { name: /start payment setup/i }); if (await setupButton.isVisible({ timeout: 5000 }).catch(() => false)) { await setupButton.click(); // Should show error state await expect(page.getByText(/failed|error|setup failed/i)).toBeVisible({ timeout: 10000 }); // Should show try again button await expect(page.getByRole('button', { name: /try again/i })).toBeVisible(); } }); }); test.describe('Stripe Connect Onboarding - Full Flow', () => { // This test walks through the complete Stripe test account setup // Uses Stripe's test mode data test('should complete full onboarding with test data', async ({ page }) => { test.setTimeout(120000); // Allow 2 minutes for full Stripe flow await loginAsBusinessOwner(page); await navigateToPayments(page); const setupButton = page.getByRole('button', { name: /start payment setup/i }); if (!await setupButton.isVisible({ timeout: 5000 }).catch(() => false)) { // Already set up, skip this test console.log('Connect already configured, skipping full flow test'); return; } await setupButton.click(); // Wait for Stripe embedded component to load await page.waitForTimeout(5000); // Look for Stripe iframe const stripeFrames = page.frames().filter(f => f.url().includes('stripe')); if (stripeFrames.length > 0) { const stripeFrame = stripeFrames[0]; // Fill in test business information // Note: Stripe's embedded forms vary, these are common fields // Try to find and fill phone number const phoneInput = stripeFrame.locator('input[name*="phone"], input[placeholder*="phone"]'); if (await phoneInput.isVisible({ timeout: 3000 }).catch(() => false)) { await phoneInput.fill('5555555555'); } // Try to find and fill business details const einInput = stripeFrame.locator('input[name*="tax_id"], input[placeholder*="EIN"]'); if (await einInput.isVisible({ timeout: 3000 }).catch(() => false)) { await einInput.fill('000000000'); // Stripe test EIN } // Fill address if visible const addressInput = stripeFrame.locator('input[name*="address"], input[placeholder*="address"]'); if (await addressInput.isVisible({ timeout: 3000 }).catch(() => false)) { await addressInput.fill('123 Test Street'); } // Look for continue/submit button const continueBtn = stripeFrame.locator('button:has-text("Continue"), button:has-text("Submit")'); if (await continueBtn.isVisible({ timeout: 3000 }).catch(() => false)) { await continueBtn.click(); } // Wait and check for completion await page.waitForTimeout(5000); } // After onboarding, check if status updated const completionMessage = page.getByText(/onboarding complete|stripe connected|set up/i); const isComplete = await completionMessage.isVisible({ timeout: 10000 }).catch(() => false); // Log result for debugging console.log('Onboarding completion visible:', isComplete); }); test('should refresh status after onboarding exit', async ({ page }) => { await loginAsBusinessOwner(page); await navigateToPayments(page); // Mock successful refresh response await page.route('**/api/payments/connect/refresh-status/**', (route) => { route.fulfill({ status: 200, body: JSON.stringify({ id: 1, business: 1, stripe_account_id: 'acct_test123', account_type: 'custom', status: 'active', charges_enabled: true, payouts_enabled: true, details_submitted: true, onboarding_complete: true, }), }); }); // Trigger a status refresh const refreshButton = page.getByRole('button', { name: /refresh|check status/i }); if (await refreshButton.isVisible({ timeout: 5000 }).catch(() => false)) { await refreshButton.click(); // Should show updated status await expect(page.getByText(/active|connected/i)).toBeVisible({ timeout: 5000 }); } }); }); test.describe('Payment Configuration Status', () => { test('should show correct status for unconfigured business', async ({ page }) => { // Mock unconfigured payment status await page.route('**/api/payments/config/status/**', (route) => { route.fulfill({ status: 200, body: JSON.stringify({ payment_mode: 'none', tier: 'Professional', can_accept_payments: false, api_keys: null, connect_account: null, }), }); }); await loginAsBusinessOwner(page); await navigateToPayments(page); // Should show setup required message await expect(page.getByText(/set up payments|not configured/i)).toBeVisible(); }); test('should show correct status for configured Connect account', async ({ page }) => { // Mock configured payment status await page.route('**/api/payments/config/status/**', (route) => { route.fulfill({ status: 200, body: JSON.stringify({ payment_mode: 'connect', tier: 'Business', can_accept_payments: true, api_keys: null, connect_account: { id: 1, stripe_account_id: 'acct_test123', account_type: 'custom', status: 'active', charges_enabled: true, payouts_enabled: true, details_submitted: true, onboarding_complete: true, }, }), }); }); await loginAsBusinessOwner(page); await navigateToPayments(page); // Should show connected status await expect(page.getByText(/stripe connected|connected/i)).toBeVisible(); await expect(page.getByText(/charges.*enabled|enabled/i).first()).toBeVisible(); }); test('should show API keys option for free tier', async ({ page }) => { // Mock free tier status await page.route('**/api/payments/config/status/**', (route) => { route.fulfill({ status: 200, body: JSON.stringify({ payment_mode: 'none', tier: 'Free', can_accept_payments: false, api_keys: null, connect_account: null, }), }); }); await loginAsBusinessOwner(page); await navigateToPayments(page); // For free tier, should show API keys configuration option const apiKeysSection = page.getByText(/api keys|stripe keys/i); const hasApiKeysOption = await apiKeysSection.isVisible({ timeout: 5000 }).catch(() => false); // Free tier uses direct API keys instead of Connect expect(hasApiKeysOption).toBeTruthy(); }); }); test.describe('Onboarding Wizard Integration', () => { test('should show onboarding wizard for new paid tier business', async ({ page }) => { await loginAsBusinessOwner(page); // Check if onboarding wizard appears const welcomeHeading = page.getByRole('heading', { name: /welcome/i }); const isWizardVisible = await welcomeHeading.isVisible({ timeout: 5000 }).catch(() => false); if (isWizardVisible) { // Navigate through wizard to Stripe step await page.getByRole('button', { name: /get started/i }).click(); // Should show Stripe Connect step await expect(page.getByRole('heading', { name: /connect stripe|payment/i })).toBeVisible(); // Should have option to start Connect onboarding const connectButton = page.getByRole('button', { name: /connect|set up|start/i }); await expect(connectButton.first()).toBeVisible(); } }); test('should handle URL parameters for Stripe return', async ({ page }) => { // Test handling of connect=complete parameter await page.goto('http://acme.lvh.me:5173/?connect=complete'); await page.waitForLoadState('networkidle'); // Login if needed const loginHeading = page.getByRole('heading', { name: /sign in/i }); if (await loginHeading.isVisible({ timeout: 3000 }).catch(() => false)) { await page.getByPlaceholder(/username/i).fill('acme_owner'); await page.getByPlaceholder(/password/i).fill('testpass123'); await page.getByRole('button', { name: /sign in/i }).click(); await page.waitForLoadState('networkidle'); } // Page should handle the connect=complete parameter // and potentially show success message or refresh status console.log('Connect complete parameter handled'); }); });