@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
@@ -1,792 +0,0 @@
1
- import DescriptionOutlinedIcon from '@mui/icons-material/DescriptionOutlined';
2
- import EditIcon from '@mui/icons-material/Edit';
3
- import VisibilityIcon from '@mui/icons-material/Visibility';
4
-
5
- import { useState } from 'react';
6
-
7
- import { TestQueryProvider } from '../..';
8
- import buildListingEndpoint from '../../api/buildListingEndpoint';
9
- import type { SelectEntry } from '../../InputField/Select';
10
- import SingleConnectedAutocompleteField from '../../InputField/Select/Autocomplete/Connected/Single';
11
- import TextField from '../../InputField/Text';
12
- import type { RenderRowParams, RowParams, SortableRowAction } from './models';
13
- import { type Props, SortableEntriesList } from './SortableEntriesList';
14
-
15
- type Stub = ReturnType<typeof cy.stub>;
16
-
17
- const ControlledList = <T,>({
18
- initialValues,
19
- onChangeSpy,
20
- ...props
21
- }: Omit<Props<T>, 'values' | 'onChange'> & {
22
- initialValues: Array<T>;
23
- onChangeSpy: Stub;
24
- }): JSX.Element => {
25
- const [values, setValues] = useState(initialValues);
26
-
27
- return (
28
- <SortableEntriesList<T>
29
- {...props}
30
- onChange={(nextValues): void => {
31
- onChangeSpy(nextValues);
32
- setValues(nextValues);
33
- }}
34
- values={values}
35
- />
36
- );
37
- };
38
-
39
- const getLastValues = <T,>(onChange: Stub): Array<T> =>
40
- onChange.lastCall.args[0];
41
-
42
- const getRows = (): Cypress.Chainable => cy.findAllByRole('listitem');
43
-
44
- const endpoint = '/configuration/hosts/templates';
45
-
46
- const options: Array<SelectEntry> = [
47
- { id: 1, name: 'generic-active-host' },
48
- { id: 2, name: 'generic-passive-host' },
49
- { id: 3, name: 'linux-server-standard' }
50
- ];
51
-
52
- type TemplateRow = SelectEntry | null;
53
-
54
- const renderTemplateRow = ({
55
- setValue,
56
- value,
57
- values
58
- }: RenderRowParams<TemplateRow>): JSX.Element => {
59
- const selectedElsewhere = values
60
- .map((template) => template?.id)
61
- .filter((id) => id !== value?.id);
62
-
63
- return (
64
- <SingleConnectedAutocompleteField
65
- baseEndpoint=""
66
- field="name"
67
- fullWidth
68
- getEndpoint={(parameters): string =>
69
- buildListingEndpoint({ baseEndpoint: endpoint, parameters })
70
- }
71
- getOptionDisabled={(option: SelectEntry): boolean =>
72
- selectedElsewhere.includes(option.id)
73
- }
74
- label="Template"
75
- onChange={(_event, template): void => setValue(template as TemplateRow)}
76
- value={value}
77
- />
78
- );
79
- };
80
-
81
- const initializeTemplates = ({
82
- draggable = true,
83
- templates = [options[0], options[1]]
84
- }: {
85
- draggable?: boolean;
86
- templates?: Array<TemplateRow>;
87
- } = {}): { onChange: Stub; onEdit: Stub } => {
88
- const onChange = cy.stub();
89
- const onEdit = cy.stub();
90
-
91
- cy.intercept('GET', `**${endpoint}**`, {
92
- body: {
93
- meta: { limit: 10, page: 1, total: options.length },
94
- result: options
95
- },
96
- statusCode: 200
97
- }).as('getTemplates');
98
-
99
- cy.mount({
100
- Component: (
101
- <TestQueryProvider>
102
- <div className="p-4">
103
- <ControlledList<TemplateRow>
104
- actions={({
105
- index,
106
- value
107
- }: RowParams<TemplateRow>): Array<SortableRowAction> => [
108
- {
109
- icon: <EditIcon />,
110
- id: 'edit-template',
111
- label: 'Edit template',
112
- onClick: () => onEdit(value, index)
113
- }
114
- ]}
115
- createValue={(): TemplateRow => null}
116
- draggable={draggable}
117
- initialValues={templates}
118
- label="Templates"
119
- onChangeSpy={onChange}
120
- renderRow={renderTemplateRow}
121
- />
122
- </div>
123
- </TestQueryProvider>
124
- )
125
- });
126
-
127
- return { onChange, onEdit };
128
- };
129
-
130
- describe('SortableEntriesList - single input rows', () => {
131
- it('displays one row per value with its value, its actions, a delete button and a trailing drag handle', () => {
132
- initializeTemplates();
133
-
134
- cy.findByRole('list', { name: 'Templates' }).should('exist');
135
- getRows().should('have.length', 2);
136
- cy.get('[data-testid="Template"]')
137
- .eq(0)
138
- .should('have.value', options[0].name);
139
- cy.get('[data-testid="Template"]')
140
- .eq(1)
141
- .should('have.value', options[1].name);
142
- cy.findAllByTestId('edit-template').should('have.length', 2);
143
- cy.findAllByTestId('delete-row').should('have.length', 2);
144
-
145
- getRows()
146
- .eq(0)
147
- .find('button')
148
- .last()
149
- .should('have.attr', 'data-testid', 'drag-handle');
150
-
151
- cy.makeSnapshot();
152
- });
153
-
154
- it('reports the plain values, without any internal id, when an option is selected', () => {
155
- const { onChange } = initializeTemplates();
156
-
157
- cy.get('[data-testid="Template"]').eq(1).click();
158
- cy.wait('@getTemplates');
159
- cy.findByRole('option', { name: options[2].name }).click();
160
-
161
- cy.get('[data-testid="Template"]')
162
- .eq(1)
163
- .should('have.value', options[2].name);
164
- cy.wrap(onChange).should(() => {
165
- expect(getLastValues(onChange)).to.deep.equal([options[0], options[2]]);
166
- });
167
- });
168
-
169
- it('lets a row disable the options already selected in sibling rows', () => {
170
- initializeTemplates();
171
-
172
- cy.get('[data-testid="Template"]').eq(1).click();
173
- cy.wait('@getTemplates');
174
-
175
- cy.findByRole('option', { name: options[0].name }).should(
176
- 'have.attr',
177
- 'aria-disabled',
178
- 'true'
179
- );
180
- cy.findByRole('option', { name: options[2].name }).should(
181
- 'not.have.attr',
182
- 'aria-disabled',
183
- 'true'
184
- );
185
- });
186
-
187
- it('appends a row built with createValue and focuses its first input', () => {
188
- const { onChange } = initializeTemplates();
189
-
190
- cy.findByTestId('Add').click();
191
-
192
- getRows().should('have.length', 3);
193
- cy.get('[data-testid="Template"]').eq(2).should('have.value', '');
194
- cy.focused().should('have.attr', 'data-testid', 'Template');
195
- cy.focused()
196
- .closest('li')
197
- .should(($row) => {
198
- expect($row.index()).to.equal(2);
199
- });
200
- cy.wrap(onChange).should(() => {
201
- expect(getLastValues(onChange)).to.deep.equal([
202
- options[0],
203
- options[1],
204
- null
205
- ]);
206
- });
207
- });
208
-
209
- it('removes a row and moves the focus to the first input of the row taking its place', () => {
210
- const { onChange } = initializeTemplates({
211
- templates: [options[0], options[1], options[2]]
212
- });
213
-
214
- cy.findAllByTestId('delete-row').eq(0).click();
215
-
216
- getRows().should('have.length', 2);
217
- cy.wrap(onChange).should(() => {
218
- expect(getLastValues(onChange)).to.deep.equal([options[1], options[2]]);
219
- });
220
- cy.focused()
221
- .should('have.attr', 'data-testid', 'Template')
222
- .should('have.value', options[1].name);
223
- });
224
-
225
- it('moves the focus to the previous row when the last row is removed, then to the add button when the list becomes empty', () => {
226
- initializeTemplates();
227
-
228
- cy.findAllByTestId('delete-row').eq(1).click();
229
- cy.focused()
230
- .closest('li')
231
- .find('[data-testid="Template"]')
232
- .should('have.value', options[0].name);
233
-
234
- cy.findAllByTestId('delete-row').eq(0).click();
235
- getRows().should('not.exist');
236
- cy.focused().should('have.attr', 'data-testid', 'Add');
237
- });
238
-
239
- it('reorders the rows with the keyboard through the drag handle', () => {
240
- const { onChange } = initializeTemplates();
241
-
242
- cy.moveSortableElement({
243
- direction: 'down',
244
- element: cy.findAllByTestId('drag-handle').eq(0)
245
- });
246
-
247
- cy.get('[data-testid="Template"]')
248
- .eq(0)
249
- .should('have.value', options[1].name);
250
- cy.get('[data-testid="Template"]')
251
- .eq(1)
252
- .should('have.value', options[0].name);
253
- cy.wrap(onChange).should(() => {
254
- expect(getLastValues(onChange)).to.deep.equal([options[1], options[0]]);
255
- });
256
- });
257
-
258
- it('passes the up-to-date row value and index to the actions after a reorder', () => {
259
- const { onEdit } = initializeTemplates();
260
-
261
- cy.moveSortableElement({
262
- direction: 'down',
263
- element: cy.findAllByTestId('drag-handle').eq(0)
264
- });
265
- cy.get('[data-testid="Template"]')
266
- .eq(0)
267
- .should('have.value', options[1].name);
268
-
269
- cy.findAllByTestId('edit-template').eq(1).click();
270
- cy.wrap(onEdit).should('have.been.calledWith', options[0], 1);
271
- });
272
-
273
- it('hides the drag handle when the list is not draggable', () => {
274
- initializeTemplates({ draggable: false });
275
-
276
- cy.findAllByTestId('delete-row').should('have.length', 2);
277
- cy.findByTestId('drag-handle').should('not.exist');
278
- });
279
-
280
- it('lets the inputs take the remaining width and keeps the row buttons at a 32px pitch', () => {
281
- initializeTemplates();
282
-
283
- cy.get('[data-row-fields]')
284
- .eq(0)
285
- .next()
286
- .find('button')
287
- .should('have.length', 3)
288
- .then(($buttons) => {
289
- const lefts = $buttons
290
- .toArray()
291
- .map((button) => button.getBoundingClientRect().left);
292
-
293
- lefts.slice(1).forEach((left, index) => {
294
- expect(left - lefts[index]).to.equal(32);
295
- });
296
- });
297
-
298
- cy.get('[data-row-fields]')
299
- .eq(0)
300
- .invoke('outerWidth')
301
- .should('be.greaterThan', 500);
302
- });
303
- });
304
-
305
- interface MacroRow {
306
- isPassword: boolean;
307
- name: string;
308
- origin: 'direct' | 'fromTpl';
309
- value: string;
310
- }
311
-
312
- const macros: Array<MacroRow> = [
313
- { isPassword: false, name: 'MACRO_A', origin: 'direct', value: 'a' },
314
- { isPassword: true, name: 'MACRO_B', origin: 'fromTpl', value: 'b' }
315
- ];
316
-
317
- const emptyMacro: MacroRow = {
318
- isPassword: false,
319
- name: '',
320
- origin: 'direct',
321
- value: ''
322
- };
323
-
324
- const renderMacroRow = ({
325
- setField,
326
- value: macro
327
- }: RenderRowParams<MacroRow>): JSX.Element => (
328
- <div className="grid grid-cols-2 gap-2 @max-[600px]:grid-cols-1">
329
- <TextField
330
- dataTestId="macro-name"
331
- fullWidth
332
- label="Name"
333
- onChange={(event): void => setField('name', event.target.value)}
334
- value={macro.name}
335
- />
336
- <TextField
337
- dataTestId="macro-value"
338
- fullWidth
339
- label="Value"
340
- onChange={(event): void => setField('value', event.target.value)}
341
- type={macro.isPassword ? 'password' : 'text'}
342
- value={macro.value}
343
- />
344
- </div>
345
- );
346
-
347
- const MacrosList = ({
348
- copyOnChange,
349
- onChangeSpy
350
- }: {
351
- copyOnChange: boolean;
352
- onChangeSpy: Stub;
353
- }): JSX.Element => {
354
- const [values, setValues] = useState(macros);
355
-
356
- const change = (nextValues: Array<MacroRow>): void => {
357
- onChangeSpy(nextValues);
358
- // Simulates a parent that stores a copy (new array and objects), like a
359
- // form library that clones its state.
360
- setValues(
361
- copyOnChange ? nextValues.map((macro) => ({ ...macro })) : nextValues
362
- );
363
- };
364
-
365
- return (
366
- <>
367
- <button
368
- data-testid="reset"
369
- onClick={(): void => setValues([emptyMacro])}
370
- type="button"
371
- >
372
- Reset
373
- </button>
374
- <SortableEntriesList<MacroRow>
375
- actions={({ index, value }) => [
376
- {
377
- icon: <VisibilityIcon />,
378
- id: 'toggle-password',
379
- label: 'Toggle password',
380
- onClick: () =>
381
- change(
382
- values.map((macro, macroIndex) =>
383
- macroIndex === index
384
- ? { ...macro, isPassword: !value.isPassword }
385
- : macro
386
- )
387
- )
388
- }
389
- ]}
390
- createValue={(): MacroRow => emptyMacro}
391
- getRowClassName={({ value }) => `origin-${value.origin}`}
392
- onChange={change}
393
- renderRow={renderMacroRow}
394
- values={values}
395
- />
396
- </>
397
- );
398
- };
399
-
400
- const initializeMacros = ({
401
- copyOnChange = false
402
- }: {
403
- copyOnChange?: boolean;
404
- } = {}): { onChange: Stub } => {
405
- const onChange = cy.stub();
406
-
407
- cy.mount({
408
- Component: <MacrosList copyOnChange={copyOnChange} onChangeSpy={onChange} />
409
- });
410
-
411
- return { onChange };
412
- };
413
-
414
- describe('SortableEntriesList - multiple input rows', () => {
415
- it('renders every input of every row', () => {
416
- initializeMacros();
417
-
418
- cy.get('input[data-testid="macro-name"]').should('have.length', 2);
419
- cy.get('input[data-testid="macro-value"]').should('have.length', 2);
420
- cy.get('input[data-testid="macro-value"]')
421
- .eq(1)
422
- .should('have.attr', 'type', 'password');
423
- });
424
-
425
- it('keeps the focus in the edited input while typing and reports a structured value', () => {
426
- const { onChange } = initializeMacros();
427
-
428
- cy.get('input[data-testid="macro-name"]').eq(0).clear();
429
- cy.get('input[data-testid="macro-name"]').eq(0).type('HOST_IP');
430
- cy.focused().should('have.value', 'HOST_IP');
431
-
432
- cy.get('input[data-testid="macro-value"]').eq(0).clear();
433
- cy.get('input[data-testid="macro-value"]').eq(0).type('10.0.0.1');
434
- cy.focused().should('have.value', '10.0.0.1');
435
-
436
- cy.wrap(onChange).should(() => {
437
- expect(getLastValues(onChange)).to.deep.equal([
438
- {
439
- isPassword: false,
440
- name: 'HOST_IP',
441
- origin: 'direct',
442
- value: '10.0.0.1'
443
- },
444
- macros[1]
445
- ]);
446
- });
447
- });
448
-
449
- it('keeps the focus when a new row is typed into right after being added', () => {
450
- const { onChange } = initializeMacros();
451
-
452
- cy.findByTestId('Add').click();
453
- cy.focused().should('have.attr', 'data-testid', 'macro-name').type('NEW');
454
- cy.get('input[data-testid="macro-value"]').eq(2).type('value');
455
-
456
- cy.wrap(onChange).should(() => {
457
- expect(getLastValues<MacroRow>(onChange)[2]).to.deep.equal({
458
- ...emptyMacro,
459
- name: 'NEW',
460
- value: 'value'
461
- });
462
- });
463
- });
464
-
465
- it('keeps the rows and the focus stable when the parent stores a copy of the values', () => {
466
- const { onChange } = initializeMacros({ copyOnChange: true });
467
-
468
- cy.get('input[data-testid="macro-name"]').eq(1).clear();
469
- cy.get('input[data-testid="macro-name"]').eq(1).type('RENAMED');
470
- cy.focused().should('have.value', 'RENAMED');
471
-
472
- cy.findByTestId('Add').click();
473
- cy.focused().should('have.attr', 'data-testid', 'macro-name').type('NEW');
474
-
475
- cy.wrap(onChange).should(() => {
476
- expect(
477
- getLastValues<MacroRow>(onChange).map(({ name }) => name)
478
- ).to.deep.equal(['MACRO_A', 'RENAMED', 'NEW']);
479
- });
480
- });
481
-
482
- it('renders the new values when the parent replaces them', () => {
483
- initializeMacros();
484
-
485
- cy.findByTestId('reset').click();
486
-
487
- getRows().should('have.length', 1);
488
- cy.get('input[data-testid="macro-name"]').should('have.value', '');
489
- });
490
-
491
- it('lets a row action change its own row', () => {
492
- initializeMacros();
493
-
494
- cy.findAllByTestId('toggle-password').eq(0).click();
495
-
496
- cy.get('input[data-testid="macro-value"]')
497
- .eq(0)
498
- .should('have.attr', 'type', 'password');
499
- });
500
-
501
- it('applies the class returned by getRowClassName to each row', () => {
502
- initializeMacros();
503
-
504
- getRows().eq(0).should('have.class', 'origin-direct');
505
- getRows().eq(1).should('have.class', 'origin-fromTpl');
506
- });
507
- });
508
-
509
- const buildMacros = (count: number): Array<MacroRow> =>
510
- Array.from({ length: count }, (_, index) => ({
511
- ...emptyMacro,
512
- name: `MACRO_${index + 1}`
513
- }));
514
-
515
- const panelWidths = [1400, 900, 720, 560];
516
-
517
- // The panel width is state, so changing it resizes the mounted list at runtime.
518
- const ResizablePanel = ({
519
- count,
520
- maxVisibleRows,
521
- onChangeSpy
522
- }: {
523
- count: number;
524
- maxVisibleRows?: number;
525
- onChangeSpy: Stub;
526
- }): JSX.Element => {
527
- const [width, setWidth] = useState(panelWidths[0]);
528
-
529
- return (
530
- <div className="p-4">
531
- {panelWidths.map((panelWidth) => (
532
- <button
533
- data-testid={`width-${panelWidth}`}
534
- key={panelWidth}
535
- onClick={(): void => setWidth(panelWidth)}
536
- type="button"
537
- >
538
- {panelWidth}
539
- </button>
540
- ))}
541
- <div data-testid="panel" style={{ width }}>
542
- <ControlledList<MacroRow>
543
- actions={(): Array<SortableRowAction> => [
544
- {
545
- icon: <VisibilityIcon />,
546
- id: 'toggle-password',
547
- label: 'Toggle password',
548
- onClick: () => undefined
549
- },
550
- {
551
- icon: <DescriptionOutlinedIcon />,
552
- id: 'macro-description',
553
- label: 'Description',
554
- onClick: () => undefined
555
- }
556
- ]}
557
- createValue={(): MacroRow => emptyMacro}
558
- initialValues={buildMacros(count)}
559
- maxVisibleRows={maxVisibleRows}
560
- onChangeSpy={onChangeSpy}
561
- renderRow={renderMacroRow}
562
- />
563
- </div>
564
- </div>
565
- );
566
- };
567
-
568
- const initializePanel = ({
569
- count,
570
- maxVisibleRows
571
- }: {
572
- count: number;
573
- maxVisibleRows?: number;
574
- }): { onChange: Stub } => {
575
- const onChange = cy.stub();
576
-
577
- cy.viewport(1600, 900);
578
- cy.mount({
579
- Component: (
580
- <ResizablePanel
581
- count={count}
582
- maxVisibleRows={maxVisibleRows}
583
- onChangeSpy={onChange}
584
- />
585
- )
586
- });
587
-
588
- return { onChange };
589
- };
590
-
591
- const getScrollArea = (): Cypress.Chainable =>
592
- cy.findByTestId('sortable-entries-scroll');
593
-
594
- const getRect = (element: Element): DOMRect => element.getBoundingClientRect();
595
-
596
- const getLastNames = (onChange: Stub): Array<string> =>
597
- getLastValues<MacroRow>(onChange).map(({ name }) => name);
598
-
599
- describe('SortableEntriesList - capped list', () => {
600
- it('shows maxVisibleRows rows plus half of the next one, and keeps the add button outside the scroll area', () => {
601
- initializePanel({ count: 15, maxVisibleRows: 10 });
602
-
603
- getScrollArea().should(($scrollArea) => {
604
- const scrollArea = $scrollArea[0];
605
- const rows = scrollArea.querySelectorAll('li');
606
- const visibleBottom = getRect(scrollArea).bottom;
607
-
608
- expect(scrollArea.scrollHeight).to.be.greaterThan(
609
- scrollArea.clientHeight
610
- );
611
- expect(getRect(rows[9]).bottom).to.be.at.most(visibleBottom);
612
- expect(visibleBottom - getRect(rows[10]).top).to.be.closeTo(
613
- getRect(rows[10]).height / 2,
614
- 1
615
- );
616
- });
617
-
618
- cy.findByTestId('Add')
619
- .should('be.visible')
620
- .closest('[data-testid="sortable-entries-scroll"]')
621
- .should('not.exist');
622
- });
623
-
624
- it('fades the cut-off row only while rows are hidden below', () => {
625
- initializePanel({ count: 15, maxVisibleRows: 10 });
626
-
627
- getScrollArea().should('not.have.css', 'mask-image', 'none');
628
- getScrollArea().scrollTo('bottom');
629
- getScrollArea().should('have.css', 'mask-image', 'none');
630
- });
631
-
632
- it('does not cap a list shorter than maxVisibleRows', () => {
633
- initializePanel({ count: 5, maxVisibleRows: 10 });
634
-
635
- getScrollArea()
636
- .should('have.css', 'max-height', 'none')
637
- .should(($scrollArea) => {
638
- expect($scrollArea[0].scrollHeight).to.equal(
639
- $scrollArea[0].clientHeight
640
- );
641
- });
642
- });
643
-
644
- it('focuses and scrolls to a row added at the end of a capped list', () => {
645
- initializePanel({ count: 15, maxVisibleRows: 10 });
646
-
647
- cy.findByTestId('Add').click();
648
-
649
- getRows().should('have.length', 16);
650
- cy.focused()
651
- .should('have.attr', 'data-testid', 'macro-name')
652
- .should(($input) => {
653
- const scrollAreaRect = getRect(
654
- $input[0].closest(
655
- '[data-testid="sortable-entries-scroll"]'
656
- ) as Element
657
- );
658
- const inputRect = getRect($input[0]);
659
-
660
- expect(inputRect.top).to.be.at.least(scrollAreaRect.top);
661
- expect(inputRect.bottom).to.be.at.most(scrollAreaRect.bottom);
662
- });
663
- });
664
-
665
- it('scrolls to the focused new row when adding it makes the list reach the cap', () => {
666
- initializePanel({ count: 10, maxVisibleRows: 10 });
667
-
668
- getScrollArea().should('have.css', 'max-height', 'none');
669
- cy.findByTestId('Add').click();
670
-
671
- getRows().should('have.length', 11);
672
- getScrollArea().should('not.have.css', 'max-height', 'none');
673
- cy.focused()
674
- .should('have.attr', 'data-testid', 'macro-name')
675
- .closest('li')
676
- .should(($row) => {
677
- const scrollAreaRect = getRect(
678
- $row[0].closest('[data-testid="sortable-entries-scroll"]') as Element
679
- );
680
-
681
- // 1px tolerance for sub-pixel row heights.
682
- expect(getRect($row[0]).bottom).to.be.at.most(
683
- scrollAreaRect.bottom + 1
684
- );
685
- });
686
- getScrollArea().should('have.css', 'mask-image', 'none');
687
- });
688
-
689
- it('moves a row past the visible area with the keyboard, scrolling the list', () => {
690
- const { onChange } = initializePanel({ count: 15, maxVisibleRows: 10 });
691
-
692
- cy.moveSortableElement({
693
- direction: 'down',
694
- element: cy.findAllByTestId('drag-handle').eq(0),
695
- times: 12
696
- });
697
-
698
- cy.wrap(onChange).should(() => {
699
- expect(getLastNames(onChange).indexOf('MACRO_1')).to.equal(12);
700
- });
701
- getScrollArea().its('0.scrollTop').should('be.greaterThan', 0);
702
- });
703
-
704
- it('auto-scrolls while a row is dragged with the pointer near the bottom edge', () => {
705
- const { onChange } = initializePanel({ count: 15, maxVisibleRows: 10 });
706
-
707
- cy.findAllByTestId('drag-handle')
708
- .eq(0)
709
- .then(($handle) => {
710
- const { left, top, width, height } = getRect($handle[0]);
711
- const pointer = {
712
- button: 0,
713
- clientX: left + width / 2,
714
- isPrimary: true,
715
- pointerId: 1
716
- };
717
-
718
- cy.wrap($handle).trigger('pointerdown', {
719
- ...pointer,
720
- clientY: top + height / 2
721
- });
722
-
723
- getScrollArea().then(($scrollArea) => {
724
- const clientY = getRect($scrollArea[0]).bottom - 5;
725
-
726
- cy.document().trigger('pointermove', { ...pointer, clientY });
727
- getScrollArea().its('0.scrollTop').should('be.greaterThan', 100);
728
- cy.document().trigger('pointermove', { ...pointer, clientY });
729
- cy.document().trigger('pointerup', { ...pointer, clientY });
730
- });
731
- });
732
-
733
- cy.wrap(onChange).should(() => {
734
- expect(getLastNames(onChange).indexOf('MACRO_1')).to.be.greaterThan(10);
735
- });
736
- });
737
- });
738
-
739
- describe('SortableEntriesList - container responsive rows', () => {
740
- const getFieldRects = (): Cypress.Chainable<Array<DOMRect>> =>
741
- getRows()
742
- .eq(0)
743
- .find('.MuiFormControl-root')
744
- .then(($fields) => $fields.toArray().map(getRect));
745
-
746
- it('keeps the fields and the four buttons on one line at 1400, 900 and 720px, resized at runtime', () => {
747
- initializePanel({ count: 3 });
748
-
749
- [1400, 900, 720, 1400].forEach((width) => {
750
- cy.findByTestId(`width-${width}`).click();
751
- cy.findByTestId('panel').should('have.css', 'width', `${width}px`);
752
-
753
- getFieldRects().should(([name, value]) => {
754
- expect(value.top).to.equal(name.top);
755
- expect(name.width).to.be.at.least(275);
756
- expect(value.width).to.be.at.least(275);
757
- });
758
-
759
- cy.get('[data-row-fields]')
760
- .eq(0)
761
- .next()
762
- .find('button')
763
- .should('have.length', 4)
764
- .should(($buttons) => {
765
- const rects = $buttons.toArray().map(getRect);
766
- const panelRight = getRect(
767
- Cypress.$('[data-testid="panel"]')[0]
768
- ).right;
769
-
770
- rects.slice(1).forEach((rect, index) => {
771
- expect(rect.top).to.equal(rects[index].top);
772
- expect(rect.left - rects[index].left).to.equal(32);
773
- });
774
- expect(rects[3].right).to.be.at.most(panelRight);
775
- });
776
- });
777
- });
778
-
779
- it('stacks the fields when the list gets narrower than 600px, whatever the viewport', () => {
780
- initializePanel({ count: 3 });
781
-
782
- cy.findByTestId('width-560').click();
783
- getFieldRects().should(([name, value]) => {
784
- expect(value.top).to.be.greaterThan(name.bottom);
785
- });
786
-
787
- cy.findByTestId('width-900').click();
788
- getFieldRects().should(([name, value]) => {
789
- expect(value.top).to.equal(name.top);
790
- });
791
- });
792
- });