roymondchen 2ad5101471 fix(editor): 修复 StyleSetter 嵌套场景下 propPath 丢失上下文路径的问题
当 prop 与 name 不一致(如 data.items.0.style)时,原实现固定使用 name 会丢失上下文路径,
改为优先使用 prop,回退到 name,确保 changeRecords 携带完整路径。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-14 19:17:48 +08:00

135 lines
4.5 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 StyleSetter from '@editor/fields/StyleSetter/Index.vue';
vi.mock('@tmagic/design', () => ({
TMagicCollapse: defineComponent({
name: 'TMagicCollapse',
props: ['modelValue'],
setup(_props, { slots }) {
return () => h('div', { class: 'collapse' }, slots.default?.());
},
}),
TMagicCollapseItem: defineComponent({
name: 'TMagicCollapseItem',
props: ['name'],
setup(_props, { slots }) {
return () => h('div', { class: 'collapse-item' }, [slots.title?.(), slots.default?.()]);
},
}),
}));
vi.mock('@editor/components/Icon.vue', () => ({
default: defineComponent({ name: 'MIcon', props: ['icon'], setup: () => () => h('i') }),
}));
vi.mock('@editor/fields/StyleSetter/pro/index', () => {
const make = (name: string) =>
defineComponent({
name,
props: ['values', 'size', 'disabled'],
emits: ['change'],
setup(_p, { emit }) {
return () =>
h('div', {
class: name,
onClick: () => emit('change', { foo: 1 }, { changeRecords: [{ propPath: 'foo', value: 1 }] }),
});
},
});
return {
Layout: make('Layout'),
Position: make('Position'),
Background: make('Background'),
Font: make('Font'),
Border: make('Border'),
Transform: make('Transform'),
};
});
describe('StyleSetter Index', () => {
test('渲染 6 个 collapse-item', () => {
const wrapper = mount(StyleSetter, {
props: { model: { style: {} }, name: 'style', prop: 'style' } as any,
});
expect(wrapper.findAll('.collapse-item').length).toBe(6);
});
test('change 时为 propPath 添加 prop 前缀', async () => {
const wrapper = mount(StyleSetter, {
props: { model: { style: {} }, name: 'style', prop: 'style' } as any,
});
await wrapper.find('.Layout').trigger('click');
const events = wrapper.emitted('change');
expect(events).toBeTruthy();
expect((events?.[0]?.[1] as any).changeRecords[0].propPath).toBe('style.foo');
});
test('prop 与 name 不一致时propPath 使用完整的 prop 路径而非 name', async () => {
const wrapper = mount(StyleSetter, {
props: { model: { style: {} }, name: 'style', prop: 'data.items.0.style' } as any,
});
await wrapper.find('.Position').trigger('click');
const events = wrapper.emitted('change');
expect(events).toBeTruthy();
expect((events?.[0]?.[1] as any).changeRecords[0].propPath).toBe('data.items.0.style.foo');
});
test('change 透传原始 value 并保留 changeRecords 其他字段', async () => {
const wrapper = mount(StyleSetter, {
props: { model: { style: {} }, name: 'style', prop: 'style' } as any,
});
await wrapper.find('.Background').trigger('click');
const events = wrapper.emitted('change');
expect(events).toBeTruthy();
const [value, eventData] = events![0] as any[];
expect(value).toEqual({ foo: 1 });
expect(eventData.changeRecords).toHaveLength(1);
expect(eventData.changeRecords[0]).toEqual({ propPath: 'style.foo', value: 1 });
});
test('eventData 无 changeRecords 时也能正常 emit', async () => {
const wrapper = mount(StyleSetter, {
props: { model: { style: {} }, name: 'style', prop: 'style' } as any,
global: {
stubs: {
Layout: defineComponent({
name: 'LayoutStub',
emits: ['change'],
setup(_p, { emit }) {
return () => h('div', { class: 'Layout-no-records', onClick: () => emit('change', { foo: 2 }, {}) });
},
}),
},
},
});
await wrapper.find('.Layout-no-records').trigger('click');
const events = wrapper.emitted('change');
expect(events).toBeTruthy();
expect((events?.[0]?.[1] as any).changeRecords).toBeUndefined();
});
test('values/size/disabled 正确透传到子组件', () => {
const wrapper = mount(StyleSetter, {
props: {
model: { style: { color: 'red' } },
name: 'style',
prop: 'style',
size: 'small',
disabled: true,
} as any,
});
const layout = wrapper.findComponent({ name: 'Layout' });
expect(layout.props('values')).toEqual({ color: 'red' });
expect(layout.props('size')).toBe('small');
expect(layout.props('disabled')).toBe(true);
});
});