import { test, expect } from '@playwright/test'; import { login, navigateTo, apiHeaders, PORT_SLUG } from './helpers'; test.describe('Invoicing', () => { test.beforeEach(async ({ page }) => { await login(page, 'super_admin'); }); test('navigate to invoices page', async ({ page }) => { await navigateTo(page, '/invoices'); await page.waitForLoadState('networkidle'); const heading = page.getByText(/invoices/i).first(); await expect(heading).toBeVisible({ timeout: 10_000 }); }); test('create a new invoice with 3 line items', async ({ page }) => { // Seed a client via API to pick in the billing-entity picker. const clientName = `Invoice Test Client ${Date.now()}`; const createRes = await page.request.post('/api/v1/clients', { headers: await apiHeaders(page), data: { fullName: clientName, contacts: [{ channel: 'email', value: 'billing@test.com', isPrimary: true }], }, }); expect(createRes.ok(), `client create returned ${createRes.status()}`).toBe(true); await navigateTo(page, '/invoices'); await page.waitForLoadState('networkidle'); const newBtn = page .getByRole('link', { name: /new invoice/i }) .first() .or(page.getByRole('button', { name: /new invoice/i }).first()); await newBtn.first().click(); await page.waitForURL(`**/${PORT_SLUG}/invoices/new**`, { timeout: 10_000 }); // Step 1: pick the client in the OwnerPicker. The trigger renders as a // button with role="combobox" and the placeholder "Select owner..." while // empty. const ownerTrigger = page.locator('button[role="combobox"]:has-text("Select owner")').first(); await expect(ownerTrigger).toBeVisible({ timeout: 5_000 }); await ownerTrigger.click(); const searchInput = page.getByPlaceholder(/search clients/i); await expect(searchInput).toBeVisible({ timeout: 5_000 }); await searchInput.fill(clientName); await page.waitForTimeout(500); // let the debounced query fire await page.getByRole('option', { name: clientName }).first().click(); await page.fill('#billingEmail', 'billing@test.com'); const dueDate = new Date(); dueDate.setDate(dueDate.getDate() + 30); await page.fill('#dueDate', dueDate.toISOString().split('T')[0]!); await page.getByRole('button', { name: /next/i }).click(); await page.waitForTimeout(1000); for (let i = 0; i < 3; i++) { await page.getByRole('button', { name: /add line item/i }).click(); await page.waitForTimeout(300); } await page.locator('input[name="lineItems.0.description"]').fill('Berth Rental - Annual'); await page.locator('input[name="lineItems.0.quantity"]').fill('1'); await page.locator('input[name="lineItems.0.unitPrice"]').fill('50000'); await page.locator('input[name="lineItems.1.description"]').fill('Utilities Package'); await page.locator('input[name="lineItems.1.quantity"]').fill('12'); await page.locator('input[name="lineItems.1.unitPrice"]').fill('150'); await page.locator('input[name="lineItems.2.description"]').fill('Maintenance Fee'); await page.locator('input[name="lineItems.2.quantity"]').fill('4'); await page.locator('input[name="lineItems.2.unitPrice"]').fill('500'); await expect(page.getByText(/53[,.]?800/).first()).toBeVisible({ timeout: 5_000 }); await page.getByRole('button', { name: /^next$/i }).click(); const createBtn = page.getByRole('button', { name: /create invoice/i }); await expect(createBtn).toBeVisible({ timeout: 10_000 }); await expect(page.getByText(/53[,.]?800/).first()).toBeVisible(); await createBtn.click(); await page.waitForURL( (url) => url.pathname.includes('/invoices') && !url.pathname.includes('/new'), { timeout: 15_000 }, ); }); test('invoice shows as Draft', async ({ page }) => { await navigateTo(page, '/invoices'); await page.waitForLoadState('networkidle'); await page.waitForTimeout(2000); await expect(page.getByText(/draft/i).first()).toBeVisible({ timeout: 10_000 }); }); test('invoice detail page loads', async ({ page }) => { await navigateTo(page, '/invoices'); await page.waitForLoadState('networkidle'); await page.waitForTimeout(2000); // Click into the first invoice const invoiceLink = page.locator('table a, table [role="link"]').first(); if (await invoiceLink.isVisible({ timeout: 5_000 }).catch(() => false)) { await invoiceLink.click(); await page.waitForTimeout(3000); const url = page.url(); expect(url.includes('/invoices/')).toBeTruthy(); } else { // Try clicking the first row const row = page.locator('table tbody tr').first(); if (await row.isVisible({ timeout: 3_000 }).catch(() => false)) { await row.click(); await page.waitForTimeout(3000); } } }); });