import { test, expect } from '@playwright/test'; async function openEditModal(page: any, transactionIndex: number = 0) { // 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 specified transaction const editButton = page.locator('button[hx-get*="/transaction2/"][hx-get*="/edit"]').nth(transactionIndex); 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 setAccountLocation(page: any, rowIndex: number, location: string) { const row = await findAccountRow(page, rowIndex); const locationSelect = row.locator('select[name*="location"]').first(); // If the option doesn't exist, add it (for testing invalid locations) const optionExists = await locationSelect.locator(`option[value="${location}"]`).count() > 0; if (!optionExists) { await locationSelect.evaluate((el: HTMLSelectElement, value: string) => { const option = document.createElement('option'); option.value = value; option.textContent = value; el.appendChild(option); }, location); } await locationSelect.selectOption(location); await locationSelect.dispatchEvent('change'); await page.waitForTimeout(300); } async function getAccountLocation(page: any, rowIndex: number): Promise { const row = await findAccountRow(page, rowIndex); const locationSelect = row.locator('select[name*="location"]').first(); return await locationSelect.inputValue(); } async function removeAllAccounts(page: any) { const allRows = page.locator('#account-grid-body tbody tr'); const rowCount = await allRows.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 tag, not a