Files
integreat/e2e/invoice-bulk-edit.spec.ts
Bryce 4139919036 test(ssr): Phase 5 parity gate — characterization spec for Invoice Bulk Edit
Behavior-parity safety net before migrating the Invoice Bulk Edit modal off the
wizard. The modal had no e2e coverage; the existing seeded invoice is bulk-editable
as-is, so no seed change was needed (avoids interfering with the transaction-link
spec).

e2e/invoice-bulk-edit.spec.ts (4 tests): open the modal (expense-account grid with
Account/Location/%/TOTAL/BALANCE + a default row + New account), add an account row,
save a 100% coding (modalclose), and the percentage-validation rejection. Models the
bulk-code-transactions spec.

Full Playwright suite 50/50 (46 prior + 4 new).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 22:50:44 -07:00

129 lines
5.1 KiB
TypeScript

import { test, expect } from '@playwright/test';
// Characterization spec for the Invoice Bulk Edit modal. Captures CURRENT
// (pre-migration) behavior so the wizard -> plain-Selmer migration can be proven
// behavior-preserving. Reset the shared dataset before each test for isolation.
test.beforeEach(async ({ request }) => {
await request.post('/test-reset');
});
async function getTestInfo(page: any) {
return (await page.request.get('/test-info')).json();
}
async function navigateToInvoices(page: any) {
await page.setExtraHTTPHeaders({ 'x-clients': '"mine"' });
await page.goto('/invoice');
await page.waitForSelector('#entity-table tbody tr');
}
async function selectFirstInvoice(page: any) {
await page.locator('#entity-table tbody input[type="checkbox"]').first().click();
await page.waitForTimeout(200);
}
async function openBulkEditModal(page: any) {
await page.locator('button:has-text("Bulk Edit")').first().click();
await page.waitForSelector('#wizardmodal');
}
async function addNewAccount(page: any) {
await page.locator('a:has-text("New account")').first().click();
await page.waitForTimeout(500);
}
// Set an account on a row by replacing its Alpine-managed hidden input with a plain
// one (Solr-backed typeahead is unavailable in tests), then dispatching the location
// reload -- the same approach the bulk-code spec uses.
async function setRowAccount(page: any, rowIndex: number, accountId: string) {
const rows = page.locator('#wizard-form tbody tr');
const row = rows.nth(rowIndex);
const hidden = row.locator('input[type="hidden"][name*="[account]"]').first();
await hidden.evaluate((el: HTMLInputElement, value: string) => {
const n = document.createElement('input');
n.type = 'hidden'; n.name = el.name; n.value = value;
el.parentNode!.replaceChild(n, el);
}, accountId);
await page.waitForTimeout(200);
const loc = row.locator('[x-dispatch\\:changed]').first();
if (await loc.count() > 0) {
await loc.evaluate((el: HTMLElement) => el.dispatchEvent(new CustomEvent('changed', { bubbles: true })));
await page.waitForTimeout(400);
}
}
async function setRowPercentage(page: any, rowIndex: number, pct: string) {
const row = page.locator('#wizard-form tbody tr').nth(rowIndex);
const input = row.locator('input.amount-field, input[name*="percentage"]').first();
await input.fill(pct);
await input.dispatchEvent('change');
await page.waitForTimeout(200);
}
async function submitForm(page: any) {
await page.locator('#wizard-form').evaluate((f: HTMLFormElement) =>
f.dispatchEvent(new Event('submit', { bubbles: true })));
}
test.describe.configure({ mode: 'serial' });
test.describe('Invoice Bulk Edit (characterization)', () => {
test('opens the modal with the expense-account grid', async ({ page }) => {
await navigateToInvoices(page);
await selectFirstInvoice(page);
await openBulkEditModal(page);
const modal = page.locator('#wizardmodal');
await expect(modal).toContainText('Bulk editing 1 invoices');
await expect(modal).toContainText('Account');
await expect(modal).toContainText('Location');
await expect(modal).toContainText('TOTAL');
await expect(modal).toContainText('BALANCE');
// a default expense-account row is present, plus the New account button
expect(await modal.locator('input[name*="[expense-accounts]"][name*="[account]"]').count()).toBeGreaterThanOrEqual(1);
await expect(modal.locator('a:has-text("New account")')).toBeVisible();
});
test('New account adds an expense-account row', async ({ page }) => {
await navigateToInvoices(page);
await selectFirstInvoice(page);
await openBulkEditModal(page);
const accountRows = () => page.locator('#wizard-form input[name*="[expense-accounts]"][name*="[account]"]');
const before = await accountRows().count();
await addNewAccount(page);
expect(await accountRows().count()).toBe(before + 1);
});
test('saving a 100% account coding closes the modal', async ({ page }) => {
const info = await getTestInfo(page);
await navigateToInvoices(page);
await selectFirstInvoice(page);
await openBulkEditModal(page);
// the default row is at 100% already; set its account and save
await setRowAccount(page, 0, info.accounts['test-account'].toString());
await setRowPercentage(page, 0, '100');
await submitForm(page);
// a successful save fires modalclose -> the modal closes
await expect(page.locator('#wizardmodal')).toBeHidden({ timeout: 8000 });
});
test('rejects when account percentages do not total 100%', async ({ page }) => {
const info = await getTestInfo(page);
await navigateToInvoices(page);
await selectFirstInvoice(page);
await openBulkEditModal(page);
await setRowAccount(page, 0, info.accounts['test-account'].toString());
await setRowPercentage(page, 0, '50');
await submitForm(page);
await page.waitForTimeout(1000);
// modal stays open on validation failure
await expect(page.locator('#modal-holder[x-show="open"]')).toBeVisible();
await expect(page.locator('#wizardmodal')).toContainText('does not equal 100%');
});
});