From f67479fd85222457989e51aec5863f64d446f9f3 Mon Sep 17 00:00:00 2001 From: kalu5 <451660550@qq.com> Date: Mon, 29 Dec 2025 14:37:42 +0800 Subject: [PATCH 1/3] fix(toast): toast cannot be cleared in strict mode --- src/packages/toast/Notification.tsx | 3 +- src/packages/toast/__test__/toast.spec.tsx | 32 ++++++++++++++++++++++ src/packages/toast/toast.tsx | 31 +++++++++++++++++---- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/packages/toast/Notification.tsx b/src/packages/toast/Notification.tsx index 4400240e71..e6467c1650 100644 --- a/src/packages/toast/Notification.tsx +++ b/src/packages/toast/Notification.tsx @@ -156,13 +156,14 @@ Notification.newInstance = (properties, callback) => { let called = false - function ref(instance: any) { + function ref(instance: Notification | null) { if (called) { return } called = true callback({ component: instance, + id, destroy() { unmount(element) element && element.parentNode && element.parentNode.removeChild(element) diff --git a/src/packages/toast/__test__/toast.spec.tsx b/src/packages/toast/__test__/toast.spec.tsx index 185f278bae..b7b74fc361 100644 --- a/src/packages/toast/__test__/toast.spec.tsx +++ b/src/packages/toast/__test__/toast.spec.tsx @@ -136,3 +136,35 @@ test('event show-loading-toast', async () => { expect(document.querySelector('.nut-toast-text')?.innerHTML).toBe('loading') }) }) + +test('manually close in strict mode', async () => { + const time = 2000 + const content = 'strict mode loading' + const { getByTestId } = render( + { + onClickToast('loading', content) + onClickToast('loading', content) + }} + /> + ) + await waitFor(() => { + fireEvent.click(getByTestId('emit-click')) + expect(onClickToast).toBeCalled() + expect(document.querySelector('.nut-toast-text')?.innerHTML).toBe(content) + }) + + Toast.clear() + + await waitFor( + () => { + expect(document.querySelector('.nut-toast-text')?.innerHTML).toBe( + undefined + ) + }, + { + timeout: time, + } + ) +}) diff --git a/src/packages/toast/toast.tsx b/src/packages/toast/toast.tsx index 4942a6ab8c..371a43a8d6 100644 --- a/src/packages/toast/toast.tsx +++ b/src/packages/toast/toast.tsx @@ -1,8 +1,17 @@ +import { ReactNode } from 'react' import Notification from './Notification' import { WebToastProps } from '@/types' import { defaultOverlayProps } from '@/packages/overlay/overlay' +import { clone } from '@/utils' -let messageInstance: any = null +type NotificationInstance = { + component: Notification + id: number + destroy: () => void +} + +let messageInstance: NotificationInstance | null = null +const messageInstaceSet = new Set() let defaultProps: WebToastProps = { ...defaultOverlayProps, @@ -26,18 +35,18 @@ type ToastNativeProps = Partial function getInstance( props: ToastNativeProps, - callback: (notification: any) => void + callback: (notification: NotificationInstance) => void ) { if (messageInstance) { messageInstance.destroy() messageInstance = null } - Notification.newInstance(props, (notification: any) => { + Notification.newInstance(props, (notification: NotificationInstance) => { return callback && callback(notification) }) } -function notice(opts: any) { +function notice(opts: ToastNativeProps) { function close() { if (messageInstance) { messageInstance.destroy() @@ -46,12 +55,16 @@ function notice(opts: any) { } } const opts2 = { ...defaultProps, ...opts, onClose: close } - getInstance(opts2, (notification: any) => { + getInstance(opts2, (notification: NotificationInstance) => { + const oldInstance = messageInstance ? clone(messageInstance) : null + if (notification.id === oldInstance?.id) { + messageInstaceSet.add(oldInstance) + } messageInstance = notification }) } -const errorMsg = (msg: any) => { +const errorMsg = (msg: ReactNode) => { if (!msg) { console.warn('[NutUI Toast]: msg cannot be null') } @@ -79,6 +92,12 @@ export default { if (messageInstance) { messageInstance.destroy() messageInstance = null + if (messageInstaceSet?.size) { + messageInstaceSet.forEach((instance: NotificationInstance) => { + instance?.destroy() + }) + messageInstaceSet.clear() + } } }, } From af932b6eb1b24724d509e8bae5190192f62f965b Mon Sep 17 00:00:00 2001 From: kalu5 <451660550@qq.com> Date: Tue, 30 Dec 2025 14:06:35 +0800 Subject: [PATCH 2/3] fix(toast): correct variable name and change id type from number to string --- src/packages/toast/toast.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/packages/toast/toast.tsx b/src/packages/toast/toast.tsx index 371a43a8d6..43be54813e 100644 --- a/src/packages/toast/toast.tsx +++ b/src/packages/toast/toast.tsx @@ -6,12 +6,12 @@ import { clone } from '@/utils' type NotificationInstance = { component: Notification - id: number + id: string destroy: () => void } let messageInstance: NotificationInstance | null = null -const messageInstaceSet = new Set() +const messageInstanceSet = new Set() let defaultProps: WebToastProps = { ...defaultOverlayProps, @@ -58,7 +58,7 @@ function notice(opts: ToastNativeProps) { getInstance(opts2, (notification: NotificationInstance) => { const oldInstance = messageInstance ? clone(messageInstance) : null if (notification.id === oldInstance?.id) { - messageInstaceSet.add(oldInstance) + messageInstanceSet.add(oldInstance) } messageInstance = notification }) @@ -92,11 +92,11 @@ export default { if (messageInstance) { messageInstance.destroy() messageInstance = null - if (messageInstaceSet?.size) { - messageInstaceSet.forEach((instance: NotificationInstance) => { + if (messageInstanceSet?.size) { + messageInstanceSet.forEach((instance: NotificationInstance) => { instance?.destroy() }) - messageInstaceSet.clear() + messageInstanceSet.clear() } } }, From 1f45f73d97b1a54aea1b4a3bc5fa282b4233d963 Mon Sep 17 00:00:00 2001 From: kalu5 <451660550@qq.com> Date: Tue, 30 Dec 2025 17:31:00 +0800 Subject: [PATCH 3/3] test(toast): enhance toast component tests with new scenarios and timeout handling --- src/packages/toast/__test__/toast.spec.tsx | 94 +++++++++++++++++++--- 1 file changed, 84 insertions(+), 10 deletions(-) diff --git a/src/packages/toast/__test__/toast.spec.tsx b/src/packages/toast/__test__/toast.spec.tsx index b7b74fc361..3834e051fa 100644 --- a/src/packages/toast/__test__/toast.spec.tsx +++ b/src/packages/toast/__test__/toast.spec.tsx @@ -29,6 +29,12 @@ const onClickToast = vi.fn((type, msg, options?) => { } }) +const waitTimeout = (delay: number) => { + return new Promise((resolve) => { + setTimeout(resolve, delay) + }) +} + test('event click-show-toast test', async () => { const { getByTestId } = render( { await waitFor(() => { fireEvent.click(getByTestId('emit-click')) expect(onClickToast).toBeCalled() - expect(document.querySelector('.nut-toast-text')?.innerHTML).toBe(content) + expect(document.querySelectorAll('.nut-toast-text')?.length).toBe(2) }) Toast.clear() - await waitFor( - () => { - expect(document.querySelector('.nut-toast-text')?.innerHTML).toBe( - undefined - ) - }, - { - timeout: time, - } + await waitTimeout(time) + expect(document.querySelector('.nut-toast-text')?.innerHTML).toBe(undefined) +}) + +test('no content', async () => { + const { getByTestId } = render( + { + Toast.show({}) + }} + /> + ) + await waitFor(() => { + fireEvent.click(getByTestId('emit-click')) + expect(document.querySelector('.nut-toast-text')?.innerHTML).toBe(undefined) + }) +}) + +test('string option', async () => { + const content = 'string option' + const { getByTestId } = render( + { + Toast.show(content) + }} + /> ) + await waitFor(() => { + fireEvent.click(getByTestId('emit-click')) + expect(document.querySelector('.nut-toast-text')?.innerHTML).toBe(content) + }) +}) + +test('global config', async () => { + const content = 'global config' + const contentClassName = 'content-demo' + Toast.config({ contentClassName }) + const { getByTestId } = render( + { + onClickToast('text', content) + }} + /> + ) + await waitFor(() => { + fireEvent.click(getByTestId('emit-click')) + expect(document.querySelector(`.${contentClassName}`)).toBeTruthy() + expect(document.querySelector('.nut-toast-text')?.innerHTML).toBe(content) + }) +}) + +test('1s after close', async () => { + const content = '1' + let isCalledClose = false + const { getByTestId } = render( + { + onClickToast('text', content, { + duration: 1, + onClose: () => { + isCalledClose = true + }, + }) + }} + /> + ) + await waitFor(() => { + fireEvent.click(getByTestId('emit-click')) + expect(document.querySelector('.nut-toast-text')?.innerHTML).toBe(content) + }) + + await waitTimeout(2000) + expect(document.querySelector('.nut-toast-text')).toBe(null) + expect(isCalledClose).toBeTruthy() })