mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-05-15 13:04:29 +00:00
401 lines
12 KiB
TypeScript
401 lines
12 KiB
TypeScript
import { describe, expect, test, vi } from 'vitest';
|
|
|
|
import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX, dataSourceTemplateRegExp, NodeType } from '@tmagic/core';
|
|
|
|
import {
|
|
compiledCondition,
|
|
compiledNodeField,
|
|
compliedConditions,
|
|
compliedDataSourceField,
|
|
compliedIteratorItem,
|
|
createIteratorContentData,
|
|
registerDataSourceOnDemand,
|
|
template,
|
|
updateNode,
|
|
} from '@data-source/utils';
|
|
|
|
describe('compiledCondition', () => {
|
|
test('=,true', () => {
|
|
const result = compiledCondition(
|
|
[
|
|
{
|
|
field: ['a', 'b'],
|
|
op: '=',
|
|
value: 1,
|
|
},
|
|
],
|
|
{ a: { b: 1 } },
|
|
);
|
|
|
|
expect(result).toBeTruthy();
|
|
});
|
|
|
|
test('=,false', () => {
|
|
const result = compiledCondition(
|
|
[
|
|
{
|
|
field: ['a', 'b'],
|
|
op: '=',
|
|
value: 2,
|
|
},
|
|
],
|
|
{ a: { b: 1 } },
|
|
);
|
|
|
|
expect(result).toBeFalsy();
|
|
});
|
|
});
|
|
|
|
describe('template', () => {
|
|
test('template', () => {
|
|
const value = template('xxx${aa.bb}123${aa1.bb1}dsf', { aa: { bb: 1 }, aa1: { bb1: 2 } });
|
|
expect(value).toBe('xxx11232dsf');
|
|
});
|
|
|
|
// 测试1: 普通字符串,没有模板变量
|
|
test('should return original string when no template variables', () => {
|
|
const value = 'This is a plain text';
|
|
const result = template(value);
|
|
expect(result).toBe(value);
|
|
});
|
|
|
|
// 测试2: 包含模板变量但未提供数据源
|
|
test('should keep template variables when no data provided', () => {
|
|
const value = 'Hello ${user.name}';
|
|
const result = template(value);
|
|
expect(result).toBe(value);
|
|
});
|
|
|
|
// 测试3: 包含模板变量且数据源中有对应值
|
|
test('should replace template variables with data source values', () => {
|
|
const value = 'Hello ${user.name}, your age is ${user.age}';
|
|
const data = {
|
|
user: {
|
|
name: 'John',
|
|
age: 30,
|
|
},
|
|
};
|
|
const result = template(value, data);
|
|
expect(result).toBe('Hello John, your age is 30');
|
|
});
|
|
|
|
// 测试4: 模板变量在数据源中不存在
|
|
test('should keep template variables when path not found in data', () => {
|
|
const value = 'Hello ${user.name}, your job is ${user.job}';
|
|
const data = {
|
|
user: {
|
|
name: 'John',
|
|
},
|
|
};
|
|
const result = template(value, data);
|
|
expect(result).toBe('Hello John, your job is undefined');
|
|
});
|
|
|
|
// 测试5: 多层嵌套的数据源路径
|
|
test('should handle deeply nested data paths', () => {
|
|
const value = 'User info: ${user.personal.details.address.city}';
|
|
const data = {
|
|
user: {
|
|
personal: {
|
|
details: {
|
|
address: {
|
|
city: 'Beijing',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
const result = template(value, data);
|
|
expect(result).toBe('User info: Beijing');
|
|
});
|
|
|
|
// 测试6: 多个模板变量混合的情况
|
|
test('should handle multiple template variables mixed with text', () => {
|
|
const value = 'User: ${user.name}, Age: ${user.age}, Score: ${user.score}';
|
|
const data = {
|
|
user: {
|
|
name: 'Alice',
|
|
age: 25,
|
|
score: 95,
|
|
},
|
|
};
|
|
const result = template(value, data);
|
|
expect(result).toBe('User: Alice, Age: 25, Score: 95');
|
|
});
|
|
|
|
// 测试7: 数据源为 undefined 或 null
|
|
test('should handle undefined or null data source', () => {
|
|
const value = 'Test ${some.value}';
|
|
|
|
// 测试 undefined
|
|
const result1 = template(value, undefined);
|
|
expect(result1).toBe(value);
|
|
|
|
// 测试 null
|
|
const result2 = template(value, null as any);
|
|
expect(result2).toBe(value);
|
|
});
|
|
|
|
// 测试8: 空字符串输入
|
|
test('should handle empty string input', () => {
|
|
const value = '';
|
|
const result = template(value);
|
|
expect(result).toBe('');
|
|
});
|
|
|
|
// 测试9: 正则表达式匹配测试
|
|
test('should correctly match template patterns', () => {
|
|
// 测试正则表达式是否能正确匹配模板变量
|
|
const testString = '${a.b.c} ${x.y.z}';
|
|
const matches = testString.match(dataSourceTemplateRegExp);
|
|
expect(matches).not.toBeNull();
|
|
expect(matches?.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
// 测试10: 复杂混合情况
|
|
test('should handle complex mixed cases', () => {
|
|
const value = 'User: ${user.name}, ${user.age} years old. ${not.found} ${user.address.city}';
|
|
const data = {
|
|
user: {
|
|
name: 'Bob',
|
|
age: 40,
|
|
address: {
|
|
city: 'Shanghai',
|
|
},
|
|
},
|
|
};
|
|
const result = template(value, data);
|
|
expect(result).toBe('User: Bob, 40 years old. ${not.found} Shanghai');
|
|
});
|
|
});
|
|
|
|
describe('createIteratorContentData', () => {
|
|
test('createIteratorContentData', () => {
|
|
const ctxData: any = createIteratorContentData({ b: 1 }, 'ds', ['a'], { ds: { a: [{ b: 1 }] } });
|
|
expect(ctxData.ds.a.b).toBe(1);
|
|
});
|
|
test('混用', () => {
|
|
const ctxData: any = createIteratorContentData({ b: 1 }, 'ds', ['a'], { ds: { a: [{ b: 1 }], b: 2 } });
|
|
expect(ctxData.ds.b).toBe(2);
|
|
});
|
|
|
|
test('二维数组', () => {
|
|
const ctxData: any = createIteratorContentData({ a: 1 }, 'ds', ['a', 'c'], {
|
|
ds: {
|
|
a: [
|
|
{
|
|
b: 0,
|
|
c: [{ a: 1 }],
|
|
},
|
|
],
|
|
b: 2,
|
|
},
|
|
});
|
|
expect(ctxData.ds.a.c.a).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('compliedConditions', () => {
|
|
test('未配置 conditions 时直接返回 true', () => {
|
|
expect(compliedConditions({}, {})).toBe(true);
|
|
expect(compliedConditions({ ['displayConds' as any]: [] } as any, {})).toBe(true);
|
|
});
|
|
|
|
test('任一 cond 通过即返回 true', () => {
|
|
const node: any = {
|
|
displayConds: [
|
|
{ cond: [{ field: ['ds_1', 'a'], op: '=', value: 2 }] },
|
|
{ cond: [{ field: ['ds_1', 'a'], op: '=', value: 1 }] },
|
|
],
|
|
};
|
|
expect(compliedConditions(node, { ds_1: { a: 1 } })).toBe(true);
|
|
});
|
|
|
|
test('全部不通过则返回 false', () => {
|
|
const node: any = {
|
|
displayConds: [{ cond: [{ field: ['ds_1', 'a'], op: '=', value: 2 }] }],
|
|
};
|
|
expect(compliedConditions(node, { ds_1: { a: 1 } })).toBe(false);
|
|
});
|
|
|
|
test('cond 为空被跳过', () => {
|
|
const node: any = { displayConds: [{ cond: undefined }] };
|
|
expect(compliedConditions(node, {})).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('compiledCondition 边界', () => {
|
|
test('数据源不存在时直接 break 视为通过', () => {
|
|
expect(compiledCondition([{ field: ['unknown', 'a'], op: '=', value: 1 }], {})).toBe(true);
|
|
});
|
|
|
|
test('field 取值异常(如类型错)时 console.warn 不阻断', () => {
|
|
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
const result = compiledCondition([{ field: ['ds', 'a', 'b', 'c'], op: '=', value: 1 }], { ds: { a: 'string' } });
|
|
expect(result).toBe(true);
|
|
warn.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('updateNode', () => {
|
|
test('页面节点直接替换 dsl.items', () => {
|
|
const dsl: any = {
|
|
type: NodeType.ROOT,
|
|
id: 'app',
|
|
items: [{ id: 'p1', type: NodeType.PAGE, items: [{ id: 'btn' }] }],
|
|
};
|
|
updateNode({ id: 'p1', type: NodeType.PAGE, items: [{ id: 'btn2' }] } as any, dsl);
|
|
expect(dsl.items[0].items[0].id).toBe('btn2');
|
|
});
|
|
|
|
test('非页面节点走 replaceChildNode', () => {
|
|
const dsl: any = {
|
|
type: NodeType.ROOT,
|
|
id: 'app',
|
|
items: [
|
|
{
|
|
id: 'p1',
|
|
type: NodeType.PAGE,
|
|
items: [{ id: 'btn', type: 'button', text: 'old' }],
|
|
},
|
|
],
|
|
};
|
|
updateNode({ id: 'btn', type: 'button', text: 'new' } as any, dsl);
|
|
expect(dsl.items[0].items[0].text).toBe('new');
|
|
});
|
|
});
|
|
|
|
describe('compliedDataSourceField', () => {
|
|
test('不带前缀直接返回原值', () => {
|
|
expect(compliedDataSourceField(['no-prefix-id', 'name'], { id: { name: 'x' } })).toEqual(['no-prefix-id', 'name']);
|
|
});
|
|
|
|
test('数据源不存在时返回原值', () => {
|
|
expect(compliedDataSourceField([`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}id`, 'name'], {})).toEqual([
|
|
`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}id`,
|
|
'name',
|
|
]);
|
|
});
|
|
|
|
test('正常解析数据源字段', () => {
|
|
const value = compliedDataSourceField([`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}id`, 'name'], {
|
|
id: { name: 'x' },
|
|
});
|
|
expect(value).toBe('x');
|
|
});
|
|
|
|
test('字段路径不存在时返回原值', () => {
|
|
expect(
|
|
compliedDataSourceField([`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}id`, 'name', 'sub'], { id: { name: 'x' } }),
|
|
).toEqual([`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}id`, 'name', 'sub']);
|
|
});
|
|
});
|
|
|
|
describe('compiledNodeField', () => {
|
|
const data = { id: { name: 'world' } };
|
|
|
|
test('字符串模板', () => {
|
|
expect(compiledNodeField('hello ${id.name}', data)).toBe('hello world');
|
|
});
|
|
|
|
test('isBindDataSource 直接取整个数据源', () => {
|
|
expect(compiledNodeField({ isBindDataSource: true, dataSourceId: 'id' }, data)).toEqual({ name: 'world' });
|
|
});
|
|
|
|
test('isBindDataSourceField 走模板', () => {
|
|
expect(compiledNodeField({ isBindDataSourceField: true, dataSourceId: 'id', template: 'hi ${name}' }, data)).toBe(
|
|
'hi world',
|
|
);
|
|
});
|
|
|
|
test('数组形式走 compliedDataSourceField', () => {
|
|
expect(compiledNodeField([`${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}id`, 'name'], data)).toBe('world');
|
|
});
|
|
|
|
test('未匹配格式直接返回原值', () => {
|
|
expect(compiledNodeField(123 as any, data)).toBe(123);
|
|
});
|
|
});
|
|
|
|
describe('compliedIteratorItem', () => {
|
|
test('递归 compile items 并应用条件', () => {
|
|
const item: any = {
|
|
id: 'parent',
|
|
items: [{ id: 'child', text: 'origin' }],
|
|
};
|
|
const ctxData = { ds: { name: 'V' } };
|
|
const result = compliedIteratorItem({
|
|
compile: (v: any) => `compiled-${v}`,
|
|
dsId: 'ds',
|
|
item,
|
|
deps: { child: { name: 'c', keys: ['text'] } },
|
|
condDeps: {},
|
|
inEditor: false,
|
|
ctxData,
|
|
});
|
|
expect(result.items[0].text).toBe('compiled-origin');
|
|
expect(result.id).toBe('parent');
|
|
});
|
|
|
|
test('items 不是数组时保留原值', () => {
|
|
const result = compliedIteratorItem({
|
|
compile: (v: any) => v,
|
|
dsId: 'ds',
|
|
item: { id: 'p', items: 'not-array' as any } as any,
|
|
deps: {},
|
|
condDeps: {},
|
|
inEditor: true,
|
|
ctxData: {},
|
|
});
|
|
expect(result.items).toBe('not-array');
|
|
});
|
|
|
|
test('条件依赖在非编辑器中会写入 condResult', () => {
|
|
const result = compliedIteratorItem({
|
|
compile: (v: any) => v,
|
|
dsId: 'ds',
|
|
item: {
|
|
id: 'p',
|
|
displayConds: [{ cond: [{ field: ['ds', 'a'], op: '=', value: 1 }] }],
|
|
} as any,
|
|
deps: {},
|
|
condDeps: { p: { name: 'p', keys: ['displayConds'] } },
|
|
inEditor: false,
|
|
ctxData: { ds: { a: 1 } },
|
|
});
|
|
expect(result.condResult).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('registerDataSourceOnDemand', () => {
|
|
test('按依赖按需返回模块', async () => {
|
|
const dsl: any = {
|
|
dataSources: [
|
|
{ id: 'a', type: 'http' },
|
|
{ id: 'b', type: 'mock' },
|
|
{ id: 'c', type: 'http' },
|
|
],
|
|
dataSourceDeps: { a: { node1: { name: 'n', keys: ['x'] } } },
|
|
dataSourceCondDeps: { c: { node2: { name: 'n', keys: ['y'] } } },
|
|
dataSourceMethodsDeps: {},
|
|
};
|
|
const httpModule = { default: class HttpDS {} };
|
|
const mockModule = { default: class MockDS {} };
|
|
const modules = await registerDataSourceOnDemand(dsl, {
|
|
http: () => Promise.resolve(httpModule as any),
|
|
mock: () => Promise.resolve(mockModule as any),
|
|
});
|
|
expect(modules.http).toBe(httpModule.default);
|
|
expect(modules.mock).toBeUndefined();
|
|
});
|
|
|
|
test('找不到对应模块时跳过', async () => {
|
|
const dsl: any = {
|
|
dataSources: [{ id: 'a', type: 'unknown' }],
|
|
dataSourceDeps: { a: { node: { name: 'n', keys: ['x'] } } },
|
|
};
|
|
const modules = await registerDataSourceOnDemand(dsl, {});
|
|
expect(Object.keys(modules)).toHaveLength(0);
|
|
});
|
|
});
|