mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-10-07 11:38:56 +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:
|
web:
|
||||||
name: Web Frontend Check
|
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:
|
with:
|
||||||
working-directory: web
|
working-directory: web
|
||||||
cache-dependency-path: web/package-lock.json
|
cache-dependency-path: web/package-lock.json
|
||||||
node-version: "24"
|
node-version: "24"
|
||||||
run-cdk-synth: false
|
run-lint: false
|
||||||
run-conventions-check: false
|
required-scripts: "format:check,build,test,test:e2e"
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
python:
|
python:
|
||||||
name: Python Lint
|
name: Python Lint
|
||||||
|
|
@ -106,7 +92,7 @@ jobs:
|
||||||
# required check would otherwise never report on this repo.
|
# required check would otherwise never report on this repo.
|
||||||
ci:
|
ci:
|
||||||
name: ci / ci
|
name: ci / ci
|
||||||
needs: [dotnet, web, web-test, python, python-test, mobile, infra]
|
needs: [dotnet, web, python, python-test, mobile, infra]
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -69,3 +69,7 @@ api/src/ProposalSystem.Api/Data/verified-sites.json
|
||||||
*.zip
|
*.zip
|
||||||
.claude/worktrees/
|
.claude/worktrees/
|
||||||
.claude/agents/
|
.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)
|
### 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 |
|
| Job | Workflow | What it checks |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| .NET Build & Test | `ci-dotnet.yaml` | Restore, build, test the API solution (104 xUnit tests) |
|
| .NET Build & Test | `ci-dotnet.yaml` | Restore, build, test the API solution (xUnit) |
|
||||||
| Web Frontend Check | `ci-typescript-cdk.yaml` | TypeScript typecheck for web |
|
| 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) |
|
||||||
| Web Tests | `ci-typescript-cdk.yaml` | vitest suite (26 tests — auth, interceptors, components) |
|
|
||||||
| Mobile Typecheck | `ci-typescript-cdk.yaml` | TypeScript typecheck for mobile |
|
| Mobile Typecheck | `ci-typescript-cdk.yaml` | TypeScript typecheck for mobile |
|
||||||
| Python Lint | `ci-python-sam.yaml` | ruff check + format on lambdas/ |
|
| 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) |
|
| 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"
|
"zod": "^4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.61.1",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.2",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
|
|
@ -35,6 +36,7 @@
|
||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
"@vitejs/plugin-react": "^6.0.3",
|
"@vitejs/plugin-react": "^6.0.3",
|
||||||
"jsdom": "^29.1.1",
|
"jsdom": "^29.1.1",
|
||||||
|
"prettier": "^3.9.5",
|
||||||
"typescript": "~7.0.2",
|
"typescript": "~7.0.2",
|
||||||
"vite": "^8.1.3",
|
"vite": "^8.1.3",
|
||||||
"vitest": "^4.1.10"
|
"vitest": "^4.1.10"
|
||||||
|
|
@ -934,6 +936,22 @@
|
||||||
"url": "https://github.com/sponsors/Boshen"
|
"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": {
|
"node_modules/@popperjs/core": {
|
||||||
"version": "2.11.8",
|
"version": "2.11.8",
|
||||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||||
|
|
@ -3170,6 +3188,53 @@
|
||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"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": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.16",
|
"version": "8.5.16",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
|
||||||
|
|
@ -3199,6 +3264,22 @@
|
||||||
"node": "^10 || ^12 || >=14"
|
"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": {
|
"node_modules/pretty-format": {
|
||||||
"version": "27.5.1",
|
"version": "27.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,11 @@
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check .",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest"
|
"test:watch": "vitest",
|
||||||
|
"test:e2e": "playwright test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
|
|
@ -31,6 +34,7 @@
|
||||||
"zod": "^4.4.3"
|
"zod": "^4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.61.1",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.2",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
|
|
@ -38,6 +42,7 @@
|
||||||
"@types/react-dom": "^19.0.0",
|
"@types/react-dom": "^19.0.0",
|
||||||
"@vitejs/plugin-react": "^6.0.3",
|
"@vitejs/plugin-react": "^6.0.3",
|
||||||
"jsdom": "^29.1.1",
|
"jsdom": "^29.1.1",
|
||||||
|
"prettier": "^3.9.5",
|
||||||
"typescript": "~7.0.2",
|
"typescript": "~7.0.2",
|
||||||
"vite": "^8.1.3",
|
"vite": "^8.1.3",
|
||||||
"vitest": "^4.1.10"
|
"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