import React, { Fragment, useState, useRef, useLayoutEffect, useEffect } from 'react' import { render, fireEvent } from '@testing-library/react' import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs' import { Transition } from './transition' import { executeTimeline } from '../../test-utils/execute-timeline' function nextFrame() { return new Promise((resolve) => { requestAnimationFrame(() => { requestAnimationFrame(() => { resolve() }) }) }) } it('should not steal the ref from the child', async () => { let fn = jest.fn() render(
...
) await nextFrame() expect(fn).toHaveBeenCalled() }) it('should render without crashing', () => { render(
Children
) }) it('should be possible to render a Transition without children', () => { render() expect(document.getElementsByClassName('transition')).not.toBeNull() }) it( 'should yell at us when we forget the required show prop', suppressConsoleLogs(() => { expect.assertions(1) expect(() => { render(
Children
) }).toThrowErrorMatchingInlineSnapshot( `"A is used but it is missing a \`show={true | false}\` prop."` ) }) ) describe('Setup API', () => { describe('shallow', () => { it('should render a div and its children by default', () => { let { container } = render(Children) expect(container.firstChild).toMatchInlineSnapshot(`
Children
`) }) it('should passthrough all the props (that we do not use internally)', () => { let { container } = render( Children ) expect(container.firstChild).toMatchInlineSnapshot(`
Children
`) }) it('should render another component if the `as` prop is used and its children by default', () => { let { container } = render( Children ) expect(container.firstChild).toMatchInlineSnapshot(` Children `) }) it('should passthrough all the props (that we do not use internally) even when using an `as` prop', () => { let { container } = render( Children ) expect(container.firstChild).toMatchInlineSnapshot(` Children `) }) it('should render nothing when the show prop is false', () => { let { container } = render(Children) expect(container.firstChild).toMatchInlineSnapshot(`null`) }) it('should be possible to change the underlying DOM tag', () => { let { container } = render( Children ) expect(container.firstChild).toMatchInlineSnapshot(` Children `) }) it('should be possible to use a render prop', () => { let { container } = render( {() => Children} ) expect(container.firstChild).toMatchInlineSnapshot(` Children `) }) it( 'should yell at us when we forget to forward the ref when using a render prop', suppressConsoleLogs(() => { expect.assertions(1) function Dummy(props: any) { return Children } expect(() => { render( {() => } ) }).toThrowErrorMatchingInlineSnapshot( `"Did you forget to passthrough the \`ref\` to the actual DOM node?"` ) }) ) }) describe('nested', () => { it( 'should yell at us when we forget to wrap the `` in a parent component', suppressConsoleLogs(() => { expect.assertions(1) expect(() => { render(
Oops
) }).toThrowErrorMatchingInlineSnapshot( `"A is used but it is missing a parent or ."` ) }) ) it('should be possible to render a Transition.Child without children', () => { render( ) expect(document.getElementsByClassName('transition')).not.toBeNull() }) it('should be possible to use a Transition.Root and a Transition.Child', () => { render( ) expect(document.getElementsByClassName('transition')).not.toBeNull() }) it('should be possible to nest transition components', () => { let { container } = render(
Sidebar Content
) expect(container.firstChild).toMatchInlineSnapshot(`
Sidebar
Content
`) }) it('should be possible to change the underlying DOM tag of the Transition.Child components', () => { let { container } = render(
Sidebar Content
) expect(container.firstChild).toMatchInlineSnapshot(`
Content
`) }) it('should be possible to change the underlying DOM tag of the Transition component and Transition.Child components', () => { let { container } = render(
Sidebar Content
) expect(container.firstChild).toMatchInlineSnapshot(`
Content
`) }) it('should be possible to use render props on the Transition.Child components', () => { let { container } = render(
{() => } {() =>
Content
}
) expect(container.firstChild).toMatchInlineSnapshot(`
Content
`) }) it('should be possible to use render props on the Transition and Transition.Child components', () => { let { container } = render(
{() => (
{() => } {() =>
Content
}
)}
) expect(container.firstChild).toMatchInlineSnapshot(`
Content
`) }) it( 'should yell at us when we forgot to forward the ref on one of the Transition.Child components', suppressConsoleLogs(() => { expect.assertions(1) function Dummy(props: any) { return
} expect(() => { render(
{() => Sidebar} {() => Content}
) }).toThrowErrorMatchingInlineSnapshot( `"Did you forget to passthrough the \`ref\` to the actual DOM node?"` ) }) ) it( 'should yell at us when we forgot to forward a ref on the Transition component', suppressConsoleLogs(() => { expect.assertions(1) function Dummy(props: any) { return
} expect(() => { render(
{() => ( {() => } {() =>
Content
}
)}
) }).toThrowErrorMatchingInlineSnapshot( `"Did you forget to passthrough the \`ref\` to the actual DOM node?"` ) }) ) }) describe('transition classes', () => { it('should be possible to passthrough the transition classes', () => { let { container } = render( Children ) expect(container.firstChild).toMatchInlineSnapshot(`
Children
`) }) it('should be possible to passthrough the transition classes and immediately apply the enter transitions when appear is set to true', () => { let { container } = render( Children ) expect(container.firstChild).toMatchInlineSnapshot(`
Children
`) }) }) }) describe('Transitions', () => { describe('shallow transitions', () => { xit('should transition in completely (duration defined in milliseconds)', async () => { let enterDuration = 50 function Example() { let [show, setShow] = useState(false) return ( <> Hello! ) } let timeline = await executeTimeline(, [ // Toggle to show ({ getByTestId }) => { fireEvent.click(getByTestId('toggle')) return executeTimeline.fullTransition(enterDuration) }, ]) expect(timeline).toMatchInlineSnapshot(` "Render 1: +
+ + Hello! + +
Render 2: - class=\\"enter from\\" + class=\\"enter to\\" Render 3: Transition took at least 50ms (yes) - class=\\"enter to\\" + class=\\"to\\"" `) }) xit('should transition in completely (duration defined in seconds)', async () => { let enterDuration = 100 function Example() { let [show, setShow] = useState(false) return ( <> Hello! ) } let timeline = await executeTimeline(, [ // Toggle to show ({ getByTestId }) => { fireEvent.click(getByTestId('toggle')) return executeTimeline.fullTransition(enterDuration) }, ]) expect(timeline).toMatchInlineSnapshot(` "Render 1: +
+ + Hello! + +
Render 2: - class=\\"enter from\\" + class=\\"enter to\\" Render 3: Transition took at least 100ms (yes) - class=\\"enter to\\" + class=\\"to\\"" `) }) xit('should transition in completely (duration defined in seconds) in (render strategy = hidden)', async () => { let enterDuration = 100 function Example() { let [show, setShow] = useState(false) return ( <> Hello! ) } let timeline = await executeTimeline(, [ // Toggle to show ({ getByTestId }) => { fireEvent.click(getByTestId('toggle')) return executeTimeline.fullTransition(enterDuration) }, ]) expect(timeline).toMatchInlineSnapshot(` "Render 1: - hidden=\\"\\" - style=\\"display: none;\\" + class=\\"enter from\\" + style=\\"\\" Render 2: - class=\\"enter from\\" + class=\\"enter to\\" Render 3: Transition took at least 100ms (yes) - class=\\"enter to\\" + class=\\"to\\"" `) }) xit('should transition in completely', async () => { let enterDuration = 100 function Example() { let [show, setShow] = useState(false) return ( <> Hello! ) } let timeline = await executeTimeline(, [ // Toggle to show ({ getByTestId }) => { fireEvent.click(getByTestId('toggle')) return executeTimeline.fullTransition(enterDuration) }, ]) expect(timeline).toMatchInlineSnapshot(` "Render 1: +
+ + Hello! + +
Render 2: - class=\\"enter from\\" + class=\\"enter to\\" Render 3: Transition took at least 100ms (yes) - class=\\"enter to\\" + class=\\"to\\"" `) }) xit( 'should transition out completely', suppressConsoleLogs(async () => { let leaveDuration = 100 function Example() { let [show, setShow] = useState(true) return ( <> Hello! ) } let timeline = await executeTimeline(, [ // Toggle to hide ({ getByTestId }) => { fireEvent.click(getByTestId('toggle')) return executeTimeline.fullTransition(leaveDuration) }, ]) expect(timeline).toMatchInlineSnapshot(` "Render 1: -
+
Render 2: - class=\\"leave from\\" + class=\\"leave to\\" Render 3: Transition took at least 100ms (yes) -
- - Hello! - -
" `) }) ) xit( 'should transition out completely (render strategy = hidden)', suppressConsoleLogs(async () => { let leaveDuration = 50 function Example() { let [show, setShow] = useState(true) return ( <> Hello! ) } let timeline = await executeTimeline(, [ // Toggle to hide ({ getByTestId }) => { fireEvent.click(getByTestId('toggle')) return executeTimeline.fullTransition(leaveDuration) }, ]) expect(timeline).toMatchInlineSnapshot(` "Render 1: -
+
Render 2: - class=\\"leave from\\" + class=\\"leave to\\" Render 3: Transition took at least 50ms (yes) - class=\\"leave to\\" + class=\\"to\\" + hidden=\\"\\" + style=\\"display: none;\\"" `) }) ) xit( 'should transition in and out completely', suppressConsoleLogs(async () => { let enterDuration = 100 let leaveDuration = 250 function Example() { let [show, setShow] = useState(false) return ( <> Hello! ) } let timeline = await executeTimeline(, [ // Toggle to show ({ getByTestId }) => { fireEvent.click(getByTestId('toggle')) return executeTimeline.fullTransition(enterDuration) }, // Toggle to hide ({ getByTestId }) => { fireEvent.click(getByTestId('toggle')) return executeTimeline.fullTransition(leaveDuration) }, ]) expect(timeline).toMatchInlineSnapshot(` "Render 1: +
+ + Hello! + +
Render 2: - class=\\"enter enter-from\\" + class=\\"enter enter-to\\" Render 3: Transition took at least 100ms (yes) - class=\\"enter enter-to\\" + class=\\"enter-to\\" Render 4: - class=\\"enter-to\\" + class=\\"leave leave-from\\" Render 5: - class=\\"leave leave-from\\" + class=\\"leave leave-to\\" Render 6: Transition took at least 250ms (yes) -
- - Hello! - -
" `) }) ) xit( 'should transition in and out completely (render strategy = hidden)', suppressConsoleLogs(async () => { let enterDuration = 50 let leaveDuration = 75 function Example() { let [show, setShow] = useState(false) return ( <> Hello! ) } let timeline = await executeTimeline(, [ // Toggle to show ({ getByTestId }) => { fireEvent.click(getByTestId('toggle')) return executeTimeline.fullTransition(enterDuration) }, // Toggle to hide ({ getByTestId }) => { fireEvent.click(getByTestId('toggle')) return executeTimeline.fullTransition(leaveDuration) }, // Toggle to show ({ getByTestId }) => { fireEvent.click(getByTestId('toggle')) return executeTimeline.fullTransition(leaveDuration) }, ]) expect(timeline).toMatchInlineSnapshot(` "Render 1: - hidden=\\"\\" - style=\\"display: none;\\" + class=\\"enter enter-from\\" + style=\\"\\" Render 2: - class=\\"enter enter-from\\" + class=\\"enter enter-to\\" Render 3: Transition took at least 50ms (yes) - class=\\"enter enter-to\\" + class=\\"enter-to\\" Render 4: - class=\\"enter-to\\" + class=\\"leave leave-from\\" Render 5: - class=\\"leave leave-from\\" + class=\\"leave leave-to\\" Render 6: Transition took at least 75ms (yes) - class=\\"leave leave-to\\" - style=\\"\\" + class=\\"leave-to\\" + hidden=\\"\\" + style=\\"display: none;\\" Render 7: - class=\\"leave-to\\" - hidden=\\"\\" - style=\\"display: none;\\" + class=\\"enter enter-from\\" + style=\\"\\" Render 8: - class=\\"enter enter-from\\" + class=\\"enter enter-to\\" Render 9: Transition took at least 75ms (yes) - class=\\"enter enter-to\\" + class=\\"enter-to\\"" `) }) ) }) describe('nested transitions', () => { xit( 'should not unmount the whole tree when some children are still transitioning', suppressConsoleLogs(async () => { let slowLeaveDuration = 500 let fastLeaveDuration = 150 function Example() { let [show, setShow] = useState(true) return ( <> I am fast I am slow ) } let timeline = await executeTimeline(, [ // Toggle to hide ({ getByTestId }) => { fireEvent.click(getByTestId('toggle')) return [ null, // Initial render null, // Setup leave classes fastLeaveDuration, // Done with fast leave slowLeaveDuration - fastLeaveDuration, // Done with slow leave (which starts at the same time, but it is compaired with previous render snapshot so we have to subtract those) ] }, ]) expect(timeline).toMatchInlineSnapshot(` "Render 1: -
+
--- -
+
Render 2: - class=\\"leave-fast leave-from\\" + class=\\"leave-fast leave-to\\" --- - class=\\"leave-slow leave-from\\" + class=\\"leave-slow leave-to\\" Render 3: Transition took at least 150ms (yes) - class=\\"leave-fast leave-to\\" - > - I am fast -
-
-
- I am slow -
-
" `) }) ) xit( 'should not unmount the whole tree when some children are still transitioning', suppressConsoleLogs(async () => { let slowLeaveDuration = 150 let fastLeaveDuration = 50 function Example() { let [show, setShow] = useState(true) return ( <> I am fast I am my own root component and I don't talk to the parent I am slow ) } let timeline = await executeTimeline(, [ // Toggle to hide ({ getByTestId }) => { fireEvent.click(getByTestId('toggle')) return [ null, // Initial render null, // Setup leave classes fastLeaveDuration, // Done with fast leave slowLeaveDuration - fastLeaveDuration, // Done with slow leave (which starts at the same time, but it is compaired with previous render snapshot so we have to subtract those) ] }, ]) expect(timeline).toMatchInlineSnapshot(` "Render 1: -
+
--- -
+
--- -
+
Render 2: - class=\\"leave-fast leave-from\\" + class=\\"leave-fast leave-to\\" --- - class=\\"leave-slow leave-from\\" + class=\\"leave-slow leave-to\\" Render 3: Transition took at least 50ms (yes) - class=\\"leave-fast leave-to\\" - > - - I am fast - -
- I am my own root component and I don't talk to the parent -
-
-
-
- I am slow -
-
" `) }) ) }) }) describe('Events', () => { xit( 'should fire events for all the stages', suppressConsoleLogs(async () => { let eventHandler = jest.fn() let enterDuration = 50 let leaveDuration = 75 function Example() { let [show, setShow] = useState(false) let start = useRef(Date.now()) useLayoutEffect(() => { start.current = Date.now() }, []) return ( <> eventHandler('beforeEnter', Date.now() - start.current)} afterEnter={() => eventHandler('afterEnter', Date.now() - start.current)} beforeLeave={() => eventHandler('beforeLeave', Date.now() - start.current)} afterLeave={() => eventHandler('afterLeave', Date.now() - start.current)} // Class names enter="enter" enterFrom="enter-from" enterTo="enter-to" leave="leave" leaveFrom="leave-from" leaveTo="leave-to" > Hello! ) } let timeline = await executeTimeline(, [ // Toggle to show ({ getByTestId }) => { fireEvent.click(getByTestId('toggle')) return executeTimeline.fullTransition(enterDuration) }, // Toggle to hide ({ getByTestId }) => { fireEvent.click(getByTestId('toggle')) return executeTimeline.fullTransition(leaveDuration) }, ]) expect(timeline).toMatchInlineSnapshot(` "Render 1: +
+ + Hello! + +
Render 2: - class=\\"enter enter-from\\" + class=\\"enter enter-to\\" Render 3: Transition took at least 50ms (yes) - class=\\"enter enter-to\\" + class=\\"enter-to\\" Render 4: - class=\\"enter-to\\" + class=\\"leave leave-from\\" Render 5: - class=\\"leave leave-from\\" + class=\\"leave leave-to\\" Render 6: Transition took at least 75ms (yes) -
- - Hello! - -
" `) expect(eventHandler).toHaveBeenCalledTimes(4) expect(eventHandler.mock.calls.map(([name]) => name)).toEqual([ // Order is important here 'beforeEnter', 'afterEnter', 'beforeLeave', 'afterLeave', ]) let enterHookDiff = eventHandler.mock.calls[1][1] - eventHandler.mock.calls[0][1] expect(enterHookDiff).toBeGreaterThanOrEqual(enterDuration) expect(enterHookDiff).toBeLessThanOrEqual(enterDuration * 3) let leaveHookDiff = eventHandler.mock.calls[3][1] - eventHandler.mock.calls[2][1] expect(leaveHookDiff).toBeGreaterThanOrEqual(leaveDuration) expect(leaveHookDiff).toBeLessThanOrEqual(leaveDuration * 3) }) ) it( 'should fire events in the correct order', suppressConsoleLogs(async () => { let eventHandler = jest.fn() let enterDuration = 50 let leaveDuration = 75 function Example() { let [show, setShow] = useState(false) let start = useRef(Date.now()) useLayoutEffect(() => { start.current = Date.now() }, []) function mark(input: string) { eventHandler(input) } function getProps(name: string) { return { // Events beforeEnter: () => mark(`${name}: beforeEnter`), afterEnter: () => mark(`${name}: afterEnter`), beforeLeave: () => mark(`${name}: beforeLeave`), afterLeave: () => mark(`${name}: afterLeave`), // Class names enter: `${name} enter`, enterFrom: 'enter-from', enterTo: 'enter-to', leave: `${name} leave`, leaveFrom: 'leave-from', leaveTo: 'leave-to', } } return ( <> Child 1. Child 2. Child 2.1. Child 2.2. ) } await executeTimeline(, [ // Toggle to show ({ getByTestId }) => { fireEvent.click(getByTestId('toggle')) return executeTimeline.fullTransition(enterDuration * 3) }, // Toggle to hide ({ getByTestId }) => { fireEvent.click(getByTestId('toggle')) return executeTimeline.fullTransition(leaveDuration * 3) }, // Toggle to show (again) ({ getByTestId }) => { fireEvent.click(getByTestId('toggle')) return executeTimeline.fullTransition(enterDuration * 3) }, // Toggle to hide (again) ({ getByTestId }) => { fireEvent.click(getByTestId('toggle')) return executeTimeline.fullTransition(leaveDuration * 3) }, ]) expect(eventHandler.mock.calls.flat()).toEqual([ 'action(SHOW)', 'root: beforeEnter', 'child-1: beforeEnter', 'child-2: beforeEnter', 'child-2-1: beforeEnter', 'child-2-2: beforeEnter', 'child-1: afterEnter', 'child-2-2: afterEnter', 'child-2-1: afterEnter', 'child-2: afterEnter', 'root: afterEnter', 'action(HIDE)', 'child-1: beforeLeave', 'child-2-1: beforeLeave', 'child-2-2: beforeLeave', 'child-2: beforeLeave', 'root: beforeLeave', 'child-1: afterLeave', 'child-2-2: afterLeave', 'child-2-1: afterLeave', 'child-2: afterLeave', 'root: afterLeave', 'action(SHOW)', 'root: beforeEnter', 'child-1: beforeEnter', 'child-2: beforeEnter', 'child-2-1: beforeEnter', 'child-2-2: beforeEnter', 'child-1: afterEnter', 'child-2-2: afterEnter', 'child-2-1: afterEnter', 'child-2: afterEnter', 'root: afterEnter', 'action(HIDE)', 'child-1: beforeLeave', 'child-2-1: beforeLeave', 'child-2-2: beforeLeave', 'child-2: beforeLeave', 'root: beforeLeave', 'child-1: afterLeave', 'child-2-2: afterLeave', 'child-2-1: afterLeave', 'child-2: afterLeave', 'root: afterLeave', ]) }) ) it( 'should fire only one event for a given component change', suppressConsoleLogs(async () => { let eventHandler = jest.fn() let enterDuration = 50 let leaveDuration = 75 function Example() { let [show, setShow] = useState(false) let [start, setStart] = useState(Date.now()) useEffect(() => setStart(Date.now()), []) return ( <> eventHandler('beforeEnter', Date.now() - start)} afterEnter={() => eventHandler('afterEnter', Date.now() - start)} beforeLeave={() => eventHandler('beforeLeave', Date.now() - start)} afterLeave={() => eventHandler('afterLeave', Date.now() - start)} enter="enter-2" enterFrom="enter-from" enterTo="enter-to" leave="leave-2" leaveFrom="leave-from" leaveTo="leave-to" > ) } render() fireEvent.click(document.querySelector('[data-testid=show]')!) await new Promise((resolve) => setTimeout(resolve, 1000)) fireEvent.click(document.querySelector('[data-testid=hide]')!) await new Promise((resolve) => setTimeout(resolve, 1000)) expect(eventHandler).toHaveBeenCalledTimes(4) expect(eventHandler.mock.calls.map(([name]) => name)).toEqual([ // Order is important here 'beforeEnter', 'afterEnter', 'beforeLeave', 'afterLeave', ]) }) ) })