Add e2e tests for bulk coding transactions and fix SSR location validation

- Create requirements document based on master cljs implementation
- Add Playwright e2e tests covering happy path, validation, and distribution
- Fix hiccup id syntax in SSR bulk code form (div#id.class order)
- Add missing account location validation to SSR bulk code submit
- Enhance test server with multiple transactions and fixed-location account
This commit is contained in:
2026-05-21 13:21:22 -07:00
parent 76c6eaddb9
commit 8bd0cee1b1
9 changed files with 543 additions and 632 deletions

View File

@@ -0,0 +1,387 @@
import { test, expect } from '@playwright/test';
let testInfoCache: any = null;
async function getTestInfo(page: any) {
const response = await page.request.get('/test-info');
testInfoCache = await response.json();
return testInfoCache;
}
async function navigateToTransactions(page: any) {
await page.goto('/transaction2');
await page.waitForSelector('table tbody tr');
}
async function selectTransactionByIndex(page: any, index: number) {
const rows = page.locator('table tbody tr');
const row = rows.nth(index);
const checkbox = row.locator('input[type="checkbox"][name="id"]').first();
await checkbox.click();
await page.waitForTimeout(200);
}
async function selectAllTransactions(page: any) {
const headerCheckbox = page.locator('input#checkbox-all').first();
await headerCheckbox.click();
await page.waitForTimeout(200);
}
async function openBulkCodeModal(page: any) {
const codeButton = page.locator('button:has-text("Code")').first();
await codeButton.click();
await page.waitForSelector('#modal-holder[x-show="open"]', { state: 'visible' });
await page.waitForSelector('#wizardmodal');
}
async function closeBulkCodeModal(page: any) {
// The success response swaps the modal content, but the modal holder stays open
// Wait for the success message to appear
await page.waitForSelector('text=Successfully coded', { timeout: 10000 });
}
async function selectAccountFromTypeahead(page: any, rowIndex: number, accountKey: string) {
const testInfo = await getTestInfo(page);
const accountId = testInfo.accounts[accountKey];
if (!accountId) {
throw new Error(`Could not find account with key ${accountKey}`);
}
const allRows = page.locator('#account-entries tbody tr');
const rowCount = await allRows.count();
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*="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}`);
}
const hiddenInput = accountRow.locator('input[type="hidden"][name*="[account]"]').first();
await hiddenInput.evaluate((el: HTMLInputElement, value: string) => {
// Replace the Alpine-managed hidden input with a plain one
const newInput = document.createElement('input');
newInput.type = 'hidden';
newInput.name = el.name;
newInput.value = value;
el.parentNode.replaceChild(newInput, el);
}, accountId.toString());
await page.waitForTimeout(300);
// Trigger the location select reload by dispatching 'changed' event
const locationContainer = accountRow.locator('[x-dispatch\\:changed]').first();
if (await locationContainer.count() > 0) {
await locationContainer.evaluate((el: HTMLElement) => {
el.dispatchEvent(new CustomEvent('changed', { bubbles: true }));
});
await page.waitForTimeout(500);
}
}
async function findAccountRow(page: any, rowIndex: number) {
const allRows = page.locator('#account-entries 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*="account"]').count() > 0;
if (hasAccountInput) {
if (accountRowIndex === rowIndex) {
return row;
}
accountRowIndex++;
}
}
throw new Error(`Could not find account row at index ${rowIndex}`);
}
async function setAccountPercentage(page: any, rowIndex: number, percentage: string) {
const row = await findAccountRow(page, rowIndex);
const percentageInput = row.locator('input[name*="percentage"]').first();
await percentageInput.fill(percentage);
await percentageInput.dispatchEvent('change');
await page.waitForTimeout(300);
}
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 addNewAccount(page: any) {
const newAccountButton = page.locator('a:has-text("New account")').first();
await newAccountButton.click();
await page.waitForTimeout(500);
}
async function submitBulkCodeForm(page: any) {
const form = page.locator('#wizard-form');
await form.evaluate((el: HTMLFormElement) => {
el.dispatchEvent(new Event('submit', { bubbles: true }));
});
}
async function getModalErrorText(page: any) {
const errorElement = page.locator('#form-errors .error-content');
try {
await errorElement.waitFor({ state: 'visible', timeout: 3000 });
return await errorElement.textContent();
} catch {
return null;
}
}
test.describe.configure({ mode: 'serial' });
test.describe('Bulk Code Transactions - Happy Path', () => {
test('should bulk code a single transaction with vendor, status, and 100% account', async ({ page }) => {
await navigateToTransactions(page);
await selectTransactionByIndex(page, 0);
await openBulkCodeModal(page);
// Verify modal shows correct count
await expect(page.locator('text=Bulk editing 1 transactions')).toBeVisible();
// Select vendor
const vendorHidden = page.locator('input[type="hidden"][name="step-params[vendor]"]').first();
const testInfo = await getTestInfo(page);
await vendorHidden.evaluate((el: HTMLInputElement, value: string) => {
const newInput = document.createElement('input');
newInput.type = 'hidden';
newInput.name = el.name;
newInput.value = value;
el.parentNode.replaceChild(newInput, el);
}, testInfo.accounts.vendor.toString());
await page.waitForTimeout(300);
// Select approval status
const statusSelect = page.locator('select[name="step-params[approval-status]"]').first();
await statusSelect.selectOption('approved');
// Add account
await addNewAccount(page);
await selectAccountFromTypeahead(page, 0, 'test-account');
await setAccountLocation(page, 0, 'Shared');
await setAccountPercentage(page, 0, '100');
// Submit
await submitBulkCodeForm(page);
await closeBulkCodeModal(page);
// Verify success by checking table refreshed
await page.waitForSelector('table tbody tr');
});
test('should bulk code multiple selected transactions', async ({ page }) => {
await navigateToTransactions(page);
await selectTransactionByIndex(page, 0);
await selectTransactionByIndex(page, 1);
await openBulkCodeModal(page);
// Should show count of selected transactions
await expect(page.locator('text=Bulk editing 2 transactions')).toBeVisible();
// Add account at 100%
await addNewAccount(page);
await selectAccountFromTypeahead(page, 0, 'test-account');
await setAccountLocation(page, 0, 'Shared');
await setAccountPercentage(page, 0, '100');
await submitBulkCodeForm(page);
await closeBulkCodeModal(page);
await page.waitForSelector('table tbody tr');
});
test('should bulk code all visible transactions via header checkbox', async ({ page }) => {
await navigateToTransactions(page);
await selectAllTransactions(page);
await openBulkCodeModal(page);
// Should show all transactions
await expect(page.locator('text=Bulk editing 3 transactions')).toBeVisible();
// Add account at 100%
await addNewAccount(page);
await selectAccountFromTypeahead(page, 0, 'test-account');
await setAccountLocation(page, 0, 'Shared');
await setAccountPercentage(page, 0, '100');
await submitBulkCodeForm(page);
await closeBulkCodeModal(page);
await page.waitForSelector('table tbody tr');
});
});
test.describe('Bulk Code Transactions - Validation', () => {
test('should reject when no vendor, status, or accounts provided', async ({ page }) => {
await navigateToTransactions(page);
await selectTransactionByIndex(page, 0);
await openBulkCodeModal(page);
// Submit without any changes
await submitBulkCodeForm(page);
await page.waitForTimeout(1000);
// Modal should still be open
await expect(page.locator('#modal-holder[x-show="open"]')).toBeVisible();
});
test('should reject when account percentages total less than 100%', async ({ page }) => {
await navigateToTransactions(page);
await selectTransactionByIndex(page, 0);
await openBulkCodeModal(page);
await addNewAccount(page);
await selectAccountFromTypeahead(page, 0, 'test-account');
await setAccountLocation(page, 0, 'Shared');
await setAccountPercentage(page, 0, '50');
await submitBulkCodeForm(page);
await page.waitForTimeout(1000);
// Modal should still be open
await expect(page.locator('#modal-holder[x-show="open"]')).toBeVisible();
// Should show validation error
const errorText = await getModalErrorText(page);
expect(errorText).toContain('does not equal 100%');
});
test('should reject when account percentages total more than 100%', async ({ page }) => {
await navigateToTransactions(page);
await selectTransactionByIndex(page, 0);
await openBulkCodeModal(page);
await addNewAccount(page);
await selectAccountFromTypeahead(page, 0, 'test-account');
await setAccountLocation(page, 0, 'Shared');
await setAccountPercentage(page, 0, '150');
await submitBulkCodeForm(page);
await page.waitForTimeout(1000);
// Modal should still be open
await expect(page.locator('#modal-holder[x-show="open"]')).toBeVisible();
});
test('should reject invalid location for account with fixed location', async ({ page }) => {
await navigateToTransactions(page);
await selectTransactionByIndex(page, 0);
await openBulkCodeModal(page);
await addNewAccount(page);
// Use the fixed-location account
await selectAccountFromTypeahead(page, 0, 'fixed-location-account');
// Try to set wrong location (account is fixed to "DT", try "INVALID")
await setAccountLocation(page, 0, 'INVALID');
await setAccountPercentage(page, 0, '100');
await submitBulkCodeForm(page);
await page.waitForTimeout(1000);
// Modal should still be open
await expect(page.locator('#modal-holder[x-show="open"]')).toBeVisible();
// Should show validation error about location mismatch
const errorText = await getModalErrorText(page);
expect(errorText).toContain('location');
});
test('should reject location not belonging to client', async ({ page }) => {
await navigateToTransactions(page);
await selectTransactionByIndex(page, 0);
await openBulkCodeModal(page);
await addNewAccount(page);
await selectAccountFromTypeahead(page, 0, 'test-account');
// Client only has "DT" location, try "GR"
await setAccountLocation(page, 0, 'GR');
await setAccountPercentage(page, 0, '100');
await submitBulkCodeForm(page);
await page.waitForTimeout(1000);
// Modal should still be open
await expect(page.locator('#modal-holder[x-show="open"]')).toBeVisible();
// Should show validation error
const errorText = await getModalErrorText(page);
expect(errorText).toContain('location');
});
});
test.describe('Bulk Code Transactions - Account Distribution', () => {
test('should split 50/50 between two accounts', async ({ page }) => {
await navigateToTransactions(page);
await selectTransactionByIndex(page, 0);
await openBulkCodeModal(page);
// First account at 50%
await addNewAccount(page);
await selectAccountFromTypeahead(page, 0, 'test-account');
await setAccountLocation(page, 0, 'DT');
await setAccountPercentage(page, 0, '50');
// Second account at 50%
await addNewAccount(page);
await selectAccountFromTypeahead(page, 1, 'second-account');
await setAccountLocation(page, 1, 'DT');
await setAccountPercentage(page, 1, '50');
await submitBulkCodeForm(page);
await closeBulkCodeModal(page);
await page.waitForSelector('table tbody tr');
});
test('should allow Shared location for account without fixed location', async ({ page }) => {
await navigateToTransactions(page);
await selectTransactionByIndex(page, 0);
await openBulkCodeModal(page);
await addNewAccount(page);
await selectAccountFromTypeahead(page, 0, 'test-account');
// "Shared" should be valid for accounts without fixed location
await setAccountLocation(page, 0, 'Shared');
await setAccountPercentage(page, 0, '100');
await submitBulkCodeForm(page);
await closeBulkCodeModal(page);
await page.waitForSelector('table tbody tr');
});
});

