@eeacms/volto-bise-policy 1.3.11 → 1.3.13
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.
- package/CHANGELOG.md +37 -1
- package/package.json +2 -1
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyCard.jsx +404 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyCard.test.jsx +366 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyExplorerView.jsx +192 -91
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyExplorerView.test.jsx +205 -58
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMap.jsx +329 -178
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMap.test.jsx +243 -123
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMapLazy.jsx +61 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyMapLazy.test.jsx +89 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyResults.jsx +128 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyResults.test.jsx +160 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/FacetPopover.jsx +180 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/FacetPopover.test.jsx +125 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/FilterPanel.jsx +171 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/FilterPanel.test.jsx +181 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/Pagination.jsx +81 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/filters.js +350 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/filters.test.js +399 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/hooks.js +28 -4
- package/src/components/manage/Blocks/CaseStudyExplorer/hooks.test.js +73 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/messages.js +138 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/styles.less +1157 -317
- package/src/components/manage/Blocks/CaseStudyExplorer/testUtils.js +44 -46
- package/src/components/manage/Blocks/CaseStudyExplorer/utils.js +68 -184
- package/src/components/manage/Blocks/CaseStudyExplorer/utils.test.js +91 -158
- package/src/index.js +39 -0
- package/src/openlayers.js +51 -0
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyFilters.jsx +0 -383
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyFilters.test.jsx +0 -90
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyListing.jsx +0 -342
- package/src/components/manage/Blocks/CaseStudyExplorer/CaseStudyListing.test.jsx +0 -196
- package/src/components/manage/Blocks/CaseStudyExplorer/FeatureDisplay.jsx +0 -24
- package/src/components/manage/Blocks/CaseStudyExplorer/FeatureDisplay.test.jsx +0 -49
- package/src/components/manage/Blocks/CaseStudyExplorer/FeatureInteraction.jsx +0 -100
- package/src/components/manage/Blocks/CaseStudyExplorer/FeatureInteraction.test.jsx +0 -189
- package/src/components/manage/Blocks/CaseStudyExplorer/InfoOverlay.jsx +0 -111
- package/src/components/manage/Blocks/CaseStudyExplorer/InfoOverlay.test.jsx +0 -159
- package/src/components/manage/Blocks/CaseStudyExplorer/styles.less_old +0 -201
|
@@ -1,342 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import DOMPurify from 'dompurify';
|
|
3
|
-
import { withOpenLayers } from '@eeacms/volto-openlayers-map';
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
centerAndResetMapZoom,
|
|
7
|
-
escapeRegExp,
|
|
8
|
-
getSelectInteraction,
|
|
9
|
-
scrollToElement,
|
|
10
|
-
zoomMapToFeatures,
|
|
11
|
-
} from './utils';
|
|
12
|
-
|
|
13
|
-
const showPageNr = (pageNr, currentPage, numberOfPages) => {
|
|
14
|
-
// show first 5 pages
|
|
15
|
-
if (currentPage < 4 && pageNr <= 5) {
|
|
16
|
-
return true;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// show last 5 pages
|
|
20
|
-
if (numberOfPages - currentPage < 4 && numberOfPages - pageNr < 5) {
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (
|
|
25
|
-
currentPage >= 4 &&
|
|
26
|
-
numberOfPages - currentPage >= 4 &&
|
|
27
|
-
pageNr >= currentPage - 2 &&
|
|
28
|
-
pageNr <= currentPage + 2
|
|
29
|
-
) {
|
|
30
|
-
return true;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return false;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
function CaseStudyList(props) {
|
|
37
|
-
const { selectedCase, onSelectedCase, pointsSource, map, searchInput, ol } =
|
|
38
|
-
props;
|
|
39
|
-
const reSearch = searchInput
|
|
40
|
-
? new RegExp(`\\b(${escapeRegExp(searchInput)})\\b`, 'gi')
|
|
41
|
-
: null;
|
|
42
|
-
const [currentPage, setCurrentPage] = React.useState(1);
|
|
43
|
-
|
|
44
|
-
const features = pointsSource
|
|
45
|
-
.getFeatures(selectedCase)
|
|
46
|
-
.sort((item1, item2) =>
|
|
47
|
-
item1.values_.title.localeCompare(item2.values_.title),
|
|
48
|
-
);
|
|
49
|
-
const numberOfPages = Math.ceil(features.length / 10);
|
|
50
|
-
|
|
51
|
-
const displayFatures = features.slice(
|
|
52
|
-
10 * (currentPage - 1),
|
|
53
|
-
10 * currentPage,
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
return displayFatures.length === 0 ? (
|
|
57
|
-
<>
|
|
58
|
-
<h3 style={{ margin: 'calc(2rem - 0.1em) 0 1rem' }}>
|
|
59
|
-
We could not find any results for your search criteria
|
|
60
|
-
</h3>
|
|
61
|
-
<ul>
|
|
62
|
-
<li>check the selected filters</li>
|
|
63
|
-
</ul>
|
|
64
|
-
</>
|
|
65
|
-
) : (
|
|
66
|
-
<>
|
|
67
|
-
<div className="listing">
|
|
68
|
-
{selectedCase ? (
|
|
69
|
-
<div
|
|
70
|
-
className="content-box u-item listing-item result-item"
|
|
71
|
-
style={{
|
|
72
|
-
marginTop: '2em',
|
|
73
|
-
padding: 'em',
|
|
74
|
-
// border: '3px solid #f2f2f2',
|
|
75
|
-
// borderTop: '1em solid #f2f2f2',
|
|
76
|
-
paddingTop: 0,
|
|
77
|
-
backgroundColor: '#f2f2f2',
|
|
78
|
-
border: 'none',
|
|
79
|
-
}}
|
|
80
|
-
>
|
|
81
|
-
<div className="slot-top">
|
|
82
|
-
<div className="listing-body">
|
|
83
|
-
<h3 className="listing-header">
|
|
84
|
-
<a
|
|
85
|
-
target="_blank"
|
|
86
|
-
rel="noopener noreferrer"
|
|
87
|
-
href={selectedCase.path}
|
|
88
|
-
title={selectedCase.title}
|
|
89
|
-
datatest-id="selected-case"
|
|
90
|
-
>
|
|
91
|
-
{selectedCase.title}
|
|
92
|
-
</a>
|
|
93
|
-
</h3>
|
|
94
|
-
<p className="listing-description">
|
|
95
|
-
{selectedCase.description}
|
|
96
|
-
</p>
|
|
97
|
-
<div className="slot-bottom">
|
|
98
|
-
<div className="result-bottom">
|
|
99
|
-
{/* <div className="result-info">
|
|
100
|
-
<span className="result-info-title">Typology of measures:</span>
|
|
101
|
-
<span>
|
|
102
|
-
{selectedCase.typology_of_measures
|
|
103
|
-
? selectedCase.typology_of_measures.join(', ')
|
|
104
|
-
: ''}
|
|
105
|
-
</span>
|
|
106
|
-
</div> */}
|
|
107
|
-
{/* <div className="result-info">
|
|
108
|
-
<span className="result-info-title">
|
|
109
|
-
Measures implemented:
|
|
110
|
-
</span>
|
|
111
|
-
{selectedCase.measures_implemented.map((measure, index) => {
|
|
112
|
-
return (
|
|
113
|
-
<span>
|
|
114
|
-
<a
|
|
115
|
-
target="_blank"
|
|
116
|
-
rel="noopener noreferrer"
|
|
117
|
-
href={measure.path}
|
|
118
|
-
>
|
|
119
|
-
{measure.title}
|
|
120
|
-
{index !==
|
|
121
|
-
selectedCase.measures_implemented.length - 1
|
|
122
|
-
? ', '
|
|
123
|
-
: ''}
|
|
124
|
-
</a>
|
|
125
|
-
</span>
|
|
126
|
-
);
|
|
127
|
-
})}
|
|
128
|
-
</div> */}
|
|
129
|
-
<div
|
|
130
|
-
className="result-info show-on-map"
|
|
131
|
-
tabIndex="0"
|
|
132
|
-
role="button"
|
|
133
|
-
onKeyDown={() => {}}
|
|
134
|
-
onClick={() => {
|
|
135
|
-
// scroll to the map
|
|
136
|
-
scrollToElement('search-input');
|
|
137
|
-
// reset map zoom
|
|
138
|
-
onSelectedCase(null);
|
|
139
|
-
centerAndResetMapZoom({ map, ol });
|
|
140
|
-
getSelectInteraction(map)?.getFeatures().clear();
|
|
141
|
-
}}
|
|
142
|
-
>
|
|
143
|
-
<span
|
|
144
|
-
className="result-info-title"
|
|
145
|
-
data-testid="reset-map"
|
|
146
|
-
>
|
|
147
|
-
Reset map
|
|
148
|
-
</span>
|
|
149
|
-
<i className="icon ri-map-2-line"></i>
|
|
150
|
-
</div>
|
|
151
|
-
</div>
|
|
152
|
-
</div>
|
|
153
|
-
</div>
|
|
154
|
-
</div>
|
|
155
|
-
</div>
|
|
156
|
-
) : (
|
|
157
|
-
displayFatures.map((item, index) => {
|
|
158
|
-
return (
|
|
159
|
-
<div className="u-item listing-item result-item" key={index}>
|
|
160
|
-
<div className="slot-top">
|
|
161
|
-
<div className="listing-body">
|
|
162
|
-
<h3 className="listing-header">
|
|
163
|
-
<a
|
|
164
|
-
target="_blank"
|
|
165
|
-
rel="noopener noreferrer"
|
|
166
|
-
href={item.values_.path}
|
|
167
|
-
title={item.values_.title}
|
|
168
|
-
>
|
|
169
|
-
{item.values_.title}
|
|
170
|
-
</a>
|
|
171
|
-
</h3>
|
|
172
|
-
<p
|
|
173
|
-
className="listing-description"
|
|
174
|
-
dangerouslySetInnerHTML={{
|
|
175
|
-
// description is server data and may contain HTML;
|
|
176
|
-
// sanitize the highlighted result so no scripts or
|
|
177
|
-
// event handlers can reach the DOM
|
|
178
|
-
__html: DOMPurify.sanitize(
|
|
179
|
-
searchInput && reSearch && item.values_.description
|
|
180
|
-
? item.values_.description.replaceAll(
|
|
181
|
-
reSearch,
|
|
182
|
-
'<b>$1</b>',
|
|
183
|
-
)
|
|
184
|
-
: item.values_.description || '',
|
|
185
|
-
),
|
|
186
|
-
}}
|
|
187
|
-
></p>
|
|
188
|
-
<div className="slot-bottom">
|
|
189
|
-
<div className="result-bottom">
|
|
190
|
-
{/* <div className="result-info">
|
|
191
|
-
<span className="result-info-title">Typology of measures:</span>
|
|
192
|
-
<span>{item.values_.typology_of_measures.join(', ')}</span>
|
|
193
|
-
</div> */}
|
|
194
|
-
{/* <div className="result-info">
|
|
195
|
-
<span className="result-info-title">
|
|
196
|
-
Measures implemented:
|
|
197
|
-
</span>
|
|
198
|
-
|
|
199
|
-
{item.values_.measures_implemented.map(
|
|
200
|
-
(measure, index) => {
|
|
201
|
-
return (
|
|
202
|
-
<span key={index}>
|
|
203
|
-
<a
|
|
204
|
-
target="_blank"
|
|
205
|
-
rel="noopener noreferrer"
|
|
206
|
-
href={measure.path}
|
|
207
|
-
>
|
|
208
|
-
{measure.title}
|
|
209
|
-
{index !==
|
|
210
|
-
item.values_.measures_implemented.length - 1
|
|
211
|
-
? ', '
|
|
212
|
-
: ''}
|
|
213
|
-
</a>
|
|
214
|
-
</span>
|
|
215
|
-
);
|
|
216
|
-
},
|
|
217
|
-
)}
|
|
218
|
-
</div> */}
|
|
219
|
-
<div
|
|
220
|
-
className="result-info show-on-map"
|
|
221
|
-
tabIndex="0"
|
|
222
|
-
role="button"
|
|
223
|
-
onKeyDown={() => {}}
|
|
224
|
-
onClick={() => {
|
|
225
|
-
const selectInteraction = getSelectInteraction(map);
|
|
226
|
-
selectInteraction?.getFeatures().clear();
|
|
227
|
-
|
|
228
|
-
// scroll to the map
|
|
229
|
-
scrollToElement('ol-map-container');
|
|
230
|
-
|
|
231
|
-
zoomMapToFeatures({
|
|
232
|
-
map,
|
|
233
|
-
features: [item],
|
|
234
|
-
threshold: 5000,
|
|
235
|
-
ol,
|
|
236
|
-
});
|
|
237
|
-
onSelectedCase(item.values_);
|
|
238
|
-
|
|
239
|
-
setTimeout(() => {
|
|
240
|
-
const coords =
|
|
241
|
-
item.values_.geometry.flatCoordinates;
|
|
242
|
-
const pixel = map.getPixelFromCoordinate(coords);
|
|
243
|
-
const selectedFeature =
|
|
244
|
-
map.getFeaturesAtPixel(pixel)[0];
|
|
245
|
-
if (selectedFeature) {
|
|
246
|
-
selectInteraction
|
|
247
|
-
?.getFeatures()
|
|
248
|
-
.push(selectedFeature);
|
|
249
|
-
}
|
|
250
|
-
}, 1100);
|
|
251
|
-
}}
|
|
252
|
-
>
|
|
253
|
-
<span className="result-info-title">Show on map</span>
|
|
254
|
-
<i className="icon ri-road-map-line"></i>
|
|
255
|
-
</div>
|
|
256
|
-
</div>
|
|
257
|
-
</div>
|
|
258
|
-
</div>
|
|
259
|
-
</div>
|
|
260
|
-
</div>
|
|
261
|
-
);
|
|
262
|
-
})
|
|
263
|
-
)}
|
|
264
|
-
</div>
|
|
265
|
-
{!selectedCase ? (
|
|
266
|
-
<div className="search-body-footer">
|
|
267
|
-
<div className="ui centered grid">
|
|
268
|
-
<div className="center aligned column">
|
|
269
|
-
<div className="prev-next-paging">
|
|
270
|
-
<div className="paging-wrapper">
|
|
271
|
-
{currentPage !== 1 ? (
|
|
272
|
-
<button
|
|
273
|
-
className="ui button prev double-angle"
|
|
274
|
-
onClick={() => {
|
|
275
|
-
setCurrentPage(1);
|
|
276
|
-
}}
|
|
277
|
-
></button>
|
|
278
|
-
) : (
|
|
279
|
-
''
|
|
280
|
-
)}
|
|
281
|
-
{currentPage !== 1 ? (
|
|
282
|
-
<button
|
|
283
|
-
className="ui button prev single-angle"
|
|
284
|
-
onClick={() => {
|
|
285
|
-
setCurrentPage(currentPage - 1);
|
|
286
|
-
}}
|
|
287
|
-
></button>
|
|
288
|
-
) : (
|
|
289
|
-
''
|
|
290
|
-
)}
|
|
291
|
-
{Array.from(Array(numberOfPages).keys()).map((index) => {
|
|
292
|
-
const pageNr = index + 1;
|
|
293
|
-
return showPageNr(pageNr, currentPage, numberOfPages) ? (
|
|
294
|
-
<button
|
|
295
|
-
className={
|
|
296
|
-
'ui button pagination-item' +
|
|
297
|
-
(currentPage === pageNr ? ' active' : '')
|
|
298
|
-
}
|
|
299
|
-
onClick={() => {
|
|
300
|
-
setCurrentPage(pageNr);
|
|
301
|
-
}}
|
|
302
|
-
key={index}
|
|
303
|
-
>
|
|
304
|
-
{pageNr}
|
|
305
|
-
</button>
|
|
306
|
-
) : (
|
|
307
|
-
''
|
|
308
|
-
);
|
|
309
|
-
})}
|
|
310
|
-
{currentPage !== numberOfPages ? (
|
|
311
|
-
<button
|
|
312
|
-
className="ui button next single-angle"
|
|
313
|
-
onClick={() => {
|
|
314
|
-
setCurrentPage(currentPage + 1);
|
|
315
|
-
}}
|
|
316
|
-
></button>
|
|
317
|
-
) : (
|
|
318
|
-
''
|
|
319
|
-
)}
|
|
320
|
-
{currentPage !== numberOfPages ? (
|
|
321
|
-
<button
|
|
322
|
-
className="ui button next double-angle"
|
|
323
|
-
onClick={() => {
|
|
324
|
-
setCurrentPage(numberOfPages);
|
|
325
|
-
}}
|
|
326
|
-
></button>
|
|
327
|
-
) : (
|
|
328
|
-
''
|
|
329
|
-
)}{' '}
|
|
330
|
-
</div>
|
|
331
|
-
</div>
|
|
332
|
-
</div>
|
|
333
|
-
</div>
|
|
334
|
-
</div>
|
|
335
|
-
) : (
|
|
336
|
-
''
|
|
337
|
-
)}
|
|
338
|
-
</>
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
export default withOpenLayers(CaseStudyList);
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { render, fireEvent } from '@testing-library/react';
|
|
3
|
-
import '@testing-library/jest-dom';
|
|
4
|
-
import CaseStudyList from './CaseStudyListing';
|
|
5
|
-
import {
|
|
6
|
-
centerAndResetMapZoom,
|
|
7
|
-
scrollToElement,
|
|
8
|
-
zoomMapToFeatures,
|
|
9
|
-
} from './utils';
|
|
10
|
-
import { createMockMap, createMockPointsSource } from './testUtils';
|
|
11
|
-
|
|
12
|
-
jest.mock('@eeacms/volto-openlayers-map', () => ({
|
|
13
|
-
withOpenLayers: (Component) => Component,
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
jest.mock('./utils', () => ({
|
|
17
|
-
...jest.requireActual('./utils'),
|
|
18
|
-
centerAndResetMapZoom: jest.fn(),
|
|
19
|
-
scrollToElement: jest.fn(),
|
|
20
|
-
zoomMapToFeatures: jest.fn(),
|
|
21
|
-
}));
|
|
22
|
-
|
|
23
|
-
const mockMap = createMockMap();
|
|
24
|
-
const mockPointsSource = createMockPointsSource([
|
|
25
|
-
{
|
|
26
|
-
values_: {
|
|
27
|
-
title: 'Feature 1',
|
|
28
|
-
path: '/feature1',
|
|
29
|
-
description: 'Description 1',
|
|
30
|
-
typology_of_measures: ['SectorA'],
|
|
31
|
-
measures_implemented: [{ title: 'M1', path: '/m1' }],
|
|
32
|
-
geometry: { flatCoordinates: [0, 0] },
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
]);
|
|
36
|
-
|
|
37
|
-
const selectedCase = {
|
|
38
|
-
title: 'Selected Title',
|
|
39
|
-
path: '/selected',
|
|
40
|
-
description: 'Selected description',
|
|
41
|
-
typology_of_measures: ['S1', 'S2'],
|
|
42
|
-
measures_implemented: [{ title: 'M1', path: '/m1' }],
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
describe('CaseStudyList', () => {
|
|
46
|
-
beforeEach(() => {
|
|
47
|
-
jest.clearAllMocks();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('renders empty state when no features', () => {
|
|
51
|
-
const emptyPointsSource = { getFeatures: () => [] };
|
|
52
|
-
const { getByText } = render(
|
|
53
|
-
<CaseStudyList
|
|
54
|
-
selectedCase={null}
|
|
55
|
-
onSelectedCase={jest.fn()}
|
|
56
|
-
pointsSource={emptyPointsSource}
|
|
57
|
-
searchInput=""
|
|
58
|
-
ol={{}}
|
|
59
|
-
/>,
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
expect(
|
|
63
|
-
getByText('We could not find any results for your search criteria'),
|
|
64
|
-
).toBeInTheDocument();
|
|
65
|
-
expect(getByText('check the selected filters')).toBeInTheDocument();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('renders selectedCase details', () => {
|
|
69
|
-
const { getByText } = render(
|
|
70
|
-
<CaseStudyList
|
|
71
|
-
selectedCase={selectedCase}
|
|
72
|
-
onSelectedCase={jest.fn()}
|
|
73
|
-
pointsSource={mockPointsSource}
|
|
74
|
-
map={mockMap}
|
|
75
|
-
searchInput=""
|
|
76
|
-
ol={{}}
|
|
77
|
-
/>,
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
expect(getByText('Selected Title')).toBeInTheDocument();
|
|
81
|
-
expect(getByText('Selected description')).toBeInTheDocument();
|
|
82
|
-
// Note: Sectors and NWRMs sections are commented out in the component
|
|
83
|
-
// So we only check the title and description render
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('calls reset map utils when clicking Reset map', () => {
|
|
87
|
-
const { getByTestId } = render(
|
|
88
|
-
<CaseStudyList
|
|
89
|
-
selectedCase={selectedCase}
|
|
90
|
-
onSelectedCase={jest.fn()}
|
|
91
|
-
pointsSource={mockPointsSource}
|
|
92
|
-
map={mockMap}
|
|
93
|
-
searchInput=""
|
|
94
|
-
ol={{}}
|
|
95
|
-
/>,
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
fireEvent.click(getByTestId('reset-map'));
|
|
99
|
-
|
|
100
|
-
expect(scrollToElement).toHaveBeenCalledWith('search-input');
|
|
101
|
-
expect(centerAndResetMapZoom).toHaveBeenCalledWith({
|
|
102
|
-
map: mockMap,
|
|
103
|
-
ol: {},
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('renders features and highlights search term', () => {
|
|
108
|
-
const { getByText } = render(
|
|
109
|
-
<CaseStudyList
|
|
110
|
-
selectedCase={null}
|
|
111
|
-
onSelectedCase={jest.fn()}
|
|
112
|
-
pointsSource={mockPointsSource}
|
|
113
|
-
map={mockMap}
|
|
114
|
-
searchInput="match"
|
|
115
|
-
ol={{}}
|
|
116
|
-
/>,
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
expect(getByText('Feature 1')).toBeInTheDocument();
|
|
120
|
-
expect(getByText('Feature 1').getAttribute('href')).toBe('/feature1');
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('does not crash on regex metacharacters in search input', () => {
|
|
124
|
-
const { getByText } = render(
|
|
125
|
-
<CaseStudyList
|
|
126
|
-
selectedCase={null}
|
|
127
|
-
onSelectedCase={jest.fn()}
|
|
128
|
-
pointsSource={mockPointsSource}
|
|
129
|
-
map={mockMap}
|
|
130
|
-
searchInput="("
|
|
131
|
-
ol={{}}
|
|
132
|
-
/>,
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
// must not throw (previously: SyntaxError from invalid RegExp)
|
|
136
|
-
expect(getByText('Feature 1')).toBeInTheDocument();
|
|
137
|
-
expect(getByText('Description 1')).toBeInTheDocument();
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('sanitizes description HTML and keeps search highlights', () => {
|
|
141
|
-
const evilPointsSource = createMockPointsSource([
|
|
142
|
-
{
|
|
143
|
-
values_: {
|
|
144
|
-
title: 'Evil Case',
|
|
145
|
-
path: '/evil',
|
|
146
|
-
description:
|
|
147
|
-
'<img src=x onerror=alert(1)> <script>alert(2)</script> a river bed',
|
|
148
|
-
typology_of_measures: ['SectorA'],
|
|
149
|
-
measures_implemented: [{ title: 'M1', path: '/m1' }],
|
|
150
|
-
geometry: { flatCoordinates: [0, 0] },
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
]);
|
|
154
|
-
|
|
155
|
-
const { container } = render(
|
|
156
|
-
<CaseStudyList
|
|
157
|
-
selectedCase={null}
|
|
158
|
-
onSelectedCase={jest.fn()}
|
|
159
|
-
pointsSource={evilPointsSource}
|
|
160
|
-
map={mockMap}
|
|
161
|
-
searchInput="river"
|
|
162
|
-
ol={{}}
|
|
163
|
-
/>,
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
// XSS payloads must not survive into the DOM
|
|
167
|
-
expect(container.querySelector('img[onerror]')).toBeNull();
|
|
168
|
-
expect(container.querySelector('script')).toBeNull();
|
|
169
|
-
// harmless text still renders and the highlight survives sanitization
|
|
170
|
-
expect(container.textContent).toContain('a river bed');
|
|
171
|
-
const highlight = container.querySelector('b');
|
|
172
|
-
expect(highlight).not.toBeNull();
|
|
173
|
-
expect(highlight.textContent).toBe('river');
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('calls zoomMapToFeatures and onSelectedCase when clicking Show on map', async () => {
|
|
177
|
-
const onSelectedCase = jest.fn();
|
|
178
|
-
|
|
179
|
-
const { getByText } = render(
|
|
180
|
-
<CaseStudyList
|
|
181
|
-
selectedCase={null}
|
|
182
|
-
onSelectedCase={onSelectedCase}
|
|
183
|
-
pointsSource={mockPointsSource}
|
|
184
|
-
map={mockMap}
|
|
185
|
-
searchInput=""
|
|
186
|
-
ol={{}}
|
|
187
|
-
/>,
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
fireEvent.click(getByText('Show on map'));
|
|
191
|
-
|
|
192
|
-
expect(scrollToElement).toHaveBeenCalledWith('ol-map-container');
|
|
193
|
-
expect(zoomMapToFeatures).toHaveBeenCalled();
|
|
194
|
-
expect(onSelectedCase).toHaveBeenCalled();
|
|
195
|
-
});
|
|
196
|
-
});
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import Image from '@plone/volto/components/theme/Image/Image';
|
|
3
|
-
|
|
4
|
-
export default function FeatureDisplay({ feature }) {
|
|
5
|
-
return feature ? (
|
|
6
|
-
<div id="csepopup">
|
|
7
|
-
{feature.image ? (
|
|
8
|
-
<Image
|
|
9
|
-
className="csepopup-image"
|
|
10
|
-
src={feature.image}
|
|
11
|
-
alt={feature.title || ''}
|
|
12
|
-
/>
|
|
13
|
-
) : null}
|
|
14
|
-
<h3>
|
|
15
|
-
<strong>
|
|
16
|
-
<a target="_blank" rel="noopener noreferrer" href={feature.path}>
|
|
17
|
-
{feature.title}
|
|
18
|
-
<i class="ri-external-link-line"></i>
|
|
19
|
-
</a>
|
|
20
|
-
</strong>
|
|
21
|
-
</h3>
|
|
22
|
-
</div>
|
|
23
|
-
) : null;
|
|
24
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { render } from '@testing-library/react';
|
|
3
|
-
import '@testing-library/jest-dom';
|
|
4
|
-
|
|
5
|
-
import FeatureDisplay from './FeatureDisplay';
|
|
6
|
-
|
|
7
|
-
describe('FeatureDisplay Component', () => {
|
|
8
|
-
it('renders correctly with feature data', () => {
|
|
9
|
-
const feature = {
|
|
10
|
-
title: 'Sample Feature',
|
|
11
|
-
path: 'https://example.com/sample-feature',
|
|
12
|
-
image: 'https://example.com/preview.jpg',
|
|
13
|
-
measures_implemented: [
|
|
14
|
-
{ path: 'https://example.com/nwrm-1', title: 'NWRM 1' },
|
|
15
|
-
{ path: 'https://example.com/nwrm-2', title: 'NWRM 2' },
|
|
16
|
-
],
|
|
17
|
-
typology_of_measures: ['Sector 1', 'Sector 2'],
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const { getByRole } = render(<FeatureDisplay feature={feature} />);
|
|
21
|
-
|
|
22
|
-
const titleLink = getByRole('link', { name: 'Sample Feature' });
|
|
23
|
-
expect(titleLink).toHaveAttribute(
|
|
24
|
-
'href',
|
|
25
|
-
'https://example.com/sample-feature',
|
|
26
|
-
);
|
|
27
|
-
expect(getByRole('img')).toHaveAttribute(
|
|
28
|
-
'src',
|
|
29
|
-
'https://example.com/preview.jpg',
|
|
30
|
-
);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('does not render an image when the feature has none', () => {
|
|
34
|
-
const { queryByRole, getByRole } = render(
|
|
35
|
-
<FeatureDisplay feature={{ title: 'No image', path: '/no-image' }} />,
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
expect(queryByRole('img')).not.toBeInTheDocument();
|
|
39
|
-
expect(getByRole('link', { name: 'No image' })).toHaveAttribute(
|
|
40
|
-
'href',
|
|
41
|
-
'/no-image',
|
|
42
|
-
);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('renders nothing when feature is null', () => {
|
|
46
|
-
const { container } = render(<FeatureDisplay feature={null} />);
|
|
47
|
-
expect(container.firstChild).toBeNull();
|
|
48
|
-
});
|
|
49
|
-
});
|