mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-10-07 05:52:00 +00:00
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.
This commit is contained in:
parent
97cc26b03e
commit
191f710752
9 changed files with 251 additions and 26 deletions
28
.github/workflows/ci.yaml
vendored
28
.github/workflows/ci.yaml
vendored
|
|
@ -27,30 +27,16 @@ jobs:
|
|||
|
||||
web:
|
||||
name: Web Frontend Check
|
||||
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@main
|
||||
# Phase 5: full frontend pipeline — format:check, build (tsc -b included),
|
||||
# vitest, Playwright chromium smoke. Replaces the old typecheck-only job
|
||||
# plus the standalone Web Tests job. No lint script yet (run-lint: false).
|
||||
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-frontend.yaml@main
|
||||
with:
|
||||
working-directory: web
|
||||
cache-dependency-path: web/package-lock.json
|
||||
node-version: "24"
|
||||
run-cdk-synth: false
|
||||
run-conventions-check: false
|
||||
|
||||
web-test:
|
||||
name: Web Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
cache-dependency-path: web/package-lock.json
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
run-lint: false
|
||||
required-scripts: "format:check,build,test,test:e2e"
|
||||
|
||||
python:
|
||||
name: Python Lint
|
||||
|
|
@ -106,7 +92,7 @@ jobs:
|
|||
# required check would otherwise never report on this repo.
|
||||
ci:
|
||||
name: ci / ci
|
||||
needs: [dotnet, web, web-test, python, python-test, mobile, infra]
|
||||
needs: [dotnet, web, python, python-test, mobile, infra]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
|
|
|||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -69,3 +69,7 @@ api/src/ProposalSystem.Api/Data/verified-sites.json
|
|||
*.zip
|
||||
.claude/worktrees/
|
||||
.claude/agents/
|
||||
|
||||
# Playwright (web e2e smoke)
|
||||
web/test-results/
|
||||
web/playwright-report/
|
||||
|
|
|
|||
|
|
@ -114,13 +114,12 @@ npx cdk synth
|
|||
|
||||
### CI (on pull request to main)
|
||||
|
||||
Seven parallel jobs calling org reusable workflows:
|
||||
Six parallel jobs calling org reusable workflows:
|
||||
|
||||
| Job | Workflow | What it checks |
|
||||
|---|---|---|
|
||||
| .NET Build & Test | `ci-dotnet.yaml` | Restore, build, test the API solution (104 xUnit tests) |
|
||||
| Web Frontend Check | `ci-typescript-cdk.yaml` | TypeScript typecheck for web |
|
||||
| Web Tests | `ci-typescript-cdk.yaml` | vitest suite (26 tests — auth, interceptors, components) |
|
||||
| .NET Build & Test | `ci-dotnet.yaml` | Restore, build, test the API solution (xUnit) |
|
||||
| Web Frontend Check | `ci-typescript-frontend.yaml` | Prettier `format:check`, build (includes `tsc -b`), vitest suite, Playwright chromium smoke (dev-login → proposal list, API mocked) |
|
||||
| Mobile Typecheck | `ci-typescript-cdk.yaml` | TypeScript typecheck for mobile |
|
||||
| Python Lint | `ci-python-sam.yaml` | ruff check + format on lambdas/ |
|
||||
| Python Tests | `ci-python-sam.yaml` | pytest suite (19 tests — pdf-generate, suggestions handlers) |
|
||||
|
|
|
|||
6
web/.prettierignore
Normal file
6
web/.prettierignore
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
dist
|
||||
coverage
|
||||
node_modules
|
||||
package-lock.json
|
||||
playwright-report
|
||||
test-results
|
||||
4
web/.prettierrc
Normal file
4
web/.prettierrc
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
113
web/e2e/smoke.spec.ts
Normal file
113
web/e2e/smoke.spec.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
// 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();
|
||||
});
|
||||
81
web/package-lock.json
generated
81
web/package-lock.json
generated
|
|
@ -28,6 +28,7 @@
|
|||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
|
|
@ -35,6 +36,7 @@
|
|||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
"jsdom": "^29.1.1",
|
||||
"prettier": "^3.9.5",
|
||||
"typescript": "~7.0.2",
|
||||
"vite": "^8.1.3",
|
||||
"vitest": "^4.1.10"
|
||||
|
|
@ -934,6 +936,22 @@
|
|||
"url": "https://github.com/sponsors/Boshen"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
|
||||
"integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.61.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
|
|
@ -3170,6 +3188,53 @@
|
|||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
||||
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.61.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
||||
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.16",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
|
||||
|
|
@ -3199,6 +3264,22 @@
|
|||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.9.5",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz",
|
||||
"integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-format": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
||||
|
|
|
|||
|
|
@ -7,8 +7,11 @@
|
|||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
"test:watch": "vitest",
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
|
|
@ -31,6 +34,7 @@
|
|||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
|
|
@ -38,6 +42,7 @@
|
|||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
"jsdom": "^29.1.1",
|
||||
"prettier": "^3.9.5",
|
||||
"typescript": "~7.0.2",
|
||||
"vite": "^8.1.3",
|
||||
"vitest": "^4.1.10"
|
||||
|
|
|
|||
27
web/playwright.config.ts
Normal file
27
web/playwright.config.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Browser smoke config — mirrors shoc-frontend-new/playwright.config.ts.
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
fullyParallel: true,
|
||||
forbidOnly: Boolean(process.env.CI),
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: process.env.CI ? 'github' : 'list',
|
||||
use: {
|
||||
baseURL: 'http://localhost:4173',
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
command: 'npm run dev -- --port 4173 --strictPort',
|
||||
url: 'http://localhost:4173',
|
||||
reuseExistingServer: false,
|
||||
timeout: 120_000,
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue