Files
integreat/e2e/transaction-navigation.spec.ts
Bryce 200056098f feat: add memo filter and enhance description filter with regex matching
- Add new memo filter to transaction page (searches :transaction/memo)
- Enhance existing description filter to use case-insensitive regex
- Both filters support wildcard matching via .* pattern
- Add e2e tests for filter functionality
- Update test data with memo fields
2026-05-26 16:34:56 -07:00

210 lines
7.9 KiB
TypeScript

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<number> {
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, '', '250');
// Step 2: Wait for URL to update
await page.waitForURL(url => url.search.includes('amount-lte=250'), { 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=250');
});
});
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');
});
});