Proves the Phase-6a wizard engine against a real 2-step modal: the Transaction Rule wizard (edit step + read-only test/preview step) now runs on wizard2 / wizard-state, fully de-cursored. What changed - Wizard machinery removed: deleted the EditModal / TestModal / TransactionRuleWizard defrecords (mm/ModalWizardStep + LinearModalWizard), MultiStepFormState, the EDN snapshot, and the step-params[...] prefix. Replaced with a data-driven `transaction-rule-wizard-config` (two steps + init-fn + done-fn) driven by the engine. - De-cursored the whole edit form (82 fc/ refs -> 0): every field reads explicit data + path->name2; errors via a bound *errors* / ferr. The account row's Alpine cross-field dispatch wiring (clientId -> accountId -> location) is preserved verbatim — only the data plumbing moved off the cursor. - The test step's :render reads :all-data (the engine's get-all), so the formtools "combine at the end" mechanism feeds the preview table. - Routes 4 -> 2: open-rule-wizard (new + edit), save-step (every transition via the engine's `direction` field). The dedicated `navigate` route is deleted. - decode-rule-form select-keys to the schema's known keys so the engine's nav fields (wizard-id/current-step/direction) don't leak into the upserted entity. Scorecard (admin/transaction_rules.clj): fc/ 82->0, mm/ 20->0, defrecords 3->0, LOC 1000->964, routes 4->2. Scope note: the de-cursored edit step keeps com/* Hiccup leaf components (not yet sc/* Selmer); the value here was removing fc/ + mm/ and proving the engine, not re-templating the conditional/Alpine-cross-field layout. Hiccup-in-render is a documented partial; the com/ -> sc/ swap is a mechanical follow-up. Verification: rule spec 4/4 (new + edit dialogs, advance-to-test preview, save); full Playwright suite 55/55; cljfmt clean. Skill fed: scorecard row + narrative (engine's first real modal; generalizes for a one-data-step wizard); gotchas (strip engine nav fields in decode, new-row temp-id, direction-button nav). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
127 lines
5.7 KiB
TypeScript
127 lines
5.7 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
|
|
// Characterization spec for the Transaction Rule wizard (edit step + test/preview step).
|
|
// Captures CURRENT (pre-migration) behavior so the migration onto the session-backed
|
|
// wizard engine can be proven behavior-preserving. Reset the dataset before each test.
|
|
test.beforeEach(async ({ request }) => {
|
|
await request.post('/test-reset');
|
|
});
|
|
|
|
async function getTestInfo(page: any) {
|
|
return (await page.request.get('/test-info')).json();
|
|
}
|
|
|
|
async function navigateToRules(page: any) {
|
|
// The rule fixtures live under client TEST2 (to stay out of the single-client TEST
|
|
// transaction grid the other specs use), so view as an admin who sees all clients.
|
|
await page.request.get('/test-set-client-mode?mode=multi-client');
|
|
await page.setExtraHTTPHeaders({ 'x-clients': '"all"' });
|
|
await page.goto('/admin/transaction-rule');
|
|
await page.waitForSelector('#entity-table');
|
|
}
|
|
|
|
async function openNewDialog(page: any) {
|
|
await page.locator('button:has-text("New Transaction Rule")').first().click();
|
|
await page.waitForSelector('#wizard-form');
|
|
}
|
|
|
|
async function openEditDialog(page: any) {
|
|
// the edit pencil on the seeded rule's row (hx-get .../<id>/edit)
|
|
await page.locator('#entity-table tbody tr').first()
|
|
.locator('[hx-get*="/edit"]').first().click();
|
|
await page.waitForSelector('#wizard-form');
|
|
}
|
|
|
|
// Add a valid account-coding row (Solr typeahead unavailable in tests, so inject the
|
|
// account id into the row's hidden input), location Shared, percentage 100.
|
|
async function addAccount(page: any, accountId: string) {
|
|
await page.locator('#wizard-form a:has-text("New account")').first().click();
|
|
await page.waitForTimeout(400);
|
|
const hidden = page.locator('#wizard-form input[type="hidden"][name*="[transaction-rule-account/account]"]').first();
|
|
await hidden.evaluate((el: HTMLInputElement, v: string) => {
|
|
const n = document.createElement('input'); n.type = 'hidden'; n.name = el.name; n.value = v;
|
|
el.parentNode!.replaceChild(n, el);
|
|
}, accountId);
|
|
await page.waitForTimeout(200);
|
|
const loc = page.locator('#wizard-form select[name*="[transaction-rule-account/location]"]').first();
|
|
if (await loc.count() > 0) await loc.selectOption('Shared').catch(() => {});
|
|
const pct = page.locator('#wizard-form input[name*="[transaction-rule-account/percentage]"]').first();
|
|
await pct.fill('100');
|
|
await pct.dispatchEvent('change');
|
|
await page.waitForTimeout(200);
|
|
}
|
|
|
|
async function fillDescription(page: any, desc: string) {
|
|
await page.locator('#wizard-form input[name="transaction-rule/description"]').first().fill(desc);
|
|
}
|
|
|
|
// Approval status is required to advance/save; the radio-card's first option is "Approved".
|
|
async function selectApproved(page: any) {
|
|
const radio = page.locator('#wizard-form input[type="radio"][name*="transaction-approval-status"]').first();
|
|
await radio.check({ force: true }).catch(async () => {
|
|
await page.locator('#wizard-form label:has-text("Approved")').first().click();
|
|
});
|
|
await page.waitForTimeout(100);
|
|
}
|
|
|
|
async function clickTest(page: any) {
|
|
// the footer "Test" button navigates edit -> test
|
|
await page.locator('#wizard-form button:has-text("Test"), #wizard-form a:has-text("Test")').first().click();
|
|
await page.waitForTimeout(800);
|
|
}
|
|
|
|
test.describe.configure({ mode: 'serial' });
|
|
|
|
test.describe('Transaction Rule wizard (characterization)', () => {
|
|
test('New dialog opens the edit step with rule form + account grid', async ({ page }) => {
|
|
await navigateToRules(page);
|
|
await openNewDialog(page);
|
|
|
|
const modal = page.locator('#wizard-form');
|
|
await expect(modal).toContainText('Description');
|
|
await expect(modal).toContainText('Outcomes');
|
|
await expect(modal).toContainText('New account');
|
|
await expect(modal).toContainText('Approval status');
|
|
// the step indicator + the Test (advance) control
|
|
await expect(modal.locator('button:has-text("Test"), a:has-text("Test")').first()).toBeVisible();
|
|
});
|
|
|
|
test('Edit dialog pre-populates the seeded rule', async ({ page }) => {
|
|
await navigateToRules(page);
|
|
await openEditDialog(page);
|
|
const desc = page.locator('#wizard-form input[name="transaction-rule/description"]').first();
|
|
await expect(desc).toHaveValue('ZZRULEMATCH');
|
|
});
|
|
|
|
test('advancing to the test step renders the matching-transactions preview', async ({ page }) => {
|
|
const info = await getTestInfo(page);
|
|
await navigateToRules(page);
|
|
await openNewDialog(page);
|
|
await fillDescription(page, 'ZZRULEMATCH');
|
|
await addAccount(page, info.accounts['test-account'].toString());
|
|
await selectApproved(page);
|
|
await clickTest(page);
|
|
// the wizard advances to the test/preview step (the test-table query + render is
|
|
// reused unchanged by the migration; the seed has no recent match, so the count is 0)
|
|
const modal = page.locator('#wizard-form');
|
|
await expect(modal).toContainText('Matching transactions');
|
|
});
|
|
|
|
test('Saving from the test step creates the rule and closes the modal', async ({ page }) => {
|
|
const info = await getTestInfo(page);
|
|
await navigateToRules(page);
|
|
const before = await page.locator('#entity-table tbody tr').count();
|
|
await openNewDialog(page);
|
|
await fillDescription(page, 'ZZRULEMATCH');
|
|
await addAccount(page, info.accounts['test-account'].toString());
|
|
await selectApproved(page);
|
|
await clickTest(page);
|
|
// Save from the test step (the precise Save button, not Back which is also submit)
|
|
await page.locator('#wizard-form button:has-text("Save")').first().click();
|
|
await page.waitForTimeout(1000);
|
|
// modal closed + a new rule row added
|
|
await expect(page.locator('#wizard-form')).toBeHidden();
|
|
expect(await page.locator('#entity-table tbody tr').count()).toBe(before + 1);
|
|
});
|
|
});
|