View File

@@ -1,111 +0,0 @@
import { test, expect } from '@playwright/test';
async function openEditModal(page: any) {
await page.goto('/transaction2');
await page.waitForSelector('table tbody tr');
const editButton = page.locator('button[hx-get*="/transaction2/"][hx-get*="/edit"]').first();
await editButton.click();
await page.waitForSelector('#modal-holder[x-show="open"]', { state: 'visible' });
await page.waitForSelector('#wizardmodal');
await page.click('button:has-text("Transaction Actions")');
await page.waitForSelector('text=Transaction Actions', { state: 'visible' });
await page.click('button:has-text("Manual")');
await page.waitForSelector('#account-grid-body');
}
async function addNewAccount(page: any) {
await page.click('text=New account');
await page.waitForTimeout(500);
}
async function selectAccountFromTypeahead(page: any, rowIndex: number, accountName: string) {
const allRows = page.locator('#account-grid-body tbody tr');
const rowCount = await allRows.count();
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}`);
}
const hiddenInput = accountRow.locator('input[type="hidden"][name*="transaction-account/account"]').first();
const testInfoResponse = await page.request.get('/test-info');
const testInfo = await testInfoResponse.json();
const accountKey = accountName === 'Test' ? 'test-account' : 'second-account';
const accountId = testInfo.accounts[accountKey];
await hiddenInput.evaluate((el: HTMLInputElement, value: string) => {
el.value = value;
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';
}
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());
await page.waitForTimeout(300);
}
async function setAccountAmount(page: any, rowIndex: number, amount: string) {
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) {
const amountInput = row.locator('input[name*="transaction-account/amount"]').first();
await amountInput.fill(amount);
await amountInput.dispatchEvent('change');
await page.waitForTimeout(300);
return;
}
accountRowIndex++;
}
}
throw new Error(`Could not find account row at index ${rowIndex}`);
}
async function toggleToPercentMode(page: any) {
const percentRadio = page.locator('input[name="step-params[amount-mode]"][value="%"]');
await percentRadio.click();
await page.waitForResponse(response =>
response.url().includes('/toggle-amount-mode') && response.status() === 200
);
await page.waitForTimeout(200);
}
async function saveTransaction(page: any) {
await page.click('button:has-text("Done")');
await page.waitForSelector('#modal-holder[x-show="open"]', { state: 'hidden', timeout: 10000 });
}
test('exact workflow', async ({ page }) => {
await openEditModal(page);
await toggleToPercentMode(page);
await addNewAccount(page);
await selectAccountFromTypeahead(page, 0, 'Test');
await setAccountAmount(page, 0, '100');
await saveTransaction(page);
});

View File

@@ -1,151 +0,0 @@
import { test, expect } from '@playwright/test';
async function openEditModal(page: any) {
await page.goto('/transaction2');
await page.waitForSelector('table tbody tr');
const editButton = page.locator('button[hx-get*="/transaction2/"][hx-get*="/edit"]').first();
await editButton.click();
await page.waitForSelector('#modal-holder[x-show="open"]', { state: 'visible' });
await page.waitForSelector('#wizardmodal');
await page.click('button:has-text("Transaction Actions")');
await page.waitForSelector('text=Transaction Actions', { state: 'visible' });
await page.click('button:has-text("Manual")');
await page.waitForSelector('#account-grid-body');
}
async function addNewAccount(page: any) {
await page.click('text=New account');
await page.waitForTimeout(500);
}
async function selectAccountFromTypeahead(page: any, rowIndex: number, accountName: string) {
const allRows = page.locator('#account-grid-body tbody tr');
const rowCount = await allRows.count();
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}`);
}
const hiddenInput = accountRow.locator('input[type="hidden"][name*="transaction-account/account"]').first();
// Get account ID from test-info
const testInfoResponse = await page.request.get('/test-info');
const testInfo = await testInfoResponse.json();
const accountKey = accountName === 'Test' ? 'test-account' : 'second-account';
const accountId = testInfo.accounts[accountKey];
await hiddenInput.evaluate((el: HTMLInputElement, value: string) => {
el.value = value;
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';
}
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());
await page.waitForTimeout(300);
}
async function setAccountAmount(page: any, rowIndex: number, amount: string) {
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) {
const amountInput = row.locator('input[name*="transaction-account/amount"]').first();
await amountInput.fill(amount);
await amountInput.dispatchEvent('change');
await page.waitForTimeout(300);
return;
}
accountRowIndex++;
}
}
throw new Error(`Could not find account row at index ${rowIndex}`);
}
test('debug save with percentage', async ({ page }) => {
await openEditModal(page);
// Switch to percentage mode
await page.locator('input[name="step-params[amount-mode]"][value="%"]').click();
await page.waitForResponse(response =>
response.url().includes('/toggle-amount-mode') && response.status() === 200
);
await page.waitForTimeout(200);
await addNewAccount(page);
await selectAccountFromTypeahead(page, 0, 'Test');
await setAccountAmount(page, 0, '100');
// Intercept the form submission to see what happens
let responseStatus = 0;
let responseBody = '';
page.on('request', (request: any) => {
if (request.url().includes('/edit-submit')) {
console.log('Request URL:', request.url());
console.log('Request method:', request.method());
}
});
page.on('response', async (response: any) => {
if (response.url().includes('/edit-submit')) {
responseStatus = response.status();
try {
responseBody = await response.text();
} catch (e) {
responseBody = 'Could not read body';
}
console.log('Response status:', responseStatus);
console.log('Response body:', responseBody.substring(0, 500));
}
});
// Click Done
await page.click('button:has-text("Done")');
// Wait a bit
await page.waitForTimeout(2000);
console.log('Final status:', responseStatus);
console.log('Response body length:', responseBody.length);
// Check for error messages in the response
if (responseBody.includes('error') || responseBody.includes('Error') || responseBody.includes('has-error')) {
console.log('Response contains errors!');
// Extract error messages
const errorMatches = responseBody.match(/text-red-600[^>]*>([^<]+)/g);
if (errorMatches) {
errorMatches.forEach((match: string) => console.log('Error:', match));
}
}
// Check if modal is open
const modalVisible = await page.locator('#modal-holder[x-show="open"]').isVisible();
console.log('Modal visible:', modalVisible);
});

