diff --git a/Jenkinsfile b/Jenkinsfile index 4ed2f00..53ada8a 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -18,6 +18,20 @@ pipeline { } } + stage('Install Dependencies') { + steps { + sh 'npm ci' + } + } + + stage('Quality Gate') { + steps { + sh 'npm run lint' + sh 'npx tsc --noEmit' + sh 'npm run test:coverage' + } + } + stage('Build Docker Image') { steps { sh "docker build --build-arg NEXT_PUBLIC_AMAP_API_KEY=${AMAP_KEY} -t ${APP_NAME}:${BUILD_NUMBER} -t ${APP_NAME}:latest ." diff --git a/PROJECT_AUDIT_2026-03-03.md b/PROJECT_AUDIT_2026-03-03.md index 21190ff..0e6906e 100644 --- a/PROJECT_AUDIT_2026-03-03.md +++ b/PROJECT_AUDIT_2026-03-03.md @@ -182,11 +182,16 @@ - 为上述 hook 增加 `useEffect` cleanup 清理 `timersRef`; - confetti 动画增加销毁标志与取消逻辑。 -### P2-6 构建/测试门禁链路不完整 +### P2-6 构建/测试门禁链路不完整【已完成】 +- 修复状态:✅ 已完成(2026-03-03) +- 修复内容: + - `Jenkinsfile` 新增 `Install Dependencies` 与 `Quality Gate` 阶段,执行 `npm ci`、`npm run lint`、`npx tsc --noEmit`、`npm run test:coverage`; + - 补齐 `@vitest/coverage-v8` 依赖,恢复 `npm run test:coverage` 可执行; + - 在 `vitest.config.ts` 增加覆盖率阈值(`statements:57 / branches:50 / functions:47 / lines:60`); + - 调整 `blindbox/page` 与 `profile/page` 测试异步断言,减少 `act(...)` 噪声。 - 证据: - - `Jenkinsfile` 当前仅构建和部署,无 `lint/test/tsc` 阶段。 - - `npm run test:coverage` 缺少 `@vitest/coverage-v8`。 - - `npm test` 虽通过,但出现多处 `act(...)` 警告。 + - `npm run test:coverage` 通过(`54 files / 337 tests`,覆盖率总览满足阈值); + - `npm run lint` 当前 `0 errors`,`npx tsc --noEmit` 通过。 - 影响: - 回归问题可能绕过 CI 直接进入部署。 - 建议: diff --git a/eslint.config.mjs b/eslint.config.mjs index 05e726d..43f553a 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -11,6 +11,7 @@ const eslintConfig = defineConfig([ ".next/**", "out/**", "build/**", + "coverage/**", "next-env.d.ts", ]), ]); diff --git a/package-lock.json b/package-lock.json index 370be2f..545fbcd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,8 @@ "qrcode.react": "^4.2.0", "react": "19.2.3", "react-dom": "19.2.3", - "swr": "^2.4.0" + "swr": "^2.4.0", + "zod": "^4.3.6" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -38,6 +39,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "@vitejs/plugin-react": "^5.1.4", + "@vitest/coverage-v8": "^4.0.18", "eslint": "^9", "eslint-config-next": "16.1.6", "jsdom": "^28.1.0", @@ -280,7 +282,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -290,7 +292,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -324,7 +326,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -416,7 +418,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -426,6 +428,16 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@bramus/specificity": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", @@ -3753,6 +3765,49 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", + "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.0.18", + "ast-v8-to-istanbul": "^0.3.10", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.1", + "obug": "^2.1.1", + "std-env": "^3.10.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.0.18", + "vitest": "4.0.18" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/coverage-v8/node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, "node_modules/@vitest/expect": { "version": "4.0.18", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", @@ -4135,6 +4190,25 @@ "dev": true, "license": "MIT" }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -6192,6 +6266,13 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/html-to-image": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz", @@ -6741,6 +6822,45 @@ "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -7282,6 +7402,35 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -9639,6 +9788,7 @@ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", @@ -10026,7 +10176,6 @@ "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "devOptional": true, "license": "MIT", "peer": true, "funding": { diff --git a/package.json b/package.json index dd29340..1a18d07 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "@vitejs/plugin-react": "^5.1.4", + "@vitest/coverage-v8": "^4.0.18", "eslint": "^9", "eslint-config-next": "16.1.6", "jsdom": "^28.1.0", diff --git a/src/app/blindbox/page.test.tsx b/src/app/blindbox/page.test.tsx index d457cb4..fb91a10 100644 --- a/src/app/blindbox/page.test.tsx +++ b/src/app/blindbox/page.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render, screen } from "@testing-library/react"; +import { render, screen, waitFor } from "@testing-library/react"; import React from "react"; import { ToastContext, type ToastContextValue } from "@/hooks/useToast"; @@ -40,19 +40,23 @@ beforeEach(() => { }); describe("BlindboxPage", () => { - it("renders page heading", () => { + it("renders page heading", async () => { renderPage(); - expect(screen.getByText("周末契约")).toBeInTheDocument(); + expect(await screen.findByText("周末契约")).toBeInTheDocument(); + await waitFor(() => { + expect(mockFetch).toHaveBeenCalled(); + }); }); - it("renders back button", () => { + it("renders back button", async () => { renderPage(); + await screen.findByText("周末契约"); const backBtns = screen.getAllByRole("button"); expect(backBtns.length).toBeGreaterThan(0); }); - it("renders subtitle text", () => { + it("renders subtitle text", async () => { renderPage(); - expect(screen.getByText("ADVENTURE ROULETTE")).toBeInTheDocument(); + expect(await screen.findByText("ADVENTURE ROULETTE")).toBeInTheDocument(); }); }); diff --git a/src/app/profile/page.test.tsx b/src/app/profile/page.test.tsx index c87f0b8..077f741 100644 --- a/src/app/profile/page.test.tsx +++ b/src/app/profile/page.test.tsx @@ -70,9 +70,14 @@ beforeEach(() => { }); describe("ProfilePage", () => { - it("renders profile heading", () => { + it("renders profile heading", async () => { renderPage(); - expect(screen.getByText("个人中心")).toBeInTheDocument(); + expect(await screen.findByText("个人中心")).toBeInTheDocument(); + await waitFor(() => { + expect(mockFetch).toHaveBeenCalledWith( + "/api/user?id=user-1", + ); + }); }); it("fetches user profile data with correct URL", async () => { @@ -91,8 +96,13 @@ describe("ProfilePage", () => { }); }); - it("renders navigation element", () => { + it("renders navigation element", async () => { renderPage(); + await waitFor(() => { + expect(mockFetch).toHaveBeenCalledWith( + "/api/user?id=user-1", + ); + }); expect(screen.getByRole("navigation")).toBeInTheDocument(); }); }); diff --git a/vitest.config.ts b/vitest.config.ts index 09c011a..f3ae37c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -19,6 +19,16 @@ export default defineConfig({ }, }, test: { + coverage: { + provider: "v8", + reporter: ["text", "html"], + thresholds: { + statements: 57, + branches: 50, + functions: 47, + lines: 60, + }, + }, projects: [ { plugins: [react()],