Migrates the POS Sales Summary edit modal off the wizard to a plain Selmer form, building on the parity gate committed earlier. Largest migration so far and the first with no prior test coverage. What changed - Wizard removed: deleted MainStep/EditWizard records, MultiStepFormState, the step-params[...] prefix, the EDN snapshot round-trip, and all mm/* middleware. Replaced with a plain handler + flat wrap-decode/wrap-derive-state. The 51 fc/ cursor refs are de-cursored into explicit data + Selmer templates. - db/id-keyed item merge: wrap-derive-state overlays posted items onto the persisted items by :db/id, so read-only fields the form doesn't post (ledger-side, amount) survive a re-render and the debit/credit split + totals stay correct. New manual rows (temp db/id) ride through as-is. - Inline click-to-edit account cell preserved as three small targeted .account-cell-swap routes (edit/save/cancel-item-account), ported to Selmer with the new field-name scheme. - 100% Selmer modal render path (the remaining Hiccup / hx-swap-oob / "hx-" strings are all grid-page code — grid render lambdas, the filters form, and the submit response-header map — not the modal). - Routes: dropped edit-wizard-navigate + new-summary-item; added form-changed. Fixes (two pre-existing bugs, per request) - "New Summary Item" add button (was throwing `newRowIndex is not defined` and targeting a non-existent `.new-row`) is now a whole-form-swap op=new-item that adds an editable manual row (category + account typeahead + debit/credit money inputs + remove). - The dead totals/balance display (malformed Hiccup that discarded its labels) is replaced by a proper #summary-totals block showing running Total + Balanced/Unbalanced, refreshed via a Rule-4 targeted swap on manual amount edits. Scorecard delta (pos/sales_summaries.clj): LOC 790->732, mm coupling 20->0, wizard records 4->0, fc/ cursor 51->0, step-params 27->0 (2 comments), modal routes 8->6. (hx-swap-oob 1 and mixed-hx live in the grid page, not the modal.) Verification: sales-summary spec 7/7 (incl. the two fixes); full Playwright suite 46/46; cljfmt clean. Skill fed: scorecard row + narrative; gotchas (parity-gate- first, characterize-then-fix, keyup-trigger tests); cookbook (inline click-to-edit cell, db/id-keyed item merge). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
142 lines
7.3 KiB
TypeScript
142 lines
7.3 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
|
|
// Characterization spec for the POS Sales Summary 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 openEditModal(page: any) {
|
|
await page.setExtraHTTPHeaders({ 'x-clients': '"mine"' });
|
|
await page.goto('/pos/summaries');
|
|
await page.waitForSelector('#entity-table tbody tr');
|
|
// The row's edit button is an hx-get to /pos/summaries/<id> (the edit-wizard route).
|
|
await page.locator('#entity-table tbody tr').first()
|
|
.locator('a[hx-get], button[hx-get]').first().click();
|
|
await page.waitForSelector('#wizardmodal');
|
|
}
|
|
|
|
test.describe.configure({ mode: 'serial' });
|
|
|
|
test.describe('Sales Summary Edit (characterization)', () => {
|
|
test('opens the edit modal with debit/credit columns, categories, accounts and amounts', async ({ page }) => {
|
|
await openEditModal(page);
|
|
|
|
const modal = page.locator('#wizardmodal');
|
|
await expect(modal).toContainText('Edit Summary');
|
|
await expect(modal).toContainText('Debits');
|
|
await expect(modal).toContainText('Credits');
|
|
// seeded items
|
|
await expect(modal).toContainText('Cash Deposit'); // debit item category
|
|
await expect(modal).toContainText('Food Sales'); // credit item category
|
|
// resolved account names (account-display-cell pulls the account name)
|
|
await expect(modal).toContainText('Second Account'); // debit item account
|
|
await expect(modal).toContainText('Test Account'); // credit item account
|
|
// amounts render
|
|
await expect(modal).toContainText('$500.00');
|
|
|
|
// two account cells, each with an inline-edit pencil
|
|
expect(await modal.locator('.account-cell').count()).toBe(2);
|
|
expect(await modal.locator('[hx-get*="edit/item-account"]').count()).toBe(2);
|
|
});
|
|
|
|
test('seeded summary is balanced (shows Balanced totals, no out-of-balance)', async ({ page }) => {
|
|
await openEditModal(page);
|
|
const modal = page.locator('#wizardmodal');
|
|
// balanced: $500 debit == $500 credit -> the (now-fixed) totals block shows Balanced
|
|
await expect(modal.locator('#summary-totals')).toContainText('Balanced');
|
|
await expect(modal.locator('#summary-totals')).toContainText('$500.00');
|
|
await expect(modal).not.toContainText('Unbalanced');
|
|
});
|
|
|
|
test('inline account edit: pencil opens the typeahead editor; cancel restores the display', async ({ page }) => {
|
|
await openEditModal(page);
|
|
const modal = page.locator('#wizardmodal');
|
|
|
|
// The debit row shows "Second Account" with a pencil. Click it -> account-edit-cell.
|
|
const debitCell = modal.locator('.account-cell', { hasText: 'Second Account' }).first();
|
|
await debitCell.locator('[hx-get*="edit/item-account"]').click();
|
|
|
|
// edit cell: a typeahead plus check (save) + cancel buttons
|
|
const editCell = modal.locator('.account-cell').filter({ has: page.locator('[hx-put*="save-item-account"]') }).first();
|
|
await expect(editCell.locator('[hx-put*="save-item-account"]')).toBeVisible();
|
|
await expect(editCell.locator('[hx-get*="cancel-item-account"]')).toBeVisible();
|
|
|
|
// Cancel -> back to display mode showing the original account
|
|
await editCell.locator('[hx-get*="cancel-item-account"]').click();
|
|
await page.waitForTimeout(300);
|
|
await expect(modal.locator('.account-cell', { hasText: 'Second Account' }).first()).toBeVisible();
|
|
// back in display mode: the pencil (edit) is shown again
|
|
await expect(modal.locator('.account-cell', { hasText: 'Second Account' })
|
|
.first().locator('[hx-get*="edit/item-account"]')).toBeVisible();
|
|
});
|
|
|
|
test('inline account edit: save (check) re-renders the account display cell', async ({ page }) => {
|
|
await openEditModal(page);
|
|
const modal = page.locator('#wizardmodal');
|
|
|
|
const creditCell = modal.locator('.account-cell', { hasText: 'Test Account' }).first();
|
|
await creditCell.locator('[hx-get*="edit/item-account"]').click();
|
|
|
|
const editCell = modal.locator('.account-cell').filter({ has: page.locator('[hx-put*="save-item-account"]') }).first();
|
|
await expect(editCell.locator('[hx-put*="save-item-account"]')).toBeVisible();
|
|
// Save without changing -> display cell re-renders, account preserved, pencil back.
|
|
await editCell.locator('[hx-put*="save-item-account"]').click();
|
|
await page.waitForTimeout(300);
|
|
const display = modal.locator('.account-cell', { hasText: 'Test Account' }).first();
|
|
await expect(display).toBeVisible();
|
|
await expect(display.locator('[hx-get*="edit/item-account"]')).toBeVisible();
|
|
});
|
|
|
|
// The "New Summary Item" button was broken in the pre-migration wizard (its Alpine
|
|
// handler threw "newRowIndex is not defined"); the migration fixes it as a whole-form
|
|
// swap (op=new-item). This asserts the FIXED behavior: clicking adds an editable manual
|
|
// row (category + account typeahead + debit/credit money inputs).
|
|
test('New Summary Item adds an editable manual row (fixed)', async ({ page }) => {
|
|
await openEditModal(page);
|
|
const modal = page.locator('#wizardmodal');
|
|
expect(await modal.locator('.manual-item-row').count()).toBe(0);
|
|
|
|
await modal.locator('a[hx-vals*="new-item"]').click();
|
|
await expect(modal.locator('.manual-item-row').first()).toBeVisible();
|
|
expect(await modal.locator('.manual-item-row').count()).toBe(1);
|
|
|
|
const row = modal.locator('.manual-item-row').first();
|
|
await expect(row.locator('input[placeholder="Category/Explanation"]')).toBeVisible();
|
|
expect(await row.locator('input[name*="[debit]"]').count()).toBe(1);
|
|
expect(await row.locator('input[name*="[credit]"]').count()).toBe(1);
|
|
});
|
|
|
|
test('a manual debit amount recomputes the totals to Unbalanced (fixed)', async ({ page }) => {
|
|
await openEditModal(page);
|
|
const modal = page.locator('#wizardmodal');
|
|
await expect(modal.locator('#summary-totals')).toContainText('Balanced');
|
|
await modal.locator('a[hx-vals*="new-item"]').click();
|
|
await expect(modal.locator('.manual-item-row').first()).toBeVisible();
|
|
// adding a $500 debit -> $1000 debit vs $500 credit -> the totals block recomputes
|
|
const debit = modal.locator('.manual-item-row input[name*="[debit]"]').first();
|
|
await debit.click();
|
|
await debit.pressSequentially('500'); // fires keyup -> hx-trigger "keyup changed delay:300ms"
|
|
await expect(modal.locator('#summary-totals')).toContainText('Unbalanced', { timeout: 5000 });
|
|
});
|
|
|
|
test('Save closes the modal and the summary stays in the grid', async ({ page }) => {
|
|
await openEditModal(page);
|
|
const modal = page.locator('#wizardmodal');
|
|
// submit the form via the Save button; the PUT swaps the grid row + fires modalclose
|
|
const putResp = page.waitForResponse(r =>
|
|
r.url().includes('/pos/summaries') && r.request().method() === 'PUT');
|
|
await modal.locator('button[type="submit"]').click();
|
|
expect((await putResp).status()).toBe(200);
|
|
// modalclose hides the modal (it is hidden, not removed from the DOM)
|
|
await expect(modal).toBeHidden({ timeout: 5000 });
|
|
// the grid still shows the summary row
|
|
await expect(page.locator('#entity-table tbody tr')).toHaveCount(1);
|
|
});
|
|
});
|