refactor(ssr): collapse 5 manual-coding operation routes into edit-form-changed (heuristic 6)

The 5 manual-coding operations (vendor change, simple/advanced toggle, add row, remove
row, $/% toggle) each had their own route + handler, all doing "mutate form state ->
render-full-form". Fold them into the single edit-form-changed endpoint, which now
dispatches on an `op` form-param to the relevant pure apply-* mutation fn (apply-vendor-
changed / apply-toggle-mode / apply-new-account / apply-remove-account /
apply-toggle-amount-mode) then re-renders. A missing/unknown op (a plain dependent-field
change, e.g. account->location or amount->totals) just re-renders, as before.

- edit.clj: 6 handlers -> 1 dispatcher + 5 pure apply-* fns; markup posts to
  edit-form-changed with :hx-vals {:op "..."}.
- routes/transactions.cljc: remove the 5 now-unused route keys.
- e2e specs: retarget the vendor selector by op (div[hx-vals*="vendor-changed"]) and
  point the toggle-amount-mode / vendor response waits at edit-form-changed, since the
  old per-op route names are gone. (Behavioral assertions unchanged.)

Scorecard: manual-coding routes ~10 -> ~5 (operations now one dispatcher). Parity held:
swap spec 6/6, full suite 32 pass (Shared Location green; no new regression).
This commit is contained in:
2026-06-03 07:07:52 -07:00
parent 1d5a95196f
commit 0f5650b73e
5 changed files with 63 additions and 79 deletions

View File

