proposal-system/web/e2e/smoke.spec.ts
Adam Moussa 191f710752
ci(web): Phase 5 — prettier check + Playwright smoke via org frontend workflow
Converts the web CI job from ci-typescript-cdk.yaml (typecheck only) to
ci-typescript-frontend.yaml: format:check, build (tsc -b included),
vitest, and a Playwright chromium smoke. Folds the standalone Web Tests
job into it (aggregator needs updated). Pure CI — no AWS secrets.

The smoke (e2e/smoke.spec.ts) drives dev-login → dashboard shell →
proposal list, plus the unauthenticated bounce, against a fully mocked
API (pathname-anchored route interception — a '**/api/**' glob would
swallow vite's /src/lib/api/* module URLs). Config mirrors SHOC's
playwright.config.ts (port 4173, chromium, dev-server webServer).

Prettier: singleQuote + printWidth 100 to match the existing codebase
style; lint intentionally not added (no ESLint config yet — run-lint
false, out of Phase 5 scope). rollback = revert this workflow file.
2026-07-13 20:14:41 -04:00

113 lines
3.7 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,
},
{
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',
},
];
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();
});