diff --git a/e2e/transaction-rule.spec.ts b/e2e/transaction-rule.spec.ts new file mode 100644 index 00000000..091c24e5 --- /dev/null +++ b/e2e/transaction-rule.spec.ts @@ -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 ...//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); + }); +}); diff --git a/test/clj/auto_ap/test_server.clj b/test/clj/auto_ap/test_server.clj index 15c03a2d..ec683a54 100644 --- a/test/clj/auto_ap/test_server.clj +++ b/test/clj/auto_ap/test_server.clj @@ -30,6 +30,7 @@ (def test-account-ids (atom {})) (def test-client-ids (atom {})) (def test-sales-summary-id (atom nil)) +(def test-rule-id (atom nil)) (defn admin-identity [] (case @test-identity-mode @@ -182,6 +183,25 @@ :ledger-mapped/amount 500.0 :ledger-mapped/account "account-id-2"}]}]) tempids (:tempids tx-result) + ;; A pre-existing transaction rule (for the wizard edit flow), in a SEPARATE + ;; transaction so the first one's tempid->entity-id allocation (and thus the TEST + ;; transaction grid order other specs depend on) is byte-identical to before. + ;; Under client TEST2 so it stays out of the single-client TEST views. We do NOT + ;; seed a recent matching transaction: a date-NOW txn perturbs an unrelated + ;; transaction-edit save spec, and the rule test step's query/render is reused + ;; unchanged by the migration, so characterizing that the preview table renders is + ;; sufficient parity (the specific match count is not what the migration risks). + rule-tx (:tempids + @(dc/transact conn + [{:db/id "rule-id" + :transaction-rule/client (get tempids "client-id-2") + :transaction-rule/description "ZZRULEMATCH" + :transaction-rule/note "ZZRULEMATCH" + :transaction-rule/transaction-approval-status :transaction-approval-status/approved + :transaction-rule/accounts [{:db/id "rule-acct" + :transaction-rule-account/account (get tempids "account-id") + :transaction-rule-account/location "Shared" + :transaction-rule-account/percentage 1.0}]}])) tx-entity-id (get tempids "transaction-id")] (println "Test transaction entity ID:" tx-entity-id) (reset! test-account-ids @@ -195,6 +215,7 @@ {:test (get tempids "client-id") :test2 (get tempids "client-id-2")}) (reset! test-sales-summary-id (get tempids "sales-summary-id")) + (reset! test-rule-id (get rule-tx "rule-id")) tx-entity-id)) (defn test-info-handler [request] @@ -205,6 +226,7 @@ :accounts @test-account-ids :clientMode @test-identity-mode :salesSummaryId @test-sales-summary-id + :ruleId @test-rule-id :clients (mapv :client/code (:clients request))})}) (defn test-set-client-mode-handler [request]