@eeacms/volto-cca-policy 1.0.16 → 1.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/package.json +1 -1
  3. package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueCardItem.jsx +4 -3
  4. package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueCardItem.test.jsx +63 -0
  5. package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueContentView.jsx +1 -1
  6. package/src/components/Search/NavigatorCatalogue/utils.js +4 -7
  7. package/src/components/Search/NavigatorCatalogue/utils.test.js +11 -22
  8. package/src/components/Search/NavigatorGuide/NavigatorGuideContentView.jsx +7 -9
  9. package/src/components/Search/NavigatorGuide/NavigatorGuideContentView.test.jsx +94 -0
  10. package/src/components/theme/CompareTools/CompareToolsPanel.jsx +15 -13
  11. package/src/components/theme/CompareTools/CompareToolsPanel.test.jsx +74 -12
  12. package/src/components/theme/CompareTools/CompareToolsView.jsx +26 -31
  13. package/src/components/theme/CompareTools/CompareToolsView.test.jsx +47 -12
  14. package/src/components/theme/CompareTools/utils.js +19 -9
  15. package/src/components/theme/CompareTools/utils.test.jsx +21 -8
  16. package/src/components/theme/ToolThumbnail/ToolThumbnail.jsx +40 -0
  17. package/src/components/theme/ToolThumbnail/ToolThumbnail.test.jsx +29 -0
  18. package/src/components/theme/ToolThumbnail/utils.js +49 -0
  19. package/src/components/theme/ToolThumbnail/utils.test.js +124 -0
  20. package/src/components/theme/Views/ExtendedToolView.jsx +22 -0
  21. package/src/components/theme/Views/ExtendedToolView.test.jsx +93 -0
  22. package/src/components/theme/Views/RelatedTools.jsx +2 -6
  23. package/src/hocs/withClientOnly.jsx +13 -1
  24. package/src/hocs/withClientOnly.test.jsx +19 -0
  25. package/src/search/navigator_guide/config.js +0 -2
  26. package/theme/globals/navigator.less +9 -0
  27. package/theme/globals/views.less +24 -49
package/CHANGELOG.md CHANGED
@@ -4,6 +4,29 @@ All notable changes to this project will be documented in this file. Dates are d
4
4
 
5
5
  Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
6
 
