Skip to content

Commit ecaec4d

Browse files
chore(eui): add meaningful repositionOnScroll Cypress tests
1 parent 4446019 commit ecaec4d

File tree

8 files changed

+293
-144
lines changed

8 files changed

+293
-144
lines changed

packages/eui/cypress/support/component.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import './copy/select_and_copy';
2424
import './setup/mount';
2525
import './setup/realMount';
2626
import './css/cssVar';
27+
import './helpers/wait_for_position_to_settle';
2728

2829
// @see https://github.com/quasarframework/quasar/issues/2233#issuecomment-492975745
2930
// @see also https://github.com/cypress-io/cypress/issues/20341
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
/// <reference path="../index.d.ts" />
10+
11+
/**
12+
* A recursive helper function that polls an element's position.
13+
*
14+
* @param subject The Cypress subject (the DOM element).
15+
* @param stabilityCount The number of times the position has been stable.
16+
* @param retries The number of remaining retries.
17+
* @param prevRect The element's rect from the previous check.
18+
*/
19+
export function waitForPositionToSettle(
20+
subject: JQuery<HTMLElement>,
21+
stabilityCount = 0,
22+
retries = 40, // 40 * 50ms = 2s timeout
23+
prevRect?: DOMRect
24+
): Cypress.Chainable<JQuery<HTMLElement>> {
25+
const STABILITY_THRESHOLD = 3; // require 3 consecutive stable checks
26+
27+
if (retries < 0) {
28+
throw new Error('Position did not settle in time after 2s');
29+
}
30+
31+
const currentRect = subject[0].getBoundingClientRect();
32+
33+
let nextStabilityCount = stabilityCount;
34+
if (
35+
prevRect &&
36+
currentRect.top === prevRect.top &&
37+
currentRect.left === prevRect.left
38+
) {
39+
nextStabilityCount++;
40+
} else {
41+
// Position changed, reset counter
42+
nextStabilityCount = 0;
43+
}
44+
45+
if (nextStabilityCount >= STABILITY_THRESHOLD) {
46+
cy.log('Position settled');
47+
return cy.wrap(subject);
48+
}
49+
50+
return cy.wait(50, { log: false }).then(() => {
51+
return waitForPositionToSettle(
52+
subject,
53+
nextStabilityCount,
54+
retries - 1,
55+
currentRect
56+
);
57+
});
58+
}
59+
60+
Cypress.Commands.add(
61+
'waitForPositionToSettle',
62+
{ prevSubject: 'element' },
63+
(subject: JQuery<HTMLElement>) => {
64+
return waitForPositionToSettle(subject);
65+
}
66+
);

packages/eui/cypress/support/index.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ declare global {
6262
* Params: variableName - the name of the CSS variable (e.g. '--euiColorPrimary')
6363
*/
6464
cssVar(variableName: string): Chainable<string | null>;
65+
66+
/**
67+
* Waits for an element's position to remain stable for a few consecutive checks.
68+
* This is useful for ensuring that repositioning logic has completed after an event like a scroll.
69+
*/
70+
waitForPositionToSettle(): Chainable<JQuery<HTMLElement>>;
6571
}
6672
}
6773
}

