OK, I’m mostly React Native so this has been interesting…
There are different ways of testing. This tests the behaviors or the page. I didn’t have an actual component from you so I had to make up one that was close to what you had:
import React, {useState, useEffect, useCallback} from 'react'
const App = () => {
const [showDropdown, setShowDropdown] = useState(false)
const handler = useCallback(() => setShowDropdown(false), [setShowDropdown])
useEffect(() => {
window.addEventListener('click', handler)
return () => {
window.removeEventListener('click', handler)
}
}, [handler])
const handleDropdownClick = (e) => {
e.stopPropagation()
setShowDropdown(showDropdown => !showDropdown)
}
return (
<div>
<h1>My App</h1>
{showDropdown && (
<div data-testid="dropdown">
<h2>My Dropdown</h2>
</div>
)}
<button type="button" onClick={handleDropdownClick} data-testid="toggle-dropdown-button">Toggle Dropdown</button>
</div>
)
}
export default App
I tested it thusly:
import App from './App';
import {fireEvent, render, screen } from '@testing-library/react'
describe('App', () => {
describe('the component', () => {
const { container } = render(<App />)
it('should match snapshot', () => {
expect(container).toMatchSnapshot()
})
})
describe('the dropdown', () => {
describe('on initial render', () => {
it('should not show', async () => {
render(<App />)
expect(screen.queryByTestId('dropdown')).not.toBeInTheDocument()
})
it('should show toggle show button', () => {
render(<App />)
expect(screen.getByTestId('toggle-dropdown-button')).toBeInTheDocument()
})
})
})
describe('the toggle button', () => {
describe('with a single press', () => {
it('should reveal dropdown', () => {
render(<App />)
expect(screen.queryByTestId('dropdown')).toBeNull()
fireEvent.click(screen.getByTestId('toggle-dropdown-button'))
expect(screen.getByTestId('dropdown')).toBeTruthy()
})
})
describe('with one press opens, a second closes', () => {
it('should reveal dropdown', () => {
render(<App />)
expect(screen.queryByTestId('dropdown')).toBeNull()
fireEvent.click(screen.getByTestId('toggle-dropdown-button'))
expect(screen.getByTestId('dropdown')).toBeTruthy()
fireEvent.click(screen.getByTestId('toggle-dropdown-button'))
expect(screen.queryByTestId('dropdown')).toBeNull()
})
})
})
describe('the document', () => {
it('should close an open dropdown', () => {
render(<App />)
expect(screen.queryByTestId('dropdown')).toBeNull()
fireEvent.click(screen.getByTestId('toggle-dropdown-button'))
expect(screen.getByTestId('dropdown')).toBeTruthy()
fireEvent.click(document.body)
expect(screen.queryByTestId('dropdown')).toBeNull()
})
})
})
In reality, I probably would have pulled that useState and useEffect guts into a custom hook to simplify this component. Then I could just test the hook exhaustively as a function.
In order to test this, I added some test ids, but there are other ways to do that. I also used a useCallback on your handler function. I think that that has to be constant reference otherwise the teardown in useEffect won’t work properly - how will it know what to tear down?