import { test, expect } from '@playwright/test'; test.describe('Expense Tracking Tests', () => { test.beforeEach(async ({ page }) => { // Clear cookies before each test await page.context().clearCookies(); }); test('Create and manage expenses', async ({ page }) => { // Create a trip first (use create=true to prevent redirect) await page.goto('http://localhost:3002?create=true'); await page.fill('input#trip-name', 'Expense Test Trip'); await page.fill('input#creator-name', 'Alice'); await page.selectOption('select#currency', 'USD'); await page.click('button:has-text("Create Trip")'); await page.click('button:has-text("Go to Trip Dashboard")'); // Add a participant for expense splitting page.on('dialog', async dialog => { await dialog.accept('Bob'); }); await page.click('button:has-text("Add Person")'); await page.waitForTimeout(2000); // Wait longer for participant to be added // Verify participants are available before opening expense modal await expect(page.locator('text=Bob')).toBeVisible(); // Click "Add Expense" button (use the blue button in the footer) await page.click('button.text-blue-600:has-text("Add Expense")'); // Wait for expense modal to appear await expect(page.locator('h2:has-text("Add Expense")')).toBeVisible(); await expect(page.locator('label:has-text("Description")')).toBeVisible(); await expect(page.locator('label:has-text("Amount")')).toBeVisible(); await expect(page.locator('label:has-text("Paid By")')).toBeVisible(); // Fill out expense form await page.fill('input#description', 'Dinner at Restaurant'); await page.fill('input#amount', '100.50'); await page.selectOption('select#category', 'Food & Dining'); // Select Alice from the paid-by dropdown await page.selectOption('select#paid-by', 'Alice'); // Check that splits are initialized equally (50/50) await expect(page.locator('text=Alice')).toBeVisible(); await expect(page.locator('text=Bob')).toBeVisible(); // Verify split percentages const alicePercentage = await page.locator('input[placeholder*="Alice"]').inputValue(); const bobPercentage = await page.locator('input[placeholder*="Bob"]').inputValue(); expect(alicePercentage).toBe('50'); expect(bobPercentage).toBe('50'); // Check total percentage await expect(page.locator('text=Total: 100.00%')).toBeVisible(); // Submit the expense await page.click('button:has-text("Add Expense")'); // Wait for modal to close and expense to appear await page.waitForTimeout(1000); await expect(page.locator('text=Add Expense')).not.toBeVisible(); // Check that expense appears in the list await expect(page.locator('text=Dinner at Restaurant')).toBeVisible(); await expect(page.locator('text=$100.50')).toBeVisible(); await expect(page.locator('text=Food & Dining')).toBeVisible(); await expect(page.locator('text=Paid by Alice')).toBeVisible(); // Check expense splits await expect(page.locator('text=Alice')).toBeVisible(); await expect(page.locator('text=$50.25')).toBeVisible(); await expect(page.locator('text=50%')).toBeVisible(); }); test('Expense with different currencies', async ({ page }) => { // Create a trip with EUR await page.goto('http://localhost:3002'); await page.fill('input#trip-name', 'Currency Test Trip'); await page.fill('input#creator-name', 'Alice'); await page.selectOption('select#currency', 'EUR'); await page.click('button:has-text("Create Trip")'); await page.click('button:has-text("Go to Trip Dashboard")'); // Add expense in USD await page.click('button:has-text("Add Expense")'); await page.fill('input#description', 'Hotel Booking'); await page.fill('input#amount', '120.00'); await page.selectOption('select#currency', 'USD'); // Different from trip currency await page.selectOption('select#category', 'Accommodation'); await page.selectOption('select#paid-by', 'Alice'); // Submit expense await page.click('button:has-text("Add Expense")'); await page.waitForTimeout(1000); // Check that both currencies are shown await expect(page.locator('text=$120.00')).toBeVisible(); // Original amount await expect(page.locator('text=€')).toBeVisible(); // Converted amount should appear }); test('Expense splitting validation', async ({ page }) => { // Create a trip await page.goto('http://localhost:3002'); await page.fill('input#trip-name', 'Validation Test Trip'); await page.fill('input#creator-name', 'Alice'); await page.click('button:has-text("Create Trip")'); await page.click('button:has-text("Go to Trip Dashboard")'); // Add participant page.on('dialog', async dialog => { await dialog.accept('Bob'); }); await page.click('button:has-text("Add Person")'); await page.waitForTimeout(1000); // Try to create expense with invalid splits await page.click('button:has-text("Add Expense")'); await page.fill('input#description', 'Invalid Expense'); await page.fill('input#amount', '100.00'); await page.selectOption('select#paid-by', 'Alice'); // Change split to invalid total (less than 100%) const aliceSplitInput = page.locator('input').filter({ hasText: '50' }).first(); await aliceSplitInput.fill('80'); // Alice 80%, Bob 20% = 100% - this should work // Now make it invalid by changing Alice to 70% await aliceSplitInput.fill('70'); // Alice 70%, Bob 20% = 90% - this should show error // Check validation error await expect(page.locator('text=Splits must total 100%')).toBeVisible(); // Try to submit anyway (should fail) await page.click('button:has-text("Add Expense")'); await expect(page.locator('text=Add Expense')).toBeVisible(); // Modal should stay open // Fix the splits await page.click('text=Even Split'); // Should reset to 50/50 await expect(page.locator('text=Total: 100.00%')).toBeVisible(); // Now submit should work await page.click('button:has-text("Add Expense")'); await page.waitForTimeout(1000); await expect(page.locator('text=Add Expense')).not.toBeVisible(); }); test('Expense categories and icons', async ({ page }) => { // Create a trip await page.goto('http://localhost:3002'); await page.fill('input#trip-name', 'Category Test Trip'); await page.fill('input#creator-name', 'Alice'); await page.click('button:has-text("Create Trip")'); await page.click('button:has-text("Go to Trip Dashboard")'); // Test different categories const categories = [ { name: 'Food & Dining', icon: '🍔' }, { name: 'Transportation', icon: '🚗' }, { name: 'Accommodation', icon: '🏨' }, { name: 'Entertainment', icon: '🎬' }, { name: 'Shopping', icon: '🛍️' }, { name: 'Healthcare', icon: '🏥' }, { name: 'Education', icon: '📚' }, { name: 'Business', icon: '💼' } ]; for (const category of categories) { await page.click('button:has-text("Add Expense")'); await page.fill('input#description', `Test ${category.name}`); await page.fill('input#amount', '50.00'); await page.selectOption('select#category', category.name); await page.selectOption('select#paid-by', 'Alice'); await page.click('button:has-text("Add Expense")'); await page.waitForTimeout(500); // Check that category appears with icon await expect(page.locator(`text=${category.icon}`)).toBeVisible(); await expect(page.locator(`text=Test ${category.name}`)).toBeVisible(); } }); test('Multiple expense management', async ({ page }) => { // Create a trip with multiple participants await page.goto('http://localhost:3002'); await page.fill('input#trip-name', 'Multi Expense Test'); await page.fill('input#creator-name', 'Alice'); await page.click('button:has-text("Create Trip")'); await page.click('button:has-text("Go to Trip Dashboard")'); // Add two participants const participants = ['Bob', 'Charlie']; for (const name of participants) { page.on('dialog', async dialog => { await dialog.accept(name); }); await page.click('button:has-text("Add Person")'); await page.waitForTimeout(500); } // Create multiple expenses const expenses = [ { description: 'Restaurant', amount: '150.00', paidBy: 'Alice', splitType: 'equal' }, { description: 'Taxi', amount: '40.00', paidBy: 'Bob', splitType: 'alice-only' }, { description: 'Hotel', amount: '300.00', paidBy: 'Charlie', splitType: 'equal' } ]; for (const expense of expenses) { await page.click('button:has-text("Add Expense")'); await page.fill('input#description', expense.description); await page.fill('input#amount', expense.amount); await page.selectOption('select#paid-by', expense.paidBy); if (expense.splitType === 'alice-only') { // Set Alice to 100%, others to 0% await page.locator('input').filter({ hasText: '33.33' }).first().fill('100'); } // Equal split is default await page.click('button:has-text("Add Expense")'); await page.waitForTimeout(500); } // Check total expenses summary await expect(page.locator('text=Total Expenses')).toBeVisible(); const totalAmount = await page.locator('text=$').first().textContent(); expect(totalAmount).toContain('$490.00'); // 150 + 40 + 300 // Check individual expenses are listed await expect(page.locator('text=Restaurant')).toBeVisible(); await expect(page.locator('text=Taxi')).toBeVisible(); await expect(page.locator('text=Hotel')).toBeVisible(); }); test('Analytics dashboard functionality', async ({ page }) => { // Create a trip with expenses await page.goto('http://localhost:3002'); await page.fill('input#trip-name', 'Analytics Test Trip'); await page.fill('input#creator-name', 'Alice'); await page.click('button:has-text("Create Trip")'); await page.click('button:has-text("Go to Trip Dashboard")'); // Add participant page.on('dialog', async dialog => { await dialog.accept('Bob'); }); await page.click('button:has-text("Add Person")'); await page.waitForTimeout(1000); // Create some expenses await page.click('button:has-text("Add Expense")'); await page.fill('input#description', 'Dinner'); await page.fill('input#amount', '100.00'); await page.selectOption('select#category', 'Food & Dining'); await page.selectOption('select#paid-by', 'Alice'); await page.click('button:has-text("Add Expense")'); await page.waitForTimeout(500); await page.click('button:has-text("Add Expense")'); await page.fill('input#description', 'Taxi'); await page.fill('input#amount', '50.00'); await page.selectOption('select#category', 'Transportation'); await page.selectOption('select#paid-by', 'Bob'); await page.click('button:has-text("Add Expense")'); await page.waitForTimeout(500); // Switch to Analytics tab await page.click('text=Analytics'); // Check analytics sections await expect(page.locator('text=Spending Overview')).toBeVisible(); await expect(page.locator('text=Participant Balances')).toBeVisible(); await expect(page.locator('text=Category Breakdown')).toBeVisible(); // Check spending overview await expect(page.locator('text=$150.00')).toBeVisible(); // Total expenses await expect(page.locator('text=2 expenses')).toBeVisible(); // Check participant balances await expect(page.locator('text=Alice')).toBeVisible(); await expect(page.locator('text=Bob')).toBeVisible(); // Check category breakdown await expect(page.locator('text=Food & Dining')).toBeVisible(); await expect(page.locator('text=Transportation')).toBeVisible(); }); test('Export functionality', async ({ page }) => { // Create a trip with expenses await page.goto('http://localhost:3002'); await page.fill('input#trip-name', 'Export Test Trip'); await page.fill('input#creator-name', 'Alice'); await page.click('button:has-text("Create Trip")'); await page.click('button:has-text("Go to Trip Dashboard")'); // Create an expense await page.click('button:has-text("Add Expense")'); await page.fill('input#description', 'Test Export Expense'); await page.fill('input#amount', '75.00'); await page.selectOption('select#category', 'Shopping'); await page.selectOption('select#paid-by', 'Alice'); await page.click('button:has-text("Add Expense")'); await page.waitForTimeout(1000); // Test share button await expect(page.locator('button:has-text("Copy Link")')).toBeVisible(); // Click share button and check for clipboard functionality await page.click('button:has-text("Copy Link")'); await expect(page.locator('text=Link copied to clipboard!')).toBeVisible(); // Test export button await expect(page.locator('button:has-text("Export")')).toBeVisible(); // Click export to open dropdown await page.click('button:has-text("Export")'); // Check export options await expect(page.locator('text=Export Expenses (CSV)')).toBeVisible(); await expect(page.locator('text=Export Participants (CSV)')).toBeVisible(); await expect(page.locator('text=Export All Data (CSV)')).toBeVisible(); await expect(page.locator('text=Export Summary (JSON)')).toBeVisible(); // Test expenses export (download will be handled by browser) const downloadPromise = page.waitForEvent('download'); await page.click('text=Export Expenses (CSV)'); const download = await downloadPromise; expect(download.suggestedFilename()).toContain('expenses'); expect(download.suggestedFilename()).toContain('.csv'); }); test('Expense form validation', async ({ page }) => { // Create a trip await page.goto('http://localhost:3002'); await page.fill('input#trip-name', 'Validation Test'); await page.fill('input#creator-name', 'Alice'); await page.click('button:has-text("Create Trip")'); await page.click('button:has-text("Go to Trip Dashboard")'); // Test empty description await page.click('button:has-text("Add Expense")'); await page.click('button:has-text("Add Expense")'); await expect(page.locator('text=Please enter a description')).toBeVisible(); // Fill description await page.fill('input#description', 'Test'); // Test invalid amount await page.fill('input#amount', '-50'); await page.click('button:has-text("Add Expense")'); await expect(page.locator('text=Please enter a valid amount')).toBeVisible(); // Test zero amount await page.fill('input#amount', '0'); await page.click('button:has-text("Add Expense")'); await expect(page.locator('text=Please enter a valid amount')).toBeVisible(); // Test missing paid by selection await page.fill('input#amount', '50'); await page.click('button:has-text("Add Expense")'); await expect(page.locator('text=Please select who paid')).toBeVisible(); // Fix all validations await page.selectOption('select#paid-by', 'Alice'); await page.click('button:has-text("Add Expense")'); await page.waitForTimeout(1000); await expect(page.locator('text=Add Expense')).not.toBeVisible(); }); test('Even split functionality', async ({ page }) => { // Create trip with 3 participants await page.goto('http://localhost:3002'); await page.fill('input#trip-name', 'Split Test'); await page.fill('input#creator-name', 'Alice'); await page.click('button:has-text("Create Trip")'); await page.click('button:has-text("Go to Trip Dashboard")'); // Add 2 more participants for (let i = 0; i < 2; i++) { page.on('dialog', async dialog => { await dialog.accept(`Participant ${i + 2}`); }); await page.click('button:has-text("Add Person")'); await page.waitForTimeout(500); } // Create expense await page.click('button:has-text("Add Expense")'); await page.fill('input#description', 'Group Dinner'); await page.fill('input#amount', '120.00'); await page.selectOption('select#paid-by', 'Alice'); // Check even split (should be 33.33% each for 3 people) await expect(page.locator('text=Total: 100.00%')).toBeVisible(); // Modify one split const firstSplitInput = page.locator('input').filter({ hasText: '33.33' }).first(); await firstSplitInput.fill('50'); // Check that total is no longer 100% await expect(page.locator('text=Total: 116.66%')).toBeVisible(); // Click Even Split to reset await page.click('text=Even Split'); // Check that splits are reset to equal await expect(page.locator('text=Total: 100.00%')).toBeVisible(); // Submit expense await page.click('button:has-text("Add Expense")'); await page.waitForTimeout(1000); await expect(page.locator('text=Add Expense')).not.toBeVisible(); // Check expense was created with correct splits await expect(page.locator('text=Group Dinner')).toBeVisible(); await expect(page.locator('text=$120.00')).toBeVisible(); // Each person should owe $40 (120 / 3) await expect(page.locator('text=$40.00')).toBeVisible(); }); });