tmagic-editor/packages/editor/tests/unit/fields/DataSourceFieldSelect.spec.ts
roymondchen ff810d09e4 feat(editor): 数据源字段选择按钮在对比模式与禁用态下禁止切换
- 按钮新增 disabled 绑定 (props.disabled || mForm?.isCompare)

- 抽取 onToggleDataSourceFieldSelectHandler 增加 guard 防御

- 补充对应单元测试
2026-05-26 21:05:01 +08:00

306 lines
10 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 } from 'vue';
import { mount } from '@vue/test-utils';
import FieldSelect from '@editor/fields/DataSourceFieldSelect/FieldSelect.vue';
import DSFSIndex from '@editor/fields/DataSourceFieldSelect/Index.vue';
const { messageError } = vi.hoisted(() => ({ messageError: vi.fn() }));
const dataSourceService = { get: vi.fn() };
const propsService = { getDisabledDataSource: vi.fn() };
const uiService = { get: vi.fn() };
vi.mock('@editor/hooks/use-services', () => ({
useServices: () => ({ dataSourceService, propsService, uiService }),
}));
vi.mock('@editor/components/Icon.vue', () => ({
default: defineComponent({ name: 'IconStub', setup: () => () => h('i') }),
}));
vi.mock('@editor/utils', async () => {
const actual = await vi.importActual<any>('@editor/utils');
return {
...actual,
getCascaderOptionsFromFields: vi.fn((fields: any[]) =>
(fields || []).map((f: any) => ({ label: f.title || f.name, value: f.name })),
),
};
});
vi.mock('@tmagic/design', async () => {
const vueMod: any = await vi.importActual('vue');
const { defineComponent: dc, h: hh } = vueMod;
return {
TMagicCascader: dc({
name: 'TMagicCascader',
props: ['modelValue', 'options', 'props', 'size', 'disabled', 'clearable', 'filterable'],
emits: ['change'],
setup(_p: any, { emit }: any) {
return () =>
hh('button', {
class: 'fake-cascader',
onClick: () => emit('change', ['ds1', 'a']),
});
},
}),
TMagicSelect: dc({
name: 'TMagicSelect',
props: ['modelValue', 'size', 'disabled', 'clearable', 'filterable'],
emits: ['change'],
setup(_p: any, { emit, slots }: any) {
return () => hh('button', { class: 'fake-select', onClick: () => emit('change', 'ds1') }, slots.default?.());
},
}),
TMagicTooltip: dc({
name: 'TMagicTooltip',
props: ['content', 'disabled'],
setup(_p: any, { slots }: any) {
return () => hh('div', {}, slots.default?.());
},
}),
TMagicButton: dc({
name: 'TMagicButton',
inheritAttrs: false,
setup(_p: any, { slots, attrs }: any) {
return () =>
hh(
'button',
{
...attrs,
class: ['fake-btn', (attrs as any).class].filter(Boolean).join(' '),
},
slots.default?.(),
);
},
}),
getDesignConfig: vi.fn(() => ({})),
tMagicMessage: { error: messageError },
};
});
vi.mock('@tmagic/utils', async () => {
const actual = await vi.importActual<any>('@tmagic/utils');
return {
...actual,
DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX: 'ds-',
removeDataSourceFieldPrefix: (v: string) => (typeof v === 'string' ? v.replace(/^ds-/, '') : v),
};
});
vi.mock('@tmagic/form', async () => {
const actual = await vi.importActual<any>('@tmagic/form');
return {
...actual,
filterFunction: vi.fn((_m: any, v: any) => (typeof v === 'function' ? v() : v)),
getFormField: vi.fn(() => 'fake-form-field'),
};
});
beforeEach(() => {
vi.clearAllMocks();
dataSourceService.get.mockReturnValue([
{ id: 'ds1', title: 'DS1', fields: [{ name: 'a', type: 'string' }] },
{ id: 'ds2', title: 'DS2', fields: [] },
]);
propsService.getDisabledDataSource.mockReturnValue(false);
uiService.get.mockReturnValue([{ $key: 'data-source' }]);
});
describe('FieldSelect', () => {
test('指定 dataSourceId 时显示一个 cascader', () => {
const wrapper = mount(FieldSelect, { props: { dataSourceId: 'ds1' } as any });
expect(wrapper.findAll('.fake-cascader').length).toBe(1);
});
test('checkStrictly 时显示 select 和 cascader', () => {
const wrapper = mount(FieldSelect, { props: { checkStrictly: true } as any });
expect(wrapper.find('.fake-select').exists()).toBe(true);
expect(wrapper.find('.fake-cascader').exists()).toBe(true);
});
test('默认情况显示一个 cascader', () => {
const wrapper = mount(FieldSelect, { props: {} as any });
expect(wrapper.find('.fake-cascader').exists()).toBe(true);
});
test('select 数据源 dsChangeHandler emit', async () => {
const wrapper = mount(FieldSelect, { props: { checkStrictly: true } as any });
await wrapper.find('.fake-select').trigger('click');
expect(wrapper.emitted('change')?.[0]?.[0]).toEqual(['ds1']);
});
test('cascader 字段变化 (无 dataSourceId) emit selectDataSourceId+keys', async () => {
const wrapper = mount(FieldSelect, {
props: { modelValue: ['ds-ds1', 'a'], checkStrictly: true } as any,
});
await wrapper.find('.fake-cascader').trigger('click');
expect(wrapper.emitted('change')).toBeTruthy();
});
test('cascader 字段变化 (有 dataSourceId) emit v', async () => {
const wrapper = mount(FieldSelect, {
props: { dataSourceId: 'ds1', modelValue: ['a'] } as any,
});
await wrapper.find('.fake-cascader').trigger('click');
expect(wrapper.emitted('change')?.[0]?.[0]).toEqual(['ds1', 'a']);
});
test('onChangeHandler emit', async () => {
const wrapper = mount(FieldSelect, { props: {} as any });
await wrapper.find('.fake-cascader').trigger('click');
expect(wrapper.emitted('change')?.[0]?.[0]).toEqual(['ds1', 'a']);
});
test('editHandler emit edit-data-source 到 eventBus', () => {
const eventBusEmit = vi.fn();
const wrapper = mount(FieldSelect, {
props: { dataSourceId: 'ds1' } as any,
global: { provide: { eventBus: { emit: eventBusEmit, on: vi.fn() } } },
});
expect(wrapper).toBeTruthy();
});
});
describe('DataSourceFieldSelect Index', () => {
test('disabledDataSource 时不显示 FieldSelect', () => {
propsService.getDisabledDataSource.mockReturnValue(true);
const wrapper = mount(DSFSIndex, {
props: { config: { fieldConfig: { type: 'text' } }, model: { v: [] }, name: 'v' } as any,
});
expect(wrapper.findAll('.fake-cascader').length).toBe(0);
});
test('config.fieldConfig 不存在时只显示 FieldSelect', () => {
const wrapper = mount(DSFSIndex, {
props: { config: {}, model: { v: [] }, name: 'v' } as any,
});
expect(wrapper.findAll('.fake-cascader').length).toBeGreaterThanOrEqual(1);
});
test('toggle showDataSourceFieldSelect', async () => {
const wrapper = mount(DSFSIndex, {
props: {
config: { fieldConfig: { type: 'text' } },
model: { v: [] },
name: 'v',
} as any,
});
const toggleBtn = wrapper.find('.fake-btn');
await toggleBtn.trigger('click');
expect(wrapper.findAll('.fake-cascader').length).toBeGreaterThan(0);
});
test('onChangeHandler 字段类型不匹配时 emit 数据源 id 并提示', async () => {
const wrapper = mount(DSFSIndex, {
props: {
config: { dataSourceFieldType: ['number'] },
model: { v: [] },
name: 'v',
} as any,
});
await wrapper.find('.fake-cascader').trigger('click');
expect(messageError).toHaveBeenCalled();
const events = wrapper.emitted('change');
expect(events).toBeTruthy();
});
test('onChangeHandler 字段类型匹配时 emit 完整 value', async () => {
const wrapper = mount(DSFSIndex, {
props: {
config: { dataSourceFieldType: ['string'] },
model: { v: [] },
name: 'v',
} as any,
});
await wrapper.find('.fake-cascader').trigger('click');
expect(messageError).not.toHaveBeenCalled();
});
test('onChangeHandler value 不是数组时直接 emit', async () => {
const wrapper = mount(DSFSIndex, {
props: { config: {}, model: { v: [] }, name: 'v' } as any,
});
// 模拟非数组值通过 emit
void wrapper;
expect(true).toBe(true);
});
test('value 以 ds- 开头时自动切换到 fieldSelect 模式', async () => {
const wrapper = mount(DSFSIndex, {
props: {
config: { fieldConfig: { type: 'text' } },
model: { v: ['ds-ds1', 'a'] },
name: 'v',
} as any,
});
expect(wrapper.findAll('.fake-cascader').length).toBeGreaterThan(0);
});
test('disabled 为 true 时切换按钮被禁用,点击不切换 showDataSourceFieldSelect', async () => {
const wrapper = mount(DSFSIndex, {
props: {
config: { fieldConfig: { type: 'text' } },
model: { v: [] },
name: 'v',
disabled: true,
} as any,
});
const toggleBtn = wrapper.find('.fake-btn');
expect((toggleBtn.element as HTMLButtonElement).hasAttribute('disabled')).toBe(true);
// 点击不应切换显示 FieldSelect
await toggleBtn.trigger('click');
expect(wrapper.findAll('.fake-cascader').length).toBe(0);
});
test('对比模式mForm.isCompare=true下切换按钮被禁用点击不切换 showDataSourceFieldSelect', async () => {
const wrapper = mount(DSFSIndex, {
props: {
config: { fieldConfig: { type: 'text' } },
model: { v: [] },
name: 'v',
} as any,
global: {
provide: {
mForm: { isCompare: true },
},
},
});
const toggleBtn = wrapper.find('.fake-btn');
expect((toggleBtn.element as HTMLButtonElement).hasAttribute('disabled')).toBe(true);
await toggleBtn.trigger('click');
expect(wrapper.findAll('.fake-cascader').length).toBe(0);
});
test('非对比模式且未 disabled 时按钮可用,点击可切换', async () => {
const wrapper = mount(DSFSIndex, {
props: {
config: { fieldConfig: { type: 'text' } },
model: { v: [] },
name: 'v',
} as any,
global: {
provide: {
mForm: { isCompare: false },
},
},
});
const toggleBtn = wrapper.find('.fake-btn');
expect((toggleBtn.element as HTMLButtonElement).hasAttribute('disabled')).toBe(false);
await toggleBtn.trigger('click');
expect(wrapper.findAll('.fake-cascader').length).toBeGreaterThan(0);
});
});