Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM, PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source files covering clients, berths, interests/pipeline, documents/EOI, expenses/invoices, email, notifications, dashboard, admin, and client portal. CI/CD via Gitea Actions with Docker builds. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
108
tests/e2e/smoke/05-invoices.spec.ts
Normal file
108
tests/e2e/smoke/05-invoices.spec.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user