feat(image): 新建 knowai-core:1.0.0 镜像并完成推送
Some checks reported errors
continuous-integration/drone/push Build was killed
Some checks reported errors
continuous-integration/drone/push Build was killed
- 搭建 api、auth、utils 等逻辑模块 - 通过 tsc、eslint、vitest 测试验证 BREAKING CHANGE: 新镜像分支
This commit is contained in:
482
test/unit/utils/data.test.ts
Normal file
482
test/unit/utils/data.test.ts
Normal file
@@ -0,0 +1,482 @@
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
157
test/unit/utils/date.test.ts
Normal file
157
test/unit/utils/date.test.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { dateUtils } from '@/utils/date';
|
||||
|
||||
describe('dateUtils', () => {
|
||||
// 模拟当前时间
|
||||
const mockCurrentDate = new Date('2023-05-15T12:00:00Z');
|
||||
|
||||
beforeEach(() => {
|
||||
// 使用 vi.useFakeTimers 模拟当前时间
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(mockCurrentDate);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// 恢复真实时间
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('formatDate', () => {
|
||||
it('应该格式化日期对象', () => {
|
||||
const date = new Date('2023-05-15T10:30:45Z');
|
||||
expect(dateUtils.formatDate(date, 'YYYY-MM-DD')).toBe('2023-05-15');
|
||||
expect(dateUtils.formatDate(date, 'YYYY-MM-DD HH:mm:ss')).toBe('2023-05-15 10:30:45');
|
||||
});
|
||||
|
||||
it('应该格式化日期字符串', () => {
|
||||
const dateString = '2023-05-15T10:30:45Z';
|
||||
expect(dateUtils.formatDate(dateString, 'YYYY-MM-DD')).toBe('2023-05-15');
|
||||
expect(dateUtils.formatDate(dateString, 'YYYY-MM-DD HH:mm')).toBe('2023-05-15 10:30');
|
||||
});
|
||||
|
||||
it('应该使用默认格式', () => {
|
||||
const date = new Date('2023-05-15T10:30:45Z');
|
||||
expect(dateUtils.formatDate(date)).toBe('2023-05-15');
|
||||
});
|
||||
|
||||
it('应该处理无效日期', () => {
|
||||
const invalidDate = new Date('invalid');
|
||||
expect(dateUtils.formatDate(invalidDate)).toBe('');
|
||||
expect(dateUtils.formatDate('invalid date')).toBe('');
|
||||
});
|
||||
|
||||
it('应该正确格式化单个数字的月份和日期', () => {
|
||||
const date = new Date('2023-01-02T03:04:05Z');
|
||||
expect(dateUtils.formatDate(date, 'YYYY-MM-DD HH:mm:ss')).toBe('2023-01-02 03:04:05');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRelativeTime', () => {
|
||||
it('应该显示"刚刚"对于几秒前的时间', () => {
|
||||
const date = new Date(mockCurrentDate.getTime() - 30 * 1000); // 30秒前
|
||||
expect(dateUtils.getRelativeTime(date)).toBe('刚刚');
|
||||
});
|
||||
|
||||
it('应该显示"X分钟前"对于几分钟前的时间', () => {
|
||||
const date = new Date(mockCurrentDate.getTime() - 5 * 60 * 1000); // 5分钟前
|
||||
expect(dateUtils.getRelativeTime(date)).toBe('5分钟前');
|
||||
});
|
||||
|
||||
it('应该显示"X小时前"对于几小时前的时间', () => {
|
||||
const date = new Date(mockCurrentDate.getTime() - 3 * 60 * 60 * 1000); // 3小时前
|
||||
expect(dateUtils.getRelativeTime(date)).toBe('3小时前');
|
||||
});
|
||||
|
||||
it('应该显示"X天前"对于几天前的时间', () => {
|
||||
const date = new Date(mockCurrentDate.getTime() - 5 * 24 * 60 * 60 * 1000); // 5天前
|
||||
expect(dateUtils.getRelativeTime(date)).toBe('5天前');
|
||||
});
|
||||
|
||||
it('应该显示"X周前"对于几周前的时间', () => {
|
||||
const date = new Date(mockCurrentDate.getTime() - 2 * 7 * 24 * 60 * 60 * 1000); // 2周前
|
||||
expect(dateUtils.getRelativeTime(date)).toBe('2周前');
|
||||
});
|
||||
|
||||
it('应该显示"X个月前"对于几个月前的时间', () => {
|
||||
const date = new Date(mockCurrentDate.getTime() - 3 * 30 * 24 * 60 * 60 * 1000); // 3个月前
|
||||
expect(dateUtils.getRelativeTime(date)).toBe('3个月前');
|
||||
});
|
||||
|
||||
it('应该显示"X年前"对于几年前的时间', () => {
|
||||
const date = new Date(mockCurrentDate.getTime() - 2 * 365 * 24 * 60 * 60 * 1000); // 2年前
|
||||
expect(dateUtils.getRelativeTime(date)).toBe('2年前');
|
||||
});
|
||||
|
||||
it('应该返回绝对时间对于未来的时间', () => {
|
||||
const futureDate = new Date(mockCurrentDate.getTime() + 24 * 60 * 60 * 1000); // 1天后
|
||||
expect(dateUtils.getRelativeTime(futureDate)).toBe('2023-05-16 12:00');
|
||||
});
|
||||
|
||||
it('应该处理字符串日期', () => {
|
||||
const dateString = '2023-05-14T12:00:00Z'; // 1天前
|
||||
expect(dateUtils.getRelativeTime(dateString)).toBe('1天前');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isToday', () => {
|
||||
it('应该识别今天的日期', () => {
|
||||
const today = new Date(mockCurrentDate);
|
||||
expect(dateUtils.isToday(today)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该识别不是今天的日期', () => {
|
||||
const yesterday = new Date(mockCurrentDate.getTime() - 24 * 60 * 60 * 1000);
|
||||
const tomorrow = new Date(mockCurrentDate.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
expect(dateUtils.isToday(yesterday)).toBe(false);
|
||||
expect(dateUtils.isToday(tomorrow)).toBe(false);
|
||||
});
|
||||
|
||||
it('应该处理字符串日期', () => {
|
||||
const todayString = mockCurrentDate.toISOString();
|
||||
expect(dateUtils.isToday(todayString)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该处理不同时间但同一天的日期', () => {
|
||||
const morning = new Date(mockCurrentDate);
|
||||
morning.setHours(0, 0, 0, 0);
|
||||
|
||||
const evening = new Date(mockCurrentDate);
|
||||
evening.setHours(23, 59, 59, 999);
|
||||
|
||||
expect(dateUtils.isToday(morning)).toBe(true);
|
||||
expect(dateUtils.isToday(evening)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isYesterday', () => {
|
||||
it('应该识别昨天的日期', () => {
|
||||
const yesterday = new Date(mockCurrentDate.getTime() - 24 * 60 * 60 * 1000);
|
||||
expect(dateUtils.isYesterday(yesterday)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该识别不是昨天的日期', () => {
|
||||
const today = new Date(mockCurrentDate);
|
||||
const twoDaysAgo = new Date(mockCurrentDate.getTime() - 2 * 24 * 60 * 60 * 1000);
|
||||
|
||||
expect(dateUtils.isYesterday(today)).toBe(false);
|
||||
expect(dateUtils.isYesterday(twoDaysAgo)).toBe(false);
|
||||
});
|
||||
|
||||
it('应该处理字符串日期', () => {
|
||||
const yesterdayString = new Date(mockCurrentDate.getTime() - 24 * 60 * 60 * 1000).toISOString();
|
||||
expect(dateUtils.isYesterday(yesterdayString)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该处理不同时间但昨天的日期', () => {
|
||||
const yesterdayMorning = new Date(mockCurrentDate.getTime() - 24 * 60 * 60 * 1000);
|
||||
yesterdayMorning.setHours(0, 0, 0, 0);
|
||||
|
||||
const yesterdayEvening = new Date(mockCurrentDate.getTime() - 24 * 60 * 60 * 1000);
|
||||
yesterdayEvening.setHours(23, 59, 59, 999);
|
||||
|
||||
expect(dateUtils.isYesterday(yesterdayMorning)).toBe(true);
|
||||
expect(dateUtils.isYesterday(yesterdayEvening)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
183
test/unit/utils/string.test.ts
Normal file
183
test/unit/utils/string.test.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { stringUtils } from '@/utils/string';
|
||||
|
||||
describe('stringUtils', () => {
|
||||
describe('truncate', () => {
|
||||
it('应该截断长字符串并添加省略号', () => {
|
||||
const longString = 'This is a very long string that should be truncated';
|
||||
const result = stringUtils.truncate(longString, 20);
|
||||
expect(result).toBe('This is a very lo...');
|
||||
});
|
||||
|
||||
it('应该使用自定义后缀', () => {
|
||||
const longString = 'This is a very long string that should be truncated';
|
||||
const result = stringUtils.truncate(longString, 21, ' [more]');
|
||||
expect(result).toBe('This is a very [more]');
|
||||
});
|
||||
|
||||
it('不应该截断短字符串', () => {
|
||||
const shortString = 'Short string';
|
||||
const result = stringUtils.truncate(shortString, 20);
|
||||
expect(result).toBe(shortString);
|
||||
});
|
||||
|
||||
it('应该处理长度等于最大长度的字符串', () => {
|
||||
const exactString = 'Exact length';
|
||||
const result = stringUtils.truncate(exactString, 12);
|
||||
expect(result).toBe(exactString);
|
||||
});
|
||||
});
|
||||
|
||||
describe('capitalize', () => {
|
||||
it('应该将字符串首字母大写', () => {
|
||||
expect(stringUtils.capitalize('hello')).toBe('Hello');
|
||||
expect(stringUtils.capitalize('world')).toBe('World');
|
||||
});
|
||||
|
||||
it('应该处理空字符串', () => {
|
||||
expect(stringUtils.capitalize('')).toBe('');
|
||||
});
|
||||
|
||||
it('应该保持已大写的首字母', () => {
|
||||
expect(stringUtils.capitalize('Hello')).toBe('Hello');
|
||||
});
|
||||
|
||||
it('应该只改变首字母,保持其余部分不变', () => {
|
||||
expect(stringUtils.capitalize('hELLO')).toBe('HELLO');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toCamelCase', () => {
|
||||
it('应该将短横线分隔的字符串转换为驼峰命名', () => {
|
||||
expect(stringUtils.toCamelCase('hello-world')).toBe('helloWorld');
|
||||
expect(stringUtils.toCamelCase('my-variable-name')).toBe('myVariableName');
|
||||
});
|
||||
|
||||
it('应该将下划线分隔的字符串转换为驼峰命名', () => {
|
||||
expect(stringUtils.toCamelCase('hello_world')).toBe('helloWorld');
|
||||
expect(stringUtils.toCamelCase('my_variable_name')).toBe('myVariableName');
|
||||
});
|
||||
|
||||
it('应该将空格分隔的字符串转换为驼峰命名', () => {
|
||||
expect(stringUtils.toCamelCase('hello world')).toBe('helloWorld');
|
||||
expect(stringUtils.toCamelCase('my variable name')).toBe('myVariableName');
|
||||
});
|
||||
|
||||
it('应该处理混合分隔符', () => {
|
||||
expect(stringUtils.toCamelCase('hello-world_test variable')).toBe('helloWorldTestVariable');
|
||||
});
|
||||
|
||||
it('应该处理空字符串', () => {
|
||||
expect(stringUtils.toCamelCase('')).toBe('');
|
||||
});
|
||||
|
||||
it('应该处理已经驼峰命名的字符串', () => {
|
||||
expect(stringUtils.toCamelCase('helloWorld')).toBe('helloWorld');
|
||||
});
|
||||
|
||||
it('应该将首字母小写', () => {
|
||||
expect(stringUtils.toCamelCase('Hello-World')).toBe('helloWorld');
|
||||
expect(stringUtils.toCamelCase('HELLO_WORLD')).toBe('helloWorld');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toKebabCase', () => {
|
||||
it('应该将驼峰命名转换为短横线分隔', () => {
|
||||
expect(stringUtils.toKebabCase('helloWorld')).toBe('hello-world');
|
||||
expect(stringUtils.toKebabCase('myVariableName')).toBe('my-variable-name');
|
||||
});
|
||||
|
||||
it('应该处理已短横线分隔的字符串', () => {
|
||||
expect(stringUtils.toKebabCase('hello-world')).toBe('hello-world');
|
||||
});
|
||||
|
||||
it('应该处理下划线分隔的字符串', () => {
|
||||
expect(stringUtils.toKebabCase('hello_world')).toBe('hello-world');
|
||||
});
|
||||
|
||||
it('应该处理空字符串', () => {
|
||||
expect(stringUtils.toKebabCase('')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toSnakeCase', () => {
|
||||
it('应该将驼峰命名转换为下划线分隔', () => {
|
||||
expect(stringUtils.toSnakeCase('helloWorld')).toBe('hello_world');
|
||||
expect(stringUtils.toSnakeCase('myVariableName')).toBe('my_variable_name');
|
||||
});
|
||||
|
||||
it('应该处理已下划线分隔的字符串', () => {
|
||||
expect(stringUtils.toSnakeCase('hello_world')).toBe('hello_world');
|
||||
});
|
||||
|
||||
it('应该处理短横线分隔的字符串', () => {
|
||||
expect(stringUtils.toSnakeCase('hello-world')).toBe('hello_world');
|
||||
});
|
||||
|
||||
it('应该处理空字符串', () => {
|
||||
expect(stringUtils.toSnakeCase('')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeHtml', () => {
|
||||
it('应该转义HTML特殊字符', () => {
|
||||
expect(stringUtils.escapeHtml('<div>hello & world</div>')).toBe('<div>hello & world</div>');
|
||||
expect(stringUtils.escapeHtml('a "quote" and \'apostrophe\'')).toBe('a "quote" and 'apostrophe'');
|
||||
});
|
||||
|
||||
it('应该处理不包含特殊字符的字符串', () => {
|
||||
expect(stringUtils.escapeHtml('plain text')).toBe('plain text');
|
||||
});
|
||||
|
||||
it('应该处理空字符串', () => {
|
||||
expect(stringUtils.escapeHtml('')).toBe('');
|
||||
});
|
||||
|
||||
it('应该只转义已知的特殊字符', () => {
|
||||
expect(stringUtils.escapeHtml('hello @ world #test')).toBe('hello @ world #test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('randomString', () => {
|
||||
it('应该生成指定长度的随机字符串', () => {
|
||||
const length = 10;
|
||||
const result = stringUtils.randomString(length);
|
||||
expect(result).toHaveLength(length);
|
||||
});
|
||||
|
||||
it('应该生成不同的随机字符串', () => {
|
||||
const result1 = stringUtils.randomString(10);
|
||||
const result2 = stringUtils.randomString(10);
|
||||
expect(result1).not.toBe(result2);
|
||||
});
|
||||
|
||||
it('应该使用默认长度', () => {
|
||||
const result = stringUtils.randomString();
|
||||
expect(result).toHaveLength(10);
|
||||
});
|
||||
|
||||
it('应该只包含字母和数字', () => {
|
||||
const result = stringUtils.randomString(100);
|
||||
const regex = /^[A-Za-z0-9]+$/;
|
||||
expect(regex.test(result)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isEmpty', () => {
|
||||
it('应该识别空字符串', () => {
|
||||
expect(stringUtils.isEmpty('')).toBe(true);
|
||||
expect(stringUtils.isEmpty(' ')).toBe(true);
|
||||
expect(stringUtils.isEmpty('\t\n')).toBe(true);
|
||||
});
|
||||
|
||||
it('应该识别非空字符串', () => {
|
||||
expect(stringUtils.isEmpty('hello')).toBe(false);
|
||||
expect(stringUtils.isEmpty(' hello ')).toBe(false);
|
||||
});
|
||||
|
||||
it('应该处理null和undefined', () => {
|
||||
expect(stringUtils.isEmpty(null)).toBe(true);
|
||||
expect(stringUtils.isEmpty(undefined)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
224
test/unit/utils/validation.test.ts
Normal file
224
test/unit/utils/validation.test.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { validationUtils } from '@/utils/validation';
|
||||
|
||||
describe('validationUtils', () => {
|
||||
describe('isEmail', () => {
|
||||
it('应该验证有效的邮箱地址', () => {
|
||||
expect(validationUtils.isEmail('test@example.com')).toBe(true);
|
||||
expect(validationUtils.isEmail('user.name@domain.co.uk')).toBe(true);
|
||||
expect(validationUtils.isEmail('user+tag@example.org')).toBe(true);
|
||||
});
|
||||
|
||||
it('应该拒绝无效的邮箱地址', () => {
|
||||
expect(validationUtils.isEmail('')).toBe(false);
|
||||
expect(validationUtils.isEmail('test')).toBe(false);
|
||||
expect(validationUtils.isEmail('test@')).toBe(false);
|
||||
expect(validationUtils.isEmail('@example.com')).toBe(false);
|
||||
expect(validationUtils.isEmail('test@.com')).toBe(false);
|
||||
expect(validationUtils.isEmail('test@example')).toBe(false);
|
||||
expect(validationUtils.isEmail('test example.com')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPhoneNumber', () => {
|
||||
it('应该验证有效的手机号(中国大陆)', () => {
|
||||
expect(validationUtils.isPhoneNumber('13812345678')).toBe(true);
|
||||
expect(validationUtils.isPhoneNumber('15987654321')).toBe(true);
|
||||
expect(validationUtils.isPhoneNumber('18612345678')).toBe(true);
|
||||
});
|
||||
|
||||
it('应该拒绝无效的手机号', () => {
|
||||
expect(validationUtils.isPhoneNumber('')).toBe(false);
|
||||
expect(validationUtils.isPhoneNumber('12345678901')).toBe(false);
|
||||
expect(validationUtils.isPhoneNumber('12812345678')).toBe(false);
|
||||
expect(validationUtils.isPhoneNumber('1381234567')).toBe(false);
|
||||
expect(validationUtils.isPhoneNumber('138123456789')).toBe(false);
|
||||
expect(validationUtils.isPhoneNumber('abcdefghijk')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isIdCard', () => {
|
||||
it('应该验证有效的身份证号(中国大陆)', () => {
|
||||
expect(validationUtils.isIdCard('123456789012345')).toBe(true); // 15位
|
||||
expect(validationUtils.isIdCard('123456789012345678')).toBe(true); // 18位数字
|
||||
expect(validationUtils.isIdCard('12345678901234567X')).toBe(true); // 18位以X结尾
|
||||
expect(validationUtils.isIdCard('12345678901234567x')).toBe(true); // 18位以x结尾
|
||||
});
|
||||
|
||||
it('应该拒绝无效的身份证号', () => {
|
||||
expect(validationUtils.isIdCard('')).toBe(false);
|
||||
expect(validationUtils.isIdCard('12345678901234')).toBe(false); // 14位
|
||||
expect(validationUtils.isIdCard('1234567890123456')).toBe(false); // 16位
|
||||
expect(validationUtils.isIdCard('1234567890123456789')).toBe(false); // 19位
|
||||
expect(validationUtils.isIdCard('12345678901234567Y')).toBe(false); // 18位以Y结尾
|
||||
});
|
||||
});
|
||||
|
||||
describe('isStrongPassword', () => {
|
||||
it('应该验证强密码', () => {
|
||||
expect(validationUtils.isStrongPassword('Password123!')).toBe(true);
|
||||
expect(validationUtils.isStrongPassword('StrongPass@2023')).toBe(true);
|
||||
expect(validationUtils.isStrongPassword('Pass123!')).toBe(true);
|
||||
});
|
||||
|
||||
it('应该拒绝弱密码', () => {
|
||||
expect(validationUtils.isStrongPassword('')).toBe(false);
|
||||
expect(validationUtils.isStrongPassword('password')).toBe(false); // 没有大写字母、数字和特殊字符
|
||||
expect(validationUtils.isStrongPassword('PASSWORD')).toBe(false); // 没有小写字母、数字和特殊字符
|
||||
expect(validationUtils.isStrongPassword('12345678')).toBe(false); // 没有字母和特殊字符
|
||||
expect(validationUtils.isStrongPassword('Password')).toBe(false); // 没有数字和特殊字符
|
||||
expect(validationUtils.isStrongPassword('Password123')).toBe(false); // 没有特殊字符
|
||||
});
|
||||
});
|
||||
|
||||
describe('isEmpty', () => {
|
||||
it('应该识别空值', () => {
|
||||
expect(validationUtils.isEmpty(null)).toBe(true);
|
||||
expect(validationUtils.isEmpty(undefined)).toBe(true);
|
||||
expect(validationUtils.isEmpty('')).toBe(true);
|
||||
expect(validationUtils.isEmpty(' ')).toBe(true);
|
||||
expect(validationUtils.isEmpty([])).toBe(true);
|
||||
expect(validationUtils.isEmpty({})).toBe(true);
|
||||
});
|
||||
|
||||
it('应该识别非空值', () => {
|
||||
expect(validationUtils.isEmpty('text')).toBe(false);
|
||||
expect(validationUtils.isEmpty(' text ')).toBe(false);
|
||||
expect(validationUtils.isEmpty([1, 2, 3])).toBe(false);
|
||||
expect(validationUtils.isEmpty({ key: 'value' })).toBe(false);
|
||||
expect(validationUtils.isEmpty(0)).toBe(false);
|
||||
expect(validationUtils.isEmpty(false)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isInRange', () => {
|
||||
it('应该验证范围内的数字', () => {
|
||||
expect(validationUtils.isInRange(5, 1, 10)).toBe(true);
|
||||
expect(validationUtils.isInRange(1, 1, 10)).toBe(true);
|
||||
expect(validationUtils.isInRange(10, 1, 10)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该拒绝超出范围的数字', () => {
|
||||
expect(validationUtils.isInRange(0, 1, 10)).toBe(false);
|
||||
expect(validationUtils.isInRange(11, 1, 10)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLengthValid', () => {
|
||||
it('应该验证有效长度', () => {
|
||||
expect(validationUtils.isLengthValid('hello', 3, 10)).toBe(true);
|
||||
expect(validationUtils.isLengthValid('hello', 5)).toBe(true);
|
||||
expect(validationUtils.isLengthValid('hello', 1, 5)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该拒绝无效长度', () => {
|
||||
expect(validationUtils.isLengthValid('hello', 6, 10)).toBe(false); // 太短
|
||||
expect(validationUtils.isLengthValid('hello', 1, 4)).toBe(false); // 太长
|
||||
expect(validationUtils.isLengthValid('', 1, 10)).toBe(false); // 太短
|
||||
});
|
||||
|
||||
it('应该处理只有最小长度的情况', () => {
|
||||
expect(validationUtils.isLengthValid('hello', 5)).toBe(true);
|
||||
expect(validationUtils.isLengthValid('hello', 6)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNumber', () => {
|
||||
it('应该验证数字', () => {
|
||||
expect(validationUtils.isNumber(0)).toBe(true);
|
||||
expect(validationUtils.isNumber(123)).toBe(true);
|
||||
expect(validationUtils.isNumber(-456)).toBe(true);
|
||||
expect(validationUtils.isNumber(3.14)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该拒绝非数字', () => {
|
||||
expect(validationUtils.isNumber('123')).toBe(false);
|
||||
expect(validationUtils.isNumber(NaN)).toBe(false);
|
||||
expect(validationUtils.isNumber(Infinity)).toBe(true); // Infinity被认为是数字
|
||||
expect(validationUtils.isNumber(null)).toBe(false);
|
||||
expect(validationUtils.isNumber(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isInteger', () => {
|
||||
it('应该验证整数', () => {
|
||||
expect(validationUtils.isInteger(0)).toBe(true);
|
||||
expect(validationUtils.isInteger(123)).toBe(true);
|
||||
expect(validationUtils.isInteger(-456)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该拒绝非整数', () => {
|
||||
expect(validationUtils.isInteger(3.14)).toBe(false);
|
||||
expect(validationUtils.isInteger('123')).toBe(false);
|
||||
expect(validationUtils.isInteger(NaN)).toBe(false);
|
||||
expect(validationUtils.isInteger(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isPositive', () => {
|
||||
it('应该验证正数', () => {
|
||||
expect(validationUtils.isPositive(1)).toBe(true);
|
||||
expect(validationUtils.isPositive(123.456)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该拒绝非正数', () => {
|
||||
expect(validationUtils.isPositive(0)).toBe(false);
|
||||
expect(validationUtils.isPositive(-1)).toBe(false);
|
||||
expect(validationUtils.isPositive(-123.456)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNegative', () => {
|
||||
it('应该验证负数', () => {
|
||||
expect(validationUtils.isNegative(-1)).toBe(true);
|
||||
expect(validationUtils.isNegative(-123.456)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该拒绝非负数', () => {
|
||||
expect(validationUtils.isNegative(0)).toBe(false);
|
||||
expect(validationUtils.isNegative(1)).toBe(false);
|
||||
expect(validationUtils.isNegative(123.456)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isEven', () => {
|
||||
it('应该验证偶数', () => {
|
||||
expect(validationUtils.isEven(0)).toBe(true);
|
||||
expect(validationUtils.isEven(2)).toBe(true);
|
||||
expect(validationUtils.isEven(-4)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该拒绝奇数', () => {
|
||||
expect(validationUtils.isEven(1)).toBe(false);
|
||||
expect(validationUtils.isEven(3)).toBe(false);
|
||||
expect(validationUtils.isEven(-5)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isOdd', () => {
|
||||
it('应该验证奇数', () => {
|
||||
expect(validationUtils.isOdd(1)).toBe(true);
|
||||
expect(validationUtils.isOdd(3)).toBe(true);
|
||||
expect(validationUtils.isOdd(-5)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该拒绝偶数', () => {
|
||||
expect(validationUtils.isOdd(0)).toBe(false);
|
||||
expect(validationUtils.isOdd(2)).toBe(false);
|
||||
expect(validationUtils.isOdd(-4)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLeapYear', () => {
|
||||
it('应该验证闰年', () => {
|
||||
expect(validationUtils.isLeapYear(2000)).toBe(true); // 能被400整除
|
||||
expect(validationUtils.isLeapYear(2020)).toBe(true); // 能被4整除但不能被100整除
|
||||
});
|
||||
|
||||
it('应该拒绝非闰年', () => {
|
||||
expect(validationUtils.isLeapYear(1900)).toBe(false); // 能被100整除但不能被400整除
|
||||
expect(validationUtils.isLeapYear(2023)).toBe(false); // 不能被4整除
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user