From a289ff255743b4f5dc5775fbc35f25fa7b48e7af Mon Sep 17 00:00:00 2001 From: Bryce Date: Wed, 24 Jun 2026 21:15:28 -0700 Subject: [PATCH] =?UTF-8?q?test(ssr):=20Phase=204=20parity=20gate=20?= =?UTF-8?q?=E2=80=94=20seed=20+=20characterization=20spec=20for=20Sales=20?= =?UTF-8?q?Summary=20edit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Establishes the behavior-parity safety net required before migrating the POS Sales Summary edit modal off the wizard (the modal had zero test coverage and the test server seeded no POS data). - test_server.clj: seed a balanced sales summary ($500 credit = $500 debit) with two auto items referencing the existing test client + accounts; surface its id via /test-info (`salesSummaryId`). - e2e/sales-summary-edit.spec.ts: characterization spec (6 tests) capturing current behavior — open modal (debit/credit columns, categories, resolved account names, amounts), balanced state, inline account editor (pencil -> typeahead editor -> cancel restores / save re-renders the cell), and Save (PUT round-trip closes the modal + keeps the grid row). Exercises the edit-wizard, edit/save/cancel-item-account, and edit-wizard-submit routes. Notable finding: the "New Summary Item" button is currently BROKEN (its Alpine handler throws "newRowIndex is not defined" and hx-target="closest .new-row" matches no ancestor, so the new-summary-item route never fires). The spec documents this as inert rather than asserting it works; the migration will decide fix-vs-preserve. Full Playwright suite 45/45 (39 prior + 6 new). Co-Authored-By: Claude Opus 4.8 --- e2e/sales-summary-edit.spec.ts | 126 +++++++++++++++++++++++++++++++ test/clj/auto_ap/test_server.clj | 24 +++++- 2 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 e2e/sales-summary-edit.spec.ts diff --git a/e2e/sales-summary-edit.spec.ts b/e2e/sales-summary-edit.spec.ts new file mode 100644 index 00000000..55e2fd57 --- /dev/null +++ b/e2e/sales-summary-edit.spec.ts @@ -0,0 +1,126 @@ +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/ (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 (no out-of-balance indicator)', async ({ page }) => { + await openEditModal(page); + const modal = page.locator('#wizardmodal'); + // balanced: $500 debit == $500 credit, so no unbalanced warning text + await expect(modal).not.toContainText('Out of balance'); + 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(); + }); + + // NOTE: the "New Summary Item" button is currently BROKEN in this modal and is + // therefore intentionally not characterized as working. Its Alpine handler + // (`@click="$dispatch('newRow', {index: (newRowIndex++)})"`) throws + // "newRowIndex is not defined" (the modal's x-data is empty), and even if it + // fired, `hx-target="closest .new-row"` matches no ancestor in this div layout, + // so the `new-summary-item` route never fires. We assert the *current* behavior: + // the button is present but adds nothing. The migration may fix or preserve this. + test('New Summary Item button is present but currently inert (documents the break)', async ({ page }) => { + await openEditModal(page); + const modal = page.locator('#wizardmodal'); + const before = await modal.locator('input').count(); + await modal.locator('[hx-get*="sales-summary-item"]').first().click(); + await page.waitForTimeout(500); + // no manual row was added (no category input appeared, input count unchanged) + expect(await modal.locator('input[placeholder="Category/Explanation"]').count()).toBe(0); + expect(await modal.locator('input').count()).toBe(before); + }); + + 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); + }); +}); diff --git a/test/clj/auto_ap/test_server.clj b/test/clj/auto_ap/test_server.clj index f15f0b47..15c03a2d 100644 --- a/test/clj/auto_ap/test_server.clj +++ b/test/clj/auto_ap/test_server.clj @@ -29,6 +29,7 @@ (def test-transaction-id (atom nil)) (def test-account-ids (atom {})) (def test-client-ids (atom {})) +(def test-sales-summary-id (atom nil)) (defn admin-identity [] (case @test-identity-mode @@ -160,7 +161,26 @@ :invoice/invoice-number "UNPAID-001" :invoice/expense-accounts [{:invoice-expense-account/account "account-id" :invoice-expense-account/amount 150.0 - :invoice-expense-account/location "DT"}])]) + :invoice-expense-account/location "DT"}]) + ;; Sales summary for the POS sales-summary edit modal e2e + ;; (balanced: $500 credit = $500 debit). + {:db/id "sales-summary-id" + :sales-summary/client "client-id" + :sales-summary/date #inst "2026-06-20T00:00:00Z" + :sales-summary/items [{:db/id "ss-item-credit" + :sales-summary-item/category "Food Sales" + :sales-summary-item/sort-order 0 + :sales-summary-item/manual? false + :ledger-mapped/ledger-side :ledger-side/credit + :ledger-mapped/amount 500.0 + :ledger-mapped/account "account-id"} + {:db/id "ss-item-debit" + :sales-summary-item/category "Cash Deposit" + :sales-summary-item/sort-order 1 + :sales-summary-item/manual? false + :ledger-mapped/ledger-side :ledger-side/debit + :ledger-mapped/amount 500.0 + :ledger-mapped/account "account-id-2"}]}]) tempids (:tempids tx-result) tx-entity-id (get tempids "transaction-id")] (println "Test transaction entity ID:" tx-entity-id) @@ -174,6 +194,7 @@ (reset! test-client-ids {:test (get tempids "client-id") :test2 (get tempids "client-id-2")}) + (reset! test-sales-summary-id (get tempids "sales-summary-id")) tx-entity-id)) (defn test-info-handler [request] @@ -183,6 +204,7 @@ {:transactionId @test-transaction-id :accounts @test-account-ids :clientMode @test-identity-mode + :salesSummaryId @test-sales-summary-id :clients (mapv :client/code (:clients request))})}) (defn test-set-client-mode-handler [request]