Files
pn-new-crm/tests/e2e/smoke/21-role-based-ui.spec.ts
Matt Ciaccio dd138547fb test(e2e): fix admin-nav locator + add residential interest API coverage
- 21-role-based-ui: tighten the Settings link locator. The previous
  `getByRole('link', { name: /settings/i }).first().or(getByText(/.../) .first())`
  chain hit a strict-mode violation once the sidebar Admin section became
  default-expanded — both the section header text node and the Settings
  link matched. Match the link directly with exact: true.
- 26-residential: extend smoke with two API-driven specs covering the
  residential interest pipeline — create+list and detail-page render —
  using preferences-string stamp + heading match for assertions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 00:19:51 +02:00

182 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect } from '@playwright/test';
import { login, navigateTo } from './helpers';
test.describe('Role-Based UI', () => {
test('super_admin sees admin nav items', async ({ page }) => {
await login(page, 'super_admin');
// Give React Query time to resolve permissions
await page.waitForTimeout(3_000);
// Sidebar exposes a Settings link inside the Admin section (visible by
// default for super_admin). Match the link directly — earlier OR-fallbacks
// ambiguously matched the section header label too.
const settingsLink = page.getByRole('link', { name: 'Settings', exact: true }).first();
await expect(settingsLink).toBeVisible({ timeout: 10_000 });
// "+ New" button (or equivalent CTA) should be visible
const newButton = page
.getByRole('button', { name: /new/i })
.first()
.or(page.locator('[data-testid*="new-btn"]').first());
const newButtonVisible = await newButton.isVisible({ timeout: 5_000 }).catch(() => false);
if (!newButtonVisible) {
// Navigate to a page that definitely shows the new button
await navigateTo(page, '/clients');
await page.waitForTimeout(2_000);
const clientsNewBtn = page.getByRole('button', { name: /new client/i }).first();
await expect(clientsNewBtn).toBeVisible({ timeout: 10_000 });
}
// Admin monitoring page should load without being blocked
await navigateTo(page, '/admin/monitoring');
await page.waitForTimeout(3_000);
const monitoringUrl = page.url();
// Should still be on the monitoring page (not redirected away)
const isOnMonitoring = monitoringUrl.includes('/admin/monitoring');
const hasMonitoringContent = await page
.getByText(/monitor|health|queue|postgres/i)
.isVisible({ timeout: 5_000 })
.catch(() => false);
const wasBlocked = await page
.getByText(/permission|forbidden|access denied|not authorized/i)
.isVisible({ timeout: 2_000 })
.catch(() => false);
expect((isOnMonitoring && !wasBlocked) || hasMonitoringContent).toBeTruthy();
});
test('sales_agent sees limited nav', async ({ page }) => {
await login(page, 'sales_agent');
// Give React Query time to resolve permissions
await page.waitForTimeout(3_000);
// Main navigation items should be visible for sales_agent
const mainNavItems = [
page.getByRole('link', { name: /dashboard/i }).first(),
page.getByRole('link', { name: /clients/i }).first(),
page.getByRole('link', { name: /interests/i }).first(),
];
// At least 2 of the core nav items should be visible
let foundCount = 0;
for (const navItem of mainNavItems) {
const visible = await navItem.isVisible({ timeout: 3_000 }).catch(() => false);
if (visible) foundCount++;
}
// A sales agent should see at least the dashboard or clients
expect(foundCount).toBeGreaterThanOrEqual(1);
// Admin section should either be hidden or inaccessible
await navigateTo(page, '/admin/monitoring');
await page.waitForTimeout(3_000);
const monitoringUrl = page.url();
const isStillOnMonitoring = monitoringUrl.includes('/admin/monitoring');
const hasPermError = await page
.getByText(/permission|forbidden|access denied|not authorized|unauthorized/i)
.first()
.isVisible({ timeout: 3_000 })
.catch(() => false);
const wasRedirected = !isStillOnMonitoring;
// With APIs returning 403 for non-admins, queue cards don't render — no data leak.
// Scope to <main> because sidebar has "Email"/"Documents" nav links.
const queueCardCount = await page
.locator('main')
.getByText(/^(webhooks|notifications|reports|maintenance|ai|bulk)$/i)
.count();
const dataLeaked = queueCardCount > 0;
// Accept: redirect, permission error, or simply no data leak (API-enforced)
expect(hasPermError || wasRedirected || !dataLeaked).toBeTruthy();
});
test('viewer has read-only access on clients list', async ({ page }) => {
await login(page, 'viewer');
await navigateTo(page, '/clients');
await page.waitForLoadState('networkidle');
// Allow time for permission-gated UI to resolve
await page.waitForTimeout(3_000);
// The clients list itself should load (viewer can read)
const pageContent = page.locator('main').first();
await expect(pageContent).toBeVisible({ timeout: 10_000 });
// "New Client" button should be hidden or disabled for viewer
const newClientBtn = page.getByRole('button', { name: /new client/i });
const isBtnVisible = await newClientBtn.isVisible({ timeout: 3_000 }).catch(() => false);
if (isBtnVisible) {
// If visible (PermissionGate not enforcing client-side), it should at
// minimum be disabled, or the server rejects the action
const isDisabled = await newClientBtn.isDisabled().catch(() => false);
if (!isDisabled) {
console.warn(
' ⚠️ APP BUG: New Client button not gated for viewer — server-side must enforce',
);
}
}
// Test passes regardless — this validates the UI state (server is authoritative)
expect(true).toBeTruthy();
});
test('viewer cannot edit client records', async ({ page }) => {
await login(page, 'viewer');
await navigateTo(page, '/clients');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(3_000);
// If any client rows exist, click into one
const firstRow = page.locator('table tbody tr').first();
const rowVisible = await firstRow.isVisible({ timeout: 5_000 }).catch(() => false);
if (!rowVisible) {
console.log(' No client rows found — skipping edit button check');
return;
}
const link = firstRow.locator('a').first();
if (await link.isVisible({ timeout: 3_000 }).catch(() => false)) {
await link.click();
} else {
await firstRow.click();
}
await page.waitForTimeout(3_000);
const url = page.url();
if (!url.includes('/clients/')) {
console.log(' Could not navigate to client detail — skipping');
return;
}
// Edit buttons should be hidden or disabled for viewer
const editButtons = page.getByRole('button', { name: /edit|save changes|update/i });
const editCount = await editButtons.count();
if (editCount > 0) {
const firstEditBtn = editButtons.first();
const isVisible = await firstEditBtn.isVisible({ timeout: 2_000 }).catch(() => false);
const isDisabled = await firstEditBtn.isDisabled().catch(() => false);
if (isVisible && !isDisabled) {
console.warn(' ⚠️ Edit button visible/enabled for viewer — PermissionGate should hide it');
}
}
// Page should load without crashing
const body = await page
.locator('body')
.textContent()
.catch(() => '');
expect(body && body.length > 10).toBeTruthy();
});
});