import React from 'react'; import { resetWarned } from 'rc-util/lib/warning'; import scrollIntoView from 'scroll-into-view-if-needed'; import Anchor from '..'; import { fireEvent, render, waitFakeTimer } from '../../../tests/utils'; const { Link } = Anchor; function createDiv() { const root = document.createElement('div'); document.body.appendChild(root); return root; } let idCounter = 0; const getHashUrl = () => `Anchor-API-${idCounter++}`; jest.mock('scroll-into-view-if-needed', () => jest.fn()); describe('Anchor Render', () => { const getBoundingClientRectMock = jest.spyOn( HTMLHeadingElement.prototype, 'getBoundingClientRect', ); const getClientRectsMock = jest.spyOn(HTMLHeadingElement.prototype, 'getClientRects'); const scrollIntoViewMock = jest.createMockFromModule('scroll-into-view-if-needed'); beforeAll(() => { jest.useFakeTimers(); getBoundingClientRectMock.mockReturnValue({ width: 100, height: 100, top: 1000, } as DOMRect); getClientRectsMock.mockReturnValue([1] as unknown as DOMRectList); }); beforeEach(() => { jest.useFakeTimers(); scrollIntoViewMock.mockReset(); }); afterEach(() => { jest.clearAllTimers(); jest.useRealTimers(); }); afterAll(() => { jest.clearAllTimers(); jest.useRealTimers(); getBoundingClientRectMock.mockRestore(); getClientRectsMock.mockRestore(); }); it('renders items correctly', () => { const { container, asFragment } = render( , ); expect(container.querySelectorAll('.ant-anchor .ant-anchor-link').length).toBe(5); const linkTitles = Array.from(container.querySelector('.ant-anchor')?.childNodes!).map((n) => (n as HTMLElement).querySelector('.ant-anchor-link-title'), ); expect((linkTitles[1] as HTMLAnchorElement).href).toContain('#components-anchor-demo-basic'); expect((linkTitles[2] as HTMLAnchorElement).href).toContain('#components-anchor-demo-static'); expect((linkTitles[3] as HTMLAnchorElement).href).toContain('#api'); expect( ( container.querySelector( '.ant-anchor .ant-anchor-link .ant-anchor-link .ant-anchor-link-title', ) as HTMLAnchorElement )?.href, ).toContain('#anchor-props'); expect( ( container.querySelector( '.ant-anchor .ant-anchor-link .ant-anchor-link .ant-anchor-link .ant-anchor-link-title', ) as HTMLAnchorElement )?.href, ).toContain('#link-props'); expect(asFragment().firstChild).toMatchSnapshot(); }); it('renders items correctly#horizontal', () => { const { container, asFragment } = render( , ); expect(container.querySelectorAll('.ant-anchor .ant-anchor-link').length).toBe(3); const linkTitles = Array.from(container.querySelector('.ant-anchor')?.childNodes!).map((n) => (n as HTMLElement).querySelector('.ant-anchor-link-title'), ); expect((linkTitles[1] as HTMLAnchorElement).href).toContain('#components-anchor-demo-basic'); expect((linkTitles[2] as HTMLAnchorElement).href).toContain('#components-anchor-demo-static'); expect((linkTitles[3] as HTMLAnchorElement).href).toContain('#api'); expect(asFragment().firstChild).toMatchSnapshot(); }); it('render items and ignore jsx children', () => { const { container, asFragment } = render( , ); expect(container.querySelectorAll('.ant-anchor .ant-anchor-link').length).toBe(1); expect( (container.querySelector('.ant-anchor .ant-anchor-link-title') as HTMLAnchorElement).href, ).toContain('#components-anchor-demo-basic'); expect(asFragment().firstChild).toMatchSnapshot(); }); it('actives the target when clicking a link', async () => { const hash = getHashUrl(); const { container } = render( , ); const link = container.querySelector(`a[href="http://www.example.com/#${hash}"]`)!; fireEvent.click(link); await waitFakeTimer(); expect(link.classList).toContain('ant-anchor-link-title-active'); }); it('scrolls the page when clicking a link', async () => { const root = createDiv(); const scrollToSpy = jest.spyOn(window, 'scrollTo'); render(
Q1
, { container: root }); const { container } = render( , ); const link = container.querySelector(`a[href="/#/faq?locale=en#Q1"]`)!; fireEvent.click(link); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenCalled(); }); it('handleScroll should not be triggered when scrolling caused by clicking a link', async () => { const hash1 = getHashUrl(); const hash2 = getHashUrl(); const root = createDiv(); const onChange = jest.fn(); render(
Hello
World
, { container: root }, ); const { container } = render( , ); onChange.mockClear(); const link = container.querySelector(`a[href="#${hash2}"]`)!; // this will trigger 1 onChange fireEvent.click(link); // smooth scroll caused by clicking needs time to finish. // we scroll the window before it finish, the scroll listener should not be triggered, fireEvent.scroll(window); await waitFakeTimer(); // if the scroll listener is triggered, we will get 2 onChange, now we expect only 1. expect(onChange).toHaveBeenCalledTimes(1); }); it('should update DOM when children are unmounted', () => { const hash = getHashUrl(); const { container, rerender } = render( , ); expect(container.querySelectorAll('.ant-anchor-link-title')).toHaveLength(1); expect(container.querySelector('.ant-anchor-link-title')).toHaveAttribute('href', `#${hash}`); rerender(); expect(container.querySelector('.ant-anchor-link-title')).toBeFalsy(); }); it('should update DOM when link href is changed', async () => { const hash = getHashUrl(); function AnchorUpdate({ href }: { href: string }) { return ; } const { container, rerender } = render(); expect(container.querySelector(`a[href="#${hash}"]`)).toBeTruthy(); rerender(); expect(container.querySelector(`a[href="#${hash}_1"]`)).toBeTruthy(); }); it('targetOffset prop', async () => { const hash = getHashUrl(); const scrollToSpy = jest.spyOn(window, 'scrollTo'); const root = createDiv(); render(

Hello

, { container: root }); const { container, rerender } = render( , ); const setProps = (props: Record) => rerender(); fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 1000); setProps({ offsetTop: 100 }); fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 900); setProps({ targetOffset: 200 }); fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800); }); // https://github.com/ant-design/ant-design/issues/31941 it('targetOffset prop when contain spaces', async () => { const hash = `${getHashUrl()} s p a c e s`; const scrollToSpy = jest.spyOn(window, 'scrollTo'); const root = createDiv(); render(

