@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.
- package/package.json +6 -3
- package/public/mockServiceWorker.js +31 -8
- package/src/Dashboard/Dashboard.browser.test.tsx +107 -0
- package/src/Form/CollapsibleGroup.tsx +6 -33
- package/src/Form/Form.browser.test.tsx +553 -0
- package/src/Form/Inputs/ExclusiveCheckboxGroup.browser.test.tsx +304 -0
- package/src/Form/Inputs/Grid.tsx +1 -2
- package/src/Form/Inputs/SegmentedButtons.browser.test.tsx +113 -0
- package/src/Form/Inputs/index.tsx +10 -52
- package/src/InputField/Number/Number.browser.test.tsx +109 -0
- package/src/InputField/Select/Autocomplete/Connected/Multi/MultiConnectedAutocompleteField.browser.test.tsx +250 -0
- package/src/Listing/Listing.browser.test.tsx +476 -0
- package/src/Module/Module.browser.test.tsx +145 -0
- package/src/Pagination/Pagination.browser.test.tsx +184 -0
- package/src/RichTextEditor/RichTextEditor.browser.test.tsx +567 -0
- package/src/RichTextEditor/plugins/FloatingLinkEditorPlugin.tsx +3 -1
- package/src/TimePeriods/ResolutionTimePeriod.browser.test.tsx +143 -0
- package/src/TimePeriods/TimePeriods.browser.test.tsx +468 -0
- package/src/Typography/FluidTypography/FluidTypography.browser.test.tsx +54 -0
- package/src/api/useMutationQuery/useMutationQuery.browser.test.tsx +154 -0
- package/src/components/CollapsibleItem/CollapsibleItem.browser.test.tsx +120 -0
- package/src/components/CopyCommand/CopyCommand.browser.test.tsx +287 -0
- package/src/components/CrudPage/CrudPage.browser.test.tsx +659 -0
- package/src/components/DataTable/DataTable.browser.test.tsx +257 -0
- package/src/components/Form/AccessRights/AccessRights.browser.test.tsx +527 -0
- package/src/components/ItemComposition/ItemComposition.browser.test.tsx +172 -0
- package/src/components/Layout/PageLayout.browser.test.tsx +114 -0
- package/src/components/Modal/ConfirmationModal/ConfirmationModal.browser.test.tsx +237 -0
- package/src/components/SortableEntriesList/SortableEntriesList.browser.test.tsx +981 -0
- package/src/components/Tabs/Tabs.browser.test.tsx +115 -0
- package/src/utils/useFullscreen/Fullscreen.browser.test.tsx +176 -0
- package/src/utils/useLocale/useLocale.browser.test.tsx +49 -0
- package/src/utils/useLocaleTimezoneDate/{LocaleTimezoneDate.cypress.spec.tsx → LocaleTimezoneDate.browser.test.tsx} +24 -11
- package/src/Dashboard/Dashboard.cypress.spec.tsx +0 -71
- package/src/Form/Form.cypress.spec.tsx +0 -306
- package/src/Form/Inputs/ExclusiveCheckboxGroup.cypress.spec.tsx +0 -236
- package/src/Form/Inputs/SegmentedButtons.cypress.spec.tsx +0 -101
- package/src/InputField/Number/Number.cypress.spec.tsx +0 -89
- package/src/InputField/Select/Autocomplete/Connected/Multi/MultiConnectedAutocompleteField.cypress.spec.tsx +0 -182
- package/src/Listing/Listing.cypress.spec.tsx +0 -349
- package/src/Module/Module.cypress.spec.tsx +0 -127
- package/src/Pagination/Pagination.cypress.spec.tsx +0 -140
- package/src/RichTextEditor/RichTextEditor.cypress.spec.tsx +0 -408
- package/src/TimePeriods/ResolutionTimePeriod.cypress.spec.tsx +0 -74
- package/src/TimePeriods/TimePeriods.cypress.spec.tsx +0 -488
- package/src/Typography/FluidTypography/FluidTypography.cypress.spec.tsx +0 -27
- package/src/api/useMutationQuery/useMutationQuery.cypress.spec.tsx +0 -93
- package/src/components/CollapsibleItem/CollapsibleItem.cypress.spec.tsx +0 -76
- package/src/components/CopyCommand/CopyCommand.cypress.spec.tsx +0 -167
- package/src/components/CrudPage/CrudPage.cypress.spec.tsx +0 -561
- package/src/components/DataTable/DataTable.cypress.spec.tsx +0 -174
- package/src/components/Form/AccessRights/AccessRights.cypress.spec.tsx +0 -376
- package/src/components/ItemComposition/ItemComposition.cypress.spec.tsx +0 -112
- package/src/components/Layout/PageLayout.cypress.spec.tsx +0 -64
- package/src/components/Modal/ConfirmationModal/ConfirmationModal.cypress.spec.tsx +0 -171
- package/src/components/SortableEntriesList/SortableEntriesList.cypress.spec.tsx +0 -792
- package/src/components/Tabs/Tabs.cypress.spec.tsx +0 -69
- package/src/utils/useFullscreen/Fullscreen.cypress.spec.tsx +0 -130
- 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
|
-
});
|