import React from 'react'; import Anchor from '..'; import { fireEvent, render, waitFakeTimer } from '../../../tests/utils'; import type { InternalAnchorClass } from '../Anchor'; const { Link } = Anchor; function createGetContainer(id: string) { return () => { const container = document.getElementById(id); if (container == null) { throw new Error(); } return container; }; } function createDiv() { const root = document.createElement('div'); document.body.appendChild(root); return root; } let idCounter = 0; const getHashUrl = () => `Anchor-API-${idCounter++}`; describe('Anchor Render', () => { const getBoundingClientRectMock = jest.spyOn( HTMLHeadingElement.prototype, 'getBoundingClientRect', ); const getClientRectsMock = jest.spyOn(HTMLHeadingElement.prototype, 'getClientRects'); beforeAll(() => { jest.useFakeTimers(); getBoundingClientRectMock.mockReturnValue({ width: 100, height: 100, top: 1000, } as DOMRect); getClientRectsMock.mockReturnValue({ length: 1 } as DOMRectList); }); afterEach(() => { jest.clearAllTimers(); }); afterAll(() => { jest.useRealTimers(); getBoundingClientRectMock.mockRestore(); getClientRectsMock.mockRestore(); }); it('Anchor render perfectly', () => { const hash = getHashUrl(); let anchorInstance: InternalAnchorClass; const { container } = render( { anchorInstance = node as InternalAnchorClass; }} > , ); fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); anchorInstance!.handleScroll(); expect(anchorInstance!.state).not.toBe(null); }); it('Anchor render perfectly for complete href - click', () => { const hash = getHashUrl(); let anchorInstance: InternalAnchorClass; const { container } = render( { anchorInstance = node as InternalAnchorClass; }} > , ); fireEvent.click(container.querySelector(`a[href="http://www.example.com/#${hash}"]`)!); expect(anchorInstance!.state!.activeLink).toBe(`http://www.example.com/#${hash}`); }); it('Anchor render perfectly for complete href - hash router', async () => { const root = createDiv(); const scrollToSpy = jest.spyOn(window, 'scrollTo'); render(
Q1
, { container: root }); let anchorInstance: InternalAnchorClass; render( { anchorInstance = node as InternalAnchorClass; }} > , ); anchorInstance!.handleScrollTo('/#/faq?locale=en#Q1'); expect(anchorInstance!.state.activeLink).toBe('/#/faq?locale=en#Q1'); expect(scrollToSpy).not.toHaveBeenCalled(); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenCalled(); }); it('Anchor render perfectly for complete href - scroll', () => { const hash = getHashUrl(); const root = createDiv(); render(
Hello
, { container: root }); let anchorInstance: InternalAnchorClass; render( { anchorInstance = node as InternalAnchorClass; }} > , ); anchorInstance!.handleScroll(); expect(anchorInstance!.state!.activeLink).toBe(`http://www.example.com/#${hash}`); }); it('Anchor render perfectly for complete href - scrollTo', async () => { const hash = getHashUrl(); const scrollToSpy = jest.spyOn(window, 'scrollTo'); const root = createDiv(); render(
Hello
, { container: root }); let anchorInstance: InternalAnchorClass; render( { anchorInstance = node as InternalAnchorClass; }} > , ); anchorInstance!.handleScrollTo(`##${hash}`); expect(anchorInstance!.state.activeLink).toBe(`##${hash}`); const calls = scrollToSpy.mock.calls.length; await waitFakeTimer(); expect(scrollToSpy.mock.calls.length).toBeGreaterThan(calls); }); it('should remove listener when unmount', async () => { const hash = getHashUrl(); let anchorInstance: InternalAnchorClass; const { unmount } = render( { anchorInstance = node as InternalAnchorClass; }} > , ); const removeListenerSpy = jest.spyOn((anchorInstance! as any).scrollEvent, 'remove'); unmount(); expect(removeListenerSpy).toHaveBeenCalled(); }); it('should unregister link when unmount children', () => { 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 links when link href update', async () => { const hash = getHashUrl(); let anchorInstance: InternalAnchorClass; function AnchorUpdate({ href }: { href: string }) { return ( { anchorInstance = node as InternalAnchorClass; }} > ); } const { rerender } = render(); if (anchorInstance! == null) { throw new Error('anchorInstance should not be null'); } expect((anchorInstance as any)!.links).toEqual([`#${hash}`]); rerender(); expect((anchorInstance as any)!.links).toEqual([`#${hash}_1`]); }); it('Anchor 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; let anchorInstance: InternalAnchorClass; const { container } = render( { anchorInstance = node as InternalAnchorClass; }} > , ); fireEvent.click(container.querySelector(`a[href="${href}"]`)!); anchorInstance!.handleScroll(); expect(event).not.toBe(undefined); expect(link).toEqual({ href, title }); }); it('Different function returns the same DOM', async () => { const hash = getHashUrl(); const root = createDiv(); render(
Hello
, { container: root }); const getContainerA = createGetContainer(hash); const getContainerB = createGetContainer(hash); let anchorInstance: InternalAnchorClass; const { rerender } = render( { anchorInstance = node as InternalAnchorClass; }} > , ); const removeListenerSpy = jest.spyOn((anchorInstance! as any).scrollEvent, 'remove'); await waitFakeTimer(); rerender( , ); expect(removeListenerSpy).not.toHaveBeenCalled(); }); it('Different function returns different DOM', async () => { const hash1 = getHashUrl(); const hash2 = getHashUrl(); const root = createDiv(); render(
Hello
World
, { container: root }, ); const getContainerA = createGetContainer(hash1); const getContainerB = createGetContainer(hash2); let anchorInstance: InternalAnchorClass; const { rerender } = render( { anchorInstance = node as InternalAnchorClass; }} > , ); const removeListenerSpy = jest.spyOn((anchorInstance! as any).scrollEvent, 'remove'); expect(removeListenerSpy).not.toHaveBeenCalled(); await waitFakeTimer(); rerender( , ); expect(removeListenerSpy).toHaveBeenCalled(); }); it('Same function returns the same DOM', () => { const hash = getHashUrl(); const root = createDiv(); render(
Hello
, { container: root }); const getContainer = createGetContainer(hash); let anchorInstance: InternalAnchorClass; const { container } = render( { anchorInstance = node as InternalAnchorClass; }} > , ); fireEvent.click(container.querySelector(`a[href="#${hash}"]`)!); anchorInstance!.handleScroll(); expect(anchorInstance!.state).not.toBe(null); }); it('Same function returns different DOM', async () => { const hash1 = getHashUrl(); const hash2 = getHashUrl(); const root = createDiv(); render(
Hello
World
, { container: root }, ); const holdContainer = { container: document.getElementById(hash1), }; const getContainer = () => { if (holdContainer.container == null) { throw new Error('container should not be null'); } return holdContainer.container; }; let anchorInstance: InternalAnchorClass; const { rerender } = render( { anchorInstance = node as InternalAnchorClass; }} > , ); const removeListenerSpy = jest.spyOn((anchorInstance! as any).scrollEvent, 'remove'); expect(removeListenerSpy).not.toHaveBeenCalled(); await waitFakeTimer(); holdContainer.container = document.getElementById(hash2); rerender( , ); expect(removeListenerSpy).toHaveBeenCalled(); }); it('Anchor targetOffset prop', async () => { const hash = getHashUrl(); let dateNowMock; function dataNowMockFn() { let start = 0; const handler = () => { start += 1000; return start; }; return jest.spyOn(Date, 'now').mockImplementation(handler); } dateNowMock = dataNowMockFn(); const scrollToSpy = jest.spyOn(window, 'scrollTo'); const root = createDiv(); render(

Hello

, { container: root }); let anchorInstance: InternalAnchorClass; const { rerender } = render( { anchorInstance = node as InternalAnchorClass; }} > , ); const setProps = (props: Record) => rerender( { anchorInstance = node as InternalAnchorClass; }} {...props} > , ); anchorInstance!.handleScrollTo(`#${hash}`); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 1000); dateNowMock = dataNowMockFn(); setProps({ offsetTop: 100 }); anchorInstance!.handleScrollTo(`#${hash}`); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 900); dateNowMock = dataNowMockFn(); setProps({ targetOffset: 200 }); anchorInstance!.handleScrollTo(`#${hash}`); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800); dateNowMock.mockRestore(); }); // https://github.com/ant-design/ant-design/issues/31941 it('Anchor targetOffset prop when contain spaces', async () => { const hash = `${getHashUrl()} s p a c e s`; let dateNowMock; function dataNowMockFn() { let start = 0; const handler = () => { start += 1000; return start; }; return jest.spyOn(Date, 'now').mockImplementation(handler); } dateNowMock = dataNowMockFn(); const scrollToSpy = jest.spyOn(window, 'scrollTo'); const root = createDiv(); render(

