@zhin.js/core 1.0.0 → 1.0.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 +9 -0
- package/LICENSE +21 -0
- package/README.md +295 -74
- package/lib/adapter.d.ts +39 -0
- package/lib/adapter.d.ts.map +1 -0
- package/{dist → lib}/adapter.js +20 -2
- package/lib/adapter.js.map +1 -0
- package/lib/app.d.ts +115 -0
- package/lib/app.d.ts.map +1 -0
- package/{dist → lib}/app.js +148 -78
- package/lib/app.js.map +1 -0
- package/lib/bot.d.ts +31 -0
- package/lib/bot.d.ts.map +1 -0
- package/lib/command.d.ts +32 -0
- package/lib/command.d.ts.map +1 -0
- package/lib/command.js +46 -0
- package/lib/command.js.map +1 -0
- package/lib/component.d.ts +107 -0
- package/lib/component.d.ts.map +1 -0
- package/lib/component.js +273 -0
- package/lib/component.js.map +1 -0
- package/{dist → lib}/config.d.ts.map +1 -1
- package/{dist → lib}/config.js +6 -9
- package/lib/config.js.map +1 -0
- package/lib/cron.d.ts +81 -0
- package/lib/cron.d.ts.map +1 -0
- package/lib/cron.js +159 -0
- package/lib/cron.js.map +1 -0
- package/lib/errors.d.ts +165 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +306 -0
- package/lib/errors.js.map +1 -0
- package/lib/index.d.ts +15 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +17 -0
- package/lib/index.js.map +1 -0
- package/lib/message.d.ts +44 -0
- package/lib/message.d.ts.map +1 -0
- package/lib/message.js +11 -0
- package/lib/message.js.map +1 -0
- package/lib/plugin.d.ts +50 -0
- package/lib/plugin.d.ts.map +1 -0
- package/lib/plugin.js +170 -0
- package/lib/plugin.js.map +1 -0
- package/lib/prompt.d.ts +116 -0
- package/lib/prompt.d.ts.map +1 -0
- package/lib/prompt.js +240 -0
- package/lib/prompt.js.map +1 -0
- package/lib/schema.d.ts +83 -0
- package/lib/schema.d.ts.map +1 -0
- package/lib/schema.js +245 -0
- package/lib/schema.js.map +1 -0
- package/{dist → lib}/types-generator.d.ts.map +1 -1
- package/{dist → lib}/types-generator.js +6 -3
- package/lib/types-generator.js.map +1 -0
- package/lib/types.d.ts +119 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/utils.d.ts +52 -0
- package/lib/utils.d.ts.map +1 -0
- package/lib/utils.js +338 -0
- package/lib/utils.js.map +1 -0
- package/package.json +15 -9
- package/src/adapter.ts +25 -9
- package/src/app.ts +363 -258
- package/src/bot.ts +29 -8
- package/src/command.ts +50 -0
- package/src/component.ts +318 -0
- package/src/config.ts +9 -12
- package/src/cron.ts +176 -0
- package/src/errors.ts +365 -0
- package/src/index.ts +16 -13
- package/src/message.ts +44 -0
- package/src/plugin.ts +148 -66
- package/src/prompt.ts +290 -0
- package/src/schema.ts +273 -0
- package/src/types-generator.ts +7 -3
- package/src/types.ts +77 -30
- package/src/utils.ts +312 -0
- package/tests/adapter.test.ts +36 -22
- package/tests/app.test.ts +30 -0
- package/tests/command.test.ts +545 -0
- package/tests/component.test.ts +656 -0
- package/tests/config.test.ts +1 -1
- package/tests/errors.test.ts +311 -0
- package/tests/message.test.ts +402 -0
- package/tests/plugin.test.ts +275 -143
- package/tests/utils.test.ts +80 -0
- package/tsconfig.json +3 -4
- package/dist/adapter.d.ts +0 -22
- package/dist/adapter.d.ts.map +0 -1
- package/dist/adapter.js.map +0 -1
- package/dist/app.d.ts +0 -69
- package/dist/app.d.ts.map +0 -1
- package/dist/app.js.map +0 -1
- package/dist/bot.d.ts +0 -9
- package/dist/bot.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/index.d.ts +0 -9
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -12
- package/dist/index.js.map +0 -1
- package/dist/logger.d.ts +0 -3
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -3
- package/dist/logger.js.map +0 -1
- package/dist/plugin.d.ts +0 -41
- package/dist/plugin.d.ts.map +0 -1
- package/dist/plugin.js +0 -95
- package/dist/plugin.js.map +0 -1
- package/dist/types-generator.js.map +0 -1
- package/dist/types.d.ts +0 -69
- package/dist/types.d.ts.map +0 -1
- package/src/logger.ts +0 -3
- package/tests/logger.test.ts +0 -170
- package/tsconfig.tsbuildinfo +0 -1
- /package/{dist → lib}/bot.js +0 -0
- /package/{dist → lib}/bot.js.map +0 -0
- /package/{dist → lib}/config.d.ts +0 -0
- /package/{dist → lib}/types-generator.d.ts +0 -0
- /package/{dist → lib}/types.js +0 -0
- /package/{dist → lib}/types.js.map +0 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
import { Component, defineComponent, CapWithChild, CapWithClose } from '../src/component'
|
|
3
|
+
import { Message } from '../src/message'
|
|
4
|
+
|
|
5
|
+
// Mock utils functions
|
|
6
|
+
vi.mock('../src/utils', () => ({
|
|
7
|
+
getValueWithRuntime: vi.fn((expression, context) => {
|
|
8
|
+
// Simple mock implementation
|
|
9
|
+
if (typeof expression === 'string' && context) {
|
|
10
|
+
// Handle simple variable access
|
|
11
|
+
if (expression in context) {
|
|
12
|
+
return context[expression]
|
|
13
|
+
}
|
|
14
|
+
// Handle object property access like 'user.name'
|
|
15
|
+
if (expression.includes('.')) {
|
|
16
|
+
const [obj, prop] = expression.split('.')
|
|
17
|
+
return context[obj]?.[prop]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return expression
|
|
21
|
+
}),
|
|
22
|
+
compiler: vi.fn((template, context) => template),
|
|
23
|
+
segment: {
|
|
24
|
+
toString: vi.fn((content) => typeof content === 'string' ? content : JSON.stringify(content)),
|
|
25
|
+
from: vi.fn((content) => content),
|
|
26
|
+
escape: vi.fn((content) => content.replace(/</g, '<').replace(/>/g, '>'))
|
|
27
|
+
}
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
describe('Component系统测试', () => {
|
|
31
|
+
let mockContext: Component.Context
|
|
32
|
+
let mockMessage: Message
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
mockMessage = {
|
|
36
|
+
$id: '1',
|
|
37
|
+
$adapter: 'test',
|
|
38
|
+
$bot: 'test-bot',
|
|
39
|
+
$content: [],
|
|
40
|
+
$sender: { id: 'user1', name: 'User' },
|
|
41
|
+
$reply: vi.fn(),
|
|
42
|
+
$channel: { id: 'channel1', type: 'private' },
|
|
43
|
+
$timestamp: Date.now(),
|
|
44
|
+
$raw: 'test'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
mockContext = {
|
|
48
|
+
$slots: {},
|
|
49
|
+
$message: mockMessage,
|
|
50
|
+
$root: 'test template',
|
|
51
|
+
parent: {} as any,
|
|
52
|
+
render: vi.fn().mockResolvedValue('rendered content'),
|
|
53
|
+
children: 'default children'
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('Component类基础功能测试', () => {
|
|
58
|
+
it('应该正确创建Component实例', () => {
|
|
59
|
+
const component = new Component({
|
|
60
|
+
name: 'test-component',
|
|
61
|
+
render: (props) => `Hello ${props.name || 'World'}`
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
expect(component).toBeInstanceOf(Component)
|
|
65
|
+
expect(component.name).toBe('test-component')
|
|
66
|
+
expect(component.$props).toEqual([])
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('应该正确处理带属性的组件', () => {
|
|
70
|
+
const component = new Component({
|
|
71
|
+
name: 'user-card',
|
|
72
|
+
props: {
|
|
73
|
+
name: String,
|
|
74
|
+
age: Number,
|
|
75
|
+
active: Boolean
|
|
76
|
+
},
|
|
77
|
+
render: (props) => `User: ${props.name}, Age: ${props.age}, Active: ${props.active}`
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
expect(component.$props).toHaveLength(3)
|
|
81
|
+
expect(component.$props[0].name).toBe('name')
|
|
82
|
+
expect(component.$props[0].type).toBe(String)
|
|
83
|
+
expect(component.$props[1].name).toBe('age')
|
|
84
|
+
expect(component.$props[1].type).toBe(Number)
|
|
85
|
+
expect(component.$props[2].name).toBe('active')
|
|
86
|
+
expect(component.$props[2].type).toBe(Boolean)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('应该正确处理带默认值的属性', () => {
|
|
90
|
+
const component = new Component({
|
|
91
|
+
name: 'greeting',
|
|
92
|
+
props: {
|
|
93
|
+
message: {
|
|
94
|
+
type: String,
|
|
95
|
+
default: 'Hello World'
|
|
96
|
+
},
|
|
97
|
+
count: {
|
|
98
|
+
type: Number,
|
|
99
|
+
default: 0
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
render: (props) => `${props.message} (${props.count})`
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
expect(component.$props).toHaveLength(2)
|
|
106
|
+
expect(component.$props[0].default).toBe('Hello World')
|
|
107
|
+
expect(component.$props[1].default).toBe(0)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('应该支持动态名称设置', () => {
|
|
111
|
+
const component = new Component({
|
|
112
|
+
name: 'initial-name',
|
|
113
|
+
render: () => 'content'
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
expect(component.name).toBe('initial-name')
|
|
117
|
+
|
|
118
|
+
component.name = 'new-name'
|
|
119
|
+
expect(component.name).toBe('new-name')
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
describe('模板匹配测试', () => {
|
|
124
|
+
let component: Component
|
|
125
|
+
|
|
126
|
+
beforeEach(() => {
|
|
127
|
+
component = new Component({
|
|
128
|
+
name: 'test-comp',
|
|
129
|
+
render: () => 'test'
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('应该正确匹配自闭合标签', () => {
|
|
134
|
+
const template = '<test-comp name="test" />'
|
|
135
|
+
expect(component.isClosing(template)).toBe(true)
|
|
136
|
+
expect(component.match(template)).toBe(template)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('应该正确匹配带children的标签', () => {
|
|
140
|
+
const template = '<test-comp name="test">Hello World</test-comp>'
|
|
141
|
+
expect(component.isClosing(template)).toBe(false)
|
|
142
|
+
expect(component.match(template)).toBe(template)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('应该在不匹配时返回undefined', () => {
|
|
146
|
+
const template = '<other-comp name="test" />'
|
|
147
|
+
expect(component.match(template)).toBeUndefined()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('应该正确识别符号常量', () => {
|
|
151
|
+
expect(component[CapWithChild]).toBeInstanceOf(RegExp)
|
|
152
|
+
expect(component[CapWithClose]).toBeInstanceOf(RegExp)
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
describe('属性解析测试', () => {
|
|
157
|
+
let component: Component
|
|
158
|
+
|
|
159
|
+
beforeEach(() => {
|
|
160
|
+
component = new Component({
|
|
161
|
+
name: 'prop-test',
|
|
162
|
+
props: {
|
|
163
|
+
name: String,
|
|
164
|
+
age: {
|
|
165
|
+
type: Number,
|
|
166
|
+
default: 18
|
|
167
|
+
},
|
|
168
|
+
active: {
|
|
169
|
+
type: Boolean,
|
|
170
|
+
default: false
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
render: () => 'test'
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('应该正确解析属性', () => {
|
|
178
|
+
const template = '<prop-test name="John" age="25" active="true" />'
|
|
179
|
+
const props = component.parseProps(template)
|
|
180
|
+
|
|
181
|
+
expect(props.name).toBe('John')
|
|
182
|
+
expect(props.age).toBe('25') // Note: parseProps returns strings
|
|
183
|
+
expect(props.active).toBe('true')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('应该使用默认值填充缺失的属性', () => {
|
|
187
|
+
const template = '<prop-test name="John" />'
|
|
188
|
+
const props = component.parseProps(template)
|
|
189
|
+
|
|
190
|
+
expect(props.name).toBe('John')
|
|
191
|
+
expect(props.age).toBe(18)
|
|
192
|
+
expect(props.active).toBe(false)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('应该正确处理带引号的属性值', () => {
|
|
196
|
+
const template = `<prop-test name='John Doe' description="A 'test' user" />`
|
|
197
|
+
const props = component.parseProps(template)
|
|
198
|
+
|
|
199
|
+
expect(props.name).toBe('John Doe')
|
|
200
|
+
expect(props.description).toBe("A 'test' user")
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('应该处理kebab-case到camelCase的转换', () => {
|
|
204
|
+
const template = '<prop-test user-name="john" max-count="10" />'
|
|
205
|
+
// This test would need the actual render method to see the conversion
|
|
206
|
+
// For now, we just test that parseProps extracts the attributes
|
|
207
|
+
const props = component.parseProps(template)
|
|
208
|
+
|
|
209
|
+
expect(props['user-name']).toBe('john')
|
|
210
|
+
expect(props['max-count']).toBe('10')
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
describe('Children解析测试', () => {
|
|
215
|
+
let component: Component
|
|
216
|
+
|
|
217
|
+
beforeEach(() => {
|
|
218
|
+
component = new Component({
|
|
219
|
+
name: 'child-test',
|
|
220
|
+
render: () => 'test'
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('应该正确解析children内容', () => {
|
|
225
|
+
const template = '<child-test>Hello World</child-test>'
|
|
226
|
+
const children = component.parseChildren(template)
|
|
227
|
+
|
|
228
|
+
expect(children).toBe('Hello World')
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('应该在自闭合标签中返回空字符串', () => {
|
|
232
|
+
const template = '<child-test />'
|
|
233
|
+
const children = component.parseChildren(template)
|
|
234
|
+
|
|
235
|
+
expect(children).toBe('')
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('应该处理复杂的children内容', () => {
|
|
239
|
+
const template = '<child-test><span>Nested content</span></child-test>'
|
|
240
|
+
const children = component.parseChildren(template)
|
|
241
|
+
|
|
242
|
+
// parseChildren实际只提取标签之间的文本内容
|
|
243
|
+
expect(children).toBe('Nested content')
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
describe('渲染功能测试', () => {
|
|
248
|
+
it('应该正确渲染简单组件', async () => {
|
|
249
|
+
const component = new Component({
|
|
250
|
+
name: 'simple',
|
|
251
|
+
render: () => 'Hello World'
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
const template = '<simple />'
|
|
255
|
+
const result = await component.render(template, mockContext)
|
|
256
|
+
|
|
257
|
+
expect(result).toBe('rendered content') // Mocked render function result
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('应该正确传递props给渲染函数', async () => {
|
|
261
|
+
const renderSpy = vi.fn().mockReturnValue('rendered')
|
|
262
|
+
|
|
263
|
+
const component = new Component({
|
|
264
|
+
name: 'props-comp',
|
|
265
|
+
props: {
|
|
266
|
+
name: String
|
|
267
|
+
},
|
|
268
|
+
render: renderSpy
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
const template = '<props-comp name="John" />'
|
|
272
|
+
await component.render(template, mockContext)
|
|
273
|
+
|
|
274
|
+
expect(renderSpy).toHaveBeenCalled()
|
|
275
|
+
// The first argument should contain the parsed props
|
|
276
|
+
const [props] = renderSpy.mock.calls[0]
|
|
277
|
+
expect(props.name).toBe('John')
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('应该正确处理data函数', async () => {
|
|
281
|
+
const component = new Component({
|
|
282
|
+
name: 'data-comp',
|
|
283
|
+
props: {
|
|
284
|
+
name: String
|
|
285
|
+
},
|
|
286
|
+
data() {
|
|
287
|
+
return {
|
|
288
|
+
computed: `Hello ${this.name}`
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
render: (props, context) => context.computed
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
const template = '<data-comp name="World" />'
|
|
295
|
+
const result = await component.render(template, mockContext)
|
|
296
|
+
|
|
297
|
+
expect(result).toBe('rendered content') // Mocked result
|
|
298
|
+
})
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
describe('指令系统测试', () => {
|
|
302
|
+
it('应该正确处理v-if指令', async () => {
|
|
303
|
+
const component = new Component({
|
|
304
|
+
name: 'conditional',
|
|
305
|
+
render: () => 'Content'
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
// Mock getValueWithRuntime to return false for v-if
|
|
309
|
+
const { getValueWithRuntime } = await import('../src/utils')
|
|
310
|
+
vi.mocked(getValueWithRuntime).mockReturnValueOnce(false)
|
|
311
|
+
|
|
312
|
+
const template = '<conditional v-if="false" />'
|
|
313
|
+
const result = await component.render(template, mockContext)
|
|
314
|
+
|
|
315
|
+
expect(result).toBe('') // Should return empty string when v-if is false
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it('应该正确处理v-for指令', async () => {
|
|
319
|
+
const component = new Component({
|
|
320
|
+
name: 'list-item',
|
|
321
|
+
render: (props) => `Item: ${props.item}`
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
const template = '<list-item v-for="item in items" />'
|
|
325
|
+
const contextWithItems = {
|
|
326
|
+
...mockContext,
|
|
327
|
+
items: ['A', 'B', 'C']
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// This test is complex due to the v-for implementation
|
|
331
|
+
// We'll test that the render method is called
|
|
332
|
+
const result = await component.render(template, contextWithItems)
|
|
333
|
+
expect(result).toBeDefined()
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
describe('循环处理测试', () => {
|
|
338
|
+
it('应该正确解析简单循环表达式', () => {
|
|
339
|
+
const result = Component.fixLoop('item in items')
|
|
340
|
+
|
|
341
|
+
expect(result).toEqual({
|
|
342
|
+
name: 'item',
|
|
343
|
+
value: 'items'
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it('应该正确处理数字范围循环', () => {
|
|
348
|
+
const result = Component.fixLoop('i in 3')
|
|
349
|
+
|
|
350
|
+
expect(result).toEqual({
|
|
351
|
+
name: 'i',
|
|
352
|
+
value: '__loop__',
|
|
353
|
+
__loop__: [0, 1, 2]
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('应该正确处理数组字面量循环', () => {
|
|
358
|
+
const result = Component.fixLoop('item in ["a","b","c"]')
|
|
359
|
+
|
|
360
|
+
expect(result).toEqual({
|
|
361
|
+
name: 'item',
|
|
362
|
+
value: '__loop__',
|
|
363
|
+
__loop__: ["a", "b", "c"]
|
|
364
|
+
})
|
|
365
|
+
})
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
describe('静态渲染方法测试', () => {
|
|
369
|
+
it('应该正确处理空组件映射', async () => {
|
|
370
|
+
const componentMap = new Map()
|
|
371
|
+
const options = { content: 'Hello World' }
|
|
372
|
+
|
|
373
|
+
const result = await Component.render(componentMap, options)
|
|
374
|
+
|
|
375
|
+
expect(result).toEqual(options) // Should return original options unchanged
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
it('应该正确渲染带组件的内容', async () => {
|
|
379
|
+
const component = new Component({
|
|
380
|
+
name: 'test-comp',
|
|
381
|
+
render: () => 'Rendered'
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
const componentMap = new Map([['test-comp', component]])
|
|
385
|
+
const options = { content: '<test-comp />' }
|
|
386
|
+
|
|
387
|
+
// Mock segment functions
|
|
388
|
+
const { segment } = await import('../src/utils')
|
|
389
|
+
vi.mocked(segment.toString).mockReturnValue('<test-comp />')
|
|
390
|
+
vi.mocked(segment.from).mockReturnValue('processed content')
|
|
391
|
+
|
|
392
|
+
const result = await Component.render(componentMap, options)
|
|
393
|
+
|
|
394
|
+
expect(result.content).toBe('processed content')
|
|
395
|
+
})
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
describe('内置组件测试', () => {
|
|
399
|
+
describe('Template组件', () => {
|
|
400
|
+
it('应该正确处理模板组件的render函数', () => {
|
|
401
|
+
const props = { '#header': 'true' }
|
|
402
|
+
const contextWithParent = {
|
|
403
|
+
...mockContext,
|
|
404
|
+
parent: { ...mockContext.parent, $slots: {} }
|
|
405
|
+
}
|
|
406
|
+
const result = Component.Template.$options.render(props, contextWithParent)
|
|
407
|
+
|
|
408
|
+
expect(result).toBe('') // Template components return empty string
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
it('应该正确设置插槽', () => {
|
|
412
|
+
const props = { '#header': 'true', '#footer': 'true' }
|
|
413
|
+
const contextWithParent = {
|
|
414
|
+
...mockContext,
|
|
415
|
+
parent: { ...mockContext.parent, $slots: {} }
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
Component.Template.$options.render(props, contextWithParent)
|
|
419
|
+
|
|
420
|
+
// Should have set slots in parent context
|
|
421
|
+
expect(Object.keys(contextWithParent.parent.$slots)).toContain('header')
|
|
422
|
+
expect(Object.keys(contextWithParent.parent.$slots)).toContain('footer')
|
|
423
|
+
})
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
describe('Slot组件', () => {
|
|
427
|
+
it('应该正确渲染默认插槽', () => {
|
|
428
|
+
const contextWithSlots = {
|
|
429
|
+
...mockContext,
|
|
430
|
+
parent: {
|
|
431
|
+
...mockContext.parent,
|
|
432
|
+
$slots: {
|
|
433
|
+
default: vi.fn().mockReturnValue('Slot Content')
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const result = Component.Slot.$options.render({}, contextWithSlots)
|
|
439
|
+
|
|
440
|
+
expect(result).toBe('Slot Content')
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
it('应该正确渲染命名插槽', () => {
|
|
444
|
+
const contextWithSlots = {
|
|
445
|
+
...mockContext,
|
|
446
|
+
parent: {
|
|
447
|
+
...mockContext.parent,
|
|
448
|
+
$slots: {
|
|
449
|
+
header: vi.fn().mockReturnValue('Header Slot')
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const result = Component.Slot.$options.render({ name: 'header' }, contextWithSlots)
|
|
455
|
+
|
|
456
|
+
expect(result).toBe('Header Slot')
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
it('应该在插槽不存在时使用children', () => {
|
|
460
|
+
const contextWithoutSlots = {
|
|
461
|
+
...mockContext,
|
|
462
|
+
parent: { ...mockContext.parent, $slots: {} },
|
|
463
|
+
children: 'Fallback Content'
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const result = Component.Slot.$options.render({ name: 'missing' }, contextWithoutSlots)
|
|
467
|
+
|
|
468
|
+
expect(result).toBe('Fallback Content')
|
|
469
|
+
})
|
|
470
|
+
})
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
describe('defineComponent工厂函数测试', () => {
|
|
474
|
+
it('应该正确创建函数式组件', () => {
|
|
475
|
+
const renderFn = (props: any) => `Hello ${props.name}`
|
|
476
|
+
const component = defineComponent(renderFn, 'func-comp')
|
|
477
|
+
|
|
478
|
+
expect(component).toBeInstanceOf(Component)
|
|
479
|
+
expect(component.name).toBe('func-comp')
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
it('应该正确创建配置式组件', () => {
|
|
483
|
+
const component = defineComponent({
|
|
484
|
+
name: 'config-comp',
|
|
485
|
+
props: {
|
|
486
|
+
title: String
|
|
487
|
+
},
|
|
488
|
+
render: (props) => props.title
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
expect(component).toBeInstanceOf(Component)
|
|
492
|
+
expect(component.name).toBe('config-comp')
|
|
493
|
+
expect(component.$props).toHaveLength(1)
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
it('应该正确处理重载函数', () => {
|
|
497
|
+
// Test function overload
|
|
498
|
+
const renderFn = () => 'test'
|
|
499
|
+
const funcComponent = defineComponent(renderFn)
|
|
500
|
+
expect(funcComponent).toBeInstanceOf(Component)
|
|
501
|
+
|
|
502
|
+
// Test options overload
|
|
503
|
+
const optionsComponent = defineComponent({
|
|
504
|
+
name: 'options-comp',
|
|
505
|
+
render: () => 'test'
|
|
506
|
+
})
|
|
507
|
+
expect(optionsComponent).toBeInstanceOf(Component)
|
|
508
|
+
expect(optionsComponent.name).toBe('options-comp')
|
|
509
|
+
})
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
describe('复杂场景测试', () => {
|
|
513
|
+
it('应该正确处理嵌套组件', async () => {
|
|
514
|
+
const childComponent = new Component({
|
|
515
|
+
name: 'child',
|
|
516
|
+
render: (props) => `Child: ${props.content}`
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
const parentComponent = new Component({
|
|
520
|
+
name: 'parent',
|
|
521
|
+
render: () => '<child content="nested" />'
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
const componentMap = new Map([
|
|
525
|
+
['child', childComponent],
|
|
526
|
+
['parent', parentComponent]
|
|
527
|
+
])
|
|
528
|
+
|
|
529
|
+
const options = { content: '<parent />' }
|
|
530
|
+
const result = await Component.render(componentMap, options)
|
|
531
|
+
|
|
532
|
+
expect(result).toBeDefined()
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
it('应该正确处理属性绑定', async () => {
|
|
536
|
+
const component = new Component({
|
|
537
|
+
name: 'bind-test',
|
|
538
|
+
render: (props) => `Value: ${props.dynamicValue}`
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
const template = '<bind-test :dynamic-value="someVar" />'
|
|
542
|
+
const contextWithVar = {
|
|
543
|
+
...mockContext,
|
|
544
|
+
someVar: 'Dynamic Content'
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const result = await component.render(template, contextWithVar)
|
|
548
|
+
expect(result).toBeDefined()
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
it('应该正确处理上下文传递', async () => {
|
|
552
|
+
const component = new Component({
|
|
553
|
+
name: 'context-test',
|
|
554
|
+
render: (props, context) => {
|
|
555
|
+
expect(context.$message).toBe(mockMessage)
|
|
556
|
+
expect(context.parent).toBeDefined()
|
|
557
|
+
return 'Context OK'
|
|
558
|
+
}
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
const template = '<context-test />'
|
|
562
|
+
await component.render(template, mockContext)
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
it('应该正确处理错误情况', async () => {
|
|
566
|
+
const component = new Component({
|
|
567
|
+
name: 'error-test',
|
|
568
|
+
render: () => {
|
|
569
|
+
throw new Error('Render error')
|
|
570
|
+
}
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
const template = '<error-test />'
|
|
574
|
+
|
|
575
|
+
// The render method should handle errors gracefully
|
|
576
|
+
await expect(component.render(template, mockContext)).rejects.toThrow('Render error')
|
|
577
|
+
})
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
describe('性能和边界测试', () => {
|
|
581
|
+
it('应该处理空模板', async () => {
|
|
582
|
+
const component = new Component({
|
|
583
|
+
name: 'empty-test',
|
|
584
|
+
render: () => ''
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
const result = await component.render('', mockContext)
|
|
588
|
+
expect(result).toBe('rendered content') // Mocked result
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
it('应该处理大量属性', async () => {
|
|
592
|
+
const manyProps: any = {}
|
|
593
|
+
for (let i = 0; i < 100; i++) {
|
|
594
|
+
manyProps[`prop${i}`] = String
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const component = new Component({
|
|
598
|
+
name: 'many-props',
|
|
599
|
+
props: manyProps,
|
|
600
|
+
render: () => 'OK'
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
expect(component.$props).toHaveLength(100)
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
it('应该处理长模板字符串', async () => {
|
|
607
|
+
const component = new Component({
|
|
608
|
+
name: 'long-template',
|
|
609
|
+
render: () => 'OK'
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
const longContent = 'A'.repeat(10000)
|
|
613
|
+
const template = `<long-template content="${longContent}" />`
|
|
614
|
+
|
|
615
|
+
const result = await component.render(template, mockContext)
|
|
616
|
+
expect(result).toBeDefined()
|
|
617
|
+
})
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
describe('类型系统测试', () => {
|
|
621
|
+
it('应该支持泛型类型约束', () => {
|
|
622
|
+
interface Props {
|
|
623
|
+
title: string
|
|
624
|
+
count: number
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const component: Component<{}, {}, Props> = new Component({
|
|
628
|
+
name: 'typed-comp',
|
|
629
|
+
render: (props: Props) => `${props.title}: ${props.count}`
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
expect(component).toBeInstanceOf(Component)
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
it('应该正确处理PropConfig类型', () => {
|
|
636
|
+
const component = new Component({
|
|
637
|
+
name: 'prop-config-test',
|
|
638
|
+
props: {
|
|
639
|
+
stringProp: String,
|
|
640
|
+
numberProp: Number,
|
|
641
|
+
booleanProp: Boolean,
|
|
642
|
+
arrayProp: Array,
|
|
643
|
+
objectProp: Object
|
|
644
|
+
},
|
|
645
|
+
render: () => 'test'
|
|
646
|
+
})
|
|
647
|
+
|
|
648
|
+
const propTypes = component.$props.map(p => p.type)
|
|
649
|
+
expect(propTypes).toContain(String)
|
|
650
|
+
expect(propTypes).toContain(Number)
|
|
651
|
+
expect(propTypes).toContain(Boolean)
|
|
652
|
+
expect(propTypes).toContain(Array)
|
|
653
|
+
expect(propTypes).toContain(Object)
|
|
654
|
+
})
|
|
655
|
+
})
|
|
656
|
+
})
|
package/tests/config.test.ts
CHANGED
|
@@ -101,7 +101,7 @@ module.exports = {
|
|
|
101
101
|
]
|
|
102
102
|
}
|
|
103
103
|
`
|
|
104
|
-
const configPath = path.join(testDir, 'zhin.config.
|
|
104
|
+
const configPath = path.join(testDir, 'zhin.config.ts')
|
|
105
105
|
fs.writeFileSync(configPath, config)
|
|
106
106
|
|
|
107
107
|
const [loadedPath, loadedConfig] = await loadConfig({ configPath })
|