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  });