fix(M3-M11): clients meta pagination, client detail types, sidebar icon imports, payments error state; add E2E specs for all modules
This commit is contained in:
31
e2e/audit-log.spec.ts
Normal file
31
e2e/audit-log.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { login } from './helpers/auth';
|
||||
|
||||
test.describe('Audit Log', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await login(page);
|
||||
await page.goto('/audit-log');
|
||||
});
|
||||
|
||||
test('audit log page renders', async ({ page }) => {
|
||||
await expect(page.locator('h1:has-text("Audit Log")')).toBeVisible();
|
||||
await expect(page.locator('table')).toBeVisible();
|
||||
});
|
||||
|
||||
test('audit log entries load', async ({ page }) => {
|
||||
await page.waitForTimeout(5000);
|
||||
const hasRows = await page.locator('tbody tr').count();
|
||||
const hasEmpty = await page.locator('text=No logs, text=No audit').isVisible().catch(() => false);
|
||||
expect(hasRows > 0 || hasEmpty).toBeTruthy();
|
||||
});
|
||||
|
||||
test('pagination controls work', async ({ page }) => {
|
||||
await page.waitForTimeout(3000);
|
||||
const nextBtn = page.locator('button:has-text("Next"), [aria-label="Next page"]').first();
|
||||
if (await nextBtn.isVisible() && !await nextBtn.isDisabled()) {
|
||||
await nextBtn.click();
|
||||
await page.waitForTimeout(1000);
|
||||
await expect(page.locator('h1:has-text("Audit Log")')).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
68
e2e/clients.spec.ts
Normal file
68
e2e/clients.spec.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { login } from './helpers/auth';
|
||||
|
||||
test.describe('Clients', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await login(page);
|
||||
await page.goto('/clients');
|
||||
});
|
||||
|
||||
test('clients list page renders', async ({ page }) => {
|
||||
await expect(page.locator('h1:has-text("Clients")')).toBeVisible();
|
||||
await expect(page.locator('table')).toBeVisible();
|
||||
});
|
||||
|
||||
test('client rows load and show data', async ({ page }) => {
|
||||
// Wait for loading to complete
|
||||
await page.waitForSelector('tr.border-b', { timeout: 15000 });
|
||||
const rows = page.locator('tbody tr.border-b');
|
||||
const count = await rows.count();
|
||||
expect(count).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('search filters clients', async ({ page }) => {
|
||||
await page.waitForSelector('tbody tr', { timeout: 10000 });
|
||||
const searchInput = page.locator('input[placeholder*="Search"]');
|
||||
await searchInput.fill('abc_no_match_xyz');
|
||||
await page.waitForTimeout(500);
|
||||
await expect(page.locator('text=No clients found')).toBeVisible();
|
||||
await searchInput.fill('');
|
||||
});
|
||||
|
||||
test('Add Client modal opens and closes', async ({ page }) => {
|
||||
await page.click('button:has-text("Add Client")');
|
||||
await expect(page.locator('text=Add New Client')).toBeVisible();
|
||||
await page.click('button:has-text("Cancel")');
|
||||
await expect(page.locator('text=Add New Client')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('Add Client form requires plan', async ({ page }) => {
|
||||
await page.click('button:has-text("Add Client")');
|
||||
// Create button should be disabled without required fields
|
||||
const createBtn = page.locator('button:has-text("Create Client")');
|
||||
await expect(createBtn).toBeDisabled();
|
||||
});
|
||||
|
||||
test('clicking a client row navigates to detail', async ({ page }) => {
|
||||
await page.waitForSelector('tbody tr.border-b', { timeout: 15000 });
|
||||
await page.locator('tbody tr.border-b').first().click();
|
||||
await expect(page).toHaveURL(/\/clients\/.+/);
|
||||
});
|
||||
|
||||
test('client detail page loads with tabs', async ({ page }) => {
|
||||
await page.waitForSelector('tbody tr.border-b', { timeout: 15000 });
|
||||
await page.locator('tbody tr.border-b').first().click();
|
||||
await expect(page.locator('button:has-text("Profile")')).toBeVisible();
|
||||
await expect(page.locator('button:has-text("Subscriptions")')).toBeVisible();
|
||||
await expect(page.locator('button:has-text("Invoices")')).toBeVisible();
|
||||
await expect(page.locator('button:has-text("Payments")')).toBeVisible();
|
||||
await expect(page.locator('button:has-text("Tickets")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('client detail — invoices tab loads', async ({ page }) => {
|
||||
await page.waitForSelector('tbody tr.border-b', { timeout: 15000 });
|
||||
await page.locator('tbody tr.border-b').first().click();
|
||||
await page.click('button:has-text("Invoices")');
|
||||
await expect(page.locator('table')).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
});
|
||||
42
e2e/invoices.spec.ts
Normal file
42
e2e/invoices.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { login } from './helpers/auth';
|
||||
|
||||
test.describe('Invoices', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await login(page);
|
||||
await page.goto('/invoices');
|
||||
});
|
||||
|
||||
test('invoices page renders', async ({ page }) => {
|
||||
await expect(page.locator('h1:has-text("Invoices")')).toBeVisible();
|
||||
await expect(page.locator('table')).toBeVisible();
|
||||
});
|
||||
|
||||
test('invoice rows load', async ({ page }) => {
|
||||
await page.waitForSelector('tbody tr', { timeout: 15000 });
|
||||
const rows = page.locator('tbody tr');
|
||||
const count = await rows.count();
|
||||
expect(count).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('status filter chips work', async ({ page }) => {
|
||||
await page.waitForSelector('tbody tr', { timeout: 10000 });
|
||||
// Status filter buttons should exist
|
||||
const filterBtn = page.locator('button:has-text("OVERDUE"), button:has-text("Overdue")').first();
|
||||
if (await filterBtn.isVisible()) {
|
||||
await filterBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
// Should filter without crashing
|
||||
await expect(page.locator('h1:has-text("Invoices")')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('clicking invoice row opens detail modal', async ({ page }) => {
|
||||
await page.waitForSelector('tbody tr', { timeout: 15000 });
|
||||
await page.locator('tbody tr').first().click();
|
||||
// Modal or detail should open
|
||||
await page.waitForTimeout(500);
|
||||
// Should show some invoice details
|
||||
await expect(page.locator('text=Invoice')).toBeVisible();
|
||||
});
|
||||
});
|
||||
31
e2e/leads.spec.ts
Normal file
31
e2e/leads.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { login } from './helpers/auth';
|
||||
|
||||
test.describe('Leads', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await login(page);
|
||||
await page.goto('/leads');
|
||||
});
|
||||
|
||||
test('leads page renders with pipeline summary', async ({ page }) => {
|
||||
await expect(page.locator('h1:has-text("Leads")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('leads list loads', async ({ page }) => {
|
||||
await page.waitForTimeout(2000);
|
||||
// Either shows rows or empty state
|
||||
const hasRows = await page.locator('tbody tr').count();
|
||||
const hasEmpty = await page.locator('text=No leads').isVisible().catch(() => false);
|
||||
expect(hasRows > 0 || hasEmpty).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Add Lead modal opens and closes', async ({ page }) => {
|
||||
const addBtn = page.locator('button:has-text("Add Lead")');
|
||||
if (await addBtn.isVisible()) {
|
||||
await addBtn.click();
|
||||
await page.waitForTimeout(300);
|
||||
await expect(page.locator('text=Add Lead, text=New Lead').first()).toBeVisible();
|
||||
await page.keyboard.press('Escape');
|
||||
}
|
||||
});
|
||||
});
|
||||
37
e2e/payments.spec.ts
Normal file
37
e2e/payments.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { login } from './helpers/auth';
|
||||
|
||||
test.describe('Payments', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await login(page);
|
||||
await page.goto('/payments');
|
||||
});
|
||||
|
||||
test('payments page renders', async ({ page }) => {
|
||||
await expect(page.locator('h1:has-text("Payments")')).toBeVisible();
|
||||
await expect(page.locator('table')).toBeVisible();
|
||||
});
|
||||
|
||||
test('payments load with amounts', async ({ page }) => {
|
||||
await page.waitForSelector('tbody tr', { timeout: 15000 });
|
||||
const rows = page.locator('tbody tr');
|
||||
const count = await rows.count();
|
||||
expect(count).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('channel filter buttons work', async ({ page }) => {
|
||||
const cashBtn = page.locator('button:has-text("CASH")').first();
|
||||
await cashBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
await expect(page.locator('h1:has-text("Payments")')).toBeVisible();
|
||||
// All filter works
|
||||
await page.locator('button:has-text("All")').click();
|
||||
});
|
||||
|
||||
test('clicking payment row opens detail modal', async ({ page }) => {
|
||||
await page.waitForSelector('tbody tr', { timeout: 15000 });
|
||||
await page.locator('tbody tr').first().click();
|
||||
await page.waitForTimeout(300);
|
||||
await expect(page.locator('text=Payment Details')).toBeVisible();
|
||||
});
|
||||
});
|
||||
30
e2e/remittances.spec.ts
Normal file
30
e2e/remittances.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { login } from './helpers/auth';
|
||||
|
||||
test.describe('Remittances', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await login(page);
|
||||
await page.goto('/remittances');
|
||||
});
|
||||
|
||||
test('remittances page renders', async ({ page }) => {
|
||||
await expect(page.locator('h1:has-text("Remittances")')).toBeVisible();
|
||||
await expect(page.locator('table')).toBeVisible();
|
||||
});
|
||||
|
||||
test('remittances load or show empty state', async ({ page }) => {
|
||||
await page.waitForTimeout(3000);
|
||||
const hasRows = await page.locator('tbody tr').count();
|
||||
const hasEmpty = await page.locator('text=No remittances').isVisible().catch(() => false);
|
||||
expect(hasRows > 0 || hasEmpty).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Submit Remittance button opens modal', async ({ page }) => {
|
||||
const btn = page.locator('button:has-text("Submit Remittance"), button:has-text("New Remittance")').first();
|
||||
if (await btn.isVisible()) {
|
||||
await btn.click();
|
||||
await page.waitForTimeout(300);
|
||||
await expect(page.locator('text=Submit Remittance, text=Remittance').first()).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
37
e2e/reports.spec.ts
Normal file
37
e2e/reports.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { login } from './helpers/auth';
|
||||
|
||||
test.describe('Reports', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await login(page);
|
||||
await page.goto('/reports');
|
||||
});
|
||||
|
||||
test('reports page renders', async ({ page }) => {
|
||||
await expect(page.locator('h1:has-text("Reports")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('KPI summary cards are visible', async ({ page }) => {
|
||||
await page.waitForTimeout(3000);
|
||||
// At least one KPI card should render
|
||||
const cards = page.locator('[class*="card"], .fiberops-card, [class*="Card"]');
|
||||
const count = await cards.count();
|
||||
expect(count).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('collection section renders', async ({ page }) => {
|
||||
await page.waitForTimeout(3000);
|
||||
const collectionEl = page.locator('text=Collection, text=Collector').first();
|
||||
await expect(collectionEl).toBeVisible({ timeout: 10000 }).catch(() => {});
|
||||
});
|
||||
|
||||
test('no crash on date range change', async ({ page }) => {
|
||||
await page.waitForTimeout(2000);
|
||||
const fromInput = page.locator('input[type="date"]').first();
|
||||
if (await fromInput.isVisible()) {
|
||||
await fromInput.fill('2026-01-01');
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
await expect(page.locator('h1:has-text("Reports")')).toBeVisible();
|
||||
});
|
||||
});
|
||||
37
e2e/settings.spec.ts
Normal file
37
e2e/settings.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { login } from './helpers/auth';
|
||||
|
||||
test.describe('Settings', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await login(page);
|
||||
await page.goto('/settings');
|
||||
});
|
||||
|
||||
test('settings page renders', async ({ page }) => {
|
||||
await expect(page.locator('h1:has-text("Settings")')).toBeVisible();
|
||||
});
|
||||
|
||||
test('tenant section loads org info', async ({ page }) => {
|
||||
await page.waitForTimeout(3000);
|
||||
// Should show tenant fields like name, address
|
||||
await expect(page.locator('text=Tenant, text=Organization').first()).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('users section visible to admin', async ({ page }) => {
|
||||
await page.waitForTimeout(2000);
|
||||
const usersSection = page.locator('text=Users');
|
||||
await expect(usersSection).toBeVisible({ timeout: 10000 }).catch(() => {});
|
||||
});
|
||||
|
||||
test('no crash when switching sections', async ({ page }) => {
|
||||
await page.waitForTimeout(2000);
|
||||
// Click through tab/section buttons
|
||||
const tabs = page.locator('button:has-text("Billing"), button:has-text("Areas"), button:has-text("Plans")');
|
||||
const count = await tabs.count();
|
||||
if (count > 0) {
|
||||
await tabs.first().click();
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
await expect(page.locator('h1:has-text("Settings")')).toBeVisible();
|
||||
});
|
||||
});
|
||||
46
e2e/tickets.spec.ts
Normal file
46
e2e/tickets.spec.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { login } from './helpers/auth';
|
||||
|
||||
test.describe('Tickets', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await login(page);
|
||||
await page.goto('/tickets');
|
||||
});
|
||||
|
||||
test('tickets page renders', async ({ page }) => {
|
||||
await expect(page.locator('h1:has-text("Tickets")')).toBeVisible();
|
||||
await expect(page.locator('table')).toBeVisible();
|
||||
});
|
||||
|
||||
test('tickets load', async ({ page }) => {
|
||||
await page.waitForSelector('tbody tr', { timeout: 15000 });
|
||||
const count = await page.locator('tbody tr').count();
|
||||
expect(count).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('type filter chips work', async ({ page }) => {
|
||||
const chip = page.locator('button:has-text("SUPPORT"), button:has-text("Support")').first();
|
||||
if (await chip.isVisible()) {
|
||||
await chip.click();
|
||||
await page.waitForTimeout(500);
|
||||
await expect(page.locator('h1:has-text("Tickets")')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('clicking ticket row opens detail modal', async ({ page }) => {
|
||||
await page.waitForSelector('tbody tr', { timeout: 15000 });
|
||||
await page.locator('tbody tr').first().click();
|
||||
await page.waitForTimeout(300);
|
||||
// Detail modal should appear
|
||||
await expect(page.locator('text=Ticket Detail, text=Subject, text=Status').first()).toBeVisible({ timeout: 5000 }).catch(() => {});
|
||||
});
|
||||
|
||||
test('New Ticket button opens modal', async ({ page }) => {
|
||||
const newBtn = page.locator('button:has-text("New Ticket")');
|
||||
if (await newBtn.isVisible()) {
|
||||
await newBtn.click();
|
||||
await page.waitForTimeout(300);
|
||||
await expect(page.locator('text=New Ticket, text=Create Ticket').first()).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user