@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.
Files changed (56) hide show
  1. package/package.json +6 -3
  2. package/public/mockServiceWorker.js +31 -8
  3. package/src/Dashboard/Dashboard.browser.test.tsx +107 -0
  4. package/src/Form/Form.browser.test.tsx +445 -0
  5. package/src/Form/Inputs/ExclusiveCheckboxGroup.browser.test.tsx +304 -0
  6. package/src/Form/Inputs/SegmentedButtons.browser.test.tsx +113 -0
  7. package/src/InputField/Number/Number.browser.test.tsx +109 -0
  8. package/src/InputField/Select/Autocomplete/Connected/Multi/MultiConnectedAutocompleteField.browser.test.tsx +250 -0
  9. package/src/Listing/Listing.browser.test.tsx +476 -0
  10. package/src/Module/Module.browser.test.tsx +145 -0
  11. package/src/Pagination/Pagination.browser.test.tsx +184 -0
  12. package/src/RichTextEditor/RichTextEditor.browser.test.tsx +567 -0
  13. package/src/RichTextEditor/plugins/FloatingLinkEditorPlugin.tsx +3 -1
  14. package/src/TimePeriods/ResolutionTimePeriod.browser.test.tsx +143 -0
  15. package/src/TimePeriods/TimePeriods.browser.test.tsx +468 -0
  16. package/src/Typography/FluidTypography/FluidTypography.browser.test.tsx +54 -0
  17. package/src/api/useMutationQuery/useMutationQuery.browser.test.tsx +154 -0
  18. package/src/components/CollapsibleItem/CollapsibleItem.browser.test.tsx +120 -0
  19. package/src/components/CopyCommand/CopyCommand.browser.test.tsx +287 -0
  20. package/src/components/CrudPage/CrudPage.browser.test.tsx +659 -0
  21. package/src/components/DataTable/DataTable.browser.test.tsx +257 -0
  22. package/src/components/Form/AccessRights/AccessRights.browser.test.tsx +527 -0
  23. package/src/components/ItemComposition/ItemComposition.browser.test.tsx +172 -0
  24. package/src/components/Layout/PageLayout.browser.test.tsx +114 -0
  25. package/src/components/Modal/ConfirmationModal/ConfirmationModal.browser.test.tsx +237 -0
  26. package/src/components/SortableEntriesList/SortableEntriesList.browser.test.tsx +981 -0
  27. package/src/components/Tabs/Tabs.browser.test.tsx +115 -0
  28. package/src/utils/useFullscreen/Fullscreen.browser.test.tsx +176 -0
  29. package/src/utils/useLocale/useLocale.browser.test.tsx +49 -0
  30. package/src/utils/useLocaleTimezoneDate/{LocaleTimezoneDate.cypress.spec.tsx → LocaleTimezoneDate.browser.test.tsx} +24 -11
  31. package/src/Dashboard/Dashboard.cypress.spec.tsx +0 -71
  32. package/src/Form/Form.cypress.spec.tsx +0 -306
  33. package/src/Form/Inputs/ExclusiveCheckboxGroup.cypress.spec.tsx +0 -236
  34. package/src/Form/Inputs/SegmentedButtons.cypress.spec.tsx +0 -101
  35. package/src/InputField/Number/Number.cypress.spec.tsx +0 -89
  36. package/src/InputField/Select/Autocomplete/Connected/Multi/MultiConnectedAutocompleteField.cypress.spec.tsx +0 -182
  37. package/src/Listing/Listing.cypress.spec.tsx +0 -349
  38. package/src/Module/Module.cypress.spec.tsx +0 -127
  39. package/src/Pagination/Pagination.cypress.spec.tsx +0 -140
  40. package/src/RichTextEditor/RichTextEditor.cypress.spec.tsx +0 -408
  41. package/src/TimePeriods/ResolutionTimePeriod.cypress.spec.tsx +0 -74
  42. package/src/TimePeriods/TimePeriods.cypress.spec.tsx +0 -488
  43. package/src/Typography/FluidTypography/FluidTypography.cypress.spec.tsx +0 -27
  44. package/src/api/useMutationQuery/useMutationQuery.cypress.spec.tsx +0 -93
  45. package/src/components/CollapsibleItem/CollapsibleItem.cypress.spec.tsx +0 -76
  46. package/src/components/CopyCommand/CopyCommand.cypress.spec.tsx +0 -167
  47. package/src/components/CrudPage/CrudPage.cypress.spec.tsx +0 -561
  48. package/src/components/DataTable/DataTable.cypress.spec.tsx +0 -174
  49. package/src/components/Form/AccessRights/AccessRights.cypress.spec.tsx +0 -376
  50. package/src/components/ItemComposition/ItemComposition.cypress.spec.tsx +0 -112
  51. package/src/components/Layout/PageLayout.cypress.spec.tsx +0 -64
  52. package/src/components/Modal/ConfirmationModal/ConfirmationModal.cypress.spec.tsx +0 -171
  53. package/src/components/SortableEntriesList/SortableEntriesList.cypress.spec.tsx +0 -792
  54. package/src/components/Tabs/Tabs.cypress.spec.tsx +0 -69
  55. package/src/utils/useFullscreen/Fullscreen.cypress.spec.tsx +0 -130
  56. package/src/utils/useLocale/useLocale.cypress.spec.tsx +0 -40