packages/eui/src/components/popover/popover.spec.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import React, {
2020

2121
import { EuiButton, EuiConfirmModal } from '../../components';
2222
import { EuiPopover, EuiPopoverProps } from './popover';
23+
import { performRepositionTest } from '../../test/cypress/perform_reposition_test';
2324

2425
const PopoverToggle: FC<{ onClick: MouseEventHandler<HTMLButtonElement> }> = ({
2526
onClick,
@@ -215,6 +216,69 @@ describe('EuiPopover', () => {
215216
});
216217
});
217218

219+
describe('repositionOnScroll', () => {
220+
const renderPopover = (props: { repositionOnScroll?: boolean }) => (
221+
<PopoverComponent {...props}>Test popover</PopoverComponent>
222+
);
223+
224+
const config = {
225+
renderComponent: renderPopover,
226+
componentName: 'EuiPopover' as const,
227+
triggerSelector: '[data-test-subj="togglePopover"]',
228+
panelSelector: '[data-test-subj="popoverPanel"]',
229+
};
230+
231+
describe('is repositioned', () => {
232+
it('when `repositionOnScroll=true`', () => {
233+
performRepositionTest({
234+
...config,
235+
shouldReposition: true,
236+
propValue: true,
237+
});
238+
});
239+
240+
it('when `componentDefaults` has `repositionOnScroll=true`', () => {
241+
performRepositionTest({
242+
...config,
243+
shouldReposition: true,
244+
componentDefaultValue: true,
245+
});
246+
});
247+
248+
it('when `repositionOnScroll=true` even if `componentDefaults` has `repositionOnScroll=false`', () => {
249+
performRepositionTest({
250+
...config,
251+
shouldReposition: true,
252+
propValue: true,
253+
componentDefaultValue: false,
254+
});
255+
});
256+
});
257+
258+
describe('is not repositioned', () => {
259+
it('when `repositionOnScroll=false` (default value)', () => {
260+
performRepositionTest({ ...config, shouldReposition: false });
261+
});
262+
263+
it('when `componentDefaults` has `repositionOnScroll=false`', () => {
264+
performRepositionTest({
265+
...config,
266+
shouldReposition: false,
267+
componentDefaultValue: false,
268+
});
269+
});
270+
271+
it('when `repositionOnScroll=false` even if `componentDefaults` has `repositionOnScroll=true`', () => {
272+
performRepositionTest({
273+
...config,
274+
shouldReposition: false,
275+
propValue: false,
276+
componentDefaultValue: true,
277+
});
278+
});
279+
});
280+
});
281+
218282
describe('repositionToCrossAxis', () => {
219283
beforeEach(() => {
220284
// Set a forced viewport with not enough room to render the popover vertically

packages/eui/src/components/popover/popover.test.tsx

Lines changed: 0 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import {
2525
getPopoverAlignFromAnchorPosition,
2626
PopoverAnchorPosition,
2727
} from './popover';
28-
import { EuiProvider } from '../provider';
2928

3029
const actAdvanceTimersByTime = (time: number) =>
3130
act(() => jest.advanceTimersByTime(time));
@@ -398,41 +397,6 @@ describe('EuiPopover', () => {
398397
});
399398
});
400399

401-
test('repositionOnScroll', () => {
402-
const addEventSpy = jest.spyOn(window, 'addEventListener');
403-
const removeEventSpy = jest.spyOn(window, 'removeEventListener');
404-
const repositionFn = expect.any(Function);
405-
406-
const { rerender, unmount } = render(
407-
<EuiPopover
408-
button={<button data-test-subj="trigger">Trigger</button>}
409-
closePopover={() => {}}
410-
isOpen
411-
repositionOnScroll={false}
412-
>
413-
<p>Content</p>
414-
</EuiPopover>
415-
);
416-
expect(addEventSpy).not.toHaveBeenCalledWith('scroll');
417-
418-
// Should add a scroll event listener on mount and on update
419-
rerender(
420-
<EuiPopover
421-
button={<button data-test-subj="trigger">Trigger</button>}
422-
closePopover={() => {}}
423-
isOpen
424-
repositionOnScroll={true}
425-
>
426-
<p>Content</p>
427-
</EuiPopover>
428-
);
429-
expect(addEventSpy).toHaveBeenCalledWith('scroll', repositionFn, true);
430-
431-
// Should remove the scroll event listener on unmount
432-
unmount();
433-
expect(removeEventSpy).toHaveBeenCalledWith('scroll', repositionFn, true);
434-
});
435-
436400
test('buffer', () => {
437401
const { container } = render(
438402
<div>
@@ -481,53 +445,6 @@ describe('EuiPopover', () => {
481445

482446
expect(container.firstChild).toMatchSnapshot();
483447
});
484-
485-
describe('configurable defaults', () => {
486-
test('repositionOnScroll', () => {
487-
const addEventSpy = jest.spyOn(window, 'addEventListener');
488-
const removeEventSpy = jest.spyOn(window, 'removeEventListener');
489-
const repositionFn = expect.any(Function);
490-
491-
const { rerender, unmount } = render(
492-
<EuiProvider
493-
componentDefaults={{ EuiPopover: { repositionOnScroll: false } }}
494-
>
495-
<EuiPopover
496-
button={<button data-test-subj="trigger">Trigger</button>}
497-
closePopover={() => {}}
498-
isOpen
499-
>
500-
<p>Content</p>
501-
</EuiPopover>
502-
</EuiProvider>
503-
);
504-
expect(addEventSpy).not.toHaveBeenCalledWith('scroll');
505-
506-
// Should add a scroll event listener on mount and on update
507-
rerender(
508-
<EuiProvider
509-
componentDefaults={{ EuiPopover: { repositionOnScroll: true } }}
510-
>
511-
<EuiPopover
512-
button={<button data-test-subj="trigger">Trigger</button>}
513-
closePopover={() => {}}
514-
isOpen
515-
>
516-
<p>Content</p>
517-
</EuiPopover>
518-
</EuiProvider>
519-
);
520-
expect(addEventSpy).toHaveBeenCalledWith('scroll', repositionFn, true);
521-
522-
// Should remove the scroll event listener on unmount
523-
unmount();
524-
expect(removeEventSpy).toHaveBeenCalledWith(
525-
'scroll',
526-
repositionFn,
527-
true
528-
);
529-
});
530-
});
531448
});
532449

533450
describe('listener cleanup', () => {

packages/eui/src/components/tool_tip/tool_tip.spec.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { EuiFlyout } from '../flyout';
1717
import { EuiModal } from '../modal';
1818
import { EuiPopover } from '../popover';
1919
import { EuiToolTip } from './tool_tip';
20+
import { performRepositionTest } from '../../test/cypress/perform_reposition_test';
2021

2122
describe('EuiToolTip', () => {
2223
it('shows the tooltip on hover and hides it on mouseout', () => {
@@ -216,4 +217,69 @@ describe('EuiToolTip', () => {
216217
cy.get('[data-test-subj="popover"]').should('not.exist');
217218
});
218219
});
220+
221+
describe('repositionOnScroll', () => {
222+
const renderTooltip = (props: { repositionOnScroll?: boolean }) => (
223+
<EuiToolTip content="Test tooltip" data-test-subj="tooltip" {...props}>
224+
<EuiButton data-test-subj="fixed-trigger">Fixed Button</EuiButton>
225+
</EuiToolTip>
226+
);
227+
228+
const config = {
229+
renderComponent: renderTooltip,
230+
componentName: 'EuiToolTip' as const,
231+
triggerSelector: '[data-test-subj="fixed-trigger"]',
232+
panelSelector: '[data-test-subj="tooltip"]',
233+
};
234+
235+
describe('is repositioned', () => {
236+
it('when `repositionOnScroll=true`', () => {
237+
performRepositionTest({
238+
...config,
239+
shouldReposition: true,
240+
propValue: true,
241+
});
242+
});
243+
244+
it('when `componentDefaults` has `repositionOnScroll=true`', () => {
245+
performRepositionTest({
246+
...config,
247+
shouldReposition: true,
248+
componentDefaultValue: true,
249+
});
250+
});
251+
252+
it('when `repositionOnScroll=true` even if `componentDefaults` has `repositionOnScroll=false`', () => {
253+
performRepositionTest({
254+
...config,
255+
shouldReposition: true,
256+
propValue: true,
257+
componentDefaultValue: false,
258+
});
259+
});
260+
});
261+
262+
describe('is not repositioned', () => {
263+
it('when `repositionOnScroll=false` (default value)', () => {
264+
performRepositionTest({ ...config, shouldReposition: false });
265+
});
266+
267+
it('when `componentDefaults` has `repositionOnScroll=false`', () => {
268+
performRepositionTest({
269+
...config,
270+
shouldReposition: false,
271+
componentDefaultValue: false,
272+
});
273+
});
274+
275+
it('when `repositionOnScroll=false` even if `componentDefaults` has `repositionOnScroll=true`', () => {
276+
performRepositionTest({
277+
...config,
278+
shouldReposition: false,
279+
propValue: false,
280+
componentDefaultValue: true,
281+
});
282+
});
283+
});
284+
});
219285
});

0 commit comments

Comments
 (0)