Skip to content

Commit c2043fa

Browse files
committed
feat: utils/hooks unit tests
1 parent 9f9dd69 commit c2043fa

14 files changed

+2819
-1
lines changed

frontend/jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ module.exports = {
55
'^.+\\.tsx?$': 'ts-jest',
66
'^.+\\.jsx?$': 'babel-jest',
77
},
8-
collectCoverageFrom: ['src/**'],
8+
collectCoverageFrom: ['src/**', '!src/static/lib/**'],
99
};

frontend/tests/utils/hooks/useBulkActions.test.tsx

Lines changed: 749 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 380 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,380 @@
1+
import React from 'react';
2+
import { render } from '@testing-library/react';
3+
import { useItem } from '../../../src/static/js/utils/hooks/useItem';
4+
5+
// Mock the item components
6+
jest.mock('../../../src/static/js/components/list-item/includes/items', () => ({
7+
ItemDescription: ({ description }: { description: string }) => (
8+
<div data-testid="item-description">{description}</div>
9+
),
10+
ItemMain: ({ children }: { children: React.ReactNode }) => <div data-testid="item-main">{children}</div>,
11+
ItemMainInLink: ({ children, link, title }: { children: React.ReactNode; link: string; title: string }) => (
12+
<div data-testid="item-main-in-link" data-link={link} data-title={title}>
13+
{children}
14+
</div>
15+
),
16+
ItemTitle: ({ title, ariaLabel }: { title: string; ariaLabel: string }) => (
17+
<h3 data-testid="item-title" data-aria-label={ariaLabel}>
18+
{title}
19+
</h3>
20+
),
21+
ItemTitleLink: ({ title, link, ariaLabel }: { title: string; link: string; ariaLabel: string }) => (
22+
<h3 data-testid="item-title-link" data-link={link} data-aria-label={ariaLabel}>
23+
{title}
24+
</h3>
25+
),
26+
}));
27+
28+
// Mock PageStore
29+
jest.mock('../../../src/static/js/utils/stores/PageStore.js', () => ({
30+
__esModule: true,
31+
default: {
32+
get: (key: string) => (key === 'config-site' ? { url: 'https://example.com' } : null),
33+
},
34+
}));
35+
36+
// HookConsumer component to test the hook
37+
function HookConsumer(props: any) {
38+
const { titleComponent, descriptionComponent, thumbnailUrl, UnderThumbWrapper } = useItem(props);
39+
40+
return (
41+
<div>
42+
<div data-testid="title">{titleComponent()}</div>
43+
<div data-testid="description">{descriptionComponent()}</div>
44+
<div data-testid="thumbnail-url">{thumbnailUrl || 'null'}</div>
45+
<div data-testid="wrapper-type">{(UnderThumbWrapper as any).name}</div>
46+
<div data-testid="wrapper-component">
47+
<div>Wrapper content</div>
48+
</div>
49+
</div>
50+
);
51+
}
52+
53+
// Wrapper consumer to test wrapper selection
54+
function WrapperTest(props: any) {
55+
const { UnderThumbWrapper } = useItem(props);
56+
57+
return (
58+
<UnderThumbWrapper link={props.link} title={props.title} data-testid="wrapper-test">
59+
<span data-testid="wrapper-content">Content</span>
60+
</UnderThumbWrapper>
61+
);
62+
}
63+
64+
describe('utils/hooks', () => {
65+
describe('useItem', () => {
66+
beforeEach(() => {
67+
jest.clearAllMocks();
68+
});
69+
70+
describe('titleComponent Rendering', () => {
71+
test('Renders ItemTitle when singleLinkContent is true', () => {
72+
const { getByTestId } = render(
73+
<HookConsumer
74+
title="Test Title"
75+
description="Test Description"
76+
link="https://example.com"
77+
thumbnail=""
78+
singleLinkContent={true}
79+
/>
80+
);
81+
82+
expect(getByTestId('title').querySelector('[data-testid="item-title"]')).toBeTruthy();
83+
expect(getByTestId('title').querySelector('[data-testid="item-title-link"]')).toBeFalsy();
84+
});
85+
86+
test('Renders ItemTitleLink when singleLinkContent is false', () => {
87+
const { getByTestId } = render(
88+
<HookConsumer
89+
title="Test Title"
90+
description="Test Description"
91+
link="https://example.com"
92+
thumbnail=""
93+
singleLinkContent={false}
94+
/>
95+
);
96+
97+
expect(getByTestId('title').querySelector('[data-testid="item-title"]')).toBeFalsy();
98+
expect(getByTestId('title').querySelector('[data-testid="item-title-link"]')).toBeTruthy();
99+
});
100+
101+
test('Renders with default link when singleLinkContent is not provided', () => {
102+
const { getByTestId } = render(
103+
<HookConsumer title="Test Title" description="Test Description" link="/media/test" thumbnail="" />
104+
);
105+
106+
// Default is false for singleLinkContent
107+
expect(getByTestId('title').querySelector('[data-testid="item-title-link"]')).toBeTruthy();
108+
});
109+
});
110+
111+
describe('descriptionComponent Rendering', () => {
112+
test('Renders single ItemDescription when hasMediaViewer is false', () => {
113+
const { getByTestId, queryAllByTestId } = render(
114+
<HookConsumer
115+
title="Test Title"
116+
description="My Description"
117+
link="https://example.com"
118+
thumbnail=""
119+
hasMediaViewer={false}
120+
/>
121+
);
122+
123+
const descriptions = queryAllByTestId('item-description');
124+
expect(descriptions.length).toBe(1);
125+
expect(descriptions[0].textContent).toBe('My Description');
126+
});
127+
128+
test('Renders single ItemDescription when hasMediaViewerDescr is false', () => {
129+
const { getByTestId, queryAllByTestId } = render(
130+
<HookConsumer
131+
title="Test Title"
132+
description="My Description"
133+
link="https://example.com"
134+
thumbnail=""
135+
hasMediaViewer={true}
136+
hasMediaViewerDescr={false}
137+
/>
138+
);
139+
140+
const descriptions = queryAllByTestId('item-description');
141+
expect(descriptions.length).toBe(1);
142+
expect(descriptions[0].textContent).toBe('My Description');
143+
});
144+
145+
test('Renders two ItemDescriptions when hasMediaViewer and hasMediaViewerDescr are both true', () => {
146+
const { queryAllByTestId } = render(
147+
<HookConsumer
148+
title="Test Title"
149+
description="Main Description"
150+
link="https://example.com"
151+
thumbnail=""
152+
hasMediaViewer={true}
153+
hasMediaViewerDescr={true}
154+
meta_description="Meta Description"
155+
/>
156+
);
157+
158+
const descriptions = queryAllByTestId('item-description');
159+
expect(descriptions.length).toBe(2);
160+
expect(descriptions[0].textContent).toBe('Meta Description');
161+
expect(descriptions[1].textContent).toBe('Main Description');
162+
});
163+
164+
test('Trims description text', () => {
165+
const { queryAllByTestId } = render(
166+
<HookConsumer
167+
title="Test Title"
168+
description=" Description with spaces "
169+
link="https://example.com"
170+
thumbnail=""
171+
/>
172+
);
173+
174+
expect(queryAllByTestId('item-description')[0].textContent).toBe('Description with spaces');
175+
});
176+
177+
test('Trims meta_description text', () => {
178+
const { queryAllByTestId } = render(
179+
<HookConsumer
180+
title="Test Title"
181+
description="Main Description"
182+
link="https://example.com"
183+
thumbnail=""
184+
hasMediaViewer={true}
185+
hasMediaViewerDescr={true}
186+
meta_description=" Meta with spaces "
187+
/>
188+
);
189+
190+
expect(queryAllByTestId('item-description')[0].textContent).toBe('Meta with spaces');
191+
});
192+
});
193+
194+
describe('thumbnailUrl', () => {
195+
test('Returns null when thumbnail is empty string', () => {
196+
const { getByTestId } = render(
197+
<HookConsumer
198+
title="Test Title"
199+
description="Test Description"
200+
link="https://example.com"
201+
thumbnail=""
202+
/>
203+
);
204+
205+
expect(getByTestId('thumbnail-url').textContent).toBe('null');
206+
});
207+
208+
test('Returns formatted URL when thumbnail has value', () => {
209+
const { getByTestId } = render(
210+
<HookConsumer
211+
title="Test Title"
212+
description="Test Description"
213+
link="https://example.com"
214+
thumbnail="/media/thumbnail.jpg"
215+
/>
216+
);
217+
218+
expect(getByTestId('thumbnail-url').textContent).toBe('https://example.com/media/thumbnail.jpg');
219+
});
220+
221+
test('Handles absolute URLs as thumbnails', () => {
222+
const { getByTestId } = render(
223+
<HookConsumer
224+
title="Test Title"
225+
description="Test Description"
226+
link="https://example.com"
227+
thumbnail="https://cdn.example.com/image.jpg"
228+
/>
229+
);
230+
231+
// formatInnerLink should preserve absolute URLs
232+
expect(getByTestId('thumbnail-url').textContent).toBe('https://cdn.example.com/image.jpg');
233+
});
234+
});
235+
236+
describe('UnderThumbWrapper', () => {
237+
test('Uses ItemMainInLink when singleLinkContent is true', () => {
238+
const { getByTestId } = render(
239+
<WrapperTest
240+
title="Test Title"
241+
description="Test Description"
242+
link="https://example.com"
243+
thumbnail=""
244+
singleLinkContent={true}
245+
/>
246+
);
247+
248+
// When singleLinkContent is true, UnderThumbWrapper should be ItemMainInLink
249+
expect(getByTestId('item-main-in-link')).toBeTruthy();
250+
expect(getByTestId('item-main-in-link').getAttribute('data-link')).toBe('https://example.com');
251+
expect(getByTestId('item-main-in-link').getAttribute('data-title')).toBe('Test Title');
252+
});
253+
254+
test('Uses ItemMain when singleLinkContent is false', () => {
255+
const { getByTestId } = render(
256+
<WrapperTest
257+
title="Test Title"
258+
description="Test Description"
259+
link="https://example.com"
260+
thumbnail=""
261+
singleLinkContent={false}
262+
/>
263+
);
264+
265+
// When singleLinkContent is false, UnderThumbWrapper should be ItemMain
266+
expect(getByTestId('item-main')).toBeTruthy();
267+
});
268+
269+
test('Uses ItemMain by default when singleLinkContent is not provided', () => {
270+
const { getByTestId } = render(
271+
<WrapperTest
272+
title="Test Title"
273+
description="Test Description"
274+
link="https://example.com"
275+
thumbnail=""
276+
/>
277+
);
278+
279+
// Default is singleLinkContent=false, so ItemMain
280+
expect(getByTestId('item-main')).toBeTruthy();
281+
});
282+
});
283+
284+
describe('onMount callback', () => {
285+
test('Calls onMount callback when component mounts', () => {
286+
const onMountCallback = jest.fn();
287+
288+
render(
289+
<HookConsumer
290+
title="Test Title"
291+
description="Test Description"
292+
link="https://example.com"
293+
thumbnail=""
294+
onMount={onMountCallback}
295+
/>
296+
);
297+
298+
expect(onMountCallback).toHaveBeenCalledTimes(1);
299+
});
300+
301+
test('Calls onMount only once on initial mount', () => {
302+
const onMountCallback = jest.fn();
303+
304+
const { rerender } = render(
305+
<HookConsumer
306+
title="Test Title"
307+
description="Test Description"
308+
link="https://example.com"
309+
thumbnail=""
310+
onMount={onMountCallback}
311+
/>
312+
);
313+
314+
expect(onMountCallback).toHaveBeenCalledTimes(1);
315+
316+
rerender(
317+
<HookConsumer
318+
title="Updated Title"
319+
description="Updated Description"
320+
link="https://example.com"
321+
thumbnail=""
322+
onMount={onMountCallback}
323+
/>
324+
);
325+
326+
// Should still be called only once (useEffect with empty dependency array)
327+
expect(onMountCallback).toHaveBeenCalledTimes(1);
328+
});
329+
});
330+
331+
describe('Integration tests', () => {
332+
test('Complete rendering with all props', () => {
333+
const onMount = jest.fn();
334+
const { getByTestId, queryAllByTestId } = render(
335+
<HookConsumer
336+
title="Complete Test"
337+
description="Complete Description"
338+
link="/media/complete"
339+
thumbnail="/img/thumb.jpg"
340+
type="media"
341+
hasMediaViewer={true}
342+
hasMediaViewerDescr={true}
343+
meta_description="Complete Meta"
344+
singleLinkContent={false}
345+
onMount={onMount}
346+
/>
347+
);
348+
349+
const descriptions = queryAllByTestId('item-description');
350+
expect(descriptions.length).toBe(2);
351+
expect(onMount).toHaveBeenCalledTimes(1);
352+
expect(getByTestId('thumbnail-url').textContent).toBe('https://example.com/img/thumb.jpg');
353+
});
354+
355+
test('Minimal props required', () => {
356+
const { getByTestId } = render(
357+
<HookConsumer title="Title" description="Description" link="/link" thumbnail="" />
358+
);
359+
360+
expect(getByTestId('title')).toBeTruthy();
361+
expect(getByTestId('description')).toBeTruthy();
362+
expect(getByTestId('thumbnail-url').textContent).toBe('null');
363+
});
364+
365+
test('Renders with special characters in title and description', () => {
366+
const { queryAllByTestId } = render(
367+
<HookConsumer
368+
title="Title with & < > special chars"
369+
description={`Description with 'quotes' and "double quotes"`}
370+
link="/media"
371+
thumbnail=""
372+
/>
373+
);
374+
375+
const descriptions = queryAllByTestId('item-description');
376+
expect(descriptions[0].textContent).toContain('Description with');
377+
});
378+
});
379+
});
380+
});

0 commit comments

Comments
 (0)