Files
integreat/e2e/sales-summary-edit.spec.ts
Bryce a289ff2557 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>
2026-06-24 21:15:28 -07:00

127 lines
6.5 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 (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);
});
});