wix-style-react
Version:
wix-style-react
408 lines • 20.1 kB
JavaScript
import * as React from 'react';
import Popover from '../Popover';
import { PopoverCore } from '../PopoverCore/PopoverCore';
import { act, createAsyncRendererWithUniDriver, waitFor, } from '../../test-utils/utils/unit';
import { popoverUniDriverFactory } from '../Popover.uni.driver';
const renderPopover = (props, content = 'Content') => (React.createElement(Popover, { ...props },
React.createElement(Popover.Element, null,
React.createElement("div", null, "Element")),
React.createElement(Popover.Content, null,
React.createElement("div", null, content))));
describe('Popover', () => {
const render = createAsyncRendererWithUniDriver(popoverUniDriverFactory);
describe('props', () => {
describe('shown', () => {
it(`doesn't display popup when shown={false}`, async () => {
const props = {
placement: 'bottom',
shown: false,
};
const { driver } = await render(renderPopover(props));
expect(await driver.isTargetElementExists()).toBe(true);
expect(await driver.isContentElementExists()).toBe(false);
});
it(`displays popup when shown={true}`, async () => {
const props = {
placement: 'bottom',
shown: true,
};
const { driver } = await render(renderPopover(props));
await waitFor(async () => {
expect(await driver.isContentElementExists()).toBe(true);
});
});
});
describe('onMouseEnter & onMouseLeave', () => {
it(`calls mouseEnter and mouseLeave callbacks`, async () => {
const onMouseEnter = jest.fn();
const onMouseLeave = jest.fn();
const props = { onMouseEnter, onMouseLeave };
const { driver } = await render(renderPopover(props));
await driver.mouseEnter();
expect(onMouseEnter).toHaveBeenCalled();
await driver.mouseLeave();
expect(onMouseLeave).toBeCalled();
});
});
describe('onClick', () => {
it(`calls onClick callback`, async () => {
const onClick = jest.fn();
const props = { onClick };
const { driver } = await render(renderPopover(props));
await driver.click();
expect(onClick).toBeCalled();
});
});
describe('onClickOutside', () => {
it('should be triggered when outside of the popover is called', async () => {
const onClickOutside = jest.fn();
const props = { onClickOutside, shown: true };
const { driver } = await render(renderPopover(props));
await driver.clickOutside();
await waitFor(async () => expect(onClickOutside).toBeCalled());
});
it('should not trigger onClickOutside when clicking inside with an excluded class', async () => {
const onClickOutside = jest.fn();
const props = {
onClickOutside,
shown: false,
excludeClass: 'excludeClass',
};
const { driver } = await render(renderPopover(props));
await driver.click();
expect(onClickOutside).not.toBeCalled();
});
});
describe('disableClickOutsideWhenClosed', () => {
it('should be triggered when outside of the popover is called', async () => {
const onClickOutside = jest.fn();
const props = {
shown: true,
onClickOutside,
disableClickOutsideWhenClosed: true,
};
const { driver } = await render(renderPopover(props));
await waitFor(async () => {
await driver.clickOutside();
});
expect(onClickOutside).toBeCalled();
});
it('should *not* be triggered when outside of the popover is called and the popover is *not* shown', async () => {
const onClickOutside = jest.fn();
const props = {
shown: false,
onClickOutside,
disableClickOutsideWhenClosed: true,
};
const { driver } = await render(renderPopover(props));
await driver.clickOutside();
expect(onClickOutside).not.toBeCalled();
});
});
describe('dataHook', () => {
it('should be found on target element container', async () => {
const props = {
shown: true,
appendTo: 'window',
dataHook: 'random',
};
const { driver } = await render(renderPopover(props));
await waitFor(async () => {
const target = await driver.getTargetElement();
expect(target.parentNode.getAttribute('data-hook')).toBe('random');
});
});
it('should construct data-content-hook', async () => {
const props = {
shown: true,
appendTo: 'window',
dataHook: 'random',
};
const { driver } = await render(renderPopover(props));
await waitFor(async () => {
const target = await driver.getTargetElement();
expect(target.parentNode.getAttribute('data-content-hook')).toMatch(/popover-content-random-/);
});
});
it('should apply data-content-element on content element', async () => {
const props = {
shown: true,
appendTo: 'window',
dataHook: 'random',
};
const { driver } = await render(renderPopover(props));
await waitFor(async () => {
expect(await driver.isContentElementExists()).toBe(true);
});
const content = await driver.getContentElement();
expect(content.getAttribute('data-content-element')).toMatch(/popover-content-random-/);
});
it('should not override portal component data-hook', async () => {
const props = {
shown: true,
appendTo: 'window',
dataHook: 'random',
};
const { driver } = await render(renderPopover(props));
await waitFor(async () => {
expect(await driver.isContentElementExists()).toBe(true);
});
const content = await driver.getContentElement();
expect(content.parentNode.getAttribute('data-hook')).toBe('popover-portal');
});
});
describe('customArrow', () => {
it('should display a custom arrow element', async () => {
const customArrow = (placement, arrowProps) => (React.createElement("p", { "data-test": "custom-arrow", "data-placement": placement, ...arrowProps }));
const props = {
shown: true,
showArrow: true,
customArrow,
placement: 'top',
};
const { driver, container } = await render(renderPopover(props));
await waitFor(async () => {
expect(await driver.isContentElementExists()).toBe(true);
});
const arrowElement = container.querySelector('[data-test="custom-arrow"]');
expect(!!arrowElement).toBe(true);
});
});
describe('moveArrowTo', () => {
it(`offsets the popup arrow by specified amount`, async () => {
const props = {
shown: true,
showArrow: true,
moveArrowTo: 10,
};
const { driver } = await render(renderPopover(props));
await waitFor(async () => {
expect((await driver.getArrowOffset()).left).toBe('10px');
});
});
});
describe('timeout', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it(`remove timeout on close [when] given a timeout={0}`, async () => {
const timeout = { timeout: 0 };
const props = { shown: true, ...timeout };
const { driver, rerender } = await render(renderPopover(props));
await waitFor(async () => {
expect(await driver.isContentElementExists()).toBe(true);
});
await rerender(renderPopover({ ...props, shown: false }));
expect(await driver.isContentElementExists()).toBe(false);
});
it('adds timeout of 100ms on close [when] given a timeout={100}', async () => {
const timeout = 100;
const props = { shown: true, timeout };
const { driver, rerender } = await render(renderPopover(props));
await waitFor(async () => {
expect(await driver.isContentElementExists()).toBe(true);
});
await rerender(renderPopover({ ...props, shown: false }));
expect(await driver.isContentElementExists()).toBe(true);
await act(async () => {
jest.advanceTimersByTime(timeout);
});
expect(await driver.isContentElementExists()).toBe(false);
});
});
describe('hideDelay', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it(`should close after hideDelay`, async () => {
const timeout = 500;
const props = { shown: true, hideDelay: timeout };
const { driver, rerender } = await render(renderPopover(props));
await waitFor(async () => {
expect(await driver.isContentElementExists()).toBe(true);
});
await rerender(renderPopover({ ...props, shown: false }));
expect(await driver.isContentElementExists()).toBe(true);
await act(async () => {
jest.advanceTimersByTime(timeout);
});
expect(await driver.isContentElementExists()).toBe(false);
});
it(`should not update delay until the popover visibillity has fully changed`, async () => {
const timeout = 500;
const props = { shown: true, hideDelay: timeout };
const { driver, rerender } = await render(renderPopover(props));
await waitFor(async () => {
expect(await driver.isContentElementExists()).toBe(true);
});
await rerender(renderPopover({ ...props, shown: false, hideDelay: 100 }));
expect(await driver.isContentElementExists()).toBe(true);
await act(async () => {
jest.advanceTimersByTime(timeout);
});
expect(await driver.isContentElementExists()).toBe(false);
});
});
describe('showDelay', () => {
it(`should show the content immediately on first render`, async () => {
const props = { shown: true, showDelay: 100 };
const { driver } = await render(renderPopover(props));
await waitFor(async () => {
expect(await driver.isContentElementExists()).toBe(true);
});
});
it(`should open after showDelay`, async () => {
const timeout = { timeout: 500 };
const props = { shown: false, showDelay: 100 };
const { driver, rerender } = await render(renderPopover(props));
expect(await driver.isContentElementExists()).toBe(false);
await rerender(renderPopover({ ...props, shown: true }));
expect(await driver.isContentElementExists()).toBe(false);
await waitFor(async () => {
expect(await driver.isContentElementExists()).toBe(true);
}, timeout);
});
});
describe('onShow', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('should call the onShow callback when popover is shown', async () => {
const onShow = jest.fn();
const props = { shown: false, onShow };
const { driver, rerender } = await render(renderPopover(props));
expect(await driver.isContentElementExists()).toBe(false);
expect(onShow).not.toHaveBeenCalled();
await act(async () => {
rerender(renderPopover({ ...props, shown: true }));
});
expect(await driver.isContentElementExists()).toBe(true);
expect(onShow).toHaveBeenCalledTimes(1);
});
it('should call the onShow callback when popover is shown after a delay', async () => {
const onShow = jest.fn();
const showDelay = 100;
const props = { shown: false, showDelay, onShow };
const { driver, rerender } = await render(renderPopover(props));
expect(await driver.isContentElementExists()).toBe(false);
expect(onShow).not.toHaveBeenCalled();
rerender(renderPopover({ ...props, shown: true }));
expect(await driver.isContentElementExists()).toBe(false);
expect(onShow).not.toHaveBeenCalled();
await act(async () => {
jest.advanceTimersByTime(showDelay + 10);
});
expect(await driver.isContentElementExists()).toBe(true);
expect(onShow).toHaveBeenCalledTimes(1);
});
});
});
describe('Accessibility', () => {
it('should pass aria attrs to content wrapper', async () => {
const role = 'someRole';
const ariaAttributes = {
'aria-label': 'someAriaLabel',
'aria-labelledby': 'someAriaLabelledby',
'aria-describedby': 'someAriaDescribedby',
};
const props = {
shown: true,
role,
...ariaAttributes,
};
const { driver } = await render(renderPopover(props));
await waitFor(async () => {
expect(await driver.isContentElementExists()).toBe(true);
});
const contentElement = await driver.getContentElement();
expect(contentElement.getAttribute('role')).toBe(role);
for (const [ariaAttr, ariaAttrValue] of Object.entries(ariaAttributes)) {
expect(contentElement.getAttribute(ariaAttr)).toBe(ariaAttrValue);
}
});
it('should apply id attribute only on popover content', async () => {
const ariaId = 'test-labelled';
const { driver, container } = await render(renderPopover({ id: ariaId, shown: true }));
await waitFor(async () => {
expect(container.querySelectorAll(`[id="${ariaId}"]`)).toHaveLength(1);
});
expect(await driver.getContentElement()).toHaveProperty('id', ariaId);
});
});
describe('Popper update behaviour', () => {
it(`should update popper's position when props are chaning`, async () => {
const updatePositionSpy = jest.spyOn(PopoverCore.prototype, 'updatePosition');
const { rerender } = await render(renderPopover({ shown: true }));
await rerender(renderPopover({ shown: true }, 'New Content'));
expect(updatePositionSpy).toHaveBeenCalledTimes(2);
updatePositionSpy.mockRestore();
});
it(`should not directly update popper's position when the visibillity hasn't changed`, async () => {
const updatePositionSpy = jest.spyOn(PopoverCore.prototype, 'updatePosition');
const { rerender } = await render(renderPopover({ hideDelay: 10, showDelay: 10, shown: false }));
await rerender(renderPopover({ hideDelay: 10, showDelay: 10, shown: true }));
await rerender(renderPopover({ hideDelay: 10, showDelay: 10, shown: false }));
expect(updatePositionSpy).toHaveBeenCalledTimes(1);
updatePositionSpy.mockRestore();
});
});
describe('Portal and containment', () => {
it('should render content inside portal [when] appendTo = window', async () => {
const props = { shown: true, appendTo: 'window' };
const { driver } = await render(renderPopover(props));
await waitFor(async () => {
expect(await driver.isContentElementExists()).toBe(true);
});
const portalElement = document.body.querySelector('[data-hook="popover-portal"]');
expect(!!portalElement).toBe(true);
});
it(`renders an empty portal when closed`, async () => {
const props = { shown: false, appendTo: 'window' };
await render(renderPopover(props));
const portalElement = document.body.querySelector('[data-hook="popover-portal"]');
expect(!!portalElement).toBe(true);
});
it(`removes portal on unmount`, async () => {
const props = { shown: false, appendTo: 'window' };
const { unmount } = await render(renderPopover(props));
const portalElement = document.body.querySelector('[data-hook="popover-portal"]');
expect(!!portalElement).toBe(true);
unmount();
const portalElementUnounted = document.body.querySelector('[data-hook="popover-portal"]');
expect(!!portalElementUnounted).toBe(false);
});
it(`adds the portal to the closest scrollable element when appendTo="scrollParent"`, async () => {
const srollableHook = 'scrollable-element';
const props = { shown: false, appendTo: 'scrollParent' };
await render(React.createElement("div", { "data-hook": srollableHook, style: { overflow: 'scroll' } },
React.createElement("div", { style: { overflow: 'visible' } }, renderPopover(props))));
const parentElementHook = document.body
.querySelector('[data-hook="popover-portal"]')
?.parentElement?.getAttribute('data-hook');
expect(parentElementHook).toBe(srollableHook);
});
it(`should update the portal's styles when updated`, async () => {
const className = 'some-class';
const props = {
shown: true,
appendTo: 'window',
className,
};
await render(renderPopover(props));
const portalElement = document.body.querySelector('[data-hook="popover-portal"]');
await waitFor(async () => {
expect(portalElement?.classList).toContain(className);
});
});
});
});
//# sourceMappingURL=Popover.spec.js.map