UNPKG

@plone/volto

Version:
429 lines (376 loc) 12.3 kB
import React from 'react'; import renderer from 'react-test-renderer'; import { Provider } from 'react-intl-redux'; import configureStore from 'redux-mock-store'; import { render } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import UniversalLink, { __test } from './UniversalLink'; import config from '@plone/volto/registry'; const mockStore = configureStore(); const store = mockStore({ userSession: { token: null, }, intl: { locale: 'en', messages: {}, }, }); global.console.error = vi.fn(); describe('UniversalLink', () => { it('renders a UniversalLink component with internal link', () => { const component = renderer.create( <Provider store={store}> <MemoryRouter> <UniversalLink href={'/en/welcome-to-volto'}> <h1>Title</h1> </UniversalLink> </MemoryRouter> </Provider>, ); const json = component.toJSON(); expect(json).toMatchSnapshot(); }); it('renders a UniversalLink component with external link', () => { const component = renderer.create( <Provider store={store}> <MemoryRouter> <UniversalLink href="https://github.com/plone/volto" className="custom-link" > <h1>Title</h1> </UniversalLink> </MemoryRouter> </Provider>, ); const json = component.toJSON(); expect(json).toMatchSnapshot(); }); it('renders a UniversalLink component if no external(href) link passed', () => { const component = renderer.create( <Provider store={store}> <MemoryRouter> <UniversalLink item={{ '@id': 'http://localhost:3000/en/welcome-to-volto', }} > <h1>Title</h1> </UniversalLink> </MemoryRouter> </Provider>, ); const json = component.toJSON(); expect(json).toMatchSnapshot(); }); it('check UniversalLink set rel attribute for ext links', () => { const { getByTitle } = render( <Provider store={store}> <MemoryRouter> <UniversalLink href="https://github.com/plone/volto" title="Volto GitHub repository" > <h1>Title</h1> </UniversalLink> </MemoryRouter> </Provider>, ); expect(getByTitle('Volto GitHub repository').getAttribute('rel')).toBe( 'noopener noreferrer', ); }); it('check UniversalLink append http when user has not entered the protocol', () => { const { getByTitle } = render( <Provider store={store}> <MemoryRouter> <UniversalLink href="www.github.com" title="Volto GitHub repository"> <h1>Title</h1> </UniversalLink> </MemoryRouter> </Provider>, ); expect(getByTitle('Volto GitHub repository').getAttribute('href')).toBe( 'http://www.github.com', ); }); it('check UniversalLink set target attribute for ext links', () => { const { getByTitle } = render( <Provider store={store}> <MemoryRouter> <UniversalLink href="https://github.com/plone/volto" title="Volto GitHub repository" > <h1>Title</h1> </UniversalLink> </MemoryRouter> </Provider>, ); expect(getByTitle('Volto GitHub repository').getAttribute('target')).toBe( '_blank', ); }); it('check UniversalLink can unset target for ext links with prop', () => { const { getByTitle } = render( <Provider store={store}> <MemoryRouter> <UniversalLink href="https://github.com/plone/volto" title="Volto GitHub repository" openLinkInNewTab={false} > <h1>Title</h1> </UniversalLink> </MemoryRouter> </Provider>, ); expect(getByTitle('Volto GitHub repository').getAttribute('target')).toBe( null, ); }); it('check UniversalLink renders ext link for blacklisted urls', () => { config.settings.externalRoutes = [ { match: { path: '/external-app', exact: true, strict: false, }, url(payload) { return payload.location.pathname; }, }, ]; const { getByTitle } = render( <Provider store={store}> <MemoryRouter> <UniversalLink href="http://localhost:3000/external-app" title="Blacklisted route" > <h1>Title</h1> </UniversalLink> </MemoryRouter> </Provider>, ); expect(getByTitle('Blacklisted route').getAttribute('target')).toBe( '_blank', ); }); it('UniversalLink renders external link where link is blacklisted', () => { const notInEN = /^(?!.*(#|\/en|\/static|\/controlpanel|\/cypress|\/login|\/logout|\/contact-form)).*$/; config.settings.externalRoutes = [ { match: { path: notInEN, exact: false, strict: false, }, url(payload) { return payload.location.pathname; }, }, ]; const { getByTitle } = render( <Provider store={store}> <MemoryRouter> <UniversalLink href="http://localhost:3000/blacklisted-app" title="External blacklisted app" > <h1>Title</h1> </UniversalLink> </MemoryRouter> </Provider>, ); expect(getByTitle('External blacklisted app').getAttribute('target')).toBe( '_blank', ); expect(getByTitle('External blacklisted app').getAttribute('rel')).toBe( 'noopener noreferrer', ); }); it('check UniversalLink does not break with error in item', () => { const component = renderer.create( <Provider store={store}> <MemoryRouter> <UniversalLink item={{ error: 'Error while fetching content', message: 'Something went wrong', }} > <h1>Title</h1> </UniversalLink> </MemoryRouter> </Provider>, ); const json = component.toJSON(); expect(json).toMatchSnapshot(); expect(global.console.error).toHaveBeenCalled(); }); it('renders a UniversalLink component when url ends with @@display-file', () => { const component = renderer.create( <Provider store={store}> <MemoryRouter> <UniversalLink href="http://localhost:3000/en/welcome-to-volto/@@display-file"> <h1>Title</h1> </UniversalLink> </MemoryRouter> </Provider>, ); const json = component.toJSON(); expect(json).toMatchSnapshot(); }); test('only one UniversalLink re-renders when prop changes (stable references)', () => { const renderCounter = vi.fn(); __test.renderCounter = renderCounter; const itemA = { '@id': '/en/a' }; const itemB = { '@id': '/en/b' }; const itemC = { '@id': '/en/c' }; const Wrapper = ({ children }) => ( <Provider store={store}> <MemoryRouter>{children}</MemoryRouter> </Provider> ); const { rerender } = render( <> <UniversalLink item={itemA} /> <UniversalLink item={itemB} /> <UniversalLink item={itemC} /> </>, { wrapper: Wrapper }, ); // expect 3 renders expect(renderCounter).toHaveBeenCalledTimes(3); const updatedItemB = { '@id': '/en/b-updated' }; rerender( <> <UniversalLink item={itemA} /> <UniversalLink item={updatedItemB} /> <UniversalLink item={itemC} /> </>, ); // expect 4 renders (only one UniversalLink re-renders) expect(renderCounter).toHaveBeenCalledTimes(4); }); test('only one UniversalLink re-renders when prop changes (with children - stable references)', () => { const renderCounter = vi.fn(); __test.renderCounter = renderCounter; const itemA = { '@id': '/en/a' }; const itemB = { '@id': '/en/b' }; const itemC = { '@id': '/en/c' }; const title = 'Title'; const Wrapper = ({ children }) => ( <Provider store={store}> <MemoryRouter>{children}</MemoryRouter> </Provider> ); const { rerender } = render( <> <UniversalLink item={itemA}>{title}</UniversalLink> <UniversalLink item={itemB}>{title}</UniversalLink> <UniversalLink item={itemC}>{title}</UniversalLink> </>, { wrapper: Wrapper }, ); // expect 3 renders expect(renderCounter).toHaveBeenCalledTimes(3); const updatedItemB = { '@id': '/en/b-updated' }; rerender( <> <UniversalLink item={itemA}>{title}</UniversalLink> <UniversalLink item={updatedItemB}>{title}</UniversalLink> <UniversalLink item={itemC}>{title}</UniversalLink> </>, ); // expect 4 renders (only one UniversalLink re-renders) expect(renderCounter).toHaveBeenCalledTimes(4); }); test('[NEGATIVE TEST] UniversalLink re-renders all instances when children are inline JSX (React.memo ineffective)', () => { // NEGATIVE TEST: // This test demonstrates that React.memo does NOT prevent re-renders // when props like `children` are passed as inline JSX. // This is expected behavior due to unstable object references. // Do NOT use inline props if render optimization is required. const renderCounter = vi.fn(); __test.renderCounter = renderCounter; const itemA = { '@id': '/en/a' }; const itemB = { '@id': '/en/b' }; const itemC = { '@id': '/en/c' }; const Wrapper = ({ children }) => ( <Provider store={store}> <MemoryRouter>{children}</MemoryRouter> </Provider> ); const { rerender } = render( <> <UniversalLink item={itemA}> <h1>Title</h1> </UniversalLink> <UniversalLink item={itemB}> <h1>Title</h1> </UniversalLink> <UniversalLink item={itemC}> <h1>Title</h1> </UniversalLink> </>, { wrapper: Wrapper }, ); // expect 3 renders expect(renderCounter).toHaveBeenCalledTimes(3); const updatedItemB = { '@id': '/en/b-updated' }; rerender( <> <UniversalLink item={itemA}> <h1>Title</h1> </UniversalLink> <UniversalLink item={updatedItemB}> <h1>Title</h1> </UniversalLink> <UniversalLink item={itemC}> <h1>Title</h1> </UniversalLink> </>, ); // expect 6 renders (React.memo does NOT prevent re-renders when props like `children` are passed as inline JSX.) expect(renderCounter).toHaveBeenCalledTimes(6); }); test('[NEGATIVE TEST] UniversalLink re-renders all instances when props are inline JSX (React.memo ineffective)', () => { // NEGATIVE TEST: // This test demonstrates that React.memo does NOT prevent re-renders // when props like `item` are passed as inline object. // This is expected behavior due to unstable object references. // Do NOT use inline props if render optimization is required. const renderCounter = vi.fn(); __test.renderCounter = renderCounter; const title = 'Title'; const Wrapper = ({ children }) => ( <Provider store={store}> <MemoryRouter>{children}</MemoryRouter> </Provider> ); const { rerender } = render( <> <UniversalLink item={{ '@id': '/en/a' }}>{title}</UniversalLink> <UniversalLink item={{ '@id': '/en/b' }}>{title}</UniversalLink> <UniversalLink item={{ '@id': '/en/c' }}>{title}</UniversalLink> </>, { wrapper: Wrapper }, ); // expect 3 renders expect(renderCounter).toHaveBeenCalledTimes(3); rerender( <> <UniversalLink item={{ '@id': '/en/a' }}>{title}</UniversalLink> <UniversalLink item={{ '@id': '/en/b' }}>{title}</UniversalLink> <UniversalLink item={{ '@id': '/en/c' }}>{title}</UniversalLink> </>, ); // expect 6 renders (React.memo does NOT prevent re-renders when props like `children` are passed as inline JSX.) expect(renderCounter).toHaveBeenCalledTimes(6); }); });