-
Notifications
You must be signed in to change notification settings - Fork 938
Expand file tree
/
Copy pathdownshift.lifecycle.js
More file actions
285 lines (251 loc) · 8.35 KB
/
downshift.lifecycle.js
File metadata and controls
285 lines (251 loc) · 8.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
import * as React from 'react'
import {act, fireEvent, render, screen} from '@testing-library/react'
import Downshift from '../'
import {setStatus, scrollIntoView} from '../utils'
jest.useFakeTimers()
jest.mock('../utils/scrollIntoView.ts', () => ({
scrollIntoView: jest.fn(),
}))
jest.mock('../utils/setA11yStatus.ts', () => ({
setStatus: jest.fn(),
}))
afterEach(() => {
scrollIntoView.mockReset()
})
test('do not set state after unmount', () => {
const handleStateChange = jest.fn()
const childrenSpy = jest.fn(({getInputProps}) => (
<div>
<input {...getInputProps({'data-testid': 'input'})} />
<button {...getInputProps({'data-testid': 'button'})}>Toggle</button>
</div>
))
const MyComponent = () => (
<Downshift onStateChange={handleStateChange}>{childrenSpy}</Downshift>
)
const {container, unmount} = render(<MyComponent />)
const button = screen.queryByTestId('button')
document.body.appendChild(container)
// blur toggle button
fireEvent.blur(button)
handleStateChange.mockClear()
// unmount
unmount()
expect(handleStateChange).toHaveBeenCalledTimes(0)
})
test('handles mouse events properly to reset state', () => {
const handleStateChange = jest.fn()
const childrenSpy = jest.fn(({getInputProps}) => (
<div>
<input {...getInputProps({'data-testid': 'input'})} />
</div>
))
const MyComponent = () => (
<Downshift onStateChange={handleStateChange}>{childrenSpy}</Downshift>
)
const {container, unmount} = render(<MyComponent />)
const input = screen.queryByTestId('input')
document.body.appendChild(container)
// open the menu
fireEvent.keyDown(input, {key: 'ArrowDown'})
handleStateChange.mockClear()
// mouse down and up on within the autocomplete node
mouseDownAndUp(input)
expect(handleStateChange).toHaveBeenCalledTimes(0)
// mouse down and up on outside the autocomplete node
mouseDownAndUp(document.body)
expect(handleStateChange).toHaveBeenCalledTimes(1)
childrenSpy.mockClear()
// does not call our state change handler when no state changes
mouseDownAndUp(document.body)
expect(handleStateChange).toHaveBeenCalledTimes(1)
// does not rerender when no state changes
expect(childrenSpy).not.toHaveBeenCalled()
// cleans up
unmount()
mouseDownAndUp(document.body)
expect(handleStateChange).toHaveBeenCalledTimes(1)
})
test('handles state change for touchevent events', () => {
const handleStateChange = jest.fn()
const childrenSpy = jest.fn(({getToggleButtonProps}) => (
<button {...getToggleButtonProps({'data-testid': 'button'})} />
))
const MyComponent = () => (
<Downshift onStateChange={handleStateChange}>{childrenSpy}</Downshift>
)
const {container, unmount} = render(<MyComponent />)
document.body.appendChild(container)
const button = screen.queryByTestId('button')
// touch outside for coverage
fireEvent.touchStart(document.body)
fireEvent.touchEnd(document.body)
// open menu
fireEvent.click(button)
jest.runAllTimers()
expect(handleStateChange).toHaveBeenCalledTimes(1)
// touchmove (scroll) outside downshift should not trigger state change
fireEvent.touchStart(document.body)
fireEvent.touchMove(document.body)
fireEvent.touchEnd(document.body)
jest.runAllTimers()
expect(handleStateChange).toHaveBeenCalledTimes(1)
// touch outside downshift
fireEvent.touchStart(document.body)
fireEvent.touchEnd(document.body)
jest.runAllTimers()
expect(handleStateChange).toHaveBeenCalledTimes(2)
unmount()
})
test('props update causes the a11y status to be updated', () => {
setStatus.mockReset()
const MyComponent = () => (
<Downshift isOpen={false}>
{({getInputProps, getItemProps, isOpen}) => (
<div>
<input {...getInputProps()} />
{/* eslint-disable-next-line jest/no-conditional-in-test */}
{isOpen ? <div {...getItemProps({item: 'foo', index: 0})} /> : null}
</div>
)}
</Downshift>
)
const {container, unmount} = render(<MyComponent />)
render(<MyComponent isOpen={true} />, {container})
jest.runAllTimers()
expect(setStatus).toHaveBeenCalledTimes(1)
render(<MyComponent isOpen={false} />, {container})
unmount()
jest.runAllTimers()
expect(setStatus).toHaveBeenCalledTimes(1)
})
test('inputValue initializes properly if the selectedItem is controlled and set', () => {
const childrenSpy = jest.fn(() => null)
render(<Downshift selectedItem="foo">{childrenSpy}</Downshift>)
expect(childrenSpy).toHaveBeenCalledWith(
expect.objectContaining({
inputValue: 'foo',
}),
)
})
test('inputValue initializes properly if selectedItem is set to 0', () => {
const childrenSpy = jest.fn(() => null)
render(<Downshift selectedItem={0}>{childrenSpy}</Downshift>)
expect(childrenSpy).toHaveBeenCalledWith(
expect.objectContaining({
inputValue: '0',
}),
)
})
test('props update of selectedItem will update the inputValue state', () => {
const childrenSpy = jest.fn(() => null)
const {container} = render(
<Downshift selectedItem={null}>{childrenSpy}</Downshift>,
)
childrenSpy.mockClear()
render(<Downshift selectedItem="foo">{childrenSpy}</Downshift>, {container})
expect(childrenSpy).toHaveBeenCalledWith(
expect.objectContaining({
inputValue: 'foo',
}),
)
})
test('the callback is invoked on selected item only if it is a function', () => {
let renderArg
const childrenSpy = jest.fn(controllerArg => {
renderArg = controllerArg
return <div />
})
const callbackSpy = jest.fn(x => x)
render(<Downshift selectedItem="foo">{childrenSpy}</Downshift>)
childrenSpy.mockClear()
callbackSpy.mockClear()
act(() => {
renderArg.selectItem('foo', {}, callbackSpy)
})
expect(callbackSpy).toHaveBeenCalledTimes(1)
act(() => {
renderArg.selectItem('foo', {})
})
})
test('props update of selectedItem will not update inputValue state', () => {
const onInputValueChangeSpy = jest.fn(() => null)
const initialProps = {
onInputValueChange: onInputValueChangeSpy,
selectedItemChanged: (prevItem, item) => prevItem.id !== item.id,
selectedItem: {id: '123', value: 'wow'},
// eslint-disable-next-line jest/no-conditional-in-test
itemToString: i => (i ? i.value : ''),
render: () => null,
}
const {container} = render(<Downshift {...initialProps} />)
onInputValueChangeSpy.mockClear()
render(
<Downshift
{...initialProps}
selectedItem={{id: '123', value: 'not wow'}}
/>,
{container},
)
expect(onInputValueChangeSpy).not.toHaveBeenCalled()
})
test('controlled highlighted index change scrolls the item into view', () => {
// sadly, testing scroll is really difficult in a jsdom environment.
// Perhaps eventually we'll add real integration tests with cypress
// or something, but for now we'll just mock the implementation of
// utils.scrollIntoView and ensure it's called with the proper arguments
// assuming that the test suite for utils.scrollIntoView will ensure
// this functionality doesn't break.
const oneHundredItems = Array.from({length: 100})
const renderFn = jest.fn(({getItemProps, getMenuProps}) => (
<div>
<div data-testid="menu" {...getMenuProps()}>
{oneHundredItems.map((x, i) => (
<div key={i} {...getItemProps({item: i})} data-testid={`item-${i}`} />
))}
</div>
</div>
))
const {container, updateProps} = setup({
highlightedIndex: 1,
render: renderFn,
})
document.body.appendChild(container)
renderFn.mockClear()
updateProps({highlightedIndex: 75})
expect(renderFn).toHaveBeenCalledTimes(1)
expect(scrollIntoView).toHaveBeenCalledTimes(1)
const menuDiv = screen.queryByTestId('menu')
expect(scrollIntoView).toHaveBeenCalledWith(
screen.queryByTestId('item-75'),
menuDiv,
)
})
function mouseDownAndUp(node) {
fireEvent.mouseDown(node)
fireEvent.mouseUp(node)
}
function setup({render: renderFn = () => <div />, ...props} = {}) {
// eslint-disable-next-line prefer-const
let container, renderArg
const childrenSpy = jest.fn(controllerArg => {
renderArg = controllerArg
return renderFn(controllerArg)
})
const updateProps = newProps => {
return render(
<Downshift children={childrenSpy} {...props} {...newProps} />,
{
container,
},
)
}
const renderUtils = updateProps()
container = renderUtils.container
return {
childrenSpy,
updateProps,
...renderUtils,
...renderArg,
}
}