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();
+ });
+ });
+});