@@ -114,7 +114,7 @@ describe('selection', () => {
114114
115115 input . focus ( ) ;
116116 input . setSelectionRange ( selectionStart , selectionEnd , selectionDirection ) ;
117- user . type ( input , '{Shift}' , {
117+ await user . type ( input , '{Shift}' , {
118118 skipClick : true ,
119119 initialSelectionStart : selectionStart ,
120120 initialSelectionEnd : selectionEnd ,
@@ -150,14 +150,14 @@ describe('selection', () => {
150150} ) ;
151151
152152describe ( 'select and type' , ( ) => {
153- async function testSelectAndType (
153+ async function selectAndType (
154154 user : ReturnType < typeof userEvent . setup > ,
155155 cursorPosition : number ,
156- str : string ,
157- expectedResult : string
156+ str : string
158157 ) {
159158 const elementRef = React . createRef < TimeInputElement > ( ) ;
160- const { unmount } = makeTimeInput ( { ref : elementRef } ) ;
159+ const onChange = jest . fn ( ) ;
160+ const { unmount } = makeTimeInput ( { ref : elementRef , onChange } ) ;
161161 const input : HTMLInputElement = screen . getByRole ( 'textbox' ) ;
162162
163163 input . focus ( ) ;
@@ -169,10 +169,36 @@ describe('select and type', () => {
169169 initialSelectionEnd : cursorPosition ,
170170 } ) ;
171171 await user . keyboard ( str ) ;
172+ return { input, onChange, unmount } ;
173+ }
174+ // Test internal/displayed value, but not the onChange callback
175+ async function testSelectAndType (
176+ user : ReturnType < typeof userEvent . setup > ,
177+ cursorPosition : number ,
178+ str : string ,
179+ expectedResult : string
180+ ) {
181+ const { input, unmount } = await selectAndType ( user , cursorPosition , str ) ;
172182
173183 expect ( input . value ) . toEqual ( expectedResult ) ;
174184 unmount ( ) ;
175185 }
186+ // Test the value in onChange callback
187+ async function testSelectAndTypeOnChange (
188+ user : ReturnType < typeof userEvent . setup > ,
189+ cursorPosition : number ,
190+ str : string ,
191+ expectedResult : string
192+ ) {
193+ const { onChange, unmount } = await selectAndType (
194+ user ,
195+ cursorPosition ,
196+ str
197+ ) ;
198+ expect ( onChange ) . lastCalledWith ( TimeUtils . parseTime ( expectedResult ) ) ;
199+ unmount ( ) ;
200+ }
201+
176202 it ( 'handles typing after autoselecting a segment' , async ( ) => {
177203 const user = userEvent . setup ( ) ;
178204 await testSelectAndType ( user , 0 , '0' , '02:34:56' ) ;
@@ -247,6 +273,58 @@ describe('select and type', () => {
247273 unmount ( ) ;
248274 } ) ;
249275
276+ it ( 'fills in missing chars and triggers onChange' , async ( ) => {
277+ const user = userEvent . setup ( ) ;
278+ await testSelectAndTypeOnChange ( user , 1 , '{backspace}' , '00:34:56' ) ;
279+ await testSelectAndTypeOnChange ( user , 3 , '{backspace}' , '12:00:56' ) ;
280+ await testSelectAndTypeOnChange ( user , 6 , '{backspace}' , '12:34:00' ) ;
281+ await testSelectAndTypeOnChange (
282+ user ,
283+ 8 ,
284+ // First backspace clears the whole section
285+ '{backspace}{backspace}{backspace}{backspace}' ,
286+ '10:00:00'
287+ ) ;
288+ } ) ;
289+
290+ it ( 'updates the displayed value on blur' , async ( ) => {
291+ const user = userEvent . setup ( ) ;
292+ const onChange = jest . fn ( ) ;
293+ const { unmount } = makeTimeInput ( { onChange } ) ;
294+ const input : HTMLInputElement = screen . getByRole ( 'textbox' ) ;
295+ input . focus ( ) ;
296+ await user . type ( input , '{shift}{backspace}{backspace}' , {
297+ initialSelectionStart : 6 ,
298+ initialSelectionEnd : 6 ,
299+ } ) ;
300+ expect ( onChange ) . toBeCalledTimes ( 2 ) ;
301+ expect ( onChange ) . lastCalledWith ( TimeUtils . parseTime ( '12:30:00' ) ) ;
302+ expect ( input . value ) . toEqual ( '12:3' ) ;
303+
304+ input . blur ( ) ;
305+
306+ // Blur should update the internal value to match the last onChange
307+ // but not trigger another onChange
308+ expect ( onChange ) . toBeCalledTimes ( 2 ) ;
309+
310+ expect ( input . value ) . toEqual ( '12:30:00' ) ;
311+
312+ // Fill in missing chars in the middle
313+ input . focus ( ) ;
314+ await user . type ( input , '{shift}{backspace}' , {
315+ skipClick : true ,
316+ initialSelectionStart : 3 ,
317+ initialSelectionEnd : 3 ,
318+ } ) ;
319+ expect ( input . value ) . toEqual (
320+ `12:${ FIXED_WIDTH_SPACE } ${ FIXED_WIDTH_SPACE } :00`
321+ ) ;
322+ input . blur ( ) ;
323+ expect ( input . value ) . toEqual ( '12:00:00' ) ;
324+
325+ unmount ( ) ;
326+ } ) ;
327+
250328 it ( 'existing edge cases' , async ( ) => {
251329 const user = userEvent . setup ( ) ;
252330 // Ideally it should change the first section to 20, i.e. '20:34:56'
@@ -440,3 +518,37 @@ it('updates properly when the value prop is updated', () => {
440518
441519 expect ( textbox . value ) . toEqual ( '00:00:00' ) ;
442520} ) ;
521+
522+ it ( 'ignores value prop changes matching displayed value' , async ( ) => {
523+ const user = userEvent . setup ( ) ;
524+ const onChange = jest . fn ( ) ;
525+ const { rerender } = makeTimeInput ( { value : 1 , onChange } ) ;
526+
527+ const textbox : HTMLInputElement = screen . getByRole ( 'textbox' ) ;
528+ expect ( textbox . value ) . toEqual ( '00:00:01' ) ;
529+
530+ textbox . focus ( ) ;
531+ await user . type ( textbox , '{backspace}' , {
532+ skipClick : true ,
533+ initialSelectionStart : 8 ,
534+ initialSelectionEnd : 8 ,
535+ } ) ;
536+
537+ expect ( textbox . value ) . toEqual ( '00:00:0' ) ;
538+ expect ( onChange ) . toBeCalledWith ( 0 ) ;
539+
540+ // Ignore prop update matching internal state
541+ rerender ( < TimeInput value = { 0 } onChange = { onChange } /> ) ;
542+ expect ( textbox . value ) . toEqual ( '00:00:0' ) ;
543+ expect ( onChange ) . toBeCalledTimes ( 1 ) ;
544+
545+ // Update internal value
546+ rerender ( < TimeInput value = { 1 } onChange = { onChange } /> ) ;
547+ expect ( textbox . value ) . toEqual ( '00:00:01' ) ;
548+ expect ( onChange ) . toBeCalledTimes ( 1 ) ;
549+
550+ // Update internal value
551+ rerender ( < TimeInput value = { 0 } onChange = { onChange } /> ) ;
552+ expect ( textbox . value ) . toEqual ( '00:00:00' ) ;
553+ expect ( onChange ) . toBeCalledTimes ( 1 ) ;
554+ } ) ;
0 commit comments