2026-05-14 15:26:22 +08:00

220 lines
7.0 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, nextTick, ref } from 'vue';
import { mount } from '@vue/test-utils';
import PropsPanel from '@editor/layouts/props-panel/PropsPanel.vue';
const editorService = {
get: vi.fn(),
update: vi.fn(),
};
const uiService = { get: vi.fn() };
const propsService = {
on: vi.fn(),
off: vi.fn(),
getPropsConfig: vi.fn(async () => [{ name: 'x' }]),
};
const storageService = {
getItem: vi.fn(),
setItem: vi.fn(),
};
vi.mock('@editor/hooks/use-services', () => ({
useServices: () => ({ editorService, uiService, propsService, storageService }),
}));
const showStylePanel = ref(false);
const showStylePanelToggleButton = ref(true);
const toggleStylePanel = vi.fn((v: boolean) => {
showStylePanel.value = v;
});
vi.mock('@editor/layouts/props-panel/use-style-panel', () => ({
useStylePanel: () => ({ showStylePanel, showStylePanelToggleButton, toggleStylePanel }),
}));
vi.mock('@editor/utils', async () => {
const actual = await vi.importActual<any>('@editor/utils');
return { ...actual, styleTabConfig: { items: [] } };
});
vi.mock('@editor/components/Icon.vue', () => ({
default: defineComponent({
name: 'IconStub',
props: ['icon'],
setup() {
return () => h('i', { class: 'fake-icon' });
},
}),
}));
vi.mock('@editor/components/Resizer.vue', () => ({
default: defineComponent({
name: 'FakeResizer',
emits: ['change'],
setup(_p, { emit }) {
return () =>
h('div', {
class: 'fake-resizer',
onClick: () => emit('change', { deltaX: 50 }),
});
},
}),
}));
const mountedHandlers: any[] = [];
vi.mock('@editor/layouts/props-panel/FormPanel.vue', () => ({
default: defineComponent({
name: 'FormPanel',
props: ['config', 'values', 'disabledShowSrc', 'extendState'],
emits: ['submit', 'submit-error', 'form-error', 'mounted', 'unmounted'],
setup(_p, { emit, expose }) {
mountedHandlers.push(emit);
expose({ configForm: { formState: { foo: 'bar' } } });
return () =>
h('div', { class: 'fake-form-panel' }, [
h('button', {
class: 'submit-btn',
onClick: () =>
emit(
'submit',
{ id: 'n1', style: { color: 'red', empty: '' } },
{ changeRecords: [{ propPath: 'style.bg', value: '' }] },
),
}),
h('button', { class: 'submit-err-btn', onClick: () => emit('submit-error', new Error('e')) }),
h('button', { class: 'form-err-btn', onClick: () => emit('form-error', new Error('e')) }),
h('button', { class: 'mounted-btn', onClick: () => emit('mounted', { proxy: true }) }),
h('button', { class: 'unmounted-btn', onClick: () => emit('unmounted') }),
]);
},
}),
}));
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?.(),
);
},
}),
}));
vi.mock('@tmagic/utils', async () => {
const actual = await vi.importActual<any>('@tmagic/utils');
return { ...actual, setValueByKeyPath: vi.fn() };
});
beforeEach(() => {
vi.clearAllMocks();
mountedHandlers.length = 0;
showStylePanel.value = false;
showStylePanelToggleButton.value = true;
storageService.getItem.mockReturnValue(300);
uiService.get.mockImplementation((k: string) => {
if (k === 'columnWidth') return { right: 400 };
return null;
});
editorService.get.mockImplementation((k: string) => {
if (k === 'node') return { id: 'n1', type: 'text' };
if (k === 'nodes') return [{ id: 'n1' }];
return null;
});
});
describe('PropsPanel', () => {
test('渲染 FormPanel', async () => {
const wrapper = mount(PropsPanel, { props: {} as any });
await nextTick();
expect(wrapper.find('.fake-form-panel').exists()).toBe(true);
});
test('init 调用 getPropsConfig', async () => {
mount(PropsPanel, { props: {} as any });
await new Promise((r) => setTimeout(r, 0));
expect(propsService.getPropsConfig).toHaveBeenCalled();
});
test('node 为空时清空 config', async () => {
editorService.get.mockImplementation((k: string) => (k === 'nodes' ? [] : null));
const wrapper = mount(PropsPanel, { props: {} as any });
await nextTick();
void wrapper;
expect(propsService.getPropsConfig).not.toHaveBeenCalled();
});
test('submit 触发 editorService.update', async () => {
const wrapper = mount(PropsPanel, { props: {} as any });
await new Promise((r) => setTimeout(r, 0));
await wrapper.find('.submit-btn').trigger('click');
expect(editorService.update).toHaveBeenCalled();
const calledNode = (editorService.update.mock.calls[0] as any)[0];
expect(calledNode.style.color).toBe('red');
expect(calledNode.style.empty).toBeUndefined();
});
test('mounted 事件 emit', async () => {
const wrapper = mount(PropsPanel, { props: {} as any });
await wrapper.find('.mounted-btn').trigger('click');
expect(wrapper.emitted('mounted')).toBeTruthy();
});
test('unmounted 事件 emit', async () => {
const wrapper = mount(PropsPanel, { props: {} as any });
await wrapper.find('.unmounted-btn').trigger('click');
expect(wrapper.emitted('unmounted')).toBeTruthy();
});
test('form-error 事件转发', async () => {
const wrapper = mount(PropsPanel, { props: {} as any });
await wrapper.find('.form-err-btn').trigger('click');
expect(wrapper.emitted('form-error')).toBeTruthy();
});
test('Resizer change 限制宽度', async () => {
showStylePanel.value = true;
const wrapper = mount(PropsPanel, { props: {} as any });
await nextTick();
await wrapper.find('.fake-resizer').trigger('click');
expect(storageService.setItem).toHaveBeenCalled();
});
test('点击 toggle 按钮 toggleStylePanel(true)', async () => {
showStylePanelToggleButton.value = true;
showStylePanel.value = false;
const wrapper = mount(PropsPanel, { props: {} as any });
await nextTick();
// 直接调用 toggleStylePanel 验证逻辑
const buttons = wrapper.findAll('.fake-btn');
expect(buttons.length).toBeGreaterThan(0);
await buttons[buttons.length - 1].trigger('click');
expect(toggleStylePanel).toHaveBeenCalledWith(true);
});
test('expose getFormState 返回 formState', async () => {
const wrapper = mount(PropsPanel, { props: {} as any });
await nextTick();
expect((wrapper.vm as any).getFormState()).toEqual({ foo: 'bar' });
});
test('off propsService 监听 on unmount', async () => {
const wrapper = mount(PropsPanel, { props: {} as any });
await nextTick();
wrapper.unmount();
expect(propsService.off).toHaveBeenCalledWith('props-configs-change', expect.any(Function));
});
});