@@ -33,10 +33,14 @@ const ControlledComboBox = ({ controlledItem }) => {
3333 const items = generateItems ( 5 , generateGenericItem ) ;
3434 const [ value , setValue ] = useState ( controlledItem || items [ 0 ] ) ;
3535 const [ onChangeCallCount , setOnChangeCallCount ] = useState ( 0 ) ;
36+ const [ onInputChangeCallCount , setOnInputChangeCallCount ] = useState ( 0 ) ;
3637 const controlledOnChange = ( { selectedItem } ) => {
3738 setValue ( selectedItem ) ;
3839 setOnChangeCallCount ( ( prevCount ) => prevCount + 1 ) ;
3940 } ;
41+ const controlledOnInputChange = ( ) => {
42+ setOnInputChangeCallCount ( ( prevCount ) => prevCount + 1 ) ;
43+ } ;
4044
4145 return (
4246 < div >
@@ -45,11 +49,13 @@ const ControlledComboBox = ({ controlledItem }) => {
4549 items = { items }
4650 selectedItem = { value }
4751 onChange = { controlledOnChange }
52+ onInputChange = { controlledOnInputChange }
4853 placeholder = "Filter..."
4954 type = "default"
5055 />
5156 < div > value: { value ?. label || 'none' } </ div >
5257 < div > onChangeCallCount: { onChangeCallCount } </ div >
58+ < div > onInputChangeCallCount: { onInputChangeCallCount } </ div >
5359 < button onClick = { ( ) => setValue ( items [ 3 ] ) } > Choose item 3</ button >
5460 < button onClick = { ( ) => setValue ( null ) } > reset</ button >
5561 </ div >
@@ -184,13 +190,74 @@ describe('ComboBox', () => {
184190 } ) ;
185191 } ) ;
186192
187- it ( 'capture filter text event onInputChange' , async ( ) => {
188- const onInputChange = jest . fn ( ) ;
189- render ( < ComboBox { ...mockProps } onInputChange = { onInputChange } /> ) ;
193+ describe ( 'onInputChange' , ( ) => {
194+ let onInputChange ;
195+ beforeEach ( ( ) => {
196+ onInputChange = jest . fn ( ) ;
197+ } ) ;
198+ it ( 'should not call onChange or onInputChange on initial render' , ( ) => {
199+ render ( < ComboBox { ...mockProps } onInputChange = { onInputChange } /> ) ;
200+ expect ( onInputChange ) . not . toHaveBeenCalled ( ) ;
201+ expect ( mockProps . onChange ) . not . toHaveBeenCalled ( ) ;
202+ } ) ;
190203
191- await userEvent . type ( findInputNode ( ) , 'something' ) ;
204+ it ( 'capture filter text event onInputChange' , async ( ) => {
205+ render ( < ComboBox { ...mockProps } onInputChange = { onInputChange } /> ) ;
206+ await userEvent . type ( findInputNode ( ) , 'something' ) ;
207+ expect ( onInputChange ) . toHaveBeenCalledWith ( 'something' ) ;
208+ } ) ;
209+
210+ it ( 'should call onInputChange when option is selected from dropdown' , async ( ) => {
211+ render ( < ComboBox { ...mockProps } onInputChange = { onInputChange } /> ) ;
212+ await openMenu ( ) ;
213+ expect ( onInputChange ) . not . toHaveBeenCalled ( ) ;
214+ await userEvent . click ( screen . getByRole ( 'option' , { name : 'Item 2' } ) ) ;
215+ expect ( onInputChange ) . toHaveBeenCalledWith ( 'Item 2' ) ;
216+ } ) ;
217+
218+ it ( 'should call onInputChange when option is cleared with button' , async ( ) => {
219+ render (
220+ < ComboBox
221+ { ...mockProps }
222+ initialSelectedItem = { mockProps . items [ 0 ] }
223+ onInputChange = { onInputChange }
224+ />
225+ ) ;
226+ expect ( onInputChange ) . not . toHaveBeenCalled ( ) ;
227+ await userEvent . click (
228+ screen . getByRole ( 'button' , { name : 'Clear selected item' } )
229+ ) ;
230+ expect ( onInputChange ) . toHaveBeenCalledWith ( '' ) ;
231+ } ) ;
232+
233+ it ( 'should not call onInputChange when combobox is interacted with but input value does not change' , async ( ) => {
234+ render (
235+ < ComboBox
236+ { ...mockProps }
237+ initialSelectedItem = { mockProps . items [ 0 ] }
238+ onInputChange = { onInputChange }
239+ />
240+ ) ;
241+ expect ( onInputChange ) . not . toHaveBeenCalled ( ) ;
242+ await openMenu ( ) ;
243+ await userEvent . click ( screen . getByRole ( 'option' , { name : 'Item 0' } ) ) ;
244+ expect ( onInputChange ) . not . toHaveBeenCalled ( ) ;
245+ } ) ;
192246
193- expect ( onInputChange ) . toHaveBeenCalledWith ( 'something' ) ;
247+ it ( 'should call onInputChange when custom value is entered into combobox' , async ( ) => {
248+ render (
249+ < ComboBox
250+ { ...mockProps }
251+ initialSelectedItem = { mockProps . items [ 0 ] }
252+ onInputChange = { onInputChange }
253+ />
254+ ) ;
255+ await userEvent . clear ( findInputNode ( ) ) ;
256+ expect ( onInputChange ) . toHaveBeenCalledWith ( '' ) ;
257+ await userEvent . type ( findInputNode ( ) , 'custom value' ) ;
258+ await userEvent . keyboard ( '[Enter]' ) ;
259+ expect ( onInputChange ) . toHaveBeenCalledWith ( 'custom value' ) ;
260+ } ) ;
194261 } ) ;
195262
196263 it ( 'should render custom item components' , async ( ) => {
@@ -372,6 +439,35 @@ describe('ComboBox', () => {
372439 await waitForPosition ( ) ;
373440 expect ( findInputNode ( ) ) . toHaveDisplayValue ( mockProps . items [ 0 ] . label ) ;
374441 } ) ;
442+ it ( 'should not call onChange or onInputChange on initial render' , ( ) => {
443+ render ( < ControlledComboBox /> ) ;
444+ expect ( screen . getByText ( 'onChangeCallCount: 0' ) ) . toBeInTheDocument ( ) ;
445+ expect ( screen . getByText ( 'onInputChangeCallCount: 0' ) ) . toBeInTheDocument ( ) ;
446+ } ) ;
447+ it ( 'should call onInputChange when input changes' , async ( ) => {
448+ render ( < ControlledComboBox /> ) ;
449+ await userEvent . type ( findInputNode ( ) , 'Item 2' ) ;
450+ expect ( screen . getByText ( 'onInputChangeCallCount: 6' ) ) . toBeInTheDocument ( ) ;
451+ } ) ;
452+ it ( 'should call onInputChange when external state managing selectedItem is updated' , async ( ) => {
453+ render ( < ControlledComboBox /> ) ;
454+ await userEvent . click (
455+ screen . getByRole ( 'button' , { name : 'Choose item 3' } )
456+ ) ;
457+ expect ( screen . getByText ( 'onInputChangeCallCount: 1' ) ) . toBeInTheDocument ( ) ;
458+ } ) ;
459+ it ( 'should not call onChange or onInputChange when external state managing selectedItem is updated to same value' , async ( ) => {
460+ render (
461+ < ControlledComboBox
462+ controlledItem = { { id : 'id-3' , label : 'Item 3' , value : 3 } }
463+ />
464+ ) ;
465+ await userEvent . click (
466+ screen . getByRole ( 'button' , { name : 'Choose item 3' } )
467+ ) ;
468+ expect ( screen . getByText ( 'onChangeCallCount: 0' ) ) . toBeInTheDocument ( ) ;
469+ expect ( screen . getByText ( 'onInputChangeCallCount: 0' ) ) . toBeInTheDocument ( ) ;
470+ } ) ;
375471
376472 it ( 'should display selected item using a string type for the `selectedItem` prop' , async ( ) => {
377473 // Replace the 'items' property in mockProps with a list of strings
0 commit comments