import React, { useState } from 'react'; import { resetWarned } from 'rc-util/lib/warning'; import scrollIntoView from 'scroll-into-view-if-needed'; import Anchor from '..'; import { act, fireEvent, render, waitFakeTimer } from '../../../tests/utils'; import Button from '../../button'; import type { AnchorDirection } from '../Anchor'; 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()); Object.defineProperty(window, 'location', { value: { replace: 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('#anchor-demo-basic'); expect((linkTitles[2] as HTMLAnchorElement).href).toContain('#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('#anchor-demo-basic'); expect((linkTitles[2] as HTMLAnchorElement).href).toContain('#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('#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('replaces item href in browser history', () => { const hash = getHashUrl(); const href = `#${hash}`; const title = hash; const { container } = render(); fireEvent.click(container.querySelector(`a[href="${href}"]`)!); expect(window.location.replace).toHaveBeenCalledWith(href); }); 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('should be used the latest onChange method', () => { const hash1 = getHashUrl(); const hash2 = getHashUrl(); const beforeFn = jest.fn(); const afterFn = jest.fn(); const Demo: React.FC = () => { const [trigger, setTrigger] = useState(false); const onChange = trigger ? afterFn : beforeFn; return ( <> ); }; const wrapper = await render(); (await wrapper.findByText('part-1')).click(); await waitFakeTimer(); const ink = wrapper.container.querySelector('.ant-anchor-ink')!; const toggleButton = wrapper.container.querySelector('button')!; fireEvent.click(toggleButton); act(() => jest.runAllTimers()); expect(!!ink.style.left).toBe(true); expect(!!ink.style.width).toBe(true); expect(ink.style.top).toBe(''); expect(ink.style.height).toBe(''); fireEvent.click(toggleButton); act(() => jest.runAllTimers()); expect(!!ink.style.top).toBe(true); expect(!!ink.style.height).toBe(true); expect(ink.style.left).toBe(''); expect(ink.style.width).toBe(''); }); }); });