@mdxui/terminal 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +571 -0
- package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
- package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
- package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
- package/dist/chunk-3EFDH7PK.js +5235 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-3X5IR6WE.js +884 -0
- package/dist/chunk-4FV5ZDCE.js +5236 -0
- package/dist/chunk-4OVMSF2J.js +243 -0
- package/dist/chunk-63FEETIS.js +4048 -0
- package/dist/chunk-B43KP7XJ.js +884 -0
- package/dist/chunk-BMTJXWUV.js +655 -0
- package/dist/chunk-C3SVH4N7.js +882 -0
- package/dist/chunk-EVWR7Y47.js +874 -0
- package/dist/chunk-F6A5VWUC.js +1285 -0
- package/dist/chunk-FD7KW7GE.js +882 -0
- package/dist/chunk-GBQ6UD6I.js +655 -0
- package/dist/chunk-GMDD3M6U.js +5227 -0
- package/dist/chunk-JBHRXOXM.js +1058 -0
- package/dist/chunk-JFOO3EYO.js +1182 -0
- package/dist/chunk-JQ5H3WXL.js +1291 -0
- package/dist/chunk-JQD5NASE.js +234 -0
- package/dist/chunk-KRHJP5R7.js +592 -0
- package/dist/chunk-KWF6WVJE.js +962 -0
- package/dist/chunk-LHYQVN3H.js +1038 -0
- package/dist/chunk-M3TLQLGC.js +1032 -0
- package/dist/chunk-MVW4Q5OP.js +240 -0
- package/dist/chunk-NXCZSWLU.js +1294 -0
- package/dist/chunk-O25TNRO6.js +607 -0
- package/dist/chunk-PNECDA2I.js +884 -0
- package/dist/chunk-QIHWRLJR.js +962 -0
- package/dist/chunk-QW5YMQ7K.js +882 -0
- package/dist/chunk-R5U7XKVJ.js +16 -0
- package/dist/chunk-RP2MVQLR.js +962 -0
- package/dist/chunk-TP6RXGXA.js +1087 -0
- package/dist/chunk-TQQSTITZ.js +655 -0
- package/dist/chunk-X24GWXQV.js +1281 -0
- package/dist/components/index.d.ts +802 -0
- package/dist/components/index.js +149 -0
- package/dist/data/index.d.ts +2554 -0
- package/dist/data/index.js +51 -0
- package/dist/forms/index.d.ts +1596 -0
- package/dist/forms/index.js +464 -0
- package/dist/index-CQRFZntR.d.ts +867 -0
- package/dist/index.d.ts +579 -0
- package/dist/index.js +786 -0
- package/dist/interactive-D0JkWosD.d.ts +217 -0
- package/dist/keyboard/index.d.ts +2 -0
- package/dist/keyboard/index.js +43 -0
- package/dist/renderers/index.d.ts +546 -0
- package/dist/renderers/index.js +2157 -0
- package/dist/storybook/index.d.ts +396 -0
- package/dist/storybook/index.js +641 -0
- package/dist/theme/index.d.ts +1339 -0
- package/dist/theme/index.js +123 -0
- package/dist/types-Bxu5PAgA.d.ts +710 -0
- package/dist/types-CIlop5Ji.d.ts +701 -0
- package/dist/types-Ca8p_p5X.d.ts +710 -0
- package/package.json +90 -0
- package/src/__tests__/components/data/card.test.ts +458 -0
- package/src/__tests__/components/data/list.test.ts +473 -0
- package/src/__tests__/components/data/metrics.test.ts +541 -0
- package/src/__tests__/components/data/table.test.ts +448 -0
- package/src/__tests__/components/input/field.test.ts +555 -0
- package/src/__tests__/components/input/form.test.ts +870 -0
- package/src/__tests__/components/input/search.test.ts +1238 -0
- package/src/__tests__/components/input/select.test.ts +658 -0
- package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
- package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
- package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
- package/src/__tests__/components/navigation/tabs.test.ts +995 -0
- package/src/__tests__/components.test.tsx +1197 -0
- package/src/__tests__/core/compiler.test.ts +986 -0
- package/src/__tests__/core/parser.test.ts +785 -0
- package/src/__tests__/core/tier-switcher.test.ts +1103 -0
- package/src/__tests__/core/types.test.ts +1398 -0
- package/src/__tests__/data/collections.test.ts +1337 -0
- package/src/__tests__/data/db.test.ts +1265 -0
- package/src/__tests__/data/reactive.test.ts +1010 -0
- package/src/__tests__/data/sync.test.ts +1614 -0
- package/src/__tests__/errors.test.ts +660 -0
- package/src/__tests__/forms/integration.test.ts +444 -0
- package/src/__tests__/integration.test.ts +905 -0
- package/src/__tests__/keyboard.test.ts +1791 -0
- package/src/__tests__/renderer.test.ts +489 -0
- package/src/__tests__/renderers/ansi-css.test.ts +948 -0
- package/src/__tests__/renderers/ansi.test.ts +1366 -0
- package/src/__tests__/renderers/ascii.test.ts +1360 -0
- package/src/__tests__/renderers/interactive.test.ts +2353 -0
- package/src/__tests__/renderers/markdown.test.ts +1483 -0
- package/src/__tests__/renderers/text.test.ts +1369 -0
- package/src/__tests__/renderers/unicode.test.ts +1307 -0
- package/src/__tests__/theme.test.ts +639 -0
- package/src/__tests__/utils/assertions.ts +685 -0
- package/src/__tests__/utils/index.ts +115 -0
- package/src/__tests__/utils/test-renderer.ts +381 -0
- package/src/__tests__/utils/utils.test.ts +560 -0
- package/src/components/containers/card.ts +56 -0
- package/src/components/containers/dialog.ts +53 -0
- package/src/components/containers/index.ts +9 -0
- package/src/components/containers/panel.ts +59 -0
- package/src/components/feedback/badge.ts +40 -0
- package/src/components/feedback/index.ts +8 -0
- package/src/components/feedback/spinner.ts +23 -0
- package/src/components/helpers.ts +81 -0
- package/src/components/index.ts +153 -0
- package/src/components/layout/breadcrumb.ts +31 -0
- package/src/components/layout/index.ts +10 -0
- package/src/components/layout/list.ts +29 -0
- package/src/components/layout/sidebar.ts +79 -0
- package/src/components/layout/table.ts +62 -0
- package/src/components/primitives/box.ts +95 -0
- package/src/components/primitives/button.ts +54 -0
- package/src/components/primitives/index.ts +11 -0
- package/src/components/primitives/input.ts +88 -0
- package/src/components/primitives/select.ts +97 -0
- package/src/components/primitives/text.ts +60 -0
- package/src/components/render.ts +155 -0
- package/src/components/templates/app.ts +43 -0
- package/src/components/templates/index.ts +8 -0
- package/src/components/templates/site.ts +54 -0
- package/src/components/types.ts +777 -0
- package/src/core/compiler.ts +718 -0
- package/src/core/parser.ts +127 -0
- package/src/core/tier-switcher.ts +607 -0
- package/src/core/types.ts +672 -0
- package/src/data/collection.ts +316 -0
- package/src/data/collections.ts +50 -0
- package/src/data/context.tsx +174 -0
- package/src/data/db.ts +127 -0
- package/src/data/hooks.ts +532 -0
- package/src/data/index.ts +138 -0
- package/src/data/reactive.ts +1225 -0
- package/src/data/saas-collections.ts +375 -0
- package/src/data/sync.ts +1213 -0
- package/src/data/types.ts +660 -0
- package/src/forms/converters.ts +512 -0
- package/src/forms/index.ts +133 -0
- package/src/forms/schemas.ts +403 -0
- package/src/forms/types.ts +476 -0
- package/src/index.ts +542 -0
- package/src/keyboard/focus.ts +748 -0
- package/src/keyboard/index.ts +96 -0
- package/src/keyboard/integration.ts +371 -0
- package/src/keyboard/manager.ts +377 -0
- package/src/keyboard/presets.ts +90 -0
- package/src/renderers/ansi-css.ts +576 -0
- package/src/renderers/ansi.ts +802 -0
- package/src/renderers/ascii.ts +680 -0
- package/src/renderers/breadcrumb.ts +480 -0
- package/src/renderers/command-palette.ts +802 -0
- package/src/renderers/components/field.ts +210 -0
- package/src/renderers/components/form.ts +327 -0
- package/src/renderers/components/index.ts +21 -0
- package/src/renderers/components/search.ts +449 -0
- package/src/renderers/components/select.ts +222 -0
- package/src/renderers/index.ts +101 -0
- package/src/renderers/interactive/component-handlers.ts +622 -0
- package/src/renderers/interactive/cursor-manager.ts +147 -0
- package/src/renderers/interactive/focus-manager.ts +279 -0
- package/src/renderers/interactive/index.ts +661 -0
- package/src/renderers/interactive/input-handler.ts +164 -0
- package/src/renderers/interactive/keyboard-handler.ts +212 -0
- package/src/renderers/interactive/mouse-handler.ts +167 -0
- package/src/renderers/interactive/state-manager.ts +109 -0
- package/src/renderers/interactive/types.ts +338 -0
- package/src/renderers/interactive-string.ts +299 -0
- package/src/renderers/interactive.ts +59 -0
- package/src/renderers/markdown.ts +950 -0
- package/src/renderers/sidebar.ts +549 -0
- package/src/renderers/tabs.ts +682 -0
- package/src/renderers/text.ts +791 -0
- package/src/renderers/unicode.ts +917 -0
- package/src/renderers/utils.ts +942 -0
- package/src/router/adapters.ts +383 -0
- package/src/router/types.ts +140 -0
- package/src/router/utils.ts +452 -0
- package/src/schemas.ts +205 -0
- package/src/storybook/index.ts +91 -0
- package/src/storybook/interactive-decorator.tsx +659 -0
- package/src/storybook/keyboard-simulator.ts +501 -0
- package/src/theme/ansi-codes.ts +80 -0
- package/src/theme/box-drawing.ts +132 -0
- package/src/theme/color-convert.ts +254 -0
- package/src/theme/color-support.ts +321 -0
- package/src/theme/index.ts +134 -0
- package/src/theme/strip-ansi.ts +50 -0
- package/src/theme/tailwind-map.ts +469 -0
- package/src/theme/text-styles.ts +206 -0
- package/src/theme/theme-system.ts +568 -0
- package/src/types.ts +103 -0
|
@@ -0,0 +1,1791 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal Keyboard Navigation Tests
|
|
3
|
+
*
|
|
4
|
+
* TDD RED Phase: These tests define the contract for keyboard navigation
|
|
5
|
+
* in terminal UIs. All tests should FAIL initially because the APIs
|
|
6
|
+
* aren't implemented yet.
|
|
7
|
+
*
|
|
8
|
+
* Requirements tested:
|
|
9
|
+
* - j/k movement (Vim-style vertical navigation)
|
|
10
|
+
* - h/l movement (Vim-style horizontal navigation)
|
|
11
|
+
* - Enter/Space selection and activation
|
|
12
|
+
* - Tab/Shift+Tab focus cycling
|
|
13
|
+
* - Escape cancel/back navigation
|
|
14
|
+
* - Custom key bindings
|
|
15
|
+
* - Focus management (tracking, trapping)
|
|
16
|
+
*/
|
|
17
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
18
|
+
import React from 'react'
|
|
19
|
+
|
|
20
|
+
// Mock React for testing components without full React reconciler
|
|
21
|
+
vi.mock('react', async () => {
|
|
22
|
+
const actual = await vi.importActual('react')
|
|
23
|
+
return {
|
|
24
|
+
...actual,
|
|
25
|
+
createElement: vi.fn((type, props, ...children) => ({
|
|
26
|
+
type,
|
|
27
|
+
props: { ...props, children },
|
|
28
|
+
})),
|
|
29
|
+
useState: vi.fn((initial) => [initial, vi.fn()]),
|
|
30
|
+
useEffect: vi.fn(),
|
|
31
|
+
useCallback: vi.fn((fn) => fn),
|
|
32
|
+
useRef: vi.fn((initial) => ({ current: initial })),
|
|
33
|
+
useContext: vi.fn(),
|
|
34
|
+
createContext: vi.fn((defaultValue) => ({
|
|
35
|
+
Provider: vi.fn(),
|
|
36
|
+
Consumer: vi.fn(),
|
|
37
|
+
_currentValue: defaultValue,
|
|
38
|
+
})),
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// createKeyboardManager Tests
|
|
44
|
+
// ============================================================================
|
|
45
|
+
|
|
46
|
+
describe('createKeyboardManager', () => {
|
|
47
|
+
describe('basic creation', () => {
|
|
48
|
+
it('creates a keyboard manager with bindings', async () => {
|
|
49
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
50
|
+
|
|
51
|
+
const manager = createKeyboardManager({
|
|
52
|
+
bindings: {
|
|
53
|
+
j: 'move-down',
|
|
54
|
+
k: 'move-up',
|
|
55
|
+
enter: 'select',
|
|
56
|
+
escape: 'back',
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
expect(manager).toBeDefined()
|
|
61
|
+
expect(typeof manager.handleKey).toBe('function')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('returns actions for bound keys', async () => {
|
|
65
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
66
|
+
|
|
67
|
+
const manager = createKeyboardManager({
|
|
68
|
+
bindings: {
|
|
69
|
+
j: 'move-down',
|
|
70
|
+
k: 'move-up',
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
expect(manager.getAction('j')).toBe('move-down')
|
|
75
|
+
expect(manager.getAction('k')).toBe('move-up')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('returns undefined for unbound keys', async () => {
|
|
79
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
80
|
+
|
|
81
|
+
const manager = createKeyboardManager({
|
|
82
|
+
bindings: {
|
|
83
|
+
j: 'move-down',
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
expect(manager.getAction('x')).toBeUndefined()
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('modifier key support', () => {
|
|
92
|
+
it('handles ctrl modifier', async () => {
|
|
93
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
94
|
+
|
|
95
|
+
const manager = createKeyboardManager({
|
|
96
|
+
bindings: {
|
|
97
|
+
'ctrl+c': 'cancel',
|
|
98
|
+
'ctrl+s': 'save',
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
expect(manager.getAction('ctrl+c')).toBe('cancel')
|
|
103
|
+
expect(manager.getAction('ctrl+s')).toBe('save')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('handles alt modifier', async () => {
|
|
107
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
108
|
+
|
|
109
|
+
const manager = createKeyboardManager({
|
|
110
|
+
bindings: {
|
|
111
|
+
'alt+h': 'help',
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
expect(manager.getAction('alt+h')).toBe('help')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('handles shift modifier', async () => {
|
|
119
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
120
|
+
|
|
121
|
+
const manager = createKeyboardManager({
|
|
122
|
+
bindings: {
|
|
123
|
+
'shift+tab': 'focus-prev',
|
|
124
|
+
K: 'move-up-fast', // Capital K = shift+k
|
|
125
|
+
},
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
expect(manager.getAction('shift+tab')).toBe('focus-prev')
|
|
129
|
+
expect(manager.getAction('K')).toBe('move-up-fast')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('handles combined modifiers', async () => {
|
|
133
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
134
|
+
|
|
135
|
+
const manager = createKeyboardManager({
|
|
136
|
+
bindings: {
|
|
137
|
+
'ctrl+shift+s': 'save-as',
|
|
138
|
+
'ctrl+alt+delete': 'force-quit',
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
expect(manager.getAction('ctrl+shift+s')).toBe('save-as')
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
describe('vim-style bindings', () => {
|
|
147
|
+
it('supports standard vim movement (h/j/k/l)', async () => {
|
|
148
|
+
const { createKeyboardManager, VIM_BINDINGS } = await import('@mdxui/terminal')
|
|
149
|
+
|
|
150
|
+
const manager = createKeyboardManager({
|
|
151
|
+
bindings: VIM_BINDINGS,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
expect(manager.getAction('h')).toBe('move-left')
|
|
155
|
+
expect(manager.getAction('j')).toBe('move-down')
|
|
156
|
+
expect(manager.getAction('k')).toBe('move-up')
|
|
157
|
+
expect(manager.getAction('l')).toBe('move-right')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('supports vim navigation bindings', async () => {
|
|
161
|
+
const { createKeyboardManager, VIM_BINDINGS } = await import('@mdxui/terminal')
|
|
162
|
+
|
|
163
|
+
const manager = createKeyboardManager({
|
|
164
|
+
bindings: VIM_BINDINGS,
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
expect(manager.getAction('gg')).toBe('move-first')
|
|
168
|
+
expect(manager.getAction('G')).toBe('move-last')
|
|
169
|
+
expect(manager.getAction('/')).toBe('search')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('supports vim action bindings', async () => {
|
|
173
|
+
const { createKeyboardManager, VIM_BINDINGS } = await import('@mdxui/terminal')
|
|
174
|
+
|
|
175
|
+
const manager = createKeyboardManager({
|
|
176
|
+
bindings: VIM_BINDINGS,
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
expect(manager.getAction('enter')).toBe('select')
|
|
180
|
+
expect(manager.getAction('escape')).toBe('back')
|
|
181
|
+
expect(manager.getAction('q')).toBe('quit')
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
describe('handleKey callback', () => {
|
|
186
|
+
it('calls action handler when key matches', async () => {
|
|
187
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
188
|
+
const onAction = vi.fn()
|
|
189
|
+
|
|
190
|
+
const manager = createKeyboardManager({
|
|
191
|
+
bindings: { j: 'move-down' },
|
|
192
|
+
onAction,
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
manager.handleKey('j')
|
|
196
|
+
expect(onAction).toHaveBeenCalledWith('move-down', { key: 'j' })
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('does not call handler for unbound keys', async () => {
|
|
200
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
201
|
+
const onAction = vi.fn()
|
|
202
|
+
|
|
203
|
+
const manager = createKeyboardManager({
|
|
204
|
+
bindings: { j: 'move-down' },
|
|
205
|
+
onAction,
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
manager.handleKey('x')
|
|
209
|
+
expect(onAction).not.toHaveBeenCalled()
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('returns true when key was handled', async () => {
|
|
213
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
214
|
+
|
|
215
|
+
const manager = createKeyboardManager({
|
|
216
|
+
bindings: { j: 'move-down' },
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
expect(manager.handleKey('j')).toBe(true)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('returns false when key was not handled', async () => {
|
|
223
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
224
|
+
|
|
225
|
+
const manager = createKeyboardManager({
|
|
226
|
+
bindings: { j: 'move-down' },
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
expect(manager.handleKey('x')).toBe(false)
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
describe('binding management', () => {
|
|
234
|
+
it('allows adding bindings dynamically', async () => {
|
|
235
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
236
|
+
|
|
237
|
+
const manager = createKeyboardManager({
|
|
238
|
+
bindings: { j: 'move-down' },
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
manager.addBinding('x', 'delete')
|
|
242
|
+
expect(manager.getAction('x')).toBe('delete')
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('allows removing bindings', async () => {
|
|
246
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
247
|
+
|
|
248
|
+
const manager = createKeyboardManager({
|
|
249
|
+
bindings: { j: 'move-down', k: 'move-up' },
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
manager.removeBinding('j')
|
|
253
|
+
expect(manager.getAction('j')).toBeUndefined()
|
|
254
|
+
expect(manager.getAction('k')).toBe('move-up')
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('allows replacing all bindings', async () => {
|
|
258
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
259
|
+
|
|
260
|
+
const manager = createKeyboardManager({
|
|
261
|
+
bindings: { j: 'move-down' },
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
manager.setBindings({ x: 'delete' })
|
|
265
|
+
expect(manager.getAction('j')).toBeUndefined()
|
|
266
|
+
expect(manager.getAction('x')).toBe('delete')
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('returns all current bindings', async () => {
|
|
270
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
271
|
+
|
|
272
|
+
const bindings = { j: 'move-down', k: 'move-up' }
|
|
273
|
+
const manager = createKeyboardManager({ bindings })
|
|
274
|
+
|
|
275
|
+
expect(manager.getBindings()).toEqual(bindings)
|
|
276
|
+
})
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
describe('enabled state', () => {
|
|
280
|
+
it('can be disabled', async () => {
|
|
281
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
282
|
+
const onAction = vi.fn()
|
|
283
|
+
|
|
284
|
+
const manager = createKeyboardManager({
|
|
285
|
+
bindings: { j: 'move-down' },
|
|
286
|
+
onAction,
|
|
287
|
+
enabled: false,
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
manager.handleKey('j')
|
|
291
|
+
expect(onAction).not.toHaveBeenCalled()
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('can toggle enabled state', async () => {
|
|
295
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
296
|
+
const onAction = vi.fn()
|
|
297
|
+
|
|
298
|
+
const manager = createKeyboardManager({
|
|
299
|
+
bindings: { j: 'move-down' },
|
|
300
|
+
onAction,
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
manager.disable()
|
|
304
|
+
manager.handleKey('j')
|
|
305
|
+
expect(onAction).not.toHaveBeenCalled()
|
|
306
|
+
|
|
307
|
+
manager.enable()
|
|
308
|
+
manager.handleKey('j')
|
|
309
|
+
expect(onAction).toHaveBeenCalledWith('move-down', { key: 'j' })
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('reports enabled status', async () => {
|
|
313
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
314
|
+
|
|
315
|
+
const manager = createKeyboardManager({
|
|
316
|
+
bindings: {},
|
|
317
|
+
enabled: false,
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
expect(manager.isEnabled()).toBe(false)
|
|
321
|
+
manager.enable()
|
|
322
|
+
expect(manager.isEnabled()).toBe(true)
|
|
323
|
+
})
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
describe('key sequence support', () => {
|
|
327
|
+
it('handles multi-key sequences like gg', async () => {
|
|
328
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
329
|
+
const onAction = vi.fn()
|
|
330
|
+
|
|
331
|
+
const manager = createKeyboardManager({
|
|
332
|
+
bindings: {
|
|
333
|
+
gg: 'move-first',
|
|
334
|
+
G: 'move-last',
|
|
335
|
+
},
|
|
336
|
+
onAction,
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
manager.handleKey('g')
|
|
340
|
+
expect(onAction).not.toHaveBeenCalled() // Waiting for second key
|
|
341
|
+
|
|
342
|
+
manager.handleKey('g')
|
|
343
|
+
expect(onAction).toHaveBeenCalledWith('move-first', { key: 'gg' })
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
it('times out pending sequence', async () => {
|
|
347
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
348
|
+
const onAction = vi.fn()
|
|
349
|
+
|
|
350
|
+
const manager = createKeyboardManager({
|
|
351
|
+
bindings: {
|
|
352
|
+
gg: 'move-first',
|
|
353
|
+
g: 'go', // Fallback if only single g
|
|
354
|
+
},
|
|
355
|
+
onAction,
|
|
356
|
+
sequenceTimeout: 500,
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
manager.handleKey('g')
|
|
360
|
+
// Simulate timeout
|
|
361
|
+
await new Promise((r) => setTimeout(r, 600))
|
|
362
|
+
|
|
363
|
+
expect(onAction).toHaveBeenCalledWith('go', { key: 'g' })
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
it('cancels pending sequence on escape', async () => {
|
|
367
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
368
|
+
|
|
369
|
+
const manager = createKeyboardManager({
|
|
370
|
+
bindings: {
|
|
371
|
+
gg: 'move-first',
|
|
372
|
+
},
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
manager.handleKey('g')
|
|
376
|
+
expect(manager.getPendingSequence()).toBe('g')
|
|
377
|
+
|
|
378
|
+
manager.handleKey('escape')
|
|
379
|
+
expect(manager.getPendingSequence()).toBe('')
|
|
380
|
+
})
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
describe('destroy method', () => {
|
|
384
|
+
it('provides destroy function', async () => {
|
|
385
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
386
|
+
|
|
387
|
+
const manager = createKeyboardManager({
|
|
388
|
+
bindings: { j: 'move-down' },
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
expect(typeof manager.destroy).toBe('function')
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
it('clears pending sequence on destroy', async () => {
|
|
395
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
396
|
+
|
|
397
|
+
const manager = createKeyboardManager({
|
|
398
|
+
bindings: {
|
|
399
|
+
gg: 'move-first',
|
|
400
|
+
},
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
manager.handleKey('g')
|
|
404
|
+
expect(manager.getPendingSequence()).toBe('g')
|
|
405
|
+
|
|
406
|
+
manager.destroy()
|
|
407
|
+
expect(manager.getPendingSequence()).toBe('')
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it('clears sequence timer on destroy to prevent memory leak', async () => {
|
|
411
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
412
|
+
const onAction = vi.fn()
|
|
413
|
+
|
|
414
|
+
const manager = createKeyboardManager({
|
|
415
|
+
bindings: {
|
|
416
|
+
gg: 'move-first',
|
|
417
|
+
g: 'go',
|
|
418
|
+
},
|
|
419
|
+
onAction,
|
|
420
|
+
sequenceTimeout: 100,
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
// Start a sequence which creates a timer
|
|
424
|
+
// When 'g' is pressed and 'gg' exists, it waits for potential sequence completion
|
|
425
|
+
manager.handleKey('g')
|
|
426
|
+
|
|
427
|
+
// Immediately destroy before timeout fires
|
|
428
|
+
manager.destroy()
|
|
429
|
+
|
|
430
|
+
// Now verify destruction worked by checking onAction was not called yet
|
|
431
|
+
// (if the timer fired, it would have called onAction with 'go')
|
|
432
|
+
const callCountBeforeWait = onAction.mock.calls.length
|
|
433
|
+
|
|
434
|
+
// Wait for what would have been the timeout
|
|
435
|
+
await new Promise((r) => setTimeout(r, 200))
|
|
436
|
+
|
|
437
|
+
// The timeout handler should NOT have been called because destroy cleared it
|
|
438
|
+
// Without destroy, the timeout would have triggered 'go' action for 'g'
|
|
439
|
+
expect(onAction.mock.calls.length).toBe(callCountBeforeWait)
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
it('can be called multiple times safely', async () => {
|
|
443
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
444
|
+
|
|
445
|
+
const manager = createKeyboardManager({
|
|
446
|
+
bindings: { gg: 'move-first' },
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
manager.handleKey('g')
|
|
450
|
+
|
|
451
|
+
// Should not throw when called multiple times
|
|
452
|
+
expect(() => {
|
|
453
|
+
manager.destroy()
|
|
454
|
+
manager.destroy()
|
|
455
|
+
manager.destroy()
|
|
456
|
+
}).not.toThrow()
|
|
457
|
+
})
|
|
458
|
+
})
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
// ============================================================================
|
|
462
|
+
// useKeyboard Hook Tests
|
|
463
|
+
// ============================================================================
|
|
464
|
+
|
|
465
|
+
describe('useKeyboard hook', () => {
|
|
466
|
+
beforeEach(() => {
|
|
467
|
+
vi.clearAllMocks()
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
it('registers keyboard handler', async () => {
|
|
471
|
+
const { useKeyboard } = await import('@mdxui/terminal')
|
|
472
|
+
const handler = vi.fn()
|
|
473
|
+
|
|
474
|
+
const result = useKeyboard(handler)
|
|
475
|
+
|
|
476
|
+
expect(result).toBeDefined()
|
|
477
|
+
expect(result.enabled).toBe(true)
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it('provides enable/disable controls', async () => {
|
|
481
|
+
const { useKeyboard } = await import('@mdxui/terminal')
|
|
482
|
+
const handler = vi.fn()
|
|
483
|
+
|
|
484
|
+
const result = useKeyboard(handler)
|
|
485
|
+
|
|
486
|
+
expect(typeof result.enable).toBe('function')
|
|
487
|
+
expect(typeof result.disable).toBe('function')
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
it('respects enabled option', async () => {
|
|
491
|
+
const { useKeyboard } = await import('@mdxui/terminal')
|
|
492
|
+
const handler = vi.fn()
|
|
493
|
+
|
|
494
|
+
const result = useKeyboard(handler, { enabled: false })
|
|
495
|
+
|
|
496
|
+
expect(result.enabled).toBe(false)
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
it('provides key information in handler', async () => {
|
|
500
|
+
const { useKeyboard } = await import('@mdxui/terminal')
|
|
501
|
+
|
|
502
|
+
// The handler should receive key and modifier info
|
|
503
|
+
type KeyHandler = (
|
|
504
|
+
key: string,
|
|
505
|
+
modifiers: { ctrl: boolean; alt: boolean; shift: boolean; meta: boolean }
|
|
506
|
+
) => void
|
|
507
|
+
|
|
508
|
+
const handler: KeyHandler = vi.fn()
|
|
509
|
+
useKeyboard(handler)
|
|
510
|
+
|
|
511
|
+
// Handler signature is tested; actual calls depend on terminal events
|
|
512
|
+
expect(handler).toBeDefined()
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
it('supports priority for layered handlers', async () => {
|
|
516
|
+
const { useKeyboard } = await import('@mdxui/terminal')
|
|
517
|
+
const handler = vi.fn()
|
|
518
|
+
|
|
519
|
+
// Higher priority handlers should receive events first
|
|
520
|
+
const result = useKeyboard(handler, { priority: 10 })
|
|
521
|
+
|
|
522
|
+
expect(result.priority).toBe(10)
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
it('cleans up on unmount', async () => {
|
|
526
|
+
const { useKeyboard } = await import('@mdxui/terminal')
|
|
527
|
+
const handler = vi.fn()
|
|
528
|
+
|
|
529
|
+
const result = useKeyboard(handler)
|
|
530
|
+
|
|
531
|
+
// Should have cleanup function
|
|
532
|
+
expect(typeof result.cleanup).toBe('function')
|
|
533
|
+
})
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
// ============================================================================
|
|
537
|
+
// useFocus Hook Tests
|
|
538
|
+
// ============================================================================
|
|
539
|
+
|
|
540
|
+
describe('useFocus hook', () => {
|
|
541
|
+
beforeEach(() => {
|
|
542
|
+
vi.clearAllMocks()
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
describe('basic focus state', () => {
|
|
546
|
+
it('returns focused state', async () => {
|
|
547
|
+
const { useFocus } = await import('@mdxui/terminal')
|
|
548
|
+
|
|
549
|
+
const result = useFocus()
|
|
550
|
+
|
|
551
|
+
expect(typeof result.focused).toBe('boolean')
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
it('provides focus function', async () => {
|
|
555
|
+
const { useFocus } = await import('@mdxui/terminal')
|
|
556
|
+
|
|
557
|
+
const result = useFocus()
|
|
558
|
+
|
|
559
|
+
expect(typeof result.focus).toBe('function')
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
it('provides blur function', async () => {
|
|
563
|
+
const { useFocus } = await import('@mdxui/terminal')
|
|
564
|
+
|
|
565
|
+
const result = useFocus()
|
|
566
|
+
|
|
567
|
+
expect(typeof result.blur).toBe('function')
|
|
568
|
+
})
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
describe('focus navigation', () => {
|
|
572
|
+
it('provides focusNext function', async () => {
|
|
573
|
+
const { useFocus } = await import('@mdxui/terminal')
|
|
574
|
+
|
|
575
|
+
const result = useFocus()
|
|
576
|
+
|
|
577
|
+
expect(typeof result.focusNext).toBe('function')
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
it('provides focusPrev function', async () => {
|
|
581
|
+
const { useFocus } = await import('@mdxui/terminal')
|
|
582
|
+
|
|
583
|
+
const result = useFocus()
|
|
584
|
+
|
|
585
|
+
expect(typeof result.focusPrev).toBe('function')
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
it('provides focusFirst function', async () => {
|
|
589
|
+
const { useFocus } = await import('@mdxui/terminal')
|
|
590
|
+
|
|
591
|
+
const result = useFocus()
|
|
592
|
+
|
|
593
|
+
expect(typeof result.focusFirst).toBe('function')
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
it('provides focusLast function', async () => {
|
|
597
|
+
const { useFocus } = await import('@mdxui/terminal')
|
|
598
|
+
|
|
599
|
+
const result = useFocus()
|
|
600
|
+
|
|
601
|
+
expect(typeof result.focusLast).toBe('function')
|
|
602
|
+
})
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
describe('focus identification', () => {
|
|
606
|
+
it('accepts id parameter', async () => {
|
|
607
|
+
const { useFocus } = await import('@mdxui/terminal')
|
|
608
|
+
|
|
609
|
+
const result = useFocus({ id: 'my-element' })
|
|
610
|
+
|
|
611
|
+
expect(result.id).toBe('my-element')
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
it('generates id if not provided', async () => {
|
|
615
|
+
const { useFocus } = await import('@mdxui/terminal')
|
|
616
|
+
|
|
617
|
+
const result = useFocus()
|
|
618
|
+
|
|
619
|
+
expect(typeof result.id).toBe('string')
|
|
620
|
+
expect(result.id.length).toBeGreaterThan(0)
|
|
621
|
+
})
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
describe('auto focus', () => {
|
|
625
|
+
it('supports autoFocus option', async () => {
|
|
626
|
+
const { useFocus } = await import('@mdxui/terminal')
|
|
627
|
+
|
|
628
|
+
const result = useFocus({ autoFocus: true })
|
|
629
|
+
|
|
630
|
+
// With autoFocus, component should request focus on mount
|
|
631
|
+
expect(result.focused).toBe(true)
|
|
632
|
+
})
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
describe('tab index', () => {
|
|
636
|
+
it('returns tabIndex for ordering', async () => {
|
|
637
|
+
const { useFocus } = await import('@mdxui/terminal')
|
|
638
|
+
|
|
639
|
+
const result = useFocus({ tabIndex: 5 })
|
|
640
|
+
|
|
641
|
+
expect(result.tabIndex).toBe(5)
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
it('defaults tabIndex to 0', async () => {
|
|
645
|
+
const { useFocus } = await import('@mdxui/terminal')
|
|
646
|
+
|
|
647
|
+
const result = useFocus()
|
|
648
|
+
|
|
649
|
+
expect(result.tabIndex).toBe(0)
|
|
650
|
+
})
|
|
651
|
+
|
|
652
|
+
it('negative tabIndex removes from tab order', async () => {
|
|
653
|
+
const { useFocus } = await import('@mdxui/terminal')
|
|
654
|
+
|
|
655
|
+
const result = useFocus({ tabIndex: -1 })
|
|
656
|
+
|
|
657
|
+
expect(result.tabIndex).toBe(-1)
|
|
658
|
+
})
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
describe('focus callbacks', () => {
|
|
662
|
+
it('calls onFocus when focused', async () => {
|
|
663
|
+
const { useFocus } = await import('@mdxui/terminal')
|
|
664
|
+
const onFocus = vi.fn()
|
|
665
|
+
|
|
666
|
+
useFocus({ onFocus })
|
|
667
|
+
|
|
668
|
+
// onFocus should be registered
|
|
669
|
+
expect(onFocus).toBeDefined()
|
|
670
|
+
})
|
|
671
|
+
|
|
672
|
+
it('calls onBlur when blurred', async () => {
|
|
673
|
+
const { useFocus } = await import('@mdxui/terminal')
|
|
674
|
+
const onBlur = vi.fn()
|
|
675
|
+
|
|
676
|
+
useFocus({ onBlur })
|
|
677
|
+
|
|
678
|
+
// onBlur should be registered
|
|
679
|
+
expect(onBlur).toBeDefined()
|
|
680
|
+
})
|
|
681
|
+
})
|
|
682
|
+
})
|
|
683
|
+
|
|
684
|
+
// ============================================================================
|
|
685
|
+
// FocusProvider Tests
|
|
686
|
+
// ============================================================================
|
|
687
|
+
|
|
688
|
+
describe('FocusProvider', () => {
|
|
689
|
+
beforeEach(() => {
|
|
690
|
+
vi.clearAllMocks()
|
|
691
|
+
})
|
|
692
|
+
|
|
693
|
+
describe('basic rendering', () => {
|
|
694
|
+
it('renders children', async () => {
|
|
695
|
+
const { FocusProvider } = await import('@mdxui/terminal')
|
|
696
|
+
|
|
697
|
+
const element = FocusProvider({ children: 'Content' })
|
|
698
|
+
|
|
699
|
+
expect(element).toBeDefined()
|
|
700
|
+
})
|
|
701
|
+
})
|
|
702
|
+
|
|
703
|
+
describe('focus trapping', () => {
|
|
704
|
+
it('supports trap prop for modal focus', async () => {
|
|
705
|
+
const { FocusProvider } = await import('@mdxui/terminal')
|
|
706
|
+
|
|
707
|
+
const element = FocusProvider({ trap: true, children: 'Modal content' })
|
|
708
|
+
|
|
709
|
+
// FocusProvider wraps in Context.Provider, check inner div's data-focus-trap
|
|
710
|
+
const divElement = element.props?.children
|
|
711
|
+
expect(divElement?.props?.['data-focus-trap']).toBe(true)
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
it('wraps Tab focus to first element when at end', async () => {
|
|
715
|
+
const { FocusProvider } = await import('@mdxui/terminal')
|
|
716
|
+
|
|
717
|
+
const element = FocusProvider({
|
|
718
|
+
trap: true,
|
|
719
|
+
wrapFocus: true,
|
|
720
|
+
children: 'Content',
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
// wrapFocus is passed to div props and used internally for navigation
|
|
724
|
+
const divElement = element.props?.children
|
|
725
|
+
expect(divElement?.props?.wrapFocus).toBe(true)
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
it('wraps Shift+Tab focus to last element when at start', async () => {
|
|
729
|
+
const { FocusProvider } = await import('@mdxui/terminal')
|
|
730
|
+
|
|
731
|
+
// Same as above - wrapFocus handles both directions
|
|
732
|
+
const element = FocusProvider({
|
|
733
|
+
trap: true,
|
|
734
|
+
wrapFocus: true,
|
|
735
|
+
children: 'Content',
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
const divElement = element.props?.children
|
|
739
|
+
expect(divElement?.props?.wrapFocus).toBe(true)
|
|
740
|
+
})
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
describe('initial focus', () => {
|
|
744
|
+
it('supports initialFocus id', async () => {
|
|
745
|
+
const { FocusProvider } = await import('@mdxui/terminal')
|
|
746
|
+
|
|
747
|
+
const element = FocusProvider({
|
|
748
|
+
initialFocus: 'my-input',
|
|
749
|
+
children: 'Content',
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
// initialFocus is passed through to div props
|
|
753
|
+
const divElement = element.props?.children
|
|
754
|
+
expect(divElement?.props?.initialFocus).toBe('my-input')
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
it('focuses first focusable by default', async () => {
|
|
758
|
+
const { FocusProvider } = await import('@mdxui/terminal')
|
|
759
|
+
|
|
760
|
+
const element = FocusProvider({
|
|
761
|
+
focusFirstOnMount: true,
|
|
762
|
+
children: 'Content',
|
|
763
|
+
})
|
|
764
|
+
|
|
765
|
+
// focusFirstOnMount is passed through to div props
|
|
766
|
+
const divElement = element.props?.children
|
|
767
|
+
expect(divElement?.props?.focusFirstOnMount).toBe(true)
|
|
768
|
+
})
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
describe('focus restoration', () => {
|
|
772
|
+
it('restores focus on unmount', async () => {
|
|
773
|
+
const { FocusProvider } = await import('@mdxui/terminal')
|
|
774
|
+
|
|
775
|
+
const element = FocusProvider({
|
|
776
|
+
restoreFocus: true,
|
|
777
|
+
children: 'Content',
|
|
778
|
+
})
|
|
779
|
+
|
|
780
|
+
// restoreFocus is passed through to div props
|
|
781
|
+
const divElement = element.props?.children
|
|
782
|
+
expect(divElement?.props?.restoreFocus).toBe(true)
|
|
783
|
+
})
|
|
784
|
+
})
|
|
785
|
+
|
|
786
|
+
describe('focus group', () => {
|
|
787
|
+
it('supports group name for scoped focus', async () => {
|
|
788
|
+
const { FocusProvider } = await import('@mdxui/terminal')
|
|
789
|
+
|
|
790
|
+
const element = FocusProvider({
|
|
791
|
+
group: 'sidebar',
|
|
792
|
+
children: 'Content',
|
|
793
|
+
})
|
|
794
|
+
|
|
795
|
+
// group is exposed via data-focus-group attribute
|
|
796
|
+
const divElement = element.props?.children
|
|
797
|
+
expect(divElement?.props?.['data-focus-group']).toBe('sidebar')
|
|
798
|
+
})
|
|
799
|
+
})
|
|
800
|
+
})
|
|
801
|
+
|
|
802
|
+
// ============================================================================
|
|
803
|
+
// useFocusManager Hook Tests
|
|
804
|
+
// ============================================================================
|
|
805
|
+
|
|
806
|
+
describe('useFocusManager hook', () => {
|
|
807
|
+
it('provides access to focus context', async () => {
|
|
808
|
+
const { useFocusManager } = await import('@mdxui/terminal')
|
|
809
|
+
|
|
810
|
+
const result = useFocusManager()
|
|
811
|
+
|
|
812
|
+
expect(result).toBeDefined()
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
it('tracks all focusable elements', async () => {
|
|
816
|
+
const { useFocusManager } = await import('@mdxui/terminal')
|
|
817
|
+
|
|
818
|
+
const result = useFocusManager()
|
|
819
|
+
|
|
820
|
+
expect(Array.isArray(result.focusableIds)).toBe(true)
|
|
821
|
+
})
|
|
822
|
+
|
|
823
|
+
it('tracks currently focused element', async () => {
|
|
824
|
+
const { useFocusManager } = await import('@mdxui/terminal')
|
|
825
|
+
|
|
826
|
+
const result = useFocusManager()
|
|
827
|
+
|
|
828
|
+
// Could be null if nothing focused, or string id
|
|
829
|
+
expect(result.focusedId === null || typeof result.focusedId === 'string').toBe(true)
|
|
830
|
+
})
|
|
831
|
+
|
|
832
|
+
it('provides focusById function', async () => {
|
|
833
|
+
const { useFocusManager } = await import('@mdxui/terminal')
|
|
834
|
+
|
|
835
|
+
const result = useFocusManager()
|
|
836
|
+
|
|
837
|
+
expect(typeof result.focusById).toBe('function')
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
it('provides focus navigation functions', async () => {
|
|
841
|
+
const { useFocusManager } = await import('@mdxui/terminal')
|
|
842
|
+
|
|
843
|
+
const result = useFocusManager()
|
|
844
|
+
|
|
845
|
+
expect(typeof result.focusNext).toBe('function')
|
|
846
|
+
expect(typeof result.focusPrev).toBe('function')
|
|
847
|
+
expect(typeof result.focusFirst).toBe('function')
|
|
848
|
+
expect(typeof result.focusLast).toBe('function')
|
|
849
|
+
})
|
|
850
|
+
|
|
851
|
+
it('provides trap state', async () => {
|
|
852
|
+
const { useFocusManager } = await import('@mdxui/terminal')
|
|
853
|
+
|
|
854
|
+
const result = useFocusManager()
|
|
855
|
+
|
|
856
|
+
expect(typeof result.isTrapped).toBe('boolean')
|
|
857
|
+
})
|
|
858
|
+
})
|
|
859
|
+
|
|
860
|
+
// ============================================================================
|
|
861
|
+
// Vim-style Navigation Integration Tests
|
|
862
|
+
// ============================================================================
|
|
863
|
+
|
|
864
|
+
describe('vim-style navigation', () => {
|
|
865
|
+
describe('j/k vertical movement', () => {
|
|
866
|
+
it('j moves selection down', async () => {
|
|
867
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
868
|
+
const onAction = vi.fn()
|
|
869
|
+
|
|
870
|
+
const manager = createKeyboardManager({
|
|
871
|
+
bindings: { j: 'move-down', k: 'move-up' },
|
|
872
|
+
onAction,
|
|
873
|
+
})
|
|
874
|
+
|
|
875
|
+
manager.handleKey('j')
|
|
876
|
+
expect(onAction).toHaveBeenCalledWith('move-down', expect.any(Object))
|
|
877
|
+
})
|
|
878
|
+
|
|
879
|
+
it('k moves selection up', async () => {
|
|
880
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
881
|
+
const onAction = vi.fn()
|
|
882
|
+
|
|
883
|
+
const manager = createKeyboardManager({
|
|
884
|
+
bindings: { j: 'move-down', k: 'move-up' },
|
|
885
|
+
onAction,
|
|
886
|
+
})
|
|
887
|
+
|
|
888
|
+
manager.handleKey('k')
|
|
889
|
+
expect(onAction).toHaveBeenCalledWith('move-up', expect.any(Object))
|
|
890
|
+
})
|
|
891
|
+
|
|
892
|
+
it('J (shift+j) for faster movement', async () => {
|
|
893
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
894
|
+
const onAction = vi.fn()
|
|
895
|
+
|
|
896
|
+
const manager = createKeyboardManager({
|
|
897
|
+
bindings: { J: 'move-down-page', K: 'move-up-page' },
|
|
898
|
+
onAction,
|
|
899
|
+
})
|
|
900
|
+
|
|
901
|
+
manager.handleKey('J')
|
|
902
|
+
expect(onAction).toHaveBeenCalledWith('move-down-page', expect.any(Object))
|
|
903
|
+
})
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
describe('h/l horizontal movement', () => {
|
|
907
|
+
it('h moves left', async () => {
|
|
908
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
909
|
+
const onAction = vi.fn()
|
|
910
|
+
|
|
911
|
+
const manager = createKeyboardManager({
|
|
912
|
+
bindings: { h: 'move-left', l: 'move-right' },
|
|
913
|
+
onAction,
|
|
914
|
+
})
|
|
915
|
+
|
|
916
|
+
manager.handleKey('h')
|
|
917
|
+
expect(onAction).toHaveBeenCalledWith('move-left', expect.any(Object))
|
|
918
|
+
})
|
|
919
|
+
|
|
920
|
+
it('l moves right', async () => {
|
|
921
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
922
|
+
const onAction = vi.fn()
|
|
923
|
+
|
|
924
|
+
const manager = createKeyboardManager({
|
|
925
|
+
bindings: { h: 'move-left', l: 'move-right' },
|
|
926
|
+
onAction,
|
|
927
|
+
})
|
|
928
|
+
|
|
929
|
+
manager.handleKey('l')
|
|
930
|
+
expect(onAction).toHaveBeenCalledWith('move-right', expect.any(Object))
|
|
931
|
+
})
|
|
932
|
+
})
|
|
933
|
+
|
|
934
|
+
describe('arrow key alternatives', () => {
|
|
935
|
+
it('arrow keys work alongside vim keys', async () => {
|
|
936
|
+
const { createKeyboardManager, ARROW_BINDINGS } = await import('@mdxui/terminal')
|
|
937
|
+
const onAction = vi.fn()
|
|
938
|
+
|
|
939
|
+
const manager = createKeyboardManager({
|
|
940
|
+
bindings: ARROW_BINDINGS,
|
|
941
|
+
onAction,
|
|
942
|
+
})
|
|
943
|
+
|
|
944
|
+
manager.handleKey('up')
|
|
945
|
+
expect(onAction).toHaveBeenCalledWith('move-up', expect.any(Object))
|
|
946
|
+
|
|
947
|
+
manager.handleKey('down')
|
|
948
|
+
expect(onAction).toHaveBeenCalledWith('move-down', expect.any(Object))
|
|
949
|
+
|
|
950
|
+
manager.handleKey('left')
|
|
951
|
+
expect(onAction).toHaveBeenCalledWith('move-left', expect.any(Object))
|
|
952
|
+
|
|
953
|
+
manager.handleKey('right')
|
|
954
|
+
expect(onAction).toHaveBeenCalledWith('move-right', expect.any(Object))
|
|
955
|
+
})
|
|
956
|
+
})
|
|
957
|
+
})
|
|
958
|
+
|
|
959
|
+
// ============================================================================
|
|
960
|
+
// Selection & Activation Tests
|
|
961
|
+
// ============================================================================
|
|
962
|
+
|
|
963
|
+
describe('selection and activation', () => {
|
|
964
|
+
describe('enter key', () => {
|
|
965
|
+
it('triggers select action', async () => {
|
|
966
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
967
|
+
const onAction = vi.fn()
|
|
968
|
+
|
|
969
|
+
const manager = createKeyboardManager({
|
|
970
|
+
bindings: { enter: 'select', return: 'select' },
|
|
971
|
+
onAction,
|
|
972
|
+
})
|
|
973
|
+
|
|
974
|
+
manager.handleKey('enter')
|
|
975
|
+
expect(onAction).toHaveBeenCalledWith('select', expect.any(Object))
|
|
976
|
+
})
|
|
977
|
+
})
|
|
978
|
+
|
|
979
|
+
describe('space key', () => {
|
|
980
|
+
it('triggers toggle action', async () => {
|
|
981
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
982
|
+
const onAction = vi.fn()
|
|
983
|
+
|
|
984
|
+
const manager = createKeyboardManager({
|
|
985
|
+
bindings: { space: 'toggle', ' ': 'toggle' },
|
|
986
|
+
onAction,
|
|
987
|
+
})
|
|
988
|
+
|
|
989
|
+
manager.handleKey('space')
|
|
990
|
+
expect(onAction).toHaveBeenCalledWith('toggle', expect.any(Object))
|
|
991
|
+
})
|
|
992
|
+
})
|
|
993
|
+
|
|
994
|
+
describe('escape key', () => {
|
|
995
|
+
it('triggers back/cancel action', async () => {
|
|
996
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
997
|
+
const onAction = vi.fn()
|
|
998
|
+
|
|
999
|
+
const manager = createKeyboardManager({
|
|
1000
|
+
bindings: { escape: 'back' },
|
|
1001
|
+
onAction,
|
|
1002
|
+
})
|
|
1003
|
+
|
|
1004
|
+
manager.handleKey('escape')
|
|
1005
|
+
expect(onAction).toHaveBeenCalledWith('back', expect.any(Object))
|
|
1006
|
+
})
|
|
1007
|
+
})
|
|
1008
|
+
})
|
|
1009
|
+
|
|
1010
|
+
// ============================================================================
|
|
1011
|
+
// Tab Navigation Tests
|
|
1012
|
+
// ============================================================================
|
|
1013
|
+
|
|
1014
|
+
describe('tab navigation', () => {
|
|
1015
|
+
describe('Tab key', () => {
|
|
1016
|
+
it('Tab triggers focus-next action', async () => {
|
|
1017
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
1018
|
+
const onAction = vi.fn()
|
|
1019
|
+
|
|
1020
|
+
const manager = createKeyboardManager({
|
|
1021
|
+
bindings: { tab: 'focus-next', 'shift+tab': 'focus-prev' },
|
|
1022
|
+
onAction,
|
|
1023
|
+
})
|
|
1024
|
+
|
|
1025
|
+
manager.handleKey('tab')
|
|
1026
|
+
expect(onAction).toHaveBeenCalledWith('focus-next', expect.any(Object))
|
|
1027
|
+
})
|
|
1028
|
+
})
|
|
1029
|
+
|
|
1030
|
+
describe('Shift+Tab', () => {
|
|
1031
|
+
it('Shift+Tab triggers focus-prev action', async () => {
|
|
1032
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
1033
|
+
const onAction = vi.fn()
|
|
1034
|
+
|
|
1035
|
+
const manager = createKeyboardManager({
|
|
1036
|
+
bindings: { tab: 'focus-next', 'shift+tab': 'focus-prev' },
|
|
1037
|
+
onAction,
|
|
1038
|
+
})
|
|
1039
|
+
|
|
1040
|
+
manager.handleKey('shift+tab')
|
|
1041
|
+
expect(onAction).toHaveBeenCalledWith('focus-prev', expect.any(Object))
|
|
1042
|
+
})
|
|
1043
|
+
})
|
|
1044
|
+
})
|
|
1045
|
+
|
|
1046
|
+
// ============================================================================
|
|
1047
|
+
// Custom Bindings Tests
|
|
1048
|
+
// ============================================================================
|
|
1049
|
+
|
|
1050
|
+
describe('custom bindings', () => {
|
|
1051
|
+
it('allows custom action names', async () => {
|
|
1052
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
1053
|
+
const onAction = vi.fn()
|
|
1054
|
+
|
|
1055
|
+
const manager = createKeyboardManager({
|
|
1056
|
+
bindings: {
|
|
1057
|
+
d: 'delete-item',
|
|
1058
|
+
r: 'rename-item',
|
|
1059
|
+
n: 'new-item',
|
|
1060
|
+
},
|
|
1061
|
+
onAction,
|
|
1062
|
+
})
|
|
1063
|
+
|
|
1064
|
+
manager.handleKey('d')
|
|
1065
|
+
expect(onAction).toHaveBeenCalledWith('delete-item', expect.any(Object))
|
|
1066
|
+
})
|
|
1067
|
+
|
|
1068
|
+
it('supports function as action', async () => {
|
|
1069
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
1070
|
+
const customAction = vi.fn()
|
|
1071
|
+
|
|
1072
|
+
const manager = createKeyboardManager({
|
|
1073
|
+
bindings: {
|
|
1074
|
+
x: customAction,
|
|
1075
|
+
},
|
|
1076
|
+
})
|
|
1077
|
+
|
|
1078
|
+
manager.handleKey('x')
|
|
1079
|
+
expect(customAction).toHaveBeenCalled()
|
|
1080
|
+
})
|
|
1081
|
+
|
|
1082
|
+
it('supports context-aware bindings', async () => {
|
|
1083
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
1084
|
+
const onAction = vi.fn()
|
|
1085
|
+
|
|
1086
|
+
const manager = createKeyboardManager({
|
|
1087
|
+
bindings: { enter: 'select' },
|
|
1088
|
+
onAction,
|
|
1089
|
+
context: { mode: 'list' },
|
|
1090
|
+
})
|
|
1091
|
+
|
|
1092
|
+
manager.handleKey('enter')
|
|
1093
|
+
expect(onAction).toHaveBeenCalledWith('select', expect.objectContaining({
|
|
1094
|
+
context: { mode: 'list' },
|
|
1095
|
+
}))
|
|
1096
|
+
})
|
|
1097
|
+
|
|
1098
|
+
it('allows updating context', async () => {
|
|
1099
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
1100
|
+
const onAction = vi.fn()
|
|
1101
|
+
|
|
1102
|
+
const manager = createKeyboardManager({
|
|
1103
|
+
bindings: { enter: 'select' },
|
|
1104
|
+
onAction,
|
|
1105
|
+
context: { mode: 'list' },
|
|
1106
|
+
})
|
|
1107
|
+
|
|
1108
|
+
manager.setContext({ mode: 'edit' })
|
|
1109
|
+
manager.handleKey('enter')
|
|
1110
|
+
|
|
1111
|
+
expect(onAction).toHaveBeenCalledWith('select', expect.objectContaining({
|
|
1112
|
+
context: { mode: 'edit' },
|
|
1113
|
+
}))
|
|
1114
|
+
})
|
|
1115
|
+
})
|
|
1116
|
+
|
|
1117
|
+
// ============================================================================
|
|
1118
|
+
// Preset Bindings Tests
|
|
1119
|
+
// ============================================================================
|
|
1120
|
+
|
|
1121
|
+
describe('preset bindings', () => {
|
|
1122
|
+
it('exports VIM_BINDINGS preset', async () => {
|
|
1123
|
+
const { VIM_BINDINGS } = await import('@mdxui/terminal')
|
|
1124
|
+
|
|
1125
|
+
expect(VIM_BINDINGS).toBeDefined()
|
|
1126
|
+
expect(VIM_BINDINGS.j).toBe('move-down')
|
|
1127
|
+
expect(VIM_BINDINGS.k).toBe('move-up')
|
|
1128
|
+
expect(VIM_BINDINGS.h).toBe('move-left')
|
|
1129
|
+
expect(VIM_BINDINGS.l).toBe('move-right')
|
|
1130
|
+
})
|
|
1131
|
+
|
|
1132
|
+
it('exports ARROW_BINDINGS preset', async () => {
|
|
1133
|
+
const { ARROW_BINDINGS } = await import('@mdxui/terminal')
|
|
1134
|
+
|
|
1135
|
+
expect(ARROW_BINDINGS).toBeDefined()
|
|
1136
|
+
expect(ARROW_BINDINGS.up).toBe('move-up')
|
|
1137
|
+
expect(ARROW_BINDINGS.down).toBe('move-down')
|
|
1138
|
+
expect(ARROW_BINDINGS.left).toBe('move-left')
|
|
1139
|
+
expect(ARROW_BINDINGS.right).toBe('move-right')
|
|
1140
|
+
})
|
|
1141
|
+
|
|
1142
|
+
it('exports COMMON_BINDINGS preset', async () => {
|
|
1143
|
+
const { COMMON_BINDINGS } = await import('@mdxui/terminal')
|
|
1144
|
+
|
|
1145
|
+
expect(COMMON_BINDINGS).toBeDefined()
|
|
1146
|
+
expect(COMMON_BINDINGS.enter).toBe('select')
|
|
1147
|
+
expect(COMMON_BINDINGS.escape).toBe('back')
|
|
1148
|
+
expect(COMMON_BINDINGS.tab).toBe('focus-next')
|
|
1149
|
+
expect(COMMON_BINDINGS['shift+tab']).toBe('focus-prev')
|
|
1150
|
+
})
|
|
1151
|
+
|
|
1152
|
+
it('allows merging presets', async () => {
|
|
1153
|
+
const { createKeyboardManager, VIM_BINDINGS, COMMON_BINDINGS } = await import('@mdxui/terminal')
|
|
1154
|
+
|
|
1155
|
+
const manager = createKeyboardManager({
|
|
1156
|
+
bindings: { ...VIM_BINDINGS, ...COMMON_BINDINGS },
|
|
1157
|
+
})
|
|
1158
|
+
|
|
1159
|
+
// Should have both vim and common bindings
|
|
1160
|
+
expect(manager.getAction('j')).toBe('move-down')
|
|
1161
|
+
expect(manager.getAction('enter')).toBe('select')
|
|
1162
|
+
})
|
|
1163
|
+
})
|
|
1164
|
+
|
|
1165
|
+
// ============================================================================
|
|
1166
|
+
// Focus Manager Context Tests
|
|
1167
|
+
// ============================================================================
|
|
1168
|
+
|
|
1169
|
+
describe('FocusManagerContext', () => {
|
|
1170
|
+
it('exports FocusContext', async () => {
|
|
1171
|
+
const { FocusContext } = await import('@mdxui/terminal')
|
|
1172
|
+
|
|
1173
|
+
expect(FocusContext).toBeDefined()
|
|
1174
|
+
})
|
|
1175
|
+
|
|
1176
|
+
it('provides default value when not in provider', async () => {
|
|
1177
|
+
const { useFocusManager } = await import('@mdxui/terminal')
|
|
1178
|
+
|
|
1179
|
+
const result = useFocusManager()
|
|
1180
|
+
|
|
1181
|
+
// Should have default no-op implementations
|
|
1182
|
+
expect(result.focusableIds).toEqual([])
|
|
1183
|
+
expect(result.focusedId).toBeNull()
|
|
1184
|
+
})
|
|
1185
|
+
})
|
|
1186
|
+
|
|
1187
|
+
// ============================================================================
|
|
1188
|
+
// useNavigableList Hook Tests
|
|
1189
|
+
// ============================================================================
|
|
1190
|
+
|
|
1191
|
+
describe('useNavigableList hook', () => {
|
|
1192
|
+
it('provides current index', async () => {
|
|
1193
|
+
const { useNavigableList } = await import('@mdxui/terminal')
|
|
1194
|
+
|
|
1195
|
+
const result = useNavigableList({
|
|
1196
|
+
items: ['a', 'b', 'c'],
|
|
1197
|
+
})
|
|
1198
|
+
|
|
1199
|
+
expect(typeof result.currentIndex).toBe('number')
|
|
1200
|
+
expect(result.currentIndex).toBe(0) // Default to first item
|
|
1201
|
+
})
|
|
1202
|
+
|
|
1203
|
+
it('provides navigation functions', async () => {
|
|
1204
|
+
const { useNavigableList } = await import('@mdxui/terminal')
|
|
1205
|
+
|
|
1206
|
+
const result = useNavigableList({
|
|
1207
|
+
items: ['a', 'b', 'c'],
|
|
1208
|
+
})
|
|
1209
|
+
|
|
1210
|
+
expect(typeof result.moveUp).toBe('function')
|
|
1211
|
+
expect(typeof result.moveDown).toBe('function')
|
|
1212
|
+
expect(typeof result.moveToFirst).toBe('function')
|
|
1213
|
+
expect(typeof result.moveToLast).toBe('function')
|
|
1214
|
+
})
|
|
1215
|
+
|
|
1216
|
+
it('provides current item', async () => {
|
|
1217
|
+
const { useNavigableList } = await import('@mdxui/terminal')
|
|
1218
|
+
|
|
1219
|
+
const result = useNavigableList({
|
|
1220
|
+
items: ['a', 'b', 'c'],
|
|
1221
|
+
initialIndex: 1,
|
|
1222
|
+
})
|
|
1223
|
+
|
|
1224
|
+
expect(result.currentItem).toBe('b')
|
|
1225
|
+
})
|
|
1226
|
+
|
|
1227
|
+
it('supports wrap option', async () => {
|
|
1228
|
+
const { useNavigableList } = await import('@mdxui/terminal')
|
|
1229
|
+
|
|
1230
|
+
const result = useNavigableList({
|
|
1231
|
+
items: ['a', 'b', 'c'],
|
|
1232
|
+
wrap: true,
|
|
1233
|
+
})
|
|
1234
|
+
|
|
1235
|
+
expect(result.wrap).toBe(true)
|
|
1236
|
+
})
|
|
1237
|
+
|
|
1238
|
+
it('supports keyboard bindings', async () => {
|
|
1239
|
+
const { useNavigableList } = await import('@mdxui/terminal')
|
|
1240
|
+
|
|
1241
|
+
const result = useNavigableList({
|
|
1242
|
+
items: ['a', 'b', 'c'],
|
|
1243
|
+
useKeyboard: true,
|
|
1244
|
+
})
|
|
1245
|
+
|
|
1246
|
+
expect(result.keyboardEnabled).toBe(true)
|
|
1247
|
+
})
|
|
1248
|
+
|
|
1249
|
+
it('provides setIndex function', async () => {
|
|
1250
|
+
const { useNavigableList } = await import('@mdxui/terminal')
|
|
1251
|
+
|
|
1252
|
+
const result = useNavigableList({
|
|
1253
|
+
items: ['a', 'b', 'c'],
|
|
1254
|
+
})
|
|
1255
|
+
|
|
1256
|
+
expect(typeof result.setIndex).toBe('function')
|
|
1257
|
+
})
|
|
1258
|
+
})
|
|
1259
|
+
|
|
1260
|
+
// ============================================================================
|
|
1261
|
+
// useNavigableGrid Hook Tests
|
|
1262
|
+
// ============================================================================
|
|
1263
|
+
|
|
1264
|
+
describe('useNavigableGrid hook', () => {
|
|
1265
|
+
it('provides current position', async () => {
|
|
1266
|
+
const { useNavigableGrid } = await import('@mdxui/terminal')
|
|
1267
|
+
|
|
1268
|
+
const result = useNavigableGrid({
|
|
1269
|
+
rows: 3,
|
|
1270
|
+
cols: 4,
|
|
1271
|
+
})
|
|
1272
|
+
|
|
1273
|
+
expect(typeof result.row).toBe('number')
|
|
1274
|
+
expect(typeof result.col).toBe('number')
|
|
1275
|
+
expect(result.row).toBe(0)
|
|
1276
|
+
expect(result.col).toBe(0)
|
|
1277
|
+
})
|
|
1278
|
+
|
|
1279
|
+
it('provides navigation functions for all directions', async () => {
|
|
1280
|
+
const { useNavigableGrid } = await import('@mdxui/terminal')
|
|
1281
|
+
|
|
1282
|
+
const result = useNavigableGrid({
|
|
1283
|
+
rows: 3,
|
|
1284
|
+
cols: 4,
|
|
1285
|
+
})
|
|
1286
|
+
|
|
1287
|
+
expect(typeof result.moveUp).toBe('function')
|
|
1288
|
+
expect(typeof result.moveDown).toBe('function')
|
|
1289
|
+
expect(typeof result.moveLeft).toBe('function')
|
|
1290
|
+
expect(typeof result.moveRight).toBe('function')
|
|
1291
|
+
})
|
|
1292
|
+
|
|
1293
|
+
it('provides moveToCell function', async () => {
|
|
1294
|
+
const { useNavigableGrid } = await import('@mdxui/terminal')
|
|
1295
|
+
|
|
1296
|
+
const result = useNavigableGrid({
|
|
1297
|
+
rows: 3,
|
|
1298
|
+
cols: 4,
|
|
1299
|
+
})
|
|
1300
|
+
|
|
1301
|
+
expect(typeof result.moveToCell).toBe('function')
|
|
1302
|
+
})
|
|
1303
|
+
|
|
1304
|
+
it('supports wrap in both dimensions', async () => {
|
|
1305
|
+
const { useNavigableGrid } = await import('@mdxui/terminal')
|
|
1306
|
+
|
|
1307
|
+
const result = useNavigableGrid({
|
|
1308
|
+
rows: 3,
|
|
1309
|
+
cols: 4,
|
|
1310
|
+
wrapHorizontal: true,
|
|
1311
|
+
wrapVertical: true,
|
|
1312
|
+
})
|
|
1313
|
+
|
|
1314
|
+
expect(result.wrapHorizontal).toBe(true)
|
|
1315
|
+
expect(result.wrapVertical).toBe(true)
|
|
1316
|
+
})
|
|
1317
|
+
|
|
1318
|
+
it('supports keyboard bindings', async () => {
|
|
1319
|
+
const { useNavigableGrid } = await import('@mdxui/terminal')
|
|
1320
|
+
|
|
1321
|
+
const result = useNavigableGrid({
|
|
1322
|
+
rows: 3,
|
|
1323
|
+
cols: 4,
|
|
1324
|
+
useKeyboard: true,
|
|
1325
|
+
})
|
|
1326
|
+
|
|
1327
|
+
expect(result.keyboardEnabled).toBe(true)
|
|
1328
|
+
})
|
|
1329
|
+
})
|
|
1330
|
+
|
|
1331
|
+
// ============================================================================
|
|
1332
|
+
// Type Exports Tests
|
|
1333
|
+
// ============================================================================
|
|
1334
|
+
|
|
1335
|
+
describe('type exports', () => {
|
|
1336
|
+
it('exports KeyBinding type', async () => {
|
|
1337
|
+
// This test verifies types are exported - checking via usage
|
|
1338
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
1339
|
+
|
|
1340
|
+
// TypeScript would fail compilation if KeyBinding isn't exported correctly
|
|
1341
|
+
const manager = createKeyboardManager({
|
|
1342
|
+
bindings: { j: 'move-down' },
|
|
1343
|
+
})
|
|
1344
|
+
|
|
1345
|
+
expect(manager).toBeDefined()
|
|
1346
|
+
})
|
|
1347
|
+
|
|
1348
|
+
it('exports KeyboardAction type', async () => {
|
|
1349
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
1350
|
+
|
|
1351
|
+
// KeyboardAction should support string and function
|
|
1352
|
+
const manager = createKeyboardManager({
|
|
1353
|
+
bindings: {
|
|
1354
|
+
a: 'action-name',
|
|
1355
|
+
b: () => {},
|
|
1356
|
+
},
|
|
1357
|
+
})
|
|
1358
|
+
|
|
1359
|
+
expect(manager).toBeDefined()
|
|
1360
|
+
})
|
|
1361
|
+
|
|
1362
|
+
it('exports KeyModifiers type', async () => {
|
|
1363
|
+
const { useKeyboard } = await import('@mdxui/terminal')
|
|
1364
|
+
|
|
1365
|
+
// KeyModifiers is used in handler parameter
|
|
1366
|
+
useKeyboard((_key, modifiers) => {
|
|
1367
|
+
// TypeScript checks modifiers has ctrl, alt, shift, meta
|
|
1368
|
+
const _: boolean = modifiers.ctrl
|
|
1369
|
+
const __: boolean = modifiers.alt
|
|
1370
|
+
const ___: boolean = modifiers.shift
|
|
1371
|
+
})
|
|
1372
|
+
})
|
|
1373
|
+
|
|
1374
|
+
it('exports FocusableElement type', async () => {
|
|
1375
|
+
const { useFocus } = await import('@mdxui/terminal')
|
|
1376
|
+
|
|
1377
|
+
// FocusableElement is the return type of useFocus
|
|
1378
|
+
const element = useFocus({ id: 'test' })
|
|
1379
|
+
|
|
1380
|
+
expect(element.id).toBeDefined()
|
|
1381
|
+
expect(typeof element.focused).toBe('boolean')
|
|
1382
|
+
})
|
|
1383
|
+
})
|
|
1384
|
+
|
|
1385
|
+
// ============================================================================
|
|
1386
|
+
// normalizeReadlineKey Tests
|
|
1387
|
+
// ============================================================================
|
|
1388
|
+
|
|
1389
|
+
describe('normalizeReadlineKey', () => {
|
|
1390
|
+
it('normalizes single character input', async () => {
|
|
1391
|
+
const { normalizeReadlineKey } = await import('@mdxui/terminal')
|
|
1392
|
+
|
|
1393
|
+
const result = normalizeReadlineKey('a', undefined)
|
|
1394
|
+
|
|
1395
|
+
expect(result.name).toBe('a')
|
|
1396
|
+
expect(result.ctrl).toBe(false)
|
|
1397
|
+
expect(result.alt).toBe(false)
|
|
1398
|
+
expect(result.shift).toBe(false)
|
|
1399
|
+
expect(result.meta).toBe(false)
|
|
1400
|
+
})
|
|
1401
|
+
|
|
1402
|
+
it('detects shift for uppercase letters', async () => {
|
|
1403
|
+
const { normalizeReadlineKey } = await import('@mdxui/terminal')
|
|
1404
|
+
|
|
1405
|
+
const result = normalizeReadlineKey('A', undefined)
|
|
1406
|
+
|
|
1407
|
+
expect(result.name).toBe('A')
|
|
1408
|
+
expect(result.shift).toBe(true)
|
|
1409
|
+
})
|
|
1410
|
+
|
|
1411
|
+
it('normalizes return to enter', async () => {
|
|
1412
|
+
const { normalizeReadlineKey } = await import('@mdxui/terminal')
|
|
1413
|
+
|
|
1414
|
+
const result = normalizeReadlineKey('\r', { name: 'return' })
|
|
1415
|
+
|
|
1416
|
+
expect(result.name).toBe('enter')
|
|
1417
|
+
})
|
|
1418
|
+
|
|
1419
|
+
it('preserves ctrl modifier from readline key', async () => {
|
|
1420
|
+
const { normalizeReadlineKey } = await import('@mdxui/terminal')
|
|
1421
|
+
|
|
1422
|
+
const result = normalizeReadlineKey('\x03', { name: 'c', ctrl: true })
|
|
1423
|
+
|
|
1424
|
+
expect(result.name).toBe('c')
|
|
1425
|
+
expect(result.ctrl).toBe(true)
|
|
1426
|
+
})
|
|
1427
|
+
|
|
1428
|
+
it('preserves shift modifier from readline key', async () => {
|
|
1429
|
+
const { normalizeReadlineKey } = await import('@mdxui/terminal')
|
|
1430
|
+
|
|
1431
|
+
const result = normalizeReadlineKey('\t', { name: 'tab', shift: true })
|
|
1432
|
+
|
|
1433
|
+
expect(result.name).toBe('tab')
|
|
1434
|
+
expect(result.shift).toBe(true)
|
|
1435
|
+
})
|
|
1436
|
+
|
|
1437
|
+
it('normalizes arrow keys', async () => {
|
|
1438
|
+
const { normalizeReadlineKey } = await import('@mdxui/terminal')
|
|
1439
|
+
|
|
1440
|
+
expect(normalizeReadlineKey('\x1b[A', { name: 'up' }).name).toBe('up')
|
|
1441
|
+
expect(normalizeReadlineKey('\x1b[B', { name: 'down' }).name).toBe('down')
|
|
1442
|
+
expect(normalizeReadlineKey('\x1b[C', { name: 'right' }).name).toBe('right')
|
|
1443
|
+
expect(normalizeReadlineKey('\x1b[D', { name: 'left' }).name).toBe('left')
|
|
1444
|
+
})
|
|
1445
|
+
|
|
1446
|
+
it('normalizes escape key', async () => {
|
|
1447
|
+
const { normalizeReadlineKey } = await import('@mdxui/terminal')
|
|
1448
|
+
|
|
1449
|
+
const result = normalizeReadlineKey('\x1b', { name: 'escape' })
|
|
1450
|
+
|
|
1451
|
+
expect(result.name).toBe('escape')
|
|
1452
|
+
})
|
|
1453
|
+
|
|
1454
|
+
it('handles unknown keys by lowercasing', async () => {
|
|
1455
|
+
const { normalizeReadlineKey } = await import('@mdxui/terminal')
|
|
1456
|
+
|
|
1457
|
+
const result = normalizeReadlineKey('x', { name: 'CUSTOMKEY' })
|
|
1458
|
+
|
|
1459
|
+
expect(result.name).toBe('customkey')
|
|
1460
|
+
})
|
|
1461
|
+
})
|
|
1462
|
+
|
|
1463
|
+
// ============================================================================
|
|
1464
|
+
// keyToBindingString Tests
|
|
1465
|
+
// ============================================================================
|
|
1466
|
+
|
|
1467
|
+
describe('keyToBindingString', () => {
|
|
1468
|
+
it('returns plain key name for no modifiers', async () => {
|
|
1469
|
+
const { keyToBindingString } = await import('@mdxui/terminal')
|
|
1470
|
+
|
|
1471
|
+
const result = keyToBindingString({
|
|
1472
|
+
name: 'a',
|
|
1473
|
+
ctrl: false,
|
|
1474
|
+
alt: false,
|
|
1475
|
+
shift: false,
|
|
1476
|
+
meta: false,
|
|
1477
|
+
})
|
|
1478
|
+
|
|
1479
|
+
expect(result).toBe('a')
|
|
1480
|
+
})
|
|
1481
|
+
|
|
1482
|
+
it('adds ctrl modifier', async () => {
|
|
1483
|
+
const { keyToBindingString } = await import('@mdxui/terminal')
|
|
1484
|
+
|
|
1485
|
+
const result = keyToBindingString({
|
|
1486
|
+
name: 'c',
|
|
1487
|
+
ctrl: true,
|
|
1488
|
+
alt: false,
|
|
1489
|
+
shift: false,
|
|
1490
|
+
meta: false,
|
|
1491
|
+
})
|
|
1492
|
+
|
|
1493
|
+
expect(result).toBe('ctrl+c')
|
|
1494
|
+
})
|
|
1495
|
+
|
|
1496
|
+
it('adds shift modifier', async () => {
|
|
1497
|
+
const { keyToBindingString } = await import('@mdxui/terminal')
|
|
1498
|
+
|
|
1499
|
+
const result = keyToBindingString({
|
|
1500
|
+
name: 'tab',
|
|
1501
|
+
ctrl: false,
|
|
1502
|
+
alt: false,
|
|
1503
|
+
shift: true,
|
|
1504
|
+
meta: false,
|
|
1505
|
+
})
|
|
1506
|
+
|
|
1507
|
+
expect(result).toBe('shift+tab')
|
|
1508
|
+
})
|
|
1509
|
+
|
|
1510
|
+
it('combines multiple modifiers in order', async () => {
|
|
1511
|
+
const { keyToBindingString } = await import('@mdxui/terminal')
|
|
1512
|
+
|
|
1513
|
+
const result = keyToBindingString({
|
|
1514
|
+
name: 's',
|
|
1515
|
+
ctrl: true,
|
|
1516
|
+
alt: false,
|
|
1517
|
+
shift: true,
|
|
1518
|
+
meta: false,
|
|
1519
|
+
})
|
|
1520
|
+
|
|
1521
|
+
expect(result).toBe('ctrl+shift+s')
|
|
1522
|
+
})
|
|
1523
|
+
|
|
1524
|
+
it('includes all four modifiers when present', async () => {
|
|
1525
|
+
const { keyToBindingString } = await import('@mdxui/terminal')
|
|
1526
|
+
|
|
1527
|
+
const result = keyToBindingString({
|
|
1528
|
+
name: 'a',
|
|
1529
|
+
ctrl: true,
|
|
1530
|
+
alt: true,
|
|
1531
|
+
shift: true,
|
|
1532
|
+
meta: true,
|
|
1533
|
+
})
|
|
1534
|
+
|
|
1535
|
+
expect(result).toBe('ctrl+alt+shift+meta+a')
|
|
1536
|
+
})
|
|
1537
|
+
})
|
|
1538
|
+
|
|
1539
|
+
// ============================================================================
|
|
1540
|
+
// normalizeOpenTUIKey Tests
|
|
1541
|
+
// ============================================================================
|
|
1542
|
+
|
|
1543
|
+
describe('normalizeOpenTUIKey', () => {
|
|
1544
|
+
it('normalizes basic key', async () => {
|
|
1545
|
+
const { normalizeOpenTUIKey } = await import('@mdxui/terminal')
|
|
1546
|
+
|
|
1547
|
+
const result = normalizeOpenTUIKey({
|
|
1548
|
+
name: 'a',
|
|
1549
|
+
ctrl: false,
|
|
1550
|
+
meta: false,
|
|
1551
|
+
shift: false,
|
|
1552
|
+
option: false,
|
|
1553
|
+
sequence: 'a',
|
|
1554
|
+
})
|
|
1555
|
+
|
|
1556
|
+
expect(result.name).toBe('a')
|
|
1557
|
+
expect(result.ctrl).toBe(false)
|
|
1558
|
+
expect(result.alt).toBe(false)
|
|
1559
|
+
expect(result.shift).toBe(false)
|
|
1560
|
+
expect(result.meta).toBe(false)
|
|
1561
|
+
})
|
|
1562
|
+
|
|
1563
|
+
it('maps option to alt', async () => {
|
|
1564
|
+
const { normalizeOpenTUIKey } = await import('@mdxui/terminal')
|
|
1565
|
+
|
|
1566
|
+
const result = normalizeOpenTUIKey({
|
|
1567
|
+
name: 'h',
|
|
1568
|
+
ctrl: false,
|
|
1569
|
+
meta: false,
|
|
1570
|
+
shift: false,
|
|
1571
|
+
option: true,
|
|
1572
|
+
sequence: 'h',
|
|
1573
|
+
})
|
|
1574
|
+
|
|
1575
|
+
expect(result.alt).toBe(true)
|
|
1576
|
+
})
|
|
1577
|
+
|
|
1578
|
+
it('normalizes return to enter', async () => {
|
|
1579
|
+
const { normalizeOpenTUIKey } = await import('@mdxui/terminal')
|
|
1580
|
+
|
|
1581
|
+
const result = normalizeOpenTUIKey({
|
|
1582
|
+
name: 'return',
|
|
1583
|
+
ctrl: false,
|
|
1584
|
+
meta: false,
|
|
1585
|
+
shift: false,
|
|
1586
|
+
option: false,
|
|
1587
|
+
sequence: '\r',
|
|
1588
|
+
})
|
|
1589
|
+
|
|
1590
|
+
expect(result.name).toBe('enter')
|
|
1591
|
+
})
|
|
1592
|
+
|
|
1593
|
+
it('preserves all modifier keys', async () => {
|
|
1594
|
+
const { normalizeOpenTUIKey } = await import('@mdxui/terminal')
|
|
1595
|
+
|
|
1596
|
+
const result = normalizeOpenTUIKey({
|
|
1597
|
+
name: 's',
|
|
1598
|
+
ctrl: true,
|
|
1599
|
+
meta: true,
|
|
1600
|
+
shift: true,
|
|
1601
|
+
option: true,
|
|
1602
|
+
sequence: 's',
|
|
1603
|
+
})
|
|
1604
|
+
|
|
1605
|
+
expect(result.ctrl).toBe(true)
|
|
1606
|
+
expect(result.meta).toBe(true)
|
|
1607
|
+
expect(result.shift).toBe(true)
|
|
1608
|
+
expect(result.alt).toBe(true)
|
|
1609
|
+
})
|
|
1610
|
+
})
|
|
1611
|
+
|
|
1612
|
+
// ============================================================================
|
|
1613
|
+
// createOpenTUIKeyHandler Tests
|
|
1614
|
+
// ============================================================================
|
|
1615
|
+
|
|
1616
|
+
describe('createOpenTUIKeyHandler', () => {
|
|
1617
|
+
it('creates a handler function', async () => {
|
|
1618
|
+
const { createKeyboardManager, createOpenTUIKeyHandler } = await import('@mdxui/terminal')
|
|
1619
|
+
|
|
1620
|
+
const manager = createKeyboardManager({ bindings: {} })
|
|
1621
|
+
const handler = createOpenTUIKeyHandler(manager)
|
|
1622
|
+
|
|
1623
|
+
expect(typeof handler).toBe('function')
|
|
1624
|
+
})
|
|
1625
|
+
|
|
1626
|
+
it('passes key events to manager', async () => {
|
|
1627
|
+
const { createKeyboardManager, createOpenTUIKeyHandler } = await import('@mdxui/terminal')
|
|
1628
|
+
const onAction = vi.fn()
|
|
1629
|
+
|
|
1630
|
+
const manager = createKeyboardManager({
|
|
1631
|
+
bindings: { j: 'move-down' },
|
|
1632
|
+
onAction,
|
|
1633
|
+
})
|
|
1634
|
+
const handler = createOpenTUIKeyHandler(manager)
|
|
1635
|
+
|
|
1636
|
+
handler({
|
|
1637
|
+
name: 'j',
|
|
1638
|
+
ctrl: false,
|
|
1639
|
+
meta: false,
|
|
1640
|
+
shift: false,
|
|
1641
|
+
option: false,
|
|
1642
|
+
sequence: 'j',
|
|
1643
|
+
})
|
|
1644
|
+
|
|
1645
|
+
expect(onAction).toHaveBeenCalledWith('move-down', expect.any(Object))
|
|
1646
|
+
})
|
|
1647
|
+
|
|
1648
|
+
it('skips release events', async () => {
|
|
1649
|
+
const { createKeyboardManager, createOpenTUIKeyHandler } = await import('@mdxui/terminal')
|
|
1650
|
+
const onAction = vi.fn()
|
|
1651
|
+
|
|
1652
|
+
const manager = createKeyboardManager({
|
|
1653
|
+
bindings: { j: 'move-down' },
|
|
1654
|
+
onAction,
|
|
1655
|
+
})
|
|
1656
|
+
const handler = createOpenTUIKeyHandler(manager)
|
|
1657
|
+
|
|
1658
|
+
handler({
|
|
1659
|
+
name: 'j',
|
|
1660
|
+
ctrl: false,
|
|
1661
|
+
meta: false,
|
|
1662
|
+
shift: false,
|
|
1663
|
+
option: false,
|
|
1664
|
+
sequence: 'j',
|
|
1665
|
+
eventType: 'release',
|
|
1666
|
+
})
|
|
1667
|
+
|
|
1668
|
+
expect(onAction).not.toHaveBeenCalled()
|
|
1669
|
+
})
|
|
1670
|
+
|
|
1671
|
+
it('handles modifier keys', async () => {
|
|
1672
|
+
const { createKeyboardManager, createOpenTUIKeyHandler } = await import('@mdxui/terminal')
|
|
1673
|
+
const onAction = vi.fn()
|
|
1674
|
+
|
|
1675
|
+
const manager = createKeyboardManager({
|
|
1676
|
+
bindings: { 'ctrl+c': 'cancel' },
|
|
1677
|
+
onAction,
|
|
1678
|
+
})
|
|
1679
|
+
const handler = createOpenTUIKeyHandler(manager)
|
|
1680
|
+
|
|
1681
|
+
handler({
|
|
1682
|
+
name: 'c',
|
|
1683
|
+
ctrl: true,
|
|
1684
|
+
meta: false,
|
|
1685
|
+
shift: false,
|
|
1686
|
+
option: false,
|
|
1687
|
+
sequence: '\x03',
|
|
1688
|
+
})
|
|
1689
|
+
|
|
1690
|
+
expect(onAction).toHaveBeenCalledWith('cancel', expect.any(Object))
|
|
1691
|
+
})
|
|
1692
|
+
})
|
|
1693
|
+
|
|
1694
|
+
// ============================================================================
|
|
1695
|
+
// attachKeyboardManager Tests
|
|
1696
|
+
// ============================================================================
|
|
1697
|
+
|
|
1698
|
+
describe('attachKeyboardManager', () => {
|
|
1699
|
+
it('exports attachKeyboardManager function', async () => {
|
|
1700
|
+
const { attachKeyboardManager } = await import('@mdxui/terminal')
|
|
1701
|
+
|
|
1702
|
+
expect(typeof attachKeyboardManager).toBe('function')
|
|
1703
|
+
})
|
|
1704
|
+
|
|
1705
|
+
it('throws error for non-TTY stdin', async () => {
|
|
1706
|
+
const { createKeyboardManager, attachKeyboardManager } = await import('@mdxui/terminal')
|
|
1707
|
+
|
|
1708
|
+
const manager = createKeyboardManager({ bindings: {} })
|
|
1709
|
+
|
|
1710
|
+
// Create a mock non-TTY stream
|
|
1711
|
+
const mockStream = {
|
|
1712
|
+
isTTY: false,
|
|
1713
|
+
} as NodeJS.ReadStream
|
|
1714
|
+
|
|
1715
|
+
expect(() => attachKeyboardManager(manager, { input: mockStream })).toThrow('requires a TTY stdin')
|
|
1716
|
+
})
|
|
1717
|
+
})
|
|
1718
|
+
|
|
1719
|
+
// ============================================================================
|
|
1720
|
+
// Type Exports for Terminal Input
|
|
1721
|
+
// ============================================================================
|
|
1722
|
+
|
|
1723
|
+
describe('terminal input type exports', () => {
|
|
1724
|
+
it('exports NormalizedKey type', async () => {
|
|
1725
|
+
// Type check - if this compiles, the type is exported
|
|
1726
|
+
const { normalizeReadlineKey } = await import('@mdxui/terminal')
|
|
1727
|
+
|
|
1728
|
+
const key = normalizeReadlineKey('a', undefined)
|
|
1729
|
+
|
|
1730
|
+
// Verify structure matches NormalizedKey
|
|
1731
|
+
expect('name' in key).toBe(true)
|
|
1732
|
+
expect('ctrl' in key).toBe(true)
|
|
1733
|
+
expect('alt' in key).toBe(true)
|
|
1734
|
+
expect('shift' in key).toBe(true)
|
|
1735
|
+
expect('meta' in key).toBe(true)
|
|
1736
|
+
})
|
|
1737
|
+
|
|
1738
|
+
it('exports ReadlineKey type', async () => {
|
|
1739
|
+
const { normalizeReadlineKey } = await import('@mdxui/terminal')
|
|
1740
|
+
|
|
1741
|
+
// ReadlineKey is accepted as parameter type
|
|
1742
|
+
const key: { name: string; ctrl: boolean } = { name: 'a', ctrl: false }
|
|
1743
|
+
const result = normalizeReadlineKey('a', key)
|
|
1744
|
+
|
|
1745
|
+
expect(result).toBeDefined()
|
|
1746
|
+
})
|
|
1747
|
+
|
|
1748
|
+
it('exports OpenTUIKeyEvent type', async () => {
|
|
1749
|
+
const { createOpenTUIKeyHandler, createKeyboardManager } = await import('@mdxui/terminal')
|
|
1750
|
+
|
|
1751
|
+
const manager = createKeyboardManager({ bindings: {} })
|
|
1752
|
+
const handler = createOpenTUIKeyHandler(manager)
|
|
1753
|
+
|
|
1754
|
+
// OpenTUIKeyEvent structure is accepted
|
|
1755
|
+
handler({
|
|
1756
|
+
name: 'a',
|
|
1757
|
+
ctrl: false,
|
|
1758
|
+
meta: false,
|
|
1759
|
+
shift: false,
|
|
1760
|
+
option: false,
|
|
1761
|
+
sequence: 'a',
|
|
1762
|
+
})
|
|
1763
|
+
})
|
|
1764
|
+
|
|
1765
|
+
it('exports AttachKeyboardOptions type', async () => {
|
|
1766
|
+
// Type check - if this compiles with the options, the type works
|
|
1767
|
+
const { attachKeyboardManager, createKeyboardManager } = await import('@mdxui/terminal')
|
|
1768
|
+
|
|
1769
|
+
const manager = createKeyboardManager({ bindings: {} })
|
|
1770
|
+
|
|
1771
|
+
// This should type-check even though we can't run it without a TTY
|
|
1772
|
+
const options = { exitOnCtrlC: false }
|
|
1773
|
+
|
|
1774
|
+
// Just verify the options shape is correct
|
|
1775
|
+
expect(options.exitOnCtrlC).toBe(false)
|
|
1776
|
+
})
|
|
1777
|
+
|
|
1778
|
+
it('exports DetachKeyboard type', async () => {
|
|
1779
|
+
const { createOpenTUIKeyHandler, createKeyboardManager } = await import('@mdxui/terminal')
|
|
1780
|
+
|
|
1781
|
+
// DetachKeyboard is a function type
|
|
1782
|
+
// We can't actually test attachKeyboardManager without a TTY,
|
|
1783
|
+
// but we can verify the return type concept works
|
|
1784
|
+
const manager = createKeyboardManager({ bindings: {} })
|
|
1785
|
+
|
|
1786
|
+
// A DetachKeyboard function is just () => void
|
|
1787
|
+
const detach: () => void = () => {}
|
|
1788
|
+
|
|
1789
|
+
expect(typeof detach).toBe('function')
|
|
1790
|
+
})
|
|
1791
|
+
})
|