@centreon/ui 26.12.1 → 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.
- package/package.json +6 -3
- package/public/mockServiceWorker.js +31 -8
- package/src/Dashboard/Dashboard.browser.test.tsx +107 -0
- package/src/Form/Form.browser.test.tsx +445 -0
- package/src/Form/Inputs/ExclusiveCheckboxGroup.browser.test.tsx +304 -0
- package/src/Form/Inputs/SegmentedButtons.browser.test.tsx +113 -0
- 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
|
@@ -0,0 +1,981 @@
|
|
|
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 { rs } from '@rstest/core';
|
|
6
|
+
import { interceptAPIRequest, Method, waitForRequest } from '@test-setup/api';
|
|
7
|
+
import { render } from '@test-setup/render';
|
|
8
|
+
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
|
9
|
+
import userEvent from '@testing-library/user-event';
|
|
10
|
+
import { useState } from 'react';
|
|
11
|
+
|
|
12
|
+
import { TestQueryProvider } from '../..';
|
|
13
|
+
import buildListingEndpoint from '../../api/buildListingEndpoint';
|
|
14
|
+
import type { SelectEntry } from '../../InputField/Select';
|
|
15
|
+
import SingleConnectedAutocompleteField from '../../InputField/Select/Autocomplete/Connected/Single';
|
|
16
|
+
import TextField from '../../InputField/Text';
|
|
17
|
+
import type { RenderRowParams, RowParams, SortableRowAction } from './models';
|
|
18
|
+
import { type Props, SortableEntriesList } from './SortableEntriesList';
|
|
19
|
+
|
|
20
|
+
type Spy = ReturnType<typeof rs.fn>;
|
|
21
|
+
|
|
22
|
+
const ControlledList = <T,>({
|
|
23
|
+
initialValues,
|
|
24
|
+
onChangeSpy,
|
|
25
|
+
...props
|
|
26
|
+
}: Omit<Props<T>, 'values' | 'onChange'> & {
|
|
27
|
+
initialValues: Array<T>;
|
|
28
|
+
onChangeSpy: Spy;
|
|
29
|
+
}): JSX.Element => {
|
|
30
|
+
const [values, setValues] = useState(initialValues);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<SortableEntriesList<T>
|
|
34
|
+
{...props}
|
|
35
|
+
onChange={(nextValues): void => {
|
|
36
|
+
onChangeSpy(nextValues);
|
|
37
|
+
setValues(nextValues);
|
|
38
|
+
}}
|
|
39
|
+
values={values}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const getLastValues = <T,>(onChange: Spy): Array<T> =>
|
|
45
|
+
onChange.mock.lastCall?.[0] as Array<T>;
|
|
46
|
+
|
|
47
|
+
const getRows = (): Array<HTMLElement> => screen.queryAllByRole('listitem');
|
|
48
|
+
|
|
49
|
+
const getFocused = (): HTMLElement => document.activeElement as HTMLElement;
|
|
50
|
+
|
|
51
|
+
const getRect = (element: Element): DOMRect => element.getBoundingClientRect();
|
|
52
|
+
|
|
53
|
+
const getInputs = (testId: string): Array<HTMLInputElement> =>
|
|
54
|
+
Array.from(document.querySelectorAll(`input[data-testid="${testId}"]`));
|
|
55
|
+
|
|
56
|
+
const getRowButtons = (rowIndex: number): Array<HTMLButtonElement> =>
|
|
57
|
+
Array.from(
|
|
58
|
+
document
|
|
59
|
+
.querySelectorAll('[data-row-fields]')
|
|
60
|
+
[rowIndex].nextElementSibling?.querySelectorAll('button') ?? []
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
interface MoveSortableElementProps {
|
|
64
|
+
direction: 'up' | 'down';
|
|
65
|
+
handle: HTMLElement;
|
|
66
|
+
times?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The rows other than the dragged one that make way for it: as many as the
|
|
70
|
+
// dragged row moved past.
|
|
71
|
+
const countShiftedRows = (row: HTMLElement): number =>
|
|
72
|
+
Array.from(row.parentElement?.children ?? []).filter(
|
|
73
|
+
(sibling) =>
|
|
74
|
+
sibling !== row &&
|
|
75
|
+
/translate3d\(0px, -?[1-9]/.test((sibling as HTMLElement).style.transform)
|
|
76
|
+
).length;
|
|
77
|
+
|
|
78
|
+
// A keyboard drag: space to pick the row up, the arrows to move it,
|
|
79
|
+
// space to drop it. dnd-kit handles a key once the previous one took effect,
|
|
80
|
+
// so each step is awaited: the handle pressed once the row is picked up, one
|
|
81
|
+
// more row making way after each arrow, the handle released once dropped.
|
|
82
|
+
const moveSortableElement = async ({
|
|
83
|
+
handle,
|
|
84
|
+
direction,
|
|
85
|
+
times = 1
|
|
86
|
+
}: MoveSortableElementProps): Promise<void> => {
|
|
87
|
+
const row = handle.closest('li') as HTMLElement;
|
|
88
|
+
const key = direction === 'down' ? '{ArrowDown}' : '{ArrowUp}';
|
|
89
|
+
|
|
90
|
+
handle.focus();
|
|
91
|
+
await userEvent.keyboard(' ');
|
|
92
|
+
await waitFor(() => {
|
|
93
|
+
expect(handle).toHaveAttribute('aria-pressed', 'true');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
for (let move = 1; move <= times; move += 1) {
|
|
97
|
+
await userEvent.keyboard(key);
|
|
98
|
+
await waitFor(() => {
|
|
99
|
+
expect(countShiftedRows(row)).toBe(move);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await userEvent.keyboard(' ');
|
|
104
|
+
await waitFor(() => {
|
|
105
|
+
expect(handle).not.toHaveAttribute('aria-pressed', 'true');
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const endpoint = '/configuration/hosts/templates';
|
|
110
|
+
|
|
111
|
+
const options: Array<SelectEntry> = [
|
|
112
|
+
{ id: 1, name: 'generic-active-host' },
|
|
113
|
+
{ id: 2, name: 'generic-passive-host' },
|
|
114
|
+
{ id: 3, name: 'linux-server-standard' }
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
type TemplateRow = SelectEntry | null;
|
|
118
|
+
|
|
119
|
+
const renderTemplateRow = ({
|
|
120
|
+
setValue,
|
|
121
|
+
value,
|
|
122
|
+
values
|
|
123
|
+
}: RenderRowParams<TemplateRow>): JSX.Element => {
|
|
124
|
+
const selectedElsewhere = values
|
|
125
|
+
.map((template) => template?.id)
|
|
126
|
+
.filter((id) => id !== value?.id);
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<SingleConnectedAutocompleteField
|
|
130
|
+
baseEndpoint=""
|
|
131
|
+
field="name"
|
|
132
|
+
fullWidth
|
|
133
|
+
getEndpoint={(parameters): string =>
|
|
134
|
+
buildListingEndpoint({ baseEndpoint: endpoint, parameters })
|
|
135
|
+
}
|
|
136
|
+
getOptionDisabled={(option: SelectEntry): boolean =>
|
|
137
|
+
selectedElsewhere.includes(option.id)
|
|
138
|
+
}
|
|
139
|
+
label="Template"
|
|
140
|
+
onChange={(_event, template): void => setValue(template as TemplateRow)}
|
|
141
|
+
value={value}
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const getTemplateInputs = (): Array<HTMLInputElement> => getInputs('Template');
|
|
147
|
+
|
|
148
|
+
interface InitializeTemplatesProps {
|
|
149
|
+
draggable?: boolean;
|
|
150
|
+
templates?: Array<TemplateRow>;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const initializeTemplates = ({
|
|
154
|
+
draggable = true,
|
|
155
|
+
templates = [options[0], options[1]]
|
|
156
|
+
}: InitializeTemplatesProps = {}): { onChange: Spy; onEdit: Spy } => {
|
|
157
|
+
const onChange = rs.fn();
|
|
158
|
+
const onEdit = rs.fn();
|
|
159
|
+
|
|
160
|
+
interceptAPIRequest({
|
|
161
|
+
alias: 'getTemplates',
|
|
162
|
+
method: Method.GET,
|
|
163
|
+
path: `**${endpoint}?**`,
|
|
164
|
+
response: {
|
|
165
|
+
meta: { limit: 10, page: 1, total: options.length },
|
|
166
|
+
result: options
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
render(
|
|
171
|
+
<TestQueryProvider>
|
|
172
|
+
<div className="p-4">
|
|
173
|
+
<ControlledList<TemplateRow>
|
|
174
|
+
actions={({
|
|
175
|
+
index,
|
|
176
|
+
value
|
|
177
|
+
}: RowParams<TemplateRow>): Array<SortableRowAction> => [
|
|
178
|
+
{
|
|
179
|
+
icon: <EditIcon />,
|
|
180
|
+
id: 'edit-template',
|
|
181
|
+
label: 'Edit template',
|
|
182
|
+
onClick: () => onEdit(value, index)
|
|
183
|
+
}
|
|
184
|
+
]}
|
|
185
|
+
createValue={(): TemplateRow => null}
|
|
186
|
+
draggable={draggable}
|
|
187
|
+
initialValues={templates}
|
|
188
|
+
label="Templates"
|
|
189
|
+
onChangeSpy={onChange}
|
|
190
|
+
renderRow={renderTemplateRow}
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
</TestQueryProvider>
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
return { onChange, onEdit };
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
describe('SortableEntriesList - single input rows', () => {
|
|
200
|
+
beforeEach(() => {
|
|
201
|
+
expect.hasAssertions();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('displays one row per value with its value, its actions, a delete button and a trailing drag handle', async () => {
|
|
205
|
+
initializeTemplates();
|
|
206
|
+
|
|
207
|
+
expect(
|
|
208
|
+
await screen.findByRole('list', { name: 'Templates' })
|
|
209
|
+
).toBeInTheDocument();
|
|
210
|
+
expect(getRows()).toHaveLength(2);
|
|
211
|
+
await waitFor(() => {
|
|
212
|
+
expect(getTemplateInputs()[0]).toHaveValue(options[0].name);
|
|
213
|
+
});
|
|
214
|
+
expect(getTemplateInputs()[1]).toHaveValue(options[1].name);
|
|
215
|
+
expect(screen.getAllByTestId('edit-template')).toHaveLength(2);
|
|
216
|
+
expect(screen.getAllByTestId('delete-row')).toHaveLength(2);
|
|
217
|
+
|
|
218
|
+
const firstRowButtons = getRows()[0].querySelectorAll('button');
|
|
219
|
+
|
|
220
|
+
expect(firstRowButtons[firstRowButtons.length - 1]).toHaveAttribute(
|
|
221
|
+
'data-testid',
|
|
222
|
+
'drag-handle'
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// The buttons of each row in order (edit, delete, drag handle), the
|
|
226
|
+
// delete button in red, and the add button below the rows.
|
|
227
|
+
getRows().forEach((_row, rowIndex) => {
|
|
228
|
+
expect(
|
|
229
|
+
getRowButtons(rowIndex).map((button) =>
|
|
230
|
+
button.getAttribute('data-testid')
|
|
231
|
+
)
|
|
232
|
+
).toEqual(['edit-template', 'delete-row', 'drag-handle']);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const [red, green, blue] = (
|
|
236
|
+
getComputedStyle(screen.getAllByTestId('delete-row')[0]).color.match(
|
|
237
|
+
/\d+/g
|
|
238
|
+
) ?? []
|
|
239
|
+
).map(Number);
|
|
240
|
+
|
|
241
|
+
expect(red).toBeGreaterThan(green + 100);
|
|
242
|
+
expect(red).toBeGreaterThan(blue + 100);
|
|
243
|
+
|
|
244
|
+
const addButton = screen.getByTestId('Add');
|
|
245
|
+
|
|
246
|
+
expect(addButton).toBeVisible();
|
|
247
|
+
expect(getRect(addButton).top).toBeGreaterThanOrEqual(
|
|
248
|
+
getRect(getRows()[1]).bottom
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('reports the plain values, without any internal id, when an option is selected', async () => {
|
|
253
|
+
const { onChange } = initializeTemplates();
|
|
254
|
+
|
|
255
|
+
await userEvent.click(getTemplateInputs()[1]);
|
|
256
|
+
await waitForRequest('@getTemplates');
|
|
257
|
+
await userEvent.click(
|
|
258
|
+
await screen.findByRole('option', { name: options[2].name })
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
await waitFor(() => {
|
|
262
|
+
expect(getTemplateInputs()[1]).toHaveValue(options[2].name);
|
|
263
|
+
});
|
|
264
|
+
await waitFor(() => {
|
|
265
|
+
expect(getLastValues(onChange)).toEqual([options[0], options[2]]);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('lets a row disable the options already selected in sibling rows', async () => {
|
|
270
|
+
initializeTemplates();
|
|
271
|
+
|
|
272
|
+
await userEvent.click(getTemplateInputs()[1]);
|
|
273
|
+
await waitForRequest('@getTemplates');
|
|
274
|
+
|
|
275
|
+
expect(
|
|
276
|
+
await screen.findByRole('option', { name: options[0].name })
|
|
277
|
+
).toHaveAttribute('aria-disabled', 'true');
|
|
278
|
+
expect(
|
|
279
|
+
screen.getByRole('option', { name: options[2].name })
|
|
280
|
+
).not.toHaveAttribute('aria-disabled', 'true');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('appends a row built with createValue and focuses its first input', async () => {
|
|
284
|
+
const { onChange } = initializeTemplates();
|
|
285
|
+
|
|
286
|
+
await userEvent.click(await screen.findByTestId('Add'));
|
|
287
|
+
|
|
288
|
+
await waitFor(() => {
|
|
289
|
+
expect(getRows()).toHaveLength(3);
|
|
290
|
+
});
|
|
291
|
+
expect(getTemplateInputs()[2]).toHaveValue('');
|
|
292
|
+
await waitFor(() => {
|
|
293
|
+
expect(getFocused()).toHaveAttribute('data-testid', 'Template');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const focusedRow = getFocused().closest('li') as HTMLElement;
|
|
297
|
+
|
|
298
|
+
expect(
|
|
299
|
+
Array.from(focusedRow.parentElement?.children ?? []).indexOf(focusedRow)
|
|
300
|
+
).toBe(2);
|
|
301
|
+
await waitFor(() => {
|
|
302
|
+
expect(getLastValues(onChange)).toEqual([options[0], options[1], null]);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('removes a row and moves the focus to the first input of the row taking its place', async () => {
|
|
307
|
+
const { onChange } = initializeTemplates({
|
|
308
|
+
templates: [options[0], options[1], options[2]]
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
await userEvent.click((await screen.findAllByTestId('delete-row'))[0]);
|
|
312
|
+
|
|
313
|
+
await waitFor(() => {
|
|
314
|
+
expect(getRows()).toHaveLength(2);
|
|
315
|
+
});
|
|
316
|
+
await waitFor(() => {
|
|
317
|
+
expect(getLastValues(onChange)).toEqual([options[1], options[2]]);
|
|
318
|
+
});
|
|
319
|
+
await waitFor(() => {
|
|
320
|
+
expect(getFocused()).toHaveAttribute('data-testid', 'Template');
|
|
321
|
+
});
|
|
322
|
+
expect(getFocused()).toHaveValue(options[1].name);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('moves the focus to the previous row when the last row is removed, then to the add button when the list becomes empty', async () => {
|
|
326
|
+
initializeTemplates();
|
|
327
|
+
|
|
328
|
+
await userEvent.click((await screen.findAllByTestId('delete-row'))[1]);
|
|
329
|
+
await waitFor(() => {
|
|
330
|
+
expect(
|
|
331
|
+
getFocused()
|
|
332
|
+
.closest('li')
|
|
333
|
+
?.querySelector('input[data-testid="Template"]')
|
|
334
|
+
).toHaveValue(options[0].name);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
await userEvent.click(screen.getAllByTestId('delete-row')[0]);
|
|
338
|
+
await waitFor(() => {
|
|
339
|
+
expect(getRows()).toHaveLength(0);
|
|
340
|
+
});
|
|
341
|
+
await waitFor(() => {
|
|
342
|
+
expect(getFocused()).toHaveAttribute('data-testid', 'Add');
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('reorders the rows with the keyboard through the drag handle', async () => {
|
|
347
|
+
const { onChange } = initializeTemplates();
|
|
348
|
+
|
|
349
|
+
await moveSortableElement({
|
|
350
|
+
direction: 'down',
|
|
351
|
+
handle: (await screen.findAllByTestId('drag-handle'))[0]
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
await waitFor(() => {
|
|
355
|
+
expect(getTemplateInputs()[0]).toHaveValue(options[1].name);
|
|
356
|
+
});
|
|
357
|
+
expect(getTemplateInputs()[1]).toHaveValue(options[0].name);
|
|
358
|
+
await waitFor(() => {
|
|
359
|
+
expect(getLastValues(onChange)).toEqual([options[1], options[0]]);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('passes the up-to-date row value and index to the actions after a reorder', async () => {
|
|
364
|
+
const { onEdit } = initializeTemplates();
|
|
365
|
+
|
|
366
|
+
await moveSortableElement({
|
|
367
|
+
direction: 'down',
|
|
368
|
+
handle: (await screen.findAllByTestId('drag-handle'))[0]
|
|
369
|
+
});
|
|
370
|
+
await waitFor(() => {
|
|
371
|
+
expect(getTemplateInputs()[0]).toHaveValue(options[1].name);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
await userEvent.click(screen.getAllByTestId('edit-template')[1]);
|
|
375
|
+
await waitFor(() => {
|
|
376
|
+
expect(onEdit).toHaveBeenCalledWith(options[0], 1);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('hides the drag handle when the list is not draggable', async () => {
|
|
381
|
+
initializeTemplates({ draggable: false });
|
|
382
|
+
|
|
383
|
+
expect(await screen.findAllByTestId('delete-row')).toHaveLength(2);
|
|
384
|
+
expect(screen.queryByTestId('drag-handle')).not.toBeInTheDocument();
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('lets the inputs take the remaining width and keeps the row buttons at a 32px pitch', async () => {
|
|
388
|
+
initializeTemplates();
|
|
389
|
+
|
|
390
|
+
await screen.findAllByTestId('drag-handle');
|
|
391
|
+
|
|
392
|
+
const buttons = getRowButtons(0);
|
|
393
|
+
|
|
394
|
+
expect(buttons).toHaveLength(3);
|
|
395
|
+
|
|
396
|
+
const lefts = buttons.map((button) => getRect(button).left);
|
|
397
|
+
|
|
398
|
+
lefts.slice(1).forEach((left, index) => {
|
|
399
|
+
expect(left - lefts[index]).toBe(32);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const rowFields = document.querySelectorAll('[data-row-fields]')[0];
|
|
403
|
+
|
|
404
|
+
expect(getRect(rowFields).width).toBeGreaterThan(500);
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
interface MacroRow {
|
|
409
|
+
isPassword: boolean;
|
|
410
|
+
name: string;
|
|
411
|
+
origin: 'direct' | 'fromTpl';
|
|
412
|
+
value: string;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const macros: Array<MacroRow> = [
|
|
416
|
+
{ isPassword: false, name: 'MACRO_A', origin: 'direct', value: 'a' },
|
|
417
|
+
{ isPassword: true, name: 'MACRO_B', origin: 'fromTpl', value: 'b' }
|
|
418
|
+
];
|
|
419
|
+
|
|
420
|
+
const emptyMacro: MacroRow = {
|
|
421
|
+
isPassword: false,
|
|
422
|
+
name: '',
|
|
423
|
+
origin: 'direct',
|
|
424
|
+
value: ''
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
const renderMacroRow = ({
|
|
428
|
+
setField,
|
|
429
|
+
value: macro
|
|
430
|
+
}: RenderRowParams<MacroRow>): JSX.Element => (
|
|
431
|
+
<div className="grid grid-cols-2 gap-2 @max-[600px]:grid-cols-1">
|
|
432
|
+
<TextField
|
|
433
|
+
dataTestId="macro-name"
|
|
434
|
+
fullWidth
|
|
435
|
+
label="Name"
|
|
436
|
+
onChange={(event): void => setField('name', event.target.value)}
|
|
437
|
+
value={macro.name}
|
|
438
|
+
/>
|
|
439
|
+
<TextField
|
|
440
|
+
dataTestId="macro-value"
|
|
441
|
+
fullWidth
|
|
442
|
+
label="Value"
|
|
443
|
+
onChange={(event): void => setField('value', event.target.value)}
|
|
444
|
+
type={macro.isPassword ? 'password' : 'text'}
|
|
445
|
+
value={macro.value}
|
|
446
|
+
/>
|
|
447
|
+
</div>
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
interface MacrosListProps {
|
|
451
|
+
copyOnChange: boolean;
|
|
452
|
+
onChangeSpy: Spy;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const MacrosList = ({
|
|
456
|
+
copyOnChange,
|
|
457
|
+
onChangeSpy
|
|
458
|
+
}: MacrosListProps): JSX.Element => {
|
|
459
|
+
const [values, setValues] = useState(macros);
|
|
460
|
+
|
|
461
|
+
const change = (nextValues: Array<MacroRow>): void => {
|
|
462
|
+
onChangeSpy(nextValues);
|
|
463
|
+
// Simulates a parent that stores a copy (new array and objects), like a
|
|
464
|
+
// form library that clones its state.
|
|
465
|
+
setValues(
|
|
466
|
+
copyOnChange ? nextValues.map((macro) => ({ ...macro })) : nextValues
|
|
467
|
+
);
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
return (
|
|
471
|
+
<>
|
|
472
|
+
<button
|
|
473
|
+
data-testid="reset"
|
|
474
|
+
onClick={(): void => setValues([emptyMacro])}
|
|
475
|
+
type="button"
|
|
476
|
+
>
|
|
477
|
+
Reset
|
|
478
|
+
</button>
|
|
479
|
+
<SortableEntriesList<MacroRow>
|
|
480
|
+
actions={({ index, value }) => [
|
|
481
|
+
{
|
|
482
|
+
icon: <VisibilityIcon />,
|
|
483
|
+
id: 'toggle-password',
|
|
484
|
+
label: 'Toggle password',
|
|
485
|
+
onClick: () =>
|
|
486
|
+
change(
|
|
487
|
+
values.map((macro, macroIndex) =>
|
|
488
|
+
macroIndex === index
|
|
489
|
+
? { ...macro, isPassword: !value.isPassword }
|
|
490
|
+
: macro
|
|
491
|
+
)
|
|
492
|
+
)
|
|
493
|
+
}
|
|
494
|
+
]}
|
|
495
|
+
createValue={(): MacroRow => emptyMacro}
|
|
496
|
+
getRowClassName={({ value }) => `origin-${value.origin}`}
|
|
497
|
+
onChange={change}
|
|
498
|
+
renderRow={renderMacroRow}
|
|
499
|
+
values={values}
|
|
500
|
+
/>
|
|
501
|
+
</>
|
|
502
|
+
);
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const initializeMacros = ({
|
|
506
|
+
copyOnChange = false
|
|
507
|
+
}: {
|
|
508
|
+
copyOnChange?: boolean;
|
|
509
|
+
} = {}): { onChange: Spy } => {
|
|
510
|
+
const onChange = rs.fn();
|
|
511
|
+
|
|
512
|
+
render(<MacrosList copyOnChange={copyOnChange} onChangeSpy={onChange} />);
|
|
513
|
+
|
|
514
|
+
return { onChange };
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
describe('SortableEntriesList - multiple input rows', () => {
|
|
518
|
+
beforeEach(() => {
|
|
519
|
+
expect.hasAssertions();
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it('renders every input of every row', async () => {
|
|
523
|
+
initializeMacros();
|
|
524
|
+
|
|
525
|
+
await waitFor(() => {
|
|
526
|
+
expect(getInputs('macro-name')).toHaveLength(2);
|
|
527
|
+
});
|
|
528
|
+
expect(getInputs('macro-value')).toHaveLength(2);
|
|
529
|
+
expect(getInputs('macro-value')[1]).toHaveAttribute('type', 'password');
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it('keeps the focus in the edited input while typing and reports a structured value', async () => {
|
|
533
|
+
const { onChange } = initializeMacros();
|
|
534
|
+
|
|
535
|
+
await waitFor(() => {
|
|
536
|
+
expect(getInputs('macro-name')).toHaveLength(2);
|
|
537
|
+
});
|
|
538
|
+
await userEvent.clear(getInputs('macro-name')[0]);
|
|
539
|
+
await userEvent.type(getInputs('macro-name')[0], 'HOST_IP');
|
|
540
|
+
await waitFor(() => {
|
|
541
|
+
expect(getFocused()).toHaveValue('HOST_IP');
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
await userEvent.clear(getInputs('macro-value')[0]);
|
|
545
|
+
await userEvent.type(getInputs('macro-value')[0], '10.0.0.1');
|
|
546
|
+
await waitFor(() => {
|
|
547
|
+
expect(getFocused()).toHaveValue('10.0.0.1');
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
await waitFor(() => {
|
|
551
|
+
expect(getLastValues(onChange)).toEqual([
|
|
552
|
+
{
|
|
553
|
+
isPassword: false,
|
|
554
|
+
name: 'HOST_IP',
|
|
555
|
+
origin: 'direct',
|
|
556
|
+
value: '10.0.0.1'
|
|
557
|
+
},
|
|
558
|
+
macros[1]
|
|
559
|
+
]);
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('keeps the focus when a new row is typed into right after being added', async () => {
|
|
564
|
+
const { onChange } = initializeMacros();
|
|
565
|
+
|
|
566
|
+
await userEvent.click(await screen.findByTestId('Add'));
|
|
567
|
+
await waitFor(() => {
|
|
568
|
+
expect(getFocused()).toHaveAttribute('data-testid', 'macro-name');
|
|
569
|
+
});
|
|
570
|
+
await userEvent.type(getFocused(), 'NEW');
|
|
571
|
+
await userEvent.type(getInputs('macro-value')[2], 'value');
|
|
572
|
+
|
|
573
|
+
await waitFor(() => {
|
|
574
|
+
expect(getLastValues<MacroRow>(onChange)[2]).toEqual({
|
|
575
|
+
...emptyMacro,
|
|
576
|
+
name: 'NEW',
|
|
577
|
+
value: 'value'
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
it('keeps the rows and the focus stable when the parent stores a copy of the values', async () => {
|
|
583
|
+
const { onChange } = initializeMacros({ copyOnChange: true });
|
|
584
|
+
|
|
585
|
+
await waitFor(() => {
|
|
586
|
+
expect(getInputs('macro-name')).toHaveLength(2);
|
|
587
|
+
});
|
|
588
|
+
await userEvent.clear(getInputs('macro-name')[1]);
|
|
589
|
+
await userEvent.type(getInputs('macro-name')[1], 'RENAMED');
|
|
590
|
+
await waitFor(() => {
|
|
591
|
+
expect(getFocused()).toHaveValue('RENAMED');
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
await userEvent.click(screen.getByTestId('Add'));
|
|
595
|
+
await waitFor(() => {
|
|
596
|
+
expect(getFocused()).toHaveAttribute('data-testid', 'macro-name');
|
|
597
|
+
});
|
|
598
|
+
await userEvent.type(getFocused(), 'NEW');
|
|
599
|
+
|
|
600
|
+
await waitFor(() => {
|
|
601
|
+
expect(getLastValues<MacroRow>(onChange).map(({ name }) => name)).toEqual(
|
|
602
|
+
['MACRO_A', 'RENAMED', 'NEW']
|
|
603
|
+
);
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it('renders the new values when the parent replaces them', async () => {
|
|
608
|
+
initializeMacros();
|
|
609
|
+
|
|
610
|
+
await userEvent.click(await screen.findByTestId('reset'));
|
|
611
|
+
|
|
612
|
+
await waitFor(() => {
|
|
613
|
+
expect(getRows()).toHaveLength(1);
|
|
614
|
+
});
|
|
615
|
+
expect(getInputs('macro-name')[0]).toHaveValue('');
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('lets a row action change its own row', async () => {
|
|
619
|
+
initializeMacros();
|
|
620
|
+
|
|
621
|
+
await userEvent.click((await screen.findAllByTestId('toggle-password'))[0]);
|
|
622
|
+
|
|
623
|
+
await waitFor(() => {
|
|
624
|
+
expect(getInputs('macro-value')[0]).toHaveAttribute('type', 'password');
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it('applies the class returned by getRowClassName to each row', async () => {
|
|
629
|
+
initializeMacros();
|
|
630
|
+
|
|
631
|
+
await screen.findAllByRole('listitem');
|
|
632
|
+
|
|
633
|
+
expect(getRows()[0]).toHaveClass('origin-direct');
|
|
634
|
+
expect(getRows()[1]).toHaveClass('origin-fromTpl');
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
const buildMacros = (count: number): Array<MacroRow> =>
|
|
639
|
+
Array.from({ length: count }, (_, index) => ({
|
|
640
|
+
...emptyMacro,
|
|
641
|
+
name: `MACRO_${index + 1}`
|
|
642
|
+
}));
|
|
643
|
+
|
|
644
|
+
const panelWidths = [1400, 900, 720, 560];
|
|
645
|
+
|
|
646
|
+
interface ResizablePanelProps {
|
|
647
|
+
count: number;
|
|
648
|
+
maxVisibleRows?: number;
|
|
649
|
+
onChangeSpy: Spy;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// The panel width is state, so changing it resizes the mounted list at runtime.
|
|
653
|
+
const ResizablePanel = ({
|
|
654
|
+
count,
|
|
655
|
+
maxVisibleRows,
|
|
656
|
+
onChangeSpy
|
|
657
|
+
}: ResizablePanelProps): JSX.Element => {
|
|
658
|
+
const [width, setWidth] = useState(panelWidths[0]);
|
|
659
|
+
|
|
660
|
+
return (
|
|
661
|
+
<div className="p-4">
|
|
662
|
+
{panelWidths.map((panelWidth) => (
|
|
663
|
+
<button
|
|
664
|
+
data-testid={`width-${panelWidth}`}
|
|
665
|
+
key={panelWidth}
|
|
666
|
+
onClick={(): void => setWidth(panelWidth)}
|
|
667
|
+
type="button"
|
|
668
|
+
>
|
|
669
|
+
{panelWidth}
|
|
670
|
+
</button>
|
|
671
|
+
))}
|
|
672
|
+
<div data-testid="panel" style={{ width }}>
|
|
673
|
+
<ControlledList<MacroRow>
|
|
674
|
+
actions={(): Array<SortableRowAction> => [
|
|
675
|
+
{
|
|
676
|
+
icon: <VisibilityIcon />,
|
|
677
|
+
id: 'toggle-password',
|
|
678
|
+
label: 'Toggle password',
|
|
679
|
+
onClick: () => undefined
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
icon: <DescriptionOutlinedIcon />,
|
|
683
|
+
id: 'macro-description',
|
|
684
|
+
label: 'Description',
|
|
685
|
+
onClick: () => undefined
|
|
686
|
+
}
|
|
687
|
+
]}
|
|
688
|
+
createValue={(): MacroRow => emptyMacro}
|
|
689
|
+
initialValues={buildMacros(count)}
|
|
690
|
+
maxVisibleRows={maxVisibleRows}
|
|
691
|
+
onChangeSpy={onChangeSpy}
|
|
692
|
+
renderRow={renderMacroRow}
|
|
693
|
+
/>
|
|
694
|
+
</div>
|
|
695
|
+
</div>
|
|
696
|
+
);
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
interface InitializePanelProps {
|
|
700
|
+
count: number;
|
|
701
|
+
maxVisibleRows?: number;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// The viewport is 1280x1000 and the panel widths are explicit, so the 1400px
|
|
705
|
+
// panel overflows the page: the list lays out against the panel, not the
|
|
706
|
+
// viewport.
|
|
707
|
+
const initializePanel = ({
|
|
708
|
+
count,
|
|
709
|
+
maxVisibleRows
|
|
710
|
+
}: InitializePanelProps): { onChange: Spy } => {
|
|
711
|
+
const onChange = rs.fn();
|
|
712
|
+
|
|
713
|
+
render(
|
|
714
|
+
<ResizablePanel
|
|
715
|
+
count={count}
|
|
716
|
+
maxVisibleRows={maxVisibleRows}
|
|
717
|
+
onChangeSpy={onChange}
|
|
718
|
+
/>
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
return { onChange };
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
const getScrollArea = (): Promise<HTMLElement> =>
|
|
725
|
+
screen.findByTestId('sortable-entries-scroll');
|
|
726
|
+
|
|
727
|
+
const getLastNames = (onChange: Spy): Array<string> =>
|
|
728
|
+
getLastValues<MacroRow>(onChange).map(({ name }) => name);
|
|
729
|
+
|
|
730
|
+
const getMaskImage = (element: HTMLElement): string =>
|
|
731
|
+
getComputedStyle(element).maskImage;
|
|
732
|
+
|
|
733
|
+
// The list is capped from a ResizeObserver callback. A check that it is not
|
|
734
|
+
// capped first waits for an observer of its own, delivered after the list's,
|
|
735
|
+
// then a frame for the page to apply the cap.
|
|
736
|
+
const waitForResizeObservers = (element: HTMLElement): Promise<void> =>
|
|
737
|
+
new Promise((resolve) => {
|
|
738
|
+
const observer = new ResizeObserver(() => {
|
|
739
|
+
observer.disconnect();
|
|
740
|
+
requestAnimationFrame(() => setTimeout(resolve, 0));
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
observer.observe(element);
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
describe('SortableEntriesList - capped list', () => {
|
|
747
|
+
beforeEach(() => {
|
|
748
|
+
expect.hasAssertions();
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it('shows maxVisibleRows rows plus half of the next one, and keeps the add button outside the scroll area', async () => {
|
|
752
|
+
initializePanel({ count: 15, maxVisibleRows: 10 });
|
|
753
|
+
|
|
754
|
+
const scrollArea = await getScrollArea();
|
|
755
|
+
|
|
756
|
+
await waitFor(() => {
|
|
757
|
+
const rows = scrollArea.querySelectorAll('li');
|
|
758
|
+
const visibleBottom = getRect(scrollArea).bottom;
|
|
759
|
+
|
|
760
|
+
expect(scrollArea.scrollHeight).toBeGreaterThan(scrollArea.clientHeight);
|
|
761
|
+
expect(getRect(rows[9]).bottom).toBeLessThanOrEqual(visibleBottom);
|
|
762
|
+
expect(
|
|
763
|
+
Math.abs(
|
|
764
|
+
visibleBottom - getRect(rows[10]).top - getRect(rows[10]).height / 2
|
|
765
|
+
)
|
|
766
|
+
).toBeLessThanOrEqual(1);
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
const addButton = screen.getByTestId('Add');
|
|
770
|
+
|
|
771
|
+
expect(addButton).toBeVisible();
|
|
772
|
+
expect(
|
|
773
|
+
addButton.closest('[data-testid="sortable-entries-scroll"]')
|
|
774
|
+
).not.toBeInTheDocument();
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
it('fades the cut-off row only while rows are hidden below', async () => {
|
|
778
|
+
initializePanel({ count: 15, maxVisibleRows: 10 });
|
|
779
|
+
|
|
780
|
+
const scrollArea = await getScrollArea();
|
|
781
|
+
|
|
782
|
+
await waitFor(() => {
|
|
783
|
+
expect(getMaskImage(scrollArea)).not.toBe('none');
|
|
784
|
+
});
|
|
785
|
+
scrollArea.scrollTo({ top: scrollArea.scrollHeight });
|
|
786
|
+
await waitFor(() => {
|
|
787
|
+
expect(getMaskImage(scrollArea)).toBe('none');
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
it('does not cap a list shorter than maxVisibleRows', async () => {
|
|
792
|
+
initializePanel({ count: 5, maxVisibleRows: 10 });
|
|
793
|
+
|
|
794
|
+
const scrollArea = await getScrollArea();
|
|
795
|
+
|
|
796
|
+
await waitForResizeObservers(scrollArea);
|
|
797
|
+
|
|
798
|
+
expect(getComputedStyle(scrollArea).maxHeight).toBe('none');
|
|
799
|
+
expect(scrollArea.scrollHeight).toBe(scrollArea.clientHeight);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
it('focuses and scrolls to a row added at the end of a capped list', async () => {
|
|
803
|
+
initializePanel({ count: 15, maxVisibleRows: 10 });
|
|
804
|
+
|
|
805
|
+
const scrollArea = await getScrollArea();
|
|
806
|
+
|
|
807
|
+
await waitFor(() => {
|
|
808
|
+
expect(scrollArea.scrollHeight).toBeGreaterThan(scrollArea.clientHeight);
|
|
809
|
+
});
|
|
810
|
+
await userEvent.click(screen.getByTestId('Add'));
|
|
811
|
+
|
|
812
|
+
await waitFor(() => {
|
|
813
|
+
expect(getRows()).toHaveLength(16);
|
|
814
|
+
});
|
|
815
|
+
await waitFor(() => {
|
|
816
|
+
const input = getFocused();
|
|
817
|
+
|
|
818
|
+
expect(input).toHaveAttribute('data-testid', 'macro-name');
|
|
819
|
+
|
|
820
|
+
const scrollAreaRect = getRect(
|
|
821
|
+
input.closest('[data-testid="sortable-entries-scroll"]') as Element
|
|
822
|
+
);
|
|
823
|
+
const inputRect = getRect(input);
|
|
824
|
+
|
|
825
|
+
expect(inputRect.top).toBeGreaterThanOrEqual(scrollAreaRect.top);
|
|
826
|
+
expect(inputRect.bottom).toBeLessThanOrEqual(scrollAreaRect.bottom);
|
|
827
|
+
});
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
it('scrolls to the focused new row when adding it makes the list reach the cap', async () => {
|
|
831
|
+
initializePanel({ count: 10, maxVisibleRows: 10 });
|
|
832
|
+
|
|
833
|
+
const scrollArea = await getScrollArea();
|
|
834
|
+
|
|
835
|
+
await waitForResizeObservers(scrollArea);
|
|
836
|
+
expect(getComputedStyle(scrollArea).maxHeight).toBe('none');
|
|
837
|
+
await userEvent.click(screen.getByTestId('Add'));
|
|
838
|
+
|
|
839
|
+
await waitFor(() => {
|
|
840
|
+
expect(getRows()).toHaveLength(11);
|
|
841
|
+
});
|
|
842
|
+
await waitFor(() => {
|
|
843
|
+
expect(getComputedStyle(scrollArea).maxHeight).not.toBe('none');
|
|
844
|
+
});
|
|
845
|
+
await waitFor(() => {
|
|
846
|
+
expect(getFocused()).toHaveAttribute('data-testid', 'macro-name');
|
|
847
|
+
|
|
848
|
+
const row = getFocused().closest('li') as HTMLElement;
|
|
849
|
+
const scrollAreaRect = getRect(
|
|
850
|
+
row.closest('[data-testid="sortable-entries-scroll"]') as Element
|
|
851
|
+
);
|
|
852
|
+
|
|
853
|
+
// 1px tolerance for sub-pixel row heights.
|
|
854
|
+
expect(getRect(row).bottom).toBeLessThanOrEqual(
|
|
855
|
+
scrollAreaRect.bottom + 1
|
|
856
|
+
);
|
|
857
|
+
});
|
|
858
|
+
await waitFor(() => {
|
|
859
|
+
expect(getMaskImage(scrollArea)).toBe('none');
|
|
860
|
+
});
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
it('moves a row past the visible area with the keyboard, scrolling the list', async () => {
|
|
864
|
+
const { onChange } = initializePanel({ count: 15, maxVisibleRows: 10 });
|
|
865
|
+
|
|
866
|
+
await moveSortableElement({
|
|
867
|
+
direction: 'down',
|
|
868
|
+
handle: (await screen.findAllByTestId('drag-handle'))[0],
|
|
869
|
+
times: 12
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
await waitFor(() => {
|
|
873
|
+
expect(getLastNames(onChange).indexOf('MACRO_1')).toBe(12);
|
|
874
|
+
});
|
|
875
|
+
await waitFor(async () => {
|
|
876
|
+
expect((await getScrollArea()).scrollTop).toBeGreaterThan(0);
|
|
877
|
+
});
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
it('auto-scrolls while a row is dragged with the pointer near the bottom edge', async () => {
|
|
881
|
+
const { onChange } = initializePanel({ count: 15, maxVisibleRows: 10 });
|
|
882
|
+
|
|
883
|
+
const handle = (await screen.findAllByTestId('drag-handle'))[0];
|
|
884
|
+
const scrollArea = await getScrollArea();
|
|
885
|
+
|
|
886
|
+
// The list is capped once its rows are measured.
|
|
887
|
+
await waitFor(() => {
|
|
888
|
+
expect(scrollArea.scrollHeight).toBeGreaterThan(scrollArea.clientHeight);
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
const { left, top, width, height } = getRect(handle);
|
|
892
|
+
const pointer = {
|
|
893
|
+
button: 0,
|
|
894
|
+
clientX: left + width / 2,
|
|
895
|
+
isPrimary: true,
|
|
896
|
+
pointerId: 1
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
// A synthetic pointer drag: dnd-kit listens to the pointer events of the
|
|
900
|
+
// document once the drag started.
|
|
901
|
+
fireEvent.pointerDown(handle, { ...pointer, clientY: top + height / 2 });
|
|
902
|
+
|
|
903
|
+
const clientY = getRect(scrollArea).bottom - 5;
|
|
904
|
+
|
|
905
|
+
fireEvent.pointerMove(document, { ...pointer, clientY });
|
|
906
|
+
await waitFor(() => {
|
|
907
|
+
expect(scrollArea.scrollTop).toBeGreaterThan(100);
|
|
908
|
+
});
|
|
909
|
+
fireEvent.pointerMove(document, { ...pointer, clientY });
|
|
910
|
+
fireEvent.pointerUp(document, { ...pointer, clientY });
|
|
911
|
+
|
|
912
|
+
await waitFor(() => {
|
|
913
|
+
expect(getLastNames(onChange).indexOf('MACRO_1')).toBeGreaterThan(10);
|
|
914
|
+
});
|
|
915
|
+
});
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
describe('SortableEntriesList - container responsive rows', () => {
|
|
919
|
+
const getFieldRects = (): Array<DOMRect> =>
|
|
920
|
+
Array.from(getRows()[0].querySelectorAll('.MuiFormControl-root')).map(
|
|
921
|
+
getRect
|
|
922
|
+
);
|
|
923
|
+
|
|
924
|
+
beforeEach(() => {
|
|
925
|
+
expect.hasAssertions();
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
it('keeps the fields and the four buttons on one line at 1400, 900 and 720px, resized at runtime', async () => {
|
|
929
|
+
initializePanel({ count: 3 });
|
|
930
|
+
|
|
931
|
+
for (const width of [1400, 900, 720, 1400]) {
|
|
932
|
+
await userEvent.click(await screen.findByTestId(`width-${width}`));
|
|
933
|
+
await waitFor(() => {
|
|
934
|
+
expect(getComputedStyle(screen.getByTestId('panel')).width).toBe(
|
|
935
|
+
`${width}px`
|
|
936
|
+
);
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
await waitFor(() => {
|
|
940
|
+
const [name, value] = getFieldRects();
|
|
941
|
+
|
|
942
|
+
expect(value.top).toBe(name.top);
|
|
943
|
+
expect(name.width).toBeGreaterThanOrEqual(275);
|
|
944
|
+
expect(value.width).toBeGreaterThanOrEqual(275);
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
await waitFor(() => {
|
|
948
|
+
const buttons = getRowButtons(0);
|
|
949
|
+
|
|
950
|
+
expect(buttons).toHaveLength(4);
|
|
951
|
+
|
|
952
|
+
const rects = buttons.map(getRect);
|
|
953
|
+
const panelRight = getRect(screen.getByTestId('panel')).right;
|
|
954
|
+
|
|
955
|
+
rects.slice(1).forEach((rect, index) => {
|
|
956
|
+
expect(rect.top).toBe(rects[index].top);
|
|
957
|
+
expect(rect.left - rects[index].left).toBe(32);
|
|
958
|
+
});
|
|
959
|
+
expect(rects[3].right).toBeLessThanOrEqual(panelRight);
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
it('stacks the fields when the list gets narrower than 600px, whatever the viewport', async () => {
|
|
965
|
+
initializePanel({ count: 3 });
|
|
966
|
+
|
|
967
|
+
await userEvent.click(await screen.findByTestId('width-560'));
|
|
968
|
+
await waitFor(() => {
|
|
969
|
+
const [name, value] = getFieldRects();
|
|
970
|
+
|
|
971
|
+
expect(value.top).toBeGreaterThan(name.bottom);
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
await userEvent.click(screen.getByTestId('width-900'));
|
|
975
|
+
await waitFor(() => {
|
|
976
|
+
const [name, value] = getFieldRects();
|
|
977
|
+
|
|
978
|
+
expect(value.top).toBe(name.top);
|
|
979
|
+
});
|
|
980
|
+
});
|
|
981
|
+
});
|