improvements
This commit is contained in:
111
e2e/debug-exact.spec.ts
Normal file
111
e2e/debug-exact.spec.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
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);
|
||||
});
|
||||
151
e2e/debug-save.spec.ts
Normal file
151
e2e/debug-save.spec.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
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);
|
||||
});
|
||||
130
e2e/debug-step2.spec.ts
Normal file
130
e2e/debug-step2.spec.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
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)}`);
|
||||
}
|
||||
});
|
||||
85
e2e/debug-typeahead.spec.ts
Normal file
85
e2e/debug-typeahead.spec.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
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());
|
||||
});
|
||||
117
e2e/debug-workflow.spec.ts
Normal file
117
e2e/debug-workflow.spec.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
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());
|
||||
});
|
||||
283
e2e/transaction-edit.spec.ts
Normal file
283
e2e/transaction-edit.spec.ts
Normal file
@@ -0,0 +1,283 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user