test(ssr): Phase 7 parity gate — characterization spec for the Invoice Pay wizard

The Invoice Pay wizard is the first GENUINE multi-data-step wizard: choose-method
(collects bank-account + method) -> payment-details (collects check-number /
handwritten-date / amounts), merged only at submit. This gate characterizes that
flow before migrating it onto the session-backed engine, so the merge can be proven
behavior-preserving.

- Seed: make the TEST client's check bank account visible (+ name "Test Checking")
  so the choose-method step renders a usable method card. The pay flow had no e2e
  coverage, so the bank account was never visible in tests before.
- Spec drives the real 2-step flow against the unmodified wizard: choose-method
  renders the bank account + its methods (print-check/debit/handwrite-check, in the
  card tooltip); picking handwrite-check advances to payment-details (check-number +
  date + Pay); filling the check number and submitting shows the completion modal.
  The handwrite-check path is used because it transacts a pending check payment
  directly (no PDF/S3), making the success assertion stable.

Notes for the migration: the method controls live in a <template x-ref="tooltip">
revealed by the card button; the footer Pay submit is x-ref="next"; both the grid
filters and the modal carry a check-number input, so the modal selectors are scoped
to #wizard-form.

Verification: invoice-pay spec 3/3; full suite 58/58 (no regressions from the seed
change).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-25 17:17:57 -07:00
parent a2d8517668
commit 01aca9d362
2 changed files with 75 additions and 1 deletions

73
e2e/invoice-pay.spec.ts Normal file
View File

@@ -0,0 +1,73 @@
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 submit button (x-ref="next"), not the background #pay-button
await page.locator('[x-ref="next"]').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');
});
});