@cloudcome/utils-browser 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/LICENSE +21 -0
- package/package.json +122 -1
- package/src/base64.ts +25 -0
- package/{dist/cache.cjs → src/cache.ts} +48 -20
- package/src/canvas.ts +147 -0
- package/{dist/clipboard.d.ts → src/clipboard.ts} +12 -1
- package/src/cookie.ts +118 -0
- package/{dist/dom.d.ts → src/dom.ts} +21 -3
- package/src/download.ts +22 -0
- package/src/dts/global.d.ts +27 -0
- package/src/image.ts +45 -0
- package/src/index.ts +1 -0
- package/src/timer.ts +56 -0
- package/src/video.ts +19 -0
- package/test/base64.test.ts +47 -0
- package/test/cache.test.ts +199 -0
- package/test/canvas.test.ts +124 -0
- package/test/clipboard.test.ts +18 -0
- package/test/cookie.test.ts +52 -0
- package/test/dom.test.ts +67 -0
- package/test/download.test.ts +15 -0
- package/test/image.test.ts +75 -0
- package/test/index.test.ts +6 -0
- package/test/timer.test.ts +109 -0
- package/test/video.test.ts +36 -0
- package/tsconfig.json +31 -0
- package/vite.config.mts +100 -0
- package/dist/base64.cjs +0 -15
- package/dist/base64.cjs.map +0 -1
- package/dist/base64.d.ts +0 -14
- package/dist/base64.mjs +0 -15
- package/dist/base64.mjs.map +0 -1
- package/dist/cache.cjs.map +0 -1
- package/dist/cache.d.ts +0 -49
- package/dist/cache.mjs +0 -87
- package/dist/cache.mjs.map +0 -1
- package/dist/canvas.cjs +0 -58
- package/dist/canvas.cjs.map +0 -1
- package/dist/canvas.d.ts +0 -92
- package/dist/canvas.mjs +0 -58
- package/dist/canvas.mjs.map +0 -1
- package/dist/clipboard.cjs +0 -16
- package/dist/clipboard.cjs.map +0 -1
- package/dist/clipboard.mjs +0 -16
- package/dist/clipboard.mjs.map +0 -1
- package/dist/cookie.cjs +0 -50
- package/dist/cookie.cjs.map +0 -1
- package/dist/cookie.d.ts +0 -55
- package/dist/cookie.mjs +0 -50
- package/dist/cookie.mjs.map +0 -1
- package/dist/dom.cjs +0 -18
- package/dist/dom.cjs.map +0 -1
- package/dist/dom.mjs +0 -18
- package/dist/dom.mjs.map +0 -1
- package/dist/download.cjs +0 -16
- package/dist/download.cjs.map +0 -1
- package/dist/download.d.ts +0 -12
- package/dist/download.mjs +0 -16
- package/dist/download.mjs.map +0 -1
- package/dist/image.cjs +0 -35
- package/dist/image.cjs.map +0 -1
- package/dist/image.d.ts +0 -9
- package/dist/image.mjs +0 -35
- package/dist/image.mjs.map +0 -1
- package/dist/index.cjs +0 -5
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.mjs +0 -5
- package/dist/index.mjs.map +0 -1
- package/dist/timer.cjs +0 -41
- package/dist/timer.cjs.map +0 -1
- package/dist/timer.d.ts +0 -9
- package/dist/timer.mjs +0 -41
- package/dist/timer.mjs.map +0 -1
- package/dist/video.cjs +0 -14
- package/dist/video.cjs.map +0 -1
- package/dist/video.d.ts +0 -9
- package/dist/video.mjs +0 -14
- package/dist/video.mjs.map +0 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file global.d.ts
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* package.json name
|
|
7
|
+
*/
|
|
8
|
+
declare const PKG_NAME: string;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* package.json version
|
|
12
|
+
*/
|
|
13
|
+
declare const PKG_VERSION: string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* package.json description
|
|
17
|
+
*/
|
|
18
|
+
declare const PKG_DESCRIPTION: string;
|
|
19
|
+
|
|
20
|
+
declare const IS_TEST: string;
|
|
21
|
+
|
|
22
|
+
interface TEST_MOCK {
|
|
23
|
+
IS_BROWSER: boolean;
|
|
24
|
+
IS_NODE: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
declare const TEST_MOCK: TEST_MOCK;
|
package/src/image.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { setStyle } from './dom';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 加载图片并返回一个包含 HTMLImageElement 的 Promise
|
|
5
|
+
* @param {string} url - 图片的 URL 地址
|
|
6
|
+
* @returns {Promise<HTMLImageElement>} 返回一个包含 HTMLImageElement 的 Promise
|
|
7
|
+
* @example
|
|
8
|
+
* const img = await imageLoad('https://example.com/image.png');
|
|
9
|
+
* @throws {Error} 如果图片加载失败,抛出错误
|
|
10
|
+
*/
|
|
11
|
+
export async function imageLoad(url: string) {
|
|
12
|
+
return new Promise<HTMLImageElement>((resolve, reject) => {
|
|
13
|
+
const image = new Image();
|
|
14
|
+
const onFinish = (isError?: boolean) => {
|
|
15
|
+
image.onload = image.onerror = null;
|
|
16
|
+
document.body.removeChild(image);
|
|
17
|
+
isError ? reject(new Error('图片加载失败')) : resolve(image);
|
|
18
|
+
};
|
|
19
|
+
image.onload = () => onFinish();
|
|
20
|
+
image.onerror = () => onFinish(true);
|
|
21
|
+
image.crossOrigin = 'anonymous';
|
|
22
|
+
image.src = url;
|
|
23
|
+
|
|
24
|
+
// ios 拍照产生的图片,如果没有插入的 DOM 中获取到的图片尺寸是相反的
|
|
25
|
+
setStyle(image, {
|
|
26
|
+
visibility: 'hidden',
|
|
27
|
+
position: 'absolute',
|
|
28
|
+
top: '-99999%',
|
|
29
|
+
left: '-99999%',
|
|
30
|
+
maxWidth: 'none',
|
|
31
|
+
maxHeight: 'none',
|
|
32
|
+
border: '0',
|
|
33
|
+
width: 'auto',
|
|
34
|
+
height: 'auto',
|
|
35
|
+
margin: '0',
|
|
36
|
+
padding: '0',
|
|
37
|
+
transform: '',
|
|
38
|
+
});
|
|
39
|
+
document.body.appendChild(image);
|
|
40
|
+
|
|
41
|
+
if (image.complete && image.width > 0) onFinish();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 图片缩放函数
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const VERSION = PKG_VERSION;
|
package/src/timer.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type TTimerHandler, type TTimerOptions, type TTimerState, makeInterval } from '@cloudcome/utils-core/timer';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 创建一个基于 `requestAnimationFrame` 的间隔定时器
|
|
5
|
+
*
|
|
6
|
+
* @param callback - 每次间隔执行的回调函数,接收定时器状态和可选的 `next` 函数
|
|
7
|
+
* @param options - 配置选项
|
|
8
|
+
* @returns {TTimerHandler} 返回包含控制方法的对象
|
|
9
|
+
*/
|
|
10
|
+
export function frameInterval(
|
|
11
|
+
callback: (state: TTimerState, next?: () => void) => unknown,
|
|
12
|
+
options?: TTimerOptions,
|
|
13
|
+
): TTimerHandler {
|
|
14
|
+
let rafId: number;
|
|
15
|
+
const { canStart, start, canStop, stop, canPause, pause, canResume, resume, execute } = makeInterval((call) => {
|
|
16
|
+
rafId = requestAnimationFrame(call);
|
|
17
|
+
}, callback);
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
start() {
|
|
21
|
+
if (!canStart()) return;
|
|
22
|
+
|
|
23
|
+
if (options?.leading) {
|
|
24
|
+
start();
|
|
25
|
+
} else {
|
|
26
|
+
rafId = requestAnimationFrame(start);
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
stop() {
|
|
31
|
+
if (!canStop()) return;
|
|
32
|
+
if (options?.trailing) execute();
|
|
33
|
+
|
|
34
|
+
cancelAnimationFrame(rafId);
|
|
35
|
+
stop();
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
pause() {
|
|
39
|
+
if (!canPause()) return;
|
|
40
|
+
if (options?.trailing) execute();
|
|
41
|
+
|
|
42
|
+
cancelAnimationFrame(rafId);
|
|
43
|
+
pause();
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
resume(immediate?: boolean) {
|
|
47
|
+
if (!canResume()) return;
|
|
48
|
+
|
|
49
|
+
if (immediate || options?.leading) {
|
|
50
|
+
resume();
|
|
51
|
+
} else {
|
|
52
|
+
rafId = requestAnimationFrame(resume);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
package/src/video.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 加载视频并返回一个包含 HTMLVideoElement 的 Promise
|
|
3
|
+
* @param {string} url - 视频的 URL 地址
|
|
4
|
+
* @returns {Promise<HTMLVideoElement>} 返回一个包含 HTMLVideoElement 的 Promise
|
|
5
|
+
* @example
|
|
6
|
+
* const video = await videoLoad('https://example.com/video.mp4');
|
|
7
|
+
* @throws {Error} 如果视频加载失败,抛出错误
|
|
8
|
+
*/
|
|
9
|
+
export async function videoLoad(url: string) {
|
|
10
|
+
return new Promise<HTMLVideoElement>((resolve, reject) => {
|
|
11
|
+
const video = document.createElement('video');
|
|
12
|
+
|
|
13
|
+
video.src = url;
|
|
14
|
+
video.crossOrigin = 'anonymous';
|
|
15
|
+
video.currentTime = 1;
|
|
16
|
+
video.onloadedmetadata = () => resolve(video);
|
|
17
|
+
video.onerror = () => reject(new Error('视频加载失败'));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { decodeBase64, encodeBase64 } from '@/base64';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('encodeBase64', () => {
|
|
5
|
+
it('应正确编码空字符串', () => {
|
|
6
|
+
expect(encodeBase64('')).toBe('');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('应正确编码普通字符串', () => {
|
|
10
|
+
expect(encodeBase64('hello')).toBe('aGVsbG8=');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('应正确编码特殊字符', () => {
|
|
14
|
+
expect(encodeBase64('!@#$%^&*()')).toBe('IUAjJCVeJiooKQ==');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('应正确编码中文字符', () => {
|
|
18
|
+
expect(encodeBase64('你好')).toBe('5L2g5aW9');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('decodeBase64', () => {
|
|
23
|
+
it('应正确解码空字符串', () => {
|
|
24
|
+
expect(decodeBase64('')).toBe('');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('应正确解码普通字符串', () => {
|
|
28
|
+
expect(decodeBase64('aGVsbG8=')).toBe('hello');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('应正确解码特殊字符', () => {
|
|
32
|
+
expect(decodeBase64('IUAjJCVeJiooKQ==')).toBe('!@#$%^&*()');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('应正确解码中文字符', () => {
|
|
36
|
+
expect(decodeBase64('5L2g5aW9')).toBe('你好');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('encode/decode 互操作', () => {
|
|
41
|
+
it('应能正确编码并解码回原始字符串', () => {
|
|
42
|
+
const original = 'Hello, 世界!';
|
|
43
|
+
const encoded = encodeBase64(original);
|
|
44
|
+
const decoded = decodeBase64(encoded);
|
|
45
|
+
expect(decoded).toBe(original);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { type Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { StorageCache, createLocalCache, createSessionCache } from '../src/cache';
|
|
3
|
+
|
|
4
|
+
describe('StorageCache', () => {
|
|
5
|
+
let storage: Storage;
|
|
6
|
+
let cache: StorageCache<string>;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
storage = {
|
|
10
|
+
getItem: vi.fn(),
|
|
11
|
+
setItem: vi.fn(),
|
|
12
|
+
removeItem: vi.fn(),
|
|
13
|
+
clear: vi.fn(),
|
|
14
|
+
length: 0,
|
|
15
|
+
key: vi.fn(),
|
|
16
|
+
} as unknown as Storage;
|
|
17
|
+
cache = new StorageCache(storage);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
vi.clearAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('constructor', () => {
|
|
25
|
+
it('应该正确初始化实例', () => {
|
|
26
|
+
expect(cache).toBeInstanceOf(StorageCache);
|
|
27
|
+
expect(cache.storage).toBe(storage);
|
|
28
|
+
expect(cache.namespace).toBe('');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('应该支持命名空间', () => {
|
|
32
|
+
const cacheWithNamespace = new StorageCache(storage, 'test');
|
|
33
|
+
expect(cacheWithNamespace.namespace).toBe('test');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('get()', () => {
|
|
38
|
+
it('应该返回null当缓存不存在时', () => {
|
|
39
|
+
(storage.getItem as Mock).mockReturnValue(null);
|
|
40
|
+
expect(cache.get('key')).toBeNull();
|
|
41
|
+
expect(storage.getItem).toBeCalledWith('key');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('应该返回null当缓存过期时', () => {
|
|
45
|
+
const expiredCache = JSON.stringify({
|
|
46
|
+
id: 'key',
|
|
47
|
+
data: 'value',
|
|
48
|
+
createdAt: Date.now() - 10000,
|
|
49
|
+
maxAge: 5000,
|
|
50
|
+
});
|
|
51
|
+
(storage.getItem as Mock).mockReturnValue(expiredCache);
|
|
52
|
+
expect(cache.get('key')).toBeNull();
|
|
53
|
+
expect(storage.removeItem).toBeCalledWith('key');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('应该返回缓存数据当缓存有效时', () => {
|
|
57
|
+
const validCache = JSON.stringify({
|
|
58
|
+
id: 'key',
|
|
59
|
+
data: 'value',
|
|
60
|
+
createdAt: Date.now(),
|
|
61
|
+
maxAge: 5000,
|
|
62
|
+
});
|
|
63
|
+
(storage.getItem as Mock).mockReturnValue(validCache);
|
|
64
|
+
const result = cache.get('key');
|
|
65
|
+
expect(result?.data).toBe('value');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('应该返回null当缓存数据解析失败时', () => {
|
|
69
|
+
(storage.getItem as Mock).mockReturnValue('invalid json');
|
|
70
|
+
expect(cache.get('key')).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('set()', () => {
|
|
75
|
+
it('应该成功设置缓存', () => {
|
|
76
|
+
cache.set('key', 'value');
|
|
77
|
+
expect(storage.setItem).toBeCalled();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('应该支持设置缓存过期时间', () => {
|
|
81
|
+
cache.set('key', 'value', { maxAge: 1000 });
|
|
82
|
+
const [, value] = (storage.setItem as Mock).mock.calls[0];
|
|
83
|
+
const cacheData = JSON.parse(value);
|
|
84
|
+
expect(cacheData.maxAge).toBe(1000);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('del()', () => {
|
|
89
|
+
it('应该成功删除缓存', () => {
|
|
90
|
+
cache.del('key');
|
|
91
|
+
expect(storage.removeItem).toBeCalledWith('key');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('应该处理删除缓存失败的情况', () => {
|
|
95
|
+
(storage.removeItem as Mock).mockImplementation(() => {
|
|
96
|
+
throw new Error('Remove failed');
|
|
97
|
+
});
|
|
98
|
+
expect(() => cache.del('key')).not.toThrow();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('命名空间', () => {
|
|
103
|
+
it('应该在键前添加命名空间', () => {
|
|
104
|
+
const cacheWithNamespace = new StorageCache(storage, 'test');
|
|
105
|
+
cacheWithNamespace.set('key', 'value');
|
|
106
|
+
expect(storage.setItem).toBeCalledWith('test:key', expect.anything());
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('应该在获取缓存时使用命名空间', () => {
|
|
110
|
+
const cacheWithNamespace = new StorageCache(storage, 'test');
|
|
111
|
+
cacheWithNamespace.get('key');
|
|
112
|
+
expect(storage.getItem).toBeCalledWith('test:key');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('应该在删除缓存时使用命名空间', () => {
|
|
116
|
+
const cacheWithNamespace = new StorageCache(storage, 'test');
|
|
117
|
+
cacheWithNamespace.del('key');
|
|
118
|
+
expect(storage.removeItem).toBeCalledWith('test:key');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('clear()', () => {
|
|
123
|
+
it('应该清空所有缓存', () => {
|
|
124
|
+
cache.clear();
|
|
125
|
+
expect(storage.clear).toBeCalled();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('应该处理清空缓存失败的情况', () => {
|
|
129
|
+
(storage.clear as Mock).mockImplementation(() => {
|
|
130
|
+
throw new Error('Clear failed');
|
|
131
|
+
});
|
|
132
|
+
expect(() => cache.clear()).not.toThrow();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('缓存数据格式', () => {
|
|
137
|
+
it('应该正确序列化缓存数据', () => {
|
|
138
|
+
cache.set('key', 'value');
|
|
139
|
+
const [, value] = (storage.setItem as Mock).mock.calls[0];
|
|
140
|
+
const cacheData = JSON.parse(value);
|
|
141
|
+
expect(cacheData).toEqual({
|
|
142
|
+
id: 'key',
|
|
143
|
+
data: 'value',
|
|
144
|
+
createdAt: expect.any(Number),
|
|
145
|
+
maxAge: 0,
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('应该正确处理非字符串数据', () => {
|
|
150
|
+
const objCache = new StorageCache<object>(storage);
|
|
151
|
+
objCache.set('key', { foo: 'bar' });
|
|
152
|
+
const [, value] = (storage.setItem as Mock).mock.calls[0];
|
|
153
|
+
const cacheData = JSON.parse(value);
|
|
154
|
+
expect(cacheData.data).toEqual({ foo: 'bar' });
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('存储失败', () => {
|
|
159
|
+
it('应该处理getItem失败的情况', () => {
|
|
160
|
+
(storage.getItem as Mock).mockImplementation(() => {
|
|
161
|
+
throw new Error('Get failed');
|
|
162
|
+
});
|
|
163
|
+
expect(() => cache.get('key')).not.toThrow();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('应该处理setItem失败的情况', () => {
|
|
167
|
+
(storage.setItem as Mock).mockImplementation(() => {
|
|
168
|
+
throw new Error('Set failed');
|
|
169
|
+
});
|
|
170
|
+
expect(() => cache.set('key', 'value')).not.toThrow();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('createLocalCache', () => {
|
|
176
|
+
it('应该创建使用localStorage的缓存实例', () => {
|
|
177
|
+
const cache = createLocalCache<string>() as StorageCache<string>;
|
|
178
|
+
expect(cache).toBeInstanceOf(StorageCache);
|
|
179
|
+
expect(cache.storage).toBe(localStorage);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('应该支持命名空间', () => {
|
|
183
|
+
const cache = createLocalCache<string>('test') as StorageCache<string>;
|
|
184
|
+
expect(cache.namespace).toBe('test');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('createSessionCache', () => {
|
|
189
|
+
it('应该创建使用sessionStorage的缓存实例', () => {
|
|
190
|
+
const cache = createSessionCache<string>() as StorageCache<string>;
|
|
191
|
+
expect(cache).toBeInstanceOf(StorageCache);
|
|
192
|
+
expect(cache.storage).toBe(sessionStorage);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('应该支持命名空间', () => {
|
|
196
|
+
const cache = createSessionCache<string>('test') as StorageCache<string>;
|
|
197
|
+
expect(cache.namespace).toBe('test');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { canvasDrawImage, canvasToBase64, canvasToBlob } from '@/canvas';
|
|
2
|
+
import { imageLoad } from '@/image';
|
|
3
|
+
import { loadImage } from 'canvas';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
|
|
6
|
+
describe('canvasToBase64', () => {
|
|
7
|
+
it('应返回默认 png 格式的 base64 字符串', () => {
|
|
8
|
+
const canvas = document.createElement('canvas');
|
|
9
|
+
canvas.width = 100;
|
|
10
|
+
canvas.height = 100;
|
|
11
|
+
|
|
12
|
+
const result = canvasToBase64(canvas);
|
|
13
|
+
expect(result).toMatch(/^data:image\/png;base64,/);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('应返回指定格式和质量的 base64 字符串', () => {
|
|
17
|
+
const canvas = document.createElement('canvas');
|
|
18
|
+
canvas.width = 100;
|
|
19
|
+
canvas.height = 100;
|
|
20
|
+
|
|
21
|
+
const result = canvasToBase64(canvas, 'image/jpeg', 0.8);
|
|
22
|
+
expect(result).toMatch(/^data:image\/jpeg;base64,/);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('canvasToBlob', () => {
|
|
27
|
+
it('应返回默认 png 格式的 Blob 对象', async () => {
|
|
28
|
+
const canvas = document.createElement('canvas');
|
|
29
|
+
canvas.width = 100;
|
|
30
|
+
canvas.height = 100;
|
|
31
|
+
|
|
32
|
+
const blob = await canvasToBlob(canvas);
|
|
33
|
+
expect(blob).toBeInstanceOf(Blob);
|
|
34
|
+
expect(blob.type).toBe('image/png');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('应返回指定格式和质量的 Blob 对象', async () => {
|
|
38
|
+
const canvas = document.createElement('canvas');
|
|
39
|
+
canvas.width = 100;
|
|
40
|
+
canvas.height = 100;
|
|
41
|
+
|
|
42
|
+
const blob = await canvasToBlob(canvas, 'image/jpeg', 0.9);
|
|
43
|
+
expect(blob).toBeInstanceOf(Blob);
|
|
44
|
+
expect(blob.type).toBe('image/jpeg');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('当 canvas toBlob 失败时应拒绝', async () => {
|
|
48
|
+
const canvas = document.createElement('canvas');
|
|
49
|
+
vi.spyOn(canvas, 'toBlob').mockImplementationOnce((callback) => callback(null));
|
|
50
|
+
|
|
51
|
+
await expect(canvasToBlob(canvas)).rejects.toThrow('canvas 导出二进制对象失败');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('canvasDrawImage', () => {
|
|
56
|
+
it(
|
|
57
|
+
'应使用默认选项绘制图像',
|
|
58
|
+
async () => {
|
|
59
|
+
const canvas = document.createElement('canvas');
|
|
60
|
+
canvas.width = 200;
|
|
61
|
+
canvas.height = 200;
|
|
62
|
+
const ctx = canvas.getContext('2d');
|
|
63
|
+
if (!ctx) throw new Error('Canvas context is null');
|
|
64
|
+
|
|
65
|
+
const spy = vi.spyOn(ctx, 'drawImage').mockImplementation(() => true);
|
|
66
|
+
const src = 'https://www.baidu.com/img/PCfb_5bf082d29588c07f842ccde3f97243ea.png';
|
|
67
|
+
const img = await imageLoad(src);
|
|
68
|
+
|
|
69
|
+
await canvasDrawImage(canvas, img.src);
|
|
70
|
+
expect(spy).toHaveBeenCalledWith(img, 0, 0, img.width, img.height, 0, 0, canvas.width, canvas.height);
|
|
71
|
+
},
|
|
72
|
+
{ timeout: 0 },
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
it(
|
|
76
|
+
'应使用自定义选项绘制图像',
|
|
77
|
+
async () => {
|
|
78
|
+
const canvas = document.createElement('canvas');
|
|
79
|
+
canvas.width = 200;
|
|
80
|
+
canvas.height = 200;
|
|
81
|
+
const ctx = canvas.getContext('2d');
|
|
82
|
+
if (!ctx) throw new Error('Canvas context is null');
|
|
83
|
+
|
|
84
|
+
const spy = vi.spyOn(ctx, 'drawImage').mockImplementation(() => true);
|
|
85
|
+
const src = 'https://www.baidu.com/img/PCfb_5bf082d29588c07f842ccde3f97243ea.png';
|
|
86
|
+
const img = await imageLoad(src);
|
|
87
|
+
|
|
88
|
+
const options = {
|
|
89
|
+
srcLeft: 10,
|
|
90
|
+
srcTop: 10,
|
|
91
|
+
srcWidth: 50,
|
|
92
|
+
srcHeight: 50,
|
|
93
|
+
destLeft: 20,
|
|
94
|
+
destTop: 20,
|
|
95
|
+
destWidth: 100,
|
|
96
|
+
destHeight: 100,
|
|
97
|
+
};
|
|
98
|
+
await canvasDrawImage(canvas, img.src, options);
|
|
99
|
+
expect(spy).toHaveBeenCalledWith(
|
|
100
|
+
img,
|
|
101
|
+
options.srcLeft,
|
|
102
|
+
options.srcTop,
|
|
103
|
+
options.srcWidth,
|
|
104
|
+
options.srcHeight,
|
|
105
|
+
options.destLeft,
|
|
106
|
+
options.destTop,
|
|
107
|
+
options.destWidth,
|
|
108
|
+
options.destHeight,
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
{ timeout: 0 },
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
it(
|
|
115
|
+
'当 canvas context 为 null 时应抛出错误',
|
|
116
|
+
async () => {
|
|
117
|
+
const canvas = document.createElement('canvas');
|
|
118
|
+
vi.spyOn(canvas, 'getContext').mockReturnValueOnce(null);
|
|
119
|
+
|
|
120
|
+
await expect(canvasDrawImage(canvas, 'https://example.com/image.png')).rejects.toThrow('canvas context is null');
|
|
121
|
+
},
|
|
122
|
+
{ timeout: 0 },
|
|
123
|
+
);
|
|
124
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { copyText } from '@/clipboard';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('copyText', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
vi.resetAllMocks();
|
|
7
|
+
document.body.innerHTML = '';
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('应在复制后移除 textarea 元素', () => {
|
|
11
|
+
const execCommandMock = vi.fn();
|
|
12
|
+
document.execCommand = execCommandMock;
|
|
13
|
+
|
|
14
|
+
copyText('test text');
|
|
15
|
+
|
|
16
|
+
expect(document.body.querySelector('textarea')).toBeNull();
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { cookieDel, cookieGet, cookieSet } from '@/cookie';
|
|
2
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('Cookie 工具函数', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
// 清空所有 Cookie
|
|
7
|
+
for (const cookie of document.cookie.split(';')) {
|
|
8
|
+
const eqPos = cookie.indexOf('=');
|
|
9
|
+
const name = eqPos > -1 ? cookie.slice(0, eqPos) : cookie;
|
|
10
|
+
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT`;
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('cookieGet', () => {
|
|
15
|
+
it('应返回已存在 cookie 的值', () => {
|
|
16
|
+
document.cookie = 'testKey=testValue';
|
|
17
|
+
expect(cookieGet('testKey')).toBe('testValue');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('对于不存在的 cookie 应返回空字符串', () => {
|
|
21
|
+
expect(cookieGet('nonExistingKey')).toBe('');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('cookieSet', () => {
|
|
26
|
+
it('应使用默认选项设置 cookie', () => {
|
|
27
|
+
cookieSet('defaultKey', 'defaultValue');
|
|
28
|
+
expect(document.cookie).toContain('defaultKey=defaultValue');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('应使用自定义选项设置 cookie', () => {
|
|
32
|
+
cookieSet('customKey', 'customValue', {
|
|
33
|
+
expires: new Date('2030-01-01'),
|
|
34
|
+
path: '/',
|
|
35
|
+
domain: location.host,
|
|
36
|
+
secure: true,
|
|
37
|
+
sameSite: 'none',
|
|
38
|
+
httpOnly: false,
|
|
39
|
+
maxAge: 3600,
|
|
40
|
+
});
|
|
41
|
+
expect(document.cookie).toEqual('');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('cookieDel', () => {
|
|
46
|
+
it('应通过设置过期时间为过去来删除 cookie', () => {
|
|
47
|
+
document.cookie = 'deleteKey=deleteValue';
|
|
48
|
+
cookieDel('deleteKey');
|
|
49
|
+
expect(document.cookie).not.toContain('deleteKey=deleteValue');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
package/test/dom.test.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { getStyle, setStyle } from '@/dom';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('setStyle', () => {
|
|
5
|
+
it('应该能够通过字符串设置样式', () => {
|
|
6
|
+
const el = document.createElement('div');
|
|
7
|
+
setStyle(el, 'color: red; background: blue;');
|
|
8
|
+
|
|
9
|
+
expect(el.style.color).toBe('red');
|
|
10
|
+
expect(el.style.background).toBe('blue');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('应该能够通过对象设置样式', () => {
|
|
14
|
+
const el = document.createElement('div');
|
|
15
|
+
setStyle(el, { color: 'red', width: '16px' });
|
|
16
|
+
|
|
17
|
+
expect(el.style.color).toBe('red');
|
|
18
|
+
expect(el.style.width).toBe('16px');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('应该能够设置自定义属性', () => {
|
|
22
|
+
const el = document.createElement('div');
|
|
23
|
+
setStyle(el, { '--custom-property': 'value' });
|
|
24
|
+
|
|
25
|
+
expect(el.style.getPropertyValue('--custom-property')).toBe('value');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('使用字符串设置样式时应覆盖现有样式', () => {
|
|
29
|
+
const el = document.createElement('div');
|
|
30
|
+
el.style.color = 'green';
|
|
31
|
+
setStyle(el, 'color: red;');
|
|
32
|
+
|
|
33
|
+
expect(el.style.color).toBe('red');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('使用对象设置样式时应合并样式', () => {
|
|
37
|
+
const el = document.createElement('div');
|
|
38
|
+
el.style.color = 'green';
|
|
39
|
+
setStyle(el, { width: '100px' });
|
|
40
|
+
|
|
41
|
+
expect(el.style.color).toBe('green');
|
|
42
|
+
expect(el.style.width).toBe('100px');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('getStyle', () => {
|
|
47
|
+
it('应该能够获取计算样式', () => {
|
|
48
|
+
const el = document.createElement('div');
|
|
49
|
+
el.style.color = 'red';
|
|
50
|
+
|
|
51
|
+
vi.spyOn(window, 'getComputedStyle').mockReturnValue({
|
|
52
|
+
getPropertyValue: (prop: string) => (prop === 'color' ? 'red' : ''),
|
|
53
|
+
} as CSSStyleDeclaration);
|
|
54
|
+
|
|
55
|
+
expect(getStyle(el, 'color')).toBe('red');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('对于不存在的属性应返回空字符串', () => {
|
|
59
|
+
const el = document.createElement('div');
|
|
60
|
+
|
|
61
|
+
const mockStyle = new CSSStyleDeclaration();
|
|
62
|
+
vi.spyOn(mockStyle, 'getPropertyValue').mockReturnValue('');
|
|
63
|
+
vi.spyOn(window, 'getComputedStyle').mockReturnValue(mockStyle);
|
|
64
|
+
|
|
65
|
+
expect(getStyle(el, 'color')).toBe('');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { downloadBlob, downloadURL } from '@/download';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
it('download', () => {
|
|
5
|
+
downloadURL('/');
|
|
6
|
+
downloadURL('/', 'file');
|
|
7
|
+
|
|
8
|
+
URL.createObjectURL = vi.fn(() => '/');
|
|
9
|
+
URL.revokeObjectURL = vi.fn();
|
|
10
|
+
|
|
11
|
+
downloadBlob(new Blob());
|
|
12
|
+
downloadBlob(new Blob(), 'file');
|
|
13
|
+
|
|
14
|
+
expect(URL.createObjectURL).toBeTypeOf('function');
|
|
15
|
+
});
|