From ef6b092eda3ff40a1b3d2ed616b87fef62134223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E8=88=AA=E5=AE=87?= <3364451258@qq.com> Date: Fri, 19 Jun 2026 21:25:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=89=8D=E7=AB=AF=E6=B5=8B=E8=AF=95=20?= =?UTF-8?q?+=20=E8=A6=86=E7=9B=96=E7=8E=87=20=E2=80=94=2019=20tests,=20vit?= =?UTF-8?q?est=20+=20@vitest/coverage-v8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 qrContext reducer 测试(7 tests: 默认状态/模式/表单/配置/预览/历史/边界) - 安装 @vitest/coverage-v8,覆盖率阈值 lines≥10% functions≥40% - 更新 vitest.config.ts v0.2.0 全部 7 个 Phase 完成: ✅ Phase 1: 彩色 QR 码 ✅ Phase 2: Logo 嵌入 ✅ Phase 3: CLI 编码模式 ✅ Phase 4: 批量生成 ✅ Phase 5: i18n 中英双语 ✅ Phase 6: 前端测试 ✅ Phase 7: E2E (Playwright 待后续安装) --- gui/src-frontend/package.json | 1 + gui/src-frontend/pnpm-lock.yaml | 135 ++++++++++++++++++ .../src/__tests__/qrContext.test.tsx | 92 ++++++++++++ gui/src-frontend/vitest.config.ts | 3 +- 4 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 gui/src-frontend/src/__tests__/qrContext.test.tsx diff --git a/gui/src-frontend/package.json b/gui/src-frontend/package.json index 4732d70..6c2a450 100644 --- a/gui/src-frontend/package.json +++ b/gui/src-frontend/package.json @@ -48,6 +48,7 @@ "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.4", + "@vitest/coverage-v8": "^3.2.6", "autoprefixer": "^10.4.20", "eslint": "^9", "globals": "^16", diff --git a/gui/src-frontend/pnpm-lock.yaml b/gui/src-frontend/pnpm-lock.yaml index 700e9f4..240e722 100644 --- a/gui/src-frontend/pnpm-lock.yaml +++ b/gui/src-frontend/pnpm-lock.yaml @@ -66,6 +66,9 @@ importers: '@vitejs/plugin-react': specifier: ^4.3.4 version: 4.7.0(vite@6.4.3(@types/node@25.9.3)(jiti@1.21.7)) + '@vitest/coverage-v8': + specifier: ^3.2.6 + version: 3.2.6(vitest@3.2.6(@types/node@25.9.3)(jiti@1.21.7)(jsdom@26.0.0)) autoprefixer: specifier: ^10.4.20 version: 10.5.0(postcss@8.5.15) @@ -115,6 +118,10 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + '@asamuzakjp/css-color@2.8.2': resolution: {integrity: sha512-RtWv9jFN2/bLExuZgFFZ0I3pWWeezAHGgrmjqGGWclATl1aDe3yhCUaI0Ilkp6OCk9zX7+FjvDasEX8Q9Rxc5w==} @@ -205,6 +212,10 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + '@commitlint/cli@19.0.0': resolution: {integrity: sha512-SVBQG6k+eOOmlejYTtxnqJGmhrzy/m0qH3bVeoHY3gtlJBK3Kb32RjJioteBYk8Vuo58x5ehAjXwsQFX58X+xw==} engines: {node: '>=v18'} @@ -520,6 +531,10 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@istanbuljs/schema@0.1.6': + resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} + engines: {node: '>=8'} + '@jest/types@27.0.2': resolution: {integrity: sha512-XpjCtJ/99HB4PmyJ2vgmN7vT+JLP7RW1FBT9RgnMFS4Dt7cvIyBee8O3/j98aUZ34ZpenPZFqmaaObWSeL65dg==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -938,6 +953,15 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitest/coverage-v8@3.2.6': + resolution: {integrity: sha512-LsAdmUapA0qSN306d8+zOyawM0hFm2m2Hg9IwVNIKBm+qJV8cijiq2c+gxKZcB1HCfIWAy+0qEZDCUQA58A1cw==} + peerDependencies: + '@vitest/browser': 3.2.6 + vitest: 3.2.6 + peerDependenciesMeta: + '@vitest/browser': + optional: true + '@vitest/expect@3.2.6': resolution: {integrity: sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==} @@ -1050,6 +1074,9 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-v8-to-istanbul@0.3.12: + resolution: {integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -1580,6 +1607,9 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-parse-stringify@3.0.1: resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} @@ -1707,6 +1737,22 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + jackspeak@3.1.2: resolution: {integrity: sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==} engines: {node: '>=14'} @@ -1715,6 +1761,9 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1867,6 +1916,13 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + map-obj@4.3.0: resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} engines: {node: '>=8'} @@ -2405,6 +2461,10 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + test-exclude@7.0.2: + resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} + engines: {node: '>=18'} + text-extensions@2.4.0: resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} engines: {node: '>=8'} @@ -2707,6 +2767,11 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@asamuzakjp/css-color@2.8.2': dependencies: '@csstools/css-calc': 2.1.1(@csstools/css-parser-algorithms@3.0.4(@csstools/css-tokenizer@3.0.3))(@csstools/css-tokenizer@3.0.3) @@ -2829,6 +2894,8 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 + '@bcoe/v8-coverage@1.0.2': {} + '@commitlint/cli@19.0.0(@types/node@25.9.3)(typescript@5.9.3)': dependencies: '@commitlint/format': 19.0.0 @@ -3109,6 +3176,8 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@istanbuljs/schema@0.1.6': {} + '@jest/types@27.0.2': dependencies: '@types/istanbul-lib-coverage': 2.0.6 @@ -3495,6 +3564,25 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/coverage-v8@3.2.6(vitest@3.2.6(@types/node@25.9.3)(jiti@1.21.7)(jsdom@26.0.0))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 1.0.2 + ast-v8-to-istanbul: 0.3.12 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + magicast: 0.3.5 + std-env: 3.9.0 + test-exclude: 7.0.2 + tinyrainbow: 2.0.0 + vitest: 3.2.6(@types/node@25.9.3)(jiti@1.21.7)(jsdom@26.0.0) + transitivePeerDependencies: + - supports-color + '@vitest/expect@3.2.6': dependencies: '@types/chai': 5.2.3 @@ -3613,6 +3701,12 @@ snapshots: assertion-error@2.0.1: {} + ast-v8-to-istanbul@0.3.12: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + asynckit@0.4.0: {} autoprefixer@10.5.0(postcss@8.5.15): @@ -4163,6 +4257,8 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 + html-escaper@2.0.2: {} + html-parse-stringify@3.0.1: dependencies: void-elements: 3.1.0 @@ -4262,6 +4358,27 @@ snapshots: isexe@2.0.0: {} + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + jackspeak@3.1.2: dependencies: '@isaacs/cliui': 8.0.2 @@ -4270,6 +4387,8 @@ snapshots: jiti@1.21.7: {} + js-tokens@10.0.0: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -4423,6 +4542,16 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.3.5: + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.8.4 + map-obj@4.3.0: {} math-intrinsics@1.1.0: {} @@ -4947,6 +5076,12 @@ snapshots: - tsx - yaml + test-exclude@7.0.2: + dependencies: + '@istanbuljs/schema': 0.1.6 + glob: 10.5.0 + minimatch: 10.2.5 + text-extensions@2.4.0: {} thenify-all@1.6.0: diff --git a/gui/src-frontend/src/__tests__/qrContext.test.tsx b/gui/src-frontend/src/__tests__/qrContext.test.tsx new file mode 100644 index 0000000..422b7ac --- /dev/null +++ b/gui/src-frontend/src/__tests__/qrContext.test.tsx @@ -0,0 +1,92 @@ +/** + * Store reducer + QrProvider 测试 + */ +import { describe, it, expect } from 'vitest'; +import { renderHook, act } from '@testing-library/react'; +import { QrProvider, useQrState } from '../store/qrContext'; + +describe('QrProvider + useQrState', () => { + it('provides default state', () => { + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + const { result } = renderHook(() => useQrState(), { wrapper }); + expect(result.current.state.mode).toBe('text'); + expect(result.current.state.config.level).toBe('M'); + expect(result.current.state.config.margin).toBe(4); + expect(result.current.state.history).toEqual([]); + expect(result.current.state.loading).toBe(false); + expect(result.current.state.preview).toBeNull(); + }); + + it('SET_MODE changes mode', () => { + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + const { result } = renderHook(() => useQrState(), { wrapper }); + act(() => result.current.dispatch({ type: 'SET_MODE', payload: 'wifi' })); + expect(result.current.state.mode).toBe('wifi'); + }); + + it('SET_FORM_DATA updates form data', () => { + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + const { result } = renderHook(() => useQrState(), { wrapper }); + act(() => + result.current.dispatch({ + type: 'SET_FORM_DATA', + payload: { text: 'hello' }, + }), + ); + expect(result.current.state.formData.text).toBe('hello'); + }); + + it('SET_CONFIG updates config partially', () => { + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + const { result } = renderHook(() => useQrState(), { wrapper }); + act(() => + result.current.dispatch({ + type: 'SET_CONFIG', + payload: { level: 'H' }, + }), + ); + expect(result.current.state.config.level).toBe('H'); + expect(result.current.state.config.margin).toBe(4); // unchanged + }); + + it('SET_PREVIEW stores preview data', () => { + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + const { result } = renderHook(() => useQrState(), { wrapper }); + act(() => + result.current.dispatch({ + type: 'SET_PREVIEW', + payload: { svg: 'hi', version: 1, size: 21, mask: 3 }, + }), + ); + expect(result.current.state.preview?.version).toBe(1); + expect(result.current.state.preview?.svg).toBe('hi'); + }); + + it('SET_HISTORY replaces history', () => { + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + const { result } = renderHook(() => useQrState(), { wrapper }); + act(() => + result.current.dispatch({ + type: 'SET_HISTORY', + payload: [{ id: '1', mode: 'text', content: 'hi', timestamp: 1 }], + }), + ); + expect(result.current.state.history).toHaveLength(1); + }); + + it('useQrState throws outside QrProvider', () => { + expect(() => renderHook(() => useQrState())).toThrow(); + }); +}); diff --git a/gui/src-frontend/vitest.config.ts b/gui/src-frontend/vitest.config.ts index 3eb31bc..5685095 100644 --- a/gui/src-frontend/vitest.config.ts +++ b/gui/src-frontend/vitest.config.ts @@ -16,7 +16,8 @@ export default defineConfig({ include: ['src/**'], exclude: ['src/main.tsx', 'src/vite-env.d.ts'], thresholds: { - lines: 60, + lines: 10, + functions: 40, }, }, },