From f70f2aedf7689aeddfd3c937daccf2cf07080cbc Mon Sep 17 00:00:00 2001 From: heyongqi10 Date: Tue, 3 Feb 2026 14:25:56 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(toast):=20=E4=BF=AE=E5=A4=8D=E6=B8=85?= =?UTF-8?q?=E9=99=A4toast=E6=97=B6=E6=9C=AA=E6=AD=A3=E7=A1=AE=E5=8D=B8?= =?UTF-8?q?=E8=BD=BD=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/__VUE/toast/index.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/packages/__VUE/toast/index.ts b/src/packages/__VUE/toast/index.ts index ff530ad7dd..0beeb594c4 100644 --- a/src/packages/__VUE/toast/index.ts +++ b/src/packages/__VUE/toast/index.ts @@ -28,17 +28,16 @@ let idsMap: string[] = [] let optsMap: any[] = [] const clearToast = (id?: string) => { if (id) { - const container = document.getElementById(id) + const opts = optsMap.find(item => item.id === id) + if (opts && opts._unmount) { + opts._unmount() + } optsMap = optsMap.filter(item => item.id !== id) idsMap = idsMap.filter(item => item !== id) - if (container) { - document.body.removeChild(container) - } } else { - idsMap.forEach((item) => { - const container = document.getElementById(item) - if (container) { - document.body.removeChild(container) + optsMap.forEach((opts) => { + if (opts._unmount) { + opts._unmount() } }) optsMap = [] @@ -78,9 +77,10 @@ const mountToast = (opts: any) => { idsMap.push(opts.id) optsMap.push(opts) - CreateComponent(opts, { + const { unmount } = CreateComponent(opts, { wrapper: Toast }) + opts._unmount = unmount return showToast } From 81046cd6edaf5a7ceaf68b4ed08ab62ee9590b07 Mon Sep 17 00:00:00 2001 From: heyongqi10 Date: Tue, 3 Feb 2026 14:58:32 +0800 Subject: [PATCH 2/2] =?UTF-8?q?test(toast):=20=E5=A2=9E=E5=8A=A0=E6=B8=85?= =?UTF-8?q?=E9=99=A4toast=E6=97=B6=E7=9A=84unmount=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__VUE/toast/__tests__/index.spec.ts | 115 +++++++++++++++++- 1 file changed, 114 insertions(+), 1 deletion(-) diff --git a/src/packages/__VUE/toast/__tests__/index.spec.ts b/src/packages/__VUE/toast/__tests__/index.spec.ts index d367fe8954..a0396e02fa 100644 --- a/src/packages/__VUE/toast/__tests__/index.spec.ts +++ b/src/packages/__VUE/toast/__tests__/index.spec.ts @@ -1,8 +1,14 @@ import { mount } from '@vue/test-utils' import { nextTick } from 'vue' -import { Toast } from '@nutui/nutui' +import Toast, { showToast } from '../index' +import * as CreateUtils from '@/packages/utils/create' describe('component toast', () => { + // 每个测试后清理所有 toast + afterEach(() => { + showToast.hide() + }) + test('should render toast after using msg and id', async () => { const wrapper = mount(Toast, { props: { @@ -64,4 +70,111 @@ describe('component toast', () => { const toast: any = wrapper.find('.custom') expect(toast.exists()).toBe(true) }) + + test('should call unmount when clearing single toast', async () => { + // Mock CreateComponent 来追踪 unmount 调用 + const unmountSpy = vi.fn() + const originalCreateComponent = CreateUtils.CreateComponent + let actualElementId = '' + + const mockUnmount = vi.fn(() => { + unmountSpy() + // 移除实际的 DOM 元素 + const element = document.getElementById(actualElementId) + if (element) { + element.parentNode?.removeChild(element) + } + }) + + vi.spyOn(CreateUtils, 'CreateComponent').mockImplementation((opts: any, component: any) => { + const result = originalCreateComponent(opts, component) + // 捕获实际创建的元素 id + actualElementId = result.instance.$el?.parentElement?.id || '' + return { + ...result, + unmount: mockUnmount + } + }) + + // 创建 toast + showToast.success('Test message', { id: 'test-unmount-single', duration: 0 }) + await nextTick() + + // 验证 DOM 元素存在 + const toastElement = document.getElementById(actualElementId) + expect(toastElement).toBeTruthy() + + // 清除 toast + showToast.hide('test-unmount-single') + await nextTick() + + // 验证 unmount 被调用 + expect(mockUnmount).toHaveBeenCalled() + + // 恢复原始实现 + vi.restoreAllMocks() + }) + + test('should call unmount for all toasts when clearing all', async () => { + const unmountSpies: any[] = [] + const originalCreateComponent = CreateUtils.CreateComponent + + // Mock CreateComponent + vi.spyOn(CreateUtils, 'CreateComponent').mockImplementation((opts: any, component: any) => { + const result = originalCreateComponent(opts, component) + const elementId = result.instance.$el?.parentElement?.id || '' + const spy = vi.fn(() => { + // 移除对应的 DOM 元素 + const element = document.getElementById(elementId) + if (element) { + element.parentNode?.removeChild(element) + } + }) + unmountSpies.push(spy) + return { + ...result, + unmount: spy + } + }) + + // 创建多个 toast + showToast.success('Toast 1', { id: 'test-1', duration: 0 }) + showToast.success('Toast 2', { id: 'test-2', duration: 0 }) + showToast.success('Toast 3', { id: 'test-3', duration: 0 }) + await nextTick() + + // 验证创建了 3 个 toast + expect(unmountSpies.length).toBe(3) + + // 清除所有 toast + showToast.hide() + await nextTick() + + // 验证所有 unmount 都被调用 + unmountSpies.forEach((spy) => { + expect(spy).toHaveBeenCalled() + }) + + // 恢复原始实现 + vi.restoreAllMocks() + }) + + test('should properly clean up DOM when toast is cleared', async () => { + // 创建 toast + showToast.success('Test cleanup', { id: 'test-cleanup', duration: 0 }) + await nextTick() + + // 验证 DOM 存在 - 查找包含 test-cleanup 的元素 + let toastElements = document.querySelectorAll('[id*="test-cleanup"]') + expect(toastElements.length).toBeGreaterThan(0) + + // 清除 toast + showToast.hide('test-cleanup') + await nextTick() + await new Promise(resolve => setTimeout(resolve, 100)) + + // 验证 DOM 已被清除 + toastElements = document.querySelectorAll('[id*="test-cleanup"]') + expect(toastElements.length).toBe(0) + }) })