import { render } from '../src/shared/ui/render-root.ts'; // @vitest-environment jsdom import { afterEach, beforeEach, expect, it, vi } from 'vitest'; import { createElement } from 'react'; import { FloatingNotice } from '../src/ui/floating-notice.tsx'; import { dismissAllNotices, dismissNotice, notify, placeNotifications } from '../src/ui/notifications.ts'; import { noticeText, visibleNotices } from './helpers/notices.ts'; const root = document.createElement('div'); beforeEach(() => { document.body.replaceChildren(root); }); afterEach(() => { render(null, root); dismissAllNotices(); vi.useRealTimers(); }); const show = (message: string, busy = false) => render(createElement(FloatingNotice, { message, busy }), root); it('keeps ongoing operations visible, then expires completed feedback', async () => { vi.useFakeTimers(); show('正在安装', true); await vi.advanceTimersByTimeAsync(20_000); expect(noticeText()).toContain('正在安装'); expect(root.textContent).toBe(''); expect(visibleNotices()[0]?.querySelector('button')).toBeNull(); show('安装完成'); await vi.advanceTimersByTimeAsync(6000); expect(visibleNotices()).toHaveLength(0); }); it('allows dismissal and shows the next operation without retaining an old timer', async () => { vi.useFakeTimers(); show('第一条'); document.querySelector('.notyf button')!.click(); await vi.advanceTimersByTimeAsync(1); expect(visibleNotices()).toHaveLength(0); show('第二条'); await vi.advanceTimersByTimeAsync(1); expect(noticeText()).toContain('第二条'); await vi.advanceTimersByTimeAsync(4000); show('第三条'); await vi.advanceTimersByTimeAsync(3000); expect(noticeText()).toContain('第三条'); render(null, root); expect(visibleNotices()).toHaveLength(0); }); it('updates progress in place, limits the stack and treats remote markup as text', () => { const id = notify('上传 1%', { kind: 'loading' }); const card = visibleNotices()[0]; for (let progress = 2; progress < 100; progress++) notify(`上传 ${progress}%`, { kind: 'loading' }, id); expect(visibleNotices()).toEqual([card]); expect(noticeText()).toBe('上传 99%'); notify(' & ', { kind: 'error' }, id); expect(document.querySelector('.notyf img, .notyf script')).toBeNull(); expect(noticeText()).toContain(''); expect(document.querySelector('.notyf [role=alert]')).not.toBeNull(); expect(document.querySelector('.notyf button')?.getAttribute('aria-label')).toBe('关闭提示'); for (let count = 0; count < 10; count++) notify('消息 ' + count); expect(visibleNotices()).toHaveLength(3); expect(document.querySelectorAll('.notyf')).toHaveLength(1); }); it('keeps app notifications across screen disposal and moves the host into native dialogs', () => { const appNotice = notify('已返回书架'); show('正在加载', true); render(null, root); expect(noticeText()).toBe('已返回书架'); const dialog = document.createElement('dialog'); document.body.append(dialog); dialog.open = true; placeNotifications(); expect(dialog.querySelector('.notyf')).not.toBeNull(); expect(noticeText()).toBe('已返回书架'); dialog.open = false; placeNotifications(); dialog.remove(); expect(document.body.querySelector(':scope > .notyf')).not.toBeNull(); dismissNotice(appNotice); expect(visibleNotices()).toHaveLength(0); });