7
+ ### [1.0.18](https://github.com/eea/volto-cca-policy/compare/1.0.17...1.0.18) - 10 September 2026
8
+
9
+ #### :rocket: New Features
10
+
11
+ - feat: implement ToolThumbnail component and integrate it across various views [kreafox - [`edd3087`](https://github.com/eea/volto-cca-policy/commit/edd3087830076efa968ef42d1ec99efa42010cd5)]
12
+
13
+ #### :hammer_and_wrench: Others
14
+
15
+ - test: update image display style assertions in tests for NavigatorCatalogueCardItem and ToolThumbnail components [kreafox - [`24e1437`](https://github.com/eea/volto-cca-policy/commit/24e14375674dca5e73d8919eb4578d1c184db21a)]
16
+ ### [1.0.17](https://github.com/eea/volto-cca-policy/compare/1.0.16...1.0.17) - 8 September 2026
17
+
18
+ #### :rocket: New Features
19
+
20
+ - feat(navigator): add back to search link on tool view; code refactoring & cleanup - refs #304608 [kreafox - [`b80edc4`](https://github.com/eea/volto-cca-policy/commit/b80edc476f073a4023a06759765cb1ed1da874eb)]
21
+ - feat: add MetadataTags component to display tool output types - refs #304612 [kreafox - [`da36a73`](https://github.com/eea/volto-cca-policy/commit/da36a7340da0d36a807b619e1da57d246b11b07d)]
22
+
23
+ #### :nail_care: Enhancements
24
+
25
+ - change: remove sectors from CompareToolsView - refs #304612 [kreafox - [`9b9352f`](https://github.com/eea/volto-cca-policy/commit/9b9352fa116a951ceef64511bc7c75779a7f0ec4)]
26
+
27
+ #### :hammer_and_wrench: Others
28
+
29
+ - test: remove sectors from CompareToolsView accessibility tests [kreafox - [`b3df4e2`](https://github.com/eea/volto-cca-policy/commit/b3df4e21aa79c6d9c726ad498fee2ce9af7f17a0)]
7
30
  ### [1.0.16](https://github.com/eea/volto-cca-policy/compare/1.0.15...1.0.16) - 4 September 2026
8
31
 
9
32
  ### [1.0.15](https://github.com/eea/volto-cca-policy/compare/1.0.14...1.0.15) - 4 September 2026
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-cca-policy",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "@eeacms/volto-cca-policy: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -3,6 +3,8 @@ import { Checkbox, Icon, Popup } from 'semantic-ui-react';
3
3
  import { defineMessages, useIntl } from 'react-intl';
4
4
  import ExternalLink from '@eeacms/search/components/Result/ExternalLink';
5
5
  import ResultContext from '@eeacms/search/components/Result/ResultContext';
6
+ import ToolThumbnail from '../../theme/ToolThumbnail/ToolThumbnail';
7
+ import { getToolThumbnailUrl } from '../../theme/ToolThumbnail/utils';
6
8
  import {
7
9
  getCompareToolTitle,
8
10
  getCompareToolUid,
@@ -136,6 +138,7 @@ const NavigatorCatalogueCardItem = (props) => {
136
138
  uid: getCompareToolUid(result),
137
139
  title: getCompareToolTitle(result),
138
140
  href: result.href,
141
+ image: getToolThumbnailUrl(result),
139
142
  };
140
143
  const { isSelected, isLimitReached, setSelected } =
141
144
  useCompareTools(compareTool);
@@ -146,9 +149,7 @@ const NavigatorCatalogueCardItem = (props) => {
146
149
 
147
150
  return (
148
151
  <div className={`navigator-catalogue-item${isSelected ? ' selected' : ''}`}>
149
- <div className="navigator-tool-icon large" aria-hidden="true">
150
- <Icon className="ri-file-line" />
151
- </div>
152
+ <ToolThumbnail result={result} size="large" />
152
153
 
153
154
  <div className="catalogue-item-main">
154
155
  <div className="catalogue-item-top">
@@ -76,6 +76,7 @@ describe('NavigatorCatalogueCardItem', () => {
76
76
  cca_uid: { raw: 'tool-uid' },
77
77
  title: 'Climate planning tool',
78
78
  href: 'https://example.com/tool',
79
+ image: '/uploaded-tool-thumb.jpg',
79
80
  publication_date: { raw: '2026-07-24' },
80
81
  cca_adaptation_sectors: {
81
82
  raw: ['Agriculture', 'Water', 'Health', 'Energy'],
@@ -138,6 +139,7 @@ describe('NavigatorCatalogueCardItem', () => {
138
139
  uid: 'tool-uid',
139
140
  title: 'Climate planning tool',
140
141
  href: 'https://example.com/tool',
142
+ image: '/uploaded-tool-thumb.jpg',
141
143
  });
142
144
  });
143
145
 
@@ -170,4 +172,65 @@ describe('NavigatorCatalogueCardItem', () => {
170
172
  expect(screen.getByText('Water')).toBeInTheDocument();
171
173
  expect(screen.queryByText('License:')).not.toBeInTheDocument();
172
174
  });
175
+
176
+ describe('thumbnail rendering and fallback', () => {
177
+ it('renders thumbnail image and smoothly transitions from placeholder on load', () => {
178
+ const { container } = renderCard({
179
+ title: 'Tool with image',
180
+ href: '/tools/my-tool',
181
+ image: { scales: { thumb: { download: '/uploaded-thumb.jpg' } } },
182
+ });
183
+
184
+ const img = container.querySelector('.navigator-tool-icon img');
185
+ expect(img).toBeInTheDocument();
186
+ expect(img).toHaveAttribute('src', '/uploaded-thumb.jpg');
187
+ expect(img.parentElement).toHaveClass('navigator-tool-icon', 'large');
188
+ expect(img).toHaveStyle({ display: 'none' });
189
+ expect(
190
+ container.querySelector('.navigator-tool-icon .ri-file-line'),
191
+ ).toBeInTheDocument();
192
+
193
+ // Fire load event on image
194
+ fireEvent.load(img);
195
+
196
+ expect(img.style.display).toBe('');
197
+ expect(
198
+ container.querySelector('.navigator-tool-icon .ri-file-line'),
199
+ ).not.toBeInTheDocument();
200
+ });
201
+
202
+ it('falls back to placeholder icon when image fails to load', () => {
203
+ const { container } = renderCard({
204
+ title: 'Tool with broken image',
205
+ href: '/tools/broken-tool',
206
+ });
207
+
208
+ const img = container.querySelector('.navigator-tool-icon img');
209
+ expect(img).toBeInTheDocument();
210
+
211
+ // Fire error event on image (e.g. 404 from backend)
212
+ fireEvent.error(img);
213
+
214
+ expect(
215
+ container.querySelector('.navigator-tool-icon img'),
216
+ ).not.toBeInTheDocument();
217
+ expect(
218
+ container.querySelector('.navigator-tool-icon .ri-file-line'),
219
+ ).toBeInTheDocument();
220
+ });
221
+
222
+ it('renders placeholder icon directly when item has no image or href', () => {
223
+ const { container } = renderCard({
224
+ title: 'Tool without image',
225
+ image: null,
226
+ });
227
+
228
+ expect(
229
+ container.querySelector('.navigator-tool-icon img'),
230
+ ).not.toBeInTheDocument();
231
+ expect(
232
+ container.querySelector('.navigator-tool-icon .ri-file-line'),
233
+ ).toBeInTheDocument();
234
+ });
235
+ });
173
236
  });
@@ -158,7 +158,7 @@ const NavigatorCatalogueContentView = (props) => {
158
158
  {children.length === 0 && !isLoading && wasSearched && <NoResults />}
159
159
 
160
160
  <ResultViewComponent>{children}</ResultViewComponent>
161
- <CompareToolsPanel appConfig={appConfig} />
161
+ <CompareToolsPanel />
162
162
 
163
163
  {children.length > 0 && activeNavigatorViewId !== 'map' && (
164
164
  <div className="search-body-footer">
@@ -97,19 +97,16 @@ export const downloadCsv = (filename, rows) => {
97
97
  URL.revokeObjectURL(url);
98
98
  };
99
99
 
100
- export const getLocalizedLandingPageURL = (appConfig, currentLang = 'en') => {
100
+ export const getNavigatorCataloguePageURL = (currentLang = 'en') => {
101
101
  const lang = currentLang || 'en';
102
- const landingPageURL =
103
- appConfig?.landingPageURL || '/en/navigator/tool-catalogue';
104
102
 
105
- return landingPageURL.replace(/^\/en(?=\/|$)/, `/${lang}`);
103
+ return `/${lang}/navigator/tool-catalogue`;
106
104
  };
107
105
 
108
- export const getComparePageURL = (appConfig, currentLang = 'en') => {
106
+ export const getComparePageURL = (currentLang = 'en') => {
109
107
  const lang = currentLang || 'en';
110
- const comparePageURL = appConfig?.comparePageURL || '/en/navigator/compare';
111
108
 
112
- return comparePageURL.replace(/^\/en(?=\/|$)/, `/${lang}`);
109
+ return `/${lang}/navigator/compare`;
113
110
  };
114
111
 
115
112
  export const exportComparisonTable = (tools, getToolField) => {
@@ -6,7 +6,7 @@ import {
6
6
  exportComparisonTable,
7
7
  formatFunctionalityScore,
8
8
  getComparePageURL,
9
- getLocalizedLandingPageURL,
9
+ getNavigatorCataloguePageURL,
10
10
  rawValueAsArray,
11
11
  } from './utils';
12
12
 
@@ -86,27 +86,16 @@ describe('Navigator Catalogue utilities', () => {
86
86
  expect(revokeObjectURL).toHaveBeenCalledWith('blob:test');
87
87
  });
88
88
 
89
- it('localizes catalogue and comparison URLs', () => {
90
- expect(getLocalizedLandingPageURL(undefined)).toBe(
91
- '/en/navigator/tool-catalogue',
92
- );
93
- expect(
94
- getLocalizedLandingPageURL({ landingPageURL: '/en/tools' }, 'fr'),
95
- ).toBe('/fr/tools');
96
- expect(getLocalizedLandingPageURL({ landingPageURL: '/tools' }, '')).toBe(
97
- '/tools',
98
- );
99
- expect(
100
- getComparePageURL({ comparePageURL: '/en/tools/compare' }, 'de'),
101
- ).toBe('/de/tools/compare');
102
- expect(
103
- getComparePageURL(
104
- { landingPageURL: '/en/navigator/tool-catalogue' },
105
- 'de',
106
- ),
107
- ).toBe('/de/navigator/compare');
108
- expect(getComparePageURL()).toBe('/en/navigator/compare');
109
- });
89
+ it.each([undefined, '', 'en', 'fr', 'de'])(
90
+ 'builds catalogue and comparison URLs for locale %s',
91
+ (locale) => {
92
+ const lang = locale || 'en';
93
+ expect(getNavigatorCataloguePageURL(locale)).toBe(
94
+ `/${lang}/navigator/tool-catalogue`,
95
+ );
96
+ expect(getComparePageURL(locale)).toBe(`/${lang}/navigator/compare`);
97
+ },
98
+ );
110
99
 
111
100
  it('exports every comparison criterion', () => {
112
101
  const tools = [
@@ -6,10 +6,12 @@ import { defineMessages, useIntl } from 'react-intl';
6
6
  import { Button, Checkbox, Icon, Loader, Message } from 'semantic-ui-react';
7
7
  import URLManager from '@elastic/search-ui/lib/cjs/URLManager';
8
8
  import { useSearchContext } from '@eeacms/search/lib/hocs';
9
+ import ToolThumbnail from '../../theme/ToolThumbnail/ToolThumbnail';
9
10
  import guideSteps from '../../../search/navigator_guide/guideSteps';
10
11
  import { navigatorGuideStepAtom } from '../../../state';
11
12
  import { mergeGuideOptions } from './utils';
12
13
  import useGuideFacetOptions from './useGuideFacetOptions';
14
+ import { getNavigatorCataloguePageURL } from '../../Search/NavigatorCatalogue/utils';
13
15
 
14
16
  const messages = defineMessages({
15
17
  noSteps: {
@@ -177,9 +179,7 @@ const NavigatorGuideContentView = ({ appConfig }) => {
177
179
  allowedFields.has(field),
178
180
  );
179
181
  const query = new URLManager().stateToUrl({ filters: resultFilters });
180
- const pathname = (
181
- appConfig.resultsPageURL || '/en/navigator/tool-catalogue'
182
- ).replace(/^\/en(?=\/|$)/, `/${currentLang}`);
182
+ const pathname = getNavigatorCataloguePageURL(currentLang);
183
183
 
184
184
  history.push({ pathname, search: query ? `?${query}` : '' });
185
185
  };
@@ -351,12 +351,10 @@ const NavigatorGuideContentView = ({ appConfig }) => {
351
351
  className="navigator-guide-preview-result"
352
352
  key={result._original?._id || result.href}
353
353
  >
354
- <div
355
- className="navigator-tool-icon medium"
356
- aria-hidden="true"
357
- >
358
- <Icon className="ri-stack-line" />
359
- </div>
354
+ <ToolThumbnail
355
+ result={result}
356
+ fallbackIcon="ri-stack-line"
357
+ />
360
358
  <div className="navigator-guide-preview-result-content">
361
359
  <small
362
360
  className="navigator-tool-provider"
@@ -0,0 +1,94 @@
1
+ import React from 'react';
2
+ import '@testing-library/jest-dom';
3
+ import { fireEvent, render } from '@testing-library/react';
4
+ import { IntlProvider } from 'react-intl';
5
+ import { useAtom } from 'jotai';
6
+ import { useSelector } from 'react-redux';
7
+ import { useHistory } from 'react-router-dom';
8
+ import { useSearchContext } from '@eeacms/search/lib/hocs';
9
+ import NavigatorGuideContentView from './NavigatorGuideContentView';
10
+ import useGuideFacetOptions from './useGuideFacetOptions';
11
+
12
+ jest.mock('jotai', () => ({
13
+ ...jest.requireActual('jotai'),
14
+ useAtom: jest.fn(),
15
+ }));
16
+ jest.mock('react-redux', () => ({ useSelector: jest.fn() }));
17
+ jest.mock('react-router-dom', () => ({ useHistory: jest.fn() }));
18
+ jest.mock('@eeacms/search/lib/hocs', () => ({ useSearchContext: jest.fn() }));
19
+ jest.mock('./useGuideFacetOptions', () => ({
20
+ __esModule: true,
21
+ default: jest.fn(),
22
+ }));
23
+
24
+ const renderGuide = (result) => {
25
+ useSearchContext.mockReturnValue({
26
+ filters: [{ field: 'cca_adaptation_sectors.keyword', values: ['Water'] }],
27
+ facets: {},
28
+ results: [result],
29
+ totalResults: 1,
30
+ isLoading: false,
31
+ });
32
+ return render(
33
+ <IntlProvider locale="en">
34
+ <NavigatorGuideContentView appConfig={{ previewResultsLimit: 3 }} />
35
+ </IntlProvider>,
36
+ );
37
+ };
38
+
39
+ describe('NavigatorGuideContentView thumbnails', () => {
40
+ beforeEach(() => {
41
+ jest.clearAllMocks();
42
+ useAtom.mockReturnValue([0, jest.fn()]);
43
+ useSelector.mockReturnValue('en');
44
+ useHistory.mockReturnValue({ push: jest.fn() });
45
+ useGuideFacetOptions.mockReturnValue({});
46
+ });
47
+
48
+ it('shows the uploaded result image after loading, with a stack icon while loading', () => {
49
+ const { container } = renderGuide({
50
+ title: 'Guide tool',
51
+ href: '/tools/guide-tool',
52
+ image: { scales: { thumb: { download: '/uploaded-guide-thumb.jpg' } } },
53
+ });
54
+ const thumbnail = container.querySelector('.navigator-tool-icon');
55
+ const img = thumbnail.querySelector('img');
56
+
57
+ expect(thumbnail).toHaveClass('medium');
58
+ expect(thumbnail).toHaveAttribute('aria-hidden', 'true');
59
+ expect(img).toHaveAttribute('src', '/uploaded-guide-thumb.jpg');
60
+ expect(img).toHaveStyle({ display: 'none' });
61
+ expect(thumbnail.querySelector('.ri-stack-line')).toBeInTheDocument();
62
+
63
+ fireEvent.load(img);
64
+
65
+ expect(img).not.toHaveStyle({ display: 'none' });
66
+ expect(thumbnail.querySelector('.ri-stack-line')).not.toBeInTheDocument();
67
+ });
68
+
69
+ it('falls back to the stack icon when the image fails', () => {
70
+ const { container } = renderGuide({
71
+ title: 'Guide tool',
72
+ href: '/tools/guide-tool',
73
+ image: '/broken.jpg',
74
+ });
75
+ const thumbnail = container.querySelector('.navigator-tool-icon');
76
+
77
+ fireEvent.error(thumbnail.querySelector('img'));
78
+
79
+ expect(thumbnail.querySelector('img')).not.toBeInTheDocument();
80
+ expect(thumbnail.querySelector('.ri-stack-line')).toBeInTheDocument();
81
+ });
82
+
83
+ it.each([null, false])('shows the stack icon for image: %s', (image) => {
84
+ const { container } = renderGuide({
85
+ title: 'Guide tool',
86
+ href: '/tools/guide-tool',
87
+ image,
88
+ });
89
+ const thumbnail = container.querySelector('.navigator-tool-icon');
90
+
91
+ expect(thumbnail.querySelector('img')).not.toBeInTheDocument();
92
+ expect(thumbnail.querySelector('.ri-stack-line')).toBeInTheDocument();
93
+ });
94
+ });
@@ -4,10 +4,12 @@ import { useSelector } from 'react-redux';
4
4
  import { useHistory } from 'react-router-dom';
5
5
  import { Icon, Button } from 'semantic-ui-react';
6
6
  import { defineMessages, useIntl } from 'react-intl';
7
+ import ToolThumbnail from '../ToolThumbnail/ToolThumbnail';
7
8
  import {
8
9
  MAX_COMPARE_TOOLS,
9
10
  compareToolsAtom,
10
11
  getCompareLocation,
12
+ useHasMounted,
11
13
  } from './utils';
12
14
 
13
15
  const messages = defineMessages({
@@ -41,19 +43,21 @@ const messages = defineMessages({
41
43
  },
42
44
  });
43
45
 
44
- export const CompareToolsPanel = ({ appConfig: suppliedAppConfig }) => {
46
+ export const CompareToolsPanel = () => {
45
47
  const intl = useIntl();
46
48
  const history = useHistory();
47
49
  const currentLang = useSelector((state) => state.intl.locale);
48
- const appConfig = suppliedAppConfig || {
49
- landingPageURL: '/en/navigator/tool-catalogue',
50
- };
51
50
  const [selectedTools, setSelectedTools] = useAtom(compareToolsAtom);
52
- const resolvableSelectedTools = selectedTools.filter((tool) => tool.uid);
51
+ const hasMounted = useHasMounted();
52
+
53
+ const effectiveSelectedTools = hasMounted ? selectedTools : [];
54
+ const resolvableSelectedTools = effectiveSelectedTools.filter(
55
+ (tool) => tool.uid,
56
+ );
53
57
  const isReadyToCompare = resolvableSelectedTools.length >= 2;
54
- const emptySlots = MAX_COMPARE_TOOLS - selectedTools.length;
58
+ const emptySlots = MAX_COMPARE_TOOLS - effectiveSelectedTools.length;
55
59
 
56
- if (selectedTools.length === 0) return null;
60
+ if (!hasMounted || effectiveSelectedTools.length === 0) return null;
57
61
 
58
62
  const removeTool = (toolUid) => {
59
63
  setSelectedTools((tools) => tools.filter((tool) => tool.uid !== toolUid));
@@ -67,7 +71,7 @@ export const CompareToolsPanel = ({ appConfig: suppliedAppConfig }) => {
67
71
  ].join('');
68
72
 
69
73
  history.push(
70
- getCompareLocation(selectedTools, appConfig, returnURL, currentLang),
74
+ getCompareLocation(effectiveSelectedTools, returnURL, currentLang),
71
75
  );
72
76
  };
73
77
 
@@ -77,18 +81,16 @@ export const CompareToolsPanel = ({ appConfig: suppliedAppConfig }) => {
77
81
  <h2>
78
82
  {intl.formatMessage(messages.compareTools)}
79
83
  <span className="compare-panel-count">
80
- {selectedTools.length}/{MAX_COMPARE_TOOLS}
84
+ {effectiveSelectedTools.length}/{MAX_COMPARE_TOOLS}
81
85
  </span>
82
86
  </h2>
83
87
  </div>
84
88
 
85
89
  <div className="compare-panel-content">
86
90
  <div className="compare-panel-tools">
87
- {selectedTools.map((tool) => (
91
+ {effectiveSelectedTools.map((tool) => (
88
92
  <div key={tool.uid} className="compare-panel-tool">
89
- <span className="navigator-tool-icon small" aria-hidden="true">
90
- <Icon className="ri-file-line" />
91
- </span>
93
+ <ToolThumbnail result={tool} size="small" />
92
94
  <span className="compare-panel-tool-title">{tool.title}</span>
93
95
  <Button
94
96
  className="compare-panel-tool-clear"
@@ -3,12 +3,13 @@ import '@testing-library/jest-dom';
3
3
  import { fireEvent, render, screen } from '@testing-library/react';
4
4
  import { IntlProvider } from 'react-intl';
5
5
  import { Provider as JotaiProvider } from 'jotai';
6
+ import { useSelector } from 'react-redux';
6
7
  import { useHistory } from 'react-router-dom';
7
8
  import { CompareToolsPanel } from './CompareToolsPanel';
8
9
 
9
10
  jest.mock('react-redux', () => ({
10
11
  ...jest.requireActual('react-redux'),
11
- useSelector: (selector) => selector({ intl: { locale: 'en' } }),
12
+ useSelector: jest.fn(),
12
13
  }));
13
14
 
14
15
  jest.mock('react-router-dom', () => ({
@@ -16,7 +17,7 @@ jest.mock('react-router-dom', () => ({
16
17
  useHistory: jest.fn(),
17
18
  }));
18
19
 
19
- const renderPanel = (selectedTools, appConfig) => {
20
+ const renderPanel = (selectedTools) => {
20
21
  window.localStorage.setItem(
21
22
  'cca-compare-tools',
22
23
  JSON.stringify(selectedTools),
@@ -25,7 +26,7 @@ const renderPanel = (selectedTools, appConfig) => {
25
26
  return render(
26
27
  <IntlProvider locale="en">
27
28
  <JotaiProvider>
28
- <CompareToolsPanel appConfig={appConfig} />
29
+ <CompareToolsPanel />
29
30
  </JotaiProvider>
30
31
  </IntlProvider>,
31
32
  );
@@ -38,6 +39,8 @@ describe('CompareToolsPanel', () => {
38
39
  jest.clearAllMocks();
39
40
  window.localStorage.clear();
40
41
  useHistory.mockReturnValue({ push });
42
+ useSelector.mockReturnValue('en');
43
+ window.history.replaceState({}, '', '/');
41
44
  });
42
45
 
43
46
  it('does not render without selected tools', () => {
@@ -61,14 +64,17 @@ describe('CompareToolsPanel', () => {
61
64
  expect(screen.queryByText('Compare tools')).not.toBeInTheDocument();
62
65
  });
63
66
 
64
- it('shows the ready state and opens the comparison page', () => {
65
- renderPanel(
66
- [
67
- { uid: 'one', title: 'Tool one' },
68
- { uid: 'two', title: 'Tool two' },
69
- ],
70
- { landingPageURL: '/en/navigator/tool-catalogue' },
67
+ it.each(['en', 'fr'])('opens the comparison page in %s', (locale) => {
68
+ useSelector.mockReturnValue(locale);
69
+ window.history.replaceState(
70
+ {},
71
+ '',
72
+ `/${locale}/navigator/tool-catalogue?q=water#results`,
71
73
  );
74
+ renderPanel([
75
+ { uid: 'one', title: 'Tool one' },
76
+ { uid: 'two', title: 'Tool two' },
77
+ ]);
72
78
 
73
79
  expect(screen.getByText('Ready to compare')).toHaveClass(
74
80
  'compare-panel-status',
@@ -78,9 +84,11 @@ describe('CompareToolsPanel', () => {
78
84
  fireEvent.click(screen.getByText('Compare selected tools'));
79
85
 
80
86
  expect(push).toHaveBeenCalledWith({
81
- pathname: '/en/navigator/compare',
87
+ pathname: `/${locale}/navigator/compare`,
82
88
  search: '?uid=one&uid=two',
83
- state: { returnURL: '/' },
89
+ state: {
90
+ returnURL: `/${locale}/navigator/tool-catalogue?q=water#results`,
91
+ },
84
92
  });
85
93
  });
86
94
 
@@ -93,4 +101,58 @@ describe('CompareToolsPanel', () => {
93
101
  fireEvent.click(screen.getByText('Clear all'));
94
102
  expect(screen.queryByText('Compare tools')).not.toBeInTheDocument();
95
103
  });
104
+
105
+ it('shows the selected tool thumbnail after loading and preserves empty slot icons', () => {
106
+ const { container } = renderPanel([
107
+ {
108
+ uid: 'one',
109
+ title: 'Tool one',
110
+ href: '/tools/one',
111
+ image: '/uploaded-tool-thumb.jpg',
112
+ },
113
+ ]);
114
+ const thumbnail = container.querySelector(
115
+ '.compare-panel-tool:not(.placeholder) .navigator-tool-icon',
116
+ );
117
+ const img = thumbnail.querySelector('img');
118
+
119
+ expect(thumbnail).toHaveClass('small');
120
+ expect(img).toHaveAttribute('src', '/uploaded-tool-thumb.jpg');
121
+ expect(img).toHaveStyle({ display: 'none' });
122
+ expect(thumbnail.querySelector('.ri-file-line')).toBeInTheDocument();
123
+ expect(
124
+ container.querySelectorAll('.placeholder .ri-add-line'),
125
+ ).toHaveLength(3);
126
+ expect(container.querySelector('.placeholder img')).not.toBeInTheDocument();
127
+
128
+ fireEvent.load(img);
129
+
130
+ expect(img).not.toHaveStyle({ display: 'none' });
131
+ expect(thumbnail.querySelector('.ri-file-line')).not.toBeInTheDocument();
132
+ });
133
+
134
+ it('uses the URL of an older saved selection and falls back on image error', () => {
135
+ const { container } = renderPanel([
136
+ { uid: 'one', title: 'Tool one', href: '/tools/one' },
137
+ ]);
138
+ const thumbnail = container.querySelector('.navigator-tool-icon');
139
+ const img = thumbnail.querySelector('img');
140
+
141
+ expect(img).toHaveAttribute('src', '/tools/one/@@images/image/thumb');
142
+
143
+ fireEvent.error(img);
144
+
145
+ expect(thumbnail.querySelector('img')).not.toBeInTheDocument();
146
+ expect(thumbnail.querySelector('.ri-file-line')).toBeInTheDocument();
147
+ });
148
+
149
+ it.each([null, false])('shows the file icon for image: %s', (image) => {
150
+ const { container } = renderPanel([
151
+ { uid: 'one', title: 'Tool one', href: '/tools/one', image },
152
+ ]);
153
+ const thumbnail = container.querySelector('.navigator-tool-icon');
154
+
155
+ expect(thumbnail.querySelector('img')).not.toBeInTheDocument();
156
+ expect(thumbnail.querySelector('.ri-file-line')).toBeInTheDocument();
157
+ });
96
158
  });