import { test, expect } from '@playwright/test'; async function navigateToTransactions(page: any, path: string = '/transaction2') { await page.setExtraHTTPHeaders({ 'x-clients': '"mine"' }); await page.goto(path); await page.waitForSelector('table tbody tr'); } async function setAmountFilter(page: any, gte: string, lte: string) { const amountGte = page.locator('input[name="amount-gte"]').first(); const amountLte = page.locator('input[name="amount-lte"]').first(); await amountGte.fill(gte); await amountLte.fill(lte); // Trigger the filter form submission via change event await amountLte.dispatchEvent('change'); // Wait for HTMX to update the table and push URL await page.waitForTimeout(1000); } async function getTableRowCount(page: any): Promise { const rows = page.locator('table tbody tr'); return await rows.count(); } async function clickTransactionNavLink(page: any, linkText: string) { const link = page.locator(`a:has-text("${linkText}")`).first(); await link.click({ force: true }); // Wait for navigation and table to load await page.waitForTimeout(1000); await page.waitForSelector('table tbody tr', { timeout: 10000 }); } test.describe('Transaction Navigation - Amount Filter Persistence', () => { test('should persist amount filter when navigating from All to Unapproved', async ({ page }) => { // Step 1: Navigate to All transactions page await navigateToTransactions(page, '/transaction2'); // Step 2: Set amount filter await setAmountFilter(page, '250', ''); // Step 3: Verify the URL updated with the filter await page.waitForURL(url => url.search.includes('amount-gte=250'), { timeout: 5000 }); // Step 4: Click the Unapproved nav link await clickTransactionNavLink(page, 'Unapproved'); // Step 5: Verify amount filter is preserved in URL after navigation const unapprovedUrl = page.url(); expect(unapprovedUrl).toContain('amount-gte=250'); // Step 6: Verify filter still applies (only 1 row - the 300 transaction) const filteredCount = await getTableRowCount(page); expect(filteredCount).toBe(1); }); test('should persist amount filter when navigating from Unapproved to All', async ({ page }) => { // Step 1: Navigate to Unapproved page with amount filter already in URL await navigateToTransactions(page, '/transaction2/unapproved?amount-gte=200'); // Step 2: Click All nav link await clickTransactionNavLink(page, 'All'); // Step 3: Verify amount filter is preserved const allUrl = page.url(); expect(allUrl).toContain('amount-gte=200'); }); test('should persist amount filter when navigating to Client Review', async ({ page }) => { // Step 1: Navigate to All page and set amount filter await navigateToTransactions(page, '/transaction2'); await setAmountFilter(page, '', '500'); // Step 2: Wait for URL to update await page.waitForURL(url => url.search.includes('amount-lte=500'), { timeout: 5000 }); // Step 3: Click Client Review nav link await clickTransactionNavLink(page, 'Client Review'); // Step 4: Verify filter persisted const feedbackUrl = page.url(); expect(feedbackUrl).toContain('amount-lte=500'); }); }); test.describe('Transaction Navigation - Date Filter Persistence', () => { test('should persist date-range preset when navigating between pages', async ({ page }) => { // Step 1: Navigate with date-range=all (includes 2022 test data) await navigateToTransactions(page, '/transaction2?date-range=all'); // Step 2: Click Unapproved nav link await clickTransactionNavLink(page, 'Unapproved'); // Step 3: Verify date-range persisted const unapprovedUrl = page.url(); expect(unapprovedUrl).toContain('date-range=all'); }); }); async function setTextFilter(page: any, name: string, value: string) { const input = page.locator(`input[name="${name}"]`).first(); await input.fill(value); await input.dispatchEvent('change'); await page.waitForTimeout(1000); } test.describe('Transaction Filters - Description and Memo', () => { test('should filter by description with case-insensitive wildcard matching', async ({ page }) => { await navigateToTransactions(page, '/transaction2'); // Filter by "second" (lowercase) - should match "Second transaction" await setTextFilter(page, 'description', 'second'); // Wait for URL to update await page.waitForURL(url => url.search.includes('description=second'), { timeout: 5000 }); // Should show only 1 row (the "Second transaction") const rowCount = await getTableRowCount(page); expect(rowCount).toBe(1); // Verify the row contains "Second transaction" const rowText = await page.locator('table tbody tr').first().textContent(); expect(rowText).toContain('Second transaction'); }); test('should filter by memo with case-insensitive wildcard matching', async ({ page }) => { await navigateToTransactions(page, '/transaction2'); // Filter by "rent" (lowercase) - should match "Monthly rent payment" await setTextFilter(page, 'memo', 'rent'); // Wait for URL to update await page.waitForURL(url => url.search.includes('memo=rent'), { timeout: 5000 }); // Should show only 1 row (the transaction with "Monthly rent payment" memo) const rowCount = await getTableRowCount(page); expect(rowCount).toBe(1); // Verify the row contains "Test transaction" (the one with the rent memo) const rowText = await page.locator('table tbody tr').first().textContent(); expect(rowText).toContain('Test transaction'); }); test('should filter by description and memo together', async ({ page }) => { await navigateToTransactions(page, '/transaction2'); // Set both filters - should match "Test transaction" which has memo "Monthly rent payment" await setTextFilter(page, 'description', 'test'); await setTextFilter(page, 'memo', 'rent'); // Wait for URL to update await page.waitForURL(url => url.search.includes('description=test') && url.search.includes('memo=rent'), { timeout: 5000 }); // Should show only 1 row const rowCount = await getTableRowCount(page); expect(rowCount).toBe(1); // Verify it's the "Test transaction" row const rowText = await page.locator('table tbody tr').first().textContent(); expect(rowText).toContain('Test transaction'); }); test('should show no results when filter does not match', async ({ page }) => { await navigateToTransactions(page, '/transaction2'); // Filter by something that doesn't exist await setTextFilter(page, 'description', 'nonexistent'); // Wait for URL to update await page.waitForURL(url => url.search.includes('description=nonexistent'), { timeout: 5000 }); // Should show no rows const rowCount = await getTableRowCount(page); expect(rowCount).toBe(0); }); }); test.describe('Transaction Sort - Default Newest First', () => { test('should show transactions sorted by date descending by default', async ({ page }) => { await navigateToTransactions(page, '/transaction2'); // Verify no explicit sort in URL initially expect(page.url()).not.toContain('sort='); // The default sort should be newest first (descending by date) // We can verify this by checking that clicking Date header toggles to asc const dateHeader = page.locator('th').filter({ hasText: 'Date' }).first(); await dateHeader.click(); // Wait for HTMX to update await page.waitForTimeout(800); // The URL should now have an explicit sort parameter (ascending because we toggled from default desc) const currentUrl = page.url(); expect(currentUrl).toContain('sort=date%3Aasc'); // Click again to toggle to descending await dateHeader.click(); await page.waitForTimeout(800); const toggledUrl = page.url(); expect(toggledUrl).toContain('sort=date%3Adesc'); }); });