@eeacms/volto-marine-policy 3.0.2 → 3.0.3

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 CHANGED
@@ -4,6 +4,17 @@ 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
+ ### [3.0.3](https://github.com/eea/volto-marine-policy/compare/3.0.2...3.0.3) - 11 June 2026
8
+
9
+ #### :rocket: New Features
10
+
11
+ - feat: added button to show NIS duplicates [laszlocseh - [`591927c`](https://github.com/eea/volto-marine-policy/commit/591927ca76c693d8d33d7b9ab8bdedc10368c0f2)]
12
+ - feat: added button to copy a NIS listing item [laszlocseh - [`588a5fe`](https://github.com/eea/volto-marine-policy/commit/588a5fe1409292a5df742590d615e230dc09ebd9)]
13
+
14
+ #### :hammer_and_wrench: Others
15
+
16
+ - test: fix NISListingView.test.jsx [laszlocseh - [`0ba486d`](https://github.com/eea/volto-marine-policy/commit/0ba486d1058bc71fe73ef53e628fd7c5423688ae)]
17
+ - test: added NISListingView.test.jsx [laszlocseh - [`4c54331`](https://github.com/eea/volto-marine-policy/commit/4c54331dd40f178dfd88dcc0d73d1eb96d2834e6)]
7
18
  ### [3.0.2](https://github.com/eea/volto-marine-policy/compare/3.0.1...3.0.2) - 10 June 2026
8
19
 
9
20
  #### :rocket: Dependency updates
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eeacms/volto-marine-policy",
3
- "version": "3.0.2",
3
+ "version": "3.0.3",
4
4
  "description": "@eeacms/volto-marine-policy: Volto add-on",
5
5
  "main": "src/index.js",
6
6
  "author": "European Environment Agency: IDM2 A-Team",
@@ -78,6 +78,9 @@ const NISListingView = ({ items, isEditMode }) => {
78
78
  const [isLoading, setIsLoading] = useState(false);
79
79
  const [selectedItems, setSelectedItems] = useState([]);
80
80
  const [itemsTotal, setItemsTotal] = useState(0);
81
+ const [duplicateIds, setDuplicateIds] = useState(null);
82
+ const [duplicateGroups, setDuplicateGroups] = useState([]);
83
+
81
84
  const [users, setUsers] = useState([]);
82
85
  const [assignee, setAssignee] = useState(null);
83
86
  const actions = useSelector((state) => state.actions.actions);
@@ -128,6 +131,43 @@ const NISListingView = ({ items, isEditMode }) => {
128
131
  window.location.reload();
129
132
  };
130
133
 
134
+ const handleCopy = async (item) => {
135
+ setIsLoading(true);
136
+ try {
137
+ const res = await fetch(
138
+ `${window.env.apiPath}${item['@id']}/@copy-nis-record`,
139
+ {
140
+ method: 'POST',
141
+ headers: {
142
+ 'Content-Type': 'application/json',
143
+ Accept: 'application/json',
144
+ },
145
+ credentials: 'include',
146
+ },
147
+ );
148
+ if (res.ok) {
149
+ window.location.reload();
150
+ }
151
+ } catch (err) {
152
+ // eslint-disable-next-line no-console
153
+ console.error('Copy failed:', err);
154
+ setIsLoading(false);
155
+ }
156
+ };
157
+
158
+ useEffect(() => {
159
+ const parsed = qs.parse(window.location.search);
160
+ if (parsed['check-duplicates']) {
161
+ const containerPath = window.location.pathname.replace('/marine', '');
162
+ fetch(`${window.env.apiPath}${containerPath}/@check-nis-duplicates`)
163
+ .then((res) => res.json())
164
+ .then((data) => {
165
+ setDuplicateIds(new Set(data.duplicate_ids));
166
+ setDuplicateGroups(data.groups || []);
167
+ });
168
+ }
169
+ }, []);
170
+
131
171
  useEffect(() => {
132
172
  const fetchUsers = async () => {
133
173
  const res = await fetch(
@@ -183,9 +223,45 @@ const NISListingView = ({ items, isEditMode }) => {
183
223
  <i className="ri-file-download-line"></i>
184
224
  Download search results
185
225
  </a>
226
+ <Button
227
+ className="primary"
228
+ size="small"
229
+ onClick={() => {
230
+ const parsed = qs.parse(window.location.search);
231
+ parsed['check-duplicates'] = '1';
232
+ window.location.search = qs.stringify(parsed);
233
+ }}
234
+ >
235
+ Check duplicates
236
+ </Button>
186
237
  </div>
187
238
  </div>
188
239
  )}
240
+ {duplicateIds && (
241
+ <div
242
+ style={{
243
+ background: '#fff3cd',
244
+ border: '1px solid #ffc107',
245
+ padding: '10px 15px',
246
+ marginBottom: '15px',
247
+ borderRadius: '4px',
248
+ }}
249
+ >
250
+ Showing {items.filter((i) => duplicateIds.has(i['@id'])).length}{' '}
251
+ duplicate records across {duplicateGroups.length} groups
252
+ <a
253
+ href={(() => {
254
+ const p = qs.parse(window.location.search);
255
+ delete p['check-duplicates'];
256
+ const q = qs.stringify(p);
257
+ return `${window.location.pathname}${q ? '?' + q : ''}`;
258
+ })()}
259
+ style={{ marginLeft: '10px', fontSize: '0.9em' }}
260
+ >
261
+ Clear
262
+ </a>
263
+ </div>
264
+ )}
189
265
  <table className="ui table">
190
266
  <thead>
191
267
  <tr>
@@ -197,12 +273,16 @@ const NISListingView = ({ items, isEditMode }) => {
197
273
  <th>Country</th>
198
274
  <th>Status</th>
199
275
  <th>Group</th>
276
+ <th>Year</th>
200
277
  <th>Assigned to</th>
201
278
  <th></th>
202
279
  </tr>
203
280
  </thead>
204
281
  <tbody>
205
- {items.map((item, index) => (
282
+ {(duplicateIds
283
+ ? items.filter((item) => duplicateIds.has(item['@id']))
284
+ : items
285
+ ).map((item, index) => (
206
286
  <tr key={item['@id']}>
207
287
  <td>{item.nis_species_name_original}</td>
208
288
  <td>{item.nis_species_name_accepted}</td>
@@ -212,6 +292,7 @@ const NISListingView = ({ items, isEditMode }) => {
212
292
  <td>{item.nis_country}</td>
213
293
  <td>{item.nis_status}</td>
214
294
  <td>{item.nis_group}</td>
295
+ <td>{item.nis_year}</td>
215
296
  <td>
216
297
  <div className="assigned-to-container">
217
298
  <div>{formatAssignedTo(item.nis_assigned_to)}</div>
@@ -229,15 +310,27 @@ const NISListingView = ({ items, isEditMode }) => {
229
310
  <UniversalLink
230
311
  className="ui button secondary mini"
231
312
  href={`${item['@id']}`}
313
+ target="_blank"
314
+ rel="noopener noreferrer"
232
315
  >
233
316
  View
234
317
  </UniversalLink>
235
318
  <UniversalLink
236
319
  className="ui button primary mini"
237
320
  href={`${item['@id']}/edit`}
321
+ target="_blank"
322
+ rel="noopener noreferrer"
238
323
  >
239
324
  Edit
240
325
  </UniversalLink>
326
+ {canEditPage && (
327
+ <Button
328
+ className="tertiary mini"
329
+ onClick={() => handleCopy(item)}
330
+ >
331
+ Copy
332
+ </Button>
333
+ )}
241
334
  </div>
242
335
  <div className="workflow-progress">
243
336
  <ProgressWorkflow
@@ -0,0 +1,444 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import configureStore from 'redux-mock-store';
5
+ import { Provider } from 'react-redux';
6
+ import NISListingView from './NISListingView';
7
+
8
+ const mockUseSelector = jest.fn();
9
+
10
+ jest.mock('react-redux', () => ({
11
+ __esModule: true,
12
+ ...jest.requireActual('react-redux'),
13
+ useSelector: (selector) => mockUseSelector(selector),
14
+ }));
15
+
16
+ jest.mock(
17
+ '@plone/volto/components/manage/UniversalLink/UniversalLink',
18
+ () =>
19
+ function MockUniversalLink({ href, children, className }) {
20
+ return (
21
+ <a href={href} className={className}>
22
+ {children}
23
+ </a>
24
+ );
25
+ },
26
+ );
27
+
28
+ jest.mock(
29
+ '@eeacms/volto-marine-policy/components/theme/ProgressWorkflow/ProgressWorkflow',
30
+ () => ({
31
+ __esModule: true,
32
+ default: () => <div data-testid="progress-workflow" />,
33
+ }),
34
+ );
35
+
36
+ const mockStore = configureStore();
37
+
38
+ function buildItems(count = 3) {
39
+ return Array.from({ length: count }, (_, i) => ({
40
+ '@id': `/marine/item-${i + 1}`,
41
+ nis_species_name_original: `Species ${i + 1}`,
42
+ nis_species_name_accepted: `Species ${i + 1} accepted`,
43
+ nis_scientificname_accepted: `Scientificus acceptus ${i + 1}`,
44
+ nis_region: 'Europe',
45
+ nis_subregion: 'Western Europe',
46
+ nis_country: 'France',
47
+ nis_status: 'Established',
48
+ nis_group: 'Fish',
49
+ nis_year: 2020 + i,
50
+ nis_assigned_to: `User ${i + 1} (user${i + 1})`,
51
+ }));
52
+ }
53
+
54
+ function renderComponent(props = {}) {
55
+ const { items = buildItems(), ...rest } = props;
56
+ const store = mockStore({
57
+ intl: { locale: 'en', messages: {} },
58
+ });
59
+ return render(
60
+ <Provider store={store}>
61
+ <NISListingView items={items} {...rest} />
62
+ </Provider>,
63
+ );
64
+ }
65
+
66
+ describe('NISListingView', () => {
67
+ beforeEach(() => {
68
+ jest.clearAllMocks();
69
+
70
+ global.fetch = jest.fn(() =>
71
+ Promise.resolve({
72
+ json: () => Promise.resolve({ items: [] }),
73
+ ok: true,
74
+ }),
75
+ );
76
+
77
+ delete window.location;
78
+ window.location = {
79
+ href: 'http://localhost:3000/marine/test-path',
80
+ pathname: '/marine/test-path',
81
+ search: '',
82
+ reload: jest.fn(),
83
+ };
84
+
85
+ window.env = { apiPath: 'http://localhost:8080/Plone' };
86
+
87
+ global.fetch.mockImplementation((url) => {
88
+ if (typeof url === 'string' && url.includes('nis_experts_vocabulary')) {
89
+ return Promise.resolve({
90
+ json: () =>
91
+ Promise.resolve({
92
+ items: [
93
+ { token: 'jdoe', title: 'John Doe (jdoe)' },
94
+ { token: 'asmith', title: 'Alice Smith (asmith)' },
95
+ ],
96
+ }),
97
+ });
98
+ }
99
+ if (typeof url === 'string' && url.includes('check-nis-duplicates')) {
100
+ return Promise.resolve({
101
+ json: () =>
102
+ Promise.resolve({
103
+ duplicate_ids: [],
104
+ groups: [],
105
+ }),
106
+ });
107
+ }
108
+ return Promise.resolve({
109
+ json: () => Promise.resolve({}),
110
+ ok: true,
111
+ });
112
+ });
113
+
114
+ mockUseSelector.mockReturnValue({
115
+ object: [{ id: 'edit' }],
116
+ });
117
+ });
118
+
119
+ describe('table rendering', () => {
120
+ it('renders all column headers', async () => {
121
+ renderComponent();
122
+ await waitFor(() => {
123
+ expect(screen.getByText('Species name original')).toBeInTheDocument();
124
+ });
125
+ expect(screen.getByText('Species name accepted')).toBeInTheDocument();
126
+ expect(screen.getByText('Scientific name accepted')).toBeInTheDocument();
127
+ expect(screen.getByText('Region')).toBeInTheDocument();
128
+ expect(screen.getByText('Subregion')).toBeInTheDocument();
129
+ expect(screen.getByText('Country')).toBeInTheDocument();
130
+ expect(screen.getByText('Status')).toBeInTheDocument();
131
+ expect(screen.getByText('Group')).toBeInTheDocument();
132
+ expect(screen.getByText('Year')).toBeInTheDocument();
133
+ expect(screen.getByText('Assigned to')).toBeInTheDocument();
134
+ });
135
+
136
+ it('renders one row per item', async () => {
137
+ renderComponent({ items: buildItems(3) });
138
+ await waitFor(() => {
139
+ expect(screen.getByText('Species 1')).toBeInTheDocument();
140
+ });
141
+ const rows = screen.getAllByRole('row');
142
+ expect(rows).toHaveLength(4);
143
+ });
144
+
145
+ it('renders item data in correct columns', async () => {
146
+ const items = [
147
+ {
148
+ '@id': '/marine/item-x',
149
+ nis_species_name_original: 'Alienus maximus original',
150
+ nis_species_name_accepted: 'Alienus maximus (Linnaeus)',
151
+ nis_scientificname_accepted: 'Alienus maximus scientific',
152
+ nis_region: 'Asia',
153
+ nis_subregion: 'South Asia',
154
+ nis_country: 'India',
155
+ nis_status: 'Invasive',
156
+ nis_group: 'Plant',
157
+ nis_year: 2023,
158
+ nis_assigned_to: 'Jane Expert (jexpert)',
159
+ },
160
+ ];
161
+ renderComponent({ items });
162
+ await waitFor(() => {
163
+ expect(
164
+ screen.getByText('Alienus maximus original'),
165
+ ).toBeInTheDocument();
166
+ });
167
+ expect(
168
+ screen.getByText('Alienus maximus (Linnaeus)'),
169
+ ).toBeInTheDocument();
170
+ expect(
171
+ screen.getByText('Alienus maximus scientific'),
172
+ ).toBeInTheDocument();
173
+ expect(screen.getByText('Asia')).toBeInTheDocument();
174
+ expect(screen.getByText('South Asia')).toBeInTheDocument();
175
+ expect(screen.getByText('India')).toBeInTheDocument();
176
+ expect(screen.getByText('Invasive')).toBeInTheDocument();
177
+ expect(screen.getByText('Plant')).toBeInTheDocument();
178
+ expect(screen.getByText('2023')).toBeInTheDocument();
179
+ });
180
+
181
+ it('renders an empty table body when items is empty', async () => {
182
+ renderComponent({ items: [] });
183
+ await waitFor(() => {
184
+ expect(screen.getByText('Species name original')).toBeInTheDocument();
185
+ });
186
+ const rows = screen.getAllByRole('row');
187
+ expect(rows).toHaveLength(1);
188
+ });
189
+ });
190
+
191
+ describe('formatAssignedTo — rendered output', () => {
192
+ it('strips the (userid) suffix from assigned_to', async () => {
193
+ const items = [
194
+ {
195
+ ...buildItems(1)[0],
196
+ nis_assigned_to: 'Jane Expert (jexpert)',
197
+ },
198
+ ];
199
+ renderComponent({ items });
200
+ await waitFor(() => {
201
+ expect(screen.getByText('Jane Expert')).toBeInTheDocument();
202
+ });
203
+ expect(screen.queryByText('Jane Expert (jexpert)')).toBeNull();
204
+ });
205
+
206
+ it('decodes Python \\U escape sequences in assigned_to', async () => {
207
+ const items = [
208
+ {
209
+ ...buildItems(1)[0],
210
+ nis_assigned_to:
211
+ '\\U000000d6sterreich User (\\U000000d6sterreichUser)',
212
+ },
213
+ ];
214
+ renderComponent({ items });
215
+ await waitFor(() => {
216
+ expect(screen.getByText('Österreich User')).toBeInTheDocument();
217
+ });
218
+ });
219
+
220
+ it('shows empty string when assigned_to is null', async () => {
221
+ const items = [
222
+ {
223
+ ...buildItems(1)[0],
224
+ nis_assigned_to: null,
225
+ },
226
+ ];
227
+ renderComponent({ items });
228
+ await waitFor(() => {
229
+ expect(screen.getByText('Species 1')).toBeInTheDocument();
230
+ });
231
+ const containers = document.querySelectorAll('.assigned-to-container');
232
+ expect(containers.length).toBeGreaterThan(0);
233
+ expect(containers[0].firstChild.textContent).toBe('');
234
+ });
235
+ });
236
+
237
+ describe('admin controls — canEditPage', () => {
238
+ it('shows assign, download, and check-duplicates buttons when user can edit', async () => {
239
+ mockUseSelector.mockReturnValue({
240
+ object: [{ id: 'edit' }],
241
+ });
242
+ renderComponent();
243
+ await waitFor(() => {
244
+ expect(screen.getByText('Assign search results')).toBeInTheDocument();
245
+ });
246
+ expect(screen.getByText('Download search results')).toBeInTheDocument();
247
+ expect(screen.getByText('Check duplicates')).toBeInTheDocument();
248
+ });
249
+
250
+ it('hides admin controls when user cannot edit', async () => {
251
+ mockUseSelector.mockReturnValue({
252
+ object: [],
253
+ });
254
+ renderComponent();
255
+ await waitFor(() => {
256
+ expect(screen.getByText('Species name original')).toBeInTheDocument();
257
+ });
258
+ expect(
259
+ screen.queryByText('Assign search results'),
260
+ ).not.toBeInTheDocument();
261
+ expect(
262
+ screen.queryByText('Download search results'),
263
+ ).not.toBeInTheDocument();
264
+ expect(screen.queryByText('Check duplicates')).not.toBeInTheDocument();
265
+ });
266
+
267
+ it('hides checkboxes and Copy button when user cannot edit', async () => {
268
+ mockUseSelector.mockReturnValue({
269
+ object: [],
270
+ });
271
+ renderComponent();
272
+ await waitFor(() => {
273
+ expect(screen.getByText('Species 1')).toBeInTheDocument();
274
+ });
275
+ expect(document.querySelector('.ui.checkbox')).toBeNull();
276
+ expect(screen.queryByText('Copy')).not.toBeInTheDocument();
277
+ });
278
+ });
279
+
280
+ describe('duplicate checking', () => {
281
+ beforeEach(() => {
282
+ window.location.search = '?check-duplicates=1';
283
+ });
284
+
285
+ function mockDuplicateFetch(duplicateIds, groups) {
286
+ global.fetch.mockImplementation((url) => {
287
+ if (typeof url === 'string' && url.includes('nis_experts_vocabulary')) {
288
+ return Promise.resolve({
289
+ json: () =>
290
+ Promise.resolve({
291
+ items: [{ token: 'jdoe', title: 'John Doe (jdoe)' }],
292
+ }),
293
+ });
294
+ }
295
+ if (typeof url === 'string' && url.includes('check-nis-duplicates')) {
296
+ return Promise.resolve({
297
+ json: () =>
298
+ Promise.resolve({
299
+ duplicate_ids: duplicateIds,
300
+ groups: groups,
301
+ }),
302
+ });
303
+ }
304
+ return Promise.resolve({
305
+ json: () => Promise.resolve({}),
306
+ ok: true,
307
+ });
308
+ });
309
+ }
310
+
311
+ it('shows the duplicate info banner when check-duplicates param is set', async () => {
312
+ mockDuplicateFetch(
313
+ ['/marine/item-1', '/marine/item-3'],
314
+ [{ id: 1 }, { id: 2 }],
315
+ );
316
+ renderComponent();
317
+ await waitFor(() => {
318
+ expect(screen.getByText(/duplicate records/)).toBeInTheDocument();
319
+ });
320
+ expect(screen.getByText(/2 duplicate records/)).toBeInTheDocument();
321
+ expect(screen.getByText(/2 groups/)).toBeInTheDocument();
322
+ });
323
+
324
+ it('filters table to show only duplicate items', async () => {
325
+ mockDuplicateFetch(
326
+ ['/marine/item-1', '/marine/item-3'],
327
+ [{ id: 1 }, { id: 2 }],
328
+ );
329
+ const items = buildItems(5);
330
+ renderComponent({ items });
331
+ await waitFor(() => {
332
+ expect(screen.getByText(/duplicate records/)).toBeInTheDocument();
333
+ });
334
+ expect(screen.getByText('Species 1')).toBeInTheDocument();
335
+ expect(screen.getByText('Species 3')).toBeInTheDocument();
336
+ expect(screen.queryByText('Species 2')).toBeNull();
337
+ expect(screen.queryByText('Species 4')).toBeNull();
338
+ expect(screen.queryByText('Species 5')).toBeNull();
339
+ });
340
+
341
+ it('renders a Clear link that removes check-duplicates from URL', async () => {
342
+ mockDuplicateFetch([], []);
343
+ renderComponent();
344
+ await waitFor(() => {
345
+ expect(screen.getByText('Clear')).toBeInTheDocument();
346
+ });
347
+ const clearLink = screen.getByText('Clear');
348
+ expect(clearLink.tagName).toBe('A');
349
+ expect(clearLink.getAttribute('href')).not.toContain('check-duplicates');
350
+ });
351
+ });
352
+
353
+ describe('item selection', () => {
354
+ it('toggles checkbox selection', async () => {
355
+ renderComponent();
356
+ await waitFor(() => {
357
+ expect(screen.getByText('Species 1')).toBeInTheDocument();
358
+ });
359
+ const checkboxes = document.querySelectorAll('.ui.checkbox input');
360
+ expect(checkboxes.length).toBeGreaterThan(0);
361
+ fireEvent.click(checkboxes[0]);
362
+ // Selection panel should appear after clicking a checkbox
363
+ await waitFor(() => {
364
+ expect(screen.getByText(/Assign 1 selected item/)).toBeInTheDocument();
365
+ });
366
+ });
367
+
368
+ it('removes selection when checkbox is unchecked', async () => {
369
+ renderComponent();
370
+ await waitFor(() => {
371
+ expect(screen.getByText('Species 1')).toBeInTheDocument();
372
+ });
373
+ const checkboxes = document.querySelectorAll('.ui.checkbox input');
374
+ fireEvent.click(checkboxes[0]);
375
+ await waitFor(() => {
376
+ expect(screen.getByText(/Assign 1 selected item/)).toBeInTheDocument();
377
+ });
378
+ fireEvent.click(checkboxes[0]);
379
+ await waitFor(() => {
380
+ expect(
381
+ screen.queryByText(/Assign 1 selected item/),
382
+ ).not.toBeInTheDocument();
383
+ });
384
+ });
385
+
386
+ it('updates count when selecting multiple items', async () => {
387
+ renderComponent();
388
+ await waitFor(() => {
389
+ expect(screen.getByText('Species 1')).toBeInTheDocument();
390
+ });
391
+ const checkboxes = document.querySelectorAll('.ui.checkbox input');
392
+ fireEvent.click(checkboxes[0]);
393
+ fireEvent.click(checkboxes[1]);
394
+ await waitFor(() => {
395
+ expect(screen.getByText(/Assign 2 selected items/)).toBeInTheDocument();
396
+ });
397
+ });
398
+ });
399
+
400
+ describe('bulk assign panel', () => {
401
+ it('shows Cancel and Assign buttons after selection', async () => {
402
+ renderComponent();
403
+ await waitFor(() => {
404
+ expect(screen.getByText('Species 1')).toBeInTheDocument();
405
+ });
406
+ const checkboxes = document.querySelectorAll('.ui.checkbox input');
407
+ fireEvent.click(checkboxes[0]);
408
+ await waitFor(() => {
409
+ expect(screen.getByText('Cancel')).toBeInTheDocument();
410
+ });
411
+ expect(screen.getByText('Assign')).toBeInTheDocument();
412
+ });
413
+
414
+ it('Assign button is disabled when no assignee is selected', async () => {
415
+ renderComponent();
416
+ await waitFor(() => {
417
+ expect(screen.getByText('Species 1')).toBeInTheDocument();
418
+ });
419
+ const checkboxes = document.querySelectorAll('.ui.checkbox input');
420
+ fireEvent.click(checkboxes[0]);
421
+ await waitFor(() => {
422
+ expect(screen.getByText('Assign')).toBeInTheDocument();
423
+ });
424
+ const assignButton = screen.getByText('Assign').closest('button');
425
+ expect(assignButton).toBeDisabled();
426
+ });
427
+
428
+ it('Cancel button clears selection', async () => {
429
+ renderComponent();
430
+ await waitFor(() => {
431
+ expect(screen.getByText('Species 1')).toBeInTheDocument();
432
+ });
433
+ const checkboxes = document.querySelectorAll('.ui.checkbox input');
434
+ fireEvent.click(checkboxes[0]);
435
+ await waitFor(() => {
436
+ expect(screen.getByText('Cancel')).toBeInTheDocument();
437
+ });
438
+ fireEvent.click(screen.getByText('Cancel'));
439
+ await waitFor(() => {
440
+ expect(screen.queryByText('Cancel')).not.toBeInTheDocument();
441
+ });
442
+ });
443
+ });
444
+ });