@plone/volto
Version:
Volto
429 lines (376 loc) • 12.3 kB
JSX
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);
});
});