@@ -48,7 +48,7 @@ async function openManualAdvanced(page: any, transactionIndex = 0) {
// (Solr is unavailable in tests), click it, and wait for the whole-form swap.
async function selectVendor(page: any, vendorId: number, label: string) {
const vendor = page
.locator('div[hx-post*="edit-vendor-changed"]')
.locator('div[hx-vals*="vendor-changed"]')
.first()
.locator('div.relative[x-data]')
.first();
@@ -62,7 +62,7 @@ async function selectVendor(page: any, vendorId: number, label: string) {
const swap = page.waitForResponse(
(r: any) =>
r.url().includes('edit-vendor-changed') &&
r.url().includes('edit-form-changed') &&
r.request().method() === 'POST' &&
r.status() === 200
);
@@ -303,7 +303,7 @@ test.describe('Transaction Edit whole-form swap', () => {
await page.locator('button[hx-get*="/transaction2/"][hx-get*="/edit"]').nth(0).click();
await page.waitForSelector('#wizardmodal');
await page.click('button:has-text("Manual")');
await page.waitForSelector('div[hx-post*="edit-vendor-changed"]');
await page.waitForSelector('div[hx-vals*="vendor-changed"]');
const testInfo = await (await page.request.get('/test-info')).json();
const vendorId: number = testInfo.accounts.vendor;
@@ -311,7 +311,7 @@ test.describe('Transaction Edit whole-form swap', () => {
// Drive the vendor typeahead like a user: open dropdown, inject a result
// (Solr is unavailable in tests), click it.
const vendor = page.locator('div[hx-post*="edit-vendor-changed"]').first().locator('div.relative[x-data]').first();
const vendor = page.locator('div[hx-vals*="vendor-changed"]').first().locator('div.relative[x-data]').first();
await vendor.locator('a[x-ref="input"]').click();
const search = page.locator('[data-tippy-root] input[x-model="search"]').first();
await search.waitFor({ state: 'visible' });
@@ -322,7 +322,7 @@ test.describe('Transaction Edit whole-form swap', () => {
const swap = page.waitForResponse(
(r: any) =>
r.url().includes('edit-vendor-changed') &&
r.url().includes('edit-form-changed') &&
r.request().method() === 'POST' &&
r.status() === 200
);
@@ -352,7 +352,7 @@ test.describe('Transaction Edit whole-form swap', () => {
await page.locator('button[hx-get*="/transaction2/"][hx-get*="/edit"]').nth(0).click();
await page.waitForSelector('#wizardmodal');
await page.click('button:has-text("Manual")');
await page.waitForSelector('div[hx-post*="edit-vendor-changed"]');
await page.waitForSelector('div[hx-vals*="vendor-changed"]');
const testInfo = await (await page.request.get('/test-info')).json();
const vendor1: number = testInfo.accounts.vendor;
@@ -361,7 +361,7 @@ test.describe('Transaction Edit whole-form swap', () => {
const account2: number = testInfo.accounts['second-account'];
const vendorLabel = page
.locator('div[hx-post*="edit-vendor-changed"] span[x-text="value.label"]')
.locator('div[hx-vals*="vendor-changed"] span[x-text="value.label"]')
.first();
const accountHidden = page
.locator('input[type="hidden"][name*="transaction-account/account"]')

View File

@@ -150,7 +150,7 @@ async function toggleToPercentMode(page: any) {
// Wait for HTMX to swap the grid body
await page.waitForResponse(response =>
response.url().includes('/toggle-amount-mode') && response.status() === 200
response.url().includes('/edit-form-changed') && response.status() === 200
);
await page.waitForTimeout(200);
}
@@ -161,7 +161,7 @@ async function toggleToDollarMode(page: any) {
// Wait for HTMX to swap the grid body
await page.waitForResponse(response =>
response.url().includes('/toggle-amount-mode') && response.status() === 200
response.url().includes('/edit-form-changed') && response.status() === 200
);
await page.waitForTimeout(200);
}
@@ -359,7 +359,7 @@ async function selectVendorFromTypeahead(page: any, vendorName: string) {
throw new Error(`Could not find vendor with name ${vendorName}`);
}
const vendorContainer = page.locator('div[hx-post*="edit-vendor-changed"]').first();
const vendorContainer = page.locator('div[hx-vals*="vendor-changed"]').first();
const vendorHidden = vendorContainer.locator('input[type="hidden"]').first();
await vendorHidden.evaluate((el: HTMLInputElement, value: string) => {
@@ -374,7 +374,7 @@ async function selectVendorFromTypeahead(page: any, vendorName: string) {
el.dispatchEvent(new Event('change', { bubbles: true }));
});
await page.waitForResponse(response => response.url().includes('/edit-vendor-changed') && response.status() === 200);
await page.waitForResponse(response => response.url().includes('/edit-form-changed') && response.status() === 200);
await page.waitForTimeout(500);
}
@@ -434,11 +434,11 @@ test.describe('Transaction Edit Vendor Pre-population', () => {
// `elements` instead of being fetched. Everything else -- the dropdown's own
// search input firing a native `change` on blur, the `value = element` click
// handler, the Alpine reactivity, and the HTMX round-trip to
// `edit-vendor-changed` -- runs exactly as in production. This is the flow that
// `edit-form-changed` (op=vendor-changed) -- runs exactly as in production. This is the flow that
// regressed: a stale native `change` from the search input used to win the race
// and revert the vendor to its previous value.
async function selectVendorViaDropdown(page: any, vendorId: number, vendorName: string) {
const wrapper = page.locator('div[hx-post*="edit-vendor-changed"]').first();
const wrapper = page.locator('div[hx-vals*="vendor-changed"]').first();
const typeahead = wrapper.locator('div.relative[x-data]').first();
// Open the dropdown (tippy renders the popper into [data-tippy-root]).
@@ -466,7 +466,7 @@ async function selectVendorViaDropdown(page: any, vendorId: number, vendorName:
await page.waitForResponse(
(response: any) =>
response.url().includes('/edit-vendor-changed') && response.status() === 200
response.url().includes('/edit-form-changed') && response.status() === 200
);
await page.waitForTimeout(500);
}
@@ -485,7 +485,7 @@ async function openManualVendorSection(page: any, transactionIndex: number) {
await page.waitForSelector('#modal-holder[x-show="open"]', { state: 'visible' });
await page.waitForSelector('#wizardmodal');
await page.click('button:has-text("Manual")');
await page.waitForSelector('div[hx-post*="edit-vendor-changed"]');
await page.waitForSelector('div[hx-vals*="vendor-changed"]');
}
test.describe('Transaction Edit Vendor Selection', () => {
@@ -501,14 +501,14 @@ test.describe('Transaction Edit Vendor Selection', () => {
// round-trip. Before the fix this reverted to blank because a stale
// `change` event submitted the previous vendor and its response won.
const label = page
.locator('div[hx-post*="edit-vendor-changed"] span[x-text="value.label"]')
.locator('div[hx-vals*="vendor-changed"] span[x-text="value.label"]')
.first();
await expect(label).toHaveText('Test Vendor');
// The server-rendered hidden input must carry the newly selected vendor id.
const hidden = page
.locator(
'div[hx-post*="edit-vendor-changed"] input[type="hidden"][name="step-params[transaction/vendor]"]'
'div[hx-vals*="vendor-changed"] input[type="hidden"][name="step-params[transaction/vendor]"]'
)
.first();
await expect(hidden).toHaveValue(vendorId.toString());