roymondchen df8790042f feat(editor): 导航菜单支持菜单项溢出收纳,新增 NavMenuColumn 组件
- 抽离每列渲染逻辑为 NavMenuColumn 组件,监听容器宽度
- 容器空间不足时自动隐藏溢出项,并通过更多按钮 Popover 展开
- ToolButton 暴露根元素引用,便于父级测量宽度
- design ButtonProps 新增 bg 属性,用于更多按钮的激活态样式
- 补充 NavMenuColumn / NavMenu / ToolButton 的单元测试

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 19:33:53 +08:00

225 lines
6.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 { mount } from '@vue/test-utils';
import ToolButton from '@editor/components/ToolButton.vue';
const provideServices = () => ({
global: {
provide: {
services: {
editorService: {},
uiService: {},
},
},
},
});
describe('ToolButton.vue', () => {
test('display 为 false 时不渲染', () => {
const wrapper = mount(ToolButton as any, {
...provideServices(),
props: {
data: { type: 'button', display: false, text: 'btn' },
},
});
expect(wrapper.find('.menu-item').exists()).toBe(false);
});
test('data.type=text 时渲染文字', () => {
const wrapper = mount(ToolButton as any, {
...provideServices(),
props: {
data: { type: 'text', text: 'hello' } as any,
},
});
expect(wrapper.text()).toContain('hello');
});
test('data.type=divider 时渲染 divider', () => {
const wrapper = mount(ToolButton as any, {
...provideServices(),
props: {
data: { type: 'divider' } as any,
},
});
expect(wrapper.find('.menu-item').exists()).toBe(true);
});
test('data.type=button 点击触发 handler', async () => {
const handler = vi.fn();
const wrapper = mount(ToolButton as any, {
...provideServices(),
props: {
data: { type: 'button', text: 'click', handler } as any,
eventType: 'click',
},
});
await wrapper.find('.menu-item').trigger('click');
expect(handler).toHaveBeenCalled();
});
test('display 函数返回 false 时不渲染', () => {
const wrapper = mount(ToolButton as any, {
...provideServices(),
props: {
data: { type: 'button', display: () => false, text: 'x' } as any,
},
});
expect(wrapper.find('.menu-item').exists()).toBe(false);
});
test('disabled 函数返回 true 时不调用 handler', async () => {
const handler = vi.fn();
const wrapper = mount(ToolButton as any, {
...provideServices(),
props: {
data: { type: 'button', text: 'x', disabled: () => true, handler } as any,
eventType: 'click',
},
});
await wrapper.find('.menu-item').trigger('click');
expect(handler).not.toHaveBeenCalled();
});
test('eventType=mousedown 仅 mousedown 触发', async () => {
const handler = vi.fn();
const wrapper = mount(ToolButton as any, {
...provideServices(),
props: {
data: { type: 'button', text: 'x', handler } as any,
eventType: 'mousedown',
},
});
await wrapper.find('.menu-item').trigger('click');
expect(handler).not.toHaveBeenCalled();
await wrapper.find('.menu-item').trigger('mousedown');
expect(handler).toHaveBeenCalled();
});
test('eventType=mouseup 仅左键 mouseup 触发', async () => {
const handler = vi.fn();
const wrapper = mount(ToolButton as any, {
...provideServices(),
props: {
data: { type: 'button', text: 'x', handler } as any,
eventType: 'mouseup',
},
});
await wrapper.find('.menu-item').trigger('mouseup', { button: 1 });
expect(handler).not.toHaveBeenCalled();
await wrapper.find('.menu-item').trigger('mouseup', { button: 0 });
expect(handler).toHaveBeenCalled();
});
test('button 含 tooltip 时渲染 tooltip', () => {
const wrapper = mount(ToolButton as any, {
...provideServices(),
props: {
data: { type: 'button', text: 'x', tooltip: 'tip' } as any,
},
});
expect(wrapper.find('.menu-item').exists()).toBe(true);
expect(wrapper.text()).toContain('x');
});
test('data.type=dropdown 时渲染下拉菜单', () => {
const wrapper = mount(ToolButton as any, {
...provideServices(),
props: {
data: {
type: 'dropdown',
text: 'menu',
items: [{ text: 'item1', handler: vi.fn() }],
} as any,
},
});
expect(wrapper.find('.menu-item').exists()).toBe(true);
expect(wrapper.find('.menubar-menu-button').exists()).toBe(true);
expect(wrapper.text()).toContain('menu');
});
test('data.type=component 时渲染对应组件', () => {
const fakeComp = {
name: 'FakeC',
props: ['v'],
template: '<div class="custom-comp">{{v}}</div>',
};
const wrapper = mount(ToolButton as any, {
...provideServices(),
props: {
data: {
type: 'component',
component: fakeComp,
props: { v: 'hello' },
} as any,
},
});
expect(wrapper.find('.custom-comp').exists()).toBe(true);
expect(wrapper.find('.custom-comp').text()).toBe('hello');
});
test('dropdown 选中调用对应 handler', async () => {
const handler = vi.fn();
const wrapper = mount(ToolButton as any, {
...provideServices(),
props: {
data: {
type: 'dropdown',
text: 'menu',
items: [{ text: 'item1', handler }],
} as any,
},
});
const dropdown = wrapper.findComponent({ name: 'TMagicDropdown' });
if (dropdown.exists()) {
await dropdown.vm.$emit('command', { item: { handler } });
expect(handler).toHaveBeenCalled();
}
});
test('disabled 为 boolean 时直接使用', async () => {
const handler = vi.fn();
const wrapper = mount(ToolButton as any, {
...provideServices(),
props: {
data: { type: 'button', text: 'x', disabled: true, handler } as any,
eventType: 'click',
},
});
await wrapper.find('.menu-item').trigger('click');
expect(handler).not.toHaveBeenCalled();
});
test('暴露 getElRef可获取根元素引用', () => {
const wrapper = mount(ToolButton as any, {
...provideServices(),
props: {
data: { type: 'button', text: 'x' } as any,
},
});
const exposed = wrapper.vm as any;
expect(typeof exposed.getElRef).toBe('function');
const elRef = exposed.getElRef();
expect(elRef).toBeDefined();
expect(elRef.value).toBe(wrapper.find('.menu-item').element);
});
test('display 为 false 时 getElRef 返回的 ref.value 为 null', () => {
const wrapper = mount(ToolButton as any, {
...provideServices(),
props: {
data: { type: 'button', display: false, text: 'x' } as any,
},
});
const exposed = wrapper.vm as any;
const elRef = exposed.getElRef();
expect(elRef.value).toBeNull();
});
});