exportUtils.test.ts 4.42 KB
// 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<typeof vi.fn>;
  let clickSpy: ReturnType<typeof vi.fn>;
  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<Uint8Array> {
    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(['包含', '不包含', '等于']);
  });
});