Some checks reported errors
continuous-integration/drone/push Build was killed
- 搭建 api、auth、utils 等逻辑模块 - 通过 tsc、eslint、vitest 测试验证 BREAKING CHANGE: 新镜像分支
483 lines
13 KiB
TypeScript
483 lines
13 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||
import {
|
||
throttle,
|
||
deepClone,
|
||
deepEqual,
|
||
pick,
|
||
omit,
|
||
merge,
|
||
toQueryString,
|
||
fromQueryString,
|
||
unique,
|
||
groupBy,
|
||
sortBy,
|
||
debounce
|
||
} from '@/utils/data';
|
||
|
||
describe('数据处理工具函数', () => {
|
||
describe('throttle', () => {
|
||
beforeEach(() => {
|
||
vi.useFakeTimers();
|
||
});
|
||
|
||
afterEach(() => {
|
||
vi.useRealTimers();
|
||
});
|
||
|
||
it('应该节流函数调用', () => {
|
||
const mockFn = vi.fn();
|
||
const throttledFn = throttle(mockFn, 100);
|
||
|
||
// 多次调用
|
||
throttledFn();
|
||
throttledFn();
|
||
throttledFn();
|
||
|
||
// 在等待时间内,函数应该只被调用一次
|
||
expect(mockFn).toHaveBeenCalledTimes(1);
|
||
|
||
// 快进时间
|
||
vi.advanceTimersByTime(100);
|
||
|
||
// 再次调用
|
||
throttledFn();
|
||
throttledFn();
|
||
|
||
// 函数应该被调用第二次
|
||
expect(mockFn).toHaveBeenCalledTimes(2);
|
||
});
|
||
|
||
it('应该支持leading选项', () => {
|
||
const mockFn = vi.fn();
|
||
const throttledFn = throttle(mockFn, 100, { leading: false });
|
||
|
||
// 多次调用
|
||
throttledFn();
|
||
throttledFn();
|
||
|
||
// 由于leading为false,函数不应该被调用
|
||
expect(mockFn).toHaveBeenCalledTimes(0);
|
||
|
||
// 快进时间
|
||
vi.advanceTimersByTime(100);
|
||
|
||
// 函数应该被调用
|
||
expect(mockFn).toHaveBeenCalledTimes(1);
|
||
});
|
||
|
||
it('应该支持trailing选项', () => {
|
||
const mockFn = vi.fn();
|
||
const throttledFn = throttle(mockFn, 100, { trailing: false });
|
||
|
||
// 多次调用
|
||
throttledFn();
|
||
throttledFn();
|
||
|
||
// 快进时间
|
||
vi.advanceTimersByTime(100);
|
||
|
||
// 由于trailing为false,函数应该只被调用一次(第一次调用)
|
||
expect(mockFn).toHaveBeenCalledTimes(1);
|
||
});
|
||
});
|
||
|
||
describe('debounce', () => {
|
||
beforeEach(() => {
|
||
vi.useFakeTimers();
|
||
});
|
||
|
||
afterEach(() => {
|
||
vi.useRealTimers();
|
||
});
|
||
|
||
it('应该防抖函数调用', () => {
|
||
const mockFn = vi.fn();
|
||
const debouncedFn = debounce(mockFn, 100);
|
||
|
||
// 多次调用
|
||
debouncedFn();
|
||
debouncedFn();
|
||
debouncedFn();
|
||
|
||
// 在等待时间内,函数不应该被调用
|
||
expect(mockFn).toHaveBeenCalledTimes(0);
|
||
|
||
// 快进时间
|
||
vi.advanceTimersByTime(100);
|
||
|
||
// 函数应该被调用一次
|
||
expect(mockFn).toHaveBeenCalledTimes(1);
|
||
});
|
||
|
||
it('应该支持immediate选项', () => {
|
||
const mockFn = vi.fn();
|
||
const debouncedFn = debounce(mockFn, 100, true);
|
||
|
||
// 第一次调用
|
||
debouncedFn();
|
||
|
||
// 由于immediate为true,函数应该立即被调用
|
||
expect(mockFn).toHaveBeenCalledTimes(1);
|
||
|
||
// 再次调用(在等待时间内)
|
||
debouncedFn();
|
||
|
||
// 函数仍然应该只被调用一次(第二次调用被防抖了)
|
||
expect(mockFn).toHaveBeenCalledTimes(1);
|
||
|
||
// 快进时间超过等待期
|
||
vi.advanceTimersByTime(100);
|
||
|
||
// 再次调用(等待期后)
|
||
debouncedFn();
|
||
|
||
// 函数应该被调用第二次(立即执行)
|
||
expect(mockFn).toHaveBeenCalledTimes(2);
|
||
});
|
||
});
|
||
|
||
describe('deepClone', () => {
|
||
it('应该深拷贝基本类型', () => {
|
||
expect(deepClone(null)).toBe(null);
|
||
expect(deepClone(undefined)).toBe(undefined);
|
||
expect(deepClone(123)).toBe(123);
|
||
expect(deepClone('string')).toBe('string');
|
||
expect(deepClone(true)).toBe(true);
|
||
});
|
||
|
||
it('应该深拷贝日期对象', () => {
|
||
const date = new Date('2023-05-15T10:30:45Z');
|
||
const clonedDate = deepClone(date);
|
||
|
||
expect(clonedDate).not.toBe(date); // 不是同一个对象
|
||
expect(clonedDate.getTime()).toBe(date.getTime()); // 但时间相同
|
||
});
|
||
|
||
it('应该深拷贝数组', () => {
|
||
const array = [1, 'string', { a: 1 }, [2, 3]];
|
||
const clonedArray = deepClone(array);
|
||
|
||
expect(clonedArray).not.toBe(array); // 不是同一个对象
|
||
expect(clonedArray).toEqual(array); // 但内容相同
|
||
expect(clonedArray[2]).not.toBe(array[2]); // 嵌套对象也被深拷贝
|
||
expect(clonedArray[3]).not.toBe(array[3]); // 嵌套数组也被深拷贝
|
||
});
|
||
|
||
it('应该深拷贝对象', () => {
|
||
const obj = {
|
||
a: 1,
|
||
b: 'string',
|
||
c: {
|
||
d: 2,
|
||
e: [3, 4]
|
||
},
|
||
f: new Date('2023-05-15T10:30:45Z')
|
||
};
|
||
const clonedObj = deepClone(obj);
|
||
|
||
expect(clonedObj).not.toBe(obj); // 不是同一个对象
|
||
expect(clonedObj).toEqual(obj); // 但内容相同
|
||
expect(clonedObj.c).not.toBe(obj.c); // 嵌套对象也被深拷贝
|
||
expect(clonedObj.c.e).not.toBe(obj.c.e); // 嵌套数组也被深拷贝
|
||
expect(clonedObj.f).not.toBe(obj.f); // 日期对象也被深拷贝
|
||
});
|
||
});
|
||
|
||
describe('deepEqual', () => {
|
||
it('应该比较基本类型', () => {
|
||
expect(deepEqual(1, 1)).toBe(true);
|
||
expect(deepEqual('string', 'string')).toBe(true);
|
||
expect(deepEqual(true, true)).toBe(true);
|
||
expect(deepEqual(null, null)).toBe(true);
|
||
expect(deepEqual(undefined, undefined)).toBe(true);
|
||
|
||
expect(deepEqual(1, 2)).toBe(false);
|
||
expect(deepEqual('string', 'other')).toBe(false);
|
||
expect(deepEqual(true, false)).toBe(false);
|
||
expect(deepEqual(null, undefined)).toBe(false);
|
||
});
|
||
|
||
it('应该比较数组', () => {
|
||
expect(deepEqual([1, 2, 3], [1, 2, 3])).toBe(true);
|
||
expect(deepEqual([1, 2, 3], [1, 2, 4])).toBe(false);
|
||
expect(deepEqual([1, 2, 3], [1, 2])).toBe(false);
|
||
expect(deepEqual([1, 2, 3], [3, 2, 1])).toBe(false);
|
||
});
|
||
|
||
it('应该比较对象', () => {
|
||
expect(deepEqual({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true);
|
||
expect(deepEqual({ a: 1, b: 2 }, { a: 1, b: 3 })).toBe(false);
|
||
expect(deepEqual({ a: 1, b: 2 }, { a: 1 })).toBe(false);
|
||
expect(deepEqual({ a: 1, b: 2 }, { b: 2, a: 1 })).toBe(true); // 顺序不重要
|
||
});
|
||
|
||
it('应该深度比较嵌套对象', () => {
|
||
const obj1 = { a: 1, b: { c: 2, d: [3, 4] } };
|
||
const obj2 = { a: 1, b: { c: 2, d: [3, 4] } };
|
||
const obj3 = { a: 1, b: { c: 2, d: [3, 5] } };
|
||
|
||
expect(deepEqual(obj1, obj2)).toBe(true);
|
||
expect(deepEqual(obj1, obj3)).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('pick', () => {
|
||
it('应该从对象中选取指定的属性', () => {
|
||
const obj = { a: 1, b: 2, c: 3, d: 4 };
|
||
const result = pick(obj, ['a', 'c']);
|
||
|
||
expect(result).toEqual({ a: 1, c: 3 });
|
||
expect(result).not.toBe(obj); // 返回新对象
|
||
});
|
||
|
||
it('应该处理不存在的属性', () => {
|
||
const obj = { a: 1, b: 2 };
|
||
const result = pick(obj, ['a', 'c']);
|
||
|
||
expect(result).toEqual({ a: 1 });
|
||
});
|
||
|
||
it('应该处理空键数组', () => {
|
||
const obj = { a: 1, b: 2 };
|
||
const result = pick(obj, []);
|
||
|
||
expect(result).toEqual({});
|
||
});
|
||
});
|
||
|
||
describe('omit', () => {
|
||
it('应该从对象中排除指定的属性', () => {
|
||
const obj = { a: 1, b: 2, c: 3, d: 4 };
|
||
const result = omit(obj, ['a', 'c']);
|
||
|
||
expect(result).toEqual({ b: 2, d: 4 });
|
||
expect(result).not.toBe(obj); // 返回新对象
|
||
});
|
||
|
||
it('应该处理不存在的属性', () => {
|
||
const obj = { a: 1, b: 2 };
|
||
const result = omit(obj, ['a', 'c']);
|
||
|
||
expect(result).toEqual({ b: 2 });
|
||
});
|
||
|
||
it('应该处理空键数组', () => {
|
||
const obj = { a: 1, b: 2 };
|
||
const result = omit(obj, []);
|
||
|
||
expect(result).toEqual(obj);
|
||
expect(result).not.toBe(obj); // 仍然返回新对象
|
||
});
|
||
});
|
||
|
||
describe('merge', () => {
|
||
it('应该合并多个对象', () => {
|
||
const obj1 = { a: 1, b: 2 };
|
||
const obj2 = { b: 3, c: 4 };
|
||
const obj3 = { d: 5 };
|
||
|
||
const result = merge(obj1, obj2, obj3);
|
||
|
||
expect(result).toEqual({ a: 1, b: 3, c: 4, d: 5 });
|
||
});
|
||
|
||
it('应该处理null或undefined对象', () => {
|
||
const obj1 = { a: 1 };
|
||
const obj2 = null;
|
||
const obj3 = undefined;
|
||
const obj4 = { b: 2 };
|
||
|
||
const result = merge(obj1, obj2, obj3, obj4);
|
||
|
||
expect(result).toEqual({ a: 1, b: 2 });
|
||
});
|
||
|
||
it('应该处理非对象值', () => {
|
||
const obj1 = { a: 1 };
|
||
const obj2 = 'string';
|
||
const obj3 = 123;
|
||
|
||
const result = merge(obj1, obj2, obj3);
|
||
|
||
expect(result).toEqual({ a: 1 });
|
||
});
|
||
});
|
||
|
||
describe('toQueryString', () => {
|
||
it('应该将对象转换为查询字符串', () => {
|
||
const obj = { a: '1', b: 'test', c: 'value' };
|
||
const result = toQueryString(obj);
|
||
|
||
expect(result).toBe('a=1&b=test&c=value');
|
||
});
|
||
|
||
it('应该处理null和undefined值', () => {
|
||
const obj = { a: '1', b: null, c: undefined, d: '4' };
|
||
const result = toQueryString(obj);
|
||
|
||
expect(result).toBe('a=1&d=4');
|
||
});
|
||
|
||
it('应该处理数字和布尔值', () => {
|
||
const obj = { a: 1, b: true, c: false };
|
||
const result = toQueryString(obj);
|
||
|
||
expect(result).toBe('a=1&b=true&c=false');
|
||
});
|
||
|
||
it('应该处理空对象', () => {
|
||
const obj = {};
|
||
const result = toQueryString(obj);
|
||
|
||
expect(result).toBe('');
|
||
});
|
||
});
|
||
|
||
describe('fromQueryString', () => {
|
||
it('应该将查询字符串转换为对象', () => {
|
||
const queryString = 'a=1&b=test&c=value';
|
||
const result = fromQueryString(queryString);
|
||
|
||
expect(result).toEqual({ a: '1', b: 'test', c: 'value' });
|
||
});
|
||
|
||
it('应该处理空查询字符串', () => {
|
||
const queryString = '';
|
||
const result = fromQueryString(queryString);
|
||
|
||
expect(result).toEqual({});
|
||
});
|
||
|
||
it('应该处理特殊字符', () => {
|
||
const queryString = 'a=hello%20world&b=test%20%26%20value';
|
||
const result = fromQueryString(queryString);
|
||
|
||
expect(result).toEqual({ a: 'hello world', b: 'test & value' });
|
||
});
|
||
});
|
||
|
||
describe('unique', () => {
|
||
it('应该对基本类型数组去重', () => {
|
||
const array = [1, 2, 3, 2, 4, 1, 5];
|
||
const result = unique(array);
|
||
|
||
expect(result).toEqual([1, 2, 3, 4, 5]);
|
||
});
|
||
|
||
it('应该使用键函数对复杂对象数组去重', () => {
|
||
const array = [
|
||
{ id: 1, name: 'Alice' },
|
||
{ id: 2, name: 'Bob' },
|
||
{ id: 1, name: 'Alice Smith' },
|
||
{ id: 3, name: 'Charlie' },
|
||
{ id: 2, name: 'Bob Johnson' }
|
||
];
|
||
|
||
const result = unique(array, (item: { id: number }) => item.id);
|
||
|
||
expect(result).toEqual([
|
||
{ id: 1, name: 'Alice' },
|
||
{ id: 2, name: 'Bob' },
|
||
{ id: 3, name: 'Charlie' }
|
||
]);
|
||
});
|
||
|
||
it('应该处理空数组', () => {
|
||
const array: number[] = [];
|
||
const result = unique(array);
|
||
|
||
expect(result).toEqual([]);
|
||
});
|
||
});
|
||
|
||
describe('groupBy', () => {
|
||
it('应该按指定键对数组分组', () => {
|
||
const array = [
|
||
{ category: 'fruit', name: 'Apple' },
|
||
{ category: 'vegetable', name: 'Carrot' },
|
||
{ category: 'fruit', name: 'Banana' },
|
||
{ category: 'fruit', name: 'Orange' },
|
||
{ category: 'vegetable', name: 'Broccoli' }
|
||
];
|
||
|
||
const result = groupBy(array, (item: { category: string }) => item.category);
|
||
|
||
expect(result).toEqual({
|
||
fruit: [
|
||
{ category: 'fruit', name: 'Apple' },
|
||
{ category: 'fruit', name: 'Banana' },
|
||
{ category: 'fruit', name: 'Orange' }
|
||
],
|
||
vegetable: [
|
||
{ category: 'vegetable', name: 'Carrot' },
|
||
{ category: 'vegetable', name: 'Broccoli' }
|
||
]
|
||
});
|
||
});
|
||
|
||
it('应该处理空数组', () => {
|
||
const array: any[] = [];
|
||
const result = groupBy(array, (item: { category: string }) => item.category);
|
||
|
||
expect(result).toEqual({});
|
||
});
|
||
|
||
it('应该处理数字键', () => {
|
||
const array = [
|
||
{ score: 90, name: 'Alice' },
|
||
{ score: 80, name: 'Bob' },
|
||
{ score: 90, name: 'Charlie' },
|
||
{ score: 70, name: 'David' }
|
||
];
|
||
|
||
const result = groupBy(array, (item: { score: number }) => item.score);
|
||
|
||
expect(result).toEqual({
|
||
90: [
|
||
{ score: 90, name: 'Alice' },
|
||
{ score: 90, name: 'Charlie' }
|
||
],
|
||
80: [
|
||
{ score: 80, name: 'Bob' }
|
||
],
|
||
70: [
|
||
{ score: 70, name: 'David' }
|
||
]
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('sortBy', () => {
|
||
it('应该使用默认比较函数对数组排序', () => {
|
||
const array = [3, 1, 4, 1, 5, 9, 2, 6];
|
||
const result = sortBy(array);
|
||
|
||
expect(result).toEqual([1, 1, 2, 3, 4, 5, 6, 9]);
|
||
expect(result).not.toBe(array); // 返回新数组
|
||
});
|
||
|
||
it('应该使用自定义比较函数对数组排序', () => {
|
||
const array = [
|
||
{ name: 'Alice', age: 30 },
|
||
{ name: 'Bob', age: 25 },
|
||
{ name: 'Charlie', age: 35 }
|
||
];
|
||
|
||
const result = sortBy(array, (a: { age: number }, b: { age: number }) => a.age - b.age);
|
||
|
||
expect(result).toEqual([
|
||
{ name: 'Bob', age: 25 },
|
||
{ name: 'Alice', age: 30 },
|
||
{ name: 'Charlie', age: 35 }
|
||
]);
|
||
});
|
||
|
||
it('应该处理空数组', () => {
|
||
const array: any[] = [];
|
||
const result = sortBy(array);
|
||
|
||
expect(result).toEqual([]);
|
||
});
|
||
});
|
||
});
|