Locks in the sub-editor → session → save-client! round-trip the earlier blank-address fix unblocked: edit the seeded account's nickname via the bank-account sub-editor, Accept, save the client, reopen, walk back to the bank-accounts step, and confirm the new nickname is shown and the old one is gone (renamed in place, not duplicated). Full e2e suite 72/72. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
152 lines
7.9 KiB
TypeScript
152 lines
7.9 KiB
TypeScript
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');
|
|
});
|
|
|
|
test('a bank-account edit persists through save and is shown on reopen', async ({ page }) => {
|
|
// edit the seeded account's nickname via the sub-editor, then save the whole client
|
|
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);
|
|
await page.locator('#wizard-form input[name="bank-account/name"]').fill('Persisted Checking');
|
|
await page.locator('#wizard-form button[data-primary]:has-text("Accept")').first().click();
|
|
await page.waitForTimeout(450);
|
|
await expect(page.locator('#wizard-form')).toContainText('Persisted Checking');
|
|
// bank-accounts -> integrations -> cash-flow -> other-settings, then Save
|
|
await advance(page); await advance(page); await advance(page);
|
|
await expect(page.locator('#wizard-form')).toContainText('Feature Flags');
|
|
await page.locator('#wizard-form button[data-primary]').first().click();
|
|
await page.waitForTimeout(1200);
|
|
// reopen the client and walk back to the bank-accounts step: the new nickname is there,
|
|
// and the seeded "Test Checking" name is gone (it was renamed, not duplicated)
|
|
await openEditTestClient(page);
|
|
await advance(page); await advance(page); await advance(page); // -> bank-accounts
|
|
const form = page.locator('#wizard-form');
|
|
await expect(form).toContainText('Persisted Checking');
|
|
await expect(form).not.toContainText('Test Checking');
|
|
});
|
|
});
|