View File

@@ -1,130 +0,0 @@
import { test, expect } from '@playwright/test';
async function openEditModal(page: any) {
await page.goto('/transaction2');
await page.waitForSelector('table tbody tr');
const editButton = page.locator('button[hx-get*="/transaction2/"][hx-get*="/edit"]').first();
await editButton.click();
await page.waitForSelector('#modal-holder[x-show="open"]', { state: 'visible' });
await page.waitForSelector('#wizardmodal');
await page.click('button:has-text("Transaction Actions")');
await page.waitForSelector('text=Transaction Actions', { state: 'visible' });
await page.click('button:has-text("Manual")');
await page.waitForSelector('#account-grid-body');
}
async function addNewAccount(page: any) {
await page.click('text=New account');
await page.waitForTimeout(500);
}
async function selectAccountFromTypeahead(page: any, rowIndex: number, accountName: string) {
const allRows = page.locator('#account-grid-body tbody tr');
const rowCount = await allRows.count();
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}`);
}
const hiddenInput = accountRow.locator('input[type="hidden"][name*="transaction-account/account"]').first();
const testInfoResponse = await page.request.get('/test-info');
const testInfo = await testInfoResponse.json();
const accountKey = accountName === 'Test' ? 'test-account' : 'second-account';
const accountId = testInfo.accounts[accountKey];
await hiddenInput.evaluate((el: HTMLInputElement, value: string) => {
el.value = value;
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';
}
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());
await page.waitForTimeout(300);
}
async function setAccountAmount(page: any, rowIndex: number, amount: string) {
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) {
const amountInput = row.locator('input[name*="transaction-account/amount"]').first();
await amountInput.fill(amount);
await amountInput.dispatchEvent('change');
await page.waitForTimeout(300);
return;
}
accountRowIndex++;
}
}
throw new Error(`Could not find account row at index ${rowIndex}`);
}
async function toggleToPercentMode(page: any) {
const percentRadio = page.locator('input[name="step-params[amount-mode]"][value="%"]');
await percentRadio.click();
await page.waitForResponse(response =>
response.url().includes('/toggle-amount-mode') && response.status() === 200
);
await page.waitForTimeout(200);
}
async function saveTransaction(page: any) {
await page.click('button:has-text("Done")');
await page.waitForSelector('#modal-holder[x-show="open"]', { state: 'hidden', timeout: 10000 });
}
test('debug step 2', async ({ page }) => {
// Step 1
await openEditModal(page);
await toggleToPercentMode(page);
await addNewAccount(page);
await selectAccountFromTypeahead(page, 0, 'Test');
await setAccountAmount(page, 0, '100');
await saveTransaction(page);
console.log('Step 1 complete');
// Step 2: Re-open
await openEditModal(page);
await toggleToPercentMode(page);
// Debug: check what's in the grid
const allRows = page.locator('#account-grid-body tbody tr');
const rowCount = await allRows.count();
console.log('Total rows in grid:', rowCount);
for (let i = 0; i < rowCount; i++) {
const row = allRows.nth(i);
const hasAccountInput = await row.locator('input[name*="transaction-account/account"]').count() > 0;
const text = await row.textContent();
console.log(`Row ${i}: hasAccount=${hasAccountInput}, text=${text?.substring(0, 50)}`);
}
});

View File

@@ -1,85 +0,0 @@
import { test, expect } from '@playwright/test';
test('debug typeahead', async ({ page }) => {
// Navigate to transactions page
await page.goto('/transaction2');
await page.waitForSelector('table tbody tr');
// Click edit
const editButton = page.locator('button[hx-get*="/transaction2/"][hx-get*="/edit"]').first();
await editButton.click();
await page.waitForSelector('#modal-holder[x-show="open"]', { state: 'visible' });
await page.click('button:has-text("Transaction Actions")');
await page.waitForSelector('text=Transaction Actions', { state: 'visible' });
await page.click('button:has-text("Manual")');
await page.waitForSelector('#account-grid-body');
// Switch to percentage mode
await page.locator('input[name="step-params[amount-mode]"][value="%"]').click();
await page.waitForResponse(response =>
response.url().includes('/toggle-amount-mode') && response.status() === 200
);
await page.waitForTimeout(200);
// Click New account
await page.click('text=New account');
await page.waitForTimeout(500);
// Find the typeahead
const row = page.locator('#account-grid-body tbody tr').nth(0);
const typeaheadContainer = row.locator('div.relative[x-data*="baseUrl"]').first();
const typeaheadTrigger = typeaheadContainer.locator('a[x-ref="input"]').first();
// Click to open dropdown
await typeaheadTrigger.click();
await page.waitForTimeout(500);
// Take a screenshot to see what's on screen
await page.screenshot({ path: '/tmp/typeahead-debug.png' });
// Find all tippy dropdowns
const tippies = page.locator('div[id^="tippy"]');
console.log('Number of tippy elements:', await tippies.count());
// Get HTML of first tippy
if (await tippies.count() > 0) {
const html = await tippies.first().innerHTML();
console.log('First tippy HTML:', html.substring(0, 1000));
}
// Try to find search input
const searchInputs = page.locator('input[type="text"]');
console.log('Number of text inputs:', await searchInputs.count());
// Find visible text inputs
for (let i = 0; i < Math.min(await searchInputs.count(), 10); i++) {
const input = searchInputs.nth(i);
const isVisible = await input.isVisible().catch(() => false);
console.log(`Input ${i}: visible=${isVisible}`);
}
// Type in the search input
const searchInput = page.locator('div[id^="tippy"] input[type="text"]').first();
await searchInput.fill('Test');
await page.waitForTimeout(1000);
// Take another screenshot
await page.screenshot({ path: '/tmp/typeahead-debug-2.png' });
// Check for dropdown options
const options = page.locator('div[id^="tippy"] .dropdown-options a');
console.log('Number of dropdown options:', await options.count());
// Get HTML of dropdown options
if (await options.count() > 0) {
for (let i = 0; i < Math.min(await options.count(), 3); i++) {
const html = await options.nth(i).innerHTML();
console.log(`Option ${i}:`, html);
}
}
// Also check if there are any li elements
const lis = page.locator('div[id^="tippy"] li');
console.log('Number of li elements:', await lis.count());
});

View File

@@ -1,117 +0,0 @@
import { test, expect } from '@playwright/test';
async function openEditModal(page: any) {
await page.goto('/transaction2');
await page.waitForSelector('table tbody tr');
const editButton = page.locator('button[hx-get*="/transaction2/"][hx-get*="/edit"]').first();
await editButton.click();
await page.waitForSelector('#modal-holder[x-show="open"]', { state: 'visible' });
await page.waitForSelector('#wizardmodal');
await page.click('button:has-text("Transaction Actions")');
await page.waitForSelector('text=Transaction Actions', { state: 'visible' });
await page.click('button:has-text("Manual")');
await page.waitForSelector('#account-grid-body');
}
async function addNewAccount(page: any) {
await page.click('text=New account');
await page.waitForTimeout(500);
}
async function selectAccountFromTypeahead(page: any, rowIndex: number, accountName: string) {
const allRows = page.locator('#account-grid-body tbody tr');
const rowCount = await allRows.count();
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}`);
}
const hiddenInput = accountRow.locator('input[type="hidden"][name*="transaction-account/account"]').first();
const testInfoResponse = await page.request.get('/test-info');
const testInfo = await testInfoResponse.json();
const accountKey = accountName === 'Test' ? 'test-account' : 'second-account';
const accountId = testInfo.accounts[accountKey];
await hiddenInput.evaluate((el: HTMLInputElement, value: string) => {
el.value = value;
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';
}
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());
await page.waitForTimeout(300);
}
async function setAccountAmount(page: any, rowIndex: number, amount: string) {
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) {
const amountInput = row.locator('input[name*="transaction-account/amount"]').first();
await amountInput.fill(amount);
await amountInput.dispatchEvent('change');
await page.waitForTimeout(300);
return;
}
accountRowIndex++;
}
}
throw new Error(`Could not find account row at index ${rowIndex}`);
}
test('debug workflow step 1', async ({ page }) => {
// Step 1
await openEditModal(page);
await page.locator('input[name="step-params[amount-mode]"][value="%"]').click();
await page.waitForResponse(response =>
response.url().includes('/toggle-amount-mode') && response.status() === 200
);
await page.waitForTimeout(200);
await addNewAccount(page);
await selectAccountFromTypeahead(page, 0, 'Test');
await setAccountAmount(page, 0, '100');
console.log('Before save - modal visible:', await page.locator('#modal-holder[x-show="open"]').isVisible());
// Save
await page.click('button:has-text("Done")');
await page.waitForTimeout(3000);
console.log('After save - modal visible:', await page.locator('#modal-holder[x-show="open"]').isVisible());
// Step 2: Re-open
await page.waitForSelector('table tbody tr');
const editButton = page.locator('button[hx-get*="/transaction2/"][hx-get*="/edit"]').first();
await editButton.click();
await page.waitForTimeout(2000);
console.log('After re-open click - modal visible:', await page.locator('#modal-holder[x-show="open"]').isVisible());
});