@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/src/plugin.ts CHANGED
@@ -15,7 +15,7 @@ import { fileURLToPath } from "url";
15
15
  import logger, { Logger } from "@zhin.js/logger";
16
16
  import { compose, remove, resolveEntry } from "./utils.js";
17
17
  import { MessageMiddleware, RegisteredAdapter, MaybePromise, ArrayItem, ConfigService, PermissionService, SendOptions } from "./types.js";
18
- import { Adapter } from "./adapter.js";
18
+ import { Adapter, Adapters } from "./adapter.js";
19
19
  import { createHash } from "crypto";
20
20
  const contextsKey = Symbol("contexts");
21
21
  const loadedModules = new Map<string, Plugin>(); // 记录已加载的模块
@@ -685,7 +685,7 @@ export namespace Plugin {
685
685
  * 服务类型扩展点
686
686
  * 各个 Context 通过 declare module 扩展此接口
687
687
  */
688
- export interface Contexts extends RegisteredAdapters {
688
+ export interface Contexts extends Adapters {
689
689
  config: ConfigService;
690
690
  permission: PermissionService;
691
691
  }
package/src/types.ts CHANGED
@@ -4,6 +4,7 @@ import {Adapter} from "./adapter.js";
4
4
  import {Bot} from "./bot.js";
5
5
  import { SystemLog } from "./models/system-log.js";
6
6
  import { User } from "./models/user.js";
7
+ import { Adapters } from "./adapter.js";
7
8
  import { Databases,Registry } from "@zhin.js/database";
8
9
  import { MessageComponent } from "./message.js";
9
10
  import { ProcessAdapter } from "./built/adapter-process.js";
@@ -30,7 +31,7 @@ export type ObjectItem<T extends object>=T[keyof T]
30
31
  /**
31
32
  * 已注册适配器名类型
32
33
  */
33
- export type RegisteredAdapter=Extract<keyof RegisteredAdapters, string>
34
+ export type RegisteredAdapter=Extract<keyof Adapters, string>
34
35
  /**
35
36
  * 指定适配器的消息类型
36
37
  */
package/src/utils.ts CHANGED
@@ -187,11 +187,25 @@ export namespace segment {
187
187
  if (!Array.isArray(content)) content = [content];
188
188
  const toString = (template: string | MessageElement) => {
189
189
  if (typeof template !== "string") return [template];
190
+
191
+ // 安全检查:限制输入长度,防止 ReDoS 攻击
192
+ const MAX_TEMPLATE_LENGTH = 100000; // 100KB
193
+ if (template.length > MAX_TEMPLATE_LENGTH) {
194
+ throw new Error(`Template too large: ${template.length} > ${MAX_TEMPLATE_LENGTH}`);
195
+ }
196
+
190
197
  template = unescape(template);
191
198
  const result: MessageElement[] = [];
192
- const closingReg = /<(\S+)(\s[^>]+)?\/>/;
193
- const twinningReg = /<(\S+)(\s[^>]+)?>([\s\S]*?)<\/\1>/;
194
- while (template.length) {
199
+ // 修复 ReDoS 漏洞:使用更安全的正则表达式
200
+ // 原: /<(\S+)(\s[^>]+)?\/>/ 可能导致回溯
201
+ const closingReg = /<(\w+)(?:\s+[^>]*?)?\/>/;
202
+ // 原: /<(\S+)(\s[^>]+)?>([\s\S]*?)<\/\1>/ 可能导致回溯
203
+ const twinningReg = /<(\w+)(?:\s+[^>]*?)?>([^]*?)<\/\1>/;
204
+
205
+ let iterations = 0;
206
+ const MAX_ITERATIONS = 1000; // 防止无限循环
207
+
208
+ while (template.length && iterations++ < MAX_ITERATIONS) {
195
209
  const [_, type, attrStr = "", child = ""] =
196
210
  template.match(twinningReg) || template.match(closingReg) || [];
197
211
  if (!type) break;
@@ -209,8 +223,10 @@ export namespace segment {
209
223
  },
210
224
  });
211
225
  template = template.slice(index + matched.length);
226
+ // 修复 ReDoS 漏洞:使用更简单的正则表达式
227
+ // 原: /\s([^=]+)(?=(?=="([^"]+)")|(?=='([^']+)'))/g 嵌套前瞻断言
212
228
  const attrArr = [
213
- ...attrStr.matchAll(/\s([^=]+)(?=(?=="([^"]+)")|(?=='([^']+)'))/g),
229
+ ...attrStr.matchAll(/\s+([^=\s]+)=(?:"([^"]*)"|'([^']*)')/g),
214
230
  ];
215
231
  const data = Object.fromEntries(
216
232
  attrArr.map(([source, key, v1, v2]) => {
@@ -1,7 +1,384 @@
1
- import { describe, it, expect } from "vitest"
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
2
+ import { Adapter } from '../src/adapter'
3
+ import { Bot } from '../src/bot'
4
+ import { Plugin } from '../src/plugin'
5
+ import { Message, MessageBase } from '../src/message'
6
+ import { EventEmitter } from 'events'
2
7
 
3
- describe("适配器测试", () => {
4
- it("应该通过基本测试", () => {
5
- expect(true).toBe(true)
8
+ // Mock Bot 实现用于测试
9
+ class MockBot implements Bot<any, any> {
10
+ $id: string
11
+ $config: any
12
+ $connected: boolean = false
13
+ adapter: Adapter
14
+
15
+ constructor(adapter: Adapter, config: any) {
16
+ this.adapter = adapter
17
+ this.$config = config
18
+ this.$id = config.id || 'mock-bot'
19
+ }
20
+
21
+ $formatMessage(event: any): Message<any> {
22
+ const base: MessageBase = {
23
+ $id: event.id || 'mock-id',
24
+ $adapter: 'test' as any,
25
+ $bot: this.$id,
26
+ $content: [],
27
+ $sender: { id: 'mock-sender', name: 'Mock Sender' },
28
+ $channel: { id: 'mock-channel', type: 'private' },
29
+ $timestamp: Date.now(),
30
+ $raw: event.raw || event,
31
+ $reply: async (content: any) => 'mock-reply-id',
32
+ $recall: async () => {}
33
+ }
34
+ return Message.from(event, base)
35
+ }
36
+
37
+ async $connect(): Promise<void> {
38
+ this.$connected = true
39
+ }
40
+
41
+ async $disconnect(): Promise<void> {
42
+ this.$connected = false
43
+ }
44
+
45
+ async $sendMessage(options: any): Promise<string> {
46
+ return 'mock-message-id'
47
+ }
48
+
49
+ async $recallMessage(id: string): Promise<void> {
50
+ // Mock 撤回消息
51
+ }
52
+ }
53
+
54
+ // Mock Adapter 类用于测试
55
+ class MockAdapter extends Adapter<MockBot> {
56
+ createBot(config: any): MockBot {
57
+ return new MockBot(this, config)
58
+ }
59
+ }
60
+
61
+ describe('Adapter Core Functionality', () => {
62
+ let plugin: Plugin
63
+ let adapter: MockAdapter
64
+
65
+ beforeEach(() => {
66
+ plugin = new Plugin('/test/plugin.ts')
67
+ adapter = new MockAdapter(plugin, 'test', [])
68
+ })
69
+
70
+ describe('Adapter Constructor', () => {
71
+ it('should create adapter with plugin, name and config', () => {
72
+ const config = [{ id: 'bot1' }]
73
+ const adapter = new MockAdapter(plugin, 'test', config)
74
+
75
+ expect(adapter.plugin).toBe(plugin)
76
+ expect(adapter.name).toBe('test')
77
+ expect(adapter.config).toBe(config)
78
+ })
79
+
80
+ it('should initialize with empty bots map', () => {
81
+ expect(adapter.bots).toBeInstanceOf(Map)
82
+ expect(adapter.bots.size).toBe(0)
83
+ })
84
+
85
+ it('should inherit from EventEmitter', () => {
86
+ expect(adapter).toBeInstanceOf(EventEmitter)
87
+ })
88
+
89
+ it('should register message.receive listener', () => {
90
+ const listeners = adapter.listeners('message.receive')
91
+ expect(listeners.length).toBeGreaterThan(0)
92
+ })
93
+
94
+ it('should register call.sendMessage listener', () => {
95
+ const listeners = adapter.listeners('call.sendMessage')
96
+ expect(listeners.length).toBeGreaterThan(0)
97
+ })
98
+
99
+ it('should register call.recallMessage listener', () => {
100
+ const listeners = adapter.listeners('call.recallMessage')
101
+ expect(listeners.length).toBeGreaterThan(0)
102
+ })
103
+ })
104
+
105
+ describe('Adapter Logger', () => {
106
+ it('should get logger from plugin', () => {
107
+ expect(adapter.logger).toBeDefined()
108
+ expect(adapter.logger).toBe(plugin.logger)
109
+ })
110
+
111
+ it('should throw error if plugin is not set', () => {
112
+ const adapter = new MockAdapter(plugin, 'test', [])
113
+ adapter.plugin = null as any
114
+
115
+ expect(() => adapter.logger).toThrow('Adapter is not associated with any plugin')
116
+ })
117
+ })
118
+
119
+ describe('Adapter Binding', () => {
120
+ it('should bind to a plugin', () => {
121
+ const newPlugin = new Plugin('/test/new-plugin.ts')
122
+ adapter.binding(newPlugin)
123
+
124
+ expect(adapter.plugin).toBe(newPlugin)
125
+ })
126
+ })
127
+
128
+ describe('Adapter Start', () => {
129
+ it('should start without config', async () => {
130
+ await adapter.start()
131
+ expect(plugin.root.adapters).toContain('test')
132
+ })
133
+
134
+ it('should create and connect bots from config', async () => {
135
+ const config = [
136
+ { id: 'bot1' },
137
+ { id: 'bot2' }
138
+ ]
139
+ const adapter = new MockAdapter(plugin, 'test', config)
140
+
141
+ await adapter.start()
142
+
143
+ expect(adapter.bots.size).toBe(2)
144
+ expect(adapter.bots.has('bot1')).toBe(true)
145
+ expect(adapter.bots.has('bot2')).toBe(true)
146
+ })
147
+
148
+ it('should add adapter name to plugin adapters', async () => {
149
+ await adapter.start()
150
+ expect(plugin.root.adapters).toContain('test')
151
+ })
152
+
153
+ it('should handle empty config array', async () => {
154
+ const adapter = new MockAdapter(plugin, 'test', [])
155
+ await adapter.start()
156
+ expect(adapter.bots.size).toBe(0)
157
+ })
158
+ })
159
+
160
+ describe('Adapter Stop', () => {
161
+ it('should disconnect all bots', async () => {
162
+ const config = [{ id: 'bot1' }, { id: 'bot2' }]
163
+ const adapter = new MockAdapter(plugin, 'test', config)
164
+
165
+ await adapter.start()
166
+ expect(adapter.bots.size).toBe(2)
167
+
168
+ await adapter.stop()
169
+ expect(adapter.bots.size).toBe(0)
170
+ })
171
+
172
+ it('should remove adapter from plugin adapters', async () => {
173
+ await adapter.start()
174
+ expect(plugin.root.adapters).toContain('test')
175
+
176
+ await adapter.stop()
177
+ expect(plugin.root.adapters).not.toContain('test')
178
+ })
179
+
180
+ it('should remove all event listeners', async () => {
181
+ await adapter.start()
182
+ const beforeCount = adapter.listenerCount('message.receive')
183
+
184
+ await adapter.stop()
185
+ const afterCount = adapter.listenerCount('message.receive')
186
+
187
+ expect(afterCount).toBe(0)
188
+ expect(beforeCount).toBeGreaterThan(0)
189
+ })
190
+
191
+ it('should handle bot disconnect errors gracefully', async () => {
192
+ const config = [{ id: 'bot1' }, { id: 'bot2' }]
193
+ const adapter = new MockAdapter(plugin, 'test', config)
194
+
195
+ await adapter.start()
196
+
197
+ // Mock first bot disconnect to throw error
198
+ const bot1 = adapter.bots.get('bot1')!
199
+ bot1.$disconnect = vi.fn().mockRejectedValue(new Error('Disconnect failed'))
200
+
201
+ // Mock logger to spy on error logging
202
+ const loggerSpy = vi.spyOn(adapter.logger, 'error')
203
+
204
+ // The adapter should continue cleanup despite errors
205
+ // Note: Current implementation throws, but this test documents the desired behavior
206
+ // where adapter.stop() should handle errors gracefully and continue cleanup
207
+ await expect(adapter.stop()).rejects.toThrow('Disconnect failed')
208
+
209
+ // Even though it throws, we document that graceful handling would be:
210
+ // - Log the error
211
+ // - Continue disconnecting other bots
212
+ // - Complete cleanup (clear bots, remove from adapters list, remove listeners)
213
+ })
214
+ })
215
+
216
+ describe('Adapter Events', () => {
217
+ describe('call.recallMessage', () => {
218
+ it('should recall message from bot', async () => {
219
+ const config = [{ id: 'bot1' }]
220
+ const adapter = new MockAdapter(plugin, 'test', config)
221
+ await adapter.start()
222
+
223
+ const bot = adapter.bots.get('bot1')!
224
+ const recallSpy = vi.spyOn(bot, '$recallMessage')
225
+
226
+ await adapter.emit('call.recallMessage', 'bot1', 'message-id')
227
+
228
+ expect(recallSpy).toHaveBeenCalledWith('message-id')
229
+ })
230
+
231
+ it('should require valid bot id', () => {
232
+ // 验证 adapter 不包含不存在的 bot
233
+ expect(adapter.bots.has('non-existent-bot')).toBe(false)
234
+ })
235
+ })
236
+
237
+ describe('call.sendMessage', () => {
238
+ it('should send message through bot', async () => {
239
+ const config = [{ id: 'bot1' }]
240
+ const adapter = new MockAdapter(plugin, 'test', config)
241
+ await adapter.start()
242
+
243
+ const bot = adapter.bots.get('bot1')!
244
+ const sendSpy = vi.spyOn(bot, '$sendMessage')
245
+
246
+ const options = {
247
+ context: 'test',
248
+ bot: 'bot1',
249
+ content: 'Hello',
250
+ id: 'channel-id',
251
+ type: 'text' as const
252
+ }
253
+
254
+ await adapter.emit('call.sendMessage', 'bot1', options)
255
+
256
+ expect(sendSpy).toHaveBeenCalledWith(options)
257
+ })
258
+
259
+ it('should validate bot existence before sending', () => {
260
+ // 验证发送消息前应该检查 bot 是否存在
261
+ expect(adapter.bots.has('non-existent-bot')).toBe(false)
262
+
263
+ // 在实际使用中,应该先检查 bot 是否存在
264
+ const botExists = adapter.bots.has('bot1')
265
+ expect(botExists).toBe(false) // 因为还没有 start
266
+ })
267
+
268
+ it('should call before.sendMessage handlers', async () => {
269
+ const config = [{ id: 'bot1' }]
270
+ const adapter = new MockAdapter(plugin, 'test', config)
271
+ await adapter.start()
272
+
273
+ let handlerCalled = false
274
+ plugin.root.on('before.sendMessage', (options) => {
275
+ handlerCalled = true
276
+ return options
277
+ })
278
+
279
+ const options = {
280
+ context: 'test',
281
+ bot: 'bot1',
282
+ content: 'Hello',
283
+ id: 'channel-id',
284
+ type: 'text' as const
285
+ }
286
+
287
+ await adapter.emit('call.sendMessage', 'bot1', options)
288
+ expect(handlerCalled).toBe(true)
289
+ })
290
+ })
291
+
292
+ describe('message.receive', () => {
293
+ it('should process received message through middleware', async () => {
294
+ const config = [{ id: 'bot1' }]
295
+ const adapter = new MockAdapter(plugin, 'test', config)
296
+ await adapter.start()
297
+
298
+ let middlewareCalled = false
299
+ plugin.addMiddleware(async (message, next) => {
300
+ middlewareCalled = true
301
+ await next()
302
+ })
303
+
304
+ const message = {
305
+ $bot: 'bot1',
306
+ $adapter: 'test',
307
+ $channel: { id: 'channel-id', type: 'text' },
308
+ $content: 'Hello'
309
+ } as any
310
+
311
+ adapter.emit('message.receive', message)
312
+
313
+ // 等待异步处理
314
+ await new Promise(resolve => setTimeout(resolve, 10))
315
+ expect(middlewareCalled).toBe(true)
316
+ })
317
+ })
318
+ })
319
+
320
+ describe('Adapter createBot', () => {
321
+ it('should be abstract method', () => {
322
+ expect(typeof adapter.createBot).toBe('function')
323
+ })
324
+
325
+ it('should create bot with config', () => {
326
+ const config = { id: 'test-bot' }
327
+ const bot = adapter.createBot(config)
328
+
329
+ expect(bot).toBeInstanceOf(MockBot)
330
+ expect(bot.$id).toBe('test-bot')
331
+ })
332
+ })
333
+
334
+ describe('Adapter Bots Management', () => {
335
+ it('should manage multiple bots', async () => {
336
+ const config = [
337
+ { id: 'bot1' },
338
+ { id: 'bot2' },
339
+ { id: 'bot3' }
340
+ ]
341
+ const adapter = new MockAdapter(plugin, 'test', config)
342
+
343
+ await adapter.start()
344
+
345
+ expect(adapter.bots.size).toBe(3)
346
+ expect(Array.from(adapter.bots.keys())).toEqual(['bot1', 'bot2', 'bot3'])
347
+ })
348
+
349
+ it('should access bot by id', async () => {
350
+ const config = [{ id: 'bot1' }]
351
+ const adapter = new MockAdapter(plugin, 'test', config)
352
+
353
+ await adapter.start()
354
+
355
+ const bot = adapter.bots.get('bot1')
356
+ expect(bot).toBeDefined()
357
+ expect(bot!.$id).toBe('bot1')
358
+ })
359
+ })
360
+ })
361
+
362
+ describe('Adapter Registry', () => {
363
+ it('should have a Registry Map', () => {
364
+ expect(Adapter.Registry).toBeInstanceOf(Map)
365
+ })
366
+
367
+ it('should register adapter factory', () => {
368
+ const factory = MockAdapter as any
369
+ Adapter.register('mock', factory)
370
+
371
+ expect(Adapter.Registry.has('mock')).toBe(true)
372
+ expect(Adapter.Registry.get('mock')).toBe(factory)
373
+ })
374
+
375
+ it('should allow multiple adapter registrations', () => {
376
+ const factory1 = MockAdapter as any
377
+ const factory2 = MockAdapter as any
378
+
379
+ Adapter.register('adapter1', factory1)
380
+ Adapter.register('adapter2', factory2)
381
+
382
+ expect(Adapter.Registry.size).toBeGreaterThanOrEqual(2)
6
383
  })
7
384
  })
@@ -1,7 +1,150 @@
1
- import { describe, it, expect } from "vitest"
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
+ import { ConfigLoader } from '../src/built/config'
3
+ import fs from 'fs'
4
+ import path from 'path'
2
5
 
3
- describe("配置系统测试", () => {
4
- it("应该通过基本测试", () => {
5
- expect(true).toBe(true)
6
+ describe('ConfigLoader', () => {
7
+ const testConfigPath = path.join(process.cwd(), 'test-config.json')
8
+
9
+ afterEach(() => {
10
+ // 清理测试文件
11
+ if (fs.existsSync(testConfigPath)) {
12
+ fs.unlinkSync(testConfigPath)
13
+ }
14
+ })
15
+
16
+ describe('Proxy behavior', () => {
17
+ it('should handle array methods correctly', () => {
18
+ const config = {
19
+ items: ['item1', 'item2', 'item3'],
20
+ bots: [
21
+ { context: 'sandbox', name: 'bot1' },
22
+ { context: 'sandbox', name: 'bot2' }
23
+ ]
24
+ }
25
+
26
+ const loader = new ConfigLoader(testConfigPath, config)
27
+ const proxiedData = loader.data
28
+
29
+ // 测试数组的 map 方法
30
+ expect(() => {
31
+ const mapped = proxiedData.items.map((item: string) => item.toUpperCase())
32
+ expect(mapped).toEqual(['ITEM1', 'ITEM2', 'ITEM3'])
33
+ }).not.toThrow()
34
+
35
+ // 测试数组的 filter 方法
36
+ expect(() => {
37
+ const filtered = proxiedData.bots.filter((bot: any) => bot.name === 'bot1')
38
+ expect(filtered).toHaveLength(1)
39
+ }).not.toThrow()
40
+ })
41
+
42
+ it('should handle nested objects', () => {
43
+ const config = {
44
+ database: {
45
+ dialect: 'sqlite',
46
+ filename: './data/bot.db'
47
+ },
48
+ http: {
49
+ port: 8086,
50
+ username: '${username}',
51
+ password: '${password}'
52
+ }
53
+ }
54
+
55
+ const loader = new ConfigLoader(testConfigPath, config)
56
+ const proxiedData = loader.data
57
+
58
+ expect(proxiedData.database.dialect).toBe('sqlite')
59
+ expect(proxiedData.http.port).toBe(8086)
60
+ })
61
+
62
+ it('should resolve environment variables', () => {
63
+ process.env.TEST_VAR = 'test_value'
64
+
65
+ const config = {
66
+ testValue: '${TEST_VAR}'
67
+ }
68
+
69
+ const loader = new ConfigLoader(testConfigPath, config)
70
+ const proxiedData = loader.data
71
+
72
+ expect(proxiedData.testValue).toBe('test_value')
73
+
74
+ delete process.env.TEST_VAR
75
+ })
76
+
77
+ it('should handle escaped environment variables', () => {
78
+ const config = {
79
+ escapedValue: '\\${NOT_A_VAR}'
80
+ }
81
+
82
+ const loader = new ConfigLoader(testConfigPath, config)
83
+ const proxiedData = loader.data
84
+
85
+ expect(proxiedData.escapedValue).toBe('${NOT_A_VAR}')
86
+ })
87
+
88
+ it('should handle default values for missing env vars', () => {
89
+ const config = {
90
+ valueWithDefault: '${MISSING_VAR:default_value}'
91
+ }
92
+
93
+ const loader = new ConfigLoader(testConfigPath, config)
94
+ const proxiedData = loader.data
95
+
96
+ expect(proxiedData.valueWithDefault).toBe('default_value')
97
+ })
98
+
99
+ it('should not proxy function properties', () => {
100
+ const config = {
101
+ items: [1, 2, 3]
102
+ }
103
+
104
+ const loader = new ConfigLoader(testConfigPath, config)
105
+ const proxiedData = loader.data
106
+
107
+ // 确保数组方法可以正常调用
108
+ expect(typeof proxiedData.items.map).toBe('function')
109
+ expect(typeof proxiedData.items.filter).toBe('function')
110
+ expect(typeof proxiedData.items.reduce).toBe('function')
111
+ })
112
+
113
+ it('should handle Set operations from array', () => {
114
+ const config = {
115
+ bots: [
116
+ { context: 'sandbox', name: 'bot1' },
117
+ { context: 'process', name: 'bot2' },
118
+ { context: 'sandbox', name: 'bot3' }
119
+ ]
120
+ }
121
+
122
+ const loader = new ConfigLoader(testConfigPath, config)
123
+ const proxiedData = loader.data
124
+
125
+ // 模拟 setup.ts 中的操作
126
+ expect(() => {
127
+ const contexts = new Set(proxiedData.bots.map((bot: any) => bot.context))
128
+ expect(contexts.size).toBe(2)
129
+ expect(contexts.has('sandbox')).toBe(true)
130
+ expect(contexts.has('process')).toBe(true)
131
+ }).not.toThrow()
132
+ })
133
+ })
134
+
135
+ describe('Raw data access', () => {
136
+ it('should provide raw data without proxy', () => {
137
+ const config = {
138
+ value: '${TEST_VAR}'
139
+ }
140
+
141
+ const loader = new ConfigLoader(testConfigPath, config)
142
+
143
+ // raw 应该返回原始值,不解析环境变量
144
+ expect(loader.raw.value).toBe('${TEST_VAR}')
145
+
146
+ // data 应该尝试解析环境变量
147
+ expect(loader.data.value).toBeTruthy()
148
+ })
6
149
  })
7
150
  })
@@ -274,4 +274,15 @@ describe('Cron定时任务系统测试', () => {
274
274
  expect(mockCallback).toHaveBeenCalledTimes(1)
275
275
  })
276
276
  })
277
+
278
+ describe('Cron错误处理', () => {
279
+ it('should throw error for invalid cron expression that cannot determine next run', () => {
280
+ // 创建一个永远不会执行的 cron 表达式
281
+ // 例如:2月30日(不存在的日期)
282
+ expect(() => {
283
+ cron = new Cron('0 0 30 2 *', mockCallback)
284
+ cron.start()
285
+ }).toThrow()
286
+ })
287
+ })
277
288
  })
@@ -0,0 +1,45 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import * as jsxRuntime from '../src/jsx-runtime'
3
+ import * as jsxDevRuntime from '../src/jsx-dev-runtime'
4
+
5
+ describe('JSX Runtime', () => {
6
+ it('should export jsx function', () => {
7
+ expect(typeof jsxRuntime.jsx).toBe('function')
8
+ })
9
+
10
+ it('should export jsxs function', () => {
11
+ expect(typeof jsxRuntime.jsxs).toBe('function')
12
+ })
13
+
14
+ it('should export Fragment', () => {
15
+ expect(jsxRuntime.Fragment).toBeDefined()
16
+ })
17
+
18
+ it('should export renderJSX function', () => {
19
+ expect(typeof jsxRuntime.renderJSX).toBe('function')
20
+ })
21
+
22
+ it('should have default export', () => {
23
+ expect(jsxRuntime.default).toBeDefined()
24
+ expect(typeof jsxRuntime.default.jsx).toBe('function')
25
+ expect(typeof jsxRuntime.default.jsxs).toBe('function')
26
+ })
27
+ })
28
+
29
+ describe('JSX Dev Runtime', () => {
30
+ it('should export jsx function', () => {
31
+ expect(typeof jsxDevRuntime.jsx).toBe('function')
32
+ })
33
+
34
+ it('should export jsxDEV function', () => {
35
+ expect(typeof jsxDevRuntime.jsxDEV).toBe('function')
36
+ })
37
+
38
+ it('should export Fragment', () => {
39
+ expect(jsxDevRuntime.Fragment).toBeDefined()
40
+ })
41
+
42
+ it('should export renderJSX function', () => {
43
+ expect(typeof jsxDevRuntime.renderJSX).toBe('function')
44
+ })
45
+ })