2026-05-14 15:26:22 +08:00

377 lines
14 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 { describe, expect, test, vi } from 'vitest';
import { defineComponent, h } from 'vue';
import { mount } from '@vue/test-utils';
import EventSelect from '@editor/fields/EventSelect.vue';
const editorService = {
get: vi.fn(),
getNodeById: vi.fn(),
};
const dataSourceService = {
get: vi.fn(),
getDataSourceById: vi.fn(),
getFormEvent: vi.fn(() => []),
};
const eventsService = {
getEvent: vi.fn(() => [{ label: 'click', value: 'click' }]),
getMethod: vi.fn(() => [{ label: 'open', value: 'open' }]),
};
const codeBlockService = {
getCodeDsl: vi.fn(() => ({ c1: {} })),
getEditStatus: vi.fn(() => true),
};
const propsService = {
getDisabledCodeBlock: vi.fn(() => false),
getDisabledDataSource: vi.fn(() => false),
};
vi.mock('@editor/hooks/use-services', () => ({
useServices: () => ({ editorService, dataSourceService, eventsService, codeBlockService, propsService }),
}));
vi.mock('@editor/utils', async () => {
const actual = await vi.importActual<any>('@editor/utils');
return { ...actual, getCascaderOptionsFromFields: vi.fn(() => []) };
});
vi.mock('@tmagic/form', async (importOriginal) => {
const actual = await importOriginal<any>();
return {
...actual,
defineFormItem: (cfg: any) => cfg,
MTable: defineComponent({
name: 'MTable',
props: ['model', 'config', 'name', 'size', 'disabled'],
emits: ['change'],
setup() {
return () => h('div', { class: 'fake-table' });
},
}),
MPanel: defineComponent({
name: 'MPanel',
props: ['model', 'config', 'prop', 'disabled', 'size', 'labelWidth'],
emits: ['change'],
setup(_p, { slots }) {
return () => h('div', { class: 'fake-panel' }, slots.header?.());
},
}),
MContainer: defineComponent({
name: 'MFormContainer',
props: ['model', 'config', 'prop', 'disabled', 'size'],
emits: ['change'],
setup() {
return () => h('div', { class: 'fake-container' });
},
}),
};
});
vi.mock('@tmagic/design', () => ({
TMagicButton: defineComponent({
name: 'TMagicButton',
props: ['type', 'size', 'disabled', 'icon', 'link'],
emits: ['click'],
setup(_p, { emit, slots }) {
return () => h('button', { onClick: () => emit('click') }, slots.default?.());
},
}),
}));
vi.mock('@tmagic/utils', async () => {
const actual = await vi.importActual<any>('@tmagic/utils');
return {
...actual,
DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX: 'ds_change_',
traverseNode: (node: any, fn: any) => {
fn(node);
node.items?.forEach((c: any) => fn(c));
},
};
});
const baseProps = (extra: any = {}) => ({
config: { type: 'event-select', src: 'component' },
name: 'events',
prop: 'events',
model: { events: [] },
size: 'default',
disabled: false,
...extra,
});
describe('EventSelect', () => {
test('events 为空 isOldVersion=false 显示新版按钮', () => {
const wrapper = mount(EventSelect, { props: baseProps() as any });
expect(wrapper.find('.create-button').exists()).toBe(true);
expect(wrapper.find('.fake-table').exists()).toBe(false);
});
test('addEvent emit 事件并携带 modifyKey', async () => {
const wrapper = mount(EventSelect, { props: baseProps() as any });
await wrapper.find('.create-button').trigger('click');
const evts = wrapper.emitted('change');
expect((evts?.[0]?.[0] as any).name).toBe('');
});
test('removeEvent 删除指定 index', async () => {
const wrapper = mount(EventSelect, {
props: baseProps({
model: { events: [{ name: 'a', actions: [] }] },
}) as any,
});
const buttons = wrapper.findAll('button');
const lastBtn = buttons[buttons.length - 1];
await lastBtn.trigger('click');
const evts = wrapper.emitted('change');
expect(evts).toBeTruthy();
});
test('events 含 actions 字段时不算 oldVersion渲染 panel', () => {
const wrapper = mount(EventSelect, {
props: baseProps({
model: { events: [{ name: 'a', actions: [] }] },
}) as any,
});
expect(wrapper.findAll('.fake-panel').length).toBe(1);
});
test('events 不含 actions 字段时为 oldVersion渲染 table', () => {
const wrapper = mount(EventSelect, {
props: baseProps({
model: { events: [{ name: 'a' }] },
}) as any,
});
expect(wrapper.find('.fake-table').exists()).toBe(true);
});
test('Table change emit', async () => {
const wrapper = mount(EventSelect, {
props: baseProps({
model: { events: [{ name: 'a' }] },
}) as any,
});
await wrapper.findComponent({ name: 'MTable' }).vm.$emit('change', null, { modifyKey: 'foo' });
expect(wrapper.emitted('change')).toBeTruthy();
});
test('Panel header MFormContainer change emit', async () => {
const wrapper = mount(EventSelect, {
props: baseProps({
model: { events: [{ name: 'a', actions: [] }] },
}) as any,
});
await wrapper.findComponent({ name: 'MFormContainer' }).vm.$emit('change', null, { modifyKey: 'name' });
expect(wrapper.emitted('change')).toBeTruthy();
});
test('addEvent 在 model[name] 为空时初始化', async () => {
const m: any = { events: [] };
const wrapper = mount(EventSelect, { props: baseProps({ model: m }) as any });
await wrapper.find('.create-button').trigger('click');
const evts = wrapper.emitted('change');
expect(evts).toBeTruthy();
expect((evts?.[0]?.[0] as any).actions).toEqual([]);
});
test('eventNameConfig type/options src=component 返回 select', () => {
const wrapper = mount(EventSelect, {
props: baseProps({
model: { events: [{ name: 'a', actions: [] }] },
}) as any,
});
const cfg = wrapper.findComponent({ name: 'MFormContainer' }).props('config') as any;
expect(cfg.type(undefined, { formValue: { type: 'btn' } })).toBe('select');
const opts = cfg.options(undefined, { formValue: { type: 'btn' } });
expect(Array.isArray(opts)).toBe(true);
expect(opts[0]).toMatchObject({ text: 'click', value: 'click' });
});
test('eventNameConfig type 当 page-fragment 且有 pageFragmentId 返回 cascader', () => {
editorService.get.mockReturnValue({ items: [{ id: 'pf1', items: [] }] });
const wrapper = mount(EventSelect, {
props: baseProps({
model: { events: [{ name: 'a', actions: [] }] },
}) as any,
});
const cfg = wrapper.findComponent({ name: 'MFormContainer' }).props('config') as any;
expect(cfg.type(undefined, { formValue: { type: 'page-fragment-container', pageFragmentId: 'pf1' } })).toBe(
'cascader',
);
const opts = cfg.options(undefined, { formValue: { type: 'page-fragment-container', pageFragmentId: 'pf1' } });
expect(Array.isArray(opts)).toBe(true);
});
test('eventNameConfig src=datasource 返回事件 + 数据变化字段', () => {
dataSourceService.getDataSourceById.mockReturnValue({ fields: [{ name: 'f1' }] });
const wrapper = mount(EventSelect, {
props: baseProps({
config: { type: 'event-select', src: 'datasource' },
model: { events: [{ name: 'a', actions: [] }] },
}) as any,
});
const cfg = wrapper.findComponent({ name: 'MFormContainer' }).props('config') as any;
const opts = cfg.options(undefined, { formValue: { type: 'ds', id: 'd1' } });
expect(opts).toEqual([{ label: '数据变化', value: 'ds_change_', children: [] }]);
});
test('eventNameConfig src=datasource 无 fields 时返回原始事件', () => {
dataSourceService.getDataSourceById.mockReturnValue({ fields: [] });
const wrapper = mount(EventSelect, {
props: baseProps({
config: { type: 'event-select', src: 'datasource' },
model: { events: [{ name: 'a', actions: [] }] },
}) as any,
});
const cfg = wrapper.findComponent({ name: 'MFormContainer' }).props('config') as any;
const opts = cfg.options(undefined, { formValue: { type: 'ds', id: 'd1' } });
expect(opts).toEqual([]);
});
test('actionTypeConfig 含 组件/代码/数据源', () => {
propsService.getDisabledCodeBlock.mockReturnValue(false);
propsService.getDisabledDataSource.mockReturnValue(false);
const wrapper = mount(EventSelect, {
props: baseProps({
model: { events: [{ name: 'a', actions: [] }] },
}) as any,
});
const panelCfg = wrapper.findComponent({ name: 'MPanel' }).props('config') as any;
const groupItems = panelCfg.items[0].items;
const actionType = groupItems[0];
const opts = actionType.options();
expect(opts.map((o: any) => o.value).sort()).toEqual(['code', 'comp', 'data-source'].sort());
});
test('actionTypeConfig disabledCodeBlock/disabledDataSource 时不包含选项', () => {
propsService.getDisabledCodeBlock.mockReturnValue(true);
propsService.getDisabledDataSource.mockReturnValue(true);
const wrapper = mount(EventSelect, {
props: baseProps({
model: { events: [{ name: 'a', actions: [] }] },
}) as any,
});
const panelCfg = wrapper.findComponent({ name: 'MPanel' }).props('config') as any;
const actionType = panelCfg.items[0].items[0];
const opts = actionType.options();
expect(opts.map((o: any) => o.value)).toEqual(['comp']);
propsService.getDisabledCodeBlock.mockReturnValue(false);
propsService.getDisabledDataSource.mockReturnValue(false);
});
test('targetCompConfig display/onChange', () => {
const wrapper = mount(EventSelect, {
props: baseProps({
model: { events: [{ name: 'a', actions: [] }] },
}) as any,
});
const panelCfg = wrapper.findComponent({ name: 'MPanel' }).props('config') as any;
const target = panelCfg.items[0].items[1];
expect(target.display(undefined, { model: { actionType: 'comp' } })).toBe(true);
const setModel = vi.fn();
target.onChange(undefined, undefined, { setModel });
expect(setModel).toHaveBeenCalledWith('method', '');
});
test('compActionConfig 解析 type/options', () => {
editorService.getNodeById.mockReturnValue({ type: 'btn', id: '1' });
const wrapper = mount(EventSelect, {
props: baseProps({
model: { events: [{ name: 'a', actions: [] }] },
}) as any,
});
const panelCfg = wrapper.findComponent({ name: 'MPanel' }).props('config') as any;
const compAction = panelCfg.items[0].items[2];
expect(compAction.type(undefined, { model: { to: '1' } })).toBe('select');
expect(Array.isArray(compAction.options(undefined, { model: { to: '1' } }))).toBe(true);
});
test('compActionConfig type cascader 当 page-fragment-container', () => {
editorService.getNodeById.mockReturnValue({ type: 'page-fragment-container', id: '1', pageFragmentId: 'pf1' });
editorService.get.mockReturnValue({ items: [{ id: 'pf1', items: [{ id: 'c1', type: 'btn', name: 'b' }] }] });
const wrapper = mount(EventSelect, {
props: baseProps({
model: { events: [{ name: 'a', actions: [] }] },
}) as any,
});
const panelCfg = wrapper.findComponent({ name: 'MPanel' }).props('config') as any;
const compAction = panelCfg.items[0].items[2];
expect(compAction.type(undefined, { model: { to: '1' } })).toBe('cascader');
const opts = compAction.options(undefined, { model: { to: '1' } });
expect(Array.isArray(opts)).toBe(true);
});
test('compActionConfig options 当 node 无 type 返回空数组', () => {
editorService.getNodeById.mockReturnValue(null);
const wrapper = mount(EventSelect, {
props: baseProps({
model: { events: [{ name: 'a', actions: [] }] },
}) as any,
});
const panelCfg = wrapper.findComponent({ name: 'MPanel' }).props('config') as any;
const compAction = panelCfg.items[0].items[2];
expect(compAction.options(undefined, { model: { to: 'unknown' } })).toEqual([]);
});
test('codeActionConfig display/notEditable', () => {
codeBlockService.getEditStatus.mockReturnValue(false);
const wrapper = mount(EventSelect, {
props: baseProps({
model: { events: [{ name: 'a', actions: [] }] },
}) as any,
});
const panelCfg = wrapper.findComponent({ name: 'MPanel' }).props('config') as any;
const codeAction = panelCfg.items[0].items[3];
expect(codeAction.display(undefined, { model: { actionType: 'code' } })).toBe(true);
expect(codeAction.notEditable()).toBe(true);
codeBlockService.getEditStatus.mockReturnValue(true);
});
test('dataSourceActionConfig display/notEditable', () => {
dataSourceService.get.mockReturnValue(false);
const wrapper = mount(EventSelect, {
props: baseProps({
model: { events: [{ name: 'a', actions: [] }] },
}) as any,
});
const panelCfg = wrapper.findComponent({ name: 'MPanel' }).props('config') as any;
const dsAction = panelCfg.items[0].items[4];
expect(dsAction.display(undefined, { model: { actionType: 'data-source' } })).toBe(true);
expect(dsAction.notEditable()).toBe(true);
});
test('table 配置中 method options', () => {
editorService.getNodeById.mockReturnValue({ type: 'btn' });
const wrapper = mount(EventSelect, {
props: baseProps({
model: { events: [{ name: 'a' }] },
}) as any,
});
const tableCfg = wrapper.findComponent({ name: 'MTable' }).props('config') as any;
const methodCol = tableCfg.items.find((it: any) => it.name === 'method');
const opts = methodCol.options(undefined, { model: { to: '1' } });
expect(opts).toEqual([{ text: 'open', value: 'open' }]);
editorService.getNodeById.mockReturnValue(null);
expect(methodCol.options(undefined, { model: { to: '1' } })).toEqual([]);
});
test('removeEvent 通过 panel header 删除按钮调用', async () => {
const m: any = {
events: [
{ name: 'a', actions: [] },
{ name: 'b', actions: [] },
],
};
const wrapper = mount(EventSelect, { props: baseProps({ model: m }) as any });
const buttons = wrapper.findAll('button');
const deleteBtn = buttons[buttons.length - 1];
await deleteBtn.trigger('click');
expect(m.events.length).toBe(1);
});
});