@centreon/ui 26.12.1 → 26.12.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.
Files changed (59) 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/CollapsibleGroup.tsx +6 -33
  5. package/src/Form/Form.browser.test.tsx +553 -0
  6. package/src/Form/Inputs/ExclusiveCheckboxGroup.browser.test.tsx +304 -0
  7. package/src/Form/Inputs/Grid.tsx +1 -2
  8. package/src/Form/Inputs/SegmentedButtons.browser.test.tsx +113 -0
  9. package/src/Form/Inputs/index.tsx +10 -52
  10. package/src/InputField/Number/Number.browser.test.tsx +109 -0
  11. package/src/InputField/Select/Autocomplete/Connected/Multi/MultiConnectedAutocompleteField.browser.test.tsx +250 -0
  12. package/src/Listing/Listing.browser.test.tsx +476 -0
  13. package/src/Module/Module.browser.test.tsx +145 -0
  14. package/src/Pagination/Pagination.browser.test.tsx +184 -0
  15. package/src/RichTextEditor/RichTextEditor.browser.test.tsx +567 -0
  16. package/src/RichTextEditor/plugins/FloatingLinkEditorPlugin.tsx +3 -1
  17. package/src/TimePeriods/ResolutionTimePeriod.browser.test.tsx +143 -0
  18. package/src/TimePeriods/TimePeriods.browser.test.tsx +468 -0
  19. package/src/Typography/FluidTypography/FluidTypography.browser.test.tsx +54 -0
  20. package/src/api/useMutationQuery/useMutationQuery.browser.test.tsx +154 -0
  21. package/src/components/CollapsibleItem/CollapsibleItem.browser.test.tsx +120 -0
  22. package/src/components/CopyCommand/CopyCommand.browser.test.tsx +287 -0
  23. package/src/components/CrudPage/CrudPage.browser.test.tsx +659 -0
  24. package/src/components/DataTable/DataTable.browser.test.tsx +257 -0
  25. package/src/components/Form/AccessRights/AccessRights.browser.test.tsx +527 -0
  26. package/src/components/ItemComposition/ItemComposition.browser.test.tsx +172 -0
  27. package/src/components/Layout/PageLayout.browser.test.tsx +114 -0
  28. package/src/components/Modal/ConfirmationModal/ConfirmationModal.browser.test.tsx +237 -0
  29. package/src/components/SortableEntriesList/SortableEntriesList.browser.test.tsx +981 -0
  30. package/src/components/Tabs/Tabs.browser.test.tsx +115 -0
  31. package/src/utils/useFullscreen/Fullscreen.browser.test.tsx +176 -0
  32. package/src/utils/useLocale/useLocale.browser.test.tsx +49 -0
  33. package/src/utils/useLocaleTimezoneDate/{LocaleTimezoneDate.cypress.spec.tsx → LocaleTimezoneDate.browser.test.tsx} +24 -11
  34. package/src/Dashboard/Dashboard.cypress.spec.tsx +0 -71
  35. package/src/Form/Form.cypress.spec.tsx +0 -306
  36. package/src/Form/Inputs/ExclusiveCheckboxGroup.cypress.spec.tsx +0 -236
  37. package/src/Form/Inputs/SegmentedButtons.cypress.spec.tsx +0 -101
  38. package/src/InputField/Number/Number.cypress.spec.tsx +0 -89
  39. package/src/InputField/Select/Autocomplete/Connected/Multi/MultiConnectedAutocompleteField.cypress.spec.tsx +0 -182
  40. package/src/Listing/Listing.cypress.spec.tsx +0 -349
  41. package/src/Module/Module.cypress.spec.tsx +0 -127
  42. package/src/Pagination/Pagination.cypress.spec.tsx +0 -140
  43. package/src/RichTextEditor/RichTextEditor.cypress.spec.tsx +0 -408
  44. package/src/TimePeriods/ResolutionTimePeriod.cypress.spec.tsx +0 -74
  45. package/src/TimePeriods/TimePeriods.cypress.spec.tsx +0 -488
  46. package/src/Typography/FluidTypography/FluidTypography.cypress.spec.tsx +0 -27
  47. package/src/api/useMutationQuery/useMutationQuery.cypress.spec.tsx +0 -93
  48. package/src/components/CollapsibleItem/CollapsibleItem.cypress.spec.tsx +0 -76
  49. package/src/components/CopyCommand/CopyCommand.cypress.spec.tsx +0 -167
  50. package/src/components/CrudPage/CrudPage.cypress.spec.tsx +0 -561
  51. package/src/components/DataTable/DataTable.cypress.spec.tsx +0 -174
  52. package/src/components/Form/AccessRights/AccessRights.cypress.spec.tsx +0 -376
  53. package/src/components/ItemComposition/ItemComposition.cypress.spec.tsx +0 -112
  54. package/src/components/Layout/PageLayout.cypress.spec.tsx +0 -64
  55. package/src/components/Modal/ConfirmationModal/ConfirmationModal.cypress.spec.tsx +0 -171
  56. package/src/components/SortableEntriesList/SortableEntriesList.cypress.spec.tsx +0 -792
  57. package/src/components/Tabs/Tabs.cypress.spec.tsx +0 -69
  58. package/src/utils/useFullscreen/Fullscreen.cypress.spec.tsx +0 -130
  59. package/src/utils/useLocale/useLocale.cypress.spec.tsx +0 -40
