Files
integreat/e2e/invoice-pay.spec.ts
Bryce 4b2a3e53dd refactor(ssr): Phase 7 — migrate Invoice Pay onto the engine; prove the cross-step merge
Invoice Pay is the first GENUINE multi-data-step wizard, and migrating it exercises
the engine's central abstraction for the first time: choose-method collects
{:bank-account :method}, payment-details collects {:invoices :check-number
:handwritten-date :mode}, and the engine's get-all MERGES the two independent step
payloads for the per-method pay (handwrite-check transacts a pending check; the
others go through print-checks-internal). This is exactly the mechanism the Phase-6
adversarial review flagged as unproven.

What changed
- Deleted the 3 wizard records (PayWizard / ChoosePaymentMethodModal /
  PaymentDetailsStep), MultiStepFormState, the EDN snapshot, and the step-params[...]
  prefix. Replaced with pay-wizard-config (init-fn builds read-only :context;
  two steps; done-fn = pay!) driven by wizard2.
- De-cursored the payment-details amounts grid (fc/cursor-map -> explicit
  (map-indexed) over :context :invoices with path->name2 names).
- The bank-account cards' method controls now post {bank-account, method,
  direction:next} straight to the engine submit-route (was a bespoke navigate route).
- Routes 3 -> 2: open-pay-wizard (GET), pay-step (every transition); the
  pay-wizard-navigate route is deleted.
- Used the post-review engine primitives: :open-response (modal wrap), nav-footer
  (with new :save-label "Pay"), auto nav-field stripping (flat decode, no allowlist),
  Enter guard.

invoices.clj falls fully off the framework: Invoice Pay was the last mm/fc user
(bulk-edit went in Phase 5), so fc/ 0, mm/ 0, defrecord 0, step-params 0 — and the
multi-modal / form-cursor / malli.util requires are removed.

Gotcha discovered + documented: wizard session data must be EDN-safe (the cookie
session store has no clj-time readers), so the date default is computed in render,
not stored in context.

Verification: invoice-pay spec 3/3 (the merge end-to-end); full suite 58/58; load-file
clean; cljfmt clean. Skill fed: scorecard row (merge proven; whole-file zeroing) +
the EDN-session-safety gotcha.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:59:24 -07:00

74 lines
3.8 KiB
TypeScript

import { test, expect } from '@playwright/test';
// Characterization spec for the Invoice Pay wizard (the first genuine multi-data-step
// wizard: choose-method -> payment-details, merged at submit). Captures CURRENT
// (pre-migration) behavior so the migration onto the session-backed engine can be proven
// behavior-preserving. The seed's lone unpaid invoice (UNPAID-001, Test Vendor, $150,
// client TEST) is payable; its client has one visible check bank account (Test Checking).
test.beforeEach(async ({ request }) => { await request.post('/test-reset'); });
// Select the unpaid invoice on the grid and open the pay wizard (choose-method step).
async function openPayWizard(page: any) {
await page.setExtraHTTPHeaders({ 'x-clients': '"mine"' });
await page.goto('/invoice');
await page.waitForSelector('#entity-table tbody tr');
await page.locator('#entity-table tbody input[type="checkbox"]').first().click();
await page.waitForTimeout(300);
// #pay-button's container hx-gets /invoice/pay on click; wait for the wizard to land.
await page.locator('#pay-button').first().click();
await page.waitForTimeout(900);
}
// The bank-account card's method options (print-check / debit / handwrite-check) live in a
// <template x-ref="tooltip"> revealed by clicking the card's tooltip button; open it.
async function openMethodTooltip(page: any) {
await page.locator('button[x-ref="button"]').first().click();
await page.waitForTimeout(400);
}
// Advance choose-method -> payment-details by picking a method (each is an hx-put to
// .../pay/navigate?to=:payment-details carrying step-params[method]).
async function pickMethod(page: any, method: string) {
await openMethodTooltip(page);
await page.locator(`[hx-vals*="${method}"]`).first().click();
await page.waitForTimeout(900);
}
test.describe.configure({ mode: 'serial' });
test.describe('Invoice Pay wizard (characterization)', () => {
test('choose-method step renders the bank account and its payment methods', async ({ page }) => {
await openPayWizard(page);
const body = page.locator('body');
await expect(body).toContainText('Payment method');
await expect(body).toContainText('Test Checking');
// a check account offers print-check / debit / handwrite-check (in the card's tooltip)
await openMethodTooltip(page);
expect(await page.locator('[hx-vals*="handwrite-check"]').count()).toBeGreaterThan(0);
expect(await page.locator('[hx-vals*="print-check"]').count()).toBeGreaterThan(0);
});
test('picking handwrite-check advances to the payment-details step', async ({ page }) => {
await openPayWizard(page);
await pickMethod(page, 'handwrite-check');
const body = page.locator('body');
await expect(body).toContainText('Check number'); // handwrite-check-only field
await expect(body).toContainText('Date'); // check date
await expect(body.locator('button:has-text("Pay"), a:has-text("Pay")').first()).toBeVisible();
});
test('completing a handwritten-check payment shows the success modal', async ({ page }) => {
await openPayWizard(page);
await pickMethod(page, 'handwrite-check');
// step 2 collects the check number; method (step 1) + check-number (step 2) combine at submit
// scope to the wizard form (the background grid filters also have a check-number input)
await page.locator('#wizard-form input[name*="check-number"]').first().fill('10001');
await page.waitForTimeout(150);
// the footer Pay submit button, scoped to the form (not the background #pay-button)
await page.locator('#wizard-form button:has-text("Pay")').first().click();
await page.waitForTimeout(1500);
// the submit transacts a pending check payment and swaps in the completion modal
await expect(page.locator('body')).toContainText('payment is complete');
});
});