Files
headlessui/packages/@headlessui-react/src/components/transitions/transition.test.tsx
T
Jordan Pittman 0ff2326171 Don’t fire afterLeave event more than once for a given transition (#2267)
* Don’t fire afterLeave event more than once for a given component

* Port test to React

* Fix CS

* Remove focus on test

* Update changelog
2023-02-10 09:36:09 -05:00

1490 lines
43 KiB
TypeScript

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<void>((resolve) => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
resolve()
})
})
})
}
it('should not steal the ref from the child', async () => {
let fn = jest.fn()
render(
<Transition show={true} as={Fragment}>
<div ref={fn}>...</div>
</Transition>
)
await nextFrame()
expect(fn).toHaveBeenCalled()
})
it('should render without crashing', () => {
render(
<Transition show={true}>
<div className="hello">Children</div>
</Transition>
)
})
it('should be possible to render a Transition without children', () => {
render(<Transition show={true} className="transition" />)
expect(document.getElementsByClassName('transition')).not.toBeNull()
})
it(
'should yell at us when we forget the required show prop',
suppressConsoleLogs(() => {
expect.assertions(1)
expect(() => {
render(
<Transition>
<div className="hello">Children</div>
</Transition>
)
}).toThrowErrorMatchingInlineSnapshot(
`"A <Transition /> 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(<Transition show={true}>Children</Transition>)
expect(container.firstChild).toMatchInlineSnapshot(`
<div>
Children
</div>
`)
})
it('should passthrough all the props (that we do not use internally)', () => {
let { container } = render(
<Transition show={true} id="root" className="text-blue-400">
Children
</Transition>
)
expect(container.firstChild).toMatchInlineSnapshot(`
<div
class="text-blue-400"
id="root"
>
Children
</div>
`)
})
it('should render another component if the `as` prop is used and its children by default', () => {
let { container } = render(
<Transition show={true} as="a">
Children
</Transition>
)
expect(container.firstChild).toMatchInlineSnapshot(`
<a>
Children
</a>
`)
})
it('should passthrough all the props (that we do not use internally) even when using an `as` prop', () => {
let { container } = render(
<Transition show={true} as="a" href="/" className="text-blue-400">
Children
</Transition>
)
expect(container.firstChild).toMatchInlineSnapshot(`
<a
class="text-blue-400"
href="/"
>
Children
</a>
`)
})
it('should render nothing when the show prop is false', () => {
let { container } = render(<Transition show={false}>Children</Transition>)
expect(container.firstChild).toMatchInlineSnapshot(`null`)
})
it('should be possible to change the underlying DOM tag', () => {
let { container } = render(
<Transition show={true} as="a">
Children
</Transition>
)
expect(container.firstChild).toMatchInlineSnapshot(`
<a>
Children
</a>
`)
})
it('should be possible to use a render prop', () => {
let { container } = render(
<Transition show={true} as={Fragment}>
{() => <span>Children</span>}
</Transition>
)
expect(container.firstChild).toMatchInlineSnapshot(`
<span>
Children
</span>
`)
})
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 <span {...props}>Children</span>
}
expect(() => {
render(
<Transition show={true} as={Fragment}>
{() => <Dummy />}
</Transition>
)
}).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 `<Transition.Child />` in a parent <Transition /> component',
suppressConsoleLogs(() => {
expect.assertions(1)
expect(() => {
render(
<div className="My Page">
<Transition.Child>Oops</Transition.Child>
</div>
)
}).toThrowErrorMatchingInlineSnapshot(
`"A <Transition.Child /> is used but it is missing a parent <Transition /> or <Transition.Root />."`
)
})
)
it('should be possible to render a Transition.Child without children', () => {
render(
<Transition show={true}>
<Transition.Child className="transition" />
</Transition>
)
expect(document.getElementsByClassName('transition')).not.toBeNull()
})
it('should be possible to use a Transition.Root and a Transition.Child', () => {
render(
<Transition.Root show={true}>
<Transition.Child className="transition" />
</Transition.Root>
)
expect(document.getElementsByClassName('transition')).not.toBeNull()
})
it('should be possible to nest transition components', () => {
let { container } = render(
<div className="My Page">
<Transition show={true}>
<Transition.Child>Sidebar</Transition.Child>
<Transition.Child>Content</Transition.Child>
</Transition>
</div>
)
expect(container.firstChild).toMatchInlineSnapshot(`
<div
class="My Page"
>
<div>
<div>
Sidebar
</div>
<div>
Content
</div>
</div>
</div>
`)
})
it('should be possible to change the underlying DOM tag of the Transition.Child components', () => {
let { container } = render(
<div className="My Page">
<Transition show={true}>
<Transition.Child as="aside">Sidebar</Transition.Child>
<Transition.Child as="section">Content</Transition.Child>
</Transition>
</div>
)
expect(container.firstChild).toMatchInlineSnapshot(`
<div
class="My Page"
>
<div>
<aside>
Sidebar
</aside>
<section>
Content
</section>
</div>
</div>
`)
})
it('should be possible to change the underlying DOM tag of the Transition component and Transition.Child components', () => {
let { container } = render(
<div className="My Page">
<Transition show={true} as="article">
<Transition.Child as="aside">Sidebar</Transition.Child>
<Transition.Child as="section">Content</Transition.Child>
</Transition>
</div>
)
expect(container.firstChild).toMatchInlineSnapshot(`
<div
class="My Page"
>
<article>
<aside>
Sidebar
</aside>
<section>
Content
</section>
</article>
</div>
`)
})
it('should be possible to use render props on the Transition.Child components', () => {
let { container } = render(
<div className="My Page">
<Transition show={true}>
<Transition.Child as={Fragment}>{() => <aside>Sidebar</aside>}</Transition.Child>
<Transition.Child as={Fragment}>{() => <section>Content</section>}</Transition.Child>
</Transition>
</div>
)
expect(container.firstChild).toMatchInlineSnapshot(`
<div
class="My Page"
>
<div>
<aside>
Sidebar
</aside>
<section>
Content
</section>
</div>
</div>
`)
})
it('should be possible to use render props on the Transition and Transition.Child components', () => {
let { container } = render(
<div className="My Page">
<Transition show={true} as={Fragment}>
{() => (
<article>
<Transition.Child as={Fragment}>{() => <aside>Sidebar</aside>}</Transition.Child>
<Transition.Child as={Fragment}>
{() => <section>Content</section>}
</Transition.Child>
</article>
)}
</Transition>
</div>
)
expect(container.firstChild).toMatchInlineSnapshot(`
<div
class="My Page"
>
<article>
<aside>
Sidebar
</aside>
<section>
Content
</section>
</article>
</div>
`)
})
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 <div {...props} />
}
expect(() => {
render(
<div className="My Page">
<Transition show={true}>
<Transition.Child as={Fragment}>{() => <Dummy>Sidebar</Dummy>}</Transition.Child>
<Transition.Child as={Fragment}>{() => <Dummy>Content</Dummy>}</Transition.Child>
</Transition>
</div>
)
}).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 <div {...props} />
}
expect(() => {
render(
<div className="My Page">
<Transition show={true} as={Fragment}>
{() => (
<Dummy>
<Transition.Child>{() => <aside>Sidebar</aside>}</Transition.Child>
<Transition.Child>{() => <section>Content</section>}</Transition.Child>
</Dummy>
)}
</Transition>
</div>
)
}).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(
<Transition
show={true}
enter="enter"
enterFrom="enter-from"
enterTo="enter-to"
leave="leave"
leaveFrom="leave-from"
leaveTo="leave-to"
>
Children
</Transition>
)
expect(container.firstChild).toMatchInlineSnapshot(`
<div>
Children
</div>
`)
})
it('should be possible to passthrough the transition classes and immediately apply the enter transitions when appear is set to true', () => {
let { container } = render(
<Transition
show={true}
appear={true}
enter="enter"
enterFrom="enter-from"
enterTo="enter-to"
leave="leave"
leaveFrom="leave-from"
leaveTo="leave-to"
>
Children
</Transition>
)
expect(container.firstChild).toMatchInlineSnapshot(`
<div
class="enter enter-from"
>
Children
</div>
`)
})
})
})
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 (
<>
<style>{`.enter { transition-duration: ${enterDuration}ms; } .from { opacity: 0%; } .to { opacity: 100%; }`}</style>
<Transition show={show} enter="enter" enterFrom="from" enterTo="to">
<span>Hello!</span>
</Transition>
<button data-testid="toggle" onClick={() => setShow((v) => !v)}>
Toggle
</button>
</>
)
}
let timeline = await executeTimeline(<Example />, [
// Toggle to show
({ getByTestId }) => {
fireEvent.click(getByTestId('toggle'))
return executeTimeline.fullTransition(enterDuration)
},
])
expect(timeline).toMatchInlineSnapshot(`
"Render 1:
+ <div
+ class=\\"enter from\\"
+ >
+ <span>
+ Hello!
+ </span>
+ </div>
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 (
<>
<style>{`.enter { transition-duration: ${
enterDuration / 1000
}s; } .from { opacity: 0%; } .to { opacity: 100%; }`}</style>
<Transition show={show} enter="enter" enterFrom="from" enterTo="to">
<span>Hello!</span>
</Transition>
<button data-testid="toggle" onClick={() => setShow((v) => !v)}>
Toggle
</button>
</>
)
}
let timeline = await executeTimeline(<Example />, [
// Toggle to show
({ getByTestId }) => {
fireEvent.click(getByTestId('toggle'))
return executeTimeline.fullTransition(enterDuration)
},
])
expect(timeline).toMatchInlineSnapshot(`
"Render 1:
+ <div
+ class=\\"enter from\\"
+ >
+ <span>
+ Hello!
+ </span>
+ </div>
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 (
<>
<style>{`.enter { transition-duration: ${
enterDuration / 1000
}s; } .from { opacity: 0%; } .to { opacity: 100%; }`}</style>
<Transition show={show} unmount={false} enter="enter" enterFrom="from" enterTo="to">
<span>Hello!</span>
</Transition>
<button data-testid="toggle" onClick={() => setShow((v) => !v)}>
Toggle
</button>
</>
)
}
let timeline = await executeTimeline(<Example />, [
// 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 (
<>
<style>{`.enter { transition-duration: ${enterDuration}ms; } .from { opacity: 0%; } .to { opacity: 100%; }`}</style>
<Transition show={show} enter="enter" enterFrom="from" enterTo="to">
<span>Hello!</span>
</Transition>
<button data-testid="toggle" onClick={() => setShow((v) => !v)}>
Toggle
</button>
</>
)
}
let timeline = await executeTimeline(<Example />, [
// Toggle to show
({ getByTestId }) => {
fireEvent.click(getByTestId('toggle'))
return executeTimeline.fullTransition(enterDuration)
},
])
expect(timeline).toMatchInlineSnapshot(`
"Render 1:
+ <div
+ class=\\"enter from\\"
+ >
+ <span>
+ Hello!
+ </span>
+ </div>
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 (
<>
<style>{`.leave { transition-duration: ${leaveDuration}ms; } .from { opacity: 100%; } .to { opacity: 0%; }`}</style>
<Transition show={show} leave="leave" leaveFrom="from" leaveTo="to">
<span>Hello!</span>
</Transition>
<button data-testid="toggle" onClick={() => setShow((v) => !v)}>
Toggle
</button>
</>
)
}
let timeline = await executeTimeline(<Example />, [
// Toggle to hide
({ getByTestId }) => {
fireEvent.click(getByTestId('toggle'))
return executeTimeline.fullTransition(leaveDuration)
},
])
expect(timeline).toMatchInlineSnapshot(`
"Render 1:
- <div>
+ <div
+ class=\\"leave from\\"
+ >
Render 2:
- class=\\"leave from\\"
+ class=\\"leave to\\"
Render 3: Transition took at least 100ms (yes)
- <div
- class=\\"leave to\\"
- >
- <span>
- Hello!
- </span>
- </div>"
`)
})
)
xit(
'should transition out completely (render strategy = hidden)',
suppressConsoleLogs(async () => {
let leaveDuration = 50
function Example() {
let [show, setShow] = useState(true)
return (
<>
<style>{`.leave { transition-duration: ${leaveDuration}ms; } .from { opacity: 0%; } .to { opacity: 100%; }`}</style>
<Transition show={show} unmount={false} leave="leave" leaveFrom="from" leaveTo="to">
<span>Hello!</span>
</Transition>
<button data-testid="toggle" onClick={() => setShow((v) => !v)}>
Toggle
</button>
</>
)
}
let timeline = await executeTimeline(<Example />, [
// Toggle to hide
({ getByTestId }) => {
fireEvent.click(getByTestId('toggle'))
return executeTimeline.fullTransition(leaveDuration)
},
])
expect(timeline).toMatchInlineSnapshot(`
"Render 1:
- <div>
+ <div
+ class=\\"leave from\\"
+ >
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 (
<>
<style>{`.enter { transition-duration: ${enterDuration}ms; } .enter-from { opacity: 0%; } .enter-to { opacity: 100%; }`}</style>
<style>{`.leave { transition-duration: ${leaveDuration}ms; } .leave-from { opacity: 100%; } .leave-to { opacity: 0%; }`}</style>
<Transition
show={show}
enter="enter"
enterFrom="enter-from"
enterTo="enter-to"
leave="leave"
leaveFrom="leave-from"
leaveTo="leave-to"
>
<span>Hello!</span>
</Transition>
<button data-testid="toggle" onClick={() => setShow((v) => !v)}>
Toggle
</button>
</>
)
}
let timeline = await executeTimeline(<Example />, [
// 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:
+ <div
+ class=\\"enter enter-from\\"
+ >
+ <span>
+ Hello!
+ </span>
+ </div>
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)
- <div
- class=\\"leave leave-to\\"
- >
- <span>
- Hello!
- </span>
- </div>"
`)
})
)
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 (
<>
<style>{`.enter { transition-duration: ${enterDuration}ms; } .enter-from { opacity: 0%; } .enter-to { opacity: 100%; }`}</style>
<style>{`.leave { transition-duration: ${leaveDuration}ms; } .leave-from { opacity: 100%; } .leave-to { opacity: 0%; }`}</style>
<Transition
show={show}
unmount={false}
enter="enter"
enterFrom="enter-from"
enterTo="enter-to"
leave="leave"
leaveFrom="leave-from"
leaveTo="leave-to"
>
<span>Hello!</span>
</Transition>
<button data-testid="toggle" onClick={() => setShow((v) => !v)}>
Toggle
</button>
</>
)
}
let timeline = await executeTimeline(<Example />, [
// 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 (
<>
<style>{`.leave-slow { transition-duration: ${slowLeaveDuration}ms; } .leave-from { opacity: 100%; } .leave-to { opacity: 0%; }`}</style>
<style>{`.leave-fast { transition-duration: ${fastLeaveDuration}ms; }`}</style>
<Transition show={show}>
<Transition.Child leave="leave-fast" leaveFrom="leave-from" leaveTo="leave-to">
I am fast
</Transition.Child>
<Transition.Child leave="leave-slow" leaveFrom="leave-from" leaveTo="leave-to">
I am slow
</Transition.Child>
</Transition>
<button data-testid="toggle" onClick={() => setShow((v) => !v)}>
Toggle
</button>
</>
)
}
let timeline = await executeTimeline(<Example />, [
// 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:
- <div>
+ <div
+ class=\\"leave-fast leave-from\\"
+ >
---
- <div>
+ <div
+ class=\\"leave-slow leave-from\\"
+ >
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
- </div>
- <div
Render 4: Transition took at least 350ms (yes)
- <div>
- <div
- class=\\"leave-slow leave-to\\"
- >
- I am slow
- </div>
- </div>"
`)
})
)
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 (
<>
<style>{`.leave-slow { transition-duration: ${slowLeaveDuration}ms; } .leave-from { opacity: 100%; } .leave-to { opacity: 0%; }`}</style>
<style>{`.leave-fast { transition-duration: ${fastLeaveDuration}ms; }`}</style>
<Transition show={show}>
<Transition.Child leave="leave-fast" leaveFrom="leave-from" leaveTo="leave-to">
<span>I am fast</span>
<Transition show={show} leave="leave-slow">
I am my own root component and I don't talk to the parent
</Transition>
</Transition.Child>
<Transition.Child leave="leave-slow" leaveFrom="leave-from" leaveTo="leave-to">
I am slow
</Transition.Child>
</Transition>
<button data-testid="toggle" onClick={() => setShow((v) => !v)}>
Toggle
</button>
</>
)
}
let timeline = await executeTimeline(<Example />, [
// 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:
- <div>
+ <div
+ class=\\"leave-fast leave-from\\"
+ >
---
- <div>
+ <div
+ class=\\"leave-slow\\"
+ >
---
- <div>
+ <div
+ class=\\"leave-slow leave-from\\"
+ >
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\\"
- >
- <span>
- I am fast
- </span>
- <div
- class=\\"leave-slow\\"
- >
- I am my own root component and I don't talk to the parent
- </div>
- </div>
- <div
Render 4: Transition took at least 100ms (yes)
- <div>
- <div
- class=\\"leave-slow leave-to\\"
- >
- I am slow
- </div>
- </div>"
`)
})
)
})
})
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 (
<>
<style>{`.enter { transition-duration: ${enterDuration}ms; } .enter-from { opacity: 0%; } .enter-to { opacity: 100%; }`}</style>
<style>{`.leave { transition-duration: ${leaveDuration}ms; } .leave-from { opacity: 100%; } .leave-to { opacity: 0%; }`}</style>
<Transition
show={show}
// Events
beforeEnter={() => 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"
>
<span>Hello!</span>
</Transition>
<button data-testid="toggle" onClick={() => setShow((v) => !v)}>
Toggle
</button>
</>
)
}
let timeline = await executeTimeline(<Example />, [
// 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:
+ <div
+ class=\\"enter enter-from\\"
+ >
+ <span>
+ Hello!
+ </span>
+ </div>
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)
- <div
- class=\\"leave leave-to\\"
- >
- <span>
- Hello!
- </span>
- </div>"
`)
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 (
<>
<style>{`.enter-from { opacity: 0%; } .enter-to { opacity: 100%; }`}</style>
<style>{`.leave-from { opacity: 100%; } .leave-to { opacity: 0%; }`}</style>
<style>{`.root.enter { transition-duration: ${enterDuration}ms; }`}</style>
<style>{`.root.leave { transition-duration: ${leaveDuration}ms; }`}</style>
<style>{`.child-1.enter { transition-duration: ${enterDuration * 1.5}ms; }`}</style>
<style>{`.child-1.leave { transition-duration: ${leaveDuration * 1.5}ms; }`}</style>
<style>{`.child-2.enter { transition-duration: ${enterDuration * 2}ms; }`}</style>
<style>{`.child-2.leave { transition-duration: ${leaveDuration * 2}ms; }`}</style>
<style>{`.child-2-1.enter { transition-duration: ${enterDuration * 3}ms; }`}</style>
<style>{`.child-2-1.leave { transition-duration: ${leaveDuration * 3}ms; }`}</style>
<style>{`.child-2-2.enter { transition-duration: ${enterDuration * 2.5}ms; }`}</style>
<style>{`.child-2-2.leave { transition-duration: ${leaveDuration * 2.5}ms; }`}</style>
<Transition show={show} {...getProps('root')}>
<Transition.Child {...getProps('child-1')}>Child 1.</Transition.Child>
<Transition.Child {...getProps('child-2')}>
Child 2.
<Transition.Child {...getProps('child-2-1')}>Child 2.1.</Transition.Child>
<Transition.Child {...getProps('child-2-2')}>Child 2.2.</Transition.Child>
</Transition.Child>
</Transition>
<button
data-testid="toggle"
onClick={() => {
eventHandler(`action(${show ? 'HIDE' : 'SHOW'})`)
setShow((v) => !v)
}}
>
Toggle
</button>
</>
)
}
await executeTimeline(<Example />, [
// 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 (
<>
<style>{`
.enter-1 { transition-duration: ${enterDuration * 1}ms; }
.enter-2 { transition-duration: ${enterDuration * 2}ms; }
.enter-from { opacity: 0%; }
.enter-to { opacity: 100%; }
.leave-1 { transition-duration: ${leaveDuration * 1}ms; }
.leave-2 { transition-duration: ${leaveDuration * 2}ms; }
.leave-from { opacity: 100%; }
.leave-to { opacity: 0%; }
`}</style>
<Transition.Root
show={show}
as="div"
beforeEnter={() => 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"
>
<Transition.Child
enter="enter-1"
enterFrom="enter-from"
enterTo="enter-to"
leave="leave-1"
leaveFrom="leave-from"
leaveTo="leave-to"
/>
<Transition.Child
enter="enter-1"
enterFrom="enter-from"
enterTo="enter-to"
leave="leave-1"
leaveFrom="leave-from"
leaveTo="leave-to"
>
<button data-testid="hide" onClick={() => setShow(false)}>
Hide
</button>
</Transition.Child>
</Transition.Root>
<button data-testid="show" onClick={() => setShow(true)}>
Show
</button>
</>
)
}
render(<Example />)
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',
])
})
)
})