go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/ui/src/generic_libs/components/hotkey/hotkey.test.tsx (about)

     1  // Copyright 2023 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 { render, screen } from '@testing-library/react';
    16  import { HotkeysEvent } from 'hotkeys-js';
    17  import { html, LitElement } from 'lit';
    18  import { customElement } from 'lit/decorators.js';
    19  
    20  import './hotkey';
    21  import { Hotkey } from './hotkey';
    22  
    23  function simulateKeyStroke(target: EventTarget, key: string) {
    24    target.dispatchEvent(
    25      new KeyboardEvent('keydown', {
    26        bubbles: true,
    27        composed: true,
    28        keyCode: key.toUpperCase().charCodeAt(0),
    29      } as KeyboardEventInit),
    30    );
    31    target.dispatchEvent(
    32      new KeyboardEvent('keyup', {
    33        bubbles: true,
    34        composed: true,
    35        keyCode: key.toUpperCase().charCodeAt(0),
    36      } as KeyboardEventInit),
    37    );
    38  }
    39  
    40  @customElement('milo-hotkey-test-wrapper')
    41  class WrapperElement extends LitElement {
    42    protected render() {
    43      return html`
    44        <input id="input">
    45        <select id="select"></select>
    46        <textarea id="textarea">
    47      `;
    48    }
    49  }
    50  WrapperElement; // 'Use' the class to silence eslint/tsc warnings.
    51  
    52  declare global {
    53    // eslint-disable-next-line @typescript-eslint/no-namespace
    54    namespace JSX {
    55      interface IntrinsicElements {
    56        ['milo-hotkey-test-wrapper']: { ['data-testid']: string };
    57      }
    58    }
    59  }
    60  
    61  describe('Hotkey', () => {
    62    let handlerSpy: jest.MockedFunction<
    63      (keyboardEvent: KeyboardEvent, hotkeysEvent: HotkeysEvent) => void
    64    >;
    65    let handlerSpy2: jest.MockedFunction<
    66      (keyboardEvent: KeyboardEvent, hotkeysEvent: HotkeysEvent) => void
    67    >;
    68    let ele: ReturnType<typeof render>;
    69    let childEle: Element;
    70    let inputEle: HTMLInputElement;
    71    let selectEle: HTMLSelectElement;
    72    let textareaEle: HTMLTextAreaElement;
    73    let wrappedInputEle: HTMLInputElement;
    74    let wrappedSelectEle: HTMLSelectElement;
    75    let wrappedTextareaEle: HTMLTextAreaElement;
    76  
    77    beforeEach(async () => {
    78      jest.useFakeTimers();
    79      handlerSpy = jest.fn(
    80        (_keyboardEvent: KeyboardEvent, _hotkeysEvent: HotkeysEvent) => {},
    81      );
    82      handlerSpy2 = jest.fn(
    83        (_keyboardEvent: KeyboardEvent, _hotkeysEvent: HotkeysEvent) => {},
    84      );
    85  
    86      ele = render(
    87        <Hotkey hotkey="a" handler={handlerSpy}>
    88          <div data-testid="child"></div>
    89          <input data-testid="input" />
    90          <select data-testid="select"></select>
    91          <textarea data-testid="textarea"></textarea>
    92          <milo-hotkey-test-wrapper data-testid="wrapped"></milo-hotkey-test-wrapper>
    93        </Hotkey>,
    94      );
    95      await jest.runAllTimersAsync();
    96  
    97      childEle = screen.getByTestId('child');
    98      inputEle = screen.getByTestId('input');
    99      selectEle = screen.getByTestId('select');
   100      textareaEle = screen.getByTestId('textarea');
   101  
   102      const wrapped = screen.getByTestId('wrapped');
   103      wrappedInputEle = wrapped.shadowRoot!.querySelector('#input')!;
   104      wrappedSelectEle = wrapped.shadowRoot!.querySelector('#select')!;
   105      wrappedTextareaEle = wrapped.shadowRoot!.querySelector('#textarea')!;
   106    });
   107  
   108    afterEach(() => {
   109      jest.useRealTimers();
   110    });
   111  
   112    test('should react to key press on the child element', () => {
   113      simulateKeyStroke(childEle, 'a');
   114      expect(handlerSpy.mock.calls.length).toStrictEqual(1);
   115    });
   116  
   117    test('should react to key press outside of the child element', () => {
   118      simulateKeyStroke(document, 'a');
   119      expect(handlerSpy.mock.calls.length).toStrictEqual(1);
   120    });
   121  
   122    test('should react to the new key press event when the key is updated', async () => {
   123      ele.rerender(
   124        <Hotkey hotkey="b" handler={handlerSpy}>
   125          <div data-testid="child"></div>
   126          <input data-testid="input" />
   127          <select data-testid="select"></select>
   128          <textarea data-testid="textarea"></textarea>
   129          <milo-hotkey-test-wrapper data-testid="wrapped"></milo-hotkey-test-wrapper>
   130        </Hotkey>,
   131      );
   132      await jest.runAllTimersAsync();
   133  
   134      simulateKeyStroke(document, 'a');
   135      expect(handlerSpy.mock.calls.length).toStrictEqual(0);
   136  
   137      simulateKeyStroke(document, 'b');
   138      expect(handlerSpy.mock.calls.length).toStrictEqual(1);
   139    });
   140  
   141    test('should trigger the new handler when the handler is updated', async () => {
   142      ele.rerender(
   143        <Hotkey hotkey="b" handler={handlerSpy2}>
   144          <div data-testid="child"></div>
   145          <input data-testid="input" />
   146          <select data-testid="select"></select>
   147          <textarea data-testid="textarea"></textarea>
   148          <milo-hotkey-test-wrapper data-testid="wrapped"></milo-hotkey-test-wrapper>
   149        </Hotkey>,
   150      );
   151      await jest.runAllTimersAsync();
   152  
   153      simulateKeyStroke(document, 'b');
   154      expect(handlerSpy.mock.calls.length).toStrictEqual(0);
   155      expect(handlerSpy2.mock.calls.length).toStrictEqual(1);
   156    });
   157  
   158    test('should not trigger the handler when the target element is INPUT/SELECT/TEXTAREA', () => {
   159      simulateKeyStroke(inputEle, 'b');
   160      simulateKeyStroke(selectEle, 'b');
   161      simulateKeyStroke(textareaEle, 'b');
   162      expect(handlerSpy2.mock.calls.length).toStrictEqual(0);
   163    });
   164  
   165    test('should not trigger the handler when the target element is INPUT/SELECT/TEXTAREA in a web component', () => {
   166      simulateKeyStroke(wrappedInputEle, 'b');
   167      simulateKeyStroke(wrappedSelectEle, 'b');
   168      simulateKeyStroke(wrappedTextareaEle, 'b');
   169      expect(handlerSpy2.mock.calls.length).toStrictEqual(0);
   170    });
   171  
   172    test('should not trigger the handler when the component is disconnected', async () => {
   173      ele.unmount();
   174      await jest.runAllTimersAsync();
   175  
   176      simulateKeyStroke(document, 'b');
   177      expect(handlerSpy2.mock.calls.length).toStrictEqual(0);
   178    });
   179  });