Hello

, { container: root }); const { container, rerender } = render( , ); const setProps = (props: Record) => rerender(); fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 1000); setProps({ offsetTop: 100 }); fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 900); setProps({ targetOffset: 200 }); fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800); }); it('onClick event', () => { const hash = getHashUrl(); let event; let link; const handleClick = ( e: React.MouseEvent, _link: { title: React.ReactNode; href: string }, ) => { event = e; link = _link; }; const href = `#${hash}`; const title = hash; const { container } = render( , ); fireEvent.click(container.querySelector(`a[href="${href}"]`)!); expect(event).not.toBe(undefined); expect(link).toEqual({ href, title }); }); it('onChange event', () => { const hash1 = getHashUrl(); const hash2 = getHashUrl(); const onChange = jest.fn(); const { container } = render( , // https://github.com/testing-library/react-testing-library/releases/tag/v13.0.0 { legacyRoot: true }, ); expect(onChange).toHaveBeenCalledTimes(1); fireEvent.click(container.querySelector(`a[href="#${hash2}"]`)!); expect(onChange).toHaveBeenCalledTimes(2); expect(onChange).toHaveBeenLastCalledWith(`#${hash2}`); }); it('handles invalid hash correctly', () => { const { container } = render( , ); const link = container.querySelector(`a[href="notexsited"]`)!; fireEvent.click(link); expect(container.querySelector(`.ant-anchor-link-title-active`)?.textContent).toBe('title'); }); it('test edge case when getBoundingClientRect return zero size', async () => { getBoundingClientRectMock.mockReturnValue({ width: 0, height: 0, top: 1000 } as DOMRect); const hash = getHashUrl(); const scrollToSpy = jest.spyOn(window, 'scrollTo'); const root = createDiv(); render(

Hello

, { container: root }); const { container, rerender } = render( , ); const setProps = (props: Record) => rerender( , ); fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 1000); setProps({ offsetTop: 100 }); fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 900); setProps({ targetOffset: 200 }); fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800); getBoundingClientRectMock.mockReturnValue({ width: 100, height: 100, top: 1000, } as DOMRect); }); it('test edge case when container is not windows', async () => { const hash = getHashUrl(); const scrollToSpy = jest.spyOn(window, 'scrollTo'); const root = createDiv(); render(

Hello

, { container: root }); const { container, rerender } = render( document.body}> , ); const setProps = (props: Record) => rerender( document.body} {...props}> , ); fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800); setProps({ offsetTop: 100 }); fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800); setProps({ targetOffset: 200 }); fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800); }); describe('getCurrentAnchor', () => { it('getCurrentAnchor prop', () => { const hash1 = getHashUrl(); const hash2 = getHashUrl(); const getCurrentAnchor = () => `#${hash2}`; const { container } = render( , ); expect(container.querySelector(`.ant-anchor-link-title-active`)?.textContent).toBe(hash2); }); // https://github.com/ant-design/ant-design/issues/30584 it('should trigger onChange when have getCurrentAnchor', () => { const hash1 = getHashUrl(); const hash2 = getHashUrl(); const onChange = jest.fn(); const { container } = render( hash1} items={[ { key: hash1, href: `#${hash1}`, title: hash1 }, { key: hash2, href: `#${hash2}`, title: hash2 }, ]} />, // https://github.com/testing-library/react-testing-library/releases/tag/v13.0.0 { legacyRoot: true }, ); expect(onChange).toHaveBeenCalledTimes(1); fireEvent.click(container.querySelector(`a[href="#${hash2}"]`)!); expect(onChange).toHaveBeenCalledTimes(2); expect(onChange).toHaveBeenLastCalledWith(`#${hash2}`); }); // https://github.com/ant-design/ant-design/issues/34784 it('getCurrentAnchor have default link as argument', () => { const hash1 = getHashUrl(); const hash2 = getHashUrl(); const getCurrentAnchor = jest.fn(); const { container } = render( , ); fireEvent.click(container.querySelector(`a[href="#${hash1}"]`)!); expect(getCurrentAnchor).toHaveBeenCalledWith(`#${hash1}`); fireEvent.click(container.querySelector(`a[href="#${hash2}"]`)!); expect(getCurrentAnchor).toHaveBeenCalledWith(`#${hash2}`); }); // https://github.com/ant-design/ant-design/issues/37627 it('should update active link when getCurrentAnchor changes', async () => { const hash1 = getHashUrl(); const hash2 = getHashUrl(); const Demo: React.FC<{ current: string }> = ({ current }) => ( `#${current}`} items={[ { key: hash1, href: `#${hash1}`, title: hash1 }, { key: hash2, href: `#${hash2}`, title: hash2 }, ]} /> ); const { container, rerender } = render(); expect(container.querySelector(`.ant-anchor-link-title-active`)?.textContent).toBe(hash1); rerender(); expect(container.querySelector(`.ant-anchor-link-title-active`)?.textContent).toBe(hash2); }); it('should render correctly when href is null', () => { expect(() => { render( , ); fireEvent.scroll(window || document); }).not.toThrow(); }); }); describe('horizontal anchor', () => { describe('scroll x', () => { it('targetOffset horizontal', async () => { const hash = getHashUrl(); const scrollToSpy = jest.spyOn(window, 'scrollTo'); const root = createDiv(); render(

Hello

, { container: root }); const { container, rerender } = render( , ); const setProps = (props: Record) => rerender( , ); fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); await waitFakeTimer(); expect(scrollIntoView).toHaveBeenCalled(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 1000); setProps({ offsetTop: 100 }); fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 900); setProps({ targetOffset: 200 }); fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800); }); }); it('test direction prop', () => { const { container } = render( , ); expect(container.querySelectorAll('.ant-anchor-ink').length).toBe(1); expect( container .querySelector('.ant-anchor-wrapper') ?.classList.contains('ant-anchor-wrapper-horizontal'), ).toBeTruthy(); }); it('nested children via items should be filtered out when direction is horizontal', () => { const { container } = render( , ); expect(container.querySelectorAll('.ant-anchor-link').length).toBe(3); }); it('nested children via jsx should be filtered out when direction is horizontal', () => { const { container } = render( , ); expect(container.querySelectorAll('.ant-anchor-link').length).toBe(3); }); }); describe('deprecated/legacy jsx syntax', () => { it('renders jsx correctly', () => { const hash = getHashUrl(); const { container } = render( , ); expect(container.querySelector(`a[href="#${hash}"]`)).not.toBe(null); }); it('actives the target when clicking a link', async () => { const hash = getHashUrl(); const { container } = render( , ); const link = container.querySelector(`a[href="http://www.example.com/#${hash}"]`)!; fireEvent.click(link); await waitFakeTimer(); expect(link.classList).toContain('ant-anchor-link-title-active'); }); it('scrolls the page when clicking a link', async () => { const root = createDiv(); const scrollToSpy = jest.spyOn(window, 'scrollTo'); render(
Q1
, { container: root }); const { container } = render( , ); const link = container.querySelector(`a[href="/#/faq?locale=en#Q1"]`)!; fireEvent.click(link); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenCalled(); }); it('handleScroll should not be triggered when scrolling caused by clicking a link', async () => { const hash1 = getHashUrl(); const hash2 = getHashUrl(); const root = createDiv(); const onChange = jest.fn(); render(
Hello
World
, { container: root }, ); const { container } = render( , ); onChange.mockClear(); const link = container.querySelector(`a[href="#${hash2}"]`)!; // this will trigger 1 onChange fireEvent.click(link); // smooth scroll caused by clicking needs time to finish. // we scroll the window before it finish, the scroll listener should not be triggered, fireEvent.scroll(window); await waitFakeTimer(); // if the scroll listener is triggered, we will get 2 onChange, now we expect only 1. expect(onChange).toHaveBeenCalledTimes(1); }); it('should update DOM when children are unmounted', () => { const hash = getHashUrl(); const { container, rerender } = render( , ); expect(container.querySelectorAll('.ant-anchor-link-title')).toHaveLength(1); expect(container.querySelector('.ant-anchor-link-title')).toHaveAttribute('href', `#${hash}`); rerender(); expect(container.querySelector('.ant-anchor-link-title')).toBeFalsy(); }); it('should update DOM when link href is changed', async () => { const hash = getHashUrl(); function AnchorUpdate({ href }: { href: string }) { return ( ); } const { container, rerender } = render(); expect(container.querySelector(`a[href="#${hash}"]`)).toBeTruthy(); rerender(); expect(container.querySelector(`a[href="#${hash}_1"]`)).toBeTruthy(); }); it('handles invalid hash correctly', () => { const { container } = render( , ); const link = container.querySelector(`a[href="notexsited"]`)!; fireEvent.click(link); expect(container.querySelector(`.ant-anchor-link-title-active`)?.textContent).toBe('title'); }); }); describe('warning', () => { let errSpy: jest.SpyInstance; beforeEach(() => { resetWarned(); errSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); }); afterEach(() => { errSpy.mockRestore(); }); it('warning nested children when direction is horizontal ', () => { render( , ); expect(errSpy).toHaveBeenCalledWith( 'Warning: [antd: Anchor] `Anchor items#children` is not supported when `Anchor` direction is horizontal.', ); }); it('deprecated jsx style', () => { render( , ); expect(errSpy).toHaveBeenCalledWith( 'Warning: [antd: Anchor] `Anchor children` is deprecated. Please use `items` instead.', ); }); it('deprecated jsx style for direction#vertical', () => { render( , ); expect(errSpy).toHaveBeenCalledWith( 'Warning: [antd: Anchor] `Anchor children` is deprecated. Please use `items` instead.', ); }); it('deprecated jsx style for direction#vertical 1: with nested children', () => { render( , ); expect(errSpy).toHaveBeenCalledWith( 'Warning: [antd: Anchor] `Anchor children` is deprecated. Please use `items` instead.', ); expect(errSpy).toHaveBeenCalledWith( 'Warning: [antd: Anchor.Link] `Anchor.Link children` is not supported when `Anchor` direction is horizontal', ); }); }); });