import { test, expect } from '@playwright/test'; // Acceptance spec for the New Invoice wizard (basic-details -> [accounts] -> next-steps), // the dual-purpose new+edit wizard with a CONDITIONAL middle step: basic-details creates // straight away when customize-accounts = :default (the vendor's default expense account), // and routes through the expense-accounts grid when :customize. // // NOTE: the pre-migration `mm` flow's basic-details "Save" was broken in this harness (and // prod): the button PUT /invoice/new/navigate, whose `:to` query-schema 500s on empty // query-params (the {}->nil main-transformer quirk). So this is an ACCEPTANCE gate -- red on // the old code, green on the engine (whose submit is a POST with no query-schema). The seed // exposes client TEST + vendor "Test Vendor" (default account "Test Account") via /test-info. test.beforeEach(async ({ request }) => { await request.post('/test-reset'); }); async function seedIds(page: any): Promise<{ client: number; vendor: number }> { const info = await (await page.request.get('/test-info')).json(); return { client: info.clientIds.test, vendor: info.accounts.vendor }; } // Open the wizard from the invoice list (so htmx/alpine are present -- opening the modal // fragment directly would submit natively). async function openNewWizard(page: any) { await page.setExtraHTTPHeaders({ 'x-clients': '"mine"' }); await page.goto('/invoice'); await page.waitForSelector('#entity-table tbody tr'); await page.locator('button:has-text("New invoice")').first().click(); await page.waitForSelector('#wizard-form'); await page.waitForTimeout(400); } // The vendor field is a typeahead whose hidden input posts invoice/vendor; set it the way a // dropdown pick would land (the value the form submits). async function setVendor(page: any, vendorId: number) { await page.evaluate((id: number) => { const hidden = document.querySelector('input[name="invoice/vendor"]') as HTMLInputElement; hidden.value = String(id); hidden.dispatchEvent(new Event('change', { bubbles: true })); }, vendorId); } // The customize-accounts radio lives in an async fragment loaded on the "bryce" event (fired // when the Alpine vendorId changes). Trigger that htmx load explicitly after setting vendor. async function loadPrediction(page: any) { await page.evaluate(() => { const el = document.querySelector('#expense-account-prediction [hx-put], #expense-account-prediction[hx-put]'); // @ts-ignore if (el && window.htmx) window.htmx.trigger(el, 'bryce'); }); await page.waitForTimeout(600); } const save = (page: any) => page.locator('#wizard-form button:has-text("Save")').first().click(); test.describe.configure({ mode: 'serial' }); test.describe('New Invoice wizard (acceptance)', () => { test('basic-details renders the invoice fields', async ({ page }) => { await openNewWizard(page); const form = page.locator('#wizard-form'); await expect(form).toContainText('New invoice'); await expect(form).toContainText('Vendor'); await expect(form).toContainText('Date'); await expect(form).toContainText('Invoice Number'); await expect(form).toContainText('Total'); await expect(form.locator('input[name="invoice/total"]')).toBeVisible(); }); test('default-accounts path creates the invoice and offers to pay it now', async ({ page }) => { const { vendor } = await seedIds(page); await openNewWizard(page); await setVendor(page, vendor); await page.locator('input[name="invoice/invoice-number"]').fill('NEW-1001'); await page.locator('input[name="invoice/total"]').fill('212.44'); await page.waitForTimeout(200); await save(page); await page.waitForTimeout(1200); // the next-steps modal (done-fn output) -- no accounts step on the default path await expect(page.locator('body')).toContainText('Would you like to pay this invoice now?'); }); test('customize-accounts path routes through the expense-accounts grid then creates', async ({ page }) => { const { vendor } = await seedIds(page); await openNewWizard(page); await setVendor(page, vendor); await page.locator('input[name="invoice/invoice-number"]').fill('NEW-1002'); await page.locator('input[name="invoice/total"]').fill('300.00'); await loadPrediction(page); // pick "Customize accounts" (the radio in the async fragment) await page.locator('input[name="customize-accounts"][value="customize"]').first().check(); await page.waitForTimeout(150); await save(page); await page.waitForTimeout(1000); // the expense-accounts step: a grid prefilled with the vendor's default account + total const form = page.locator('#wizard-form'); await expect(form).toContainText('Invoice accounts'); await expect(form).toContainText('INVOICE TOTAL'); await save(page); // accounts -> done await page.waitForTimeout(1200); await expect(page.locator('body')).toContainText('Would you like to pay this invoice now?'); }); });