mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-05-30 12:18:10 +00:00
- form: MForm/Container 新增 showDiff prop,允许调用方自定义
'是否展示对比内容' 的判断逻辑,并在嵌套 Container 中自动透传;
不传时沿用默认的 isEqual 行为
- editor: CompareForm 利用该能力处理 code-select 字段中 '' 与
{ hookType: 'code', hookData: [] } 两种语义为空形态被 isEqual 误判为差异的问题
- docs: 补充 form-props.md 中 showDiff 的说明与示例
- test: 补充 Code 字段相关单测
248 lines
9.2 KiB
TypeScript
248 lines
9.2 KiB
TypeScript
/*
|
||
* 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 Code from '@editor/fields/Code.vue';
|
||
|
||
// 用一个简单的桩组件代替 MagicCodeEditor,把所有 props 原样渲染到 data-* 属性上,
|
||
// 这样可以直接断言父组件 Code.vue 透传过去的内容是否正确。
|
||
vi.mock('@editor/layouts/CodeEditor.vue', () => ({
|
||
default: defineComponent({
|
||
name: 'CodeEditor',
|
||
props: {
|
||
height: { type: [String, Number], default: undefined },
|
||
type: { type: String, default: undefined },
|
||
initValues: { type: null, default: undefined },
|
||
modifiedValues: { type: null, default: undefined },
|
||
language: { type: String, default: undefined },
|
||
options: { type: Object, default: undefined },
|
||
autosize: { type: Object, default: undefined },
|
||
parse: { type: Boolean, default: undefined },
|
||
editorCustomType: { type: String, default: undefined },
|
||
},
|
||
emits: ['save'],
|
||
setup(p, { emit }) {
|
||
return () =>
|
||
h('div', {
|
||
class: 'fake-code-editor',
|
||
'data-height': p.height,
|
||
'data-type': p.type ?? '',
|
||
'data-language': p.language ?? '',
|
||
'data-init': JSON.stringify(p.initValues ?? null),
|
||
'data-modified': JSON.stringify(p.modifiedValues ?? null),
|
||
'data-options': JSON.stringify(p.options ?? null),
|
||
'data-autosize': JSON.stringify(p.autosize ?? null),
|
||
'data-parse': String(p.parse ?? ''),
|
||
'data-custom-type': p.editorCustomType ?? '',
|
||
onClick: () => emit('save', 'newvalue'),
|
||
});
|
||
},
|
||
}),
|
||
}));
|
||
|
||
const mountCode = (props: Record<string, any>) =>
|
||
mount(Code, {
|
||
props: {
|
||
// FieldProps 必填字段,用 as any 绕过测试中类型严格匹配
|
||
config: { height: '100px', language: 'javascript' },
|
||
model: { codeField: 'oldval' },
|
||
name: 'codeField',
|
||
prop: 'codeField',
|
||
...props,
|
||
} as any,
|
||
});
|
||
|
||
const getEl = (wrapper: ReturnType<typeof mountCode>) => wrapper.find('.fake-code-editor').element as HTMLElement;
|
||
|
||
const readJson = (el: HTMLElement, attr: string) => JSON.parse(el.getAttribute(attr) || 'null');
|
||
|
||
describe('Code', () => {
|
||
beforeEach(() => {
|
||
vi.clearAllMocks();
|
||
});
|
||
|
||
describe('基本透传与事件', () => {
|
||
test('save 触发 change 事件,参数原样透传 (字符串)', async () => {
|
||
const wrapper = mountCode({});
|
||
await wrapper.find('.fake-code-editor').trigger('click');
|
||
expect(wrapper.emitted('change')?.[0]?.[0]).toBe('newvalue');
|
||
});
|
||
|
||
test('save 触发 change 事件,参数可以是对象', async () => {
|
||
// 替换桩的 emit 内容:通过自定义子组件方式覆盖默认 emit value 时太复杂,
|
||
// 这里直接以 vm.$emit 等价的方式构造数据:通过 wrapper 触发 onClick 是字符串,
|
||
// 但 setup 内 save 函数本身也接受任意类型,因此用一个最小用例验证函数行为:
|
||
const wrapper = mountCode({});
|
||
// 直接调用底层桩组件 emit,模拟 save 抛出对象
|
||
const child = wrapper.findComponent({ name: 'CodeEditor' });
|
||
child.vm.$emit('save', { a: 1 });
|
||
expect(wrapper.emitted('change')?.[0]?.[0]).toEqual({ a: 1 });
|
||
});
|
||
|
||
test('透传 height / language / autosize / parse / editorCustomType', () => {
|
||
const wrapper = mountCode({
|
||
config: {
|
||
height: '200px',
|
||
language: 'json',
|
||
autosize: { minRows: 2, maxRows: 8 },
|
||
parse: true,
|
||
mFormItemType: 'vs-code-extra',
|
||
options: { tabSize: 4 },
|
||
},
|
||
});
|
||
const el = getEl(wrapper);
|
||
expect(el.getAttribute('data-height')).toBe('200px');
|
||
expect(el.getAttribute('data-language')).toBe('json');
|
||
expect(readJson(el, 'data-autosize')).toEqual({ minRows: 2, maxRows: 8 });
|
||
expect(el.getAttribute('data-parse')).toBe('true');
|
||
expect(el.getAttribute('data-custom-type')).toBe('vs-code-extra');
|
||
});
|
||
|
||
test('组件名为 MFieldsVsCode', () => {
|
||
const wrapper = mountCode({});
|
||
expect((wrapper.vm.$options as any).name).toBe('MFieldsVsCode');
|
||
});
|
||
});
|
||
|
||
describe('非 diff 模式 (非对比)', () => {
|
||
test('init-values 来自 model[name],modified-values 为 null/undefined', () => {
|
||
const wrapper = mountCode({
|
||
model: { codeField: 'hello' },
|
||
});
|
||
const el = getEl(wrapper);
|
||
expect(el.getAttribute('data-type')).toBe('');
|
||
expect(readJson(el, 'data-init')).toBe('hello');
|
||
expect(readJson(el, 'data-modified')).toBe(null);
|
||
});
|
||
|
||
test('disabled=true 时 options.readOnly=true', () => {
|
||
const wrapper = mountCode({ disabled: true });
|
||
const el = getEl(wrapper);
|
||
expect(readJson(el, 'data-options')).toMatchObject({ readOnly: true });
|
||
});
|
||
|
||
test('disabled=false 时 options.readOnly=false', () => {
|
||
const wrapper = mountCode({ disabled: false });
|
||
const el = getEl(wrapper);
|
||
expect(readJson(el, 'data-options')).toMatchObject({ readOnly: false });
|
||
});
|
||
|
||
test('readOnly 应覆盖 config.options 中已有的 readOnly 字段', () => {
|
||
const wrapper = mountCode({
|
||
// 故意把 config.options.readOnly 设为 true,期望 disabled=false 时仍以 disabled 为准
|
||
config: { height: '100px', language: 'javascript', options: { tabSize: 4, readOnly: true } },
|
||
disabled: false,
|
||
});
|
||
const el = getEl(wrapper);
|
||
const opts = readJson(el, 'data-options');
|
||
expect(opts.tabSize).toBe(4);
|
||
expect(opts.readOnly).toBe(false);
|
||
});
|
||
|
||
test('isCompare=true 但缺少 lastValues 时不进入 diff', () => {
|
||
const wrapper = mountCode({
|
||
isCompare: true,
|
||
// 不传 lastValues
|
||
model: { codeField: 'cur' },
|
||
});
|
||
const el = getEl(wrapper);
|
||
expect(el.getAttribute('data-type')).toBe('');
|
||
expect(readJson(el, 'data-init')).toBe('cur');
|
||
expect(readJson(el, 'data-modified')).toBe(null);
|
||
});
|
||
|
||
test('isCompare=false 即使带 lastValues 也不进入 diff', () => {
|
||
const wrapper = mountCode({
|
||
isCompare: false,
|
||
lastValues: { codeField: 'old' },
|
||
model: { codeField: 'cur' },
|
||
});
|
||
const el = getEl(wrapper);
|
||
expect(el.getAttribute('data-type')).toBe('');
|
||
expect(readJson(el, 'data-init')).toBe('cur');
|
||
});
|
||
});
|
||
|
||
describe('diff 模式 (对比)', () => {
|
||
test('isCompare=true 且有 lastValues 时切换为 diff 模式', () => {
|
||
const wrapper = mountCode({
|
||
isCompare: true,
|
||
lastValues: { codeField: 'old' },
|
||
model: { codeField: 'new' },
|
||
});
|
||
const el = getEl(wrapper);
|
||
expect(el.getAttribute('data-type')).toBe('diff');
|
||
expect(readJson(el, 'data-init')).toBe('old');
|
||
expect(readJson(el, 'data-modified')).toBe('new');
|
||
});
|
||
|
||
test('diff 模式下 readOnly 强制为 true,忽略 disabled', () => {
|
||
const wrapper = mountCode({
|
||
isCompare: true,
|
||
lastValues: { codeField: 'old' },
|
||
model: { codeField: 'new' },
|
||
disabled: false,
|
||
});
|
||
const el = getEl(wrapper);
|
||
expect(readJson(el, 'data-options')).toMatchObject({ readOnly: true });
|
||
});
|
||
|
||
test('diff 模式下当 lastValues 中无对应 name 字段时,init-values 退化为 null/{}', () => {
|
||
const wrapper = mountCode({
|
||
isCompare: true,
|
||
// 有 lastValues 对象但没有该字段
|
||
lastValues: {},
|
||
model: { codeField: 'new' },
|
||
});
|
||
const el = getEl(wrapper);
|
||
expect(el.getAttribute('data-type')).toBe('diff');
|
||
// 源码逻辑:(lastValues || {})[name],此处 lastValues 是 {},结果为 undefined
|
||
expect(readJson(el, 'data-init')).toBe(null);
|
||
expect(readJson(el, 'data-modified')).toBe('new');
|
||
});
|
||
|
||
test('切换 isCompare 时模式跟随变化', async () => {
|
||
const wrapper = mountCode({
|
||
isCompare: false,
|
||
lastValues: { codeField: 'old' },
|
||
model: { codeField: 'new' },
|
||
});
|
||
expect(getEl(wrapper).getAttribute('data-type')).toBe('');
|
||
|
||
await wrapper.setProps({ isCompare: true } as any);
|
||
expect(getEl(wrapper).getAttribute('data-type')).toBe('diff');
|
||
expect(readJson(getEl(wrapper), 'data-init')).toBe('old');
|
||
expect(readJson(getEl(wrapper), 'data-modified')).toBe('new');
|
||
});
|
||
|
||
test('切换 lastValues 后 init-values 同步更新', async () => {
|
||
const wrapper = mountCode({
|
||
isCompare: true,
|
||
lastValues: { codeField: 'v1' },
|
||
model: { codeField: 'cur' },
|
||
});
|
||
expect(readJson(getEl(wrapper), 'data-init')).toBe('v1');
|
||
|
||
await wrapper.setProps({ lastValues: { codeField: 'v2' } } as any);
|
||
expect(readJson(getEl(wrapper), 'data-init')).toBe('v2');
|
||
});
|
||
|
||
test('diff 模式下 model 变化时 modified-values 同步更新', async () => {
|
||
const wrapper = mountCode({
|
||
isCompare: true,
|
||
lastValues: { codeField: 'old' },
|
||
model: { codeField: 'a' },
|
||
});
|
||
expect(readJson(getEl(wrapper), 'data-modified')).toBe('a');
|
||
|
||
await wrapper.setProps({ model: { codeField: 'b' } } as any);
|
||
expect(readJson(getEl(wrapper), 'data-modified')).toBe('b');
|
||
});
|
||
});
|
||
});
|