import { test, expect } from '@playwright/test'; import { login, navigateTo, 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 }) => { await navigateTo(page, '/invoices'); await page.waitForLoadState('networkidle'); // Click "New Invoice" (use first() for strict mode) const newBtn = page.getByRole('link', { name: /new invoice/i }).first() .or(page.getByRole('button', { name: /new invoice/i }).first()); await newBtn.first().click(); // Step 1: Client Info await page.waitForURL(`**/${PORT_SLUG}/invoices/new**`, { timeout: 10_000 }); await page.fill('#clientName', 'Invoice Test Client'); 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); // Step 2: Line Items — add 3 items 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'); // Verify subtotal appears (53800 formatted per locale) await expect(page.getByText(/53[,.]?800/).first()).toBeVisible({ timeout: 5_000 }); // Click Next to Review await page.getByRole('button', { name: /next/i }).click(); await page.waitForTimeout(1000); // Step 3: Review — verify summary await expect(page.getByText('Invoice Test Client')).toBeVisible(); await expect(page.getByText(/53[,.]?800/).first()).toBeVisible(); // Submit await page.getByRole('button', { name: /create invoice/i }).click(); // Should redirect to invoice detail or list 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); } } }); });