@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
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@centreon/ui",
3
- "version": "26.12.0",
3
+ "version": "26.12.2",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "update:deps": "pnpx npm-check-updates -i --format group",
7
- "type-check": "tsc --project tsconfig.typecheck.json",
7
+ "type-check": "tsc --project tsconfig.typecheck.json && tsc --project tsconfig.rstest.json",
8
8
  "type-check:staged": "node typecheck-staged.mjs",
9
9
  "lint": "biome check ./src/ --error-on-warnings",
10
10
  "lint:fix": "pnpm lint --fix",
@@ -21,7 +21,10 @@
21
21
  "cypress:install": "cypress install",
22
22
  "cypress:cli": "./cypress/scripts/cypress-cli.sh",
23
23
  "tokens:transform": "TS_NODE_PROJECT=tsconfig.node.json ts-node style-dictionary.transform.ts",
24
- "install:arm:binaries": "pnpm i -D @swc/core-linux-arm64-gnu@1.6.6 @rspack/binding-linux-arm64-gnu@1.1.6 -w && pnpm remove @swc/core-linux-arm64-gnu @rspack/binding-linux-arm64-gnu"
24
+ "install:arm:binaries": "pnpm i -D @swc/core-linux-arm64-gnu@1.6.6 @rspack/binding-linux-arm64-gnu@1.1.6 -w && pnpm remove @swc/core-linux-arm64-gnu @rspack/binding-linux-arm64-gnu",
25
+ "rstest:run": "TZ=Etc/UTC rstest run",
26
+ "rstest:ui": "TZ=Etc/UTC rstest run --browser.headless=false --pool.maxWorkers=1",
27
+ "rstest:install": "playwright install --with-deps --only-shell chromium"
25
28
  },
26
29
  "type": "module",
27
30
  "sideEffects": false,
@@ -8,8 +8,8 @@
8
8
  * - Please do NOT serve this file on production.
9
9
  */
10
10
 
11
- const PACKAGE_VERSION = '2.2.14'
12
- const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423'
11
+ const PACKAGE_VERSION = '2.7.3'
12
+ const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f'
13
13
  const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
14
14
  const activeClientIds = new Set()
15
15
 
