test(ssr): Phase 6b parity gate — characterization spec for Transaction Rule wizard
Behavior-parity safety net before migrating the Transaction Rule modal onto the session-backed wizard engine. The modal had no e2e coverage; the test server seeded no rules. - test_server.clj: seed a transaction rule (under client TEST2, in a SEPARATE transaction so the first transaction's tempid->entity-id allocation — and thus the TEST transaction grid order the other specs depend on — is byte-identical); surface its id via /test-info (ruleId). - e2e/transaction-rule.spec.ts (4 tests): the new-rule edit step renders (description, account grid, approval radios, Test control), the edit dialog pre-populates the seeded rule, advancing to the test step renders the matching-transactions preview, and saving from the test step creates the rule + closes the modal. Covers both entry points (new/edit), both steps (edit + test), and save. Note: deliberately NOT seeding a recent matching transaction — a date-NOW txn perturbs an unrelated transaction-edit save spec (pre-existing fragility), and the test-table query/render is reused unchanged by the migration, so characterizing that the preview renders is sufficient parity. Full Playwright suite 55/55 (51 prior + 4 new). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
126
e2e/transaction-rule.spec.ts
Normal file
126
e2e/transaction-rule.spec.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
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
|
||||
await page.locator('#wizard-form button:has-text("Save"), #wizard-form button[type="submit"]').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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user