go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/common/components/search_input/search_input.test.tsx (about) 1 // Copyright 2024 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 import { 16 act, 17 cleanup, 18 fireEvent, 19 render, 20 screen, 21 } from '@testing-library/react'; 22 23 import { SearchInput } from './search_input'; 24 25 describe('<SearchInput />', () => { 26 beforeEach(() => { 27 jest.useFakeTimers(); 28 }); 29 30 afterEach(() => { 31 jest.useRealTimers(); 32 cleanup(); 33 }); 34 35 it('can delay update callback', async () => { 36 const onValueChangeSpy = jest.fn((_newValue: string) => {}); 37 render( 38 <SearchInput 39 value="val" 40 onValueChange={onValueChangeSpy} 41 initDelayMs={1000} 42 />, 43 ); 44 45 const searchInputEle = screen.getByTestId('search-input'); 46 expect(searchInputEle).toHaveValue('val'); 47 48 // Input search query. 49 fireEvent.change(searchInputEle, { target: { value: 'new-val' } }); 50 expect(searchInputEle).toHaveValue('new-val'); 51 expect(onValueChangeSpy).not.toHaveBeenCalled(); 52 53 // Waiting for the search query to commit. 54 await act(() => jest.advanceTimersByTimeAsync(500)); 55 expect(onValueChangeSpy).not.toHaveBeenCalled(); 56 57 // The search query is committed. 58 await act(() => jest.advanceTimersByTimeAsync(1000)); 59 expect(searchInputEle).toHaveValue('new-val'); 60 expect(onValueChangeSpy).toHaveBeenCalledTimes(1); 61 expect(onValueChangeSpy).toHaveBeenNthCalledWith(1, 'new-val'); 62 63 // No further updates. 64 await act(() => jest.runAllTimersAsync()); 65 expect(onValueChangeSpy).toHaveBeenCalledTimes(1); 66 }); 67 68 it('can cancel update callback when value is updated during pending period', async () => { 69 const onValueChangeSpy = jest.fn((_newValue: string) => {}); 70 render( 71 <SearchInput 72 value="val" 73 onValueChange={onValueChangeSpy} 74 initDelayMs={1000} 75 />, 76 ); 77 78 const searchInputEle = screen.getByTestId('search-input'); 79 expect(searchInputEle).toHaveValue('val'); 80 81 // Input search query. 82 fireEvent.change(searchInputEle, { target: { value: 'new-val' } }); 83 expect(searchInputEle).toHaveValue('new-val'); 84 expect(onValueChangeSpy).not.toHaveBeenCalled(); 85 86 // Waiting for the search query to commit. 87 await act(() => jest.advanceTimersByTimeAsync(500)); 88 expect(onValueChangeSpy).not.toHaveBeenCalled(); 89 90 // Search query is updated again while pending. 91 fireEvent.change(searchInputEle, { target: { value: 'newer-val' } }); 92 expect(searchInputEle).toHaveValue('newer-val'); 93 expect(onValueChangeSpy).not.toHaveBeenCalled(); 94 95 // Waiting for the new search query to commit. 96 await act(() => jest.advanceTimersByTimeAsync(500)); 97 expect(onValueChangeSpy).not.toHaveBeenCalled(); 98 99 // The search query is committed. 100 await act(() => jest.advanceTimersByTimeAsync(1000)); 101 expect(searchInputEle).toHaveValue('newer-val'); 102 expect(onValueChangeSpy).toHaveBeenCalledTimes(1); 103 expect(onValueChangeSpy).toHaveBeenNthCalledWith(1, 'newer-val'); 104 105 // No further updates. 106 await act(() => jest.runAllTimersAsync()); 107 expect(onValueChangeSpy).toHaveBeenCalledTimes(1); 108 }); 109 110 it('can override pending value when parent set an update', async () => { 111 const onValueChangeSpy = jest.fn((_newValue: string) => {}); 112 const { rerender } = render( 113 <SearchInput 114 value="val" 115 onValueChange={onValueChangeSpy} 116 initDelayMs={1000} 117 />, 118 ); 119 120 const searchInputEle = screen.getByTestId('search-input'); 121 expect(searchInputEle).toHaveValue('val'); 122 123 // Input search query. 124 fireEvent.change(searchInputEle, { target: { value: 'new-val' } }); 125 expect(searchInputEle).toHaveValue('new-val'); 126 expect(onValueChangeSpy).not.toHaveBeenCalled(); 127 128 // Waiting for the search query to commit. 129 await act(() => jest.advanceTimersByTimeAsync(500)); 130 expect(onValueChangeSpy).not.toHaveBeenCalled(); 131 132 // Parent sets a new value while pending. 133 rerender( 134 <SearchInput 135 value="new-parent-val" 136 onValueChange={onValueChangeSpy} 137 initDelayMs={1000} 138 />, 139 ); 140 expect(searchInputEle).toHaveValue('new-parent-val'); 141 expect(onValueChangeSpy).not.toHaveBeenCalled(); 142 143 // No further updates. 144 await act(() => jest.runAllTimersAsync()); 145 expect(onValueChangeSpy).not.toHaveBeenCalled(); 146 }); 147 148 it('do not override pending value when parent set an update due to onValueChange', async () => { 149 const onValueChangeSpy = jest.fn((_newValue: string) => {}); 150 const { rerender } = render( 151 <SearchInput 152 value="val" 153 onValueChange={onValueChangeSpy} 154 initDelayMs={1000} 155 />, 156 ); 157 158 const searchInputEle = screen.getByTestId('search-input'); 159 expect(searchInputEle).toHaveValue('val'); 160 161 // Input search query. 162 fireEvent.change(searchInputEle, { target: { value: 'new-val' } }); 163 expect(searchInputEle).toHaveValue('new-val'); 164 expect(onValueChangeSpy).not.toHaveBeenCalled(); 165 166 // Waiting for the search query to commit. 167 await act(() => jest.advanceTimersByTimeAsync(1000)); 168 expect(onValueChangeSpy).toHaveBeenCalledTimes(1); 169 170 // Edit the value again before parent's state update got applied. 171 fireEvent.change(searchInputEle, { target: { value: 'new-val-2' } }); 172 173 // Parent updates the value to match the new value set by `onValueChange` 174 rerender( 175 <SearchInput 176 value="new-val" 177 onValueChange={onValueChangeSpy} 178 initDelayMs={1000} 179 />, 180 ); 181 expect(searchInputEle).toHaveValue('new-val-2'); 182 expect(onValueChangeSpy).toHaveBeenCalledTimes(1); 183 184 // After 1s, the 2nd edit is committed. 185 await act(() => jest.advanceTimersByTimeAsync(1000)); 186 expect(searchInputEle).toHaveValue('new-val-2'); 187 expect(onValueChangeSpy).toHaveBeenCalledTimes(2); 188 expect(onValueChangeSpy).toHaveBeenNthCalledWith(2, 'new-val-2'); 189 }); 190 191 it('can focus with shortcut', async () => { 192 const onValueChangeSpy = jest.fn((_newValue: string) => {}); 193 render( 194 <SearchInput 195 value="" 196 focusShortcut="/" 197 onValueChange={onValueChangeSpy} 198 initDelayMs={1000} 199 />, 200 ); 201 const searchInputEle = screen.getByTestId('search-input'); 202 expect(searchInputEle).not.toHaveFocus(); 203 204 await act(() => jest.runAllTimersAsync()); 205 fireEvent.keyDown(window, { 206 key: '/', 207 code: 'Slash', 208 charCode: '/'.charCodeAt(0), 209 }); 210 await act(() => jest.runAllTimersAsync()); 211 await act(() => jest.runAllTimersAsync()); 212 213 expect(searchInputEle).toHaveFocus(); 214 expect(onValueChangeSpy).not.toHaveBeenCalled(); 215 }); 216 });