@zhin.js/core 1.0.1 → 1.0.2
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 +7 -0
- package/lib/app.d.ts +1 -1
- package/lib/app.d.ts.map +1 -1
- package/lib/app.js +1 -1
- package/lib/app.js.map +1 -1
- package/lib/component.d.ts +22 -102
- package/lib/component.d.ts.map +1 -1
- package/lib/component.js +438 -242
- package/lib/component.js.map +1 -1
- package/lib/jsx-runtime.d.ts +12 -0
- package/lib/jsx-runtime.d.ts.map +1 -0
- package/lib/jsx-runtime.js +11 -0
- package/lib/jsx-runtime.js.map +1 -0
- package/lib/jsx.d.ts +32 -0
- package/lib/jsx.d.ts.map +1 -0
- package/lib/jsx.js +57 -0
- package/lib/jsx.js.map +1 -0
- package/lib/message.d.ts +9 -6
- package/lib/message.d.ts.map +1 -1
- package/lib/message.js.map +1 -1
- package/lib/plugin.d.ts +2 -2
- package/lib/plugin.d.ts.map +1 -1
- package/lib/plugin.js +2 -2
- package/lib/plugin.js.map +1 -1
- package/lib/types.d.ts +3 -1
- package/lib/types.d.ts.map +1 -1
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +3 -1
- package/lib/utils.js.map +1 -1
- package/package.json +10 -2
- package/src/app.ts +3 -3
- package/src/component.ts +523 -280
- package/src/jsx-runtime.ts +12 -0
- package/src/jsx.d.ts +52 -0
- package/src/jsx.ts +92 -0
- package/src/message.ts +7 -4
- package/src/plugin.ts +4 -4
- package/src/types.ts +3 -1
- package/src/utils.ts +6 -5
- package/tests/component-new.test.ts +348 -0
- package/tests/expression-evaluation.test.ts +258 -0
- package/tests/plugin.test.ts +26 -17
- package/tests/component.test.ts +0 -656
package/src/jsx.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { SendContent } from './types.js';
|
|
2
|
+
import { Component,ComponentContext } from './component.js';
|
|
3
|
+
import { MessageElement } from './types.js';
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
namespace JSX {
|
|
7
|
+
interface Element {
|
|
8
|
+
type: string | Component<any>;
|
|
9
|
+
data: Record<string, any>;
|
|
10
|
+
children?: JSXChildren;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ElementClass {
|
|
14
|
+
render(props: any, context?: ComponentContext): MaybePromise<SendContent>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ElementAttributesProperty {
|
|
18
|
+
data: {};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ElementChildrenAttribute {
|
|
22
|
+
children: {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface IntrinsicElements {
|
|
26
|
+
// 简化的组件元素
|
|
27
|
+
fetch: JSXFetchElement;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 简化的组件属性接口
|
|
33
|
+
interface JSXBaseElement {
|
|
34
|
+
children?: JSXChildren;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface JSXFetchElement extends JSXBaseElement {
|
|
38
|
+
url?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// JSX 子元素类型
|
|
42
|
+
type JSXChildren = MessageElement | string | number | boolean | null | undefined | JSXChildren[];
|
|
43
|
+
|
|
44
|
+
// JSX 元素类型
|
|
45
|
+
type JSXElement = {
|
|
46
|
+
type: string | Component<any>;
|
|
47
|
+
data: Record<string, any>;
|
|
48
|
+
children?: JSXChildren;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// 类型辅助
|
|
52
|
+
type MaybePromise<T> = T | Promise<T>;
|
package/src/jsx.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { MaybePromise } from '@zhin.js/types';
|
|
2
|
+
import { SendContent,MessageElement } from './types.js';
|
|
3
|
+
import { MessageComponent } from './message.js';
|
|
4
|
+
import { Component, ComponentContext } from './component.js';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
// JSX 子元素类型
|
|
8
|
+
export type JSXChildren = MessageElement | string | number | boolean | null | undefined | JSXChildren[];
|
|
9
|
+
|
|
10
|
+
// JSX 元素类型
|
|
11
|
+
export type JSXElementType = string | Component<any> ;
|
|
12
|
+
|
|
13
|
+
// JSX 属性类型
|
|
14
|
+
export type JSXProps = Record<string, any> & {
|
|
15
|
+
children?: JSXChildren;
|
|
16
|
+
};
|
|
17
|
+
export {Fragment} from './component.js'
|
|
18
|
+
// 全局 JSX 命名空间
|
|
19
|
+
declare global {
|
|
20
|
+
namespace JSX {
|
|
21
|
+
interface Element extends MessageComponent<any> {}
|
|
22
|
+
interface ElementClass {
|
|
23
|
+
render(props: any, context?: ComponentContext): MaybePromise<SendContent>;
|
|
24
|
+
}
|
|
25
|
+
interface ElementAttributesProperty {
|
|
26
|
+
data: {};
|
|
27
|
+
}
|
|
28
|
+
interface ElementChildrenAttribute {
|
|
29
|
+
children: {};
|
|
30
|
+
}
|
|
31
|
+
interface IntrinsicElements {
|
|
32
|
+
[elemName: string]: any;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// JSX 运行时函数
|
|
38
|
+
export function jsx(type: JSXElementType, data: JSXProps): MessageElement {
|
|
39
|
+
return {
|
|
40
|
+
type,
|
|
41
|
+
data,
|
|
42
|
+
} as MessageElement;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// JSX Fragment 支持
|
|
46
|
+
export function jsxs(type: JSXElementType, props: JSXProps): MessageElement {
|
|
47
|
+
return jsx(type, props);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
// JSX 渲染函数
|
|
52
|
+
export async function renderJSX(element: MessageComponent<any>, context?: ComponentContext): Promise<SendContent> {
|
|
53
|
+
if (typeof element.type === 'string') {
|
|
54
|
+
if (element.type === 'Fragment') {
|
|
55
|
+
return await renderChildren(element.data.children, context);
|
|
56
|
+
}
|
|
57
|
+
// 其他内置组件处理
|
|
58
|
+
return await renderChildren(element.data.children, context);
|
|
59
|
+
} else if (typeof element.type === 'function') {
|
|
60
|
+
// 函数组件
|
|
61
|
+
const component = element.type as Component<any>;
|
|
62
|
+
return await component(element.data, context || {} as ComponentContext);
|
|
63
|
+
} else {
|
|
64
|
+
// 类组件或其他类型
|
|
65
|
+
const component = element.type as Component<any>;
|
|
66
|
+
return await component(element.data, context || {} as ComponentContext);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 渲染子元素
|
|
71
|
+
async function renderChildren(children: JSXChildren, context?: ComponentContext): Promise<SendContent> {
|
|
72
|
+
if (children == null) return '';
|
|
73
|
+
if (typeof children === 'string' || typeof children === 'number' || typeof children === 'boolean') {
|
|
74
|
+
return String(children);
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(children)) {
|
|
77
|
+
const results = await Promise.all(children.map(async child => {
|
|
78
|
+
if (typeof child === 'string' || typeof child === 'number' || typeof child === 'boolean') {
|
|
79
|
+
return String(child);
|
|
80
|
+
}
|
|
81
|
+
if (child && typeof child === 'object' && 'type' in child) {
|
|
82
|
+
return await renderJSX(child as MessageComponent<any>, context);
|
|
83
|
+
}
|
|
84
|
+
return '';
|
|
85
|
+
}));
|
|
86
|
+
return results.join('');
|
|
87
|
+
}
|
|
88
|
+
if (children && typeof children === 'object' && 'type' in children) {
|
|
89
|
+
return await renderJSX(children as MessageComponent<any>, context);
|
|
90
|
+
}
|
|
91
|
+
return '';
|
|
92
|
+
}
|
package/src/message.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import {MessageElement, MessageSender, SendContent} from "./types";
|
|
2
|
+
import { Component } from "./component.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* 消息组件类型:用于自定义消息结构
|
|
6
6
|
*/
|
|
7
|
-
export type MessageComponent<T extends object>=
|
|
7
|
+
export type MessageComponent<T extends object>={
|
|
8
|
+
type:Component<T&{children?:SendContent}>
|
|
9
|
+
data:T
|
|
10
|
+
}
|
|
8
11
|
/**
|
|
9
12
|
* 消息频道信息
|
|
10
13
|
*/
|
|
@@ -23,7 +26,7 @@ export interface MessageBase {
|
|
|
23
26
|
$id: string;
|
|
24
27
|
$adapter:string
|
|
25
28
|
$bot:string
|
|
26
|
-
$content:
|
|
29
|
+
$content: MessageElement[];
|
|
27
30
|
$sender: MessageSender;
|
|
28
31
|
$reply(content:SendContent,quote?:boolean|string):Promise<void>
|
|
29
32
|
$channel: MessageChannel;
|
package/src/plugin.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {Message} from './message.js'
|
|
|
9
9
|
import {Dependency, Logger,} from "@zhin.js/hmr";
|
|
10
10
|
import {App} from "./app";
|
|
11
11
|
import {MessageCommand} from "./command.js";
|
|
12
|
-
import {Component} from "./component.js";
|
|
12
|
+
import {Component, renderComponents} from "./component.js";
|
|
13
13
|
import { PluginError, MessageError, errorManager } from './errors.js';
|
|
14
14
|
import {remove} from "./utils.js";
|
|
15
15
|
import {Prompt} from "./prompt.js";
|
|
@@ -30,7 +30,7 @@ export type MessageMiddleware<P extends RegisteredAdapter=RegisteredAdapter> = (
|
|
|
30
30
|
*/
|
|
31
31
|
export class Plugin extends Dependency<Plugin> {
|
|
32
32
|
middlewares: MessageMiddleware<any>[] = [];
|
|
33
|
-
components: Map<string, Component<any
|
|
33
|
+
components: Map<string, Component<any>> = new Map();
|
|
34
34
|
schemas: Map<string,Schema<any>>=new Map();
|
|
35
35
|
commands:MessageCommand[]=[];
|
|
36
36
|
crons:Cron[]=[];
|
|
@@ -54,7 +54,7 @@ export class Plugin extends Dependency<Plugin> {
|
|
|
54
54
|
return next()
|
|
55
55
|
});
|
|
56
56
|
// 发送前渲染组件
|
|
57
|
-
this.beforeSend((options)=>
|
|
57
|
+
this.beforeSend((options)=>renderComponents(this.components,options))
|
|
58
58
|
// 资源清理:卸载时清空模型、定时任务等
|
|
59
59
|
this.on('dispose',()=>{
|
|
60
60
|
for(const name of this.schemas.keys()){
|
|
@@ -140,7 +140,7 @@ export class Plugin extends Dependency<Plugin> {
|
|
|
140
140
|
return temp.getLogger(names.join('/'))
|
|
141
141
|
}
|
|
142
142
|
/** 添加组件 */
|
|
143
|
-
addComponent<T
|
|
143
|
+
addComponent<T=any>(component:Component<T>){
|
|
144
144
|
this.components.set(component.name,component);
|
|
145
145
|
}
|
|
146
146
|
/** 添加中间件 */
|
package/src/types.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {MessageChannel} from "./message.js";
|
|
|
3
3
|
import {Adapter} from "./adapter.js";
|
|
4
4
|
import {Bot,BotConfig} from "./bot.js";
|
|
5
5
|
import { Databases,Registry } from "@zhin.js/database";
|
|
6
|
+
import { MessageComponent } from "./message.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* 类型定义文件:包含适配器、消息、数据库、配置等核心类型声明。
|
|
@@ -52,6 +53,7 @@ export interface MessageSegment {
|
|
|
52
53
|
type: string;
|
|
53
54
|
data: Record<string, any>;
|
|
54
55
|
}
|
|
56
|
+
export type MessageElement=MessageSegment|MessageComponent<any>
|
|
55
57
|
/**
|
|
56
58
|
* 单个或数组类型
|
|
57
59
|
*/
|
|
@@ -59,7 +61,7 @@ export type MaybeArray<T>=T|T[]
|
|
|
59
61
|
/**
|
|
60
62
|
* 消息发送内容类型
|
|
61
63
|
*/
|
|
62
|
-
export type SendContent=MaybeArray<string|
|
|
64
|
+
export type SendContent=MaybeArray<string|MessageElement>
|
|
63
65
|
/**
|
|
64
66
|
* 消息发送者信息
|
|
65
67
|
*/
|
package/src/utils.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {Dict, MessageSegment, SendContent} from "./types";
|
|
1
|
+
import {Dict, MessageElement, MessageSegment, SendContent} from "./types";
|
|
2
2
|
|
|
3
3
|
export function getValueWithRuntime(template: string, ctx: Dict) {
|
|
4
4
|
const result = evaluate(template, ctx);
|
|
@@ -79,10 +79,10 @@ export namespace segment{
|
|
|
79
79
|
}
|
|
80
80
|
export function from(content: SendContent): SendContent {
|
|
81
81
|
if (!Array.isArray(content)) content=[content];
|
|
82
|
-
const toString=(template:string|
|
|
82
|
+
const toString=(template:string|MessageElement)=>{
|
|
83
83
|
if(typeof template!=='string') return [template]
|
|
84
84
|
template=unescape(template);
|
|
85
|
-
const result:
|
|
85
|
+
const result: MessageElement[] = [];
|
|
86
86
|
const closingReg = /<(\S+)(\s[^>]+)?\/>/;
|
|
87
87
|
const twinningReg = /<(\S+)(\s[^>]+)?>([\s\S]*?)<\/\1>/;
|
|
88
88
|
while (template.length) {
|
|
@@ -132,7 +132,7 @@ export namespace segment{
|
|
|
132
132
|
return content.reduce((result,item)=>{
|
|
133
133
|
result.push(...toString(item))
|
|
134
134
|
return result;
|
|
135
|
-
},[] as
|
|
135
|
+
},[] as MessageElement[])
|
|
136
136
|
}
|
|
137
137
|
export function raw(content:SendContent){
|
|
138
138
|
if(!Array.isArray(content)) content=[content]
|
|
@@ -147,7 +147,8 @@ export namespace segment{
|
|
|
147
147
|
if(!Array.isArray(content)) content=[content]
|
|
148
148
|
return content.map(item=>{
|
|
149
149
|
if(typeof item==='string') return item
|
|
150
|
-
|
|
150
|
+
let {type,data}=item
|
|
151
|
+
if(typeof type==='function') type=type.name
|
|
151
152
|
if(type==='text') return data.text
|
|
152
153
|
return `<${type} ${Object.keys(data).map(key=>`${key}='${escape(JSON.stringify(data[key]))}'`).join(' ')}/>`
|
|
153
154
|
}).join('')
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
defineComponent,
|
|
4
|
+
createComponentContext,
|
|
5
|
+
renderComponents,
|
|
6
|
+
getProps,
|
|
7
|
+
Component,
|
|
8
|
+
ComponentContext,
|
|
9
|
+
Fragment,
|
|
10
|
+
Fetch
|
|
11
|
+
} from '../src/component'
|
|
12
|
+
import { Message } from '../src/message'
|
|
13
|
+
import { SendOptions } from '../src/types'
|
|
14
|
+
|
|
15
|
+
// Mock utils functions
|
|
16
|
+
vi.mock('../src/utils', () => ({
|
|
17
|
+
getValueWithRuntime: vi.fn((expression, context) => {
|
|
18
|
+
// Simple mock implementation for testing
|
|
19
|
+
if (typeof expression === 'string' && context) {
|
|
20
|
+
// Handle simple variable access
|
|
21
|
+
if (expression in context) {
|
|
22
|
+
return context[expression]
|
|
23
|
+
}
|
|
24
|
+
// Handle object property access like 'user.name'
|
|
25
|
+
if (expression.includes('.')) {
|
|
26
|
+
const [obj, prop] = expression.split('.')
|
|
27
|
+
return context[obj]?.[prop]
|
|
28
|
+
}
|
|
29
|
+
// Handle simple expressions
|
|
30
|
+
try {
|
|
31
|
+
return eval(expression)
|
|
32
|
+
} catch {
|
|
33
|
+
return expression
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return expression
|
|
37
|
+
}),
|
|
38
|
+
compiler: vi.fn((template, context) => template),
|
|
39
|
+
segment: {
|
|
40
|
+
toString: vi.fn((content) => typeof content === 'string' ? content : JSON.stringify(content)),
|
|
41
|
+
from: vi.fn((content) => content),
|
|
42
|
+
escape: vi.fn((content) => content.replace(/</g, '<').replace(/>/g, '>'))
|
|
43
|
+
}
|
|
44
|
+
}))
|
|
45
|
+
|
|
46
|
+
describe('函数式组件系统测试', () => {
|
|
47
|
+
let mockContext: ComponentContext
|
|
48
|
+
let mockMessage: Message
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
mockMessage = {
|
|
52
|
+
$id: '1',
|
|
53
|
+
$adapter: 'test',
|
|
54
|
+
$bot: 'test-bot',
|
|
55
|
+
$content: [],
|
|
56
|
+
$sender: { id: 'user1', name: 'User' },
|
|
57
|
+
$reply: vi.fn(),
|
|
58
|
+
$channel: { id: 'channel1', type: 'private' },
|
|
59
|
+
$timestamp: Date.now(),
|
|
60
|
+
$raw: 'test'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
mockContext = createComponentContext(
|
|
64
|
+
{ user: { name: 'John', age: 25 } },
|
|
65
|
+
undefined,
|
|
66
|
+
'test template'
|
|
67
|
+
)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('defineComponent 函数测试', () => {
|
|
71
|
+
it('应该正确创建函数式组件', () => {
|
|
72
|
+
const TestComponent = defineComponent(async function TestComponent(props: { name: string }, context: ComponentContext) {
|
|
73
|
+
return `Hello ${props.name}`
|
|
74
|
+
}, 'test-component')
|
|
75
|
+
|
|
76
|
+
expect(TestComponent).toBeInstanceOf(Function)
|
|
77
|
+
expect(TestComponent.name).toBe('test-component')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('应该支持异步组件', async () => {
|
|
81
|
+
const AsyncComponent = defineComponent(async function AsyncComponent(props: { delay: number }, context: ComponentContext) {
|
|
82
|
+
await new Promise(resolve => setTimeout(resolve, props.delay))
|
|
83
|
+
return `Delayed: ${props.delay}ms`
|
|
84
|
+
}, 'async-component')
|
|
85
|
+
|
|
86
|
+
const result = await AsyncComponent({ delay: 10 }, mockContext)
|
|
87
|
+
expect(result).toBe('Delayed: 10ms')
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('getProps 函数测试', () => {
|
|
92
|
+
it('应该正确解析简单属性', () => {
|
|
93
|
+
const TestComponent = defineComponent(async function TestComponent(props: any, context: ComponentContext) {
|
|
94
|
+
return 'test'
|
|
95
|
+
}, 'test')
|
|
96
|
+
|
|
97
|
+
const template = '<test name="John" age={25} active={true} />'
|
|
98
|
+
const props = getProps(TestComponent, template, mockContext)
|
|
99
|
+
|
|
100
|
+
expect(props.name).toBe('John')
|
|
101
|
+
expect(props.age).toBe(25)
|
|
102
|
+
expect(props.active).toBe(true)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('应该正确解析表达式属性', () => {
|
|
106
|
+
const TestComponent = defineComponent(async function TestComponent(props: any, context: ComponentContext) {
|
|
107
|
+
return 'test'
|
|
108
|
+
}, 'test')
|
|
109
|
+
|
|
110
|
+
const template = '<test sum={1+1} />'
|
|
111
|
+
const props = getProps(TestComponent, template, mockContext)
|
|
112
|
+
|
|
113
|
+
expect(props.sum).toBe(2)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('应该正确处理 kebab-case 到 camelCase 转换', () => {
|
|
117
|
+
const TestComponent = defineComponent(async function TestComponent(props: any, context: ComponentContext) {
|
|
118
|
+
return 'test'
|
|
119
|
+
}, 'test')
|
|
120
|
+
|
|
121
|
+
const template = '<test user-name="John" user-age={25} is-active={true} />'
|
|
122
|
+
const props = getProps(TestComponent, template, mockContext)
|
|
123
|
+
|
|
124
|
+
expect(props.userName).toBe('John')
|
|
125
|
+
expect(props.userAge).toBe(25)
|
|
126
|
+
expect(props.isActive).toBe(true)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('应该正确处理 children 属性', () => {
|
|
130
|
+
const TestComponent = defineComponent(async function TestComponent(props: any, context: ComponentContext) {
|
|
131
|
+
return 'test'
|
|
132
|
+
}, 'test')
|
|
133
|
+
|
|
134
|
+
const template = '<test>Hello World</test>'
|
|
135
|
+
const props = getProps(TestComponent, template, mockContext)
|
|
136
|
+
|
|
137
|
+
expect(props.children).toBe('Hello World')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('应该正确处理自闭合标签', () => {
|
|
141
|
+
const TestComponent = defineComponent(async function TestComponent(props: any, context: ComponentContext) {
|
|
142
|
+
return 'test'
|
|
143
|
+
}, 'test')
|
|
144
|
+
|
|
145
|
+
const template = '<test name="John" />'
|
|
146
|
+
const props = getProps(TestComponent, template, mockContext)
|
|
147
|
+
|
|
148
|
+
expect(props.name).toBe('John')
|
|
149
|
+
expect(props.children).toBeUndefined()
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe('createComponentContext 函数测试', () => {
|
|
154
|
+
it('应该正确创建组件上下文', () => {
|
|
155
|
+
const context = createComponentContext(
|
|
156
|
+
{ user: { name: 'John' } },
|
|
157
|
+
undefined,
|
|
158
|
+
'test template'
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
expect(context.props).toEqual({ user: { name: 'John' } })
|
|
162
|
+
expect(context.root).toBe('test template')
|
|
163
|
+
expect(context.parent).toBeUndefined()
|
|
164
|
+
expect(context.children).toBeUndefined()
|
|
165
|
+
expect(typeof context.render).toBe('function')
|
|
166
|
+
expect(typeof context.getValue).toBe('function')
|
|
167
|
+
expect(typeof context.compile).toBe('function')
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('应该正确处理父上下文', () => {
|
|
171
|
+
const parentContext = createComponentContext({ parent: 'data' })
|
|
172
|
+
const childContext = createComponentContext(
|
|
173
|
+
{ child: 'data' },
|
|
174
|
+
parentContext,
|
|
175
|
+
'child template'
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
expect(childContext.parent).toBe(parentContext)
|
|
179
|
+
expect(childContext.props).toEqual({ child: 'data' })
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
describe('内置组件测试', () => {
|
|
184
|
+
it('Fragment 组件应该正确渲染 children', async () => {
|
|
185
|
+
const result = await Fragment({ children: 'Hello World' }, mockContext)
|
|
186
|
+
expect(result).toBe('Hello World')
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('Fetch 组件应该正确获取远程内容', async () => {
|
|
190
|
+
// Mock fetch
|
|
191
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
192
|
+
text: () => Promise.resolve('Remote content')
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
const result = await Fetch({ url: 'https://example.com' }, mockContext)
|
|
196
|
+
expect(result).toBe('Remote content')
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
describe('renderComponents 函数测试', () => {
|
|
201
|
+
it('应该正确渲染单个组件', async () => {
|
|
202
|
+
const TestComponent = defineComponent(async function TestComponent(props: { name: string }, context: ComponentContext) {
|
|
203
|
+
return `Hello ${props.name}`
|
|
204
|
+
}, 'test')
|
|
205
|
+
|
|
206
|
+
const componentMap = new Map([['test', TestComponent]])
|
|
207
|
+
const options: SendOptions = {
|
|
208
|
+
content: '<test name="John" />',
|
|
209
|
+
type: 'text',
|
|
210
|
+
context: 'test',
|
|
211
|
+
bot: 'test'
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const result = await renderComponents(componentMap, options)
|
|
215
|
+
expect(result.content).toContain('Hello John')
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('应该正确渲染多个组件', async () => {
|
|
219
|
+
const Component1 = defineComponent(async function Component1(props: { text: string }, context: ComponentContext) {
|
|
220
|
+
return `[${props.text}]`
|
|
221
|
+
}, 'comp1')
|
|
222
|
+
|
|
223
|
+
const Component2 = defineComponent(async function Component2(props: { number: number }, context: ComponentContext) {
|
|
224
|
+
return `{${props.number}}`
|
|
225
|
+
}, 'comp2')
|
|
226
|
+
|
|
227
|
+
const componentMap = new Map([
|
|
228
|
+
['comp1', Component1],
|
|
229
|
+
['comp2', Component2]
|
|
230
|
+
])
|
|
231
|
+
|
|
232
|
+
const options: SendOptions = {
|
|
233
|
+
content: '<comp1 text="Hello" /> <comp2 number={42} />',
|
|
234
|
+
type: 'text',
|
|
235
|
+
context: 'test',
|
|
236
|
+
bot: 'test'
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const result = await renderComponents(componentMap, options)
|
|
240
|
+
expect(result.content).toContain('[Hello]')
|
|
241
|
+
expect(result.content).toContain('{42}')
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('应该正确处理嵌套组件', async () => {
|
|
245
|
+
const OuterComponent = defineComponent(async function OuterComponent(props: { title: string }, context: ComponentContext) {
|
|
246
|
+
return `标题: ${props.title}`
|
|
247
|
+
}, 'outer')
|
|
248
|
+
|
|
249
|
+
const InnerComponent = defineComponent(async function InnerComponent(props: { content: string }, context: ComponentContext) {
|
|
250
|
+
return `Content: ${props.content}`
|
|
251
|
+
}, 'inner')
|
|
252
|
+
|
|
253
|
+
const componentMap = new Map([
|
|
254
|
+
['outer', OuterComponent],
|
|
255
|
+
['inner', InnerComponent]
|
|
256
|
+
])
|
|
257
|
+
|
|
258
|
+
const options: SendOptions = {
|
|
259
|
+
content: '<outer title="Test"><inner content="Nested" /></outer>',
|
|
260
|
+
type: 'text',
|
|
261
|
+
context: 'test',
|
|
262
|
+
bot: 'test'
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const result = await renderComponents(componentMap, options)
|
|
266
|
+
// 现在嵌套组件渲染应该工作了
|
|
267
|
+
expect(result.content).toContain('标题: Test')
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
describe('表达式求值测试', () => {
|
|
272
|
+
it('应该正确计算数学表达式', () => {
|
|
273
|
+
const TestComponent = defineComponent(async function TestComponent(props: any, context: ComponentContext) {
|
|
274
|
+
return 'test'
|
|
275
|
+
}, 'test')
|
|
276
|
+
|
|
277
|
+
const template = '<test sum={1+2+3} product={2*3*4} />'
|
|
278
|
+
const props = getProps(TestComponent, template, mockContext)
|
|
279
|
+
|
|
280
|
+
expect(props.sum).toBe(6)
|
|
281
|
+
expect(props.product).toBe(24)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('应该正确处理比较表达式', () => {
|
|
285
|
+
const TestComponent = defineComponent(async function TestComponent(props: any, context: ComponentContext) {
|
|
286
|
+
return 'test'
|
|
287
|
+
}, 'test')
|
|
288
|
+
|
|
289
|
+
const template = '<test greater={5>3} equal={2==2} />'
|
|
290
|
+
const props = getProps(TestComponent, template, mockContext)
|
|
291
|
+
|
|
292
|
+
expect(props.greater).toBe(true)
|
|
293
|
+
expect(props.equal).toBe(true)
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('应该正确处理三元运算符', () => {
|
|
297
|
+
const TestComponent = defineComponent(async function TestComponent(props: any, context: ComponentContext) {
|
|
298
|
+
return 'test'
|
|
299
|
+
}, 'test')
|
|
300
|
+
|
|
301
|
+
const template = '<test result={5>3 ? "yes" : "no"} />'
|
|
302
|
+
const props = getProps(TestComponent, template, mockContext)
|
|
303
|
+
|
|
304
|
+
expect(props.result).toBe('yes')
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('应该正确处理数组和对象表达式', () => {
|
|
308
|
+
const TestComponent = defineComponent(async function TestComponent(props: any, context: ComponentContext) {
|
|
309
|
+
return 'test'
|
|
310
|
+
}, 'test')
|
|
311
|
+
|
|
312
|
+
const template = '<test items={[1,2,3]} config={{name:"test",value:42}} />'
|
|
313
|
+
const props = getProps(TestComponent, template, mockContext)
|
|
314
|
+
|
|
315
|
+
expect(props.items).toEqual([1, 2, 3])
|
|
316
|
+
expect(props.config).toEqual({ name: 'test', value: 42 })
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
describe('错误处理测试', () => {
|
|
321
|
+
it('应该正确处理无效的组件模板', () => {
|
|
322
|
+
const TestComponent = defineComponent(async function TestComponent(props: any, context: ComponentContext) {
|
|
323
|
+
return 'test'
|
|
324
|
+
}, 'test')
|
|
325
|
+
|
|
326
|
+
const template = 'invalid template'
|
|
327
|
+
const props = getProps(TestComponent, template, mockContext)
|
|
328
|
+
|
|
329
|
+
expect(props).toEqual({})
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('应该正确处理组件渲染错误', async () => {
|
|
333
|
+
const ErrorComponent = defineComponent(async function ErrorComponent(props: any, context: ComponentContext) {
|
|
334
|
+
throw new Error('Test error')
|
|
335
|
+
}, 'error')
|
|
336
|
+
|
|
337
|
+
const componentMap = new Map([['error', ErrorComponent]])
|
|
338
|
+
const options: SendOptions = {
|
|
339
|
+
content: '<error />',
|
|
340
|
+
type: 'text',
|
|
341
|
+
context: 'test',
|
|
342
|
+
bot: 'test'
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
await expect(renderComponents(componentMap, options)).rejects.toThrow('Test error')
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
})
|