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