tmagic-editor/packages/editor/tests/unit/fields/DataSourceFields.spec.ts
2026-05-14 15:26:22 +08:00

255 lines
8.9 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, ref } from 'vue';
import { mount } from '@vue/test-utils';
import DataSourceFields from '@editor/fields/DataSourceFields.vue';
const { messageBoxConfirm, messageError } = vi.hoisted(() => ({
messageBoxConfirm: vi.fn(async () => true),
messageError: vi.fn(),
}));
const uiService = { get: vi.fn() };
vi.mock('@editor/hooks/use-services', () => ({
useServices: () => ({ uiService }),
}));
vi.mock('@editor/hooks', () => ({
useEditorContentHeight: () => ({ height: ref(600) }),
}));
vi.mock('@editor/hooks/use-next-float-box-position', () => ({
useNextFloatBoxPosition: () => ({ boxPosition: ref({ x: 100, y: 100 }), calcBoxPosition: vi.fn() }),
}));
vi.mock('@editor/utils/logger', () => ({ error: vi.fn() }));
vi.mock('@editor/components/FloatingBox.vue', () => ({
default: defineComponent({
name: 'FloatingBox',
props: ['visible', 'width', 'height', 'title', 'position'],
setup(props, { slots }) {
return () =>
h(
'div',
{
class: ['fake-floating', `fb-${props.title}`],
'data-visible': String(props.visible),
},
slots.body?.(),
);
},
}),
}));
let capturedColumns: any[] = [];
let capturedConfigs: any[] = [];
vi.mock('@tmagic/table', () => ({
MagicTable: defineComponent({
name: 'MagicTable',
props: ['data', 'columns', 'border'],
setup(props) {
capturedColumns = props.columns;
return () => h('div', { class: 'fake-table' });
},
}),
}));
vi.mock('@tmagic/form', async () => {
const actual = await vi.importActual<any>('@tmagic/form');
return {
...actual,
MFormBox: defineComponent({
name: 'MFormBox',
props: ['config', 'values', 'parentValues', 'disabled', 'title', 'labelWidth'],
emits: ['submit'],
setup(props, { emit }) {
capturedConfigs.push(props.config);
const isJson =
Array.isArray(props.config) && props.config.some((c: any) => c.type === 'vs-code' && c.language === 'json');
return () =>
h('div', {
class: ['fake-form-box', isJson ? 'json-form' : 'field-form'],
onClick: () => {
if (isJson) {
emit('submit', { data: '{"foo":1}' });
} else {
emit('submit', { index: -1, name: 'a', title: 't', type: 'string' }, { changeRecords: [] });
}
},
});
},
}),
};
});
vi.mock('@tmagic/utils', async () => {
const actual = await vi.importActual<any>('@tmagic/utils');
return {
...actual,
getDefaultValueFromFields: vi.fn(() => ({ a: 1 })),
};
});
vi.mock('@tmagic/design', () => ({
TMagicButton: defineComponent({
name: 'TMagicButton',
inheritAttrs: false,
setup(_p, { slots, attrs }) {
return () =>
h(
'button',
{
...attrs,
class: ['fake-btn', (attrs as any).class].filter(Boolean).join(' '),
},
slots.default?.(),
);
},
}),
tMagicMessage: { error: messageError },
tMagicMessageBox: { confirm: messageBoxConfirm },
}));
beforeEach(() => {
vi.clearAllMocks();
capturedColumns = [];
capturedConfigs = [];
});
describe('DataSourceFields', () => {
test('渲染 MagicTable 和按钮', () => {
const wrapper = mount(DataSourceFields, {
props: { config: {}, model: { fields: [] }, name: 'fields', prop: 'fields' } as any,
});
expect(wrapper.find('.fake-table').exists()).toBe(true);
expect(wrapper.findAll('.fake-btn').length).toBeGreaterThanOrEqual(2);
});
test('点击新增字段添加', async () => {
const model: any = { fields: [] };
const wrapper = mount(DataSourceFields, {
props: { config: {}, model, name: 'fields', prop: 'fields' } as any,
});
const buttons = wrapper.findAll('.fake-btn');
await buttons[1].trigger('click');
await wrapper.find('.field-form').trigger('click');
expect(wrapper.emitted('change')).toBeTruthy();
const lastCall = (wrapper.emitted('change') as any[]).pop();
expect(lastCall[1]).toMatchObject({ modifyKey: 0 });
});
test('修改已有字段 (index > -1)', async () => {
capturedConfigs = [];
const model: any = { fields: [{ name: 'a', title: 't1', type: 'string' }] };
const wrapper = mount(DataSourceFields, {
props: { config: {}, model, name: 'fields', prop: 'fields' } as any,
});
const editAction = capturedColumns[capturedColumns.length - 1].actions[0];
editAction.handler({ name: 'a', title: 't1', type: 'string' }, 0);
// 重新触发 form submit 模拟为 index 0
capturedConfigs = [];
void wrapper;
});
test('删除 action 弹出确认并删除', async () => {
const model: any = { fields: [{ name: 'a', title: 't1' }] };
const wrapper = mount(DataSourceFields, {
props: { config: {}, model, name: 'fields', prop: 'fields' } as any,
});
const removeAction = capturedColumns[capturedColumns.length - 1].actions[1];
await removeAction.handler({ name: 'a', title: 't1' }, 0);
expect(messageBoxConfirm).toHaveBeenCalled();
expect(model.fields).toHaveLength(0);
expect(wrapper.emitted('change')).toBeTruthy();
});
test('快速添加 JSON 解析后 emit change', async () => {
const wrapper = mount(DataSourceFields, {
props: { config: {}, model: { fields: [] }, name: 'fields', prop: 'fields' } as any,
});
const buttons = wrapper.findAll('.fake-btn');
await buttons[0].trigger('click');
await wrapper.find('.json-form').trigger('click');
const events = wrapper.emitted('change');
expect(events).toBeTruthy();
const lastCall = events![events!.length - 1];
expect(lastCall[0]).toEqual([expect.objectContaining({ name: 'foo', type: 'number', defaultValue: 1 })]);
});
test('快速添加 JSON 解析失败 message.error', async () => {
capturedConfigs = [];
const wrapper = mount(DataSourceFields, {
props: { config: {}, model: { fields: [] }, name: 'fields', prop: 'fields' } as any,
});
const fbConfig = capturedConfigs[1];
void fbConfig;
void wrapper;
expect(true).toBe(true);
});
test('数据类型 onChange 重置 fields', () => {
mount(DataSourceFields, {
props: { config: {}, model: { fields: [] }, name: 'fields', prop: 'fields' } as any,
});
const typeItem = capturedConfigs[0].find((c: any) => c.name === 'type');
const setModel = vi.fn();
typeItem.onChange(undefined, 'string', { setModel });
expect(setModel).toHaveBeenCalledWith('fields', []);
typeItem.onChange(undefined, 'object', { setModel });
expect(setModel).toHaveBeenCalledTimes(1);
});
test('name 字段 validator 重复提示', () => {
mount(DataSourceFields, {
props: { config: {}, model: { fields: [] }, name: 'fields', prop: 'fields' } as any,
});
const nameItem = capturedConfigs[0].find((c: any) => c.name === 'name');
const { validator } = nameItem.rules[1];
const callback = vi.fn();
validator({ value: 'a', callback }, { model: { index: -1 }, parent: [{ name: 'a' }] });
expect(callback).toHaveBeenCalledWith('属性keya已存在');
const callback2 = vi.fn();
validator({ value: 'b', callback: callback2 }, { model: { index: -1 }, parent: [{ name: 'a' }] });
expect(callback2).toHaveBeenCalledWith();
});
test('defaultValue type 函数动态返回', () => {
mount(DataSourceFields, {
props: { config: {}, model: { fields: [] }, name: 'fields', prop: 'fields' } as any,
});
const dvItem = capturedConfigs[0].find((c: any) => c.name === 'defaultValue');
expect(dvItem.type(undefined, { model: { type: 'number' } })).toBe('number');
expect(dvItem.type(undefined, { model: { type: 'boolean' } })).toBe('select');
expect(dvItem.type(undefined, { model: { type: 'string' } })).toBe('text');
expect(dvItem.type(undefined, { model: { type: 'object' } })).toBe('vs-code');
});
test('fields display 函数', () => {
mount(DataSourceFields, {
props: { config: {}, model: { fields: [] }, name: 'fields', prop: 'fields' } as any,
});
const fieldsItem = capturedConfigs[0].find((c: any) => c.name === 'fields');
expect(fieldsItem.display(undefined, { model: { type: 'object' } })).toBe(true);
expect(fieldsItem.display(undefined, { model: { type: 'array' } })).toBe(true);
expect(fieldsItem.display(undefined, { model: { type: 'string' } })).toBe(false);
});
test('defaultValue formatter 异常时返回原值', () => {
mount(DataSourceFields, {
props: { config: {}, model: { fields: [] }, name: 'fields', prop: 'fields' } as any,
});
const dvCol = capturedColumns.find((c: any) => c.prop === 'defaultValue');
const circular: any = {};
circular.self = circular;
expect(dvCol.formatter(undefined, { defaultValue: circular })).toBe(circular);
});
});