UNPKG

wix-style-react

Version:
408 lines • 20.1 kB
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