Files
smoothschedule/frontend/tests/e2e/stripe-connect-onboarding.spec.ts
poduck 2e111364a2 Initial commit: SmoothSchedule multi-tenant scheduling platform
This commit includes:
- Django backend with multi-tenancy (django-tenants)
- React + TypeScript frontend with Vite
- Platform administration API with role-based access control
- Authentication system with token-based auth
- Quick login dev tools for testing different user roles
- CORS and CSRF configuration for local development
- Docker development environment setup

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 01:43:20 -05:00

382 lines
14 KiB
TypeScript

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');
});
});