Files
integreat/e2e/transaction-edit.spec.ts
2026-05-21 11:51:29 -07:00

284 lines
9.6 KiB
TypeScript

import { test, expect } from '@playwright/test';
async function openEditModal(page: any) {
// Navigate to transactions page
await page.goto('/transaction2');
// Wait for the table to load
await page.waitForSelector('table tbody tr');
// Find and click the edit button for the test transaction
const editButton = page.locator('button[hx-get*="/transaction2/"][hx-get*="/edit"]').first();
await editButton.click();
// Wait for the modal to open
await page.waitForSelector('#modal-holder[x-show="open"]', { state: 'visible' });
await page.waitForSelector('#wizardmodal');
// Click Next to go to the links step (button says "Transaction Actions")
await page.click('button:has-text("Transaction Actions")');
// Wait for the links step to load
await page.waitForSelector('text=Transaction Actions', { state: 'visible' });
// Click on "Manual" tab
await page.click('button:has-text("Manual")');
// Wait for the manual form to appear
await page.waitForSelector('#account-grid-body');
}
let testInfoCache: any = null;
async function getTestInfo(page: any) {
// Always fetch fresh to handle server restarts
const response = await page.request.get('/test-info');
testInfoCache = await response.json();
return testInfoCache;
}
async function selectAccountFromTypeahead(page: any, rowIndex: number, accountName: string) {
// The account search uses Solr which isn't available in tests.
// Instead, we directly set the hidden input value via JavaScript.
// Get all rows except the new-row, total, balance, and transaction total rows
const allRows = page.locator('#account-grid-body tbody tr');
const rowCount = await allRows.count();
// Find the row that has a hidden input for account (actual account rows)
let accountRow = null;
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) {
accountRow = row;
break;
}
accountRowIndex++;
}
}
if (!accountRow) {
throw new Error(`Could not find account row at index ${rowIndex}`);
}
// Find the hidden input for the account
const hiddenInput = accountRow.locator('input[type="hidden"][name*="transaction-account/account"]').first();
// Get account IDs from test-info endpoint
const testInfo = await getTestInfo(page);
const accountKey = accountName === 'Test' ? 'test-account' : 'second-account';
const accountId = testInfo.accounts[accountKey];
if (!accountId) {
throw new Error(`Could not find account with name ${accountName}`);
}
// Set the hidden input value and trigger change
// Also update Alpine.js data to prevent it from overwriting our value
await hiddenInput.evaluate((el: HTMLInputElement, value: string) => {
// Set the DOM value
el.value = value;
// Update Alpine.js component data
const alpineEl = el.closest('[x-data]');
if (alpineEl && (alpineEl as any).__x) {
(alpineEl as any).__x.$data.value.value = parseInt(value);
(alpineEl as any).__x.$data.value.label = 'Selected Account';
}
// Also update any parent Alpine model (accountId)
const rowEl = el.closest('tr[x-data]');
if (rowEl && (rowEl as any).__x) {
(rowEl as any).__x.$data.accountId = parseInt(value);
}
el.dispatchEvent(new Event('change', { bubbles: true }));
}, accountId.toString());
// Wait for any HTMX updates
await page.waitForTimeout(300);
}
async function findAccountRow(page: any, rowIndex: number) {
const allRows = page.locator('#account-grid-body tbody tr');
const rowCount = await allRows.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++;
}
}
throw new Error(`Could not find account row at index ${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();
await amountInput.fill(amount);
await amountInput.dispatchEvent('change');
await page.waitForTimeout(300);
}
async function addNewAccount(page: any) {
// Click the "New account" button
await page.click('text=New account');
// Wait for the new row to be added
await page.waitForTimeout(500);
}
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 }));
}
});
// Wait for the modal to close
await page.waitForSelector('#modal-holder[x-show="open"]', { state: 'hidden', timeout: 10000 });
}
async function toggleToPercentMode(page: any) {
const percentRadio = page.locator('input[name="step-params[amount-mode]"][value="%"]');
await percentRadio.click();
// Wait for HTMX to swap the grid body
await page.waitForResponse(response =>
response.url().includes('/toggle-amount-mode') && response.status() === 200
);
await page.waitForTimeout(200);
}
async function toggleToDollarMode(page: any) {
const dollarRadio = page.locator('input[name="step-params[amount-mode]"][value="$"]');
await dollarRadio.click();
// Wait for HTMX to swap the grid body
await page.waitForResponse(response =>
response.url().includes('/toggle-amount-mode') && response.status() === 200
);
await page.waitForTimeout(200);
}
test.describe.configure({ mode: 'serial' });
test.describe('Transaction Edit Full Workflow', () => {
test('should code transaction with vendor using percentage, then split 50/50, then switch to dollars', async ({ page }) => {
// Step 1: Open edit modal and code with 100% to one account
await openEditModal(page);
// Switch to percentage mode
await toggleToPercentMode(page);
// Add a new account row
await addNewAccount(page);
// Select the account
await selectAccountFromTypeahead(page, 0, 'Test');
// Set amount to 100%
await setAccountAmount(page, 0, '100');
// Save the transaction
await saveTransaction(page);
// Step 2: Re-open and split 50/50 with two accounts
await openEditModal(page);
// Note: amount-mode is UI-only state, so it resets to $ when re-opening
// Switch back to percentage mode
await toggleToPercentMode(page);
// The existing account from step 1 should already be there
// Change its amount from 100% to 50%
await setAccountAmount(page, 0, '50');
// Add a second account at 50%
await addNewAccount(page);
await selectAccountFromTypeahead(page, 1, 'Second');
await setAccountAmount(page, 1, '50');
// Save
await saveTransaction(page);
// Step 3: Re-open and verify dollar amounts
await openEditModal(page);
// The accounts should be persisted from the previous save
// Wait for accounts to load
await page.waitForTimeout(500);
// Verify we're in dollar mode (default)
const dollarRadio = page.locator('input[name="step-params[amount-mode]"][value="$"]');
await expect(dollarRadio).toBeChecked();
// Verify amounts are in dollars (converted from percentages on save)
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();
// Each should be $50.00 (or close to it)
const val0 = await amount0.inputValue();
const val1 = await amount1.inputValue();
expect(parseFloat(val0)).toBeCloseTo(50.0, 1);
expect(parseFloat(val1)).toBeCloseTo(50.0, 1);
// Save
await saveTransaction(page);
});
});
test.describe('Transaction Edit Validation', () => {
test('should show validation error when account totals do not match transaction amount', async ({ page }) => {
await openEditModal(page);
// Stay in dollar mode (default)
// Add an account
await addNewAccount(page);
await selectAccountFromTypeahead(page, 0, 'Test');
// Set amount to $50 (which doesn't match the $100 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 }));
}
});
// Wait a bit for the response
await page.waitForTimeout(1000);
// Modal should still be open (save failed)
await expect(page.locator('#modal-holder[x-show="open"]')).toBeVisible();
// The form should still be present
const form = page.locator('#wizard-form');
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 value = await amountInput.inputValue();
expect(parseFloat(value)).toBeCloseTo(50.0, 1);
});
});