roymondchen cbc4b25072 feat(editor): 字段对比模式逐项展示差异并补充历史记录面板文档
- CodeSelect/CodeSelectCol/EventSelect/DataSource 等复合字段在对比模式下
  按索引对齐前后值,逐项展示新增/删除/修改高亮,并隐藏写操作按钮
- form 容器/列表/表格支持对比模式只读展示
- 新增「历史记录面板」指南文档,完善表单对比文档及 menu props 说明
- 补充相关单元测试

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 15:51:47 +08:00

131 lines
3.8 KiB
TypeScript

/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2025 Tencent.
*/
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { defineComponent, h, nextTick } from 'vue';
import { mount } from '@vue/test-utils';
import CodeParams from '@editor/components/CodeParams.vue';
import * as utilsMod from '@editor/utils';
const submitMock = vi.fn();
let lastConfig: any;
let lastProps: any;
vi.mock('@tmagic/form', () => ({
MForm: defineComponent({
name: 'MFormStub',
props: ['config', 'initValues', 'disabled', 'size', 'watchProps', 'lastValues', 'isCompare'],
emits: ['change'],
setup(props, { expose, emit }) {
lastConfig = props.config;
lastProps = props;
expose({ submitForm: submitMock });
return () =>
h('div', {
class: 'form-stub',
onClick: () => emit('change', { ok: true }, { changeRecords: [] }),
});
},
}),
}));
vi.mock('@editor/utils', () => ({
error: vi.fn(),
}));
describe('CodeParams.vue', () => {
beforeEach(() => {
submitMock.mockReset();
lastConfig = null;
lastProps = null;
});
afterEach(() => {
vi.clearAllMocks();
});
test('config 中包含 vs-code 类型时直接保留', () => {
mount(CodeParams as any, {
props: {
model: { p: {} },
name: 'p',
paramsConfig: [{ name: 'a', text: 'A', type: 'vs-code' }] as any,
},
});
expect(lastConfig[0].items[0].type).toBe('vs-code');
});
test('config 中其它类型会包装成 data-source-field-select', () => {
mount(CodeParams as any, {
props: {
model: { p: {} },
name: 'p',
paramsConfig: [{ name: 'a', text: 'A', type: 'text' }] as any,
},
});
expect(lastConfig[0].items[0].type).toBe('data-source-field-select');
expect(lastConfig[0].items[0].fieldConfig.type).toBe('text');
});
test('config.type 为函数时执行函数判断类型', () => {
const typeFn = vi.fn(() => 'vs-code');
mount(CodeParams as any, {
props: {
model: { p: { x: 1 } },
name: 'p',
paramsConfig: [{ name: 'a', text: 'A', type: typeFn }] as any,
},
});
expect(typeFn).toHaveBeenCalledWith(undefined, { model: { x: 1 } });
expect(lastConfig[0].items[0].name).toBe('a');
});
test('change 事件成功时 emit change 携带值', async () => {
submitMock.mockResolvedValueOnce({ p: { a: 1 } });
const wrapper = mount(CodeParams as any, {
props: {
model: { p: {} },
name: 'p',
paramsConfig: [{ name: 'a', text: 'A', type: 'vs-code' }] as any,
},
});
await wrapper.find('.form-stub').trigger('click');
await nextTick();
await new Promise((r) => setTimeout(r, 0));
const events = wrapper.emitted('change') as any[];
expect(events?.[0]?.[0]).toEqual({ p: { a: 1 } });
});
test('对比模式 isCompare/lastValues 透传给内部 MForm', () => {
mount(CodeParams as any, {
props: {
model: { p: { a: 'new' } },
name: 'p',
isCompare: true,
lastValues: { p: { a: 'old' } },
paramsConfig: [{ name: 'a', text: 'A', type: 'vs-code' }] as any,
},
});
expect(lastProps.isCompare).toBe(true);
expect(lastProps.lastValues).toEqual({ p: { a: 'old' } });
});
test('submitForm 抛错时调用 error 不抛出', async () => {
submitMock.mockRejectedValueOnce(new Error('bad'));
const wrapper = mount(CodeParams as any, {
props: {
model: { p: {} },
name: 'p',
paramsConfig: [{ name: 'a', text: 'A', type: 'vs-code' }] as any,
},
});
await wrapper.find('.form-stub').trigger('click');
await nextTick();
await new Promise((r) => setTimeout(r, 0));
expect((utilsMod as any).error).toHaveBeenCalled();
});
});