diff --git a/components/shared/__tests__/__snapshots__/backdrop.test.tsx.snap b/components/shared/__tests__/__snapshots__/backdrop.test.tsx.snap
new file mode 100644
index 0000000..8b4b402
--- /dev/null
+++ b/components/shared/__tests__/__snapshots__/backdrop.test.tsx.snap
@@ -0,0 +1,148 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Backdrop content should be offset 1`] = `
+"
"
+`;
+
+exports[`Backdrop content should be offset 2`] = `
+""
+`;
+
+exports[`Backdrop should render correctly 1`] = `
+""
+`;
diff --git a/components/shared/__tests__/__snapshots__/dropdown.test.tsx.snap b/components/shared/__tests__/__snapshots__/dropdown.test.tsx.snap
new file mode 100644
index 0000000..7cc6df2
--- /dev/null
+++ b/components/shared/__tests__/__snapshots__/dropdown.test.tsx.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Dropdown should render correctly 1`] = `""`;
diff --git a/components/shared/__tests__/__snapshots__/transition.test.tsx.snap b/components/shared/__tests__/__snapshots__/transition.test.tsx.snap
new file mode 100644
index 0000000..555abd2
--- /dev/null
+++ b/components/shared/__tests__/__snapshots__/transition.test.tsx.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CSSTransition should render correctly 1`] = `"test"`;
diff --git a/components/shared/__tests__/backdrop.test.tsx b/components/shared/__tests__/backdrop.test.tsx
new file mode 100644
index 0000000..db95fcd
--- /dev/null
+++ b/components/shared/__tests__/backdrop.test.tsx
@@ -0,0 +1,65 @@
+import React from 'react'
+import { mount } from 'enzyme'
+import Backdrop from '../backdrop'
+import { nativeEvent, updateWrapper } from 'tests/utils'
+
+describe('Backdrop', () => {
+ it('should render correctly', () => {
+ const wrapper = mount(
+ test-value
+ )
+ expect(wrapper.html()).toMatchSnapshot()
+ expect(() => wrapper.unmount()).not.toThrow()
+ })
+
+ it('should be shown or hidden by prop', async () => {
+ const wrapper = mount(
+ test-value
+ )
+ expect(wrapper.find('.backdrop').length).toBe(0)
+ wrapper.setProps({ visible: true })
+ await updateWrapper(wrapper, 350)
+ expect(wrapper.find('.backdrop').length).not.toBe(0)
+ })
+
+ it('background click events should be captured', () => {
+ const handler = jest.fn()
+ const wrapper = mount(
+ test-value
+ )
+ wrapper.find('.backdrop').simulate('click', nativeEvent)
+ expect(handler).toHaveBeenCalled()
+ handler.mockRestore()
+ })
+
+ it('should be no error when handler missing', () => {
+ const wrapper = mount(
+ test-value
+ )
+ wrapper.find('.backdrop').simulate('click', nativeEvent)
+ expect(() => wrapper.unmount()).not.toThrow()
+ })
+
+ it('should be prevent event from the container', () => {
+ const handler = jest.fn()
+ const wrapper = mount(
+ test-value
+ )
+ wrapper.find('.content').simulate('click', nativeEvent)
+ wrapper.find('.offset').simulate('click', nativeEvent)
+ expect(handler).not.toHaveBeenCalled()
+ handler.mockRestore()
+ })
+
+ it('content should be offset', () => {
+ const wrapper = mount(
+ test-value
+ )
+ const notOffset = wrapper.html()
+ expect(wrapper.html()).toMatchSnapshot()
+
+ wrapper.setProps({ offsetY: '100' })
+ expect(wrapper.html()).toMatchSnapshot()
+ expect(notOffset).not.toEqual(wrapper.html())
+ })
+})
diff --git a/components/shared/__tests__/dropdown.test.tsx b/components/shared/__tests__/dropdown.test.tsx
new file mode 100644
index 0000000..1e3159a
--- /dev/null
+++ b/components/shared/__tests__/dropdown.test.tsx
@@ -0,0 +1,165 @@
+import React, { useRef } from 'react'
+import { mount } from 'enzyme'
+import Dropdown from '../dropdown'
+import { nativeEvent, updateWrapper } from 'tests/utils'
+import { act } from 'react-dom/test-utils'
+
+const simulateGlobalClick = () => {
+ document.body.dispatchEvent(
+ new MouseEvent('click', {
+ view: window,
+ bubbles: true,
+ cancelable: true,
+ }),
+ )
+}
+
+describe('Dropdown', () => {
+ beforeAll(() => {
+ window.Element.prototype.getBoundingClientRect = () => ({
+ width: 100,
+ left: 0,
+ right: 100,
+ top: 0,
+ bottom: 100,
+ height: 100,
+ x: 0,
+ } as DOMRect)
+ })
+
+ it('should render correctly', async () => {
+ const Mock: React.FC<{ visible?: boolean }> = ({ visible = false }) => {
+ const ref = useRef(null)
+ return (
+
+
+ test-value
+
+
+ )
+ }
+ const wrapper = mount()
+ wrapper.setProps({ visible: true })
+ await updateWrapper(wrapper, 300)
+
+ expect(wrapper.find('.dropdown').html()).toContain('test-value')
+ expect(wrapper.html()).toMatchSnapshot()
+ expect(() => wrapper.unmount()).not.toThrow()
+ })
+
+ it('should be work without parent', () => {
+ const wrapper = mount(
+
+ test-value
+
+ )
+
+ expect(() => wrapper.unmount()).not.toThrow()
+ })
+
+ it('events should be prevented', () => {
+ const handler = jest.fn()
+ const Mock: React.FC<{}> = () => {
+ const ref = useRef(null)
+ return (
+
+
+ test-value
+
+
+ )
+ }
+ const wrapper = mount()
+ wrapper.find('.dropdown').simulate('click', nativeEvent)
+
+ expect(handler).not.toHaveBeenCalled()
+ expect(() => wrapper.unmount()).not.toThrow()
+ handler.mockRestore()
+ })
+
+ it('should trigger rect update', async () => {
+ let dynamicTopMock = 100, calledTimes = 0
+ window.Element.prototype.getBoundingClientRect = () => {
+ calledTimes ++
+ return {
+ width: 100,
+ left: 0,
+ right: 100,
+ top: 0,
+ bottom: dynamicTopMock,
+ height: 100,
+ x: 0,
+ } as DOMRect
+ }
+ const Mock: React.FC<{}> = () => {
+ const ref = useRef(null)
+ return (
+
+
+ test-value
+
+
+ )
+ }
+ const wrapper = mount()
+ expect(calledTimes).toBe(1)
+
+ // Do not render if position is not updated
+ act(() => simulateGlobalClick())
+ expect(calledTimes).toBe(2)
+ await updateWrapper(wrapper, 50)
+
+ // Trigger position diff first, then trigger the update
+ // Get Rect twice total
+ act(() => {
+ dynamicTopMock++
+ simulateGlobalClick()
+ })
+ expect(calledTimes).toBeGreaterThanOrEqual(4)
+
+ act(() => {
+ dynamicTopMock++
+ window.dispatchEvent(new Event('resize'))
+ })
+ expect(calledTimes).toBeGreaterThanOrEqual(5)
+
+ expect(() => wrapper.unmount()).not.toThrow()
+ })
+
+ it('should tigger rect update when mouseenter', () => {
+ let calledTimes = 0
+ window.Element.prototype.getBoundingClientRect = () => {
+ calledTimes ++
+ return {
+ width: 100,
+ left: 0,
+ right: 100,
+ top: 0,
+ bottom: 100,
+ height: 100,
+ x: 0,
+ } as DOMRect
+ }
+ const Mock: React.FC<{}> = () => {
+ const ref = useRef(null)
+ return (
+
+
+ test-value
+
+
+ )
+ }
+ const wrapper = mount()
+ expect(calledTimes).toBe(1)
+
+ // MouseEnter event is monitored by native API, the simulate can not trigger it.
+ const parent = wrapper.find('#parent').getDOMNode() as HTMLDivElement
+ act(() => {
+ parent.dispatchEvent(new Event('mouseenter'))
+ })
+ expect(calledTimes).toBe(2)
+
+ expect(() => wrapper.unmount()).not.toThrow()
+ })
+})
diff --git a/components/shared/__tests__/transition.test.tsx b/components/shared/__tests__/transition.test.tsx
new file mode 100644
index 0000000..d872ac7
--- /dev/null
+++ b/components/shared/__tests__/transition.test.tsx
@@ -0,0 +1,67 @@
+import React from 'react'
+import { mount } from 'enzyme'
+import CSSTransition from '../css-transition'
+import { updateWrapper } from 'tests/utils'
+
+describe('CSSTransition', () => {
+ it('should render correctly', () => {
+ const wrapper = mount(test)
+ expect(wrapper.text()).toContain('test')
+ expect(wrapper.html()).toMatchSnapshot()
+ expect(() => wrapper.unmount()).not.toThrow()
+ })
+
+ it('should work correctly with time props', async () => {
+ const wrapper = mount(
+
+ test
+
+ )
+ expect(wrapper.find('.transition-enter-active').length).toBe(0)
+
+ wrapper.setProps({ visible: true })
+ await updateWrapper(wrapper, 310)
+ expect(wrapper.find('.transition-enter-active').length).not.toBe(0)
+
+ wrapper.setProps({ visible: false })
+ await updateWrapper(wrapper, 310)
+ expect(wrapper.find('.transition-leave-active').length).not.toBe(0)
+ })
+
+ it('should clear css-transition classes after hidden', async () => {
+ const wrapper = mount(test)
+ // don't remove classes after shown
+ await updateWrapper(wrapper, 60)
+ expect(wrapper.find('.transition-enter-active').length).not.toBe(0)
+
+ await updateWrapper(wrapper, 150)
+ expect(wrapper.find('.transition-enter-active').length).not.toBe(0)
+
+ // remove classes after hidden
+ wrapper.setProps({ visible: false })
+ await updateWrapper(wrapper, 60)
+ expect(wrapper.find('.transition-leave-active').length).not.toBe(0)
+
+ await updateWrapper(wrapper, 150)
+ expect(wrapper.find('.transition-leave-active').length).toBe(0)
+ expect(wrapper.find('.transition-enter-active').length).toBe(0)
+ })
+
+ it('custom class names should be rendered', async () => {
+ const wrapper = mount(
+
+ test
+
+ )
+
+ expect(wrapper.find('.test-enter-active').length).toBe(0)
+
+ wrapper.setProps({ visible: true })
+ await updateWrapper(wrapper, 60)
+ expect(wrapper.find('.test-enter-active').length).not.toBe(0)
+
+ wrapper.setProps({ visible: false })
+ await updateWrapper(wrapper, 60)
+ expect(wrapper.find('.test-leave-active').length).not.toBe(0)
+ })
+})
diff --git a/components/shared/dropdown.tsx b/components/shared/dropdown.tsx
index 2b4b080..5af00b7 100644
--- a/components/shared/dropdown.tsx
+++ b/components/shared/dropdown.tsx
@@ -57,6 +57,7 @@ const Dropdown: React.FC> = React.memo(({
useEffect(() => {
if (!parent || !parent.current) return
parent.current.addEventListener('mouseenter', updateRect)
+ /* istanbul ignore next */
return () => {
if (!parent || !parent.current) return
parent.current.removeEventListener('mouseenter', updateRect)