Hello

, { container: root }); let anchorInstance: InternalAnchorClass; const { rerender } = render( { anchorInstance = node as InternalAnchorClass; }} > , ); const setProps = (props: Record) => rerender( { anchorInstance = node as InternalAnchorClass; }} {...props} > , ); anchorInstance!.handleScrollTo(`#${hash}`); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 1000); dateNowMock = dataNowMockFn(); setProps({ offsetTop: 100 }); anchorInstance!.handleScrollTo(`#${hash}`); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 900); dateNowMock = dataNowMockFn(); setProps({ targetOffset: 200 }); anchorInstance!.handleScrollTo(`#${hash}`); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800); dateNowMock.mockRestore(); }); it('Anchor onChange prop', async () => { const hash1 = getHashUrl(); const hash2 = getHashUrl(); const onChange = jest.fn(); let anchorInstance: InternalAnchorClass; render( { anchorInstance = node as InternalAnchorClass; }} > , // https://github.com/testing-library/react-testing-library/releases/tag/v13.0.0 // @ts-ignore { legacyRoot: true }, ); expect(onChange).toHaveBeenCalledTimes(1); anchorInstance!.handleScrollTo(hash2); expect(onChange).toHaveBeenCalledTimes(2); expect(onChange).toHaveBeenCalledWith(hash2); }); it('invalid hash', async () => { let anchorInstance: InternalAnchorClass; const { container } = render( { anchorInstance = node as InternalAnchorClass; }} > , ); fireEvent.click(container.querySelector(`a[href="notexsited"]`)!); anchorInstance!.handleScrollTo('notexsited'); expect(anchorInstance!.state).not.toBe(null); }); it('test edge case when getBoundingClientRect return zero size', async () => { getBoundingClientRectMock.mockReturnValue({ width: 0, height: 0, top: 1000, } as DOMRect); const hash = getHashUrl(); let dateNowMock; function dataNowMockFn() { let start = 0; const handler = () => { start += 1000; return start; }; return jest.spyOn(Date, 'now').mockImplementation(handler); } dateNowMock = dataNowMockFn(); const scrollToSpy = jest.spyOn(window, 'scrollTo'); const root = createDiv(); render(

Hello

, { container: root }); let anchorInstance: InternalAnchorClass; const { rerender } = render( { anchorInstance = node as InternalAnchorClass; }} > , ); const setProps = (props: Record) => rerender( { anchorInstance = node as InternalAnchorClass; }} {...props} > , ); anchorInstance!.handleScrollTo(`#${hash}`); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 1000); dateNowMock = dataNowMockFn(); setProps({ offsetTop: 100 }); anchorInstance!.handleScrollTo(`#${hash}`); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 900); dateNowMock = dataNowMockFn(); setProps({ targetOffset: 200 }); anchorInstance!.handleScrollTo(`#${hash}`); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800); dateNowMock.mockRestore(); getBoundingClientRectMock.mockReturnValue({ width: 100, height: 100, top: 1000, } as DOMRect); }); it('test edge case when container is not windows', async () => { const hash = getHashUrl(); let dateNowMock; function dataNowMockFn() { let start = 0; const handler = () => { start += 1000; return start; }; return jest.spyOn(Date, 'now').mockImplementation(handler); } dateNowMock = dataNowMockFn(); const scrollToSpy = jest.spyOn(window, 'scrollTo'); const root = createDiv(); render(

Hello

, { container: root }); let anchorInstance: InternalAnchorClass; const { rerender } = render( document.body} ref={node => { anchorInstance = node as InternalAnchorClass; }} > , ); const setProps = (props: Record) => rerender( document.body} ref={node => { anchorInstance = node as InternalAnchorClass; }} {...props} > , ); anchorInstance!.handleScrollTo(`#${hash}`); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800); dateNowMock = dataNowMockFn(); setProps({ offsetTop: 100 }); anchorInstance!.handleScrollTo(`#${hash}`); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800); dateNowMock = dataNowMockFn(); setProps({ targetOffset: 200 }); anchorInstance!.handleScrollTo(`#${hash}`); await waitFakeTimer(); expect(scrollToSpy).toHaveBeenLastCalledWith(0, 800); dateNowMock.mockRestore(); }); describe('getCurrentAnchor', () => { it('Anchor getCurrentAnchor prop', () => { const hash1 = getHashUrl(); const hash2 = getHashUrl(); const getCurrentAnchor = () => `#${hash2}`; let anchorInstance: InternalAnchorClass; render( { anchorInstance = node as InternalAnchorClass; }} > , ); expect(anchorInstance!.state.activeLink).toBe(`#${hash2}`); }); // https://github.com/ant-design/ant-design/issues/30584 it('should trigger onChange when have getCurrentAnchor', async () => { const hash1 = getHashUrl(); const hash2 = getHashUrl(); const onChange = jest.fn(); let anchorInstance: InternalAnchorClass; render( hash1} ref={node => { anchorInstance = node as InternalAnchorClass; }} > , // https://github.com/testing-library/react-testing-library/releases/tag/v13.0.0 // @ts-ignore { legacyRoot: true }, ); expect(onChange).toHaveBeenCalledTimes(1); anchorInstance!.handleScrollTo(hash2); expect(onChange).toHaveBeenCalledTimes(2); expect(onChange).toHaveBeenCalledWith(hash2); }); // https://github.com/ant-design/ant-design/issues/34784 it('getCurrentAnchor have default link as argument', async () => { 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 anchorLink when component is rerender', async () => { const hash1 = getHashUrl(); const hash2 = getHashUrl(); const Demo: React.FC<{ current: string }> = ({ current }) => ( `#${current}`}> ); 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); }); }); });