// Browser smoke: dev-login → dashboard shell → proposal list (Phase 5, web CI). // The API is fully mocked via route interception so the smoke runs in pure CI // with no backend — it exercises the login flow, auth context, protected // routing, the app shell, and list rendering end-to-end in a real browser. import { test, expect, type Page } from '@playwright/test'; function fakeJwt(): string { const b64 = (o: object) => Buffer.from(JSON.stringify(o)) .toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); return `${b64({ alg: 'HS256' })}.${b64({ sub: 'e2e-user', exp: Math.floor(Date.now() / 1000) + 3600, })}.e2e-signature`; } const authUser = { id: 'e2e-user', email: 'adam@seahavenind.com', displayName: 'Adam Moussa', role: 'SysAdmin', token: fakeJwt(), }; const proposals = [ { id: 'p-1', proposalNumber: 'PROP-2026-001', customerName: 'Harbor Point Marina', workOrderNumber: 'WO-1001', serviceCategory: 'HVAC', priority: 'Standard', status: 'InReview', totalBidAmount: 12500, submittedAt: '2026-07-01T12:00:00Z', submittedByName: 'Adam Moussa', assignedAdminName: null, rowVersion: 'AAAAAAAAAAE=', }, { id: 'p-2', proposalNumber: 'PROP-2026-002', customerName: 'Seaside Property Group', workOrderNumber: 'WO-1002', serviceCategory: 'Electrical', priority: 'Urgent', status: 'Approved', totalBidAmount: 4300, submittedAt: '2026-07-05T09:30:00Z', submittedByName: 'Adam Moussa', assignedAdminName: 'Sarah Chen', rowVersion: 'AAAAAAAAAAI=', }, ]; async function mockApi(page: Page) { // Match on pathname, not a '**/api/**' glob: vite dev serves app modules from // paths like /src/lib/api/client.ts, which such a glob would swallow. // Registered first = matched last (Playwright checks routes newest-first): // generic fallback so nothing the shell fetches hangs or hard-errors. await page.route( (url) => url.pathname.startsWith('/api/'), (route) => route.fulfill({ json: {} }), ); await page.route( (url) => url.pathname === '/api/auth/dev-login', (route) => route.fulfill({ json: authUser }), ); await page.route( (url) => url.pathname === '/api/proposals/stats', (route) => route.fulfill({ json: { totalCount: 2, inReviewCount: 1, approvedCount: 1, sentCount: 0 } }), ); await page.route( (url) => url.pathname === '/api/proposals' && url.searchParams.has('page'), (route) => route.fulfill({ json: { items: proposals, totalCount: proposals.length, page: 1, pageSize: 12 }, }), ); } test('dev-login lands on the dashboard and the proposal list renders', async ({ page }) => { await mockApi(page); await page.goto('/'); await expect(page).toHaveURL(/\/login$/); await expect(page.getByText('Welcome back')).toBeVisible(); await page.getByRole('button', { name: /SysAdmin/ }).click(); // Authenticated shell: topbar user chip + dashboard content. await expect(page).toHaveURL('/'); await expect(page.getByText('Adam Moussa').first()).toBeVisible(); await expect(page.getByText('Recent Proposals')).toBeVisible(); // Proposal list via sidebar nav. await page.getByText('My Proposals', { exact: true }).click(); await expect(page).toHaveURL(/\/proposals$/); await expect(page.getByRole('heading', { name: 'My Proposals' })).toBeVisible(); await expect(page.getByText('Harbor Point Marina')).toBeVisible(); await expect(page.getByText('Seaside Property Group')).toBeVisible(); }); test('unauthenticated visits are bounced to the login card', async ({ page }) => { await mockApi(page); await page.goto('/proposals'); await expect(page).toHaveURL(/\/login$/); await expect(page.getByText('Welcome back')).toBeVisible(); });