test(ssr): Phase 4 parity gate — seed + characterization spec for Sales Summary edit
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 <noreply@anthropic.com>
This commit is contained in:
126
e2e/sales-summary-edit.spec.ts
Normal file
126
e2e/sales-summary-edit.spec.ts
Normal file
@@ -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/<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 (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);
|
||||
});
|
||||
});
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user