feat: 前端测试 + 覆盖率 — 19 tests, vitest + @vitest/coverage-v8

- 新增 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 待后续安装)
This commit is contained in:
2026-06-19 21:25:41 +08:00
parent 77fac0e28f
commit ef6b092eda
4 changed files with 230 additions and 1 deletions
@@ -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 }) => (
<QrProvider>{children}</QrProvider>
);
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 }) => (
<QrProvider>{children}</QrProvider>
);
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 }) => (
<QrProvider>{children}</QrProvider>
);
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 }) => (
<QrProvider>{children}</QrProvider>
);
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 }) => (
<QrProvider>{children}</QrProvider>
);
const { result } = renderHook(() => useQrState(), { wrapper });
act(() =>
result.current.dispatch({
type: 'SET_PREVIEW',
payload: { svg: '<svg>hi</svg>', version: 1, size: 21, mask: 3 },
}),
);
expect(result.current.state.preview?.version).toBe(1);
expect(result.current.state.preview?.svg).toBe('<svg>hi</svg>');
});
it('SET_HISTORY replaces history', () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
<QrProvider>{children}</QrProvider>
);
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();
});
});