import { test, expect } from '@playwright/test'; // Acceptance spec for the New/Edit Client wizard — the largest SSR modal in the app: // seven linear steps (info → matches → contact → bank-accounts → integrations → cash-flow → // other-settings) PLUS a parameterized bank-account sub-editor reached from the // bank-accounts step. Migrated onto the session-backed engine (wizard2): flat de-cursored // field names (client/name, not step-params[client/name]), whole-form HTMX swaps, and the // bank-account add/edit/sort modeled as whole-form swaps of #wizard-form. // // The seed (test_server.clj) exposes client "Test Client" (code TEST, location DT) which // owns one "Test Checking" (TEST-CHK, a checking account) bank account. test.beforeEach(async ({ request }) => { await request.post('/test-reset'); }); // The grid lazy-loads its rows into #entity-table after the page renders. async function openClientList(page: any) { await page.goto('/admin/client'); await page.waitForSelector('#entity-table tbody tr[data-id]'); } async function openNewClient(page: any) { await openClientList(page); await page.locator('button:has-text("New Client")').first().click(); await page.waitForSelector('#wizard-form'); await page.waitForTimeout(400); } async function openEditTestClient(page: any) { await openClientList(page); await page.locator('#entity-table tbody tr', { hasText: 'Test Client' }).first() .locator('[hx-get*="/edit"]').first().click(); await page.waitForSelector('#wizard-form'); await page.waitForTimeout(400); } // Advance one step: click the data-primary Next button and wait until the whole-form swap // has actually changed the current-step hidden (the timeline lists every step name, so a // text check can't confirm progress — the hidden value can). async function advance(page: any) { const before = await page.locator('#wizard-form input[name="current-step"]').first().inputValue(); await page.locator('#wizard-form button[data-primary]').first().click(); await page.waitForFunction( (prev: string) => { const el = document.querySelector('#wizard-form input[name="current-step"]') as HTMLInputElement | null; return !!el && el.value !== prev; }, before, { timeout: 6000 }); } test.describe.configure({ mode: 'serial' }); test.describe('Client wizard (acceptance)', () => { test('new dialog renders the info step with the 7-step timeline', async ({ page }) => { await openNewClient(page); const form = page.locator('#wizard-form'); await expect(form.locator('input[name="client/name"]')).toBeVisible(); await expect(form.locator('input[name="client/code"]').first()).toBeVisible(); await expect(form).toContainText('Locations'); for (const label of ['Info', 'Matches', 'Contact', 'Bank Accounts', 'Integrations', 'Cash Flow', 'Other Settings']) { await expect(form).toContainText(label); } }); test('edit opens prefilled with the name and a disabled code', async ({ page }) => { await openEditTestClient(page); const form = page.locator('#wizard-form'); await expect(form.locator('input[name="client/name"]')).toHaveValue('Test Client'); // the visible code input is disabled on edit (a hidden twin carries the value on submit) const code = form.locator('input[name="client/code"]').first(); await expect(code).toHaveValue('TEST'); await expect(code).toBeDisabled(); }); test('bank-accounts step shows the seeded account card and a new-account affordance', async ({ page }) => { await openEditTestClient(page); await advance(page); // info -> matches await advance(page); // matches -> contact await advance(page); // contact -> bank-accounts const form = page.locator('#wizard-form'); await expect(form).toContainText('Bank Accounts'); await expect(form).toContainText('Test Checking'); await expect(form).toContainText('Add a new'); }); test('opening the bank-account editor swaps in the per-account form', async ({ page }) => { await openEditTestClient(page); await advance(page); await advance(page); await advance(page); // -> bank-accounts // click the pencil on the seeded account card to open its editor await page.locator('#wizard-form [hx-get*="/bank-account/edit"]').first().click(); await page.waitForTimeout(450); const form = page.locator('#wizard-form'); await expect(form.locator('input[name="bank-account/name"]')).toHaveValue('Test Checking'); await expect(form).toContainText('Accept'); // discard returns to the list await page.locator('#wizard-form [hx-get*="/bank-account/discard"]').first().click(); await page.waitForTimeout(450); await expect(form).toContainText('Test Checking'); }); test('accepting a bank-account edit merges the change back into the card', async ({ page }) => { await openEditTestClient(page); await advance(page); await advance(page); await advance(page); // -> bank-accounts await page.locator('#wizard-form [hx-get*="/bank-account/edit"]').first().click(); await page.waitForTimeout(450); const form = page.locator('#wizard-form'); await form.locator('input[name="bank-account/name"]').fill('Renamed Checking'); await form.locator('button[data-primary]:has-text("Accept")').first().click(); await page.waitForTimeout(450); // back on the list, the card now shows the new nickname await expect(form).toContainText('Renamed Checking'); }); test('editing through to the last step and saving keeps the client in the grid', async ({ page }) => { await openEditTestClient(page); const nameInput = page.locator('#wizard-form input[name="client/name"]'); await nameInput.fill('Test Client RENAMED'); await expect(nameInput).toHaveValue('Test Client RENAMED'); // info -> matches -> contact -> bank-accounts -> integrations -> cash-flow -> other-settings for (let i = 0; i < 6; i++) await advance(page); // the last step is the only one with a Feature Flags grid — confirm we really got here await expect(page.locator('#wizard-form')).toContainText('Feature Flags'); // Save persists the edit; reload the grid and the rename is there await page.locator('#wizard-form button[data-primary]').first().click(); await page.waitForTimeout(1200); await openClientList(page); await expect(page.locator('#entity-table')).toContainText('Test Client RENAMED'); }); });