improvements

This commit is contained in:
2026-05-26 11:18:52 -07:00
parent f239b114c3
commit ae0788e6dd
6 changed files with 299 additions and 147 deletions

View File

@@ -234,7 +234,7 @@ test.describe('Bulk Code Transactions - Happy Path', () => {
await openBulkCodeModal(page);
// Should show all transactions
await expect(page.locator('text=Bulk editing 3 transactions')).toBeVisible();
await expect(page.locator('text=Bulk editing 5 transactions')).toBeVisible();
// Add account at 100%
await addNewAccount(page);

View File

@@ -103,27 +103,19 @@ async function selectAccountFromTypeahead(page: any, rowIndex: number, accountNa
}
async function findAccountRow(page: any, rowIndex: number) {
const allRows = page.locator('#account-grid-body tbody tr');
const rowCount = await allRows.count();
const accountRows = page.locator('#account-grid-body tbody tr.account-row');
const rowCount = await accountRows.count();
let accountRowIndex = 0;
for (let i = 0; i < rowCount; i++) {
const row = allRows.nth(i);
const hasAccountInput = await row.locator('input[name*="transaction-account/account"]').count() > 0;
if (hasAccountInput) {
if (accountRowIndex === rowIndex) {
return row;
}
accountRowIndex++;
}
if (rowIndex >= rowCount) {
throw new Error(`Could not find account row at index ${rowIndex}. Only ${rowCount} account rows found.`);
}
throw new Error(`Could not find account row at index ${rowIndex}`);
return accountRows.nth(rowIndex);
}
async function setAccountAmount(page: any, rowIndex: number, amount: string) {
const row = await findAccountRow(page, rowIndex);
const amountInput = row.locator('input[name*="transaction-account/amount"]').first();
const amountInput = row.locator('.account-amount-field');
await amountInput.fill(amount);
await amountInput.dispatchEvent('change');
await page.waitForTimeout(300);
@@ -164,34 +156,24 @@ async function getAccountLocation(page: any, rowIndex: number): Promise<string>
}
async function removeAllAccounts(page: any) {
const allRows = page.locator('#account-grid-body tbody tr');
const rowCount = await allRows.count();
const accountRows = page.locator('#account-grid-body tbody tr.account-row');
const rowCount = await accountRows.count();
for (let i = rowCount - 1; i >= 0; i--) {
const row = allRows.nth(i);
const hasAccountInput = await row.locator('input[name*="transaction-account/account"]').count() > 0;
if (hasAccountInput) {
// Click the X button to remove (it's an <a> tag, not a <button>)
const removeButton = row.locator('a').first();
await removeButton.click();
// Wait for the Alpine.js removal animation (500ms + buffer)
await page.waitForTimeout(700);
}
const row = accountRows.nth(i);
const removeButton = row.locator('.account-remove-action');
await removeButton.click();
// Wait for the Alpine.js removal animation (500ms + buffer)
await page.waitForTimeout(700);
}
}
async function saveTransaction(page: any) {
// Submit the form directly instead of clicking the button
// The Done button might not have type="submit"
await page.evaluate(() => {
const form = document.querySelector('#wizard-form') as HTMLFormElement;
if (form) {
form.dispatchEvent(new Event('submit', { bubbles: true }));
}
});
// Click the save button to submit the form via HTMX
await page.locator('.wizard-save-action').click();
// Wait for the modal to close
await page.waitForSelector('#modal-holder[x-show="open"]', { state: 'hidden', timeout: 10000 });
// Wait for the modal to close (longer timeout for parallel test load)
await page.waitForSelector('#modal-holder[x-show="open"]', { state: 'hidden', timeout: 20000 });
}
async function toggleToPercentMode(page: any) {
@@ -226,6 +208,9 @@ test.describe('Transaction Edit Shared Location', () => {
// Step 1: Open edit modal and add an account with Shared location
await openEditModal(page, transactionIndex);
// Remove any existing accounts from previous tests
await removeAllAccounts(page);
// Add a new account row
await addNewAccount(page);
@@ -261,11 +246,17 @@ test.describe('Transaction Edit Full Workflow', () => {
// Step 1: Open edit modal and code with 100% to one account
await openEditModal(page);
// Switch to percentage mode
// Switch to percentage mode first (this re-renders the grid from server state)
await toggleToPercentMode(page);
// Add a new account row
await addNewAccount(page);
// Check if there's already an account from previous tests
const allRows = page.locator('#account-grid-body tbody tr');
const hasExistingAccount = await allRows.locator('input[name*="transaction-account/account"]').count() > 0;
if (!hasExistingAccount) {
// Add a new account row if none exist
await addNewAccount(page);
}
// Select the account
await selectAccountFromTypeahead(page, 0, 'Test');
@@ -289,6 +280,7 @@ test.describe('Transaction Edit Full Workflow', () => {
// Add a second account at 50%
await addNewAccount(page);
await page.waitForTimeout(1000);
await selectAccountFromTypeahead(page, 1, 'Second');
await setAccountAmount(page, 1, '50');
@@ -310,8 +302,8 @@ test.describe('Transaction Edit Full Workflow', () => {
const row0 = await findAccountRow(page, 0);
const row1 = await findAccountRow(page, 1);
const amount0 = row0.locator('input[name*="transaction-account/amount"]').first();
const amount1 = row1.locator('input[name*="transaction-account/amount"]').first();
const amount0 = row0.locator('.account-amount-field');
const amount1 = row1.locator('.account-amount-field');
// Each should be $50.00 (or close to it)
const val0 = await amount0.inputValue();
@@ -331,6 +323,9 @@ test.describe('Transaction Edit Validation', () => {
await openEditModal(page, 2);
// Stay in dollar mode (default)
// Remove any existing accounts from previous tests
await removeAllAccounts(page);
await page.waitForTimeout(2000);
// Add an account
await addNewAccount(page);
await selectAccountFromTypeahead(page, 0, 'Test');
@@ -338,17 +333,12 @@ test.describe('Transaction Edit Validation', () => {
// Set amount to $50 (which doesn't match the $300 transaction)
await setAccountAmount(page, 0, '50');
// Try to save - this should fail because $50 != $100
// We submit the form and expect the modal to stay open
await page.evaluate(() => {
const form = document.querySelector('#wizard-form') as HTMLFormElement;
if (form) {
form.dispatchEvent(new Event('submit', { bubbles: true }));
}
});
// Try to save - this should fail because $50 != $300
// Click the save button to submit the form via HTMX
await page.locator('.wizard-save-action').click();
// Wait a bit for the response
await page.waitForTimeout(1000);
// Wait for the response (longer timeout for parallel test load)
await page.waitForTimeout(3000);
// Modal should still be open (save failed)
await expect(page.locator('#modal-holder[x-show="open"]')).toBeVisible();
@@ -358,7 +348,7 @@ test.describe('Transaction Edit Validation', () => {
await expect(form).toBeVisible();
// Verify the account row is still there with our $50 value
const amountInput = page.locator('input[name*="transaction-account/amount"]').first();
const amountInput = page.locator('.account-amount-field').first();
const value = await amountInput.inputValue();
expect(parseFloat(value)).toBeCloseTo(50.0, 1);

View File

@@ -0,0 +1,131 @@
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');
});
});
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');
});
});