@zhin.js/core 1.0.19 → 1.0.21
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 +20 -0
- package/lib/bot.d.ts +3 -1
- package/lib/bot.d.ts.map +1 -1
- package/lib/built/adapter-process.d.ts +3 -3
- package/lib/built/adapter-process.d.ts.map +1 -1
- package/lib/built/adapter-process.js +2 -2
- package/lib/built/adapter-process.js.map +1 -1
- package/lib/built/config.d.ts.map +1 -1
- package/lib/built/config.js +12 -1
- package/lib/built/config.js.map +1 -1
- package/lib/built/database.d.ts +1 -1
- package/lib/jsx-runtime.d.ts +2 -2
- package/lib/message.d.ts +3 -2
- package/lib/message.d.ts.map +1 -1
- package/lib/plugin.d.ts +2 -2
- package/lib/plugin.d.ts.map +1 -1
- package/lib/plugin.js.map +1 -1
- package/lib/types.d.ts +2 -1
- package/lib/types.d.ts.map +1 -1
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +16 -4
- package/lib/utils.js.map +1 -1
- package/package.json +4 -5
- package/src/bot.ts +3 -1
- package/src/built/adapter-process.ts +4 -4
- package/src/built/config.ts +17 -1
- package/src/message.ts +3 -3
- package/src/plugin.ts +2 -2
- package/src/types.ts +2 -1
- package/src/utils.ts +20 -4
- package/tests/adapter.test.ts +381 -4
- package/tests/config.test.ts +147 -4
- package/tests/cron.test.ts +11 -0
- package/tests/jsx-runtime.test.ts +45 -0
- package/tests/plugin.test.ts +705 -0
- package/tests/prompt.test.ts +78 -0
- package/tests/redos-protection.test.ts +197 -0
- package/tests/utils.test.ts +575 -4
package/tests/utils.test.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import {
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
compiler,
|
|
4
|
+
evaluate,
|
|
5
|
+
compose,
|
|
6
|
+
segment,
|
|
7
|
+
remove,
|
|
8
|
+
isEmpty,
|
|
9
|
+
Time,
|
|
10
|
+
clearEvalCache,
|
|
11
|
+
getEvalCacheStats,
|
|
12
|
+
execute,
|
|
13
|
+
getValueWithRuntime,
|
|
14
|
+
sleep
|
|
15
|
+
} from '../src/utils'
|
|
3
16
|
|
|
4
17
|
describe('Template Security', () => {
|
|
5
18
|
|
|
@@ -39,14 +52,26 @@ describe('Template Security', () => {
|
|
|
39
52
|
expect(result).toBe('User: Alice (25)')
|
|
40
53
|
})
|
|
41
54
|
|
|
42
|
-
|
|
43
55
|
it('should allow safe Math expressions', () => {
|
|
44
56
|
const result = evaluate('Math.PI', {})
|
|
45
57
|
expect(result).toBeCloseTo(3.14159)
|
|
46
58
|
})
|
|
47
|
-
})
|
|
48
59
|
|
|
60
|
+
it('should allow access to safe process properties', () => {
|
|
61
|
+
const result = evaluate('process.version', {})
|
|
62
|
+
expect(result).toBe(process.version)
|
|
63
|
+
})
|
|
49
64
|
|
|
65
|
+
it('should block Buffer access', () => {
|
|
66
|
+
const result = evaluate('Buffer', {})
|
|
67
|
+
expect(result).toBeUndefined()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should block crypto access', () => {
|
|
71
|
+
const result = evaluate('crypto', {})
|
|
72
|
+
expect(result).toBeUndefined()
|
|
73
|
+
})
|
|
74
|
+
})
|
|
50
75
|
|
|
51
76
|
describe('Template Functionality', () => {
|
|
52
77
|
it('should handle multiple template variables', () => {
|
|
@@ -68,4 +93,550 @@ describe('Template Functionality', () => {
|
|
|
68
93
|
// Should return template with undefined when evaluation fails
|
|
69
94
|
expect(result).toBe('Result: undefined')
|
|
70
95
|
})
|
|
96
|
+
|
|
97
|
+
it('should handle templates without variables', () => {
|
|
98
|
+
const template = 'Hello World!'
|
|
99
|
+
const result = compiler(template, {})
|
|
100
|
+
expect(result).toBe('Hello World!')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should handle empty template', () => {
|
|
104
|
+
const template = ''
|
|
105
|
+
const result = compiler(template, {})
|
|
106
|
+
expect(result).toBe('')
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe('evaluate and execute', () => {
|
|
111
|
+
beforeEach(() => {
|
|
112
|
+
clearEvalCache()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('should evaluate simple expressions', () => {
|
|
116
|
+
expect(evaluate('1 + 1', {})).toBe(2)
|
|
117
|
+
expect(evaluate('2 * 3', {})).toBe(6)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should return undefined for blocked access', () => {
|
|
121
|
+
expect(evaluate('global.something', {})).toBeUndefined()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should use cache for repeated expressions', () => {
|
|
125
|
+
const expr = '1 + 1'
|
|
126
|
+
const result1 = execute(expr, {})
|
|
127
|
+
const result2 = execute(expr, {})
|
|
128
|
+
expect(result1).toBe(result2)
|
|
129
|
+
expect(getEvalCacheStats().size).toBe(1)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('should limit cache size', () => {
|
|
133
|
+
clearEvalCache()
|
|
134
|
+
// 添加超过 MAX_EVAL_CACHE_SIZE 的表达式
|
|
135
|
+
for (let i = 0; i < 150; i++) {
|
|
136
|
+
execute(`1 + ${i}`, {})
|
|
137
|
+
}
|
|
138
|
+
const stats = getEvalCacheStats()
|
|
139
|
+
expect(stats.size).toBeLessThanOrEqual(stats.maxSize)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('should handle invalid expressions gracefully', () => {
|
|
143
|
+
const result = execute('invalid syntax here !!!', {})
|
|
144
|
+
// 无效表达式会被 try-catch 捕获,返回 undefined
|
|
145
|
+
expect(result).toBeUndefined()
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('should provide safe process context', () => {
|
|
149
|
+
const result = execute('return process.platform', {})
|
|
150
|
+
expect(result).toBe(process.platform)
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('getValueWithRuntime', () => {
|
|
155
|
+
it('should return value from context', () => {
|
|
156
|
+
expect(getValueWithRuntime('name', { name: 'Alice' })).toBe('Alice')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('should return undefined for blocked access', () => {
|
|
160
|
+
expect(getValueWithRuntime('global', {})).toBeUndefined()
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('should handle complex expressions', () => {
|
|
164
|
+
expect(getValueWithRuntime('user.name', { user: { name: 'Bob' } })).toBe('Bob')
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
describe('compose middleware', () => {
|
|
169
|
+
it('should return empty function for empty middlewares', async () => {
|
|
170
|
+
const composed = compose([])
|
|
171
|
+
await expect(composed({} as any, async () => {})).resolves.toBeUndefined()
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('should handle single middleware', async () => {
|
|
175
|
+
let called = false
|
|
176
|
+
const middleware = async (msg: any, next: any) => {
|
|
177
|
+
called = true
|
|
178
|
+
await next()
|
|
179
|
+
}
|
|
180
|
+
const composed = compose([middleware])
|
|
181
|
+
await composed({} as any, async () => {})
|
|
182
|
+
expect(called).toBe(true)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('should compose multiple middlewares in order', async () => {
|
|
186
|
+
const order: number[] = []
|
|
187
|
+
const middleware1 = async (msg: any, next: any) => {
|
|
188
|
+
order.push(1)
|
|
189
|
+
await next()
|
|
190
|
+
order.push(4)
|
|
191
|
+
}
|
|
192
|
+
const middleware2 = async (msg: any, next: any) => {
|
|
193
|
+
order.push(2)
|
|
194
|
+
await next()
|
|
195
|
+
order.push(3)
|
|
196
|
+
}
|
|
197
|
+
const composed = compose([middleware1, middleware2])
|
|
198
|
+
await composed({} as any, async () => {})
|
|
199
|
+
expect(order).toEqual([1, 2, 3, 4])
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('should handle middleware execution order correctly', async () => {
|
|
203
|
+
const order: string[] = []
|
|
204
|
+
const middleware1 = async (msg: any, next: any) => {
|
|
205
|
+
order.push('m1-before')
|
|
206
|
+
await next()
|
|
207
|
+
order.push('m1-after')
|
|
208
|
+
}
|
|
209
|
+
const middleware2 = async (msg: any, next: any) => {
|
|
210
|
+
order.push('m2-before')
|
|
211
|
+
await next()
|
|
212
|
+
order.push('m2-after')
|
|
213
|
+
}
|
|
214
|
+
const middleware3 = async (msg: any, next: any) => {
|
|
215
|
+
order.push('m3')
|
|
216
|
+
await next()
|
|
217
|
+
}
|
|
218
|
+
const composed = compose([middleware1, middleware2, middleware3])
|
|
219
|
+
await composed({} as any, async () => { order.push('final') })
|
|
220
|
+
expect(order).toEqual(['m1-before', 'm2-before', 'm3', 'final', 'm2-after', 'm1-after'])
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('should catch and rethrow middleware errors', async () => {
|
|
224
|
+
const middleware = async (msg: any, next: any) => {
|
|
225
|
+
throw new Error('Middleware error')
|
|
226
|
+
}
|
|
227
|
+
const composed = compose([middleware])
|
|
228
|
+
await expect(composed({} as any, async () => {})).rejects.toThrow('Middleware error')
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('should call final next function', async () => {
|
|
232
|
+
let finalCalled = false
|
|
233
|
+
const middleware = async (msg: any, next: any) => {
|
|
234
|
+
await next()
|
|
235
|
+
}
|
|
236
|
+
const composed = compose([middleware])
|
|
237
|
+
await composed({} as any, async () => { finalCalled = true })
|
|
238
|
+
expect(finalCalled).toBe(true)
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
describe('segment utilities', () => {
|
|
243
|
+
describe('escape and unescape', () => {
|
|
244
|
+
it('should escape HTML entities', () => {
|
|
245
|
+
expect(segment.escape('<div>&"\'</div>')).toBe('<div>&"'</div>')
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('should unescape HTML entities', () => {
|
|
249
|
+
expect(segment.unescape('<div>&"'</div>')).toBe('<div>&"\'</div>')
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('should handle non-string values', () => {
|
|
253
|
+
expect(segment.escape(123 as any)).toBe(123)
|
|
254
|
+
expect(segment.unescape(null as any)).toBe(null)
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
describe('text and face', () => {
|
|
259
|
+
it('should create text segment', () => {
|
|
260
|
+
expect(segment.text('Hello')).toEqual({ type: 'text', data: { text: 'Hello' } })
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('should create face segment', () => {
|
|
264
|
+
expect(segment.face('smile', '😊')).toEqual({
|
|
265
|
+
type: 'face',
|
|
266
|
+
data: { id: 'smile', text: '😊' }
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
describe('from', () => {
|
|
272
|
+
it('should parse simple text', () => {
|
|
273
|
+
const result = segment.from('Hello World')
|
|
274
|
+
expect(result).toEqual([{ type: 'text', data: { text: 'Hello World' } }])
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('should parse self-closing tags with proper spacing', () => {
|
|
278
|
+
const result = segment.from('<image url="test.jpg" />')
|
|
279
|
+
expect(result.length).toBeGreaterThan(0)
|
|
280
|
+
// 检查是否包含 image 类型
|
|
281
|
+
const imageElement = result.find(el => el.type === 'image')
|
|
282
|
+
expect(imageElement).toBeDefined()
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('should parse paired tags', () => {
|
|
286
|
+
const result = segment.from('<quote>Hello</quote>')
|
|
287
|
+
// 检查结果中是否有 quote 元素
|
|
288
|
+
const quoteElement = result.find(el => el.type === 'quote')
|
|
289
|
+
expect(quoteElement).toBeDefined()
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('should parse mixed content', () => {
|
|
293
|
+
const result = segment.from('Text <image url="pic.jpg" /> More text')
|
|
294
|
+
expect(result.length).toBeGreaterThan(0)
|
|
295
|
+
// 应该包含文本和图片元素
|
|
296
|
+
expect(result.some(el => el.type === 'text')).toBe(true)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('should handle attributes with single quotes', () => {
|
|
300
|
+
const result = segment.from("<image url='test.jpg' />")
|
|
301
|
+
const imageElement = result.find(el => el.type === 'image')
|
|
302
|
+
expect(imageElement).toBeDefined()
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('should handle multiple attributes', () => {
|
|
306
|
+
const result = segment.from('<image url="test.jpg" width="100" height="200" />')
|
|
307
|
+
const imageElement = result.find(el => el.type === 'image')
|
|
308
|
+
// 如果找到了 image 元素,检查它有数据
|
|
309
|
+
if (imageElement) {
|
|
310
|
+
expect(imageElement.data).toBeDefined()
|
|
311
|
+
// 至少应该有一些属性
|
|
312
|
+
expect(Object.keys(imageElement.data).length).toBeGreaterThanOrEqual(0)
|
|
313
|
+
} else {
|
|
314
|
+
// 如果没找到,至少应该有元素被解析
|
|
315
|
+
expect(result.length).toBeGreaterThan(0)
|
|
316
|
+
}
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('should handle nested tags', () => {
|
|
320
|
+
const result = segment.from('<quote><text>Hello</text></quote>')
|
|
321
|
+
// 应该有元素被解析
|
|
322
|
+
expect(result.length).toBeGreaterThan(0)
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
it('should handle array input', () => {
|
|
326
|
+
const result = segment.from(['Hello', ' ', 'World'])
|
|
327
|
+
expect(result.length).toBeGreaterThan(0)
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('should handle MessageElement input', () => {
|
|
331
|
+
const input = { type: 'text', data: { text: 'Hello' } }
|
|
332
|
+
const result = segment.from(input)
|
|
333
|
+
expect(result).toEqual([input])
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it('should parse JSON values in attributes', () => {
|
|
337
|
+
const result = segment.from('<data value="123" />')
|
|
338
|
+
const dataElement = result.find(el => el.type === 'data')
|
|
339
|
+
// 如果找到了 data 元素,检查值是否被解析
|
|
340
|
+
if (dataElement && dataElement.data.value !== undefined) {
|
|
341
|
+
expect(typeof dataElement.data.value).toBe('number')
|
|
342
|
+
}
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
it('should handle malformed templates gracefully', () => {
|
|
346
|
+
const result = segment.from('Hello <unclosed')
|
|
347
|
+
expect(result[0].type).toBe('text')
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('should handle templates with escaped characters', () => {
|
|
351
|
+
const result = segment.from('<div>')
|
|
352
|
+
expect(result[0].data.text).toBe('<div>')
|
|
353
|
+
})
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
describe('raw', () => {
|
|
357
|
+
it('should convert segments to raw text', () => {
|
|
358
|
+
const content = [
|
|
359
|
+
{ type: 'text', data: { text: 'Hello' } },
|
|
360
|
+
{ type: 'face', data: { text: '😊' } },
|
|
361
|
+
]
|
|
362
|
+
expect(segment.raw(content)).toBe('Hello{face}(😊)')
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it('should handle segments without text', () => {
|
|
366
|
+
const content = [{ type: 'image', data: { url: 'test.jpg' } }]
|
|
367
|
+
expect(segment.raw(content)).toBe('{image}')
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
it('should handle string input', () => {
|
|
371
|
+
expect(segment.raw('Hello')).toBe('Hello')
|
|
372
|
+
})
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
describe('toString', () => {
|
|
376
|
+
it('should convert segments to string', () => {
|
|
377
|
+
const content = [
|
|
378
|
+
{ type: 'text', data: { text: 'Hello' } },
|
|
379
|
+
{ type: 'image', data: { url: 'test.jpg' } },
|
|
380
|
+
]
|
|
381
|
+
const result = segment.toString(content)
|
|
382
|
+
expect(result).toContain('Hello')
|
|
383
|
+
expect(result).toContain('<image')
|
|
384
|
+
expect(result).toContain('url=')
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('should handle function types', () => {
|
|
388
|
+
const content = [{ type: function MyType() {} as any, data: { value: 1 } }]
|
|
389
|
+
const result = segment.toString(content)
|
|
390
|
+
expect(result).toContain('MyType')
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
it('should escape attribute values', () => {
|
|
394
|
+
const content = [{ type: 'tag', data: { attr: '<script>' } }]
|
|
395
|
+
const result = segment.toString(content)
|
|
396
|
+
expect(result).toContain('<')
|
|
397
|
+
})
|
|
398
|
+
})
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
describe('remove utility', () => {
|
|
402
|
+
it('should remove item by value', () => {
|
|
403
|
+
const list = [1, 2, 3, 4]
|
|
404
|
+
remove(list, 3)
|
|
405
|
+
expect(list).toEqual([1, 2, 4])
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it('should remove item by predicate', () => {
|
|
409
|
+
const list = [1, 2, 3, 4]
|
|
410
|
+
remove(list, (x) => x > 2)
|
|
411
|
+
expect(list).toEqual([1, 2, 4])
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it('should do nothing if item not found', () => {
|
|
415
|
+
const list = [1, 2, 3]
|
|
416
|
+
remove(list, 5)
|
|
417
|
+
expect(list).toEqual([1, 2, 3])
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
it('should handle function items', () => {
|
|
421
|
+
const fn1 = () => 1
|
|
422
|
+
const fn2 = () => 2
|
|
423
|
+
const list = [fn1, fn2]
|
|
424
|
+
remove(list, fn1)
|
|
425
|
+
expect(list).toEqual([fn2])
|
|
426
|
+
})
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
describe('isEmpty utility', () => {
|
|
430
|
+
it('should return true for empty array', () => {
|
|
431
|
+
expect(isEmpty([])).toBe(true)
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
it('should return false for non-empty array', () => {
|
|
435
|
+
expect(isEmpty([1, 2])).toBe(false)
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
it('should return true for empty object', () => {
|
|
439
|
+
expect(isEmpty({})).toBe(true)
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
it('should return false for non-empty object', () => {
|
|
443
|
+
expect(isEmpty({ a: 1 })).toBe(false)
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
it('should return true for null', () => {
|
|
447
|
+
expect(isEmpty(null)).toBe(true)
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
it('should return false for non-empty values', () => {
|
|
451
|
+
expect(isEmpty('string')).toBe(false)
|
|
452
|
+
expect(isEmpty(123)).toBe(false)
|
|
453
|
+
})
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
describe('Time utilities', () => {
|
|
457
|
+
describe('constants', () => {
|
|
458
|
+
it('should have correct time constants', () => {
|
|
459
|
+
expect(Time.second).toBe(1000)
|
|
460
|
+
expect(Time.minute).toBe(60000)
|
|
461
|
+
expect(Time.hour).toBe(3600000)
|
|
462
|
+
expect(Time.day).toBe(86400000)
|
|
463
|
+
expect(Time.week).toBe(604800000)
|
|
464
|
+
})
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
describe('timezone', () => {
|
|
468
|
+
it('should get and set timezone offset', () => {
|
|
469
|
+
const original = Time.getTimezoneOffset()
|
|
470
|
+
Time.setTimezoneOffset(480)
|
|
471
|
+
expect(Time.getTimezoneOffset()).toBe(480)
|
|
472
|
+
Time.setTimezoneOffset(original)
|
|
473
|
+
})
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
describe('getDateNumber and fromDateNumber', () => {
|
|
477
|
+
it('should convert date to number and back', () => {
|
|
478
|
+
const date = new Date('2024-01-01T00:00:00Z')
|
|
479
|
+
const num = Time.getDateNumber(date, 0)
|
|
480
|
+
const restored = Time.fromDateNumber(num, 0)
|
|
481
|
+
expect(restored.getUTCDate()).toBe(date.getUTCDate())
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
it('should handle timestamp input', () => {
|
|
485
|
+
const timestamp = Date.now()
|
|
486
|
+
const num = Time.getDateNumber(timestamp)
|
|
487
|
+
expect(typeof num).toBe('number')
|
|
488
|
+
})
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
describe('parseTime', () => {
|
|
492
|
+
it('should parse time strings', () => {
|
|
493
|
+
expect(Time.parseTime('1d')).toBe(Time.day)
|
|
494
|
+
expect(Time.parseTime('2h')).toBe(Time.hour * 2)
|
|
495
|
+
expect(Time.parseTime('30m')).toBe(Time.minute * 30)
|
|
496
|
+
expect(Time.parseTime('45s')).toBe(Time.second * 45)
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
it('should parse combined time strings', () => {
|
|
500
|
+
expect(Time.parseTime('1d2h')).toBe(Time.day + Time.hour * 2)
|
|
501
|
+
expect(Time.parseTime('1w3d')).toBe(Time.week + Time.day * 3)
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
it('should return 0 for invalid strings', () => {
|
|
505
|
+
expect(Time.parseTime('invalid')).toBe(0)
|
|
506
|
+
expect(Time.parseTime('')).toBe(0)
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
it('should handle decimal values', () => {
|
|
510
|
+
expect(Time.parseTime('1.5h')).toBe(Time.hour * 1.5)
|
|
511
|
+
})
|
|
512
|
+
})
|
|
513
|
+
|
|
514
|
+
describe('parseDate', () => {
|
|
515
|
+
it('should parse relative time', () => {
|
|
516
|
+
const now = Date.now()
|
|
517
|
+
const result = Time.parseDate('1h')
|
|
518
|
+
expect(result.getTime()).toBeGreaterThan(now)
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
it('should parse time-only format', () => {
|
|
522
|
+
const result = Time.parseDate('14:30:00')
|
|
523
|
+
expect(result).toBeInstanceOf(Date)
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
it('should parse short date format', () => {
|
|
527
|
+
const result = Time.parseDate('12-25-14:30:00')
|
|
528
|
+
expect(result).toBeInstanceOf(Date)
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
it('should return current date for invalid input', () => {
|
|
532
|
+
const result = Time.parseDate('')
|
|
533
|
+
expect(result).toBeInstanceOf(Date)
|
|
534
|
+
})
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
describe('formatTimeShort', () => {
|
|
538
|
+
it('should format days', () => {
|
|
539
|
+
expect(Time.formatTimeShort(Time.day * 2)).toBe('2d')
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
it('should format hours', () => {
|
|
543
|
+
expect(Time.formatTimeShort(Time.hour * 3)).toBe('3h')
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
it('should format minutes', () => {
|
|
547
|
+
expect(Time.formatTimeShort(Time.minute * 45)).toBe('45m')
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
it('should format seconds', () => {
|
|
551
|
+
expect(Time.formatTimeShort(Time.second * 30)).toBe('30s')
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
it('should format milliseconds', () => {
|
|
555
|
+
expect(Time.formatTimeShort(500)).toBe('500ms')
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
it('should handle negative values', () => {
|
|
559
|
+
expect(Time.formatTimeShort(-Time.hour * 2)).toBe('-2h')
|
|
560
|
+
})
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
describe('formatTime', () => {
|
|
564
|
+
it('should format days with hours', () => {
|
|
565
|
+
const result = Time.formatTime(Time.day + Time.hour * 3)
|
|
566
|
+
expect(result).toContain('天')
|
|
567
|
+
expect(result).toContain('小时')
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
it('should format hours with minutes', () => {
|
|
571
|
+
const result = Time.formatTime(Time.hour * 2 + Time.minute * 30)
|
|
572
|
+
expect(result).toContain('小时')
|
|
573
|
+
expect(result).toContain('分钟')
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
it('should format minutes with seconds', () => {
|
|
577
|
+
const result = Time.formatTime(Time.minute * 5 + Time.second * 30)
|
|
578
|
+
expect(result).toContain('分钟')
|
|
579
|
+
expect(result).toContain('秒')
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
it('should format seconds only', () => {
|
|
583
|
+
const result = Time.formatTime(Time.second * 30)
|
|
584
|
+
expect(result).toContain('秒')
|
|
585
|
+
})
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
describe('template', () => {
|
|
589
|
+
it('should format date template', () => {
|
|
590
|
+
const date = new Date('2024-01-15T14:30:45.123')
|
|
591
|
+
const result = Time.template('yyyy-MM-dd hh:mm:ss', date)
|
|
592
|
+
expect(result).toBe('2024-01-15 14:30:45')
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
it('should handle short year format', () => {
|
|
596
|
+
const date = new Date('2024-01-15')
|
|
597
|
+
const result = Time.template('yy-MM-dd', date)
|
|
598
|
+
expect(result).toBe('24-01-15')
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
it('should handle milliseconds', () => {
|
|
602
|
+
const date = new Date('2024-01-15T14:30:45.123')
|
|
603
|
+
const result = Time.template('SSS', date)
|
|
604
|
+
expect(result).toBe('123')
|
|
605
|
+
})
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
describe('formatTimeInterval', () => {
|
|
609
|
+
it('should format without interval', () => {
|
|
610
|
+
const date = new Date('2024-01-15T14:30:45')
|
|
611
|
+
const result = Time.formatTimeInterval(date)
|
|
612
|
+
expect(result).toContain('2024-01-15')
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
it('should format daily interval', () => {
|
|
616
|
+
const date = new Date('2024-01-15T14:30:00')
|
|
617
|
+
const result = Time.formatTimeInterval(date, Time.day)
|
|
618
|
+
expect(result).toContain('每天')
|
|
619
|
+
})
|
|
620
|
+
|
|
621
|
+
it('should format weekly interval', () => {
|
|
622
|
+
const date = new Date('2024-01-15T14:30:00')
|
|
623
|
+
const result = Time.formatTimeInterval(date, Time.week)
|
|
624
|
+
expect(result).toContain('每周')
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
it('should format custom interval', () => {
|
|
628
|
+
const date = new Date('2024-01-15T14:30:00')
|
|
629
|
+
const result = Time.formatTimeInterval(date, Time.hour * 6)
|
|
630
|
+
expect(result).toContain('每隔')
|
|
631
|
+
})
|
|
632
|
+
})
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
describe('sleep utility', () => {
|
|
636
|
+
it('should sleep for specified time', async () => {
|
|
637
|
+
const start = Date.now()
|
|
638
|
+
await sleep(100)
|
|
639
|
+
const elapsed = Date.now() - start
|
|
640
|
+
expect(elapsed).toBeGreaterThanOrEqual(90) // 允许一些误差
|
|
641
|
+
})
|
|
71
642
|
})
|