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