- 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
210 lines
7.9 KiB
TypeScript
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');
|
|
});
|
|
});
|