mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-10-28 13:30:46 +00:00
- 新增 packages/editor/src/sdk,通过 sdk.ts 对外导出 - 将 services 下的核心服务迁移至 sdk/services - 工具函数拆分至 sdk/utils,校验逻辑迁移至 sdk/validate,新增 sdk/wiring - services/sdk-instance.ts 负责在编辑器中实例化 SDK - 补充 SDK 相关单元测试,新增 docs/api/editor/sdk.md 文档
306 lines
12 KiB
TypeScript
306 lines
12 KiB
TypeScript
// @vitest-environment node
|
||
import { describe, expect, test } from 'vitest';
|
||
|
||
import { DslValidationError } from '@editor/sdk';
|
||
|
||
import { createDsl, createSdk } from './helpers';
|
||
|
||
const textConfig = [{ name: 'text', text: '文本', rules: [{ required: true, message: '必填' }] }];
|
||
|
||
describe('validate: 结构', () => {
|
||
test('组件类型不在组件列表中', async () => {
|
||
const sdk = createSdk();
|
||
const error = await sdk.editor.add({ type: 'not-exist' } as any).catch((e) => e);
|
||
|
||
expect(error).toBeInstanceOf(DslValidationError);
|
||
expect(error.errors[0]).toMatchObject({ kind: 'structure', path: 'type' });
|
||
expect(error.errors[0].suggestion).toContain('text');
|
||
expect(error.message).toContain('not-exist');
|
||
});
|
||
|
||
test('内置类型不需要出现在组件列表里;组件列表为空时不校验类型', async () => {
|
||
const sdk = createSdk({ componentGroupList: [] });
|
||
await expect(sdk.editor.add({ type: 'anything' } as any)).resolves.toBeTruthy();
|
||
|
||
const sdk2 = createSdk();
|
||
await expect(sdk2.editor.add({ type: 'container' } as any)).resolves.toBeTruthy();
|
||
});
|
||
|
||
test('类型比较忽略大小写写法差异(驼峰与下划线)', async () => {
|
||
const sdk = createSdk({
|
||
componentGroupList: [{ title: 'g', items: [{ text: 'a', type: 'page-fragment-container' }] }],
|
||
});
|
||
await expect(sdk.editor.add({ type: 'pageFragmentContainer' } as any)).resolves.toBeTruthy();
|
||
});
|
||
|
||
test('缺少 type', async () => {
|
||
const sdk = createSdk();
|
||
await expect(sdk.editor.add({} as any)).rejects.toThrow('组件类型不能为空');
|
||
});
|
||
|
||
test('父节点不是容器', async () => {
|
||
const sdk = createSdk();
|
||
const error = await sdk.editor
|
||
.add({ type: 'text' } as any, sdk.editor.getNodeById('text_1') as any)
|
||
.catch((e) => e);
|
||
|
||
expect(error).toBeInstanceOf(DslValidationError);
|
||
expect(error.errors[0].message).toContain('不是容器');
|
||
});
|
||
|
||
test('app 下只能添加页面', async () => {
|
||
const sdk = createSdk();
|
||
const error = await sdk.editor.add({ type: 'text' } as any, sdk.editor.get('root') as any).catch((e) => e);
|
||
|
||
expect(error).toBeInstanceOf(DslValidationError);
|
||
expect(error.errors[0].message).toContain('app 下只能添加页面');
|
||
});
|
||
|
||
test('可以添加页面(页面总是添加到 app 下)', async () => {
|
||
const sdk = createSdk();
|
||
await expect(sdk.editor.add({ type: 'page', name: 'p2' } as any)).resolves.toBeTruthy();
|
||
expect(sdk.getDsl()!.items).toHaveLength(2);
|
||
});
|
||
|
||
test('checkAddStructure:页面 / 页面片只能放在 app 下,没有父节点', () => {
|
||
const sdk = createSdk();
|
||
const container = sdk.editor.getNodeById('container_1') as any;
|
||
|
||
expect(sdk.validate.checkAddStructure({ id: 'p', type: 'page' } as any, container)[0].message).toContain(
|
||
'只能添加到 app',
|
||
);
|
||
expect(sdk.validate.checkAddStructure({ id: 't', type: 'text' } as any, null)[0].message).toContain('未找到父节点');
|
||
});
|
||
|
||
test('id 已存在 / 批量里 id 重复', async () => {
|
||
const sdk = createSdk();
|
||
|
||
const exists = await sdk.editor.add([{ id: 'text_1', type: 'text' }] as any).catch((e) => e);
|
||
expect(exists.errors[0].message).toContain('已存在');
|
||
|
||
const dup = await sdk.editor
|
||
.add([
|
||
{ id: 'new_1', type: 'text' },
|
||
{ id: 'new_1', type: 'text' },
|
||
] as any)
|
||
.catch((e) => e);
|
||
expect(dup.errors[0].message).toContain('重复');
|
||
});
|
||
|
||
test('disabledPageFragment', async () => {
|
||
const sdk = createSdk({ disabledPageFragment: true });
|
||
const error = await sdk.editor.add({ type: 'page-fragment' } as any).catch((e) => e);
|
||
|
||
expect(error).toBeInstanceOf(DslValidationError);
|
||
expect(error.errors[0].message).toContain('disabledPageFragment');
|
||
});
|
||
|
||
test('canDropIn 返回 false 时阻止添加与移动', async () => {
|
||
const calls: any[] = [];
|
||
const sdk = createSdk({
|
||
canDropIn: (...args) => {
|
||
calls.push(args);
|
||
return false;
|
||
},
|
||
});
|
||
|
||
const container = sdk.editor.getNodeById('container_1') as any;
|
||
const add = await sdk.editor.add({ type: 'text' } as any, container).catch((e) => e);
|
||
expect(add).toBeInstanceOf(DslValidationError);
|
||
expect(calls[0]).toEqual([[], 'container_1', 'stage-add']);
|
||
|
||
const move = await sdk.editor.moveToContainer({ id: 'text_1' } as any, 'container_1').catch((e) => e);
|
||
expect(move).toBeInstanceOf(DslValidationError);
|
||
expect(calls.at(-1)).toEqual([['text_1'], 'container_1', 'stage-drag']);
|
||
|
||
const drag = await sdk.editor.dragTo({ id: 'text_1' } as any, container, 0).catch((e) => e);
|
||
expect(drag).toBeInstanceOf(DslValidationError);
|
||
expect(calls.at(-1)[2]).toBe('layer');
|
||
});
|
||
|
||
test('canDropIn 重定向到不存在的节点 / 非容器时报错,重定向到容器时放行', async () => {
|
||
const sdk = createSdk({ canDropIn: () => 'nothing' });
|
||
const container = sdk.editor.getNodeById('container_1') as any;
|
||
const bad = await sdk.editor.add({ type: 'text' } as any, container).catch((e) => e);
|
||
expect(bad.errors[0].message).toContain('重定向');
|
||
|
||
const ok = createSdk({ canDropIn: () => 'container_1' });
|
||
await expect(
|
||
ok.editor.add({ type: 'text' } as any, ok.editor.getNodeById('container_1') as any),
|
||
).resolves.toBeTruthy();
|
||
});
|
||
|
||
test('移动:目标不是容器 / 移到自己里面 / 源节点不存在', async () => {
|
||
const sdk = createSdk();
|
||
|
||
const notContainer = await sdk.editor.moveToContainer({ id: 'button_1' } as any, 'text_1').catch((e) => e);
|
||
expect(notContainer.errors[0].message).toContain('不是容器');
|
||
|
||
const inside = await sdk.editor.moveToContainer({ id: 'container_1' } as any, 'container_1').catch((e) => e);
|
||
expect(inside.errors[0].message).toContain('自己');
|
||
|
||
const missing = await sdk.editor.moveToContainer({ id: 'nope' } as any, 'container_1').catch((e) => e);
|
||
expect(missing.errors[0].message).toContain('不存在');
|
||
});
|
||
|
||
test('update:节点不存在 / 修改类型', async () => {
|
||
const sdk = createSdk();
|
||
|
||
const missing = await sdk.editor.update({ id: 'nope', text: 'x' } as any).catch((e) => e);
|
||
expect(missing).toBeInstanceOf(DslValidationError);
|
||
|
||
const typeChange = await sdk.editor.update({ id: 'text_1', type: 'button' } as any).catch((e) => e);
|
||
expect(typeChange.errors[0].message).toContain('不允许修改节点类型');
|
||
});
|
||
|
||
test('update 根节点不做校验', async () => {
|
||
const sdk = createSdk();
|
||
await expect(sdk.editor.update({ id: 'app_1', name: 'renamed' } as any)).resolves.toBeTruthy();
|
||
});
|
||
|
||
test('数据源:类型不可用时 add / update 抛错', () => {
|
||
const sdk = createSdk({ datasourceList: [{ type: 'custom', text: '自定义' }] });
|
||
|
||
expect(() => sdk.dataSource.add({ id: 'ds_x', type: 'unknown' } as any)).toThrow(DslValidationError);
|
||
expect(() => sdk.dataSource.update({ id: 'ds_1', type: 'unknown' } as any)).toThrow(DslValidationError);
|
||
expect(() =>
|
||
sdk.dataSource.add({ id: 'ds_y', type: 'custom', fields: [], methods: [], events: [] } as any),
|
||
).not.toThrow();
|
||
});
|
||
});
|
||
|
||
describe('validate: 属性', () => {
|
||
test('add 时校验属性表单', async () => {
|
||
const sdk = createSdk({ propsConfigs: { text: textConfig }, propsValues: { text: { text: '' } } });
|
||
await sdk.ready;
|
||
|
||
const error = await sdk.editor.add({ type: 'text' } as any).catch((e) => e);
|
||
expect(error).toBeInstanceOf(DslValidationError);
|
||
expect(error.errors[0]).toMatchObject({ kind: 'props', target: 'node' });
|
||
expect(error.errors[0].message).toContain('必填');
|
||
|
||
await expect(sdk.editor.add({ type: 'text', text: 'ok' } as any)).resolves.toBeTruthy();
|
||
});
|
||
|
||
test('update 时校验合并之后的属性', async () => {
|
||
const sdk = createSdk({ propsConfigs: { text: textConfig } });
|
||
await sdk.ready;
|
||
|
||
const error = await sdk.editor.update({ id: 'text_1', text: '' } as any).catch((e) => e);
|
||
expect(error).toBeInstanceOf(DslValidationError);
|
||
expect((sdk.editor.getNodeById('text_1') as any).text).toBe('${ds_1.name}');
|
||
|
||
await expect(sdk.editor.update({ id: 'text_1', text: 'good' } as any)).resolves.toBeTruthy();
|
||
});
|
||
|
||
test('取属性配置时抛错,作为校验异常上报', async () => {
|
||
const sdk = createSdk();
|
||
sdk.props.usePlugin({
|
||
beforeGetPropsConfig: () => {
|
||
throw new Error('boom');
|
||
},
|
||
});
|
||
|
||
const result = await sdk.validate.node('text_1');
|
||
expect(result.valid).toBe(false);
|
||
expect(result.errors[0].message).toContain('boom');
|
||
});
|
||
|
||
test('checkNodeProps:没有配置 / root 不校验', async () => {
|
||
const sdk = createSdk();
|
||
expect(await sdk.validate.checkNodeProps({ id: 'a', type: 'unknown-without-config' } as any)).toEqual([]);
|
||
expect(await sdk.validate.checkNodeProps({ id: 'app_1', type: 'app' } as any)).toEqual([]);
|
||
});
|
||
});
|
||
|
||
describe('validate: 查询接口', () => {
|
||
test('node()', async () => {
|
||
const sdk = createSdk({ propsConfigs: { text: textConfig } });
|
||
await sdk.ready;
|
||
|
||
expect((await sdk.validate.node('button_1')).valid).toBe(true);
|
||
expect(await sdk.validate.node('nope')).toMatchObject({ valid: false });
|
||
|
||
sdk.ctx.validateMode = 'off';
|
||
await sdk.editor.update({ id: 'text_1', text: '' } as any);
|
||
|
||
const result = await sdk.validate.node('text_1');
|
||
expect(result.valid).toBe(false);
|
||
expect(result.errors[0].kind).toBe('props');
|
||
});
|
||
|
||
test('node() 能发现已经在 DSL 中的非法类型 / 被禁用的页面片', async () => {
|
||
const dsl = createDsl();
|
||
dsl.items[0].items!.push({ id: 'weird', type: 'weird-type' } as any);
|
||
dsl.items.push({ id: 'frag_1', type: 'page-fragment', items: [] } as any);
|
||
|
||
const sdk = createSdk({ dsl, disabledPageFragment: true });
|
||
|
||
expect((await sdk.validate.node('weird')).errors[0].message).toContain('weird-type');
|
||
expect((await sdk.validate.node('frag_1')).errors[0].message).toContain('disabledPageFragment');
|
||
});
|
||
|
||
test('dataSource()', async () => {
|
||
const dsl = createDsl();
|
||
dsl.dataSources![0].title = '';
|
||
const sdk = createSdk({ dsl });
|
||
|
||
expect((await sdk.validate.dataSource('nope')).valid).toBe(false);
|
||
|
||
const result = await sdk.validate.dataSource('ds_1');
|
||
expect(result.valid).toBe(false);
|
||
expect(result.errors[0]).toMatchObject({ target: 'data-source', kind: 'props' });
|
||
});
|
||
|
||
test('dsl():汇总整份 DSL 的问题', async () => {
|
||
const dsl = createDsl();
|
||
dsl.items[0].items!.push({ id: 'text_1', type: 'text' } as any, { id: 'text_2', type: 'text', text: '' } as any);
|
||
|
||
const sdk = createSdk({ dsl, propsConfigs: { text: textConfig } });
|
||
await sdk.ready;
|
||
|
||
const result = await sdk.validate.dsl();
|
||
expect(result.valid).toBe(false);
|
||
expect(result.errors.some((item) => item.message.includes('重复'))).toBe(true);
|
||
expect(result.errors.some((item) => item.kind === 'props')).toBe(true);
|
||
|
||
expect((await createSdk({ dsl: null }).validate.dsl()).errors[0].target).toBe('dsl');
|
||
expect((await createSdk().validate.dsl()).valid).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('validateMode', () => {
|
||
test('off:不校验', async () => {
|
||
const sdk = createSdk({ validateMode: 'off' });
|
||
await expect(sdk.editor.add({ type: 'not-exist' } as any)).resolves.toBeTruthy();
|
||
expect(() => sdk.dataSource.add({ id: 'ds_z', type: 'unknown' } as any)).not.toThrow();
|
||
await expect(sdk.editor.moveToContainer({ id: 'text_1' } as any, 'container_1')).resolves.toBeTruthy();
|
||
});
|
||
|
||
test('record:不抛错,错误写进 invalidNodeIds', async () => {
|
||
const sdk = createSdk({ validateMode: 'record', propsConfigs: { text: textConfig } });
|
||
await sdk.ready;
|
||
|
||
await sdk.editor.update({ id: 'text_1', text: '' } as any);
|
||
expect(sdk.editor.getInvalidNodeInfo('text_1')?.props).toContain('必填');
|
||
|
||
await sdk.editor.update({ id: 'text_1', text: 'fixed' } as any);
|
||
expect(sdk.editor.getInvalidNodeInfo('text_1')).toBeUndefined();
|
||
|
||
// 结构问题同样只记录
|
||
const added = (await sdk.editor.add({ type: 'text' } as any)) as any;
|
||
expect(added).toBeTruthy();
|
||
expect(sdk.editor.getInvalidNodeInfo(added.id)?.props).toContain('必填');
|
||
|
||
// 数据源 / 移动只在 strict 模式下校验
|
||
expect(() => sdk.dataSource.add({ id: 'ds_z', type: 'unknown' } as any)).not.toThrow();
|
||
await expect(sdk.editor.moveToContainer({ id: 'text_1' } as any, 'container_1')).resolves.toBeTruthy();
|
||
});
|
||
|
||
test('运行时切换 validateMode', async () => {
|
||
const sdk = createSdk();
|
||
sdk.ctx.validateMode = 'off';
|
||
await expect(sdk.editor.add({ type: 'not-exist' } as any)).resolves.toBeTruthy();
|
||
});
|
||
});
|