diff --git a/packages/components/dialog/__tests__/dialog-card.test.tsx b/packages/components/dialog/__tests__/dialog-card.test.tsx
new file mode 100644
index 0000000000..ff4dc5a71e
--- /dev/null
+++ b/packages/components/dialog/__tests__/dialog-card.test.tsx
@@ -0,0 +1,278 @@
+// @ts-nocheck
+import { mount } from '@vue/test-utils';
+import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
+import { nextTick } from 'vue';
+import DialogCard from '../dialog-card';
+
+beforeEach(() => {
+ document.body.innerHTML = '';
+});
+
+afterEach(() => {
+ document.body.innerHTML = '';
+});
+
+describe('props', () => {
+ it('header[string]', async () => {
+ const wrapper = mount(() => );
+
+ await nextTick();
+ expect(wrapper.find('.t-dialog__header').exists()).toBe(true);
+ expect(wrapper.find('.t-dialog__header').text()).toBe('Test Title');
+ });
+
+ it('header[function]', async () => {
+ const renderHeader = () => ;
+ const wrapper = mount(() => );
+
+ await nextTick();
+ expect(wrapper.find('.custom-header').exists()).toBe(true);
+ expect(wrapper.find('.custom-header').text()).toBe('Custom Header');
+ });
+
+ it('header[slot]', async () => {
+ const wrapper = mount(() => (
+ }}>
+ ));
+
+ await nextTick();
+ expect(wrapper.find('.slot-header').exists()).toBe(true);
+ expect(wrapper.find('.slot-header').text()).toBe('Slot Header');
+ });
+
+ it('body[string]', async () => {
+ const wrapper = mount(() => );
+
+ await nextTick();
+ const body = wrapper.find('.t-dialog__body');
+ expect(body.exists()).toBe(true);
+ expect(body.text()).toBe('String Body');
+ });
+
+ it('body[function]', async () => {
+ const renderBody = () =>
Custom Body
;
+ const wrapper = mount(() => );
+
+ await nextTick();
+ expect(wrapper.find('.custom-body').exists()).toBe(true);
+ expect(wrapper.find('.custom-body').text()).toBe('Custom Body');
+ });
+
+ it('body[slot]', async () => {
+ const wrapper = mount(() => (
+ Slot Body
}}>
+ ));
+
+ await nextTick();
+ expect(wrapper.find('.slot-body').exists()).toBe(true);
+ expect(wrapper.find('.slot-body').text()).toBe('Slot Body');
+ });
+
+ it('footer[boolean]', async () => {
+ // footer=true 应该显示默认按钮
+ const wrapperTrue = mount(() => );
+ await nextTick();
+ expect(wrapperTrue.find('.t-dialog__footer').exists()).toBe(true);
+
+ // footer=false 应该隐藏footer
+ const wrapperFalse = mount(() => );
+ await nextTick();
+ expect(wrapperFalse.find('.t-dialog__footer').exists()).toBe(false);
+ });
+
+ it('footer[function]', async () => {
+ const renderFooter = () => ;
+ const wrapper = mount(() => );
+
+ await nextTick();
+ expect(wrapper.find('.custom-footer').exists()).toBe(true);
+ expect(wrapper.find('.custom-footer').text()).toBe('Custom Footer');
+ });
+
+ it('footer[slot]', async () => {
+ const wrapper = mount(() => (
+ }}
+ >
+ ));
+
+ await nextTick();
+ expect(wrapper.find('.slot-footer').exists()).toBe(true);
+ expect(wrapper.find('.slot-footer').text()).toBe('Slot Footer');
+ });
+
+ it('theme[string]', async () => {
+ const themes = ['default', 'info', 'warning', 'danger', 'success', ''] as const;
+
+ themes.forEach(async (theme) => {
+ const wrapper = mount(() => );
+ await nextTick();
+ const dialog = wrapper.find('.t-dialog');
+ expect(dialog.classes()).toContain(`t-dialog__modal-${theme}`);
+ });
+ });
+
+ it('closeBtn[boolean]', async () => {
+ // 默认应该显示关闭按钮
+ const wrapperDefault = mount(() => );
+ await nextTick();
+ expect(wrapperDefault.find('.t-dialog__close').exists()).toBe(true);
+
+ // closeBtn=false 应该隐藏关闭按钮
+ const wrapperFalse = mount(() => );
+ await nextTick();
+ expect(wrapperFalse.find('.t-dialog__close').exists()).toBe(false);
+ });
+
+ it('closeBtn[function]', async () => {
+ const renderCloseBtn = () => ×;
+ const wrapper = mount(() => );
+
+ await nextTick();
+ expect(wrapper.find('.custom-close').exists()).toBe(true);
+ expect(wrapper.find('.custom-close').text()).toBe('×');
+ });
+
+ it('placement[string]', async () => {
+ const placements = ['top', 'center'] as const;
+
+ placements.forEach(async (placement) => {
+ const wrapper = mount(() => );
+ await nextTick();
+ const dialog = wrapper.find('.t-dialog');
+ expect(dialog.classes()).toContain(`t-dialog--${placement}`);
+ });
+ });
+
+ it('width[number]', async () => {
+ const wrapper = mount(() => );
+
+ await nextTick();
+ const dialog = wrapper.find('.t-dialog');
+ expect(dialog.element.style.width).toBe('500px');
+ });
+
+ it('width[string]', async () => {
+ const wrapper = mount(() => );
+
+ await nextTick();
+ const dialog = wrapper.find('.t-dialog');
+ expect(dialog.element.style.width).toBe('80%');
+ });
+
+ it('draggable[boolean]', async () => {
+ const wrapper = mount(() => );
+
+ await nextTick();
+ const dialog = wrapper.find('.t-dialog');
+ expect(dialog.classes()).toContain('t-dialog--draggable');
+ });
+
+ it('mode[string]', async () => {
+ const modes = ['modal', 'modeless'] as const;
+
+ modes.forEach(async (mode) => {
+ const wrapper = mount(() => );
+ await nextTick();
+ // 这里需要根据实际的class名称进行调整
+ const dialog = wrapper.find('.t-dialog');
+ expect(dialog.exists()).toBe(true);
+ });
+ });
+
+ it('zIndex[number]', async () => {
+ const wrapper = mount(() => );
+
+ await nextTick();
+ const dialog = wrapper.find('.t-dialog');
+ expect(dialog.exists()).toBe(true);
+ // 这里需要根据实际的样式应用方式进行调整
+ });
+
+ it('preventScrollThrough[boolean]', async () => {
+ const wrapper = mount(() => );
+
+ await nextTick();
+ const dialog = wrapper.find('.t-dialog');
+ expect(dialog.exists()).toBe(true);
+ // 这里需要根据实际的行为进行测试调整
+ });
+
+ it('should switch body className correctly in full-screen mode', async () => {
+ // footer 存在时
+ const wrapperWithFooter = mount(() => (
+
+ ));
+ await nextTick();
+ const bodyWithFooter = wrapperWithFooter.find('.t-dialog__body');
+ expect(bodyWithFooter.exists()).toBe(true);
+ expect(bodyWithFooter.classes()).toContain('t-dialog__body--fullscreen');
+
+ // footer 不存在时
+ const wrapperWithoutFooter = mount(() => (
+
+ ));
+ await nextTick();
+ const bodyWithoutFooter = wrapperWithoutFooter.find('.t-dialog__body');
+ expect(bodyWithoutFooter.exists()).toBe(true);
+ expect(bodyWithoutFooter.classes()).toContain('t-dialog__body--fullscreen--without-footer');
+ });
+});
+
+describe('events', () => {
+ it('onCloseBtnClick', async () => {
+ const onCloseBtnClick = vi.fn();
+ const wrapper = mount(() => );
+
+ await nextTick();
+ const closeBtn = wrapper.find('.t-dialog__close');
+ await closeBtn.trigger('click');
+ expect(onCloseBtnClick).toHaveBeenCalled();
+ });
+
+ it('onStopDown: should call stopPropagation when mode is modeless and draggable is true', async () => {
+ const stopPropagation = vi.fn();
+ const wrapper = mount(() => );
+ await nextTick();
+ // 触发 body 区域的 mousedown
+ const body = wrapper.find('.t-dialog__body');
+ await body.trigger('mousedown', { stopPropagation });
+ expect(stopPropagation).toHaveBeenCalled();
+ });
+});
+
+describe('slots', () => {
+ it('default slot', async () => {
+ const wrapper = mount(() => (
+
+ Default Slot Content
+
+ ));
+
+ await nextTick();
+ expect(wrapper.find('.default-slot').exists()).toBe(true);
+ expect(wrapper.find('.default-slot').text()).toBe('Default Slot Content');
+ });
+});
+
+describe('edge cases', () => {
+ it('handles empty props', async () => {
+ const wrapper = mount(() => );
+
+ await nextTick();
+ // 空字符串仍应渲染对应结构
+ expect(wrapper.find('.t-dialog__header').exists()).toBe(true);
+ expect(wrapper.find('.t-dialog__body').exists()).toBe(true);
+ });
+
+ it('handles undefined props', async () => {
+ const wrapper = mount(() => );
+
+ await nextTick();
+ // 未提供props时的默认行为
+ expect(wrapper.find('.t-dialog').exists()).toBe(true);
+ });
+});
diff --git a/packages/components/dialog/__tests__/dialog.hooks.test.tsx b/packages/components/dialog/__tests__/dialog.hooks.test.tsx
new file mode 100644
index 0000000000..3105e01c2a
--- /dev/null
+++ b/packages/components/dialog/__tests__/dialog.hooks.test.tsx
@@ -0,0 +1,490 @@
+// @ts-nocheck
+import { describe, expect, it, vi, beforeEach } from 'vitest';
+import { mount } from '@vue/test-utils';
+import { ref, defineComponent } from 'vue';
+import { useAction, useSameTarget } from '../hooks';
+
+const UnifiedTestComponent = defineComponent({
+ props: {
+ // useAction 相关 props
+ confirmBtn: [String, Object, Function],
+ cancelBtn: [String, Object, Function],
+ confirmLoading: Boolean,
+ theme: String,
+ size: String,
+ className: String,
+ globalConfirm: [String, Object],
+ globalCancel: [String, Object],
+ globalConfirmBtnTheme: Object,
+
+ // useSameTarget 相关 props
+ onClick: Function,
+ disabled: Boolean,
+
+ // 测试模式控制
+ testMode: {
+ type: String,
+ default: 'action', // 'action' | 'sameTarget' | 'extended'
+ },
+ },
+ setup(props, { expose }) {
+ // useAction 测试逻辑
+ if (props.testMode === 'action') {
+ const mockAction = {
+ confirmBtnAction: vi.fn(),
+ cancelBtnAction: vi.fn(),
+ };
+
+ const { getConfirmBtn, getCancelBtn } = useAction(mockAction);
+
+ const confirmOptions = {
+ confirmBtn: props.confirmBtn,
+ globalConfirm: props.globalConfirm || '确认',
+ className: props.className || 'test-confirm',
+ size: props.size || 'medium',
+ confirmLoading: props.confirmLoading,
+ theme: props.theme,
+ globalConfirmBtnTheme: props.globalConfirmBtnTheme || {
+ default: 'primary',
+ info: 'info',
+ warning: 'warning',
+ danger: 'danger',
+ success: 'success',
+ },
+ };
+
+ const cancelOptions = {
+ cancelBtn: props.cancelBtn,
+ globalCancel: props.globalCancel || '取消',
+ className: props.className || 'test-cancel',
+ size: props.size || 'medium',
+ };
+
+ const confirmBtn = getConfirmBtn(confirmOptions);
+ const cancelBtn = getCancelBtn(cancelOptions);
+
+ expose({ confirmBtn, cancelBtn, mockAction });
+
+ return () => (
+
+ {confirmBtn}
+ {cancelBtn}
+
+ );
+ }
+
+ // useSameTarget 测试逻辑
+ if (props.testMode === 'sameTarget') {
+ const { onClick, onMousedown, onMouseup } = useSameTarget(props.onClick);
+
+ expose({ onClick, onMousedown, onMouseup });
+
+ return () => (
+
+ 测试目标
+
+ );
+ }
+
+ // 扩展测试逻辑
+ if (props.testMode === 'extended') {
+ const handleClick = vi.fn();
+ const extendDisabled = ref(props.disabled);
+ const { onClick, onMousedown, onMouseup } = useSameTarget(extendDisabled.value ? undefined : handleClick);
+
+ expose({ onClick, onMousedown, onMouseup, handleClick, extendDisabled });
+
+ return () => (
+
+ 扩展测试
+
+ );
+ }
+
+ return () => 默认测试组件
;
+ },
+});
+
+describe('useAction', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe('getConfirmBtn', () => {
+ it('should return null when confirmBtn is null', () => {
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'action',
+ confirmBtn: null,
+ },
+ });
+ expect(wrapper.vm.confirmBtn).toBe(null);
+ });
+
+ it('should return default button when confirmBtn is undefined', () => {
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'action',
+ confirmBtn: undefined,
+ },
+ });
+ const button = wrapper.find('button');
+ expect(button.exists()).toBeTruthy();
+ expect(button.text()).toBe('确认');
+ expect(button.classes()).toContain('t-button--theme-primary');
+ });
+
+ it('should display custom content when confirmBtn is string', () => {
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'action',
+ confirmBtn: '自定义确认',
+ },
+ });
+ const button = wrapper.find('button');
+ expect(button.text()).toBe('自定义确认');
+ });
+
+ it('should apply object properties when confirmBtn is object', () => {
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'action',
+ confirmBtn: { content: '保存', theme: 'success' },
+ },
+ });
+ const button = wrapper.find('button');
+ expect(button.text()).toBe('保存');
+ expect(button.classes()).toContain('t-button--theme-success');
+ });
+
+ it('should show loading state', () => {
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'action',
+ confirmBtn: '确认',
+ confirmLoading: true,
+ },
+ });
+ const button = wrapper.find('button');
+ expect(button.classes()).toContain('t-is-loading');
+ });
+
+ it('should trigger confirmBtnAction on click', async () => {
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'action',
+ confirmBtn: '确认',
+ },
+ });
+ const button = wrapper.find('button');
+ await button.trigger('click');
+ expect(wrapper.vm.mockAction.confirmBtnAction).toHaveBeenCalled();
+ });
+
+ it('should handle different theme types', () => {
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'action',
+ confirmBtn: '确认',
+ theme: 'warning',
+ },
+ });
+ const button = wrapper.find('button');
+ expect(button.classes()).toContain('t-button--theme-warning');
+ });
+
+ it('should handle global config as object', () => {
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'action',
+ confirmBtn: undefined,
+ globalConfirm: { content: '全局确认', theme: 'info' },
+ },
+ });
+ const button = wrapper.find('button');
+ expect(button.text()).toBe('全局确认');
+ expect(button.classes()).toContain('t-button--theme-info');
+ });
+
+ it('When both confirmBtn props and slot exist, props should be preferred and a warning should be output', () => {
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'action',
+ confirmBtn: 'props优先',
+ },
+ slots: {
+ confirmBtn: () => 'slot内容',
+ },
+ });
+ // 断言 warning 被调用
+ expect(warnSpy).toHaveBeenCalledWith(
+ 'Both $props.confirmBtn and $scopedSlots.confirmBtn exist, $props.confirmBtn is preferred.',
+ );
+ // 断言按钮内容为 props
+ const button = wrapper.find('button');
+ expect(button.text()).toBe('props优先');
+ warnSpy.mockRestore();
+ });
+
+ it('should render content returned by confirmBtn function', () => {
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'action',
+ confirmBtn: () => '函数按钮',
+ },
+ });
+ expect(wrapper.html()).toContain('函数按钮');
+ });
+
+ it('should render slot content when only confirmBtn slot is provided', () => {
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'action',
+ confirmBtn: undefined,
+ },
+ slots: {
+ confirmBtn: () => '插槽按钮',
+ },
+ });
+ expect(wrapper.html()).toContain('插槽按钮');
+ });
+ });
+
+ describe('getCancelBtn', () => {
+ it('When both cancelBtn props and slot exist, props should be preferred and a warning should be output', () => {
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'action',
+ cancelBtn: 'props优先',
+ },
+ slots: {
+ cancelBtn: () => 'slot内容',
+ },
+ });
+ // 断言 warning 被调用
+ expect(warnSpy).toHaveBeenCalledWith(
+ 'Both $props.cancelBtn and $scopedSlots.cancelBtn exist, $props.cancelBtn is preferred.',
+ );
+ // 断言按钮内容为 props
+ const button = wrapper.findAll('button');
+ expect(button[1].text()).toBe('props优先');
+ warnSpy.mockRestore();
+ });
+
+ it('should return null when cancelBtn is null', () => {
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'action',
+ cancelBtn: null,
+ },
+ });
+ expect(wrapper.vm.cancelBtn).toBe(null);
+ });
+
+ it('should return default button when cancelBtn is undefined', () => {
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'action',
+ confirmBtn: null,
+ cancelBtn: undefined,
+ },
+ });
+ const buttons = wrapper.findAll('button');
+ const cancelButton = buttons[0];
+ expect(cancelButton.exists()).toBeTruthy();
+ expect(cancelButton.text()).toBe('取消');
+ expect(cancelButton.classes()).toContain('t-button--theme-default');
+ });
+
+ it('should display custom content when cancelBtn is string', () => {
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'action',
+ confirmBtn: null,
+ cancelBtn: '自定义取消',
+ },
+ });
+ const button = wrapper.find('button');
+ expect(button.text()).toBe('自定义取消');
+ });
+
+ it('should apply object properties when cancelBtn is object', () => {
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'action',
+ confirmBtn: null,
+ cancelBtn: { content: '关闭', theme: 'danger' },
+ },
+ });
+ const button = wrapper.find('button');
+ expect(button.text()).toBe('关闭');
+ expect(button.classes()).toContain('t-button--theme-danger');
+ });
+
+ it('should trigger cancelBtnAction on click', async () => {
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'action',
+ confirmBtn: null,
+ cancelBtn: '取消',
+ },
+ });
+ const button = wrapper.find('button');
+ await button.trigger('click');
+ expect(wrapper.vm.mockAction.cancelBtnAction).toHaveBeenCalled();
+ });
+
+ it('should use globalCancel string as button content', () => {
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'action',
+ cancelBtn: undefined,
+ globalCancel: '全局取消',
+ },
+ });
+ const buttons = wrapper.findAll('button');
+ const cancelButton = buttons[1];
+ expect(cancelButton.text()).toBe('全局取消');
+ });
+
+ it('globalCancel object should set button content and theme', () => {
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'action',
+ cancelBtn: undefined,
+ globalCancel: { content: '全局取消对象', theme: 'danger' },
+ },
+ });
+ const buttons = wrapper.findAll('button');
+ const cancelButton = buttons[1];
+ expect(cancelButton.text()).toBe('全局取消对象');
+ expect(cancelButton.classes()).toContain('t-button--theme-danger');
+ });
+ });
+});
+
+describe('useSameTarget', () => {
+ it('should return event handler functions', () => {
+ const handleClick = vi.fn();
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'sameTarget',
+ onClick: handleClick,
+ },
+ });
+ expect(typeof wrapper.vm.onClick).toBe('function');
+ expect(typeof wrapper.vm.onMousedown).toBe('function');
+ expect(typeof wrapper.vm.onMouseup).toBe('function');
+ });
+
+ it('should call handleClick when mousedown and mouseup targets are same', async () => {
+ const handleClick = vi.fn();
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'sameTarget',
+ onClick: handleClick,
+ },
+ });
+ const mockTarget = document.createElement('div');
+ await wrapper.vm.onMousedown({ target: mockTarget, currentTarget: mockTarget } as MouseEvent);
+ await wrapper.vm.onMouseup({ target: mockTarget, currentTarget: mockTarget } as MouseEvent);
+ await wrapper.vm.onClick({} as MouseEvent);
+ expect(handleClick).toHaveBeenCalledTimes(1);
+ });
+
+ it('should not call handleClick when mousedown targets are different', async () => {
+ const handleClick = vi.fn();
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'sameTarget',
+ onClick: handleClick,
+ },
+ });
+ const mockTarget1 = document.createElement('div');
+ const mockTarget2 = document.createElement('div');
+ await wrapper.vm.onMousedown({ target: mockTarget1, currentTarget: mockTarget2 } as MouseEvent);
+ await wrapper.vm.onMouseup({ target: mockTarget1, currentTarget: mockTarget1 } as MouseEvent);
+ await wrapper.vm.onClick({} as MouseEvent);
+ expect(handleClick).not.toHaveBeenCalled();
+ });
+
+ it('should not call handleClick when mouseup targets are different', async () => {
+ const handleClick = vi.fn();
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'sameTarget',
+ onClick: handleClick,
+ },
+ });
+ const mockTarget1 = document.createElement('div');
+ const mockTarget2 = document.createElement('div');
+ await wrapper.vm.onMousedown({ target: mockTarget1, currentTarget: mockTarget1 } as MouseEvent);
+ await wrapper.vm.onMouseup({ target: mockTarget1, currentTarget: mockTarget2 } as MouseEvent);
+ await wrapper.vm.onClick({} as MouseEvent);
+ expect(handleClick).not.toHaveBeenCalled();
+ });
+
+ it('should reset state after click', async () => {
+ const handleClick = vi.fn();
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'sameTarget',
+ onClick: handleClick,
+ },
+ });
+ const mockTarget = document.createElement('div');
+ await wrapper.vm.onMousedown({ target: mockTarget, currentTarget: mockTarget } as MouseEvent);
+ await wrapper.vm.onMouseup({ target: mockTarget, currentTarget: mockTarget } as MouseEvent);
+ await wrapper.vm.onClick({} as MouseEvent);
+ expect(handleClick).toHaveBeenCalledTimes(1);
+ await wrapper.vm.onClick({} as MouseEvent);
+ expect(handleClick).toHaveBeenCalledTimes(1);
+ });
+
+ it('should not throw error when no handleClick function provided', async () => {
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'sameTarget',
+ onClick: undefined,
+ },
+ });
+ const mockTarget = document.createElement('div');
+ expect(() => {
+ wrapper.vm.onMousedown({ target: mockTarget, currentTarget: mockTarget } as MouseEvent);
+ wrapper.vm.onMouseup({ target: mockTarget, currentTarget: mockTarget } as MouseEvent);
+ wrapper.vm.onClick({} as MouseEvent);
+ }).not.toThrow();
+ });
+
+ it('should handle extended parameters correctly', async () => {
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'extended',
+ disabled: false,
+ },
+ });
+ const mockTarget = document.createElement('div');
+ await wrapper.vm.onMousedown({ target: mockTarget, currentTarget: mockTarget } as MouseEvent);
+ await wrapper.vm.onMouseup({ target: mockTarget, currentTarget: mockTarget } as MouseEvent);
+ await wrapper.vm.onClick({} as MouseEvent);
+ expect(wrapper.vm.handleClick).toHaveBeenCalled();
+ });
+
+ it('should return correct state when all conditions are not met', async () => {
+ const wrapper = mount(UnifiedTestComponent, {
+ props: {
+ testMode: 'sameTarget',
+ onClick: undefined,
+ },
+ });
+ const mockTarget1 = document.createElement('div');
+ const mockTarget2 = document.createElement('div');
+ await wrapper.vm.onMousedown({ target: mockTarget1, currentTarget: mockTarget2 } as MouseEvent);
+ await wrapper.vm.onMouseup({ target: mockTarget1, currentTarget: mockTarget2 } as MouseEvent);
+ expect(() => {
+ wrapper.vm.onClick({} as MouseEvent);
+ }).not.toThrow();
+ });
+});
diff --git a/packages/components/dialog/__tests__/dialog.plugin.test.ts b/packages/components/dialog/__tests__/dialog.plugin.test.ts
new file mode 100644
index 0000000000..fb3d321ae7
--- /dev/null
+++ b/packages/components/dialog/__tests__/dialog.plugin.test.ts
@@ -0,0 +1,333 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { createApp, nextTick } from 'vue';
+import DialogPlugin from '../../dialog/plugin';
+
+describe('DialogPlugin', () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ });
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ const app: ReturnType = createApp({ render: null });
+ app.use(DialogPlugin);
+
+ it('should install plugin and add $dialog to globalProperties', () => {
+ const dialog = app.config.globalProperties.$dialog;
+ expect(dialog).toBeDefined();
+ expect(typeof dialog).toBe('function');
+ expect(typeof dialog.confirm).toBe('function');
+ expect(typeof dialog.alert).toBe('function');
+ });
+
+ it('should open and close dialog via $dialog', async () => {
+ const dialog = app.config.globalProperties.$dialog({
+ body: 'test content',
+ });
+ await nextTick();
+ dialog.show();
+ await nextTick();
+ dialog.hide();
+ await nextTick();
+ expect(dialog).toBeDefined();
+ dialog.destroy();
+ vi.runAllTimers();
+ });
+
+ it('should open confirm dialog', async () => {
+ const dialog = app.config.globalProperties.$dialog.confirm({
+ body: 'confirm content',
+ });
+ expect(dialog).toBeDefined();
+ dialog.destroy();
+ vi.runAllTimers();
+ });
+
+ it('should open alert dialog (no cancel button)', async () => {
+ const dialog = app.config.globalProperties.$dialog.alert({
+ body: 'alert content',
+ });
+ expect(dialog).toBeDefined();
+ dialog.destroy();
+ vi.runAllTimers();
+ });
+
+ it('should set confirm loading', async () => {
+ const dialog = app.config.globalProperties.$dialog({
+ body: 'loading content',
+ });
+ dialog.setConfirmLoading(true);
+ dialog.setConfirmLoading(false);
+ dialog.destroy();
+ vi.runAllTimers();
+ });
+
+ it('should update dialog options', async () => {
+ const dialog = app.config.globalProperties.$dialog({
+ body: 'update content',
+ });
+ dialog.update({ body: 'updated', className: 'test-class2' });
+ await nextTick();
+ // 检查内容是否更新
+ const dialogBody = document.body.querySelector('.t-dialog__body');
+ expect(dialogBody).toBeTruthy();
+ expect(dialogBody && dialogBody.textContent).toBe('updated');
+ // 检查class是否更新
+ const dialogCtx = document.body.querySelector('.t-dialog__ctx');
+ expect(dialogCtx).toBeTruthy();
+ expect(dialogCtx && dialogCtx.classList.contains('test-class2')).toBeTruthy();
+ dialog.destroy();
+ vi.runAllTimers();
+ });
+
+ it('should apply style to dialog wrapper', async () => {
+ const style = 'color: red;';
+ const dialog = app.config.globalProperties.$dialog({
+ body: 'styled content',
+ style,
+ });
+ const wrapper = document.body.querySelector('.t-dialog__ctx');
+ expect(wrapper).toBeTruthy();
+ expect((wrapper as HTMLElement).style.cssText.replace(/\s/g, style)).toContain(style);
+ dialog.destroy();
+ vi.runAllTimers();
+ });
+
+ it('should use default onClose when no custom onClose is provided', async () => {
+ const dialog = app.config.globalProperties.$dialog({
+ body: 'no custom onClose',
+ onClose: null,
+ });
+ // 关闭弹窗
+ dialog.hide();
+ await nextTick();
+ // 检查 DOM 是否被隐藏
+ const wrapper = document.body.querySelector('.t-dialog__ctx');
+ // 这里可以根据实际实现,判断 wrapper 是否 display: none 或者直接被移除
+ expect(wrapper === null || (wrapper instanceof HTMLElement && wrapper.style.display === 'none')).toBeTruthy();
+ dialog.destroy();
+ vi.runAllTimers();
+ });
+
+ it('should use custom onClose when provided', async () => {
+ const onClose = vi.fn();
+ const dialog = app.config.globalProperties.$dialog({
+ body: '自定义 onClose',
+ onClose,
+ });
+ await nextTick();
+ // 模拟关闭按钮点击
+ // 这里直接调用 onClose 事件
+ dialog.hide(); // 先显示再隐藏,确保 visible 变化
+ await nextTick();
+ // 手动触发 onClose
+ onClose();
+ expect(onClose).toHaveBeenCalled();
+ // 由于自定义 onClose 不会自动隐藏,visible 仍为 true
+ // 但我们无法直接访问 visible,只能保证 onClose 被调用
+ dialog.destroy();
+ vi.runAllTimers();
+ });
+
+ it('should attach dialog to custom container when attach is provided', async () => {
+ // 创建自定义容器并加入 body
+ const customContainer = document.createElement('div');
+ customContainer.id = 'custom-dialog-container';
+ document.body.appendChild(customContainer);
+
+ // 打开 dialog,attach 指定到自定义容器
+ const dialog = app.config.globalProperties.$dialog({
+ body: 'attach content',
+ attach: '#custom-dialog-container',
+ });
+ await nextTick();
+
+ // 检查 dialog 是否被挂载到自定义容器
+ const wrapper = customContainer.querySelector('.t-dialog__ctx');
+ expect(wrapper).toBeTruthy();
+
+ dialog.destroy();
+ vi.runAllTimers();
+ document.body.removeChild(customContainer);
+ });
+
+ it('should log error when attach target does not exist', async () => {
+ // mock console.error
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
+
+ const dialog = app.config.globalProperties.$dialog({
+ body: 'attach error',
+ attach: '#not-exist-container',
+ });
+ await nextTick();
+
+ expect(errorSpy).toHaveBeenCalledWith('attach is not exist');
+ dialog.destroy();
+ vi.runAllTimers();
+ errorSpy.mockRestore();
+ });
+
+ it('should delay unmount for 0.3s after destroy', async () => {
+ const dialog = app.config.globalProperties.$dialog({
+ body: '延迟销毁测试',
+ });
+ await nextTick();
+
+ const wrapper = document.body.querySelector('.t-dialog__ctx');
+ expect(wrapper).toBeTruthy();
+
+ dialog.destroy();
+
+ // 立即检查,DOM 还在
+ expect(document.body.querySelector('.t-dialog__ctx')).toBeTruthy();
+
+ // 300ms 后检查,DOM 应该被移除
+ vi.advanceTimersByTime(310);
+ expect(document.body.querySelector('.t-dialog__ctx')).toBeFalsy();
+ });
+
+ it('should not update className if it does not change', async () => {
+ const dialog = app.config.globalProperties.$dialog({
+ body: 'className unchanged',
+ className: 'test-class',
+ });
+ await nextTick();
+ // 记录当前 classList
+ const wrapper = document.body.querySelector('.t-dialog__ctx');
+ expect(wrapper).toBeTruthy();
+ const oldClassList = [...(wrapper as HTMLElement).classList];
+
+ // update 时 className 不变
+ dialog.update({ className: 'test-class' });
+ await nextTick();
+
+ // 断言 classList 没有变化
+ const newClassList = [...(wrapper as HTMLElement).classList];
+ expect(newClassList).toEqual(oldClassList);
+
+ dialog.destroy();
+ vi.runAllTimers();
+ });
+
+ it('should remove old className when className changes', async () => {
+ const dialog = app.config.globalProperties.$dialog({
+ body: 'className remove',
+ className: 'class-a class-b',
+ });
+ await nextTick();
+ const wrapper = document.body.querySelector('.t-dialog__ctx');
+ expect(wrapper).toBeTruthy();
+ expect(wrapper && wrapper.classList.contains('class-a')).toBeTruthy();
+ expect(wrapper && wrapper.classList.contains('class-b')).toBeTruthy();
+
+ // 更新 className,移除旧的 class-a class-b,添加 class-c
+ dialog.update({ className: 'class-c' });
+ await nextTick();
+ expect(wrapper && wrapper.classList.contains('class-a')).toBeFalsy();
+ expect(wrapper && wrapper.classList.contains('class-b')).toBeFalsy();
+ expect(wrapper && wrapper.classList.contains('class-c')).toBeTruthy();
+ dialog.destroy();
+ vi.runAllTimers();
+ });
+
+ it('should append style when style changes', async () => {
+ const dialog = app.config.globalProperties.$dialog({
+ body: 'style append',
+ style: 'color: blue;',
+ });
+ await nextTick();
+ const wrapper = document.body.querySelector('.t-dialog__ctx');
+ expect(wrapper).toBeTruthy();
+ expect((wrapper as HTMLElement).style.cssText.replace(/\s/g, '')).toContain('color:blue;');
+
+ // 追加 style
+ dialog.update({ style: 'background: yellow;' });
+ await nextTick();
+ // cssText 追加了新样式
+ expect((wrapper as HTMLElement).style.cssText.replace(/\s/g, '')).toContain('color:blue;');
+ expect((wrapper as HTMLElement).style.cssText.replace(/\s/g, '')).toContain('background:yellow;');
+ dialog.destroy();
+ vi.runAllTimers();
+ });
+
+ it('should use default onClose logic when no custom onClose provided', async () => {
+ const dialog = app.config.globalProperties.$dialog({
+ body: 'default onClose test',
+ destroyOnClose: false,
+ });
+ await nextTick();
+
+ // 获取对话框元素
+ const wrapper = document.body.querySelector('.t-dialog__ctx');
+ expect(wrapper).toBeTruthy();
+
+ // 模拟点击关闭按钮触发默认的 onClose 逻辑
+ const closeBtn = wrapper.querySelector('.t-dialog__close');
+ expect(closeBtn).toBeTruthy();
+ (closeBtn as HTMLElement).click();
+ await nextTick();
+
+ // 验证对话框被隐藏但未销毁(destroyOnClose: false)
+ // 检查对话框是否有隐藏的样式类或属性
+ const dialogElement = wrapper.querySelector('.t-dialog');
+ expect(dialogElement).toBeTruthy();
+
+ dialog.destroy();
+ vi.runAllTimers();
+ });
+
+ it('should destroy dialog after 300ms when destroyOnClose is true', async () => {
+ app.config.globalProperties.$dialog({
+ body: 'destroyOnClose test',
+ destroyOnClose: true,
+ });
+ await nextTick();
+
+ // 获取对话框元素
+ let wrapper = document.body.querySelector('.t-dialog__ctx');
+ expect(wrapper).toBeTruthy();
+
+ // 模拟点击关闭按钮触发 onClose 事件
+ const closeBtn = wrapper.querySelector('.t-dialog__close');
+ expect(closeBtn).toBeTruthy();
+ (closeBtn as HTMLElement).click();
+ await nextTick();
+
+ // 立即检查,DOM 还在
+ wrapper = document.body.querySelector('.t-dialog__ctx');
+ expect(wrapper).toBeTruthy();
+
+ // 300ms 后检查,DOM 应该被移除
+ vi.advanceTimersByTime(300);
+ wrapper = document.body.querySelector('.t-dialog__ctx');
+ expect(wrapper).toBeFalsy();
+ });
+
+ it('should not destroy dialog when destroyOnClose is false', async () => {
+ const dialog = app.config.globalProperties.$dialog({
+ body: 'no destroy test',
+ destroyOnClose: false,
+ });
+ await nextTick();
+
+ // 获取对话框元素
+ let wrapper = document.body.querySelector('.t-dialog__ctx');
+ expect(wrapper).toBeTruthy();
+
+ // 模拟点击关闭按钮触发默认的 onClose 逻辑
+ const closeBtn = wrapper.querySelector('.t-dialog__close');
+ expect(closeBtn).toBeTruthy();
+ (closeBtn as HTMLElement).click();
+ await nextTick();
+
+ // 300ms 后检查,DOM 仍然存在(因为 destroyOnClose: false)
+ vi.advanceTimersByTime(300);
+ wrapper = document.body.querySelector('.t-dialog__ctx');
+ expect(wrapper).toBeTruthy();
+
+ // 手动销毁
+ dialog.destroy();
+ vi.runAllTimers();
+ });
+});
diff --git a/packages/components/dialog/__tests__/dialog.test.tsx b/packages/components/dialog/__tests__/dialog.test.tsx
index 9e5d4dad26..33a88c83b0 100644
--- a/packages/components/dialog/__tests__/dialog.test.tsx
+++ b/packages/components/dialog/__tests__/dialog.test.tsx
@@ -1,346 +1,945 @@
// @ts-nocheck
+import { nextTick, ref } from 'vue';
+import type { Ref } from 'vue';
import { mount } from '@vue/test-utils';
-import { describe, expect, it, vi } from 'vitest';
-import { ref, nextTick } from 'vue';
+import type { VueWrapper } from '@vue/test-utils';
+import { describe, expect, it, vi, beforeEach } from 'vitest';
import { CloseIcon } from 'tdesign-icons-vue-next';
import Dialog from '@tdesign/components/dialog';
-describe('Dialog', () => {
- describe(':props', () => {
- it('', async () => {
- const visible = ref(true);
- const wrapper = mount(() => );
- const body = wrapper.find('.t-dialog .t-dialog__body');
- await nextTick();
- expect(body.exists()).toBeTruthy();
- expect(body.text()).toBe('this is content');
- });
+describe('props', () => {
+ let wrapper: VueWrapper> | null = null;
+ let visible: Ref;
- it('default', async () => {
- const visible = ref(true);
- const wrapper = mount(() => );
- const body = wrapper.find('.t-dialog .t-dialog__body');
- await nextTick();
- expect(body.exists()).toBeTruthy();
- expect(body.text()).toBe('this is content');
- });
+ beforeEach(() => {
+ visible = ref(true);
+ wrapper = mount(() => ) as VueWrapper<
+ InstanceType
+ >;
+ });
- it('default', async () => {
- const visible = ref(true);
- const wrapper = mount(() => );
- const body = wrapper.find('.t-dialog .t-dialog__body');
- await nextTick();
- expect(body.exists()).toBeTruthy();
- expect(body.text()).toBe('this is content');
- });
+ it('renders dialog content through default slot', async () => {
+ const body = wrapper.find('.t-dialog .t-dialog__body');
+ await nextTick();
+ expect(body.exists()).toBeTruthy();
+ expect(body.text()).toBe('this is content');
+ });
- it(':cancelBtn', async () => {
- const visible = ref(true);
- const wrapper = mount(() => );
- const btn = wrapper.find('.t-dialog__footer .t-dialog__cancel');
- await nextTick();
- expect(btn.exists()).toBeTruthy();
- expect(btn.text()).toBe('取消');
- });
+ it(':body - renders content through body prop as string', async () => {
+ const wrapper = mount(() => );
+ const body = wrapper.find('.t-dialog .t-dialog__body');
+ await nextTick();
+ expect(body.exists()).toBeTruthy();
+ expect(body.text()).toBe('this is content');
+ });
- it(':confirmBtn', async () => {
- const visible = ref(true);
- const wrapper = mount(() => );
- const btn = wrapper.find('.t-dialog__footer .t-dialog__confirm');
- await nextTick();
- expect(btn.exists()).toBeTruthy();
- expect(btn.text()).toBe('确认');
- });
+ it(':body - renders content through body prop as function', async () => {
+ const bodyFn = () => 'function content';
+ const wrapper = mount(() => );
+ const body = wrapper.find('.t-dialog .t-dialog__body');
+ await nextTick();
+ expect(body.exists()).toBeTruthy();
+ expect(body.text()).toBe('function content');
+ });
- it(':closeBtn', async () => {
- const visible = ref(true);
- const wrapper = mount(() => );
- const close = wrapper.find('.t-dialog__close');
- await nextTick();
- expect(close.exists()).toBeTruthy();
- expect(close.findComponent(CloseIcon).exists()).toBeTruthy();
- });
+ it(':default prop works same as body', async () => {
+ const wrapper = mount(() => );
+ const body = wrapper.find('.t-dialog .t-dialog__body');
+ await nextTick();
+ expect(body.exists()).toBeTruthy();
+ expect(body.text()).toBe('default content');
+ });
- it(':footer', async () => {
- const visible = ref(true);
- const wrapper = mount(() => );
- const footer = wrapper.find('.t-dialog__footer');
- await nextTick();
- expect(footer.exists()).toBeTruthy();
- expect(footer.findAll('button').length).toBe(2);
- });
+ it(':attach - attaches dialog to specified element', async () => {
+ const attachElement = document.createElement('div');
+ attachElement.id = 'attach-container';
+ document.body.appendChild(attachElement);
- it(':header', async () => {
- const visible = ref(true);
- const wrapper = mount(() => (
-
- ));
- const header = wrapper.find('.t-dialog__header');
- await nextTick();
- expect(header.exists()).toBeTruthy();
- expect(header.text()).toBe('this is header');
- });
+ mount(() => );
+ await nextTick();
- it(':header:false', async () => {
- const visible = ref(true);
- const wrapper = mount(() => (
-
- ));
- const header = wrapper.find('.t-dialog__header');
- await nextTick();
- expect(header.exists()).toBeFalsy();
- });
+ expect(attachElement.children.length).toBeGreaterThan(0);
+ document.body.removeChild(attachElement);
+ });
- it(':placement', async () => {
- const placementList = ['top', 'center'];
- const visible = ref(true);
- placementList.forEach(async (placement) => {
- const wrapper = mount(() => (
-
- ));
- const dialog = wrapper.find('.t-dialog');
- await nextTick();
- expect(dialog.exists()).toBeTruthy();
- expect(dialog.classes()).toContain(`t-dialog--${placement}`);
- });
- });
+ it(':attach - supports function type', async () => {
+ const attachElement = document.createElement('div');
+ document.body.appendChild(attachElement);
- it(':mode:modeless', async () => {
- const visible = ref(true);
- const wrapper = mount(() => (
-
- ));
- const ctx = wrapper.find('.t-dialog__ctx');
- await nextTick();
- expect(ctx.exists()).toBeTruthy();
- expect(ctx.classes()).toContain('t-dialog__ctx--modeless');
- });
+ mount(() => );
+ await nextTick();
- it(':mode:normal', async () => {
- const visible = ref(true);
- const wrapper = mount(() => (
-
- ));
- const ctx = wrapper.find('.t-dialog__ctx');
- await nextTick();
- expect(ctx.find('.t-dialog__position').exists()).toBeFalsy();
- });
+ expect(attachElement.children.length).toBeGreaterThan(0);
+ document.body.removeChild(attachElement);
+ });
- it(':mode:full-screen', async () => {
- const visible = ref(true);
- const wrapper = mount(() => (
-
- ));
- const ctx = wrapper.find('.t-dialog__ctx');
- await nextTick();
- expect(ctx.find('.t-dialog__position_fullscreen').exists()).toBeTruthy();
- });
+ it(':cancelBtn - renders default cancel button', async () => {
+ const btn = wrapper.find('.t-dialog__footer .t-dialog__cancel');
+ await nextTick();
+ expect(btn.exists()).toBeTruthy();
+ expect(btn.text()).toBe('取消');
+ });
- it(':showOverlay', async () => {
- const visible = ref(true);
- const wrapper = mount(() => );
- const ctx = wrapper.find('.t-dialog__ctx');
- await nextTick();
- expect(ctx.find('.t-dialog__mask').exists()).toBeTruthy();
- });
+ it(':cancelBtn - renders custom cancel button with string', async () => {
+ const wrapper = mount(() => (
+
+ ));
+ const btn = wrapper.find('.t-dialog__footer .t-dialog__cancel');
+ await nextTick();
+ expect(btn.exists()).toBeTruthy();
+ expect(btn.text()).toBe('自定义取消');
+ });
- it(':theme', async () => {
- const themeList = ['default', 'success', 'info', 'warning', 'danger'];
- const visible = ref(true);
- themeList.forEach(async (theme) => {
- const wrapper = mount(() => (
-
- ));
- const dialog = wrapper.find('.t-dialog');
- await nextTick();
- expect(dialog.classes()).toContain(`t-dialog__modal-${theme}`);
- });
- });
+ it(':cancelBtn - renders custom cancel button with object', async () => {
+ const wrapper = mount(() => (
+
+ ));
+ const btn = wrapper.find('.t-dialog__footer .t-dialog__cancel');
+ await nextTick();
+ expect(btn.exists()).toBeTruthy();
+ expect(btn.text()).toBe('自定义取消');
+ expect(btn.classes()).toContain('t-button--theme-danger');
+ });
- it(':top', async () => {
- const visible = ref(true);
- const wrapper = mount(() => );
- const top = wrapper.find('.t-dialog--top');
- await nextTick();
- expect(getComputedStyle(top.element, null).paddingTop).toBe('200px');
- });
+ it(':cancelBtn - hides cancel button when null', async () => {
+ const wrapper = mount(() => (
+
+ ));
+ const btn = wrapper.find('.t-dialog__footer .t-dialog__cancel');
+ await nextTick();
+ expect(btn.exists()).toBeFalsy();
+ });
- it(':width', async () => {
- const visible = ref(true);
- const wrapper = mount(() => );
- const dialog = wrapper.find('.t-dialog');
- await nextTick();
- expect(getComputedStyle(dialog.element, null).width).toBe('80%');
- });
+ it(':cancelBtn - renders custom cancel button with function', async () => {
+ const cancelBtnFn = () => ;
+ const wrapper = mount(() => (
+
+ ));
+ await nextTick();
+ expect(wrapper.find('.custom-cancel').exists()).toBeTruthy();
+ });
- it(':zIndex', async () => {
- const visible = ref(true);
- const wrapper = mount(() => (
-
- ));
- const ctx = wrapper.find('.t-dialog__ctx');
- await nextTick();
- expect(getComputedStyle(ctx.element, null).zIndex).toBe('2022');
- });
+ it(':confirmBtn - renders default confirm button', async () => {
+ const btn = wrapper.find('.t-dialog__footer .t-dialog__confirm');
+ await nextTick();
+ expect(btn.exists()).toBeTruthy();
+ expect(btn.text()).toBe('确认');
+ });
- it(':draggable', async () => {
- const visible = ref(true);
- const wrapper = mount(() => (
-
- ));
- const dialog = wrapper.find('.t-dialog');
- await nextTick();
- expect(dialog.classes()).toContain('t-dialog--draggable');
- });
+ it(':confirmBtn - renders custom confirm button with string', async () => {
+ const wrapper = mount(() => (
+
+ ));
+ const btn = wrapper.find('.t-dialog__footer .t-dialog__confirm');
+ await nextTick();
+ expect(btn.exists()).toBeTruthy();
+ expect(btn.text()).toBe('自定义确认');
+ });
- it(':draggable', async () => {
- const visible = ref(true);
- const wrapper = mount(() => (
-
- ));
- const dialog = wrapper.find('.t-dialog');
- await nextTick();
- expect(dialog.classes()).toContain('t-dialog--draggable');
- });
+ it(':confirmBtn - renders custom confirm button with object', async () => {
+ const wrapper = mount(() => (
+
+ ));
+ const btn = wrapper.find('.t-dialog__footer .t-dialog__confirm');
+ await nextTick();
+ expect(btn.exists()).toBeTruthy();
+ expect(btn.text()).toBe('自定义确认');
+ expect(btn.classes()).toContain('t-button--theme-success');
+ });
- it(':dialogClassName', async () => {
- const visible = ref(true);
- const wrapper = mount(() => (
-
- ));
- const dialog = wrapper.find('.t-dialog');
- await nextTick();
- expect(dialog.classes()).toContain('custom-class');
- });
+ it(':confirmBtn - hides confirm button when null', async () => {
+ const wrapper = mount(() => (
+
+ ));
+ const btn = wrapper.find('.t-dialog__footer .t-dialog__confirm');
+ await nextTick();
+ expect(btn.exists()).toBeFalsy();
+ });
- it(':dialogStyle', async () => {
- const visible = ref(true);
- const wrapper = mount(() => (
-
- ));
- const dialog = wrapper.find('.t-dialog');
- await nextTick();
- expect(getComputedStyle(dialog.element, null).padding).toBe('99px');
- });
+ it(':confirmBtn - renders custom confirm button with function', async () => {
+ const confirmBtnFn = () => ;
+ const wrapper = mount(() => (
+
+ ));
+ await nextTick();
+ expect(wrapper.find('.custom-confirm').exists()).toBeTruthy();
+ });
+
+ it(':closeBtn - renders default close button', async () => {
+ const close = wrapper.find('.t-dialog__close');
+ await nextTick();
+ expect(close.exists()).toBeTruthy();
+ expect(close.findComponent(CloseIcon).exists()).toBeTruthy();
+ });
+
+ it(':closeBtn - hides close button when false', async () => {
+ const wrapper = mount(() => (
+
+ ));
+ const close = wrapper.find('.t-dialog__close');
+ await nextTick();
+ expect(close.exists()).toBeFalsy();
+ });
+
+ it(':closeBtn - renders custom close button with string', async () => {
+ const wrapper = mount(() => (
+
+ ));
+ const close = wrapper.find('.t-dialog__close');
+ await nextTick();
+ expect(close.exists()).toBeTruthy();
+ expect(close.text()).toBe('关闭');
+ });
+
+ it(':closeBtn - renders custom close button with function', async () => {
+ const closeBtnFn = () => ×;
+ const wrapper = mount(() => (
+
+ ));
+ await nextTick();
+ expect(wrapper.find('.custom-close').exists()).toBeTruthy();
+ });
+
+ it(':closeOnEscKeydown - true by default, closes dialog on ESC', async () => {
+ const onClose = vi.fn();
+ mount(() => );
+ await nextTick();
+
+ // 模拟按下ESC键
+ const escEvent = new KeyboardEvent('keydown', { code: 'Escape' });
+ document.dispatchEvent(escEvent);
+
+ expect(onClose).toHaveBeenCalledWith(expect.objectContaining({ trigger: 'esc' }));
+ });
+
+ it(':closeOnEscKeydown - false prevents closing on ESC', async () => {
+ const onClose = vi.fn();
+ mount(() => (
+
+ ));
+ await nextTick();
+
+ const escEvent = new KeyboardEvent('keydown', { code: 'Escape' });
+ document.dispatchEvent(escEvent);
+
+ expect(onClose).not.toHaveBeenCalled();
+ });
+
+ it(':closeOnOverlayClick - true by default, closes dialog on overlay click', async () => {
+ const onClose = vi.fn();
+ const wrapper = mount(() => (
+
+ ));
+ await nextTick();
+
+ const wrap = wrapper.find('.t-dialog__position');
+ await wrap.trigger('mousedown');
+ await wrap.trigger('mouseup');
+ await wrap.trigger('click');
+ await nextTick();
+
+ expect(visible.value).toBe(false); // 检查状态变化
+ expect(onClose).toHaveBeenCalledTimes(1); // 确保回调触发
+ expect(onClose).toHaveBeenCalledWith(
+ expect.objectContaining({ trigger: 'overlay' }), // 验证回调参数
+ );
+ });
+
+ it(':closeOnOverlayClick - false prevents closing on overlay click', async () => {
+ const onClose = vi.fn();
+ const wrapper = mount(() => (
+
+ ));
+ await nextTick();
- it('update dialog confirmBtnLoading', async () => {
- const visible = ref(true);
- const loading = ref(false);
+ const wrap = wrapper.find('.t-dialog__position');
+ await wrap.trigger('mousedown');
+ await wrap.trigger('mouseup');
+ await wrap.trigger('click');
+ await nextTick();
+
+ expect(visible.value).toBe(true); // 检查状态变化
+ expect(onClose).not.toHaveBeenCalled();
+ });
+
+ it(':confirmLoading - shows loading state on confirm button', async () => {
+ const confirmLoading = ref(true);
+ const wrapper = mount(() => (
+
+ ));
+ await nextTick();
+
+ const confirmBtn = wrapper.find('.t-dialog__confirm');
+ expect(confirmBtn.classes()).toContain('t-is-loading');
+ });
+
+ it(':confirmOnEnter - triggers confirm on Enter key', async () => {
+ visible = ref(true);
+ const onConfirm = vi.fn();
+ mount(() => (
+
+ ));
+ await nextTick();
+
+ // 触发 Enter 键,确保 event.target 存在,避免 undefined 报错
+ const enterEvent = new KeyboardEvent('keydown', { code: 'Enter' });
+ Object.defineProperty(enterEvent, 'target', { value: document.body });
+ document.dispatchEvent(enterEvent);
+
+ await nextTick();
+
+ expect(onConfirm).toHaveBeenCalled();
+ });
+
+ it(':confirmOnEnter - triggers confirm on NumpadEnter key', async () => {
+ visible = ref(true);
+ const onConfirm = vi.fn();
+ mount(() => (
+
+ ));
+ await nextTick();
+
+ // 触发 NumpadEnter 键,确保 event.target 存在,避免 undefined 报错
+ const enterEvent = new KeyboardEvent('keydown', { code: 'NumpadEnter' });
+ Object.defineProperty(enterEvent, 'target', { value: document.body });
+ document.dispatchEvent(enterEvent);
+
+ await nextTick();
+
+ expect(onConfirm).toHaveBeenCalled();
+ });
+
+ it(':confirmOnEnter - does not trigger confirm when target is input', async () => {
+ const onConfirm = vi.fn();
+ const wrapper = mount(() => (
+
+ ));
+ await nextTick();
+
+ const input = wrapper.find('.test-input');
+ const enterEvent = new KeyboardEvent('keydown', { code: 'Enter' });
+ Object.defineProperty(enterEvent, 'target', { value: input.element });
+ document.dispatchEvent(enterEvent);
+
+ expect(onConfirm).not.toHaveBeenCalled();
+ });
+
+ it(':destroyOnClose - destroys content when dialog closes', async () => {
+ visible = ref(false);
+ const wrapper = mount(() => (
+
+ ));
+ visible.value = true;
+ await nextTick();
+
+ expect(wrapper.find('.t-dialog__ctx').exists()).toBeTruthy();
+
+ visible.value = false;
+ await nextTick();
+
+ expect(document.querySelector('.t-dialog__ctx')).toBeNull();
+ });
+
+ it(':footer - renders default footer', async () => {
+ const footer = wrapper.find('.t-dialog__footer');
+ await nextTick();
+ expect(footer.exists()).toBeTruthy();
+ expect(footer.findAll('button').length).toBe(2);
+ });
+
+ it(':footer - hides footer when false', async () => {
+ const wrapper = mount(() => (
+
+ ));
+ const footer = wrapper.find('.t-dialog__footer');
+ await nextTick();
+ expect(footer.exists()).toBeFalsy();
+ });
+
+ it(':footer - renders custom footer with function', async () => {
+ const footerFn = () => ;
+ const wrapper = mount(() => (
+
+ ));
+ await nextTick();
+ expect(wrapper.find('.custom-footer').exists()).toBeTruthy();
+ });
+
+ it(':header - renders default header', async () => {
+ const wrapper = mount(() => (
+
+ ));
+ const header = wrapper.find('.t-dialog__header');
+ await nextTick();
+ expect(header.exists()).toBeTruthy();
+ expect(header.text()).toBe('this is header');
+ });
+
+ it(':header - hides header when false', async () => {
+ const wrapper = mount(() => (
+
+ ));
+ const header = wrapper.find('.t-dialog__header');
+ await nextTick();
+ expect(header.exists()).toBeFalsy();
+ });
+
+ it(':header - renders custom header with function', async () => {
+ const headerFn = () => ;
+ const wrapper = mount(() => (
+
+ ));
+ await nextTick();
+ expect(wrapper.find('.custom-header').exists()).toBeTruthy();
+ });
+
+ it(':lazy - does not render content when lazy=true and visible=false', async () => {
+ const invisibleVisible = ref(false);
+ const wrapper = mount(() => );
+ await nextTick();
+ expect(wrapper.find('.t-dialog__body').exists()).toBeFalsy();
+ });
+
+ it(':lazy - renders content after first open when lazy=true', async () => {
+ const lazyVisible = ref(false);
+ const wrapper = mount(() => );
+ await nextTick();
+
+ lazyVisible.value = true;
+ await nextTick();
+ expect(wrapper.find('.t-dialog__body').exists()).toBeTruthy();
+
+ lazyVisible.value = false;
+ await nextTick();
+ expect(wrapper.find('.t-dialog__body').exists()).toBeTruthy(); // 仍然存在
+ });
+
+ it(':mode - renders normal dialog', async () => {
+ const wrapper = mount(() => );
+ const ctx = wrapper.find('.t-dialog__ctx');
+ await nextTick();
+ expect(ctx.find('.t-dialog__position').exists()).toBeFalsy();
+ });
+
+ it(':mode - renders modal dialog by default', async () => {
+ const ctx = wrapper.find('.t-dialog__ctx');
+ expect(ctx.find('.t-dialog__position').exists()).toBeTruthy();
+ });
+
+ it(':mode - renders modeless dialog', async () => {
+ const wrapper = mount(() => (
+
+ ));
+ const ctx = wrapper.find('.t-dialog__ctx');
+ await nextTick();
+ expect(ctx.exists()).toBeTruthy();
+ expect(ctx.classes()).toContain('t-dialog__ctx--modeless');
+ });
+
+ it(':mode - renders normal dialog', async () => {
+ const wrapper = mount(() => );
+ const ctx = wrapper.find('.t-dialog__ctx');
+ await nextTick();
+ expect(ctx.find('.t-dialog__position').exists()).toBeFalsy();
+ });
+
+ it(':mode - renders full-screen dialog', async () => {
+ const wrapper = mount(() => (
+
+ ));
+ const ctx = wrapper.find('.t-dialog__ctx');
+ await nextTick();
+ expect(ctx.find('.t-dialog__position_fullscreen').exists()).toBeTruthy();
+ });
+
+ it(':placement - renders top placement by default', async () => {
+ const dialog = wrapper.find('.t-dialog');
+ await nextTick();
+ expect(dialog.classes()).toContain('t-dialog--top');
+ });
+
+ it(':placement - renders by placement', async () => {
+ const placements = ['top', 'center', ''];
+ for (const placement of placements) {
const wrapper = mount(() => (
-
+
));
const dialog = wrapper.find('.t-dialog');
await nextTick();
- expect(dialog.find('.t-button--theme-primary.t-is-loading.t-dialog__confirm').exists()).toBeFalsy();
- loading.value = true;
- await nextTick();
- const updateDialog = wrapper.find('.t-dialog');
- expect(updateDialog.find('.t-button--theme-primary.t-is-loading.t-dialog__confirm').exists()).toBeTruthy();
- });
+ if (placement === 'top') {
+ expect(dialog.classes()).toContain('t-dialog--top');
+ } else if (placement === 'center') {
+ expect(dialog.classes()).toContain('t-dialog--center');
+ }
+ }
+ });
+
+ it(':preventScrollThrough - prevents scroll through by default', async () => {
+ visible = ref(false);
+ mount(() => );
+ visible.value = true;
+ await nextTick();
+ const styleElements = document.body.querySelectorAll('style');
+ expect(styleElements.length).toBeGreaterThan(0);
+ });
+
+ it(':showInAttachedElement - shows dialog in attached element', async () => {
+ const attachElement = document.createElement('div');
+ attachElement.style.position = 'relative';
+ document.body.appendChild(attachElement);
+
+ mount(() => (
+
+ ));
+ await nextTick();
+
+ expect(attachElement.children.length).toBeGreaterThan(0);
+ document.body.removeChild(attachElement);
+ });
+
+ it(':showOverlay - shows overlay by default', async () => {
+ const ctx = wrapper.find('.t-dialog__ctx');
+ await nextTick();
+ expect(ctx.find('.t-dialog__mask').exists()).toBeTruthy();
+ });
+
+ it(':showOverlay - hides overlay when false', async () => {
+ const wrapper = mount(() => (
+
+ ));
+ const mask = wrapper.find('.t-dialog__mask');
+ await nextTick();
+ expect(mask.classes()).toContain('t-is-hidden');
+ });
+
+ it(':theme - renders different themes', async () => {
+ const themeList = ['default', 'success', 'info', 'warning', 'danger', ''];
- it('drag dialog', async () => {
- const visible = ref(true);
+ for (const theme of themeList) {
const wrapper = mount(() => (
-
+
));
- await nextTick();
const dialog = wrapper.find('.t-dialog');
- const dialogElement = dialog.element;
- dialogElement.style.position = 'absolute';
- dialogElement.style.left = '100px';
- dialogElement.style.top = '100px';
- dialogElement.style.width = '500px';
- dialogElement.style.height = '300px';
- const initialLeft = parseInt(getComputedStyle(dialogElement).left, 10);
- const initialTop = parseInt(getComputedStyle(dialogElement).top, 10);
- const mousedownEvent = new MouseEvent('mousedown', { clientX: 100, clientY: 100 });
- const mousemoveEvent = new MouseEvent('mousemove', { clientX: 150, clientY: 150 });
- const mouseupEvent = new MouseEvent('mouseup');
- dialogElement.dispatchEvent(mousedownEvent);
- document.dispatchEvent(mousemoveEvent);
- document.dispatchEvent(mouseupEvent);
await nextTick();
- const finalLeft = parseInt(getComputedStyle(dialogElement).left, 10);
- const finalTop = parseInt(getComputedStyle(dialogElement).top, 10);
- expect(finalLeft).not.toBe(initialLeft);
- expect(finalTop).not.toBe(initialTop);
- });
+ expect(dialog.classes()).toContain(`t-dialog__modal-${theme}`);
+ }
});
- describe(':events', () => {
- it(':onCancel', async () => {
- const visible = ref(true);
- const fn = vi.fn();
- const wrapper = mount(() => (
-
- ));
- const btn = wrapper.find('.t-dialog__footer .t-dialog__cancel');
- await nextTick();
- await btn.trigger('click');
- expect(fn).toBeCalled();
- });
+ it(':top - sets custom top position', async () => {
+ const wrapper = mount(() => );
+ const position = wrapper.find('.t-dialog__position');
+ await nextTick();
+ expect(getComputedStyle(position.element, null).paddingTop).toBe('200px');
+ });
- it(':onConfirm', async () => {
- const visible = ref(true);
- const fn = vi.fn();
- const wrapper = mount(() => (
-
- ));
- const btn = wrapper.find('.t-dialog__footer .t-dialog__confirm');
- await nextTick();
- await btn.trigger('click');
- expect(fn).toBeCalled();
- });
+ it(':top - supports number type', async () => {
+ const wrapper = mount(() => );
+ const position = wrapper.find('.t-dialog__position');
+ await nextTick();
+ expect(getComputedStyle(position.element, null).paddingTop).toBe('300px');
+ });
- it(':onClose', async () => {
- const visible = ref(true);
- const fn = vi.fn();
- const wrapper = mount(() => (
-
- ));
- const btn = wrapper.find('.t-dialog__close');
- await nextTick();
- await btn.trigger('click');
- expect(fn).toBeCalled();
- });
+ it(':width - sets custom width with percentage', async () => {
+ const wrapper = mount(() => );
+ const dialog = wrapper.find('.t-dialog');
+ await nextTick();
+ expect(getComputedStyle(dialog.element, null).width).toBe('80%');
+ });
- it(':onCloseBtnClick', async () => {
- const visible = ref(true);
- const fn = vi.fn();
- const wrapper = mount(() => (
-
- ));
- const btn = wrapper.find('.t-dialog__close');
- await nextTick();
- await btn.trigger('click');
- expect(fn).toBeCalled();
+ it(':width - sets custom width with pixels', async () => {
+ const wrapper = mount(() => );
+ const dialog = wrapper.find('.t-dialog');
+ await nextTick();
+ expect(getComputedStyle(dialog.element, null).width).toBe('500px');
+ });
+
+ it(':width - supports number type', async () => {
+ const wrapper = mount(() => );
+ const dialog = wrapper.find('.t-dialog');
+ await nextTick();
+ expect(getComputedStyle(dialog.element, null).width).toBe('600px');
+ });
+
+ it(':zIndex - sets custom z-index', async () => {
+ const wrapper = mount(() => );
+ const ctx = wrapper.find('.t-dialog__ctx');
+ await nextTick();
+ expect(getComputedStyle(ctx.element, null).zIndex).toBe('2022');
+ });
+
+ it(':draggable - enables dragging for modeless dialog', async () => {
+ const wrapper = mount(() => (
+
+ ));
+ const dialog = wrapper.find('.t-dialog');
+ await nextTick();
+ expect(dialog.classes()).toContain('t-dialog--draggable');
+ });
+
+ it(':dialogClassName - adds custom class', async () => {
+ const wrapper = mount(() => (
+
+ ));
+ const dialog = wrapper.find('.t-dialog');
+ await nextTick();
+ expect(dialog.classes()).toContain('custom-class');
+ });
+
+ it(':dialogStyle - applies custom styles', async () => {
+ const wrapper = mount(() => (
+
+ ));
+ const dialog = wrapper.find('.t-dialog');
+ await nextTick();
+ expect(getComputedStyle(dialog.element, null).padding).toBe('99px');
+ });
+
+ it('updates confirmBtnLoading reactively', async () => {
+ const loading = ref(false);
+ const wrapper = mount(() => (
+
+ ));
+ const dialog = wrapper.find('.t-dialog');
+ await nextTick();
+ expect(dialog.find('.t-button--theme-primary.t-is-loading.t-dialog__confirm').exists()).toBeFalsy();
+
+ loading.value = true;
+ await nextTick();
+ const updateDialog = wrapper.find('.t-dialog');
+ expect(updateDialog.find('.t-button--theme-primary.t-is-loading.t-dialog__confirm').exists()).toBeTruthy();
+ });
+
+ it('supports drag functionality', async () => {
+ const wrapper = mount(() => (
+
+ ));
+ await nextTick();
+
+ const dialog = wrapper.find('.t-dialog');
+ const dialogElement = dialog.element;
+
+ // 设置初始样式
+ dialogElement.style.position = 'absolute';
+ dialogElement.style.left = '100px';
+ dialogElement.style.top = '100px';
+ dialogElement.style.width = '500px';
+ dialogElement.style.height = '300px';
+
+ const initialLeft = parseInt(getComputedStyle(dialogElement).left, 10);
+ const initialTop = parseInt(getComputedStyle(dialogElement).top, 10);
+
+ // 模拟拖拽事件
+ const mousedownEvent = new MouseEvent('mousedown', { clientX: 100, clientY: 100 });
+ const mousemoveEvent = new MouseEvent('mousemove', { clientX: 150, clientY: 150 });
+ const mouseupEvent = new MouseEvent('mouseup');
+
+ dialogElement.dispatchEvent(mousedownEvent);
+ document.dispatchEvent(mousemoveEvent);
+ document.dispatchEvent(mouseupEvent);
+
+ await nextTick();
+
+ const finalLeft = parseInt(getComputedStyle(dialogElement).left, 10);
+ const finalTop = parseInt(getComputedStyle(dialogElement).top, 10);
+
+ expect(finalLeft).not.toBe(initialLeft);
+ expect(finalTop).not.toBe(initialTop);
+ });
+
+ it('mode prop: invalid value should trigger warning', () => {
+ const visible = ref(true);
+ const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
+ mount(() => );
+ expect(spy).toHaveBeenCalled();
+ spy.mockRestore();
+ });
+
+ it('placement prop: invalid value should trigger warning', () => {
+ const visible = ref(true);
+ const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
+ mount(() => );
+ expect(spy).toHaveBeenCalled();
+ spy.mockRestore();
+ });
+
+ it('theme prop: invalid value should trigger warning', () => {
+ const visible = ref(true);
+ const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
+ mount(() => );
+ expect(spy).toHaveBeenCalled();
+ spy.mockRestore();
+ });
+});
+
+describe('events', () => {
+ it('onCancel - triggers when cancel button is clicked', async () => {
+ const visible = ref(false);
+ const onCancel = vi.fn();
+ const wrapper = mount(() => (
+
+ ));
+
+ visible.value = true;
+ await nextTick();
+
+ const btn = wrapper.find('.t-dialog__footer .t-dialog__cancel');
+ await nextTick();
+ await btn.trigger('click');
+ expect(onCancel).toHaveBeenCalled();
+ });
+
+ it('onConfirm - triggers when confirm button is clicked', async () => {
+ const visible = ref(false);
+ const onConfirm = vi.fn();
+ const wrapper = mount(() => (
+
+ ));
+ visible.value = true;
+ await nextTick();
+
+ const btn = wrapper.find('.t-dialog__footer .t-dialog__confirm');
+ await nextTick();
+ await btn.trigger('click');
+
+ expect(onConfirm).toHaveBeenCalled();
+ });
+
+ it('onClose - triggers when dialog is closed', async () => {
+ const visible = ref(false);
+ const onClose = vi.fn();
+ const wrapper = mount(() => (
+
+ ));
+ visible.value = true;
+ await nextTick();
+
+ const btn = wrapper.find('.t-dialog__close');
+ await nextTick();
+ await btn.trigger('click');
+
+ expect(onClose).toHaveBeenCalledWith(expect.objectContaining({ trigger: 'close-btn' }));
+ });
+
+ it('onCloseBtnClick - triggers when close button is clicked', async () => {
+ const visible = ref(false);
+ const onCloseBtnClick = vi.fn();
+ const wrapper = mount(() => (
+
+ ));
+ visible.value = true;
+ await nextTick();
+
+ const btn = wrapper.find('.t-dialog__close');
+ await nextTick();
+ await btn.trigger('click');
+
+ expect(onCloseBtnClick).toHaveBeenCalled();
+ });
+
+ it('onEscKeydown - triggers when ESC key is pressed', async () => {
+ const visible = ref(false);
+ const onEscKeydown = vi.fn();
+ mount(() => );
+ visible.value = true;
+ await nextTick();
+
+ const escEvent = new KeyboardEvent('keydown', {
+ code: 'Escape',
});
+ Object.defineProperty(escEvent, 'target', { value: document.body }); // 保证 target 存在
+ document.dispatchEvent(escEvent);
+
+ expect(onEscKeydown).toHaveBeenCalled();
+ });
+
+ it('onOverlayClick - triggers when overlay is clicked', async () => {
+ const visible = ref(true);
+ const onOverlayClick = vi.fn();
+ const wrapper = mount(() => (
+
+ ));
+ await nextTick();
+
+ const mask = wrapper.find('.t-dialog__position');
+ await mask.trigger('mousedown');
+ await mask.trigger('mouseup');
+ await mask.trigger('click');
+
+ expect(onOverlayClick).toHaveBeenCalled();
+ });
+
+ it('closes dialog when cancel button is clicked', async () => {
+ const visible = ref(false);
+ const onClose = vi.fn();
+ const wrapper = mount(() => (
+
+ ));
+ visible.value = true;
+ await nextTick();
+
+ const cancelBtn = wrapper.find('.t-dialog__cancel');
+ await cancelBtn.trigger('click');
+
+ expect(onClose).toHaveBeenCalledWith(expect.objectContaining({ trigger: 'cancel' }));
+ });
+
+ // it('onBeforeOpen - triggers before dialog is opened', async () => {
+ // const visible = ref(false);
+ // const onBeforeOpen = vi.fn();
+ // const wrapper = mount(() => (
+ //
+ // ));
+ // visible.value = true;
+ // await nextTick(); // 确保 DOM 更新
+ // expect(onBeforeOpen).toHaveBeenCalled();
+ // });
+
+ it('onOpened - triggers after dialog is opened', async () => {
+ const visible = ref(false);
+ const onOpened = vi.fn();
+ const wrapper = mount(() => (
+
+ ));
+
+ visible.value = true;
+ await nextTick();
+
+ // 模拟动画结束事件,触发 afterEnter 回调
+ const transition = wrapper.findComponent({ name: 'Transition' });
+ if (transition.exists()) {
+ await transition.vm.$emit('after-enter');
+ }
+
+ expect(onOpened).toHaveBeenCalled();
+ });
+
+ // it('onBeforeClose - triggers before dialog is closed', async () => {
+ // const visible = ref(true);
+ // const onBeforeClose = vi.fn();
+ // const wrapper = mount(() => (
+ //
+ // ));
+ // visible.value = false;
+ // await nextTick();
+ // expect(onBeforeClose).toHaveBeenCalled();
+ // });
+
+ // it('onClosed - triggers after dialog is closed', async () => {
+ // const visible = ref(true);
+ // const onClosed = vi.fn();
+ // const wrapper = mount(() => (
+ //
+ // ));
+ // visible.value = false;
+ // await nextTick(); // 确保 DOM 更新
+ // expect(onClosed).toHaveBeenCalled();
+ // });
+});
+
+describe('special scenarios', () => {
+ it('handles multiple dialogs with different z-index', async () => {
+ const visible1 = ref(false);
+ const visible2 = ref(false);
+
+ const wrapper1 = mount(() => );
+ const wrapper2 = mount(() => );
+
+ visible1.value = true;
+ visible2.value = true;
+ await nextTick();
+
+ expect(getComputedStyle(wrapper1.find('.t-dialog__ctx').element).zIndex).toBe('1000');
+ expect(getComputedStyle(wrapper2.find('.t-dialog__ctx').element).zIndex).toBe('2000');
+ });
+
+ it('handles keyboard events only for top dialog', async () => {
+ const visible1 = ref(true);
+ const visible2 = ref(true);
+ const onEscKeydown1 = vi.fn();
+ const onEscKeydown2 = vi.fn();
+
+ mount(() => (
+
+ ));
+ mount(() => (
+
+ ));
+
+ await nextTick();
+
+ // 修复 eventSrc 可能为 undefined 的情况
+ const escEvent = new KeyboardEvent('keydown', { code: 'Escape' });
+ Object.defineProperty(escEvent, 'target', { value: document.body }); // 保证 target 存在
+ document.dispatchEvent(escEvent);
+
+ // 只有顶层dialog应该响应ESC事件
+ expect(onEscKeydown2).toHaveBeenCalled();
+ expect(onEscKeydown1).not.toHaveBeenCalled();
+ });
+
+ it('cleans up styles when dialog closes', async () => {
+ const visible = ref(true);
+ mount(() => (
+
+ ));
+
+ await nextTick();
+ const initialStyleCount = document.querySelectorAll('style').length;
+
+ visible.value = false;
+ await nextTick();
+ await new Promise((resolve) => setTimeout(resolve, 200)); // 等待清理
+
+ const finalStyleCount = document.querySelectorAll('style').length;
+ expect(finalStyleCount).toBeLessThanOrEqual(initialStyleCount);
});
});
diff --git a/packages/components/dialog/__tests__/dialog.utils.test.tsx b/packages/components/dialog/__tests__/dialog.utils.test.tsx
new file mode 100644
index 0000000000..f7f9f96a71
--- /dev/null
+++ b/packages/components/dialog/__tests__/dialog.utils.test.tsx
@@ -0,0 +1,341 @@
+import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
+import { getCSSValue, initDragEvent } from '../utils';
+
+describe('getCSSValue', () => {
+ describe('number input', () => {
+ it('positive number', () => {
+ expect(getCSSValue(100)).toBe('100px');
+ expect(getCSSValue(50)).toBe('50px');
+ expect(getCSSValue(1)).toBe('1px');
+ });
+
+ it('zero', () => {
+ expect(getCSSValue(0)).toBe('0px');
+ });
+
+ it('negative number', () => {
+ expect(getCSSValue(-50)).toBe('-50px');
+ expect(getCSSValue(-100)).toBe('-100px');
+ });
+
+ it('decimal number', () => {
+ expect(getCSSValue(1.5)).toBe('1.5px');
+ expect(getCSSValue(10.25)).toBe('10.25px');
+ expect(getCSSValue(0.5)).toBe('0.5px');
+ });
+
+ it('special number values', () => {
+ expect(getCSSValue(Infinity)).toBe('Infinitypx');
+ expect(getCSSValue(-Infinity)).toBe('-Infinitypx');
+ expect(getCSSValue(NaN)).toBe(NaN);
+ });
+ });
+
+ describe('string input', () => {
+ it('numeric string', () => {
+ expect(getCSSValue('100')).toBe('100px');
+ expect(getCSSValue('0')).toBe('0px');
+ expect(getCSSValue('-50')).toBe('-50px');
+ expect(getCSSValue('1.5')).toBe('1.5px');
+ });
+
+ it('css unit string', () => {
+ expect(getCSSValue('100px')).toBe('100px');
+ expect(getCSSValue('50%')).toBe('50%');
+ expect(getCSSValue('1rem')).toBe('1rem');
+ expect(getCSSValue('2em')).toBe('2em');
+ expect(getCSSValue('auto')).toBe('auto');
+ expect(getCSSValue('inherit')).toBe('inherit');
+ });
+
+ it('empty string', () => {
+ expect(getCSSValue('')).toBe('0px');
+ });
+
+ it('invalid string', () => {
+ expect(getCSSValue('abc')).toBe('abc');
+ expect(getCSSValue('100abc')).toBe('100abc');
+ expect(getCSSValue('px100')).toBe('px100');
+ });
+ });
+
+ describe('edge cases', () => {
+ it('null and undefined', () => {
+ expect(getCSSValue(null)).toBe('0px');
+ expect(getCSSValue(undefined)).toBe(undefined);
+ });
+ });
+});
+
+describe('initDragEvent', () => {
+ let mockElement: HTMLElement;
+ let mockMouseDownEvent: MouseEvent;
+ let mockMouseMoveEvent: MouseEvent;
+ let mockMouseUpEvent: MouseEvent;
+ let addEventListenerSpy: any;
+ let removeEventListenerSpy: any;
+
+ beforeEach(() => {
+ // 创建模拟DOM元素
+ mockElement = document.createElement('div');
+
+ // 设置元素大小
+ Object.defineProperty(mockElement, 'offsetWidth', {
+ writable: true,
+ value: 200,
+ });
+ Object.defineProperty(mockElement, 'offsetHeight', {
+ writable: true,
+ value: 100,
+ });
+ Object.defineProperty(mockElement, 'offsetLeft', {
+ writable: true,
+ value: 100,
+ });
+ Object.defineProperty(mockElement, 'offsetTop', {
+ writable: true,
+ value: 50,
+ });
+
+ // 模拟style对象
+ Object.assign(mockElement.style, {
+ position: '',
+ left: '',
+ top: '',
+ });
+
+ // 设置窗口大小
+ Object.defineProperty(window, 'innerWidth', {
+ writable: true,
+ value: 1024,
+ });
+ Object.defineProperty(window, 'innerHeight', {
+ writable: true,
+ value: 768,
+ });
+
+ // 添加到DOM
+ document.body.appendChild(mockElement);
+
+ // 创建模拟事件
+ mockMouseDownEvent = new MouseEvent('mousedown', {
+ clientX: 150,
+ clientY: 75,
+ bubbles: true,
+ });
+
+ mockMouseMoveEvent = new MouseEvent('mousemove', {
+ clientX: 200,
+ clientY: 125,
+ bubbles: true,
+ });
+
+ mockMouseUpEvent = new MouseEvent('mouseup', {
+ bubbles: true,
+ });
+
+ // 监听事件绑定
+ addEventListenerSpy = vi.spyOn(document, 'addEventListener');
+ removeEventListenerSpy = vi.spyOn(document, 'removeEventListener');
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ document.body.innerHTML = '';
+ });
+
+ describe('basic functionality', () => {
+ it('should initialize drag on mousedown', () => {
+ initDragEvent(mockElement);
+
+ mockElement.dispatchEvent(mockMouseDownEvent);
+
+ expect(addEventListenerSpy).toHaveBeenCalledWith('mousemove', expect.any(Function));
+ expect(addEventListenerSpy).toHaveBeenCalledWith('mouseup', expect.any(Function));
+ expect(addEventListenerSpy).toHaveBeenCalledWith('dragend', expect.any(Function));
+ });
+
+ it('should remove event listeners on mouseup', () => {
+ initDragEvent(mockElement);
+
+ // 触发mousedown
+ mockElement.dispatchEvent(mockMouseDownEvent);
+
+ // 获取mouseup处理函数
+ const mouseUpHandler = addEventListenerSpy.mock.calls.find((call: string[]) => call[0] === 'mouseup')[1];
+
+ // 触发mouseup
+ mouseUpHandler(mockMouseUpEvent);
+
+ expect(removeEventListenerSpy).toHaveBeenCalledWith('mousemove', expect.any(Function));
+ expect(removeEventListenerSpy).toHaveBeenCalledWith('mouseup', expect.any(Function));
+ });
+
+ it('should set element position and style on drag', () => {
+ initDragEvent(mockElement);
+
+ // 触发mousedown
+ mockElement.dispatchEvent(mockMouseDownEvent);
+
+ // 获取mousemove处理函数
+ const mouseMoveHandler = addEventListenerSpy.mock.calls.find((call: string[]) => call[0] === 'mousemove')[1];
+
+ // 触发mousemove
+ mouseMoveHandler(mockMouseMoveEvent);
+
+ expect(mockElement.style.position).toBe('absolute');
+ expect(mockElement.style.left).toBeTruthy();
+ expect(mockElement.style.top).toBeTruthy();
+ });
+ });
+
+ describe('boundary constraints', () => {
+ it('should constrain to left boundary', () => {
+ initDragEvent(mockElement);
+
+ mockElement.dispatchEvent(mockMouseDownEvent);
+
+ const mouseMoveHandler = addEventListenerSpy.mock.calls.find((call: string[]) => call[0] === 'mousemove')[1];
+
+ // 创建超出左边界的事件
+ const leftBoundaryEvent = new MouseEvent('mousemove', {
+ clientX: -100,
+ clientY: 75,
+ });
+
+ mouseMoveHandler(leftBoundaryEvent);
+
+ expect(mockElement.style.left).toBe('0px');
+ });
+
+ it('should constrain to top boundary', () => {
+ initDragEvent(mockElement);
+
+ mockElement.dispatchEvent(mockMouseDownEvent);
+
+ const mouseMoveHandler = addEventListenerSpy.mock.calls.find((call: string[]) => call[0] === 'mousemove')[1];
+
+ // 创建超出上边界的事件
+ const topBoundaryEvent = new MouseEvent('mousemove', {
+ clientX: 150,
+ clientY: -100,
+ });
+
+ mouseMoveHandler(topBoundaryEvent);
+
+ expect(mockElement.style.top).toBe('0px');
+ });
+
+ it('should constrain to right boundary', () => {
+ initDragEvent(mockElement);
+
+ mockElement.dispatchEvent(mockMouseDownEvent);
+
+ const mouseMoveHandler = addEventListenerSpy.mock.calls.find((call: string[]) => call[0] === 'mousemove')[1];
+
+ // 创建超出右边界的事件
+ const rightBoundaryEvent = new MouseEvent('mousemove', {
+ clientX: 2000,
+ clientY: 75,
+ });
+
+ mouseMoveHandler(rightBoundaryEvent);
+
+ // 应该被限制在窗口宽度减去元素宽度的位置
+ const expectedLeft = window.innerWidth - mockElement.offsetWidth;
+ expect(mockElement.style.left).toBe(`${expectedLeft}px`);
+ });
+
+ it('should constrain to bottom boundary', () => {
+ initDragEvent(mockElement);
+
+ mockElement.dispatchEvent(mockMouseDownEvent);
+
+ const mouseMoveHandler = addEventListenerSpy.mock.calls.find((call: string[]) => call[0] === 'mousemove')[1];
+
+ // 创建超出下边界的事件
+ const bottomBoundaryEvent = new MouseEvent('mousemove', {
+ clientX: 150,
+ clientY: 2000,
+ });
+
+ mouseMoveHandler(bottomBoundaryEvent);
+
+ // 应该被限制在窗口高度减去元素高度的位置
+ const expectedTop = window.innerHeight - mockElement.offsetHeight;
+ expect(mockElement.style.top).toBe(`${expectedTop}px`);
+ });
+ });
+
+ describe('edge cases', () => {
+ it('should not enable drag when element is larger than window', () => {
+ // 设置元素大小超过窗口大小
+ Object.defineProperty(mockElement, 'offsetWidth', {
+ writable: true,
+ value: 2000,
+ });
+ Object.defineProperty(mockElement, 'offsetHeight', {
+ writable: true,
+ value: 1000,
+ });
+
+ initDragEvent(mockElement);
+
+ mockElement.dispatchEvent(mockMouseDownEvent);
+
+ // 不应该绑定任何拖拽相关的事件监听器
+ expect(addEventListenerSpy).not.toHaveBeenCalledWith('mousemove', expect.any(Function));
+ expect(addEventListenerSpy).not.toHaveBeenCalledWith('mouseup', expect.any(Function));
+ });
+
+ it('should handle missing window dimensions', () => {
+ // 模拟没有window.innerWidth的情况
+ Object.defineProperty(window, 'innerWidth', {
+ writable: true,
+ value: undefined,
+ });
+ Object.defineProperty(document.documentElement, 'clientWidth', {
+ writable: true,
+ value: 1024,
+ });
+
+ expect(() => {
+ initDragEvent(mockElement);
+ mockElement.dispatchEvent(mockMouseDownEvent);
+ }).not.toThrow();
+ });
+
+ it('should handle missing window innerHeight', () => {
+ // 模拟没有 window.innerHeight 的情况
+ Object.defineProperty(window, 'innerHeight', {
+ writable: true,
+ value: undefined,
+ });
+ Object.defineProperty(document.documentElement, 'clientHeight', {
+ writable: true,
+ value: 768,
+ });
+
+ expect(() => {
+ initDragEvent(mockElement);
+ mockElement.dispatchEvent(mockMouseDownEvent);
+ }).not.toThrow();
+ });
+
+ it('should handle dragend event', () => {
+ initDragEvent(mockElement);
+
+ mockElement.dispatchEvent(mockMouseDownEvent);
+
+ // 获取dragend处理函数
+ const dragEndHandler = addEventListenerSpy.mock.calls.find((call: string[]) => call[0] === 'dragend')[1];
+
+ // 触发dragend
+ const dragEndEvent = new Event('dragend');
+ dragEndHandler(dragEndEvent);
+
+ expect(removeEventListenerSpy).toHaveBeenCalledWith('mousemove', expect.any(Function));
+ expect(removeEventListenerSpy).toHaveBeenCalledWith('mouseup', expect.any(Function));
+ });
+ });
+});
diff --git a/packages/components/dialog/dialog.tsx b/packages/components/dialog/dialog.tsx
index 660e53d7fd..f84d956f04 100644
--- a/packages/components/dialog/dialog.tsx
+++ b/packages/components/dialog/dialog.tsx
@@ -260,6 +260,10 @@ export default defineComponent({
width: calc(100% - ${scrollWidth}px);
}
`;
+
+ if (props.visible) {
+ addKeyboardEvent(props.visible);
+ }
});
onBeforeUnmount(() => {
diff --git a/packages/components/dialog/hooks/useAction.tsx b/packages/components/dialog/hooks/useAction.tsx
index 3e7d73ab2b..3a8a06f6b9 100644
--- a/packages/components/dialog/hooks/useAction.tsx
+++ b/packages/components/dialog/hooks/useAction.tsx
@@ -129,7 +129,7 @@ export function useAction(action: BtnAction) {
if (cancelBtn && ['string', 'object'].includes(typeof cancelBtn)) {
return getButtonByProps(cancelBtn as string | ButtonProps, { defaultButtonProps, className });
}
- // 渲染插槽 或 function 类型的 confirmBtn,属性优先级更高
+ // 渲染插槽 或 function 类型的 cancelBtn,属性优先级更高
return renderTNodeJSX('cancelBtn');
};
return { getConfirmBtn, getCancelBtn };
diff --git a/packages/components/dialog/plugin.tsx b/packages/components/dialog/plugin.tsx
index daa5dafb9f..30ef9de9be 100644
--- a/packages/components/dialog/plugin.tsx
+++ b/packages/components/dialog/plugin.tsx
@@ -13,7 +13,7 @@ const createDialog: DialogMethod = (props, context) => {
let preClassName = className;
const updateClassNameStyle = (className: string, style: DialogOptions['style']) => {
- if (className) {
+ if (className && wrapper.firstElementChild) {
if (preClassName && preClassName !== className) {
wrapper.firstElementChild.classList.remove(...preClassName.split(' ').map((name) => name.trim()));
}
@@ -22,7 +22,7 @@ const createDialog: DialogMethod = (props, context) => {
});
}
- if (style) {
+ if (style && wrapper.firstElementChild) {
(wrapper.firstElementChild as HTMLElement).style.cssText += style;
}