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>
This commit is contained in:
381
frontend/tests/e2e/stripe-connect-onboarding.spec.ts
Normal file
381
frontend/tests/e2e/stripe-connect-onboarding.spec.ts
Normal file
@@ -0,0 +1,381 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user