@centreon/ui 26.12.0 → 26.12.2

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 (57) hide show
  1. package/package.json +6 -3
  2. package/public/mockServiceWorker.js +31 -8
  3. package/src/Dashboard/Dashboard.browser.test.tsx +107 -0
  4. package/src/Form/Form.browser.test.tsx +445 -0
  5. package/src/Form/Inputs/ExclusiveCheckboxGroup.browser.test.tsx +304 -0
  6. package/src/Form/Inputs/SegmentedButtons.browser.test.tsx +113 -0
  7. package/src/InputField/Number/Number.browser.test.tsx +109 -0
  8. package/src/InputField/Select/Autocomplete/Connected/Multi/MultiConnectedAutocompleteField.browser.test.tsx +250 -0
  9. package/src/Listing/Listing.browser.test.tsx +476 -0
  10. package/src/Module/Module.browser.test.tsx +145 -0
  11. package/src/Pagination/Pagination.browser.test.tsx +184 -0
  12. package/src/Panel/Section/index.test.tsx +1 -1
  13. package/src/RichTextEditor/RichTextEditor.browser.test.tsx +567 -0
  14. package/src/RichTextEditor/plugins/FloatingLinkEditorPlugin.tsx +3 -1
  15. package/src/TimePeriods/ResolutionTimePeriod.browser.test.tsx +143 -0
  16. package/src/TimePeriods/TimePeriods.browser.test.tsx +468 -0
  17. package/src/Typography/FluidTypography/FluidTypography.browser.test.tsx +54 -0
  18. package/src/api/useMutationQuery/useMutationQuery.browser.test.tsx +154 -0
  19. package/src/components/CollapsibleItem/CollapsibleItem.browser.test.tsx +120 -0
  20. package/src/components/CopyCommand/CopyCommand.browser.test.tsx +287 -0
  21. package/src/components/CrudPage/CrudPage.browser.test.tsx +659 -0
  22. package/src/components/DataTable/DataTable.browser.test.tsx +257 -0
  23. package/src/components/Form/AccessRights/AccessRights.browser.test.tsx +527 -0
  24. package/src/components/ItemComposition/ItemComposition.browser.test.tsx +172 -0
  25. package/src/components/Layout/PageLayout.browser.test.tsx +114 -0
  26. package/src/components/Modal/ConfirmationModal/ConfirmationModal.browser.test.tsx +237 -0
  27. package/src/components/SortableEntriesList/SortableEntriesList.browser.test.tsx +981 -0
  28. package/src/components/Tabs/Tabs.browser.test.tsx +115 -0
  29. package/src/utils/useFullscreen/Fullscreen.browser.test.tsx +176 -0
  30. package/src/utils/useLocale/useLocale.browser.test.tsx +49 -0
  31. package/src/utils/useLocaleTimezoneDate/{LocaleTimezoneDate.cypress.spec.tsx → LocaleTimezoneDate.browser.test.tsx} +24 -11
  32. package/src/Dashboard/Dashboard.cypress.spec.tsx +0 -71
  33. package/src/Form/Form.cypress.spec.tsx +0 -306
  34. package/src/Form/Inputs/ExclusiveCheckboxGroup.cypress.spec.tsx +0 -236
  35. package/src/Form/Inputs/SegmentedButtons.cypress.spec.tsx +0 -101
  36. package/src/InputField/Number/Number.cypress.spec.tsx +0 -89
  37. package/src/InputField/Select/Autocomplete/Connected/Multi/MultiConnectedAutocompleteField.cypress.spec.tsx +0 -182
  38. package/src/Listing/Listing.cypress.spec.tsx +0 -349
  39. package/src/Module/Module.cypress.spec.tsx +0 -127
  40. package/src/Pagination/Pagination.cypress.spec.tsx +0 -140
  41. package/src/RichTextEditor/RichTextEditor.cypress.spec.tsx +0 -408
  42. package/src/TimePeriods/ResolutionTimePeriod.cypress.spec.tsx +0 -74
  43. package/src/TimePeriods/TimePeriods.cypress.spec.tsx +0 -488
  44. package/src/Typography/FluidTypography/FluidTypography.cypress.spec.tsx +0 -27
  45. package/src/api/useMutationQuery/useMutationQuery.cypress.spec.tsx +0 -93
  46. package/src/components/CollapsibleItem/CollapsibleItem.cypress.spec.tsx +0 -76
  47. package/src/components/CopyCommand/CopyCommand.cypress.spec.tsx +0 -167
  48. package/src/components/CrudPage/CrudPage.cypress.spec.tsx +0 -561
  49. package/src/components/DataTable/DataTable.cypress.spec.tsx +0 -174
  50. package/src/components/Form/AccessRights/AccessRights.cypress.spec.tsx +0 -376
  51. package/src/components/ItemComposition/ItemComposition.cypress.spec.tsx +0 -112
  52. package/src/components/Layout/PageLayout.cypress.spec.tsx +0 -64
  53. package/src/components/Modal/ConfirmationModal/ConfirmationModal.cypress.spec.tsx +0 -171
  54. package/src/components/SortableEntriesList/SortableEntriesList.cypress.spec.tsx +0 -792
  55. package/src/components/Tabs/Tabs.cypress.spec.tsx +0 -69
  56. package/src/utils/useFullscreen/Fullscreen.cypress.spec.tsx +0 -130
  57. package/src/utils/useLocale/useLocale.cypress.spec.tsx +0 -40