@@ -62,7 +62,12 @@ self.addEventListener('message', async function (event) {
62
62
 
63
63
  sendToClient(client, {
64
64
  type: 'MOCKING_ENABLED',
65
- payload: true,
65
+ payload: {
66
+ client: {
67
+ id: client.id,
68
+ frameType: client.frameType,
69
+ },
70
+ },
66
71
  })
67
72
  break
68
73
  }
@@ -155,6 +160,10 @@ async function handleRequest(event, requestId) {
155
160
  async function resolveMainClient(event) {
156
161
  const client = await self.clients.get(event.clientId)
157
162
 
163
+ if (activeClientIds.has(event.clientId)) {
164
+ return client
165
+ }
166
+
158
167
  if (client?.frameType === 'top-level') {
159
168
  return client
160
169
  }
@@ -183,12 +192,26 @@ async function getResponse(event, client, requestId) {
183
192
  const requestClone = request.clone()
184
193
 
185
194
  function passthrough() {
186
- const headers = Object.fromEntries(requestClone.headers.entries())
195
+ // Cast the request headers to a new Headers instance
196
+ // so the headers can be manipulated with.
197
+ const headers = new Headers(requestClone.headers)
198
+
199
+ // Remove the "accept" header value that marked this request as passthrough.
200
+ // This prevents request alteration and also keeps it compliant with the
201
+ // user-defined CORS policies.
202
+ const acceptHeader = headers.get('accept')
203
+ if (acceptHeader) {
204
+ const values = acceptHeader.split(',').map((value) => value.trim())
205
+ const filteredValues = values.filter(
206
+ (value) => value !== 'msw/passthrough',
207
+ )
187
208
 
188
- // Remove internal MSW request header so the passthrough request
189
- // complies with any potential CORS preflight checks on the server.
190
- // Some servers forbid unknown request headers.
191
- delete headers['x-msw-intention']
209
+ if (filteredValues.length > 0) {
210
+ headers.set('accept', filteredValues.join(', '))
211
+ } else {
212
+ headers.delete('accept')
213
+ }
214
+ }
192
215
 
193
216
  return fetch(requestClone, { headers })
194
217
  }
@@ -0,0 +1,107 @@
1
+ import { Typography } from '@mui/material';
2
+
3
+ import { render } from '@test-setup/render';
4
+ import { screen, waitFor } from '@testing-library/react';
5
+ import type { LayoutItem } from 'react-grid-layout';
6
+
7
+ import { DashboardLayout } from '.';
8
+
9
+ interface CustomLayout extends LayoutItem {
10
+ content: string;
11
+ shouldUseFluidTypography?: boolean;
12
+ }
13
+
14
+ const dashboardLayout: Array<CustomLayout> = [
15
+ {
16
+ content: 'Panel 1',
17
+ h: 4,
18
+ i: 'a',
19
+ shouldUseFluidTypography: false,
20
+ w: 6,
21
+ x: 0,
22
+ y: 0
23
+ },
24
+ {
25
+ content: 'Panel 2',
26
+ h: 3,
27
+ i: 'b',
28
+ minW: 2,
29
+ w: 7,
30
+ x: 1,
31
+ y: 7
32
+ },
33
+ {
34
+ content: 'Panel 3',
35
+ h: 7,
36
+ i: 'c',
37
+ w: 6,
38
+ x: 6,
39
+ y: 6
40
+ },
41
+ {
42
+ content: 'Panel 4',
43
+ h: 3,
44
+ i: 'd',
45
+ minW: 2,
46
+ w: 5,
47
+ x: 4,
48
+ y: 10
49
+ }
50
+ ];
51
+
52
+ const initialize = (): void => {
53
+ render(
54
+ <DashboardLayout.Layout layout={dashboardLayout}>
55
+ {dashboardLayout.map(({ i, content }) => (
56
+ <DashboardLayout.Item header={<div>header</div>} id={i} key={i}>
57
+ {[<Typography key="content">{content}</Typography>]}
58
+ </DashboardLayout.Item>
59
+ ))}
60
+ </DashboardLayout.Layout>
61
+ );
62
+ };
63
+
64
+ const panelBox = (content: string): DOMRect =>
65
+ (
66
+ screen.getByText(content).closest('.react-grid-item') as HTMLElement
67
+ ).getBoundingClientRect();
68
+
69
+ describe('Dashboard', () => {
70
+ beforeEach(() => {
71
+ expect.hasAssertions();
72
+ });
73
+
74
+ it('displays placeholder when panels is out of viewport', async () => {
75
+ initialize();
76
+
77
+ // The four panels render with their header. Panels 1 and 3 share the
78
+ // first row (Panel 3 taller), Panel 2 sits below Panel 3 starting within
79
+ // Panel 1's width, and Panel 4 is below Panel 2.
80
+ expect(await screen.findByText('Panel 1')).toBeVisible();
81
+ expect(screen.getByText('Panel 2')).toBeVisible();
82
+ expect(screen.getByText('Panel 3')).toBeVisible();
83
+ expect(screen.getByText('Panel 4')).toBeInTheDocument();
84
+ expect(screen.getAllByText('header')).toHaveLength(4);
85
+
86
+ await waitFor(() => {
87
+ const panel1 = panelBox('Panel 1');
88
+ const panel3 = panelBox('Panel 3');
89
+
90
+ expect(panel1.width).toBeGreaterThan(0);
91
+ expect(panel3.top).toBe(panel1.top);
92
+ expect(panel3.left).toBeGreaterThan(panel1.right);
93
+ expect(panel3.width).toBe(panel1.width);
94
+ expect(panel3.height).toBeGreaterThan(panel1.height);
95
+ });
96
+
97
+ const panel1 = panelBox('Panel 1');
98
+ const panel2 = panelBox('Panel 2');
99
+ const panel3 = panelBox('Panel 3');
100
+ const panel4 = panelBox('Panel 4');
101
+
102
+ expect(panel2.top).toBeGreaterThan(panel3.bottom);
103
+ expect(panel2.left).toBeGreaterThan(panel1.left);
104
+ expect(panel2.left).toBeLessThan(panel1.right);
105
+ expect(panel4.top).toBeGreaterThan(panel2.bottom);
106
+ });
107
+ });
@@ -0,0 +1,445 @@
1
+ import { Typography } from '@mui/material';
2
+
3
+ import { faker } from '@faker-js/faker';
4
+ import { rs } from '@rstest/core';
5
+ import { render } from '@test-setup/render';
6
+ import { screen, waitFor, within } from '@testing-library/react';
7
+ import userEvent from '@testing-library/user-event';
8
+ import { useFormikContext } from 'formik';
9
+ import { object } from 'yup';
10
+
11
+ import type { SelectEntry } from '..';
12
+ import { Button } from '../components';
13
+ import { Form } from './Form';
14
+ import { type InputProps, InputType } from './Inputs/models';
15
+
16
+ faker.seed(42);
17
+
18
+ interface Item {
19
+ alias: string;
20
+ id: number;
21
+ name: string;
22
+ }
23
+
24
+ interface ListValues {
25
+ list: Array<Item>;
26
+ }
27
+
28
+ const AddItem = ({
29
+ addItem
30
+ }: {
31
+ addItem: (item: SelectEntry) => void;
32
+ }): JSX.Element => {
33
+ const { values } = useFormikContext<ListValues>();
34
+ const add = (): void => {
35
+ addItem({
36
+ alias: faker.company.name(),
37
+ id: values.list.length,
38
+ name: faker.person.firstName()
39
+ } as SelectEntry);
40
+ };
41
+
42
+ return (
43
+ <Button onClick={add} variant="ghost">
44
+ Add item
45
+ </Button>
46
+ );
47
+ };
48
+
49
+ const SortContent = ({
50
+ name,
51
+ alias
52
+ }: {
53
+ alias: string;
54
+ name: string;
55
+ }): JSX.Element => (
56
+ <Typography>
57
+ {name} ({alias})
58
+ </Typography>
59
+ );
60
+
61
+ const initializeFormList = (): void => {
62
+ render(
63
+ <Form
64
+ initialValues={{
65
+ list: []
66
+ }}
67
+ inputs={[
68
+ {
69
+ fieldName: 'list',
70
+ group: '',
71
+ label: '',
72
+ list: {
73
+ AddItem,
74
+ addItemLabel: 'Add an item to the list',
75
+ itemProps: ['id', 'name', 'alias'],
76
+ SortContent: SortContent as React.ComponentType<object>,
77
+ sortLabel: 'Sort items'
78
+ },
79
+ type: InputType.List
80
+ }
81
+ ]}
82
+ submit={rs.fn()}
83
+ validationSchema={object()}
84
+ />
85
+ );
86
+ };
87
+
88
+ // A keyboard drag of the handle: space to pick the item up, an arrow to move
89
+ // it one place, space to drop it. dnd-kit handles a key once the previous one
90
+ // took effect, so each step is awaited: the item pressed once picked up, the
91
+ // other item making way after the arrow, no item pressed once dropped.
92
+ const moveSortableElementUsingAriaLabel = async ({
93
+ ariaLabel,
94
+ direction
95
+ }: {
96
+ ariaLabel: string;
97
+ direction: 'down' | 'up';
98
+ }): Promise<void> => {
99
+ const handle = screen.getByLabelText(ariaLabel);
100
+ const item = handle.closest('[aria-roledescription]') as HTMLElement;
101
+ const isShifted = (element: Element): boolean =>
102
+ /translate3d\(0px, -?[1-9]/.test((element as HTMLElement).style.transform);
103
+
104
+ handle.focus();
105
+ await userEvent.keyboard(' ');
106
+ await waitFor(() => {
107
+ expect(item).toHaveAttribute('aria-pressed', 'true');
108
+ });
109
+
110
+ await userEvent.keyboard(direction === 'down' ? '{ArrowDown}' : '{ArrowUp}');
111
+ await waitFor(() => {
112
+ expect(
113
+ Array.from(document.querySelectorAll('[aria-roledescription]')).some(
114
+ (other) => other !== item && isShifted(other)
115
+ )
116
+ ).toBe(true);
117
+ });
118
+
119
+ await userEvent.keyboard(' ');
120
+ await waitFor(() => {
121
+ expect(document.querySelector('[aria-pressed="true"]')).toBeNull();
122
+ });
123
+ };
124
+
125
+ const getItemTexts = (): Array<string | null> =>
126
+ Array.from(document.querySelectorAll('[aria-label^="sort-"]')).map(
127
+ (handle) => handle.parentElement?.textContent ?? null
128
+ );
129
+
130
+ const getSaveButton = (): HTMLElement => screen.getByLabelText('save button');
131
+
132
+ const expectFormButtons = (): void => {
133
+ expect(screen.getByRole('button', { name: 'Reset' })).toBeVisible();
134
+ expect(getSaveButton()).toBeVisible();
135
+ expect(getSaveButton()).toHaveTextContent('Save');
136
+ };
137
+
138
+ describe('Form list', () => {
139
+ beforeEach(() => {
140
+ expect.hasAssertions();
141
+ });
142
+ beforeEach(initializeFormList);
143
+
144
+ it('adds an element to the list', async () => {
145
+ expect(await screen.findByText('Add an item to the list')).toBeVisible();
146
+ expect(screen.getByText('Sort items')).toBeVisible();
147
+
148
+ await userEvent.click(screen.getByText('Add item'));
149
+
150
+ expect(await screen.findByLabelText('sort-0')).toBeVisible();
151
+ expect(screen.getByLabelText('delete-0')).toBeVisible();
152
+ expect(await screen.findByText('Lavinia (Wiegand LLC)')).toBeVisible();
153
+
154
+ expect(getItemTexts()).toEqual(['Lavinia (Wiegand LLC)']);
155
+ expectFormButtons();
156
+ await waitFor(() => {
157
+ expect(getSaveButton()).toBeEnabled();
158
+ });
159
+ });
160
+
161
+ it('sorts elements in the list', async () => {
162
+ expect(await screen.findByText('Add an item to the list')).toBeVisible();
163
+ expect(screen.getByText('Sort items')).toBeVisible();
164
+
165
+ await userEvent.click(screen.getByText('Add item'));
166
+ await userEvent.click(screen.getByText('Add item'));
167
+
168
+ expect(await screen.findByLabelText('sort-0')).toBeVisible();
169
+ expect(screen.getByLabelText('delete-0')).toBeVisible();
170
+ expect(await screen.findByText('Sammie (Crist - Beer)')).toBeVisible();
171
+ expect(await screen.findByLabelText('sort-1')).toBeVisible();
172
+ expect(screen.getByLabelText('delete-1')).toBeVisible();
173
+ expect(await screen.findByText('Waino (Quigley Group)')).toBeVisible();
174
+
175
+ expect(getItemTexts()).toEqual([
176
+ 'Sammie (Crist - Beer)',
177
+ 'Waino (Quigley Group)'
178
+ ]);
179
+
180
+ await moveSortableElementUsingAriaLabel({
181
+ ariaLabel: 'sort-0',
182
+ direction: 'down'
183
+ });
184
+
185
+ expect(await screen.findByText('Waino (Quigley Group)')).toBeVisible();
186
+ expect(screen.getAllByText('Sammie (Crist - Beer)')[0]).toBeVisible();
187
+
188
+ // The moved item is below the other one.
189
+ await waitFor(() => {
190
+ expect(getItemTexts()).toEqual([
191
+ 'Waino (Quigley Group)',
192
+ 'Sammie (Crist - Beer)'
193
+ ]);
194
+ });
195
+ expectFormButtons();
196
+ });
197
+
198
+ it('removes an element from the list', async () => {
199
+ expect(await screen.findByText('Add an item to the list')).toBeVisible();
200
+ expect(screen.getByText('Sort items')).toBeVisible();
201
+
202
+ await userEvent.click(screen.getByText('Add item'));
203
+ await userEvent.click(screen.getByText('Add item'));
204
+
205
+ expect(await screen.findByLabelText('sort-0')).toBeVisible();
206
+ expect(screen.getByLabelText('delete-0')).toBeVisible();
207
+ expect(
208
+ await screen.findByText('Elliott (Effertz, Deckow and Deckow)')
209
+ ).toBeVisible();
210
+ expect(await screen.findByLabelText('sort-1')).toBeVisible();
211
+ expect(screen.getByLabelText('delete-1')).toBeVisible();
212
+ expect(await screen.findByText('Leopoldo (Kemmer Inc)')).toBeVisible();
213
+
214
+ await userEvent.click(screen.getByLabelText('delete-0'));
215
+
216
+ await waitFor(() => {
217
+ expect(
218
+ screen.queryByText('Elliott (Effertz, Deckow and Deckow)')
219
+ ).not.toBeInTheDocument();
220
+ });
221
+
222
+ expect(getItemTexts()).toEqual(['Leopoldo (Kemmer Inc)']);
223
+ expectFormButtons();
224
+ });
225
+ });
226
+
227
+ // The rstest viewport (1000px tall) fits the whole form: the spacer keeps the
228
+ // page scrollable for the scroll-to-section test.
229
+ const initializeFormWithSections = (): void => {
230
+ render(
231
+ <>
232
+ <Form
233
+ groups={[
234
+ {
235
+ name: 'First group',
236
+ order: 1
237
+ },
238
+ {
239
+ name: 'Third group',
240
+ order: 3
241
+ },
242
+ {
243
+ name: 'Second group',
244
+ order: 2
245
+ },
246
+ {
247
+ name: 'Fourth group',
248
+ order: 4
249
+ }
250
+ ]}
251
+ initialValues={{
252
+ list: []
253
+ }}
254
+ inputs={[
255
+ {
256
+ fieldName: 'First name',
257
+ group: 'First group',
258
+ label: 'Name',
259
+ type: InputType.Text
260
+ },
261
+ {
262
+ fieldName: 'Divider',
263
+ group: 'First group',
264
+ label: 'Divider',
265
+ type: InputType.Divider
266
+ },
267
+ {
268
+ fieldName: 'Second name',
269
+ group: 'First group',
270
+ label: 'Name',
271
+ type: InputType.Text
272
+ },
273
+ {
274
+ fieldName: 'Third name',
275
+ group: 'First group',
276
+ label: 'Name',
277
+ type: InputType.Text
278
+ },
279
+ {
280
+ fieldName: 'Fourth name',
281
+ group: 'First group',
282
+ label: 'Name',
283
+ type: InputType.Text
284
+ },
285
+ {
286
+ fieldName: 'Fifth name',
287
+ group: 'First group',
288
+ label: 'Name',
289
+ type: InputType.Text
290
+ },
291
+ {
292
+ fieldName: 'Sixth name',
293
+ group: 'First group',
294
+ label: 'Name',
295
+ type: InputType.Text
296
+ },
297
+ {
298
+ fieldName: 'Seventh name',
299
+ group: 'First group',
300
+ label: 'Name',
301
+ type: InputType.Text
302
+ },
303
+ {
304
+ fieldName: 'Eighth name',
305
+ group: 'First group',
306
+ label: 'Name',
307
+ type: InputType.Text
308
+ },
309
+ {
310
+ fieldName: 'Ninth name',
311
+ group: 'First group',
312
+ label: 'Name',
313
+ type: InputType.Text
314
+ },
315
+ {
316
+ fieldName: 'First second group name',
317
+ group: 'Second group',
318
+ label: 'Name',
319
+ type: InputType.Text
320
+ },
321
+ {
322
+ fieldName: 'First third group name',
323
+ group: 'Third group',
324
+ label: 'Name',
325
+ type: InputType.Text
326
+ },
327
+ {
328
+ fieldName: 'First fourth group name',
329
+ group: 'Fourth group',
330
+ label: 'Name',
331
+ type: InputType.Text
332
+ }
333
+ ]}
334
+ isCollapsible
335
+ submit={rs.fn()}
336
+ validationSchema={object()}
337
+ />
338
+ <div style={{ height: 1000 }} />
339
+ </>
340
+ );
341
+ };
342
+
343
+ const initializeFile = (): void => {
344
+ render(
345
+ <Form
346
+ initialValues={{
347
+ list: []
348
+ }}
349
+ inputs={[
350
+ {
351
+ fieldName: 'list',
352
+ // No custom drop zone content: the default one is shown.
353
+ file: {
354
+ accept: '.json'
355
+ } as InputProps['file'],
356
+ group: '',
357
+ label: 'json',
358
+ type: InputType.File
359
+ }
360
+ ]}
361
+ submit={rs.fn()}
362
+ validationSchema={object()}
363
+ />
364
+ );
365
+ };
366
+
367
+ describe('File', () => {
368
+ beforeEach(() => {
369
+ expect.hasAssertions();
370
+ });
371
+
372
+ it('uploads a file when a file is selected', async () => {
373
+ initializeFile();
374
+
375
+ expect(await screen.findByText('Drop or select a file')).toBeVisible();
376
+ await userEvent.upload(
377
+ screen.getByLabelText('select a file'),
378
+ new File(['{}'], 'package.json', { type: 'application/json' })
379
+ );
380
+ expect(await screen.findByText('package.json')).toBeVisible();
381
+
382
+ // The field label above the drop zone, and the selected
383
+ // file listed with a file icon.
384
+ expect(screen.getByRole('heading', { name: 'json' })).toBeVisible();
385
+ expect(
386
+ within(
387
+ screen.getByText('package.json').parentElement as HTMLElement
388
+ ).getByTestId('DescriptionOutlinedIcon')
389
+ ).toBeVisible();
390
+ });
391
+ });
392
+
393
+ describe('Form with sections', () => {
394
+ beforeEach(() => {
395
+ expect.hasAssertions();
396
+ });
397
+ beforeEach(initializeFormWithSections);
398
+
399
+ it('displays sections when correct amount of sections', async () => {
400
+ expect((await screen.findAllByText('First group'))[0]).toBeVisible();
401
+ expect(screen.getAllByText('Second group')[0]).toBeVisible();
402
+ expect(screen.getAllByText('Third group')[0]).toBeVisible();
403
+ expect(screen.getAllByText('Fourth group')[0]).toBeVisible();
404
+
405
+ // The section tabs sorted by group order, the first one
406
+ // selected, and the first group expanded with its nine fields.
407
+ expect(screen.getAllByRole('tab').map((tab) => tab.textContent)).toEqual([
408
+ 'First group',
409
+ 'Second group',
410
+ 'Third group',
411
+ 'Fourth group'
412
+ ]);
413
+ expect(screen.getByRole('tab', { name: 'First group' })).toHaveAttribute(
414
+ 'aria-selected',
415
+ 'true'
416
+ );
417
+ expect(
418
+ screen
419
+ .getAllByLabelText('Name')
420
+ .filter((input) => input.tagName === 'INPUT')
421
+ .filter(
422
+ (input) =>
423
+ input.checkVisibility() &&
424
+ getComputedStyle(input).visibility !== 'hidden'
425
+ )
426
+ ).toHaveLength(9);
427
+ });
428
+
429
+ it('scrolls correctly to section', async () => {
430
+ const initialScrollY = window.scrollY;
431
+
432
+ await userEvent.click(
433
+ await screen.findByRole('tab', { name: 'Third group' })
434
+ );
435
+
436
+ await waitFor(() => {
437
+ expect(window.scrollY).toBeGreaterThan(initialScrollY);
438
+ });
439
+
440
+ expect(screen.getByRole('tab', { name: 'Third group' })).toHaveAttribute(
441
+ 'aria-selected',
442
+ 'true'
443
+ );
444
+ });
445
+ });