diff --git a/src/notice-bar/__tests__/index.test.tsx b/src/notice-bar/__tests__/index.test.tsx new file mode 100644 index 000000000..06af9dff5 --- /dev/null +++ b/src/notice-bar/__tests__/index.test.tsx @@ -0,0 +1,568 @@ +import React from 'react'; +import { describe, it, expect, render, vi, fireEvent } from '@test/utils'; +import NoticeBar from '../NoticeBar'; + +describe('NoticeBar', () => { + describe('props', () => { + it(':content string', () => { + const { queryByText } = render(); + expect(queryByText('通知消息')).toBeInTheDocument(); + }); + + it(':content TNode', () => { + const testId = 'custom-content'; + const { container } = render(自定义内容} />); + expect(container.querySelector(`[data-testid="${testId}"]`)).not.toBe(null); + }); + + it(':content array with vertical direction', () => { + const content = ['消息1', '消息2', '消息3']; + const { queryByText } = render(); + expect(queryByText('消息1')).toBeInTheDocument(); + }); + + it(':direction horizontal', () => { + const { container } = render(); + expect(container.querySelector('.t-notice-bar')).toBeTruthy(); + expect(container.querySelector('.t-notice-bar__content--vertical')).toBeFalsy(); + }); + + it(':direction vertical', () => { + const content = ['消息1', '消息2']; + const { container } = render(); + expect(container.querySelector('.t-notice-bar__content--vertical')).toBeTruthy(); + }); + + it(':marquee false', () => { + const { container } = render(); + expect(container.querySelector('.t-notice-bar__content')).toBeTruthy(); + }); + + it(':marquee true', () => { + const { container } = render(); + expect(container.querySelector('.t-notice-bar__content')).toBeTruthy(); + }); + + it(':marquee object with speed', () => { + const { container } = render( + , + ); + expect(container.querySelector('.t-notice-bar__content')).toBeTruthy(); + }); + + it(':marquee object with loop 0', () => { + const { container } = render(); + expect(container.querySelector('.t-notice-bar__content')).toBeTruthy(); + }); + + it(':operation string', () => { + const { queryByText } = render(); + expect(queryByText('查看详情')).toBeInTheDocument(); + }); + + it(':operation TNode', () => { + const testId = 'custom-operation'; + const { container } = render( + 操作} />, + ); + expect(container.querySelector(`[data-testid="${testId}"]`)).not.toBe(null); + }); + + it(':prefixIcon null', () => { + const { container } = render(); + expect(container.querySelector('.t-notice-bar__prefix-icon')).toBeFalsy(); + }); + + it(':prefixIcon custom', () => { + const testId = 'custom-prefix-icon'; + const { container } = render( + 📢} />, + ); + expect(container.querySelector(`[data-testid="${testId}"]`)).not.toBe(null); + }); + + it(':prefixIcon default with theme info', () => { + const { container } = render(); + expect(container.querySelector('.t-notice-bar__prefix-icon')).toBeTruthy(); + expect(container.querySelector('.t-icon-info-circle-filled')).toBeTruthy(); + }); + + it(':suffixIcon', () => { + const testId = 'custom-suffix-icon'; + const { container } = render( + →} />, + ); + expect(container.querySelector(`[data-testid="${testId}"]`)).not.toBe(null); + expect(container.querySelector('.t-notice-bar__suffix-icon')).toBeTruthy(); + }); + + it(':suffixIcon null', () => { + const { container } = render(); + expect(container.querySelector('.t-notice-bar__suffix-icon')).toBeFalsy(); + }); + + it(':theme info', () => { + const { container } = render(); + expect(container.querySelector('.t-notice-bar--info')).toBeTruthy(); + expect(container.querySelector('.t-icon-info-circle-filled')).toBeTruthy(); + }); + + it(':theme success', () => { + const { container } = render(); + expect(container.querySelector('.t-notice-bar--success')).toBeTruthy(); + expect(container.querySelector('.t-icon-check-circle-filled')).toBeTruthy(); + }); + + it(':theme warning', () => { + const { container } = render(); + expect(container.querySelector('.t-notice-bar--warning')).toBeTruthy(); + expect(container.querySelector('.t-icon-error-circle-filled')).toBeTruthy(); + }); + + it(':theme error', () => { + const { container } = render(); + expect(container.querySelector('.t-notice-bar--error')).toBeTruthy(); + expect(container.querySelector('.t-icon-error-circle-filled')).toBeTruthy(); + }); + + it(':visible true', () => { + const { container } = render(); + expect(container.querySelector('.t-notice-bar')).toBeTruthy(); + }); + + it(':visible false', () => { + const { container } = render(); + expect(container.querySelector('.t-notice-bar')).toBeFalsy(); + }); + + it(':defaultVisible true', () => { + const { container } = render(); + expect(container.querySelector('.t-notice-bar')).toBeTruthy(); + }); + + it(':defaultVisible false', () => { + const { container } = render(); + expect(container.querySelector('.t-notice-bar')).toBeFalsy(); + }); + + it(':className', () => { + const { container } = render(); + expect(container.querySelector('.custom-notice-bar')).toBeTruthy(); + }); + + it(':style', () => { + const { container } = render(); + const noticeBar = container.querySelector('.t-notice-bar') as HTMLElement; + expect(noticeBar?.style.backgroundColor).toBe('red'); + }); + + it(':touchable', () => { + const content = ['消息1', '消息2']; + const { container } = render(); + expect(container.querySelector('.t-swiper')).toBeTruthy(); + }); + }); + + describe('events', () => { + it(':onClick with prefix-icon trigger', () => { + const handleClick = vi.fn(); + const { container } = render(); + const prefixIcon = container.querySelector('.t-notice-bar__prefix-icon'); + fireEvent.click(prefixIcon!); + expect(handleClick).toHaveBeenCalledWith('prefix-icon'); + }); + + it(':onClick with content trigger', () => { + const handleClick = vi.fn(); + const { container } = render(); + const content = container.querySelector('.t-notice-bar__content-wrap'); + fireEvent.click(content!); + expect(handleClick).toHaveBeenCalledWith('content'); + }); + + it(':onClick with operation trigger', () => { + const handleClick = vi.fn(); + const { container } = render(); + const operation = container.querySelector('.t-notice-bar__operation'); + fireEvent.click(operation!); + expect(handleClick).toHaveBeenCalledWith('operation'); + }); + + it(':onClick with suffix-icon trigger', () => { + const handleClick = vi.fn(); + const { container } = render( + X} onClick={handleClick} />, + ); + const suffixIcon = container.querySelector('.t-notice-bar__suffix-icon'); + fireEvent.click(suffixIcon!); + expect(handleClick).toHaveBeenCalledWith('suffix-icon'); + }); + + it(':onClick operation should stop propagation', () => { + const handleClick = vi.fn(); + const { container } = render(); + const operation = container.querySelector('.t-notice-bar__operation'); + fireEvent.click(operation!); + expect(handleClick).toHaveBeenCalledTimes(1); + expect(handleClick).toHaveBeenCalledWith('operation'); + }); + + it(':onClick without handler', () => { + const { container } = render(); + const content = container.querySelector('.t-notice-bar__content-wrap'); + expect(() => { + fireEvent.click(content!); + }).not.toThrow(); + }); + }); + + describe('rendering', () => { + it('should render basic notice bar', () => { + const { container } = render(); + expect(container.querySelector('.t-notice-bar')).toBeTruthy(); + }); + + it('should render with all props', () => { + const handleClick = vi.fn(); + const { container, queryByText } = render( + 📢} + suffixIcon={
} + operation="查看" + className="custom-class" + style={{ padding: '10px' }} + onClick={handleClick} + />, + ); + + expect(container.querySelector('.t-notice-bar')).toBeTruthy(); + expect(container.querySelector('.t-notice-bar--success')).toBeTruthy(); + expect(container.querySelector('.custom-class')).toBeTruthy(); + expect(queryByText('完整通知')).toBeInTheDocument(); + expect(queryByText('查看')).toBeInTheDocument(); + }); + + it('should not render when visible is false', () => { + const { container } = render(); + expect(container.querySelector('.t-notice-bar')).toBeFalsy(); + }); + + it('should render vertical swiper correctly', () => { + const content = ['消息1', '消息2', '消息3']; + const { container, queryByText } = render(); + + expect(container.querySelector('.t-swiper')).toBeTruthy(); + expect(container.querySelector('.t-notice-bar__content--vertical')).toBeTruthy(); + expect(queryByText('消息1')).toBeInTheDocument(); + }); + + it('should render marquee content correctly', () => { + const { container } = render(); + const content = container.querySelector('.t-notice-bar__content'); + expect(content).toBeTruthy(); + }); + + it('should render without prefix icon when prefixIcon is null', () => { + const { container } = render(); + expect(container.querySelector('.t-notice-bar__prefix-icon')).toBeFalsy(); + }); + }); + + describe('edge cases', () => { + it('should handle empty content', () => { + const { container } = render(); + expect(container.querySelector('.t-notice-bar')).toBeTruthy(); + }); + + it('should handle null content', () => { + const { container } = render(); + expect(container.querySelector('.t-notice-bar')).toBeTruthy(); + }); + + it('should handle visibility changes', () => { + const { container, rerender } = render(); + expect(container.querySelector('.t-notice-bar')).toBeFalsy(); + + rerender(); + expect(container.querySelector('.t-notice-bar')).toBeTruthy(); + }); + + it('should handle theme changes', () => { + const { container, rerender } = render(); + expect(container.querySelector('.t-notice-bar--info')).toBeTruthy(); + + rerender(); + expect(container.querySelector('.t-notice-bar--success')).toBeTruthy(); + }); + + it('should handle marquee with loop 0', () => { + const { container } = render(); + expect(container.querySelector('.t-notice-bar__content')).toBeTruthy(); + }); + + it('should handle marquee with custom speed and delay', () => { + const { container } = render(); + expect(container.querySelector('.t-notice-bar__content')).toBeTruthy(); + }); + + it('should handle vertical direction with single item', () => { + const { container, queryByText } = render(); + expect(container.querySelector('.t-swiper')).toBeTruthy(); + expect(queryByText('单条消息')).toBeInTheDocument(); + }); + + it('should handle operation without onClick', () => { + const { container } = render(); + const operation = container.querySelector('.t-notice-bar__operation'); + expect(() => { + fireEvent.click(operation!); + }).not.toThrow(); + }); + }); + + describe('integration', () => { + it('should work with dynamic content', () => { + const { queryByText, rerender } = render(); + expect(queryByText('初始消息')).toBeInTheDocument(); + + rerender(); + expect(queryByText('更新消息')).toBeInTheDocument(); + expect(queryByText('初始消息')).not.toBeInTheDocument(); + }); + + it('should work with multiple triggers', () => { + const handleClick = vi.fn(); + const { container } = render( + X} onClick={handleClick} />, + ); + + // 点击内容 + const content = container.querySelector('.t-notice-bar__content-wrap'); + fireEvent.click(content!); + expect(handleClick).toHaveBeenLastCalledWith('content'); + + // 点击操作 + const operation = container.querySelector('.t-notice-bar__operation'); + fireEvent.click(operation!); + expect(handleClick).toHaveBeenLastCalledWith('operation'); + + // 点击后缀图标 + const suffixIcon = container.querySelector('.t-notice-bar__suffix-icon'); + fireEvent.click(suffixIcon!); + expect(handleClick).toHaveBeenLastCalledWith('suffix-icon'); + + expect(handleClick).toHaveBeenCalledTimes(3); + }); + + it('should work with controlled visible', () => { + const { container, rerender } = render(); + expect(container.querySelector('.t-notice-bar')).toBeTruthy(); + + rerender(); + expect(container.querySelector('.t-notice-bar')).toBeFalsy(); + + rerender(); + expect(container.querySelector('.t-notice-bar')).toBeTruthy(); + }); + + it('should work with all themes', () => { + const themes: Array<'info' | 'success' | 'warning' | 'error'> = ['info', 'success', 'warning', 'error']; + themes.forEach((theme) => { + const { container } = render(); + expect(container.querySelector(`.t-notice-bar--${theme}`)).toBeTruthy(); + }); + }); + + it('should handle marquee state transitions', () => { + const { container, rerender } = render(); + expect(container.querySelector('.t-notice-bar__content')).toBeTruthy(); + + rerender(); + expect(container.querySelector('.t-notice-bar__content')).toBeTruthy(); + + rerender(); + expect(container.querySelector('.t-notice-bar__content')).toBeTruthy(); + }); + }); + + describe('dom structure', () => { + it('should have correct structure with all elements', () => { + const { container } = render( + X} />, + ); + + const noticeBar = container.querySelector('.t-notice-bar'); + expect(noticeBar).toBeTruthy(); + expect(noticeBar?.querySelector('.t-notice-bar__prefix-icon')).toBeTruthy(); + expect(noticeBar?.querySelector('.t-notice-bar__content-wrap')).toBeTruthy(); + expect(noticeBar?.querySelector('.t-notice-bar__operation')).toBeTruthy(); + expect(noticeBar?.querySelector('.t-notice-bar__suffix-icon')).toBeTruthy(); + }); + + it('should render content wrapper correctly', () => { + const { container } = render(); + const contentWrap = container.querySelector('.t-notice-bar__content-wrap'); + expect(contentWrap).toBeTruthy(); + expect(contentWrap?.querySelector('.t-notice-bar__content')).toBeTruthy(); + }); + }); + + describe('animation and scrolling', () => { + it('should handle marquee animation with getBoundingClientRect', () => { + const { container } = render(); + const content = container.querySelector('.t-notice-bar__content'); + expect(content).toBeTruthy(); + }); + + it('should trigger handleScrolling on mount with marquee', () => { + const { container } = render(); + expect(container.querySelector('.t-notice-bar__content')).toBeTruthy(); + }); + + it('should handle transitionend event', () => { + const { container } = render(); + const content = container.querySelector('.t-notice-bar__content'); + + // 模拟 transitionend 事件 + if (content) { + fireEvent.transitionEnd(content); + } + + expect(content).toBeTruthy(); + }); + + it('should handle marquee with delay', () => { + const { container } = render(); + expect(container.querySelector('.t-notice-bar__content')).toBeTruthy(); + }); + + it('should handle marquee loop ending', () => { + const { container } = render(); + const content = container.querySelector('.t-notice-bar__content'); + + if (content) { + // 模拟完成一次循环 + fireEvent.transitionEnd(content); + } + + expect(content).toBeTruthy(); + }); + + it('should re-trigger scrolling when visible changes from false to true', async () => { + const { container, rerender } = render(); + expect(container.querySelector('.t-notice-bar')).toBeFalsy(); + + // 切换为可见,应该重新触发滚动 + rerender(); + + // 等待一小段时间让 useEffect 执行 + await new Promise((resolve) => { + setTimeout(resolve, 10); + }); + + expect(container.querySelector('.t-notice-bar')).toBeTruthy(); + }); + + it('should handle marquee true', () => { + const { container } = render(); + const content = container.querySelector('.t-notice-bar__content'); + expect(content).toBeTruthy(); + }); + + it('should handle marquee with custom speed', () => { + const { container } = render(); + const content = container.querySelector('.t-notice-bar__content'); + expect(content).toBeTruthy(); + }); + + it('should handle marquee object with all properties', () => { + const { container } = render( + , + ); + const content = container.querySelector('.t-notice-bar__content'); + expect(content).toBeTruthy(); + }); + + it('should handle content wider than container', () => { + const { container } = render( + , + ); + const content = container.querySelector('.t-notice-bar__content'); + expect(content).toBeTruthy(); + }); + }); + + describe('lifecycle', () => { + it('should handle mount with visible true and marquee', () => { + const { container } = render(); + expect(container.querySelector('.t-notice-bar')).toBeTruthy(); + }); + + it('should handle unmount', () => { + const { container, unmount } = render(); + expect(container.querySelector('.t-notice-bar')).toBeTruthy(); + + unmount(); + expect(container.querySelector('.t-notice-bar')).toBeFalsy(); + }); + + it('should cleanup timers on unmount', () => { + const { unmount } = render(); + unmount(); + // 验证卸载成功即可 + expect(true).toBe(true); + }); + + it('should re-execute scrolling when visible changes after first mount', async () => { + // 第一次挂载,visible=true,会执行一次 handleScrolling + const { rerender, container } = render(); + expect(container.querySelector('.t-notice-bar')).toBeTruthy(); + + // 切换为不可见 + rerender(); + expect(container.querySelector('.t-notice-bar')).toBeFalsy(); + + // 再次切换为可见,这次会触发 useEffect 中 hasBeenExecute.current 为 true 的分支 + rerender(); + + // 等待 setTimeout 执行 + await new Promise((resolve) => { + setTimeout(resolve, 50); + }); + + expect(container.querySelector('.t-notice-bar')).toBeTruthy(); + }); + + it('should handle getBoundingClientRect in setTimeout', async () => { + const { container } = render(); + + // 等待 setTimeout(200ms) 执行 + await new Promise((resolve) => { + setTimeout(resolve, 250); + }); + + expect(container.querySelector('.t-notice-bar__content')).toBeTruthy(); + }); + + it('should trigger animation when content is wider than container', async () => { + const { container } = render( + , + ); + + // 等待 DOM 更新和 setTimeout 执行 + await new Promise((resolve) => { + setTimeout(resolve, 250); + }); + + const content = container.querySelector('.t-notice-bar__content'); + expect(content).toBeTruthy(); + }); + }); +});