proposal-system/web/e2e/smoke.spec.ts

115 lines
3.8 KiB
TypeScript

// 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();
});