@@ -0,0 +1,527 @@
1
+ import { rs } from '@rstest/core';
2
+ import { interceptAPIRequest, Method, waitForRequest } from '@test-setup/api';
3
+ import { render } from '@test-setup/render';
4
+ import { screen, waitFor, within } from '@testing-library/react';
5
+ import userEvent from '@testing-library/user-event';
6
+ import { createStore, Provider } from 'jotai';
7
+
8
+ import { SnackbarProvider, TestQueryProvider } from '../../..';
9
+ import { AccessRights } from './AccessRights';
10
+ import type { AccessRightInitialValues } from './models';
11
+ import {
12
+ buildResult,
13
+ emptyAccessRights,
14
+ labels,
15
+ roles,
16
+ simpleAccessRights
17
+ } from './storiesData';
18
+
19
+ // The access rights live in a module-level Jotai store (`./Provider`), so
20
+ // they carry over from one test to the next: the tests below rely on that
21
+ // order.
22
+
23
+ interface InitializeProps {
24
+ initialValues?: Array<AccessRightInitialValues>;
25
+ loading?: boolean;
26
+ }
27
+
28
+ interface InitializeState {
29
+ cancel: ReturnType<typeof rs.fn>;
30
+ change: ReturnType<typeof rs.fn>;
31
+ save: ReturnType<typeof rs.fn>;
32
+ }
33
+
34
+ const initialize = ({
35
+ initialValues = simpleAccessRights,
36
+ loading = false
37
+ }: InitializeProps): InitializeState => {
38
+ const cancel = rs.fn();
39
+ const save = rs.fn();
40
+ const change = rs.fn();
41
+
42
+ interceptAPIRequest({
43
+ alias: 'getContacts',
44
+ method: Method.GET,
45
+ path: '**/contacts?**',
46
+ response: buildResult(false)
47
+ });
48
+ interceptAPIRequest({
49
+ alias: 'getContactGroups',
50
+ method: Method.GET,
51
+ path: '**/contact-groups?**',
52
+ response: buildResult(true)
53
+ });
54
+
55
+ render(
56
+ <SnackbarProvider>
57
+ <TestQueryProvider>
58
+ <Provider store={createStore()}>
59
+ <AccessRights
60
+ cancel={cancel}
61
+ endpoints={{
62
+ contact: '/contacts',
63
+ contactGroup: '/contact-groups'
64
+ }}
65
+ initialValues={initialValues}
66
+ labels={labels}
67
+ loading={loading}
68
+ onChange={change}
69
+ roles={roles}
70
+ submit={save}
71
+ />
72
+ </Provider>
73
+ </TestQueryProvider>
74
+ </SnackbarProvider>
75
+ );
76
+
77
+ return {
78
+ cancel,
79
+ change,
80
+ save
81
+ };
82
+ };
83
+
84
+ // The first element whose own text contains the given one.
85
+ const findFirstByText = async (text: string): Promise<HTMLElement> =>
86
+ (
87
+ await screen.findAllByText((_, element) =>
88
+ Array.from(element?.childNodes ?? []).some(
89
+ (node) =>
90
+ node.nodeType === Node.TEXT_NODE &&
91
+ (node.textContent ?? '').replace(/\s+/g, ' ').includes(text)
92
+ )
93
+ )
94
+ )[0];
95
+
96
+ const queryFirstByText = (text: string): HTMLElement | undefined =>
97
+ screen.queryAllByText((_, element) =>
98
+ Array.from(element?.childNodes ?? []).some(
99
+ (node) =>
100
+ node.nodeType === Node.TEXT_NODE &&
101
+ (node.textContent ?? '').replace(/\s+/g, ' ').includes(text)
102
+ )
103
+ )[0];
104
+
105
+ const getInput = (testId: string): HTMLInputElement =>
106
+ screen
107
+ .getAllByTestId(testId)
108
+ .find((element) => element.tagName === 'INPUT') as HTMLInputElement;
109
+
110
+ // Clicks the select's combobox: MUI opens the select on its mouse down.
111
+ const openSelect = async (testId: string): Promise<void> => {
112
+ const select = getInput(testId).parentElement as HTMLElement;
113
+
114
+ await userEvent.click(within(select).getByRole('combobox'));
115
+ };
116
+
117
+ const selectRole = async (testId: string, role: string): Promise<void> => {
118
+ await openSelect(testId);
119
+
120
+ const listbox = (await screen.findAllByRole('listbox')).at(-1) as HTMLElement;
121
+
122
+ await userEvent.click(
123
+ listbox.querySelector(`li[data-value="${role}"]`) as HTMLElement
124
+ );
125
+
126
+ await waitFor(() => {
127
+ expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
128
+ });
129
+ };
130
+
131
+ const openAutocomplete = async (label: string): Promise<void> => {
132
+ await userEvent.click(screen.getByLabelText(label));
133
+ };
134
+
135
+ const selectOption = async (name: string): Promise<void> => {
136
+ const listbox = await screen.findByRole('listbox');
137
+
138
+ await userEvent.click(await within(listbox).findByText(name));
139
+ };
140
+
141
+ describe('Access rights', () => {
142
+ beforeEach(() => {
143
+ expect.hasAssertions();
144
+ });
145
+
146
+ it('displays the access rights in loading state', async () => {
147
+ initialize({ loading: true });
148
+
149
+ expect(await findFirstByText('Share dashboard with')).toBeVisible();
150
+ expect(await findFirstByText('Contact')).toBeVisible();
151
+ expect(await findFirstByText('Contact group')).toBeVisible();
152
+ expect(screen.getByLabelText('Add a contact')).toBeVisible();
153
+ expect(getInput('add_role')).toBeDisabled();
154
+ expect(screen.getByTestId('add')).toBeDisabled();
155
+ expect(screen.getByLabelText('Cancel')).toBeVisible();
156
+ expect(screen.getByLabelText('Save')).toBeVisible();
157
+
158
+ // The contact type is selected, the role defaults to viewer, the list is
159
+ // replaced by its skeleton and nothing can be saved.
160
+ expect(screen.getByRole('radio', { name: 'Contact' })).toBeChecked();
161
+ expect(getInput('add_role')).toHaveValue('viewer');
162
+ expect(
163
+ screen.queryByText(simpleAccessRights[0].name)
164
+ ).not.toBeInTheDocument();
165
+ expect(screen.getByLabelText('Save')).toBeDisabled();
166
+ });
167
+
168
+ it('displays the access rights with an empty list', async () => {
169
+ initialize({ initialValues: emptyAccessRights });
170
+
171
+ expect(await findFirstByText('The contact list is empty')).toBeVisible();
172
+
173
+ // The list title, no stats and nothing to save.
174
+ expect(screen.getByText('User rights')).toBeVisible();
175
+ expect(screen.queryByText(/added|updated|removed/)).not.toBeInTheDocument();
176
+ expect(screen.getByLabelText('Save')).toBeDisabled();
177
+ });
178
+
179
+ it('displays the access rights list', async () => {
180
+ initialize({});
181
+
182
+ for (const { name, email, isContactGroup, role } of simpleAccessRights) {
183
+ expect(await findFirstByText(name)).toBeVisible();
184
+ expect(await findFirstByText(email as string)).toBeVisible();
185
+
186
+ const groupChip = document.querySelector(`[data-groupChip="${name}"]`);
187
+
188
+ if (isContactGroup) {
189
+ expect(groupChip).toBeVisible();
190
+ } else {
191
+ expect(groupChip).not.toBeInTheDocument();
192
+ }
193
+
194
+ expect(getInput(`role-${name}`)).toHaveValue(role);
195
+ expect(screen.getByTestId(`remove-${name}`)).toBeVisible();
196
+ }
197
+
198
+ // No state chip, no stats and nothing to save.
199
+ expect(document.querySelector('[data-state]')).not.toBeInTheDocument();
200
+ expect(screen.queryByText(/added|updated|removed/)).not.toBeInTheDocument();
201
+ expect(screen.getByLabelText('Save')).toBeDisabled();
202
+ });
203
+
204
+ it('displays a removed chip when the corresponding icon is clicked', async () => {
205
+ initialize({});
206
+
207
+ expect(await screen.findByTestId('remove-Erin Monahan')).toHaveAttribute(
208
+ 'data-removed',
209
+ 'false'
210
+ );
211
+
212
+ await userEvent.click(screen.getByTestId('remove-Erin Monahan'));
213
+
214
+ await waitFor(() => {
215
+ expect(screen.getByTestId('remove-Erin Monahan')).toHaveAttribute(
216
+ 'data-removed',
217
+ 'true'
218
+ );
219
+ });
220
+ expect(await findFirstByText(labels.list.removed)).toBeVisible();
221
+
222
+ // The removed contact's role is disabled, the stats count it and the
223
+ // changes can be saved.
224
+ expect(getInput('role-Erin Monahan')).toBeDisabled();
225
+ expect(await screen.findByText('1 removed')).toBeVisible();
226
+ expect(screen.getByLabelText('Save')).toBeEnabled();
227
+ });
228
+
229
+ it('restores the contact when the contact is removed and the corresponding icon is clicked', async () => {
230
+ initialize({});
231
+
232
+ await userEvent.click(await screen.findByTestId('remove-Erin Monahan'));
233
+
234
+ await waitFor(() => {
235
+ expect(screen.getByTestId('remove-Erin Monahan')).toHaveAttribute(
236
+ 'data-removed',
237
+ 'false'
238
+ );
239
+ });
240
+ expect(queryFirstByText(labels.list.removed)).toBeUndefined();
241
+
242
+ // The role is enabled again, no stats and nothing to save.
243
+ expect(getInput('role-Erin Monahan')).toBeEnabled();
244
+ expect(screen.queryByText(/removed/)).not.toBeInTheDocument();
245
+ expect(screen.getByLabelText('Save')).toBeDisabled();
246
+ });
247
+
248
+ it('submits the new acces rights list without the removed contact', async () => {
249
+ const { save } = initialize({});
250
+
251
+ await userEvent.click(await screen.findByTestId('remove-Erin Monahan'));
252
+
253
+ await waitFor(() => {
254
+ expect(screen.getByTestId('remove-Erin Monahan')).toHaveAttribute(
255
+ 'data-removed',
256
+ 'true'
257
+ );
258
+ });
259
+ expect(await findFirstByText(labels.list.removed)).toBeVisible();
260
+ expect(await findFirstByText('1 removed')).toBeVisible();
261
+
262
+ await userEvent.click(await findFirstByText(labels.actions.save));
263
+
264
+ await waitFor(() => {
265
+ expect(save).toHaveBeenCalled();
266
+ });
267
+
268
+ // The saved list is the initial one without the removed contact (id 1).
269
+ expect(save).toHaveBeenCalledWith(
270
+ expect.not.arrayContaining([
271
+ expect.objectContaining({ name: 'Erin Monahan' })
272
+ ])
273
+ );
274
+ expect(
275
+ (save.mock.calls[0][0] as Array<{ id: number }>).map(({ id }) => id)
276
+ ).toEqual([0, 2, 3, 4, 5]);
277
+ });
278
+
279
+ it('submits the new acces rights list with the updated contact', async () => {
280
+ const { save } = initialize({});
281
+
282
+ await userEvent.click(await screen.findByTestId('remove-Erin Monahan'));
283
+ await selectRole('role-Erin Monahan', 'viewer');
284
+
285
+ expect(await findFirstByText(labels.list.updated)).toBeVisible();
286
+ expect(await findFirstByText('1 updated')).toBeVisible();
287
+
288
+ await userEvent.click(await findFirstByText(labels.actions.save));
289
+
290
+ expect(save).toHaveBeenCalled();
291
+
292
+ // The contact is back, with its new role.
293
+ expect(getInput('role-Erin Monahan')).toHaveValue('viewer');
294
+ expect(screen.getByTestId('remove-Erin Monahan')).toHaveAttribute(
295
+ 'data-removed',
296
+ 'false'
297
+ );
298
+ });
299
+
300
+ it('removes the updated chip when the contact role is updated and its initial role is assigned back', async () => {
301
+ initialize({});
302
+
303
+ await selectRole('role-Erin Monahan', 'viewer');
304
+ expect(await findFirstByText(labels.list.updated)).toBeVisible();
305
+
306
+ await selectRole('role-Erin Monahan', 'editor');
307
+ await waitFor(() => {
308
+ expect(queryFirstByText(labels.list.updated)).toBeUndefined();
309
+ });
310
+
311
+ // The initial role is back and nothing to save.
312
+ expect(getInput('role-Erin Monahan')).toHaveValue('editor');
313
+ expect(screen.getByLabelText('Save')).toBeDisabled();
314
+ });
315
+
316
+ it('retrieves contacts when the contact radio is selected', async () => {
317
+ initialize({});
318
+
319
+ await userEvent.click(await findFirstByText(labels.add.contact));
320
+ await openAutocomplete(labels.add.autocompleteContact);
321
+
322
+ await waitForRequest('@getContacts');
323
+
324
+ // The contacts are listed in the autocomplete.
325
+ const listbox = await screen.findByRole('listbox');
326
+
327
+ expect(await within(listbox).findByText('Entity 10')).toBeVisible();
328
+ expect(within(listbox).getAllByRole('option')).toHaveLength(10);
329
+ });
330
+
331
+ it('retrieves contact groups when the contact group radio is selected', async () => {
332
+ initialize({});
333
+
334
+ await userEvent.click(await findFirstByText(labels.add.contactGroup));
335
+ await openAutocomplete(labels.add.autocompleteContactGroup);
336
+
337
+ await waitForRequest('@getContactGroups');
338
+
339
+ // The contact group type is selected and the groups are listed in the
340
+ // autocomplete.
341
+ expect(screen.getByRole('radio', { name: 'Contact group' })).toBeChecked();
342
+
343
+ const listbox = await screen.findByRole('listbox');
344
+
345
+ expect(await within(listbox).findByText('Entity Group 10')).toBeVisible();
346
+ expect(within(listbox).getAllByRole('option')).toHaveLength(10);
347
+ });
348
+
349
+ it('adds the contact as viewer when a contact is selected and the corresponding button is clicked', async () => {
350
+ initialize({});
351
+
352
+ await userEvent.click(await findFirstByText(labels.add.contact));
353
+ await openAutocomplete(labels.add.autocompleteContact);
354
+
355
+ await waitForRequest('@getContacts');
356
+
357
+ await selectOption('Entity 10');
358
+
359
+ await userEvent.click(screen.getByTestId('add'));
360
+
361
+ expect(await findFirstByText('Entity 10')).toBeVisible();
362
+
363
+ expect(getInput('role-Entity 10')).toHaveValue('viewer');
364
+ expect(await findFirstByText(labels.list.added)).toBeVisible();
365
+
366
+ // The autocomplete is cleared and the stats count the new contact.
367
+ expect(screen.getByLabelText(labels.add.autocompleteContact)).toHaveValue(
368
+ ''
369
+ );
370
+ expect(await screen.findByText('1 added')).toBeVisible();
371
+ });
372
+
373
+ it('adds the contact group as editor when a contact group is selected and the corresponding button is clicked', async () => {
374
+ initialize({});
375
+
376
+ await userEvent.click(await findFirstByText(labels.add.contactGroup));
377
+ await openAutocomplete(labels.add.autocompleteContactGroup);
378
+
379
+ await waitForRequest('@getContactGroups');
380
+
381
+ await selectOption('Entity Group 10');
382
+ await selectRole('add_role', 'editor');
383
+ await userEvent.click(screen.getByTestId('add'));
384
+
385
+ expect(await findFirstByText('Entity Group 10')).toBeVisible();
386
+
387
+ expect(getInput('role-Entity Group 10')).toHaveValue('editor');
388
+ expect(await findFirstByText(labels.list.added)).toBeVisible();
389
+ expect(await findFirstByText('2 added')).toBeVisible();
390
+
391
+ expect(
392
+ document.querySelector('[data-groupChip="Entity Group 10"]')
393
+ ).toBeVisible();
394
+ });
395
+
396
+ it('displays statistics when a contact is added, a contact is updated and a contact is removed', async () => {
397
+ initialize({});
398
+
399
+ await userEvent.click(await findFirstByText(labels.add.contactGroup));
400
+ await openAutocomplete(labels.add.autocompleteContactGroup);
401
+
402
+ await waitForRequest('@getContactGroups');
403
+
404
+ await selectOption('Entity Group 11');
405
+ await selectRole('add_role', 'editor');
406
+ await userEvent.click(screen.getByTestId('add'));
407
+
408
+ expect(await findFirstByText('Entity Group 11')).toBeVisible();
409
+
410
+ await selectRole('role-Erin Monahan', 'viewer');
411
+
412
+ await userEvent.click(screen.getByTestId('remove-Maryann Ferry'));
413
+
414
+ expect(
415
+ await findFirstByText('3 added | 1 updated | 1 removed')
416
+ ).toBeVisible();
417
+
418
+ // The state chips of the changed contacts.
419
+ expect(document.querySelectorAll('[data-state="added"]')).toHaveLength(3);
420
+ expect(document.querySelectorAll('[data-state="updated"]')).toHaveLength(1);
421
+ expect(document.querySelectorAll('[data-state="removed"]')).toHaveLength(1);
422
+ expect(getInput('role-Entity Group 11')).toHaveValue('editor');
423
+ });
424
+
425
+ it('clears the autocomplete when a contact is selected and the contact type is changed', async () => {
426
+ initialize({});
427
+
428
+ await userEvent.click(await findFirstByText(labels.add.contact));
429
+ await openAutocomplete(labels.add.autocompleteContact);
430
+
431
+ await waitForRequest('@getContacts');
432
+
433
+ await selectOption('Entity 11');
434
+ await waitFor(() => {
435
+ expect(screen.getByLabelText(labels.add.autocompleteContact)).toHaveValue(
436
+ 'Entity 11'
437
+ );
438
+ });
439
+
440
+ await userEvent.click(await findFirstByText(labels.add.contactGroup));
441
+ await waitFor(() => {
442
+ expect(
443
+ screen.getByLabelText(labels.add.autocompleteContactGroup)
444
+ ).toHaveValue('');
445
+ });
446
+
447
+ // Nothing selected, so nothing can be added.
448
+ expect(screen.getByTestId('add')).toBeDisabled();
449
+ expect(getInput('add_role')).toBeDisabled();
450
+ });
451
+
452
+ it('removes the contact from the list when the contact has been to the list and the corresponding button is clicked', async () => {
453
+ initialize({});
454
+
455
+ await userEvent.click(await findFirstByText(labels.add.contact));
456
+ await openAutocomplete(labels.add.autocompleteContact);
457
+
458
+ await waitForRequest('@getContacts');
459
+
460
+ await selectOption('Entity 11');
461
+
462
+ await userEvent.click(screen.getByTestId('add'));
463
+
464
+ expect(await findFirstByText('Entity 11')).toBeVisible();
465
+
466
+ expect(await findFirstByText(labels.list.added)).toBeVisible();
467
+ expect(await findFirstByText('4 added')).toBeVisible();
468
+
469
+ await userEvent.click(screen.getByTestId('remove-Entity 11'));
470
+
471
+ await waitFor(() => {
472
+ expect(queryFirstByText('Entity 11')).toBeUndefined();
473
+ });
474
+ expect(queryFirstByText('4 added')).toBeUndefined();
475
+
476
+ // The stats are back to the previous changes.
477
+ expect(
478
+ await findFirstByText('3 added | 1 updated | 1 removed')
479
+ ).toBeVisible();
480
+ });
481
+
482
+ it('resets the role to "viewer" when the "editor" role is selected and a contact with "viewer" right is selected', async () => {
483
+ initialize({});
484
+
485
+ await openAutocomplete(labels.add.autocompleteContact);
486
+
487
+ await waitForRequest('@getContacts');
488
+
489
+ await selectOption('Entity 11');
490
+
491
+ await selectRole('add_role', 'editor');
492
+
493
+ await openAutocomplete(labels.add.autocompleteContact);
494
+
495
+ await waitForRequest('@getContacts');
496
+
497
+ await selectOption('Entity 12');
498
+
499
+ await waitFor(() => {
500
+ expect(getInput('add_role')).toHaveValue('viewer');
501
+ });
502
+
503
+ // The contact can be added.
504
+ expect(screen.getByTestId('add')).toBeEnabled();
505
+ });
506
+
507
+ it('calls the change function when the corresponding prop is set and the form is updated', async () => {
508
+ const { change } = initialize({});
509
+
510
+ await userEvent.click(await findFirstByText(labels.add.contact));
511
+ await openAutocomplete(labels.add.autocompleteContact);
512
+
513
+ await waitForRequest('@getContacts');
514
+
515
+ await selectOption('Entity 11');
516
+
517
+ await userEvent.click(screen.getByTestId('add'));
518
+
519
+ expect(await findFirstByText('Entity 11')).toBeVisible();
520
+
521
+ expect(getInput('role-Entity 11')).toHaveValue('viewer');
522
+ expect(await findFirstByText(labels.list.added)).toBeVisible();
523
+ await waitFor(() => {
524
+ expect(change).toHaveBeenCalledTimes(2);
525
+ });
526
+ });
527
+ });
@@ -0,0 +1,172 @@
1
+ import { rs } from '@rstest/core';
2
+ import { render } from '@test-setup/render';
3
+ import { screen } from '@testing-library/react';
4
+ import userEvent from '@testing-library/user-event';
5
+
6
+ import { ItemComposition } from '.';
7
+ import type { Props } from './ItemComposition';
8
+
9
+ interface InitializeProps extends Partial<Props> {
10
+ deleteButtonHidden?: boolean;
11
+ }
12
+
13
+ interface Callbacks {
14
+ addItem: ReturnType<typeof rs.fn>;
15
+ deleteItem: ReturnType<typeof rs.fn>;
16
+ }
17
+
18
+ const initialize = ({
19
+ deleteButtonHidden,
20
+ ...props
21
+ }: InitializeProps): Callbacks => {
22
+ const addItem = rs.fn();
23
+ const deleteItem = rs.fn();
24
+
25
+ render(
26
+ <ItemComposition {...props} labelAdd="Add" onAddItem={addItem}>
27
+ <ItemComposition.Item
28
+ deleteButtonHidden={deleteButtonHidden}
29
+ labelDelete="Delete"
30
+ onDeleteItem={deleteItem}
31
+ >
32
+ <div>Item 1</div>
33
+ </ItemComposition.Item>
34
+ <ItemComposition.Item
35
+ deleteButtonHidden={deleteButtonHidden}
36
+ labelDelete="Delete"
37
+ onDeleteItem={deleteItem}
38
+ >
39
+ <div>Item 2</div>
40
+ </ItemComposition.Item>
41
+ </ItemComposition>
42
+ );
43
+
44
+ return {
45
+ addItem,
46
+ deleteItem
47
+ };
48
+ };
49
+
50
+ // Both items are visible, Item 1 before Item 2.
51
+ const expectItemsInOrder = (): void => {
52
+ const item1 = screen.getByText('Item 1');
53
+ const item2 = screen.getByText('Item 2');
54
+
55
+ expect(item1).toBeVisible();
56
+ expect(item2).toBeVisible();
57
+ expect(
58
+ item1.compareDocumentPosition(item2) & Node.DOCUMENT_POSITION_FOLLOWING
59
+ ).toBeTruthy();
60
+ };
61
+
62
+ const expectAddButtonWithIcon = (): void => {
63
+ const addButton = screen.getByTestId('Add');
64
+
65
+ expect(addButton).toBeVisible();
66
+ expect(addButton).toHaveTextContent('Add');
67
+ expect(addButton.querySelector('[data-testid="AddIcon"]')).toBeVisible();
68
+ };
69
+
70
+ describe('ItemComposition', () => {
71
+ beforeEach(() => {
72
+ expect.hasAssertions();
73
+ });
74
+
75
+ it('displays the component', () => {
76
+ initialize({});
77
+
78
+ expect(screen.getByText('Item 1')).toBeVisible();
79
+ expect(screen.getByText('Item 2')).toBeVisible();
80
+ expect(screen.getAllByTestId('Delete')).toHaveLength(2);
81
+ expect(screen.getByTestId('Add')).toBeEnabled();
82
+
83
+ expectItemsInOrder();
84
+ expectAddButtonWithIcon();
85
+ screen.getAllByTestId('Delete').forEach((deleteButton) => {
86
+ expect(deleteButton).toBeVisible();
87
+ });
88
+ expect(document.querySelector('[data-linked]')).not.toBeInTheDocument();
89
+ });
90
+
91
+ it('displays the component with a secondary label', () => {
92
+ initialize({ secondaryLabel: 'Secondary label' });
93
+
94
+ expect(screen.getByText('Secondary label')).toBeVisible();
95
+
96
+ // The secondary label is on the right of the add button.
97
+ expectItemsInOrder();
98
+ expectAddButtonWithIcon();
99
+ expect(
100
+ screen.getByText('Secondary label').getBoundingClientRect().left
101
+ ).toBeGreaterThan(screen.getByTestId('Add').getBoundingClientRect().right);
102
+ });
103
+
104
+ it('displays add button as hidden when the prop is set to true', () => {
105
+ initialize({ addbuttonDisabled: true });
106
+
107
+ expect(screen.getByTestId('Add')).toBeDisabled();
108
+
109
+ expectItemsInOrder();
110
+ expect(screen.getByTestId('Add')).toBeVisible();
111
+ });
112
+
113
+ it('does not display the add button when the prop is set to true', () => {
114
+ initialize({ addButtonHidden: true });
115
+
116
+ expect(screen.queryByTestId('Add')).not.toBeInTheDocument();
117
+
118
+ expectItemsInOrder();
119
+ screen.getAllByTestId('Delete').forEach((deleteButton) => {
120
+ expect(deleteButton).toBeVisible();
121
+ });
122
+ });
123
+
124
+ it('does not display the delete button when the prop is set to true', () => {
125
+ initialize({ deleteButtonHidden: true });
126
+
127
+ // The delete buttons are kept in the page with `display: none`: none of
128
+ // them is displayed.
129
+ const deleteButtons = screen.queryAllByTestId('Delete');
130
+
131
+ expect(deleteButtons).toHaveLength(2);
132
+ deleteButtons.forEach((deleteButton) => {
133
+ expect(deleteButton).not.toBeVisible();
134
+ });
135
+
136
+ expectItemsInOrder();
137
+ expectAddButtonWithIcon();
138
+ });
139
+
140
+ it('calls the add function when the button is clicked', async () => {
141
+ const { addItem } = initialize({});
142
+
143
+ await userEvent.click(screen.getByTestId('Add'));
144
+
145
+ expect(addItem).toHaveBeenCalledTimes(1);
146
+ });
147
+
148
+ it('calls the delete function when the button is clicked', async () => {
149
+ const { deleteItem } = initialize({});
150
+
151
+ await userEvent.click(screen.getAllByTestId('Delete')[1]);
152
+
153
+ expect(deleteItem).toHaveBeenCalledTimes(1);
154
+ });
155
+
156
+ it('displays items as linked when the prop is set to true', () => {
157
+ initialize({ displayItemsAsLinked: true });
158
+
159
+ expect(document.querySelector('[data-linked]')).toBeVisible();
160
+
161
+ // The link icon is on the left of the items.
162
+ expectItemsInOrder();
163
+ expect(screen.getByTestId('LinkIcon')).toBeVisible();
164
+ expect(
165
+ (
166
+ document.querySelector('[data-linked]') as HTMLElement
167
+ ).getBoundingClientRect().right
168
+ ).toBeLessThanOrEqual(
169
+ screen.getByText('Item 1').getBoundingClientRect().left
170
+ );
171
+ });
172
+ });