@@ -0,0 +1,553 @@
1
+ import { Typography } from '@mui/material';
2
+
3
+ import { ThemeMode, userAtom } from '@centreon/ui-context';
4
+
5
+ import { faker } from '@faker-js/faker';
6
+ import { rs } from '@rstest/core';
7
+ import { render } from '@test-setup/render';
8
+ import { screen, waitFor, within } from '@testing-library/react';
9
+ import userEvent from '@testing-library/user-event';
10
+ import { useFormikContext } from 'formik';
11
+ import { createStore, Provider } from 'jotai';
12
+ import { object } from 'yup';
13
+
14
+ import type { SelectEntry } from '..';
15
+ import { Button } from '../components';
16
+ import ThemeProvider from '../ThemeProvider';
17
+ import { Form } from './Form';
18
+ import { type InputProps, InputType } from './Inputs/models';
19
+
20
+ faker.seed(42);
21
+
22
+ interface Item {
23
+ alias: string;
24
+ id: number;
25
+ name: string;
26
+ }
27
+
28
+ interface ListValues {
29
+ list: Array<Item>;
30
+ }
31
+
32
+ const AddItem = ({
33
+ addItem
34
+ }: {
35
+ addItem: (item: SelectEntry) => void;
36
+ }): JSX.Element => {
37
+ const { values } = useFormikContext<ListValues>();
38
+ const add = (): void => {
39
+ addItem({
40
+ alias: faker.company.name(),
41
+ id: values.list.length,
42
+ name: faker.person.firstName()
43
+ } as SelectEntry);
44
+ };
45
+
46
+ return (
47
+ <Button onClick={add} variant="ghost">
48
+ Add item
49
+ </Button>
50
+ );
51
+ };
52
+
53
+ const SortContent = ({
54
+ name,
55
+ alias
56
+ }: {
57
+ alias: string;
58
+ name: string;
59
+ }): JSX.Element => (
60
+ <Typography>
61
+ {name} ({alias})
62
+ </Typography>
63
+ );
64
+
65
+ const initializeFormList = (): void => {
66
+ render(
67
+ <Form
68
+ initialValues={{
69
+ list: []
70
+ }}
71
+ inputs={[
72
+ {
73
+ fieldName: 'list',
74
+ group: '',
75
+ label: '',
76
+ list: {
77
+ AddItem,
78
+ addItemLabel: 'Add an item to the list',
79
+ itemProps: ['id', 'name', 'alias'],
80
+ SortContent: SortContent as React.ComponentType<object>,
81
+ sortLabel: 'Sort items'
82
+ },
83
+ type: InputType.List
84
+ }
85
+ ]}
86
+ submit={rs.fn()}
87
+ validationSchema={object()}
88
+ />
89
+ );
90
+ };
91
+
92
+ // A keyboard drag of the handle: space to pick the item up, an arrow to move
93
+ // it one place, space to drop it. dnd-kit handles a key once the previous one
94
+ // took effect, so each step is awaited: the item pressed once picked up, the
95
+ // other item making way after the arrow, no item pressed once dropped.
96
+ const moveSortableElementUsingAriaLabel = async ({
97
+ ariaLabel,
98
+ direction
99
+ }: {
100
+ ariaLabel: string;
101
+ direction: 'down' | 'up';
102
+ }): Promise<void> => {
103
+ const handle = screen.getByLabelText(ariaLabel);
104
+ const item = handle.closest('[aria-roledescription]') as HTMLElement;
105
+ const isShifted = (element: Element): boolean =>
106
+ /translate3d\(0px, -?[1-9]/.test((element as HTMLElement).style.transform);
107
+
108
+ handle.focus();
109
+ await userEvent.keyboard(' ');
110
+ await waitFor(() => {
111
+ expect(item).toHaveAttribute('aria-pressed', 'true');
112
+ });
113
+
114
+ await userEvent.keyboard(direction === 'down' ? '{ArrowDown}' : '{ArrowUp}');
115
+ await waitFor(() => {
116
+ expect(
117
+ Array.from(document.querySelectorAll('[aria-roledescription]')).some(
118
+ (other) => other !== item && isShifted(other)
119
+ )
120
+ ).toBe(true);
121
+ });
122
+
123
+ await userEvent.keyboard(' ');
124
+ await waitFor(() => {
125
+ expect(document.querySelector('[aria-pressed="true"]')).toBeNull();
126
+ });
127
+ };
128
+
129
+ const getItemTexts = (): Array<string | null> =>
130
+ Array.from(document.querySelectorAll('[aria-label^="sort-"]')).map(
131
+ (handle) => handle.parentElement?.textContent ?? null
132
+ );
133
+
134
+ const getSaveButton = (): HTMLElement => screen.getByLabelText('save button');
135
+
136
+ const expectFormButtons = (): void => {
137
+ expect(screen.getByRole('button', { name: 'Reset' })).toBeVisible();
138
+ expect(getSaveButton()).toBeVisible();
139
+ expect(getSaveButton()).toHaveTextContent('Save');
140
+ };
141
+
142
+ describe('Form list', () => {
143
+ beforeEach(() => {
144
+ expect.hasAssertions();
145
+ });
146
+ beforeEach(initializeFormList);
147
+
148
+ it('adds an element to the list', async () => {
149
+ expect(await screen.findByText('Add an item to the list')).toBeVisible();
150
+ expect(screen.getByText('Sort items')).toBeVisible();
151
+
152
+ await userEvent.click(screen.getByText('Add item'));
153
+
154
+ expect(await screen.findByLabelText('sort-0')).toBeVisible();
155
+ expect(screen.getByLabelText('delete-0')).toBeVisible();
156
+ expect(await screen.findByText('Lavinia (Wiegand LLC)')).toBeVisible();
157
+
158
+ expect(getItemTexts()).toEqual(['Lavinia (Wiegand LLC)']);
159
+ expectFormButtons();
160
+ await waitFor(() => {
161
+ expect(getSaveButton()).toBeEnabled();
162
+ });
163
+ });
164
+
165
+ it('sorts elements in the list', async () => {
166
+ expect(await screen.findByText('Add an item to the list')).toBeVisible();
167
+ expect(screen.getByText('Sort items')).toBeVisible();
168
+
169
+ await userEvent.click(screen.getByText('Add item'));
170
+ await userEvent.click(screen.getByText('Add item'));
171
+
172
+ expect(await screen.findByLabelText('sort-0')).toBeVisible();
173
+ expect(screen.getByLabelText('delete-0')).toBeVisible();
174
+ expect(await screen.findByText('Sammie (Crist - Beer)')).toBeVisible();
175
+ expect(await screen.findByLabelText('sort-1')).toBeVisible();
176
+ expect(screen.getByLabelText('delete-1')).toBeVisible();
177
+ expect(await screen.findByText('Waino (Quigley Group)')).toBeVisible();
178
+
179
+ expect(getItemTexts()).toEqual([
180
+ 'Sammie (Crist - Beer)',
181
+ 'Waino (Quigley Group)'
182
+ ]);
183
+
184
+ await moveSortableElementUsingAriaLabel({
185
+ ariaLabel: 'sort-0',
186
+ direction: 'down'
187
+ });
188
+
189
+ expect(await screen.findByText('Waino (Quigley Group)')).toBeVisible();
190
+ expect(screen.getAllByText('Sammie (Crist - Beer)')[0]).toBeVisible();
191
+
192
+ // The moved item is below the other one.
193
+ await waitFor(() => {
194
+ expect(getItemTexts()).toEqual([
195
+ 'Waino (Quigley Group)',
196
+ 'Sammie (Crist - Beer)'
197
+ ]);
198
+ });
199
+ expectFormButtons();
200
+ });
201
+
202
+ it('removes an element from the list', async () => {
203
+ expect(await screen.findByText('Add an item to the list')).toBeVisible();
204
+ expect(screen.getByText('Sort items')).toBeVisible();
205
+
206
+ await userEvent.click(screen.getByText('Add item'));
207
+ await userEvent.click(screen.getByText('Add item'));
208
+
209
+ expect(await screen.findByLabelText('sort-0')).toBeVisible();
210
+ expect(screen.getByLabelText('delete-0')).toBeVisible();
211
+ expect(
212
+ await screen.findByText('Elliott (Effertz, Deckow and Deckow)')
213
+ ).toBeVisible();
214
+ expect(await screen.findByLabelText('sort-1')).toBeVisible();
215
+ expect(screen.getByLabelText('delete-1')).toBeVisible();
216
+ expect(await screen.findByText('Leopoldo (Kemmer Inc)')).toBeVisible();
217
+
218
+ await userEvent.click(screen.getByLabelText('delete-0'));
219
+
220
+ await waitFor(() => {
221
+ expect(
222
+ screen.queryByText('Elliott (Effertz, Deckow and Deckow)')
223
+ ).not.toBeInTheDocument();
224
+ });
225
+
226
+ expect(getItemTexts()).toEqual(['Leopoldo (Kemmer Inc)']);
227
+ expectFormButtons();
228
+ });
229
+ });
230
+
231
+ // The rstest viewport (1000px tall) fits the whole form: the spacer keeps the
232
+ // page scrollable for the scroll-to-section test.
233
+ const initializeFormWithSections = (): void => {
234
+ render(
235
+ <>
236
+ <Form
237
+ groups={[
238
+ {
239
+ name: 'First group',
240
+ order: 1
241
+ },
242
+ {
243
+ name: 'Third group',
244
+ order: 3
245
+ },
246
+ {
247
+ name: 'Second group',
248
+ order: 2
249
+ },
250
+ {
251
+ name: 'Fourth group',
252
+ order: 4
253
+ }
254
+ ]}
255
+ initialValues={{
256
+ list: []
257
+ }}
258
+ inputs={[
259
+ {
260
+ fieldName: 'First name',
261
+ group: 'First group',
262
+ label: 'Name',
263
+ type: InputType.Text
264
+ },
265
+ {
266
+ fieldName: 'Divider',
267
+ group: 'First group',
268
+ label: 'Divider',
269
+ type: InputType.Divider
270
+ },
271
+ {
272
+ fieldName: 'Second name',
273
+ group: 'First group',
274
+ label: 'Name',
275
+ type: InputType.Text
276
+ },
277
+ {
278
+ fieldName: 'Third name',
279
+ group: 'First group',
280
+ label: 'Name',
281
+ type: InputType.Text
282
+ },
283
+ {
284
+ fieldName: 'Fourth name',
285
+ group: 'First group',
286
+ label: 'Name',
287
+ type: InputType.Text
288
+ },
289
+ {
290
+ fieldName: 'Fifth name',
291
+ group: 'First group',
292
+ label: 'Name',
293
+ type: InputType.Text
294
+ },
295
+ {
296
+ fieldName: 'Sixth name',
297
+ group: 'First group',
298
+ label: 'Name',
299
+ type: InputType.Text
300
+ },
301
+ {
302
+ fieldName: 'Seventh name',
303
+ group: 'First group',
304
+ label: 'Name',
305
+ type: InputType.Text
306
+ },
307
+ {
308
+ fieldName: 'Eighth name',
309
+ group: 'First group',
310
+ label: 'Name',
311
+ type: InputType.Text
312
+ },
313
+ {
314
+ fieldName: 'Ninth name',
315
+ group: 'First group',
316
+ label: 'Name',
317
+ type: InputType.Text
318
+ },
319
+ {
320
+ fieldName: 'First second group name',
321
+ group: 'Second group',
322
+ label: 'Name',
323
+ type: InputType.Text
324
+ },
325
+ {
326
+ fieldName: 'First third group name',
327
+ group: 'Third group',
328
+ label: 'Name',
329
+ type: InputType.Text
330
+ },
331
+ {
332
+ fieldName: 'First fourth group name',
333
+ group: 'Fourth group',
334
+ label: 'Name',
335
+ type: InputType.Text
336
+ }
337
+ ]}
338
+ isCollapsible
339
+ submit={rs.fn()}
340
+ validationSchema={object()}
341
+ />
342
+ <div style={{ height: 1000 }} />
343
+ </>
344
+ );
345
+ };
346
+
347
+ const initializeFile = (): void => {
348
+ render(
349
+ <Form
350
+ initialValues={{
351
+ list: []
352
+ }}
353
+ inputs={[
354
+ {
355
+ fieldName: 'list',
356
+ // No custom drop zone content: the default one is shown.
357
+ file: {
358
+ accept: '.json'
359
+ } as InputProps['file'],
360
+ group: '',
361
+ label: 'json',
362
+ type: InputType.File
363
+ }
364
+ ]}
365
+ submit={rs.fn()}
366
+ validationSchema={object()}
367
+ />
368
+ );
369
+ };
370
+
371
+ describe('File', () => {
372
+ beforeEach(() => {
373
+ expect.hasAssertions();
374
+ });
375
+
376
+ it('uploads a file when a file is selected', async () => {
377
+ initializeFile();
378
+
379
+ expect(await screen.findByText('Drop or select a file')).toBeVisible();
380
+ await userEvent.upload(
381
+ screen.getByLabelText('select a file'),
382
+ new File(['{}'], 'package.json', { type: 'application/json' })
383
+ );
384
+ expect(await screen.findByText('package.json')).toBeVisible();
385
+
386
+ // The field label above the drop zone, and the selected
387
+ // file listed with a file icon.
388
+ expect(screen.getByRole('heading', { name: 'json' })).toBeVisible();
389
+ expect(
390
+ within(
391
+ screen.getByText('package.json').parentElement as HTMLElement
392
+ ).getByTestId('DescriptionOutlinedIcon')
393
+ ).toBeVisible();
394
+ });
395
+ });
396
+
397
+ describe('Form with sections', () => {
398
+ beforeEach(() => {
399
+ expect.hasAssertions();
400
+ });
401
+ beforeEach(initializeFormWithSections);
402
+
403
+ it('displays sections when correct amount of sections', async () => {
404
+ expect((await screen.findAllByText('First group'))[0]).toBeVisible();
405
+ expect(screen.getAllByText('Second group')[0]).toBeVisible();
406
+ expect(screen.getAllByText('Third group')[0]).toBeVisible();
407
+ expect(screen.getAllByText('Fourth group')[0]).toBeVisible();
408
+
409
+ // The section tabs sorted by group order, the first one
410
+ // selected, and the first group expanded with its nine fields.
411
+ expect(screen.getAllByRole('tab').map((tab) => tab.textContent)).toEqual([
412
+ 'First group',
413
+ 'Second group',
414
+ 'Third group',
415
+ 'Fourth group'
416
+ ]);
417
+ expect(screen.getByRole('tab', { name: 'First group' })).toHaveAttribute(
418
+ 'aria-selected',
419
+ 'true'
420
+ );
421
+ expect(
422
+ screen
423
+ .getAllByLabelText('Name')
424
+ .filter((input) => input.tagName === 'INPUT')
425
+ .filter(
426
+ (input) =>
427
+ input.checkVisibility() &&
428
+ getComputedStyle(input).visibility !== 'hidden'
429
+ )
430
+ ).toHaveLength(9);
431
+ });
432
+
433
+ it('scrolls correctly to section', async () => {
434
+ const initialScrollY = window.scrollY;
435
+
436
+ await userEvent.click(
437
+ await screen.findByRole('tab', { name: 'Third group' })
438
+ );
439
+
440
+ await waitFor(() => {
441
+ expect(window.scrollY).toBeGreaterThan(initialScrollY);
442
+ });
443
+
444
+ expect(screen.getByRole('tab', { name: 'Third group' })).toHaveAttribute(
445
+ 'aria-selected',
446
+ 'true'
447
+ );
448
+ });
449
+ });
450
+
451
+ // The theme mode comes from the user atom, and the Tailwind tokens from the
452
+ // `dark` class on the document.
453
+ const initializeFormTitles = (themeMode: ThemeMode): void => {
454
+ const store = createStore();
455
+ store.set(userAtom, { ...store.get(userAtom), themeMode });
456
+
457
+ document.documentElement.classList.toggle(
458
+ 'dark',
459
+ themeMode === ThemeMode.dark
460
+ );
461
+
462
+ render(
463
+ <Provider store={store}>
464
+ <ThemeProvider>
465
+ <Form
466
+ areGroupsOpen
467
+ groups={[{ name: 'Titles section', order: 1 }]}
468
+ initialValues={{ gridField: '', topLevelField: '' }}
469
+ inputs={[
470
+ {
471
+ additionalLabel: 'Top level block title',
472
+ fieldName: 'topLevelField',
473
+ group: 'Titles section',
474
+ label: 'Top level field',
475
+ type: InputType.Text
476
+ },
477
+ {
478
+ fieldName: '',
479
+ grid: {
480
+ columns: [
481
+ {
482
+ additionalLabel: 'Grid block title',
483
+ fieldName: 'gridField',
484
+ label: 'Grid field',
485
+ type: InputType.Text
486
+ }
487
+ ]
488
+ },
489
+ group: 'Titles section',
490
+ label: 'Grid',
491
+ type: InputType.Grid
492
+ }
493
+ ]}
494
+ isCollapsible
495
+ submit={rs.fn()}
496
+ validationSchema={object()}
497
+ />
498
+ </ThemeProvider>
499
+ </Provider>
500
+ );
501
+ };
502
+
503
+ const textPrimaryColor = {
504
+ [ThemeMode.dark]: 'rgb(255, 255, 255)',
505
+ [ThemeMode.light]: 'rgb(0, 0, 0)'
506
+ };
507
+
508
+ [ThemeMode.light, ThemeMode.dark].forEach((themeMode) => {
509
+ describe(`Form titles in ${themeMode} mode`, () => {
510
+ beforeEach(() => {
511
+ expect.hasAssertions();
512
+ });
513
+ beforeEach(() => initializeFormTitles(themeMode));
514
+
515
+ afterEach(() => {
516
+ document.documentElement.classList.remove('dark');
517
+ });
518
+
519
+ it('displays the section title in 16px medium on a 24px line', async () => {
520
+ const title = await screen.findByText('Titles section');
521
+
522
+ expect(title).toHaveStyle({
523
+ fontSize: '16px',
524
+ fontWeight: '500',
525
+ lineHeight: '24px'
526
+ });
527
+ });
528
+
529
+ it('rounds the collapsible section header with an 8px radius', async () => {
530
+ expect(await screen.findByLabelText('Titles section')).toHaveStyle({
531
+ borderRadius: '8px'
532
+ });
533
+ });
534
+
535
+ it('displays the grid block title in 16px bold with the text colour', async () => {
536
+ expect(await screen.findByText('Grid block title')).toHaveStyle({
537
+ color: textPrimaryColor[themeMode],
538
+ fontSize: '16px',
539
+ fontWeight: '700',
540
+ lineHeight: '24px'
541
+ });
542
+ });
543
+
544
+ it('displays the top level block title like the grid block title', async () => {
545
+ expect(await screen.findByText('Top level block title')).toHaveStyle({
546
+ color: textPrimaryColor[themeMode],
547
+ fontSize: '16px',
548
+ fontWeight: '700',
549
+ lineHeight: '24px'
550
+ });
551
+ });
552
+ });
553
+ });