import React from 'react'; import { spyElementPrototypes } from 'rc-util/lib/test/domHook'; import { act } from 'react-dom/test-utils'; import { fireEvent, render, triggerResize, waitFakeTimer, waitFor } from '../../../tests/utils'; import type { EllipsisConfig } from '../Base'; import Base from '../Base'; jest.mock('copy-to-clipboard'); jest.mock('../../_util/styleChecker', () => ({ isStyleSupport: () => true, })); describe('Typography.Ellipsis', () => { const LINE_STR_COUNT = 20; const LINE_HEIGHT = 16; const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); let mockRectSpy: ReturnType; let computeSpy: jest.SpyInstance; let offsetWidth: number; let scrollWidth: number; function getContentHeight(this: { get: (elem?: HTMLElement) => number }, elem?: HTMLElement) { const regex = /<[^>]*>/g; let html = (elem || (this as any)).innerHTML; html = html.replace(regex, ''); const lines = Math.ceil(html.length / LINE_STR_COUNT); return lines * LINE_HEIGHT; } beforeAll(() => { jest.useFakeTimers(); mockRectSpy = spyElementPrototypes(HTMLElement, { scrollWidth: { get: () => scrollWidth, }, offsetWidth: { get: () => offsetWidth, }, scrollHeight: { get: getContentHeight, }, clientHeight: { get() { const { WebkitLineClamp } = (this as any).style; return WebkitLineClamp ? Number(WebkitLineClamp) * LINE_HEIGHT : (getContentHeight as any)(this); }, }, }); computeSpy = jest .spyOn(window, 'getComputedStyle') .mockImplementation(() => ({ fontSize: 12 }) as unknown as CSSStyleDeclaration); }); beforeEach(() => { offsetWidth = 100; scrollWidth = 0; }); afterEach(() => { errorSpy.mockReset(); }); afterAll(() => { jest.useRealTimers(); errorSpy.mockRestore(); mockRectSpy.mockRestore(); computeSpy.mockRestore(); }); const fullStr = 'Bamboo is Little Light Bamboo is Little Light Bamboo is Little Light Bamboo is Little Light Bamboo is Little Light'; it('should trigger update', async () => { const ref = React.createRef(); const onEllipsis = jest.fn(); const { container, rerender, unmount } = render( {fullStr} , ); triggerResize(ref.current!); await waitFakeTimer(); expect(container.firstChild?.textContent).toEqual('Bamboo is Little ...'); expect(onEllipsis).toHaveBeenCalledWith(true); onEllipsis.mockReset(); // Second resize rerender( {fullStr} , ); expect(container.textContent).toEqual('Bamboo is Little Light Bamboo is Litt...'); expect(onEllipsis).not.toHaveBeenCalled(); // Third resize rerender( {fullStr} , ); expect(container.querySelector('p')?.textContent).toEqual(fullStr); expect(onEllipsis).toHaveBeenCalledWith(false); unmount(); }); it('support css multiple lines', async () => { const { container: wrapper } = render( {fullStr} , ); expect( wrapper.querySelectorAll('.ant-typography-ellipsis-multiple-line').length, ).toBeGreaterThan(0); expect( ( wrapper.querySelector('.ant-typography-ellipsis-multiple-line') ?.style as any )?.WebkitLineClamp, ).toEqual('2'); }); it('string with parentheses', async () => { const parenthesesStr = `Ant Design, a design language (for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team. Ant Design, a design language for background applications, is refined by Ant UED Team.`; const ref = React.createRef(); const onEllipsis = jest.fn(); const { container: wrapper, unmount } = render( {parenthesesStr} , ); triggerResize(ref.current!); await waitFakeTimer(); expect(wrapper.firstChild?.textContent).toEqual('Ant Design, a des...'); const ellipsisSpans = wrapper.querySelectorAll('span[aria-hidden]'); expect(ellipsisSpans[ellipsisSpans.length - 1].textContent).toEqual('...'); onEllipsis.mockReset(); unmount(); }); it('should middle ellipsis', async () => { const suffix = '--suffix'; const ref = React.createRef(); const { container: wrapper, unmount } = render( {fullStr} , ); triggerResize(ref.current!); await waitFakeTimer(); expect(wrapper.querySelector('p')?.textContent).toEqual('Bamboo is...--suffix'); unmount(); }); it('should front or middle ellipsis', async () => { const suffix = '--The information is very important'; const ref = React.createRef(); const { container: wrapper, rerender, unmount, } = render( {fullStr} , ); triggerResize(ref.current!); await waitFakeTimer(); expect(wrapper.querySelector('p')?.textContent).toEqual( '...--The information is very important', ); rerender( {fullStr} , ); expect(wrapper.querySelector('p')?.textContent).toEqual( 'Ba...--The information is very important', ); rerender( {fullStr} , ); expect(wrapper.querySelector('p')?.textContent).toEqual(fullStr + suffix); unmount(); }); it('connect children', async () => { const bamboo = 'Bamboo'; const is = ' is '; const ref = React.createRef(); const { container: wrapper } = render( {bamboo} {is} Little Light , ); triggerResize(ref.current!); await waitFakeTimer(); expect(wrapper.textContent).toEqual('Bamboo is Little...'); }); it('should expandable work', async () => { const onExpand = jest.fn(); const ref = React.createRef(); const { container } = render( {fullStr} , ); triggerResize(ref.current!); await waitFakeTimer(); fireEvent.click(container.querySelector('.ant-typography-expand')!); expect(onExpand).toHaveBeenCalled(); expect(container.querySelector('p')?.textContent).toEqual(fullStr); }); it('should collapsible work', async () => { const ref = React.createRef(); const { container: wrapper } = render( (expanded ? 'CloseIt' : 'OpenIt'), }} component="p" ref={ref} > {fullStr} , ); triggerResize(ref.current!); await waitFakeTimer(); expect(wrapper.querySelector('p')?.textContent).toEqual(`Bamboo is L...OpenIt`); fireEvent.click(wrapper.querySelector('.ant-typography-expand')!); expect(wrapper.querySelector('p')?.textContent).toEqual(`${fullStr}CloseIt`); fireEvent.click(wrapper.querySelector('.ant-typography-collapse')!); expect(wrapper.querySelector('p')?.textContent).toEqual(`Bamboo is L...OpenIt`); }); it('should have custom expand style', async () => { const ref = React.createRef(); const symbol = 'more'; const { container } = render( {fullStr} , ); triggerResize(ref.current!); await waitFakeTimer(); expect(container.querySelector('.ant-typography-expand')?.textContent).toEqual('more'); }); describe('native css ellipsis', () => { it('can use css ellipsis', () => { const { container } = render(); expect(container.querySelector('.ant-typography-ellipsis-single-line')).toBeTruthy(); }); // https://github.com/ant-design/ant-design/issues/36786 it('Tooltip should recheck on parent visible change', () => { const originIntersectionObserver = global.IntersectionObserver; let elementChangeCallback: () => void; const observeFn = jest.fn(); const disconnectFn = jest.fn(); (global as any).IntersectionObserver = class MockIntersectionObserver { constructor(callback: () => IntersectionObserverCallback) { elementChangeCallback = callback; } observe = observeFn; disconnect = disconnectFn; }; const { container, unmount } = render(); expect(observeFn).toHaveBeenCalled(); // Hide first act(() => { elementChangeCallback?.(); }); // Trigger visible should trigger recheck let getOffsetParent = false; Object.defineProperty(container.querySelector('.ant-typography'), 'offsetParent', { get: () => { getOffsetParent = true; return document.body; }, }); act(() => { elementChangeCallback?.(); }); expect(getOffsetParent).toBeTruthy(); unmount(); expect(disconnectFn).toHaveBeenCalled(); global.IntersectionObserver = originIntersectionObserver; }); it('should calculate padding', () => { const { container } = render( , ); expect(container.querySelector('.ant-typography-ellipsis-single-line')).toBeTruthy(); }); }); describe('should tooltip support', () => { let domSpy: ReturnType; beforeAll(() => { domSpy = spyElementPrototypes(HTMLElement, { offsetWidth: { get: () => 100, }, scrollWidth: { get: () => 200, }, }); }); afterAll(() => { domSpy.mockRestore(); }); async function getWrapper(tooltip?: EllipsisConfig['tooltip']) { const ref = React.createRef(); const wrapper = render( {fullStr} , ); triggerResize(ref.current!); await waitFakeTimer(); return wrapper; } it('boolean', async () => { const { container, baseElement } = await getWrapper(true); fireEvent.mouseEnter(container.firstChild!); await waitFor(() => { expect(baseElement.querySelector('.ant-tooltip-open')).not.toBeNull(); }); }); it('customize', async () => { const { container, baseElement } = await getWrapper('Bamboo is Light'); fireEvent.mouseEnter(container.firstChild!); await waitFor(() => { expect(baseElement.querySelector('.ant-tooltip-open')).not.toBeNull(); }); }); it('tooltip props', async () => { const { container, baseElement } = await getWrapper({ title: 'This is tooltip', className: 'tooltip-class-name', }); fireEvent.mouseEnter(container.firstChild!); await waitFor(() => { expect(container.querySelector('.tooltip-class-name')).toBeTruthy(); expect(baseElement.querySelector('.ant-tooltip-open')).not.toBeNull(); }); }); it('tooltip title true', async () => { const { container, baseElement } = await getWrapper({ title: true, className: 'tooltip-class-name', }); fireEvent.mouseEnter(container.firstChild!); await waitFor(() => { expect(container.querySelector('.tooltip-class-name')).toBeTruthy(); expect(baseElement.querySelector('.ant-tooltip-open')).not.toBeNull(); }); }); it('tooltip element', async () => { const { container, baseElement } = await getWrapper(
title
, ); fireEvent.mouseEnter(container.firstChild!); await waitFor(() => { expect(container.querySelector('.tooltip-class-name')).toBeTruthy(); expect(baseElement.querySelector('.ant-tooltip-open')).not.toBeNull(); }); }); }); it('js ellipsis should show aria-label', () => { const { container: titleWrapper } = render( , ); expect(titleWrapper.querySelector('.ant-typography')?.getAttribute('aria-label')).toEqual( 'bamboo', ); const { container: tooltipWrapper } = render( , ); expect(tooltipWrapper.querySelector('.ant-typography')?.getAttribute('aria-label')).toEqual( 'little', ); }); it('should display tooltip if line clamp', async () => { mockRectSpy = spyElementPrototypes(HTMLElement, { scrollHeight: { get() { let html = (this as any).innerHTML; html = html.replace(/<[^>]*>/g, ''); const lines = Math.ceil(html.length / LINE_STR_COUNT); return lines * 16; }, }, offsetHeight: { get: () => 32, }, offsetWidth: { get: () => 100, }, scrollWidth: { get: () => 100, }, }); const ref = React.createRef(); const { container, baseElement } = render( Ant Design, a design language for background applications, is refined by Ant UED Team. , ); triggerResize(ref.current!); await waitFakeTimer(); fireEvent.mouseEnter(container.firstChild!); await waitFor(() => { expect(baseElement.querySelector('.ant-tooltip-open')).not.toBeNull(); }); mockRectSpy.mockRestore(); }); // https://github.com/ant-design/ant-design/issues/46580 it('dynamic to be ellipsis should show tooltip', async () => { const ref = React.createRef(); render( less , ); // Force to narrow offsetWidth = 1; scrollWidth = 100; triggerResize(ref.current!); await waitFakeTimer(); fireEvent.mouseEnter(ref.current!); await waitFakeTimer(); expect(document.querySelector('.ant-tooltip')).toBeTruthy(); }); it('not force single line if expanded', () => { const renderDemo = (expanded: boolean) => ( {fullStr} ); const { container, rerender } = render(renderDemo(false)); expect(container.querySelector('.ant-typography-single-line')).toBeTruthy(); rerender(renderDemo(true)); expect(container.querySelector('.ant-typography-single-line')).toBeFalsy(); }); });