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>
74 lines
3.8 KiB
TypeScript
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');
|
|
});
|
|
});
|