Files
pn-new-crm/tests/e2e/smoke/23-portal-flow.spec.ts
Matt 221ae5784e chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:

- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
  never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
  after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
  redirects (ocr to ai, reports to dashboard, invitations to users),
  docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
  flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
  let-reassign), set-state-in-effect disables in CountryFlag and
  UploadForSigning preview-bytes effect, unused 'confirm' destructures in
  interest contract + reservation tabs, unescaped apostrophe in test-template
  card copy
2026-05-23 00:52:59 +02:00

156 lines
5.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 { PORT_SLUG } from './helpers';
test.describe('Portal Flow', () => {
test('portal login is separate from CRM login', async ({ page }) => {
// Verify the CRM login page
await page.goto('/login');
await page.waitForLoadState('networkidle');
const crmEmailInput = page.locator('#email, input[type="email"]').first();
const crmPasswordInput = page.locator('#password, input[type="password"]').first();
await expect(crmEmailInput).toBeVisible({ timeout: 5_000 });
await expect(crmPasswordInput).toBeVisible({ timeout: 5_000 });
// Navigate to portal login - should be a different page
await page.goto('/portal/login');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1_000);
const portalUrl = page.url();
// Look for a "Client Portal" heading
const portalHeading = page
.getByText(/client portal/i)
.first()
.or(page.getByRole('heading').first());
const hasPortalHeading = await portalHeading.isVisible({ timeout: 5_000 }).catch(() => false);
// Look for an email-only input (magic link - no password field)
const portalEmailInput = page
.locator('input[type="email"], input[placeholder*="email" i], #email')
.first();
const portalPasswordInput = page.locator('input[type="password"]').first();
const hasEmail = await portalEmailInput.isVisible({ timeout: 5_000 }).catch(() => false);
const hasPassword = await portalPasswordInput.isVisible({ timeout: 2_000 }).catch(() => false);
// Portal should have an email input
expect(hasEmail || hasPortalHeading).toBeTruthy();
// Portal should NOT require a password (magic link flow)
if (hasEmail && hasPassword) {
console.warn(' ⚠️ Portal login shows password field - expected email-only magic link flow');
}
});
test('portal login page shows "Client Portal" heading', async ({ page }) => {
await page.goto('/portal/login');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1_000);
const heading = page.getByText(/client portal/i).first();
await expect(heading).toBeVisible({ timeout: 10_000 });
});
test('portal login accepts email and shows check-email confirmation', async ({ page }) => {
await page.goto('/portal/login');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1_000);
const emailInput = page
.locator('input[type="email"], input[placeholder*="email" i], #email')
.first();
const inputVisible = await emailInput.isVisible({ timeout: 5_000 }).catch(() => false);
if (!inputVisible) {
console.log(' Portal login email input not found - page may not be implemented yet');
expect(true).toBeTruthy();
return;
}
await emailInput.fill('testclient@example.com');
const submitBtn = page
.getByRole('button', { name: /send|submit|access|login|continue|magic link/i })
.first();
const btnVisible = await submitBtn.isVisible({ timeout: 5_000 }).catch(() => false);
if (!btnVisible) {
console.log(' Portal submit button not found');
expect(true).toBeTruthy();
return;
}
await submitBtn.click();
await page.waitForTimeout(3_000);
// Should show a "check your email" / "link sent" confirmation
const confirmation = page
.getByText(/check your email|link sent|magic link|email sent/i)
.first();
await expect(confirmation).toBeVisible({ timeout: 10_000 });
});
test('portal API rejects unauthenticated dashboard request with 401', async ({ page }) => {
const response = await page.request.get('/api/portal/dashboard');
expect(response.status()).toBe(401);
});
test('portal API rejects unauthenticated interests request with 401', async ({ page }) => {
const response = await page.request.get('/api/portal/interests');
expect(response.status()).toBe(401);
});
test('portal API rejects unauthenticated documents request with 401', async ({ page }) => {
const response = await page.request.get('/api/portal/documents');
expect(response.status()).toBe(401);
});
test('portal API rejects unauthenticated invoices request with 401', async ({ page }) => {
const response = await page.request.get('/api/portal/invoices');
expect(response.status()).toBe(401);
});
test('portal document download endpoint requires auth', async ({ page }) => {
const response = await page.request.get('/api/portal/documents/00000000-fake-id/download');
// Must be 401 (not 500 - endpoint exists and guards correctly)
expect(response.status()).toBe(401);
});
test('CRM routes not accessible without CRM login', async ({ page }) => {
// Ensure no residual session from other tests by clearing cookies first
await page.context().clearCookies();
await page.goto(`/${PORT_SLUG}/clients`);
await page.waitForLoadState('networkidle');
await page.waitForTimeout(3_000);
const url = page.url();
// Should redirect to the CRM login page
const redirectedToLogin = url.includes('/login');
const hasAuthPrompt = await page
.getByText(/sign in|log in|authentication required/i)
.isVisible({ timeout: 5_000 })
.catch(() => false);
expect(redirectedToLogin || hasAuthPrompt).toBeTruthy();
// Should NOT be on the clients page without auth
const onClients = url.includes('/clients') && !redirectedToLogin;
expect(onClients).toBeFalsy();
});
test('portal session cannot access CRM API endpoints', async ({ page }) => {
// Without any authentication, CRM API should reject with 401
const meResponse = await page.request.get('/api/v1/me');
expect([401, 403].includes(meResponse.status())).toBeTruthy();
const clientsResponse = await page.request.get(`/api/v1/${PORT_SLUG}/clients`);
expect([401, 403].includes(clientsResponse.status())).toBeTruthy();
});
});