tmagic-editor/packages/editor/tests/unit/initService.spec.ts

787 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent.
*/
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { defineComponent, h, nextTick } from 'vue';
import { mount } from '@vue/test-utils';
import { DepTargetType } from '@tmagic/core';
import { getNodes } from '@tmagic/utils';
import { initServiceEvents, initServiceState } from '@editor/initService';
import * as logger from '@editor/utils/logger';
const mkServices = () => {
const handlers: Record<string, Record<string, any[]>> = {};
const mkSvc = (name: string) => {
handlers[name] = {};
const svc = {
on: vi.fn((event: string, cb: any) => {
handlers[name][event] = handlers[name][event] || [];
handlers[name][event].push(cb);
}),
off: vi.fn((event: string, cb: any) => {
handlers[name][event] = (handlers[name][event] || []).filter((h) => h !== cb);
}),
emit: (event: string, ...args: any[]) => {
(handlers[name][event] || []).forEach((cb) => cb(...args));
},
};
return svc;
};
const editorService: any = {
...mkSvc('editor'),
state: {} as any,
set: vi.fn((k: string, v: any) => (editorService.state[k] = v)),
get: vi.fn((k: string) => editorService.state[k]),
select: vi.fn(),
getNodeInfo: vi.fn(() => ({ page: { id: 'p1' } })),
getNodeById: vi.fn(),
getParentById: vi.fn(),
resetState: vi.fn(),
};
const historyService: any = { ...mkSvc('history'), resetState: vi.fn() };
const componentListService: any = {
...mkSvc('componentList'),
setList: vi.fn(),
resetState: vi.fn(),
};
const propsService: any = {
...mkSvc('props'),
setPropsConfigs: vi.fn(),
setPropsValues: vi.fn(),
setDisabledCodeBlock: vi.fn(),
setDisabledDataSource: vi.fn(),
resetState: vi.fn(),
};
const eventsService: any = {
...mkSvc('events'),
setEvents: vi.fn(),
setMethods: vi.fn(),
resetState: vi.fn(),
};
const uiService: any = {
...mkSvc('ui'),
set: vi.fn(),
resetState: vi.fn(),
};
const codeBlockService: any = {
...mkSvc('codeBlock'),
setCodeDsl: vi.fn(),
resetState: vi.fn(),
};
const keybindingService: any = { ...mkSvc('kb'), reset: vi.fn() };
const dataSourceService: any = {
...mkSvc('dataSource'),
state: {} as any,
set: vi.fn((k: string, v: any) => (dataSourceService.state[k] = v)),
get: vi.fn((k: string) => dataSourceService.state[k]),
setFormConfig: vi.fn(),
setFormValue: vi.fn(),
setFormEvent: vi.fn(),
setFormMethod: vi.fn(),
};
const depService: any = {
...mkSvc('dep'),
addTarget: vi.fn(),
removeTarget: vi.fn(),
getTargets: vi.fn(() => ({})),
getTarget: vi.fn(),
hasTarget: vi.fn(() => false),
clear: vi.fn(),
clearTargets: vi.fn(),
clearIdleTasks: vi.fn(),
collectIdle: vi.fn(async () => true),
collectByWorker: vi.fn(async () => undefined),
reset: vi.fn(),
};
const stageOverlayService: any = mkSvc('stageOverlay');
return {
editorService,
historyService,
componentListService,
propsService,
eventsService,
uiService,
codeBlockService,
keybindingService,
dataSourceService,
depService,
stageOverlayService,
handlers,
};
};
vi.mock('@tmagic/core', async () => {
const actual = await vi.importActual<any>('@tmagic/core');
return {
...actual,
createCodeBlockTarget: vi.fn((id: any, c: any) => ({
id,
type: actual.DepTargetType.CODE_BLOCK,
deps: {},
name: c?.name,
})),
createDataSourceTarget: vi.fn((ds: any) => ({ id: ds.id, type: actual.DepTargetType.DATA_SOURCE, deps: {} })),
createDataSourceCondTarget: vi.fn((ds: any) => ({
id: ds.id,
type: actual.DepTargetType.DATA_SOURCE_COND,
deps: {},
})),
createDataSourceMethodTarget: vi.fn((ds: any) => ({
id: ds.id,
type: actual.DepTargetType.DATA_SOURCE_METHOD,
deps: {},
})),
updateNode: vi.fn(),
};
});
vi.mock('@tmagic/utils', async () => {
const actual = await vi.importActual<any>('@tmagic/utils');
return {
...actual,
getDepNodeIds: vi.fn(() => []),
getNodes: vi.fn(() => []),
};
});
vi.mock('@editor/utils/editor', () => ({
isIncludeDataSource: vi.fn(() => false),
}));
vi.mock('@editor/utils/logger', () => ({
error: vi.fn(),
}));
const Wrap = (props: any, services: any) =>
defineComponent({
setup() {
initServiceState(props, services);
return () => h('div');
},
});
const WrapEvents = (props: any, emit: any, services: any) =>
defineComponent({
setup() {
initServiceEvents(props, emit, services);
return () => h('div');
},
});
describe('initServiceState', () => {
let services: ReturnType<typeof mkServices>;
beforeEach(() => {
services = mkServices();
});
test('modelValue 变化设置 editor root', () => {
const props = { modelValue: { id: 'a' } } as any;
mount(Wrap(props, services));
expect(services.editorService.set).toHaveBeenCalledWith('root', { id: 'a' }, { historySource: 'initial' });
});
test('disabledMultiSelect/alwaysMultiSelect 设置', () => {
const props = { disabledMultiSelect: true, alwaysMultiSelect: true } as any;
mount(Wrap(props, services));
expect(services.editorService.set).toHaveBeenCalledWith('disabledMultiSelect', true);
expect(services.editorService.set).toHaveBeenCalledWith('alwaysMultiSelect', true);
});
test('componentGroupList 调用 setList', () => {
const props = { componentGroupList: [{ items: [] }] } as any;
mount(Wrap(props, services));
expect(services.componentListService.setList).toHaveBeenCalledWith([{ items: [] }]);
});
test('propsConfigs/propsValues 设置', () => {
const props = { propsConfigs: { a: [] }, propsValues: { a: {} } } as any;
mount(Wrap(props, services));
expect(services.propsService.setPropsConfigs).toHaveBeenCalled();
expect(services.propsService.setPropsValues).toHaveBeenCalled();
});
test('eventMethodList 设置 events/methods', () => {
const props = {
eventMethodList: { typeA: { events: [{ name: 'click' }], methods: [{ name: 'm' }] } },
} as any;
mount(Wrap(props, services));
expect(services.eventsService.setEvents).toHaveBeenCalledWith({ typeA: [{ name: 'click' }] });
expect(services.eventsService.setMethods).toHaveBeenCalledWith({ typeA: [{ name: 'm' }] });
});
test('datasourceConfigs 设置 form config', () => {
const props = { datasourceConfigs: { http: [{ name: 'url' }] } } as any;
mount(Wrap(props, services));
expect(services.dataSourceService.setFormConfig).toHaveBeenCalledWith('http', [{ name: 'url' }]);
});
test('datasourceValues 设置 form value', () => {
const props = { datasourceValues: { base: { id: 'x' } } } as any;
mount(Wrap(props, services));
expect(services.dataSourceService.setFormValue).toHaveBeenCalledWith('base', { id: 'x' });
});
test('datasourceEventMethodList 设置 form event/method', () => {
const props = {
datasourceEventMethodList: {
http: { events: [{ name: 'load' }], methods: [{ name: 'do' }] },
},
} as any;
mount(Wrap(props, services));
expect(services.dataSourceService.setFormEvent).toHaveBeenCalledWith('http', [{ name: 'load' }]);
expect(services.dataSourceService.setFormMethod).toHaveBeenCalledWith('http', [{ name: 'do' }]);
});
test('defaultSelected 调用 select', () => {
services.editorService.getNodeById.mockReturnValue({ id: 'n1' });
const props = { defaultSelected: 'n1' } as any;
mount(Wrap(props, services));
expect(services.editorService.select).toHaveBeenCalledWith('n1');
});
test('defaultSelected 对应节点不存在时不调用 select', () => {
services.editorService.getNodeById.mockReturnValue(null);
const props = { defaultSelected: 'n1' } as any;
mount(Wrap(props, services));
expect(services.editorService.select).not.toHaveBeenCalled();
});
test('defaultSelected select 失败时兜底成日志,不产生未处理的拒绝', async () => {
const err = new Error('获取不到组件信息');
vi.mocked(logger.error).mockClear();
services.editorService.getNodeById.mockReturnValue({ id: 'n1' });
services.editorService.select.mockRejectedValue(err);
const props = { defaultSelected: 'n1' } as any;
mount(Wrap(props, services));
await nextTick();
expect(logger.error).toHaveBeenCalledWith(err);
});
test('stageRect 设置 ui state', () => {
const props = { stageRect: { width: 100 } } as any;
mount(Wrap(props, services));
expect(services.uiService.set).toHaveBeenCalledWith('stageRect', { width: 100 });
});
test('disabledCodeBlock/disabledDataSource', () => {
const props = { disabledCodeBlock: true, disabledDataSource: true } as any;
mount(Wrap(props, services));
expect(services.propsService.setDisabledCodeBlock).toHaveBeenCalledWith(true);
expect(services.propsService.setDisabledDataSource).toHaveBeenCalledWith(true);
});
test('卸载时重置所有 service', () => {
const wrapper = mount(Wrap({} as any, services));
wrapper.unmount();
expect(services.editorService.resetState).toHaveBeenCalled();
expect(services.historyService.resetState).toHaveBeenCalled();
expect(services.propsService.resetState).toHaveBeenCalled();
expect(services.uiService.resetState).toHaveBeenCalled();
expect(services.componentListService.resetState).toHaveBeenCalled();
expect(services.codeBlockService.resetState).toHaveBeenCalled();
expect(services.eventsService.resetState).toHaveBeenCalled();
expect(services.keybindingService.reset).toHaveBeenCalled();
expect(services.depService.reset).toHaveBeenCalled();
});
});
const mkDataSourceManager = () => ({
addDataSource: vi.fn(),
removeDataSource: vi.fn(),
get: vi.fn(() => ({
setFields: vi.fn(),
setData: vi.fn(),
getDefaultData: vi.fn(() => ({})),
})),
init: vi.fn(),
compiledNode: vi.fn((node: any) => node),
});
/** renderer.runtime 直接就绪getTMagicApp 走同步分支 */
const mkReadyStage = (app: any) => {
const runtime = {
getApp: vi.fn(() => app),
updateRootConfig: vi.fn(),
updatePageId: vi.fn(),
};
return {
renderer: {
runtime,
getRuntime: vi.fn(async () => runtime),
once: vi.fn(),
},
select: vi.fn(),
reloadIframe: vi.fn(),
update: vi.fn(),
runtime,
};
};
describe('initServiceEvents', () => {
let services: ReturnType<typeof mkServices>;
let emit: any;
beforeEach(() => {
services = mkServices();
emit = vi.fn();
});
test('注册 editorService 事件', () => {
mount(WrapEvents({} as any, emit, services));
const events = services.editorService.on.mock.calls.map((c: any[]) => c[0]);
expect(events).toContain('root-change');
expect(events).toContain('add');
expect(events).toContain('remove');
expect(events).toContain('update');
expect(events).toContain('history-change');
});
test('注册 dataSourceService/codeBlockService/depService 事件', () => {
mount(WrapEvents({} as any, emit, services));
expect(services.dataSourceService.on.mock.calls.map((c: any[]) => c[0])).toEqual(
expect.arrayContaining(['add', 'update', 'remove']),
);
expect(services.codeBlockService.on.mock.calls.map((c: any[]) => c[0])).toEqual(
expect.arrayContaining(['addOrUpdate', 'remove']),
);
expect(services.depService.on.mock.calls.map((c: any[]) => c[0])).toEqual(
expect.arrayContaining(['add-target', 'remove-target', 'ds-collected']),
);
});
test('rootChange 处理代码块和数据源', async () => {
mount(WrapEvents({} as any, emit, services));
const value: any = {
id: 'r',
codeBlocks: { c1: { name: 'a', content: '' } },
dataSources: [{ id: 'd1', type: 'base' }],
items: [],
};
// set('root', v) 是先赋值再派发事件mock 中同样先对齐 state 再 emit
services.editorService.state.root = value;
services.editorService.emit('root-change', value, null);
await new Promise((r) => setTimeout(r, 0));
expect(services.codeBlockService.setCodeDsl).toHaveBeenCalled();
expect(services.dataSourceService.set).toHaveBeenCalledWith('dataSources', value.dataSources);
expect(services.depService.clearTargets).toHaveBeenCalled();
expect(services.depService.addTarget).toHaveBeenCalled();
});
test('rootChange null 时直接返回', () => {
mount(WrapEvents({} as any, emit, services));
services.editorService.emit('root-change', null);
expect(services.codeBlockService.setCodeDsl).not.toHaveBeenCalled();
});
test('add 事件触发 collectIdle', async () => {
mount(WrapEvents({} as any, emit, services));
services.editorService.emit('add', [{ id: 'n', type: 'text' }]);
await new Promise((r) => setTimeout(r, 0));
expect(services.depService.collectIdle).toHaveBeenCalled();
});
test('remove 事件触发 depService.clear', () => {
mount(WrapEvents({} as any, emit, services));
services.editorService.emit('remove', [{ id: 'n' }]);
expect(services.depService.clear).toHaveBeenCalled();
});
test('update 事件 changeRecords 中包含数据源', async () => {
mount(WrapEvents({} as any, emit, services));
services.editorService.emit('update', [
{
newNode: { id: 'n1', type: 'text' },
oldNode: { id: 'n1', type: 'text' },
changeRecords: [{ propPath: 'props.value', value: '${ds.field}' }],
},
]);
await new Promise((r) => setTimeout(r, 0));
expect(services.depService.collectIdle).toHaveBeenCalled();
});
test('update 事件 changeRecords 为空走 normal', async () => {
services.editorService.state.root = { id: 'r', items: [] };
mount(WrapEvents({} as any, emit, services));
services.editorService.emit('update', [
{ newNode: { id: 'n1', type: 'text' }, oldNode: { id: 'n1', type: 'text' } },
]);
await new Promise((r) => setTimeout(r, 0));
expect(services.depService.collectIdle).toHaveBeenCalled();
});
test('history-change 触发 collect', async () => {
services.editorService.state.root = { id: 'r' };
mount(WrapEvents({} as any, emit, services));
services.editorService.emit('history-change', { id: 'p1', type: 'page' });
await new Promise((r) => setTimeout(r, 0));
expect(services.depService.collectIdle).toHaveBeenCalled();
});
test('dataSourceService add 触发 initDataSourceDepTarget', () => {
mount(WrapEvents({} as any, emit, services));
services.dataSourceService.emit('add', { id: 'd1', type: 'base' });
expect(services.depService.addTarget).toHaveBeenCalled();
});
test('dataSourceService remove root 不存在时不报错', async () => {
services.editorService.state.root = null;
mount(WrapEvents({} as any, emit, services));
services.dataSourceService.emit('remove', 'd1');
await new Promise((r) => setTimeout(r, 0));
expect(services.depService.removeTarget).not.toHaveBeenCalled();
});
test('dataSourceService update 修改 fields', async () => {
services.editorService.state.root = { id: 'r', items: [{ id: 'a', type: 'text' }] };
mount(WrapEvents({} as any, emit, services));
services.dataSourceService.emit(
'update',
{ id: 'd1', type: 'base', fields: [], mocks: [], methods: [] },
{ changeRecords: [{ propPath: 'fields' }] },
);
await new Promise((r) => setTimeout(r, 0));
expect(services.depService.removeTarget).toHaveBeenCalled();
expect(services.depService.addTarget).toHaveBeenCalled();
});
test('codeBlockService addOrUpdate 新增/更新', () => {
services.depService.hasTarget.mockReturnValueOnce(false).mockReturnValueOnce(true);
services.depService.getTarget.mockReturnValue({ name: 'old' });
mount(WrapEvents({} as any, emit, services));
services.codeBlockService.emit('addOrUpdate', 'c1', { name: 'a' });
expect(services.depService.addTarget).toHaveBeenCalled();
services.codeBlockService.emit('addOrUpdate', 'c1', { name: 'b' });
expect(services.depService.getTarget).toHaveBeenCalled();
});
test('codeBlockService remove', () => {
mount(WrapEvents({} as any, emit, services));
services.codeBlockService.emit('remove', 'c1');
expect(services.depService.removeTarget).toHaveBeenCalledWith('c1', DepTargetType.CODE_BLOCK);
});
test('depService add-target 设置 root.dataSourceDeps/CondDeps/MethodDeps', () => {
services.editorService.state.root = { id: 'r' };
mount(WrapEvents({} as any, emit, services));
services.depService.emit('add-target', { id: 't1', type: DepTargetType.DATA_SOURCE, deps: {} });
services.depService.emit('add-target', { id: 't2', type: DepTargetType.DATA_SOURCE_COND, deps: {} });
services.depService.emit('add-target', { id: 't3', type: DepTargetType.DATA_SOURCE_METHOD, deps: {} });
expect(services.editorService.state.root.dataSourceDeps).toHaveProperty('t1');
expect(services.editorService.state.root.dataSourceCondDeps).toHaveProperty('t2');
expect(services.editorService.state.root.dataSourceMethodDeps).toHaveProperty('t3');
});
test('depService remove-target 清理 root deps', () => {
services.editorService.state.root = {
id: 'r',
dataSourceDeps: { a: {} },
dataSourceCondDeps: { b: {} },
dataSourceMethodDeps: { c: {} },
};
mount(WrapEvents({} as any, emit, services));
services.depService.emit('remove-target', 'a', DepTargetType.DATA_SOURCE);
services.depService.emit('remove-target', 'b', DepTargetType.DATA_SOURCE_COND);
services.depService.emit('remove-target', 'c', DepTargetType.DATA_SOURCE_METHOD);
expect(services.editorService.state.root.dataSourceDeps).not.toHaveProperty('a');
expect(services.editorService.state.root.dataSourceCondDeps).not.toHaveProperty('b');
expect(services.editorService.state.root.dataSourceMethodDeps).not.toHaveProperty('c');
});
test('卸载时取消所有事件订阅', () => {
const wrapper = mount(WrapEvents({} as any, emit, services));
wrapper.unmount();
expect(services.editorService.off).toHaveBeenCalled();
expect(services.codeBlockService.off).toHaveBeenCalled();
expect(services.dataSourceService.off).toHaveBeenCalled();
expect(services.depService.off).toHaveBeenCalled();
});
test('runtimeUrl 变化时重新加载 iframe', async () => {
const stage = {
reloadIframe: vi.fn(),
renderer: {
once: vi.fn((event: string, cb: any) => {
cb({
updateRootConfig: vi.fn(),
updatePageId: vi.fn(),
});
}),
},
select: vi.fn(),
};
services.editorService.state.stage = stage;
services.editorService.state.page = { id: 'p1' };
services.editorService.state.node = { id: 'n1' };
const hostComp = defineComponent({
props: { runtimeUrl: { type: String, default: '' } },
setup(props) {
initServiceEvents(props as any, emit, services as any);
return () => h('div');
},
});
const wrapper = mount(hostComp);
await wrapper.setProps({ runtimeUrl: 'http://x' });
await new Promise((r) => setTimeout(r, 10));
expect(stage.reloadIframe).toHaveBeenCalledWith('http://x');
});
// 因 services 中 editor.state 不是 reactivestage watch 不会触发,跳过该测试场景
test('getTMagicApp 在 runtime 未就绪时等待 runtime-ready并复用同一个 promise', async () => {
const dataSourceManager = mkDataSourceManager();
const app = { dsl: {}, dataSourceManager };
let runtimeReady: any;
const renderer: any = {
runtime: null,
getRuntime: vi.fn(),
once: vi.fn((_event: string, cb: any) => {
runtimeReady = cb;
}),
};
services.editorService.state.stage = { renderer, select: vi.fn() };
mount(WrapEvents({} as any, emit, services));
services.dataSourceService.emit('add', { id: 'd1', type: 'base' });
services.dataSourceService.emit('add', { id: 'd2', type: 'base' });
await nextTick();
// 两次调用只应注册一次 runtime-ready 监听
expect(renderer.once).toHaveBeenCalledTimes(1);
renderer.runtime = { getApp: () => app };
runtimeReady();
await new Promise((r) => setTimeout(r, 0));
expect(dataSourceManager.addDataSource).toHaveBeenCalledTimes(2);
expect(dataSourceManager.init).toHaveBeenCalled();
});
test('getTMagicApp 在 stage 没有 renderer 时返回 undefined', async () => {
services.editorService.state.stage = { select: vi.fn() };
mount(WrapEvents({} as any, emit, services));
services.dataSourceService.emit('add', { id: 'd1', type: 'base' });
await new Promise((r) => setTimeout(r, 0));
expect(services.depService.addTarget).toHaveBeenCalled();
});
test('ds-collected 事件把 dataSourceDeps 同步给 runtime app', async () => {
const app: any = { dsl: { dataSourceDeps: null }, dataSourceManager: mkDataSourceManager() };
services.editorService.state.stage = mkReadyStage(app);
services.editorService.state.root = { id: 'r', dataSourceDeps: { d1: {} } };
mount(WrapEvents({} as any, emit, services));
services.depService.emit('ds-collected');
await new Promise((r) => setTimeout(r, 0));
expect(app.dsl.dataSourceDeps).toEqual({ d1: {} });
});
test('rootChange items 是数组时更新画布 dsl', async () => {
const app: any = { dsl: {}, dataSourceManager: mkDataSourceManager() };
const stage = mkReadyStage(app);
services.editorService.state.stage = stage;
services.editorService.state.page = { id: 'p1' };
services.editorService.state.node = { id: 'n1' };
mount(WrapEvents({} as any, emit, services));
const value: any = {
id: 'r',
items: [{ id: 'n1', type: 'text' }],
dataSources: [],
dataSourceDeps: { d1: {} },
codeBlocks: {},
};
services.editorService.state.root = value;
services.editorService.emit('root-change', value);
await new Promise((r) => setTimeout(r, 10));
expect(stage.runtime.updatePageId).toHaveBeenCalledWith('p1');
expect(stage.runtime.updateRootConfig).toHaveBeenCalled();
expect(stage.select).toHaveBeenCalledWith('n1');
});
test('rootChange items 不是数组时清空依赖', async () => {
mount(WrapEvents({} as any, emit, services));
const value: any = { id: 'r', dataSourceDeps: { a: {} }, dataSourceCondDeps: { b: {} } };
services.editorService.state.root = value;
services.editorService.emit('root-change', value);
await new Promise((r) => setTimeout(r, 0));
expect(services.depService.clear).toHaveBeenCalled();
expect(value.dataSourceDeps).toBeUndefined();
expect(value.dataSourceCondDeps).toBeUndefined();
});
test('rootChange 没有可选中节点时回落到 root 自身', async () => {
services.editorService.getNodeById.mockReturnValue(null);
mount(WrapEvents({} as any, emit, services));
const value: any = { id: 'r', items: [] };
services.editorService.state.root = value;
services.editorService.emit('root-change', value, { id: 'prev' });
await new Promise((r) => setTimeout(r, 0));
expect(services.editorService.set).toHaveBeenCalledWith('nodes', [value]);
expect(services.editorService.set).toHaveBeenCalledWith('parent', null);
expect(services.editorService.set).toHaveBeenCalledWith('page', null);
expect(emit).toHaveBeenCalledWith('update:modelValue', value);
});
test('rootChange 有 items 时选中第一个节点', async () => {
services.editorService.getNodeById.mockReturnValue(null);
mount(WrapEvents({} as any, emit, services));
const value: any = { id: 'r', items: [{ id: 'first', type: 'page' }] };
services.editorService.state.root = value;
services.editorService.emit('root-change', value);
await new Promise((r) => setTimeout(r, 0));
expect(services.editorService.select).toHaveBeenCalledWith({ id: 'first', type: 'page' });
});
test('rootChange 外部已持有该 root 时不回写 modelValue', async () => {
services.editorService.getNodeById.mockReturnValue(null);
const value: any = { id: 'r', items: [] };
mount(WrapEvents({ modelValue: value } as any, emit, services));
services.editorService.state.root = value;
services.editorService.emit('root-change', value);
await new Promise((r) => setTimeout(r, 0));
expect(emit).not.toHaveBeenCalled();
});
test('rootChange 处理期间 root 被替换:不刷旧 dsl 也不回写过期快照', async () => {
const app: any = { dsl: {}, dataSourceManager: mkDataSourceManager() };
const stage = mkReadyStage(app);
services.editorService.state.stage = stage;
services.editorService.getNodeById.mockReturnValue(null);
mount(WrapEvents({} as any, emit, services));
const value: any = { id: 'r', items: [{ id: 'n1', type: 'text' }], dataSources: [], codeBlocks: {} };
services.editorService.state.root = value;
services.editorService.emit('root-change', value);
// 异步处理还没跑完root 已被新的一次整体替换顶掉
services.editorService.state.root = { id: 'r', items: [], dataSources: [], codeBlocks: {} };
await new Promise((r) => setTimeout(r, 10));
expect(stage.runtime.updateRootConfig).not.toHaveBeenCalled();
expect(emit).not.toHaveBeenCalled();
});
test('update 事件ROOT 节点、无 propPath、命中已收集依赖三种分支', async () => {
services.editorService.state.root = { id: 'r', items: [] };
services.depService.getTargets.mockReturnValue({
t1: { deps: { n3: { keys: ['props.text'] } } },
});
mount(WrapEvents({} as any, emit, services));
services.editorService.emit('update', [
{ newNode: { id: 'r', type: 'root' }, oldNode: { id: 'r', type: 'root' } },
{
newNode: { id: 'n2', type: 'text' },
oldNode: { id: 'n2', type: 'text' },
changeRecords: [{ value: 'x' }],
},
{
newNode: { id: 'n3', type: 'text' },
oldNode: { id: 'n3', type: 'text' },
changeRecords: [{ propPath: 'props.text', value: 'x' }],
},
]);
await new Promise((r) => setTimeout(r, 0));
expect(services.depService.collectIdle).toHaveBeenCalled();
});
test('dataSource update 修改 mocks / methods 分别只重建对应 target', async () => {
const app: any = { dsl: {}, dataSourceManager: mkDataSourceManager() };
services.editorService.state.stage = mkReadyStage(app);
services.editorService.state.root = { id: 'r', items: [{ id: 'a', type: 'text' }] };
mount(WrapEvents({} as any, emit, services));
services.dataSourceService.emit(
'update',
{ id: 'd1', type: 'base', fields: [], mocks: [{ useInEditor: true, data: { a: 1 } }] },
{ changeRecords: [{ propPath: 'mocks' }] },
);
await new Promise((r) => setTimeout(r, 0));
expect(services.depService.removeTarget).toHaveBeenCalledWith('d1', DepTargetType.DATA_SOURCE);
services.depService.removeTarget.mockClear();
services.dataSourceService.emit(
'update',
{ id: 'd1', type: 'base', fields: [], methods: [] },
{ changeRecords: [{ propPath: 'methods.0.name' }] },
);
await new Promise((r) => setTimeout(r, 0));
expect(services.depService.removeTarget).toHaveBeenCalledWith('d1', DepTargetType.DATA_SOURCE_METHOD);
});
test('dataSource update 无依赖变更时只刷新数据', async () => {
const dataSourceManager = mkDataSourceManager();
const app: any = { dsl: {}, dataSourceManager };
services.editorService.state.stage = mkReadyStage(app);
services.editorService.state.root = { id: 'r', items: [], dataSources: [{ id: 'd1' }] };
mount(WrapEvents({} as any, emit, services));
services.dataSourceService.emit(
'update',
{ id: 'd1', type: 'base', fields: [] },
{ changeRecords: [{ propPath: 'title' }, {}] },
);
await new Promise((r) => setTimeout(r, 0));
expect(dataSourceManager.get).toHaveBeenCalledWith('d1');
expect(services.depService.clearIdleTasks).not.toHaveBeenCalled();
});
test('dataSource remove 重新收集依赖并从 app 中移除数据源', async () => {
const dataSourceManager = mkDataSourceManager();
const app: any = { dsl: {}, dataSourceManager };
services.editorService.state.stage = mkReadyStage(app);
services.editorService.state.root = {
id: 'r',
items: [],
dataSources: [{ id: 'd1' }],
dataSourceDeps: { d1: { n1: {} } },
};
mount(WrapEvents({} as any, emit, services));
services.dataSourceService.emit('remove', 'd1');
await new Promise((r) => setTimeout(r, 0));
expect(dataSourceManager.removeDataSource).toHaveBeenCalledWith('d1');
expect(services.depService.removeTarget).toHaveBeenCalledWith('d1', DepTargetType.DATA_SOURCE);
expect(services.depService.removeTarget).toHaveBeenCalledWith('d1', DepTargetType.DATA_SOURCE_COND);
expect(services.depService.removeTarget).toHaveBeenCalledWith('d1', DepTargetType.DATA_SOURCE_METHOD);
expect(app.dsl.dataSources).toEqual([{ id: 'd1' }]);
});
test('dataSource remove 收集未完成时不移除数据源', async () => {
const dataSourceManager = mkDataSourceManager();
const app: any = { dsl: {}, dataSourceManager };
services.editorService.state.stage = mkReadyStage(app);
services.editorService.state.root = { id: 'r', items: [] };
vi.mocked(getNodes).mockReturnValueOnce([{ id: 'n1', type: 'text' }] as any);
services.depService.collectIdle.mockResolvedValue(false);
mount(WrapEvents({} as any, emit, services));
services.dataSourceService.emit('remove', 'd1');
await new Promise((r) => setTimeout(r, 0));
expect(dataSourceManager.removeDataSource).not.toHaveBeenCalled();
});
});