@@ -0,0 +1,567 @@
1
+ import { render } from '@test-setup/render';
2
+ import {
3
+ cleanup,
4
+ fireEvent,
5
+ screen,
6
+ waitFor,
7
+ within
8
+ } from '@testing-library/react';
9
+ import userEvent from '@testing-library/user-event';
10
+
11
+ import { standardMacros } from './plugins/ToolbarPlugin/MacrosButton';
12
+ import RichTextEditor from './RichTextEditor';
13
+ import { labelAlignPicker } from './translatedLabels';
14
+
15
+ interface CheckElementStyleOnRichTextEditorProps {
16
+ check: boolean;
17
+ element: HTMLElement;
18
+ }
19
+
20
+ const mockInititalStateToEditor =
21
+ '{"root": {"children": [{"children": [{"detail": 0,"format": 0,"mode": "normal","style": "","text": "test Cypress","type": "text","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "paragraph","version": 1}],"direction": "ltr","format": "","indent": 0,"type": "root","version": 1}}';
22
+
23
+ const getEditor = (namespace = 'RichTextEditor'): HTMLElement =>
24
+ screen.getByTestId(namespace);
25
+
26
+ // The first `tag` element, in document order, whose text contains `text`.
27
+ const getElementContaining = (
28
+ container: HTMLElement,
29
+ tag: string,
30
+ text: string
31
+ ): HTMLElement | undefined =>
32
+ Array.from(container.querySelectorAll<HTMLElement>(tag)).find((element) =>
33
+ element.textContent?.includes(text)
34
+ );
35
+
36
+ const checkElementStyleOnRichTextEditor = async ({
37
+ element,
38
+ check
39
+ }: CheckElementStyleOnRichTextEditorProps): Promise<void> => {
40
+ expect(element).toBeVisible();
41
+
42
+ if (check) {
43
+ await waitFor(() => {
44
+ const strong = getElementContaining(element, 'strong', 'cypress test');
45
+
46
+ expect(strong).toBeDefined();
47
+ expect(getComputedStyle(strong as HTMLElement).fontStyle).toMatch(
48
+ /italic/
49
+ );
50
+ expect(getComputedStyle(strong as HTMLElement).textDecoration).toMatch(
51
+ /underline line-through/
52
+ );
53
+ });
54
+
55
+ return;
56
+ }
57
+
58
+ await waitFor(() => {
59
+ const paragraph = getElementContaining(element, 'p', 'cypress test');
60
+
61
+ expect(paragraph).toBeDefined();
62
+ expect(getComputedStyle(paragraph as HTMLElement).fontStyle).not.toMatch(
63
+ /italic/
64
+ );
65
+ expect(
66
+ getComputedStyle(paragraph as HTMLElement).textDecoration
67
+ ).not.toMatch(/underline line-through/);
68
+ });
69
+ };
70
+
71
+ // Selects the whole content, as `.focus().type('{selectAll}')`.
72
+ const selectAll = async (editor: HTMLElement): Promise<void> => {
73
+ editor.focus();
74
+ await userEvent.keyboard('{Control>}a{/Control}');
75
+ };
76
+
77
+ // A menu item click closes the menu: wait for it to be gone before the next
78
+ // one, so the queries do not pick an item of the closing menu.
79
+ const waitForMenusToClose = async (): Promise<void> => {
80
+ await waitFor(() => {
81
+ expect(screen.queryByRole('menu')).not.toBeInTheDocument();
82
+ });
83
+ };
84
+
85
+ const clickMenuEntry = async (
86
+ menuLabel: string,
87
+ entryLabel: string
88
+ ): Promise<void> => {
89
+ await userEvent.click(screen.getByLabelText(menuLabel));
90
+ await userEvent.click(await screen.findByLabelText(entryLabel));
91
+ await waitForMenusToClose();
92
+ };
93
+
94
+ const insertLink = async (editor: HTMLElement): Promise<void> => {
95
+ await userEvent.click(screen.getByLabelText('link'));
96
+
97
+ const savedLink = await screen.findByLabelText('Saved link');
98
+
99
+ expect(savedLink).toHaveTextContent(/^https:\/\/$/);
100
+
101
+ // The link editor opens 30 px under the selected text, not where its
102
+ // position starts (0, 0).
103
+ const linkEditor = savedLink.closest('.MuiPaper-root') as HTMLElement;
104
+ const text = screen.getByText('cypress').getBoundingClientRect();
105
+ const root = editor.getBoundingClientRect();
106
+
107
+ await waitFor(() => {
108
+ const { m41: x, m42: y } = new DOMMatrix(
109
+ getComputedStyle(linkEditor).transform
110
+ );
111
+
112
+ expect(Math.abs(x - (text.x - root.x))).toBeLessThan(1);
113
+ expect(Math.abs(y - (text.bottom - root.y + 30))).toBeLessThan(1);
114
+ });
115
+
116
+ await userEvent.click(screen.getByLabelText('Edit link'));
117
+
118
+ const input = await waitFor(() => {
119
+ const field = document.querySelector<HTMLInputElement>('#InputLinkField');
120
+
121
+ expect(field).not.toBeNull();
122
+
123
+ return field as HTMLInputElement;
124
+ });
125
+
126
+ await userEvent.type(input, 'www.centreon.com');
127
+ await userEvent.type(input, '{Enter}');
128
+
129
+ expect(editor).toBeInTheDocument();
130
+ };
131
+
132
+ describe('Rich Text Editor', () => {
133
+ beforeEach(() => {
134
+ expect.hasAssertions();
135
+ });
136
+
137
+ describe('Editable Rich Text Editor', () => {
138
+ beforeEach(() => {
139
+ render(<RichTextEditor editable />);
140
+ });
141
+
142
+ it('displays all elements when RichTextEditor is called with required props', async () => {
143
+ expect(await screen.findByLabelText('Undo')).toBeVisible();
144
+ expect(screen.getByLabelText('Redo')).toBeVisible();
145
+ expect(screen.getByLabelText('align picker')).toBeVisible();
146
+ expect(screen.getByLabelText('block')).toBeVisible();
147
+ expect(screen.getByLabelText('format')).toBeVisible();
148
+ expect(screen.getByLabelText('link')).toBeVisible();
149
+ expect(screen.getByLabelText('list')).toBeVisible();
150
+
151
+ const editor = screen.getByLabelText('RichTextEditor');
152
+
153
+ expect(editor).toBeVisible();
154
+ expect(editor).toHaveTextContent(/^$/);
155
+
156
+ expect(
157
+ within(editor.parentElement as HTMLElement).getByText('Type here...')
158
+ ).toBeVisible();
159
+
160
+ // Nothing to undo nor redo yet, and no menu open.
161
+ expect(screen.getByLabelText('Undo')).toBeDisabled();
162
+ expect(screen.getByLabelText('Redo')).toBeDisabled();
163
+ expect(screen.queryByRole('menu')).not.toBeInTheDocument();
164
+ });
165
+
166
+ it('displays all format actions when the format button is clicked', async () => {
167
+ await userEvent.click(screen.getByLabelText('format'));
168
+
169
+ expect(await screen.findByLabelText('bold')).toBeVisible();
170
+ expect(screen.getByLabelText('italic')).toBeVisible();
171
+ expect(screen.getByLabelText('underline')).toBeVisible();
172
+ expect(screen.getByLabelText('strikethrough')).toBeVisible();
173
+
174
+ // The four actions in that order, in the menu of the highlighted format
175
+ // button, none of them active.
176
+ const items = within(screen.getByRole('menu')).getAllByRole('menuitem');
177
+
178
+ expect(
179
+ items.map((item) =>
180
+ item.querySelector('svg')?.getAttribute('aria-label')
181
+ )
182
+ ).toEqual(['bold', 'italic', 'underline', 'strikethrough']);
183
+ items.forEach((item) => {
184
+ expect(item).toHaveAttribute('data-is-active', 'false');
185
+ });
186
+ expect(screen.getByLabelText('format')).toHaveAttribute(
187
+ 'data-is-active',
188
+ 'true'
189
+ );
190
+ });
191
+
192
+ it('displays changes with undo redo buttons', async () => {
193
+ const editor = getEditor();
194
+
195
+ await userEvent.type(editor, 'cypress test');
196
+ await waitFor(() => {
197
+ expect(editor).toHaveTextContent(/^cypress test$/);
198
+ });
199
+
200
+ await userEvent.click(screen.getByLabelText('Undo'));
201
+
202
+ await waitFor(() => {
203
+ expect(editor).toHaveTextContent(/^$/);
204
+ });
205
+
206
+ await userEvent.click(screen.getByLabelText('Redo'));
207
+
208
+ await waitFor(() => {
209
+ expect(editor).toHaveTextContent(/^cypress test$/);
210
+ });
211
+ });
212
+
213
+ it('displays changes with bold, italic, underline and strikethrough buttons', async () => {
214
+ const editor = getEditor();
215
+
216
+ await userEvent.type(editor, 'cypress test');
217
+
218
+ await selectAll(editor);
219
+
220
+ await clickMenuEntry('format', 'bold');
221
+ await clickMenuEntry('format', 'italic');
222
+ await clickMenuEntry('format', 'underline');
223
+ await clickMenuEntry('format', 'strikethrough');
224
+
225
+ await checkElementStyleOnRichTextEditor({
226
+ check: true,
227
+ element: getEditor()
228
+ });
229
+
230
+ await clickMenuEntry('format', 'bold');
231
+ await clickMenuEntry('format', 'italic');
232
+ await clickMenuEntry('format', 'underline');
233
+ await clickMenuEntry('format', 'strikethrough');
234
+
235
+ await checkElementStyleOnRichTextEditor({
236
+ check: false,
237
+ element: getEditor()
238
+ });
239
+ });
240
+
241
+ it('adds link with an url when link button is clicked', async () => {
242
+ const editor = getEditor();
243
+
244
+ await userEvent.type(editor, 'cypress');
245
+ await selectAll(editor);
246
+
247
+ await insertLink(editor);
248
+
249
+ await waitFor(() => {
250
+ const link = screen.getByText('cypress').parentElement as HTMLElement;
251
+
252
+ expect(link.getAttribute('href')).toMatch(
253
+ /^https:\/\/www.centreon.com/
254
+ );
255
+ expect(link.getAttribute('rel')).toMatch(/^noreferrer/);
256
+ expect(link.getAttribute('target')).toMatch(/_blank/);
257
+ });
258
+
259
+ await selectAll(editor);
260
+
261
+ await userEvent.click(screen.getByLabelText('link'));
262
+
263
+ await waitFor(() => {
264
+ const element = screen.getByText('cypress')
265
+ .parentElement as HTMLElement;
266
+
267
+ expect(
268
+ getElementContaining(
269
+ element.parentElement as HTMLElement,
270
+ 'p',
271
+ 'cypress'
272
+ )
273
+ ).not.toHaveAttribute('href');
274
+ expect(element).not.toHaveAttribute('rel');
275
+ expect(element).not.toHaveAttribute('target');
276
+ expect(screen.getByText('cypress').closest('a')).toBeNull();
277
+ });
278
+ });
279
+
280
+ it('does not allow to open links in a new tab', async () => {
281
+ cleanup();
282
+ render(<RichTextEditor editable openLinkInNewTab={false} />);
283
+
284
+ const editor = getEditor();
285
+
286
+ await userEvent.type(editor, 'cypress');
287
+ await selectAll(editor);
288
+
289
+ await insertLink(editor);
290
+
291
+ await waitFor(() => {
292
+ const link = screen.getByText('cypress').parentElement as HTMLElement;
293
+
294
+ expect(link).toHaveAttribute('href');
295
+ });
296
+ expect(screen.getByText('cypress').parentElement).not.toHaveAttribute(
297
+ 'target'
298
+ );
299
+
300
+ editor.focus();
301
+ await userEvent.clear(editor);
302
+
303
+ editor.focus();
304
+ await userEvent.type(editor, 'https://centreon.com');
305
+
306
+ const autoLink = (await screen.findByText('https://centreon.com'))
307
+ .parentElement;
308
+
309
+ expect(autoLink).toHaveAttribute('href');
310
+ expect(autoLink).not.toHaveAttribute('target');
311
+ });
312
+ });
313
+
314
+ describe('Rich Text Editor not editable', () => {
315
+ beforeEach(() => {
316
+ render(
317
+ <RichTextEditor
318
+ editable={false}
319
+ initialEditorState={mockInititalStateToEditor}
320
+ />
321
+ );
322
+ });
323
+
324
+ it('displays editor when editable props is false and an initialState exist', () => {
325
+ expect(screen.queryByLabelText('Undo')).not.toBeInTheDocument();
326
+ expect(screen.queryByLabelText('Redo')).not.toBeInTheDocument();
327
+ expect(screen.queryByLabelText(labelAlignPicker)).not.toBeInTheDocument();
328
+ expect(screen.queryByLabelText('block')).not.toBeInTheDocument();
329
+ expect(screen.queryByLabelText('format')).not.toBeInTheDocument();
330
+ expect(screen.queryByLabelText('link')).not.toBeInTheDocument();
331
+ expect(screen.queryByLabelText('list')).not.toBeInTheDocument();
332
+ });
333
+ });
334
+
335
+ describe('Rich Text Editor is disabled', () => {
336
+ beforeEach(() => {
337
+ render(
338
+ <RichTextEditor
339
+ disabled
340
+ editable
341
+ initialEditorState={mockInititalStateToEditor}
342
+ />
343
+ );
344
+ });
345
+
346
+ it('displays editor when editable props is false and an initialState exist', async () => {
347
+ expect(await screen.findByLabelText('Undo')).toBeDisabled();
348
+ expect(screen.getByLabelText('Redo')).toBeDisabled();
349
+ expect(screen.getByLabelText(labelAlignPicker)).toBeDisabled();
350
+ expect(screen.getByLabelText('block')).toBeDisabled();
351
+ expect(screen.getByLabelText('format')).toBeDisabled();
352
+ expect(screen.getByLabelText('link')).toBeDisabled();
353
+ expect(screen.getByLabelText('list')).toBeDisabled();
354
+ });
355
+ });
356
+
357
+ describe('Custom style editable rich text editor ', () => {
358
+ beforeEach(() => {
359
+ render(
360
+ <RichTextEditor
361
+ editable
362
+ minInputHeight={200}
363
+ namespace="CypressComponentTest"
364
+ placeholder="Type Cypress test..."
365
+ />
366
+ );
367
+ });
368
+
369
+ it('displays editor with custom namespace', async () => {
370
+ await waitFor(() => {
371
+ expect(
372
+ document.querySelector('[data-testid="CypressComponentTest"] > p')
373
+ ).toBeInTheDocument();
374
+ });
375
+ expect(
376
+ document.querySelector('[data-testid="CypressComponentTest"]')
377
+ ).toBeInTheDocument();
378
+ });
379
+
380
+ it('displays editor with custom placeholder', async () => {
381
+ expect(
382
+ await within(
383
+ getEditor('CypressComponentTest').parentElement as HTMLElement
384
+ ).findByText('Type Cypress test...')
385
+ ).toBeInTheDocument();
386
+ });
387
+
388
+ it('displays editor with custom minimum input height', () => {
389
+ expect(
390
+ getComputedStyle(getEditor('CypressComponentTest')).minHeight
391
+ ).toBe('200px');
392
+ });
393
+ });
394
+
395
+ describe('Rich Text Editor with Macros Plugin', () => {
396
+ beforeEach(() => {
397
+ render(<RichTextEditor displayMacrosButton editable />);
398
+ });
399
+
400
+ it('displays the Macros button when the "displayMacrosButton" prop is set to true', async () => {
401
+ await userEvent.click(screen.getByLabelText('Macros'));
402
+
403
+ for (const macro of standardMacros) {
404
+ expect(await screen.findByText(macro)).toBeVisible();
405
+ }
406
+ });
407
+
408
+ it('ensures that the selected macro is inserted into the RichTextEditor', async () => {
409
+ const editor = getEditor();
410
+
411
+ await userEvent.type(editor, 'macro : ');
412
+
413
+ for (const macro of standardMacros) {
414
+ await userEvent.click(screen.getByLabelText('Macros'));
415
+ // The macro is inserted with `document.execCommand`, at the page
416
+ // selection. `userEvent.click` would move that selection into the
417
+ // menu item, as a pointer down does: `fireEvent.click` leaves it in
418
+ // the editor.
419
+ fireEvent.click(await screen.findByText(macro));
420
+
421
+ await waitFor(() => {
422
+ expect(editor).toHaveTextContent(macro);
423
+ });
424
+ await waitForMenusToClose();
425
+ }
426
+ });
427
+ });
428
+
429
+ describe('Block type', () => {
430
+ beforeEach(() => {
431
+ render(<RichTextEditor editable />);
432
+ });
433
+
434
+ const testCases = [
435
+ {
436
+ blockType: 'Huge',
437
+ tag: 'h3'
438
+ },
439
+ {
440
+ blockType: 'Large',
441
+ tag: 'h5'
442
+ },
443
+ {
444
+ blockType: 'Normal',
445
+ tag: 'p'
446
+ },
447
+ {
448
+ blockType: 'Small',
449
+ tag: 'h6'
450
+ }
451
+ ];
452
+
453
+ testCases.forEach(({ blockType, tag }) => {
454
+ it(`displays ${blockType} when the corresponding block type button is selected`, async () => {
455
+ const editor = getEditor();
456
+
457
+ await userEvent.type(editor, 'Example');
458
+
459
+ // The text starts as a paragraph: for Normal, it is made Huge first,
460
+ // so choosing Normal has something to change.
461
+ if (blockType === 'Normal') {
462
+ await userEvent.click(screen.getByLabelText('block'));
463
+ await userEvent.click(await screen.findByText('Huge'));
464
+ await waitFor(() => {
465
+ expect(editor.querySelector('h3')).toBeVisible();
466
+ });
467
+ await waitForMenusToClose();
468
+ }
469
+
470
+ await userEvent.click(screen.getByLabelText('block'));
471
+ await userEvent.click(await screen.findByText(blockType));
472
+
473
+ await waitFor(() => {
474
+ expect(editor.querySelector(tag)).toBeVisible();
475
+ });
476
+
477
+ // The text is in that block alone, the menu is closed
478
+ // and the change can be undone.
479
+ expect(editor.querySelector(tag)).toHaveTextContent(/^Example$/);
480
+ expect(editor.querySelectorAll('p, h3, h5, h6')).toHaveLength(1);
481
+ await waitForMenusToClose();
482
+ expect(screen.getByLabelText('Undo')).toBeEnabled();
483
+ });
484
+ });
485
+ });
486
+
487
+ describe('List type', () => {
488
+ beforeEach(() => {
489
+ render(<RichTextEditor editable />);
490
+ });
491
+
492
+ const testCases = [
493
+ {
494
+ icon: 'FormatListBulletedIcon',
495
+ listType: 'Unordered List',
496
+ tag: 'ul'
497
+ },
498
+ {
499
+ icon: 'FormatListNumberedIcon',
500
+ listType: 'Ordered List',
501
+ tag: 'ol'
502
+ }
503
+ ];
504
+
505
+ testCases.forEach(({ icon, listType, tag }) => {
506
+ it(`displays ${listType} when the corresponding list type button is selected`, async () => {
507
+ const editor = getEditor();
508
+
509
+ await userEvent.type(editor, 'Example');
510
+ await userEvent.click(screen.getByLabelText('list'));
511
+ await userEvent.click(await screen.findByLabelText(listType));
512
+
513
+ await waitFor(() => {
514
+ expect(editor.querySelector(tag)).toBeVisible();
515
+ });
516
+
517
+ // The text is the list item, the menu is closed
518
+ // and the list button shows the icon of the selected list type.
519
+ expect(editor.querySelector(`${tag} > li`)).toHaveTextContent(
520
+ /^Example$/
521
+ );
522
+ await waitForMenusToClose();
523
+ await waitFor(() => {
524
+ expect(
525
+ within(screen.getByLabelText('list')).getByTestId(icon)
526
+ ).toBeInTheDocument();
527
+ });
528
+ });
529
+ });
530
+ });
531
+
532
+ describe('Aligns', () => {
533
+ beforeEach(() => {
534
+ render(<RichTextEditor editable />);
535
+ });
536
+
537
+ const testCases = ['Center', 'Right', 'Left'];
538
+
539
+ testCases.forEach((label) => {
540
+ it(`aligns the text to the ${label} when the corresponding button was clicked`, async () => {
541
+ await userEvent.type(getEditor(), 'Example');
542
+
543
+ await userEvent.click(screen.getByLabelText(labelAlignPicker));
544
+ await userEvent.click(await screen.findByLabelText(label));
545
+
546
+ await waitFor(() => {
547
+ expect(
548
+ getComputedStyle(
549
+ screen.getByText('Example').parentElement as HTMLElement
550
+ ).textAlign
551
+ ).toBe(label.toLowerCase());
552
+ });
553
+
554
+ // The menu is closed and the align picker shows
555
+ // the icon of the selected alignment.
556
+ await waitForMenusToClose();
557
+ await waitFor(() => {
558
+ expect(
559
+ within(screen.getByLabelText(labelAlignPicker)).getByTestId(
560
+ `FormatAlign${label}Icon`
561
+ )
562
+ ).toBeInTheDocument();
563
+ });
564
+ });
565
+ });
566
+ });
567
+ });
@@ -122,7 +122,9 @@ const FloatingLinkEditor = ({
122
122
  const nodeHeight = rangeRect.height;
123
123
 
124
124
  setTooltipPosition({ x: nodeX, y: nodeY + nodeHeight });
125
- }, [rangeRect?.x, rangeRect?.y, rangeRect, nativeSelection]);
125
+ // The coordinates, not `rangeRect`: it is a new object on every render, so
126
+ // depending on it re-ran this effect, whose state update re-rendered endlessly.
127
+ }, [rangeRect?.x, rangeRect?.y, rangeRect?.height, nativeSelection]);
126
128
 
127
129
  if (isNil(rangeRect)) {
128
130
  return null;