Files
smoothschedule/frontend/tests/e2e/transaction-analytics.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

979 lines
31 KiB
TypeScript

import { test, expect, Page } from '@playwright/test';
/**
* Transaction Analytics and Refund E2E Tests
*
* Tests the complete transaction analytics and refund flow including:
* - Transaction list display
* - Transaction filtering
* - Transaction detail modal
* - Refund functionality
* - Export functionality
* - Payouts tab
*/
// 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 page
async function navigateToPayments(page: Page, subdomain: string = 'acme') {
const paymentsLink = page.getByRole('link', { name: /payments|billing/i });
if (await paymentsLink.isVisible({ timeout: 3000 }).catch(() => false)) {
await paymentsLink.click();
await page.waitForLoadState('networkidle');
} else {
await page.goto(`http://${subdomain}.lvh.me:5173/payments`);
await page.waitForLoadState('networkidle');
}
}
// Mock transaction data
const mockTransactions = {
count: 3,
total_pages: 1,
results: [
{
id: 1,
stripe_payment_intent_id: 'pi_test_succeeded_123',
stripe_charge_id: 'ch_test_succeeded_123',
transaction_type: 'payment',
status: 'succeeded',
amount: 5000,
amount_display: '$50.00',
application_fee_amount: 150,
fee_display: '$1.50',
net_amount: 4850,
currency: 'usd',
customer_name: 'Test Customer',
customer_email: 'test@example.com',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
},
{
id: 2,
stripe_payment_intent_id: 'pi_test_refunded_456',
stripe_charge_id: 'ch_test_refunded_456',
transaction_type: 'payment',
status: 'refunded',
amount: 10000,
amount_display: '$100.00',
application_fee_amount: 300,
fee_display: '$3.00',
net_amount: 9700,
currency: 'usd',
customer_name: 'Another Customer',
customer_email: 'another@example.com',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
},
{
id: 3,
stripe_payment_intent_id: 'pi_test_partial_789',
stripe_charge_id: 'ch_test_partial_789',
transaction_type: 'payment',
status: 'partially_refunded',
amount: 7500,
amount_display: '$75.00',
application_fee_amount: 225,
fee_display: '$2.25',
net_amount: 7275,
currency: 'usd',
customer_name: 'Partial Customer',
customer_email: 'partial@example.com',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
},
],
};
// Mock transaction detail with refund capability
const mockTransactionDetail = {
...mockTransactions.results[0],
can_refund: true,
refundable_amount: 5000,
total_refunded: 0,
refunds: [],
payment_method_info: {
type: 'card',
brand: 'Visa',
last4: '4242',
exp_month: 12,
exp_year: 2025,
funding: 'credit',
},
description: 'Payment for appointment',
stripe_data: {},
};
// Mock transaction summary
const mockSummary = {
total_transactions: 10,
total_volume: 50000,
total_volume_display: '$500.00',
total_fees: 1500,
total_fees_display: '$15.00',
net_revenue: 48500,
net_revenue_display: '$485.00',
successful_transactions: 8,
failed_transactions: 1,
refunded_transactions: 1,
average_transaction: 5000,
average_transaction_display: '$50.00',
};
// Mock balance
const mockBalance = {
available: [{ amount: 150000, currency: 'usd', amount_display: '$1,500.00' }],
pending: [{ amount: 25000, currency: 'usd', amount_display: '$250.00' }],
available_total: 150000,
pending_total: 25000,
};
// Mock payment config (can accept payments)
const mockPaymentConfig = {
payment_mode: 'connect',
tier: 'Professional',
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,
},
};
test.describe('Transaction Analytics - Overview Tab', () => {
test.beforeEach(async ({ page }) => {
// Clear storage
await page.context().clearCookies();
// Mock API responses
await page.route('**/api/payments/config/status/**', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockPaymentConfig),
});
});
await page.route('**/api/payments/transactions/**', (route) => {
if (route.request().url().includes('/summary')) {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockSummary),
});
} else {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockTransactions),
});
}
});
await page.route('**/api/payments/transactions/balance/**', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockBalance),
});
});
});
test('should display overview tab with summary cards', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Should show the Payments & Analytics heading
await expect(page.getByRole('heading', { name: /payments.*analytics/i })).toBeVisible();
// Should show overview tab by default
await expect(page.getByRole('button', { name: /overview/i })).toBeVisible();
// Wait for data to load
await page.waitForTimeout(1000);
// Should show summary cards
await expect(page.getByText(/total revenue/i)).toBeVisible();
await expect(page.getByText(/available balance/i)).toBeVisible();
await expect(page.getByText(/success rate/i)).toBeVisible();
});
test('should show recent transactions in overview', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Wait for transactions to load
await page.waitForTimeout(1000);
// Should show recent transactions section
await expect(page.getByText(/recent transactions/i)).toBeVisible();
// Should show View All link
await expect(page.getByText(/view all/i)).toBeVisible();
});
});
test.describe('Transaction List and Filtering', () => {
test.beforeEach(async ({ page }) => {
await page.context().clearCookies();
await page.route('**/api/payments/config/status/**', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockPaymentConfig),
});
});
await page.route('**/api/payments/transactions/**', (route) => {
const url = route.request().url();
if (url.includes('/summary')) {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockSummary),
});
} else if (url.includes('/balance')) {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockBalance),
});
} else if (url.includes('status=succeeded')) {
// Filtered by succeeded status
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
count: 1,
total_pages: 1,
results: [mockTransactions.results[0]],
}),
});
} else {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockTransactions),
});
}
});
});
test('should navigate to transactions tab and display list', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Click on Transactions tab
await page.getByRole('button', { name: /transactions/i }).click();
await page.waitForTimeout(500);
// Should show transaction table headers
await expect(page.getByText('Customer').first()).toBeVisible();
await expect(page.getByText('Amount').first()).toBeVisible();
await expect(page.getByText('Status').first()).toBeVisible();
});
test('should show status filters', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Click on Transactions tab
await page.getByRole('button', { name: /transactions/i }).click();
// Should show status filter dropdown
const statusFilter = page.locator('select').filter({ hasText: /all statuses/i });
await expect(statusFilter).toBeVisible();
// Should have filter options
await statusFilter.click();
await expect(page.getByRole('option', { name: /succeeded/i })).toBeVisible();
await expect(page.getByRole('option', { name: /pending/i })).toBeVisible();
await expect(page.getByRole('option', { name: /failed/i })).toBeVisible();
await expect(page.getByRole('option', { name: /refunded/i })).toBeVisible();
});
test('should show transaction type filters', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Click on Transactions tab
await page.getByRole('button', { name: /transactions/i }).click();
// Should show type filter dropdown
const typeFilter = page.locator('select').filter({ hasText: /all types/i });
await expect(typeFilter).toBeVisible();
});
test('should show date range inputs', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Click on Transactions tab
await page.getByRole('button', { name: /transactions/i }).click();
// Should show date inputs
const dateInputs = page.locator('input[type="date"]');
await expect(dateInputs.first()).toBeVisible();
});
test('should show refresh button', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Click on Transactions tab
await page.getByRole('button', { name: /transactions/i }).click();
// Should show refresh button
await expect(page.getByRole('button', { name: /refresh/i })).toBeVisible();
});
test('should display status badges correctly', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Click on Transactions tab
await page.getByRole('button', { name: /transactions/i }).click();
await page.waitForTimeout(500);
// Should show different status badges
await expect(page.getByText('Succeeded').first()).toBeVisible();
await expect(page.getByText('Refunded').first()).toBeVisible();
await expect(page.getByText('Partially Refunded').first()).toBeVisible();
});
});
test.describe('Transaction Detail Modal', () => {
test.beforeEach(async ({ page }) => {
await page.context().clearCookies();
await page.route('**/api/payments/config/status/**', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockPaymentConfig),
});
});
await page.route('**/api/payments/transactions/**', (route) => {
const url = route.request().url();
if (url.match(/\/transactions\/\d+\/?$/)) {
// Single transaction detail
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockTransactionDetail),
});
} else if (url.includes('/summary')) {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockSummary),
});
} else if (url.includes('/balance')) {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockBalance),
});
} else {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockTransactions),
});
}
});
});
test('should open transaction detail modal when clicking row', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Click on Transactions tab
await page.getByRole('button', { name: /transactions/i }).click();
await page.waitForTimeout(500);
// Click on a transaction row (click the View button)
await page.getByRole('button', { name: /view/i }).first().click();
await page.waitForTimeout(500);
// Modal should open
await expect(page.getByRole('heading', { name: /transaction details/i })).toBeVisible();
});
test('should display customer information in modal', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Navigate to transactions and open modal
await page.getByRole('button', { name: /transactions/i }).click();
await page.waitForTimeout(500);
await page.getByRole('button', { name: /view/i }).first().click();
await page.waitForTimeout(500);
// Should show customer section
await expect(page.getByText(/customer/i)).toBeVisible();
});
test('should display amount breakdown in modal', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Navigate to transactions and open modal
await page.getByRole('button', { name: /transactions/i }).click();
await page.waitForTimeout(500);
await page.getByRole('button', { name: /view/i }).first().click();
await page.waitForTimeout(500);
// Should show amount breakdown
await expect(page.getByText(/gross amount/i)).toBeVisible();
await expect(page.getByText(/platform fee/i)).toBeVisible();
});
test('should show Issue Refund button for refundable transactions', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Navigate to transactions and open modal
await page.getByRole('button', { name: /transactions/i }).click();
await page.waitForTimeout(500);
await page.getByRole('button', { name: /view/i }).first().click();
await page.waitForTimeout(500);
// Should show Issue Refund button
await expect(page.getByRole('button', { name: /issue refund/i })).toBeVisible();
});
test('should close modal when clicking X button', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Navigate to transactions and open modal
await page.getByRole('button', { name: /transactions/i }).click();
await page.waitForTimeout(500);
await page.getByRole('button', { name: /view/i }).first().click();
await page.waitForTimeout(500);
// Close modal
const closeButton = page.locator('[role="dialog"]').getByRole('button').first();
if (await closeButton.isVisible()) {
await closeButton.click();
} else {
// Alternative: click X button by finding it
await page.keyboard.press('Escape');
}
await page.waitForTimeout(300);
// Modal should be closed
await expect(page.getByRole('heading', { name: /transaction details/i })).not.toBeVisible();
});
});
test.describe('Refund Functionality', () => {
test.beforeEach(async ({ page }) => {
await page.context().clearCookies();
await page.route('**/api/payments/config/status/**', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockPaymentConfig),
});
});
await page.route('**/api/payments/transactions/**', (route) => {
const url = route.request().url();
if (url.match(/\/transactions\/\d+\/refund\/?$/)) {
// Refund endpoint
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
success: true,
refund_id: 're_test123',
amount: 5000,
amount_display: '$50.00',
status: 'succeeded',
reason: 'requested_by_customer',
transaction_status: 'refunded',
}),
});
} else if (url.match(/\/transactions\/\d+\/?$/)) {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockTransactionDetail),
});
} else if (url.includes('/summary')) {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockSummary),
});
} else if (url.includes('/balance')) {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockBalance),
});
} else {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockTransactions),
});
}
});
});
test('should show refund form when clicking Issue Refund', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Navigate to transactions and open modal
await page.getByRole('button', { name: /transactions/i }).click();
await page.waitForTimeout(500);
await page.getByRole('button', { name: /view/i }).first().click();
await page.waitForTimeout(500);
// Click Issue Refund
await page.getByRole('button', { name: /issue refund/i }).click();
// Should show refund form
await expect(page.getByText(/full refund/i)).toBeVisible();
await expect(page.getByText(/partial refund/i)).toBeVisible();
await expect(page.getByText(/refund reason/i)).toBeVisible();
});
test('should show refund reason options', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Navigate to transactions and open modal
await page.getByRole('button', { name: /transactions/i }).click();
await page.waitForTimeout(500);
await page.getByRole('button', { name: /view/i }).first().click();
await page.waitForTimeout(500);
// Click Issue Refund
await page.getByRole('button', { name: /issue refund/i }).click();
// Should show reason dropdown
const reasonSelect = page.locator('select').filter({ hasText: /requested by customer/i });
await expect(reasonSelect).toBeVisible();
});
test('should show partial refund amount input when partial selected', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Navigate to transactions and open modal
await page.getByRole('button', { name: /transactions/i }).click();
await page.waitForTimeout(500);
await page.getByRole('button', { name: /view/i }).first().click();
await page.waitForTimeout(500);
// Click Issue Refund
await page.getByRole('button', { name: /issue refund/i }).click();
// Select partial refund
await page.getByLabel(/partial refund/i).check();
// Should show amount input
await expect(page.locator('input[type="number"]')).toBeVisible();
});
test('should have Confirm Refund and Cancel buttons', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Navigate to transactions and open modal
await page.getByRole('button', { name: /transactions/i }).click();
await page.waitForTimeout(500);
await page.getByRole('button', { name: /view/i }).first().click();
await page.waitForTimeout(500);
// Click Issue Refund
await page.getByRole('button', { name: /issue refund/i }).click();
// Should show confirm and cancel buttons
await expect(page.getByRole('button', { name: /confirm refund/i })).toBeVisible();
await expect(page.getByRole('button', { name: /cancel/i })).toBeVisible();
});
test('should hide refund form when clicking Cancel', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Navigate to transactions and open modal
await page.getByRole('button', { name: /transactions/i }).click();
await page.waitForTimeout(500);
await page.getByRole('button', { name: /view/i }).first().click();
await page.waitForTimeout(500);
// Click Issue Refund
await page.getByRole('button', { name: /issue refund/i }).click();
await expect(page.getByText(/full refund/i)).toBeVisible();
// Click Cancel
await page.getByRole('button', { name: /cancel/i }).click();
// Refund form should be hidden
await expect(page.getByText(/full refund/i)).not.toBeVisible();
});
});
test.describe('Export Functionality', () => {
test.beforeEach(async ({ page }) => {
await page.context().clearCookies();
await page.route('**/api/payments/config/status/**', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockPaymentConfig),
});
});
await page.route('**/api/payments/transactions/**', (route) => {
const url = route.request().url();
if (url.includes('/export')) {
// Return a blob for export
route.fulfill({
status: 200,
contentType: 'text/csv',
body: 'id,amount,status\n1,5000,succeeded',
});
} else if (url.includes('/summary')) {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockSummary),
});
} else if (url.includes('/balance')) {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockBalance),
});
} else {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockTransactions),
});
}
});
});
test('should show Export Data button', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Should show Export Data button
await expect(page.getByRole('button', { name: /export data/i })).toBeVisible();
});
test('should open export modal when clicking Export Data', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Click Export Data button
await page.getByRole('button', { name: /export data/i }).click();
// Should show export modal
await expect(page.getByRole('heading', { name: /export transactions/i })).toBeVisible();
});
test('should show export format options', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Click Export Data button
await page.getByRole('button', { name: /export data/i }).click();
// Should show format options
await expect(page.getByText('CSV')).toBeVisible();
await expect(page.getByText('Excel')).toBeVisible();
await expect(page.getByText('PDF')).toBeVisible();
await expect(page.getByText('QuickBooks')).toBeVisible();
});
test('should have date range inputs in export modal', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Click Export Data button
await page.getByRole('button', { name: /export data/i }).click();
// Should show date range inputs
await expect(page.getByText(/date range/i)).toBeVisible();
const dateInputs = page.locator('[role="dialog"]').locator('input[type="date"]');
await expect(dateInputs.first()).toBeVisible();
});
test('should close export modal when clicking X', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Click Export Data button
await page.getByRole('button', { name: /export data/i }).click();
await expect(page.getByRole('heading', { name: /export transactions/i })).toBeVisible();
// Close modal
await page.keyboard.press('Escape');
await page.waitForTimeout(300);
// Modal should be closed
await expect(page.getByRole('heading', { name: /export transactions/i })).not.toBeVisible();
});
});
test.describe('Payouts Tab', () => {
test.beforeEach(async ({ page }) => {
await page.context().clearCookies();
await page.route('**/api/payments/config/status/**', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockPaymentConfig),
});
});
await page.route('**/api/payments/transactions/**', (route) => {
const url = route.request().url();
if (url.includes('/payouts')) {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
payouts: [
{
id: 'po_test123',
amount: 100000,
amount_display: '$1,000.00',
currency: 'usd',
status: 'paid',
arrival_date: Date.now() / 1000,
method: 'standard',
},
{
id: 'po_test456',
amount: 50000,
amount_display: '$500.00',
currency: 'usd',
status: 'pending',
arrival_date: Date.now() / 1000 + 86400,
method: 'standard',
},
],
}),
});
} else if (url.includes('/balance')) {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockBalance),
});
} else if (url.includes('/summary')) {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockSummary),
});
} else {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockTransactions),
});
}
});
});
test('should navigate to payouts tab', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Click on Payouts tab
await page.getByRole('button', { name: /payouts/i }).click();
// Should show balance summary
await expect(page.getByText(/available for payout/i)).toBeVisible();
await expect(page.getByText(/pending/i)).toBeVisible();
});
test('should display payout history', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Click on Payouts tab
await page.getByRole('button', { name: /payouts/i }).click();
// Should show payout history section
await expect(page.getByText(/payout history/i)).toBeVisible();
});
test('should show payout table headers', async ({ page }) => {
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Click on Payouts tab
await page.getByRole('button', { name: /payouts/i }).click();
await page.waitForTimeout(500);
// Should show table headers
await expect(page.getByText('Payout ID').first()).toBeVisible();
await expect(page.getByText('Arrival Date').first()).toBeVisible();
});
});
test.describe('Payment Setup Required State', () => {
test('should show setup required message when payments not configured', async ({ page }) => {
await page.context().clearCookies();
// Mock unconfigured payment status
await page.route('**/api/payments/config/status/**', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
payment_mode: 'none',
tier: 'Professional',
can_accept_payments: false,
api_keys: null,
connect_account: null,
}),
});
});
await page.route('**/api/payments/transactions/**', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ count: 0, total_pages: 0, results: [] }),
});
});
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Should show setup required message
await expect(page.getByText(/payment setup required/i)).toBeVisible();
await expect(page.getByRole('button', { name: /go to settings/i })).toBeVisible();
});
test('should navigate to settings when clicking Go to Settings', async ({ page }) => {
await page.context().clearCookies();
await page.route('**/api/payments/config/status/**', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
payment_mode: 'none',
tier: 'Professional',
can_accept_payments: false,
api_keys: null,
connect_account: null,
}),
});
});
await page.route('**/api/payments/transactions/**', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ count: 0, total_pages: 0, results: [] }),
});
});
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Click Go to Settings
await page.getByRole('button', { name: /go to settings/i }).click();
// Settings tab should be active
const settingsTab = page.getByRole('button', { name: /settings/i });
await expect(settingsTab).toHaveClass(/border-brand-500|text-brand/);
});
});
test.describe('Error Handling', () => {
test('should handle transaction load error gracefully', async ({ page }) => {
await page.context().clearCookies();
await page.route('**/api/payments/config/status/**', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockPaymentConfig),
});
});
// Mock transaction API error
await page.route('**/api/payments/transactions/**', (route) => {
if (route.request().url().includes('/summary')) {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockSummary),
});
} else if (route.request().url().includes('/balance')) {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockBalance),
});
} else if (route.request().url().match(/\/transactions\/\d+\/?$/)) {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal server error' }),
});
} else {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(mockTransactions),
});
}
});
await loginAsBusinessOwner(page);
await navigateToPayments(page);
// Navigate to transactions and try to open modal
await page.getByRole('button', { name: /transactions/i }).click();
await page.waitForTimeout(500);
await page.getByRole('button', { name: /view/i }).first().click();
await page.waitForTimeout(500);
// Should show error message in modal
await expect(page.getByText(/failed to load/i)).toBeVisible();
});
});