// REQ-USR-003: 页面常量 + 前端 CSV 导出工具单测(D-PLAN-1) import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { buildUserCsv, downloadCsv } from '../../src/pages/usr/UserList/exportUtils'; import { DEFAULT_QUERY, PAGE_SIZE_OPTIONS, QUERY_FIELD_OPTIONS, MATCH_TYPE_OPTIONS, } from '../../src/pages/usr/UserList/constants'; import type { UserVO } from '../../src/api/types'; const rowVoid0: UserVO = { id: 1, sUserName: 'admin', employeeName: null, sUserNo: 'U001', departmentName: '技术部', sUserType: '超级管理员', sLanguage: '中文', iIsVoid: 0, tLastLoginDate: null, sCreator: '系统', tCreateDate: '2024-01-01T00:00:00', }; const rowVoid1: UserVO = { id: 2, sUserName: '李丹', employeeName: '李丹', sUserNo: 'U002', departmentName: '客服部', sUserType: '普通用户', sLanguage: '中文', iIsVoid: 1, tLastLoginDate: '2024-02-01T10:00:00', sCreator: 'admin', tCreateDate: '2024-01-02T00:00:00', }; describe('exportUtils.buildUserCsv', () => { it('buildUserCsv has header row and maps 作废 0/1', () => { const csv = buildUserCsv([rowVoid0, rowVoid1]); const lines = csv.split('\n'); const header = lines[0]; // 中文表头逐字(不含序号列由前端生成,导出含中文列名) expect(header).toContain('用户名'); expect(header).toContain('员工名'); expect(header).toContain('作废'); expect(header).toContain('用户号'); expect(header).toContain('部门'); expect(header).toContain('用户类型'); expect(header).toContain('语言'); expect(header).toContain('登录日期'); expect(header).toContain('制单人'); expect(header).toContain('制单日期'); // 作废列 0→否、1→是 expect(lines[1]).toContain('否'); expect(lines[2]).toContain('是'); // 空值字段(employeeName=null)渲染为空串,不出现 "null" expect(csv).not.toContain('null'); }); }); describe('exportUtils.downloadCsv', () => { let createSpy: ReturnType; let clickSpy: ReturnType; let blobArg: Blob | null; beforeEach(() => { blobArg = null; createSpy = vi.fn((b: Blob) => { blobArg = b; return 'blob:mock'; }); clickSpy = vi.fn(); // jsdom 未实现 URL.createObjectURL / revokeObjectURL (URL as unknown as { createObjectURL: unknown }).createObjectURL = createSpy; (URL as unknown as { revokeObjectURL: unknown }).revokeObjectURL = vi.fn(); vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(clickSpy); }); afterEach(() => { vi.restoreAllMocks(); }); function readBlobBytes(blob: Blob): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(new Uint8Array(reader.result as ArrayBuffer)); reader.onerror = () => reject(reader.error); reader.readAsArrayBuffer(blob); }); } it('downloadCsv triggers blob download with UTF-8 BOM', async () => { downloadCsv('users.csv', 'x'); expect(createSpy).toHaveBeenCalledTimes(1); expect(clickSpy).toHaveBeenCalledTimes(1); expect(blobArg).toBeInstanceOf(Blob); const bytes = await readBlobBytes(blobArg!); // UTF-8 BOM 字节序列 EF BB BF expect(bytes[0]).toBe(0xef); expect(bytes[1]).toBe(0xbb); expect(bytes[2]).toBe(0xbf); // 内容 'x' 紧随 BOM 之后 expect(bytes[3]).toBe('x'.charCodeAt(0)); }); }); describe('UserList constants', () => { it('DEFAULT_QUERY defaults match BR2/BR3/D4', () => { expect(DEFAULT_QUERY.queryField).toBe('用户名'); expect(DEFAULT_QUERY.matchType).toBe('包含'); expect(DEFAULT_QUERY.queryValue).toBe(''); expect(DEFAULT_QUERY.pageNum).toBe(1); expect(DEFAULT_QUERY.pageSize).toBe(10); }); it('PAGE_SIZE_OPTIONS upper bound is 100', () => { expect(PAGE_SIZE_OPTIONS).toEqual([10, 20, 50, 100]); }); it('QUERY_FIELD_OPTIONS has 8 fields starting 用户名', () => { expect(QUERY_FIELD_OPTIONS).toHaveLength(8); expect(QUERY_FIELD_OPTIONS[0]).toBe('用户名'); expect(QUERY_FIELD_OPTIONS).toEqual([ '用户名', '员工名', '用户号', '部门', '用户类型', '作废', '登录日期', '制单人', ]); }); it('MATCH_TYPE_OPTIONS is 包含/不包含/等于', () => { expect(MATCH_TYPE_OPTIONS).toEqual(['包含', '不包含', '等于']); }); });