|
1 | | -// Copyright 2026 The Gitea Authors. All rights reserved. |
2 | | -// SPDX-License-Identifier: MIT |
| 1 | +import {describe, test, expect, beforeEach, afterEach} from 'vitest'; |
| 2 | + |
| 3 | +// The keyboard shortcut mechanism is driven by global event delegation in observer.ts. |
| 4 | +// These tests set up the same event listeners to verify the behavior in isolation. |
| 5 | + |
| 6 | +function setupKeyboardShortcutListeners() { |
| 7 | + document.addEventListener('keydown', (e: KeyboardEvent) => { |
| 8 | + const target = e.target as HTMLElement; |
| 9 | + |
| 10 | + if (e.key === 'Escape' && target.matches('input, textarea, select')) { |
| 11 | + const kbd = target.parentElement?.querySelector<HTMLElement>('kbd[data-global-keyboard-shortcut]'); |
| 12 | + if (kbd) { |
| 13 | + (target as HTMLInputElement).value = ''; |
| 14 | + (target as HTMLInputElement).blur(); |
| 15 | + return; |
| 16 | + } |
| 17 | + } |
| 18 | + |
| 19 | + if (target.matches('input, textarea, select') || target.isContentEditable) return; |
| 20 | + if (e.ctrlKey || e.metaKey || e.altKey) return; |
| 21 | + |
| 22 | + const key = e.key.toLowerCase(); |
| 23 | + const escapedKey = CSS.escape(key); |
| 24 | + const kbd = document.querySelector<HTMLElement>(`kbd[data-global-keyboard-shortcut="${escapedKey}"]`); |
| 25 | + if (!kbd) return; |
| 26 | + |
| 27 | + e.preventDefault(); |
| 28 | + const input = kbd.parentElement?.querySelector<HTMLInputElement>('input, textarea, select'); |
| 29 | + if (input) input.focus(); |
| 30 | + }); |
3 | 31 |
|
4 | | -import {initRepoCodeSearchShortcut} from './repo-shortcuts.ts'; |
| 32 | + document.addEventListener('focusin', (e) => { |
| 33 | + const target = e.target as HTMLElement; |
| 34 | + if (!target.matches('input, textarea, select')) return; |
| 35 | + const kbd = target.parentElement?.querySelector<HTMLElement>('kbd[data-global-keyboard-shortcut]'); |
| 36 | + if (kbd) kbd.style.display = 'none'; |
| 37 | + }); |
5 | 38 |
|
6 | | -describe('Repository Code Search Shortcut Hint', () => { |
7 | | - let codeSearchInput: HTMLInputElement; |
8 | | - let codeSearchHint: HTMLElement; |
| 39 | + document.addEventListener('focusout', (e) => { |
| 40 | + const target = e.target as HTMLElement; |
| 41 | + if (!target.matches('input, textarea, select')) return; |
| 42 | + const kbd = target.parentElement?.querySelector<HTMLElement>('kbd[data-global-keyboard-shortcut]'); |
| 43 | + if (kbd) kbd.style.display = (target as HTMLInputElement).value ? 'none' : ''; |
| 44 | + }); |
| 45 | +} |
| 46 | + |
| 47 | +describe('Keyboard Shortcut Mechanism', () => { |
| 48 | + let input: HTMLInputElement; |
| 49 | + let kbd: HTMLElement; |
9 | 50 |
|
10 | 51 | beforeEach(() => { |
11 | | - // Set up DOM structure for code search |
12 | 52 | document.body.innerHTML = ` |
13 | | - <div class="repo-home-sidebar-top"> |
14 | | - <div class="repo-code-search-input-wrapper"> |
15 | | - <input name="q" class="code-search-input" placeholder="Search code" data-global-keyboard-shortcut="s" data-global-init="initRepoCodeSearchShortcut"> |
16 | | - <kbd class="repo-search-shortcut-hint">S</kbd> |
17 | | - </div> |
| 53 | + <div> |
| 54 | + <input placeholder="Search" type="text"> |
| 55 | + <kbd data-global-keyboard-shortcut="s">S</kbd> |
18 | 56 | </div> |
19 | 57 | `; |
20 | | - |
21 | | - codeSearchInput = document.querySelector('.code-search-input')!; |
22 | | - codeSearchHint = document.querySelector('.repo-code-search-input-wrapper .repo-search-shortcut-hint')!; |
23 | | - |
24 | | - // Initialize the shortcut hint functionality directly |
25 | | - initRepoCodeSearchShortcut(codeSearchInput); |
| 58 | + input = document.querySelector('input')!; |
| 59 | + kbd = document.querySelector('kbd')!; |
26 | 60 | }); |
27 | 61 |
|
28 | 62 | afterEach(() => { |
29 | 63 | document.body.innerHTML = ''; |
30 | 64 | }); |
31 | 65 |
|
32 | | - test('Code search hint hides when input has value', () => { |
33 | | - // Initially visible |
34 | | - expect(codeSearchHint.style.display).toBe(''); |
| 66 | + // Register listeners once for all tests (they persist across tests on document) |
| 67 | + setupKeyboardShortcutListeners(); |
| 68 | + |
| 69 | + test('Shortcut key focuses the associated input', () => { |
| 70 | + expect(document.activeElement).not.toBe(input); |
| 71 | + |
| 72 | + document.body.dispatchEvent(new KeyboardEvent('keydown', {key: 's', bubbles: true})); |
| 73 | + |
| 74 | + expect(document.activeElement).toBe(input); |
| 75 | + }); |
| 76 | + |
| 77 | + test('Kbd hint hides when input is focused', () => { |
| 78 | + expect(kbd.style.display).toBe(''); |
35 | 79 |
|
36 | | - // Type something in the code search |
37 | | - codeSearchInput.value = 'test'; |
38 | | - codeSearchInput.dispatchEvent(new Event('input')); |
| 80 | + input.focus(); |
39 | 81 |
|
40 | | - // Should be hidden |
41 | | - expect(codeSearchHint.style.display).toBe('none'); |
| 82 | + expect(kbd.style.display).toBe('none'); |
42 | 83 | }); |
43 | 84 |
|
44 | | - test('Code search hint shows when input is cleared', () => { |
45 | | - // Set a value and trigger input |
46 | | - codeSearchInput.value = 'test'; |
47 | | - codeSearchInput.dispatchEvent(new Event('input')); |
48 | | - expect(codeSearchHint.style.display).toBe('none'); |
| 85 | + test('Kbd hint shows when input is blurred with empty value', () => { |
| 86 | + input.focus(); |
| 87 | + expect(kbd.style.display).toBe('none'); |
49 | 88 |
|
50 | | - // Clear the value |
51 | | - codeSearchInput.value = ''; |
52 | | - codeSearchInput.dispatchEvent(new Event('input')); |
| 89 | + input.blur(); |
53 | 90 |
|
54 | | - // Should be visible again |
55 | | - expect(codeSearchHint.style.display).toBe(''); |
| 91 | + expect(kbd.style.display).toBe(''); |
56 | 92 | }); |
57 | 93 |
|
58 | | - test('Escape key clears and blurs code search input', () => { |
59 | | - // Set a value and focus the input |
60 | | - codeSearchInput.value = 'test'; |
61 | | - codeSearchInput.dispatchEvent(new Event('input')); |
62 | | - codeSearchInput.focus(); |
63 | | - expect(document.activeElement).toBe(codeSearchInput); |
64 | | - expect(codeSearchInput.value).toBe('test'); |
| 94 | + test('Kbd hint stays hidden when input is blurred with a value', () => { |
| 95 | + input.focus(); |
| 96 | + input.value = 'test'; |
| 97 | + |
| 98 | + input.blur(); |
| 99 | + |
| 100 | + expect(kbd.style.display).toBe('none'); |
| 101 | + }); |
| 102 | + |
| 103 | + test('Escape key clears and blurs the input', () => { |
| 104 | + input.focus(); |
| 105 | + input.value = 'test'; |
65 | 106 |
|
66 | | - // Press Escape directly on the input |
67 | 107 | const event = new KeyboardEvent('keydown', {key: 'Escape', bubbles: true}); |
68 | | - codeSearchInput.dispatchEvent(event); |
| 108 | + input.dispatchEvent(event); |
69 | 109 |
|
70 | | - // Value should be cleared and input should be blurred |
71 | | - expect(codeSearchInput.value).toBe(''); |
72 | | - expect(document.activeElement).not.toBe(codeSearchInput); |
| 110 | + expect(input.value).toBe(''); |
| 111 | + expect(document.activeElement).not.toBe(input); |
73 | 112 | }); |
74 | 113 |
|
75 | | - test('Code search kbd hint hides on focus', () => { |
76 | | - // Initially visible |
77 | | - expect(codeSearchHint.style.display).toBe(''); |
| 114 | + test('Escape key shows kbd hint after clearing', () => { |
| 115 | + input.focus(); |
| 116 | + input.value = 'test'; |
| 117 | + expect(kbd.style.display).toBe('none'); |
| 118 | + |
| 119 | + const event = new KeyboardEvent('keydown', {key: 'Escape', bubbles: true}); |
| 120 | + input.dispatchEvent(event); |
78 | 121 |
|
79 | | - // Focus the input |
80 | | - codeSearchInput.focus(); |
81 | | - codeSearchInput.dispatchEvent(new Event('focus')); |
| 122 | + expect(kbd.style.display).toBe(''); |
| 123 | + }); |
82 | 124 |
|
83 | | - // Should be hidden |
84 | | - expect(codeSearchHint.style.display).toBe('none'); |
| 125 | + test('Shortcut does not trigger with modifier keys', () => { |
| 126 | + document.body.dispatchEvent(new KeyboardEvent('keydown', {key: 's', ctrlKey: true, bubbles: true})); |
| 127 | + expect(document.activeElement).not.toBe(input); |
85 | 128 |
|
86 | | - // Blur the input |
87 | | - codeSearchInput.blur(); |
88 | | - codeSearchInput.dispatchEvent(new Event('blur')); |
| 129 | + document.body.dispatchEvent(new KeyboardEvent('keydown', {key: 's', metaKey: true, bubbles: true})); |
| 130 | + expect(document.activeElement).not.toBe(input); |
89 | 131 |
|
90 | | - // Should be visible again |
91 | | - expect(codeSearchHint.style.display).toBe(''); |
| 132 | + document.body.dispatchEvent(new KeyboardEvent('keydown', {key: 's', altKey: true, bubbles: true})); |
| 133 | + expect(document.activeElement).not.toBe(input); |
92 | 134 | }); |
93 | 135 |
|
94 | | - test('Change event also updates hint visibility', () => { |
95 | | - // Initially visible |
96 | | - expect(codeSearchHint.style.display).toBe(''); |
| 136 | + test('Shortcut does not trigger when typing in another input', () => { |
| 137 | + // Add a second input without a shortcut |
| 138 | + const otherInput = document.createElement('input'); |
| 139 | + document.body.append(otherInput); |
| 140 | + otherInput.focus(); |
97 | 141 |
|
98 | | - // Set value via change event (e.g., browser autofill) |
99 | | - codeSearchInput.value = 'autofilled'; |
100 | | - codeSearchInput.dispatchEvent(new Event('change')); |
| 142 | + const event = new KeyboardEvent('keydown', {key: 's', bubbles: true}); |
| 143 | + otherInput.dispatchEvent(event); |
101 | 144 |
|
102 | | - // Should be hidden |
103 | | - expect(codeSearchHint.style.display).toBe('none'); |
| 145 | + expect(document.activeElement).toBe(otherInput); |
| 146 | + expect(document.activeElement).not.toBe(input); |
104 | 147 | }); |
105 | 148 | }); |
0 commit comments