@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.
Files changed (191) hide show
  1. package/README.md +571 -0
  2. package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
  3. package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
  4. package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
  5. package/dist/chunk-3EFDH7PK.js +5235 -0
  6. package/dist/chunk-3RG5ZIWI.js +10 -0
  7. package/dist/chunk-3X5IR6WE.js +884 -0
  8. package/dist/chunk-4FV5ZDCE.js +5236 -0
  9. package/dist/chunk-4OVMSF2J.js +243 -0
  10. package/dist/chunk-63FEETIS.js +4048 -0
  11. package/dist/chunk-B43KP7XJ.js +884 -0
  12. package/dist/chunk-BMTJXWUV.js +655 -0
  13. package/dist/chunk-C3SVH4N7.js +882 -0
  14. package/dist/chunk-EVWR7Y47.js +874 -0
  15. package/dist/chunk-F6A5VWUC.js +1285 -0
  16. package/dist/chunk-FD7KW7GE.js +882 -0
  17. package/dist/chunk-GBQ6UD6I.js +655 -0
  18. package/dist/chunk-GMDD3M6U.js +5227 -0
  19. package/dist/chunk-JBHRXOXM.js +1058 -0
  20. package/dist/chunk-JFOO3EYO.js +1182 -0
  21. package/dist/chunk-JQ5H3WXL.js +1291 -0
  22. package/dist/chunk-JQD5NASE.js +234 -0
  23. package/dist/chunk-KRHJP5R7.js +592 -0
  24. package/dist/chunk-KWF6WVJE.js +962 -0
  25. package/dist/chunk-LHYQVN3H.js +1038 -0
  26. package/dist/chunk-M3TLQLGC.js +1032 -0
  27. package/dist/chunk-MVW4Q5OP.js +240 -0
  28. package/dist/chunk-NXCZSWLU.js +1294 -0
  29. package/dist/chunk-O25TNRO6.js +607 -0
  30. package/dist/chunk-PNECDA2I.js +884 -0
  31. package/dist/chunk-QIHWRLJR.js +962 -0
  32. package/dist/chunk-QW5YMQ7K.js +882 -0
  33. package/dist/chunk-R5U7XKVJ.js +16 -0
  34. package/dist/chunk-RP2MVQLR.js +962 -0
  35. package/dist/chunk-TP6RXGXA.js +1087 -0
  36. package/dist/chunk-TQQSTITZ.js +655 -0
  37. package/dist/chunk-X24GWXQV.js +1281 -0
  38. package/dist/components/index.d.ts +802 -0
  39. package/dist/components/index.js +149 -0
  40. package/dist/data/index.d.ts +2554 -0
  41. package/dist/data/index.js +51 -0
  42. package/dist/forms/index.d.ts +1596 -0
  43. package/dist/forms/index.js +464 -0
  44. package/dist/index-CQRFZntR.d.ts +867 -0
  45. package/dist/index.d.ts +579 -0
  46. package/dist/index.js +786 -0
  47. package/dist/interactive-D0JkWosD.d.ts +217 -0
  48. package/dist/keyboard/index.d.ts +2 -0
  49. package/dist/keyboard/index.js +43 -0
  50. package/dist/renderers/index.d.ts +546 -0
  51. package/dist/renderers/index.js +2157 -0
  52. package/dist/storybook/index.d.ts +396 -0
  53. package/dist/storybook/index.js +641 -0
  54. package/dist/theme/index.d.ts +1339 -0
  55. package/dist/theme/index.js +123 -0
  56. package/dist/types-Bxu5PAgA.d.ts +710 -0
  57. package/dist/types-CIlop5Ji.d.ts +701 -0
  58. package/dist/types-Ca8p_p5X.d.ts +710 -0
  59. package/package.json +90 -0
  60. package/src/__tests__/components/data/card.test.ts +458 -0
  61. package/src/__tests__/components/data/list.test.ts +473 -0
  62. package/src/__tests__/components/data/metrics.test.ts +541 -0
  63. package/src/__tests__/components/data/table.test.ts +448 -0
  64. package/src/__tests__/components/input/field.test.ts +555 -0
  65. package/src/__tests__/components/input/form.test.ts +870 -0
  66. package/src/__tests__/components/input/search.test.ts +1238 -0
  67. package/src/__tests__/components/input/select.test.ts +658 -0
  68. package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
  69. package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
  70. package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
  71. package/src/__tests__/components/navigation/tabs.test.ts +995 -0
  72. package/src/__tests__/components.test.tsx +1197 -0
  73. package/src/__tests__/core/compiler.test.ts +986 -0
  74. package/src/__tests__/core/parser.test.ts +785 -0
  75. package/src/__tests__/core/tier-switcher.test.ts +1103 -0
  76. package/src/__tests__/core/types.test.ts +1398 -0
  77. package/src/__tests__/data/collections.test.ts +1337 -0
  78. package/src/__tests__/data/db.test.ts +1265 -0
  79. package/src/__tests__/data/reactive.test.ts +1010 -0
  80. package/src/__tests__/data/sync.test.ts +1614 -0
  81. package/src/__tests__/errors.test.ts +660 -0
  82. package/src/__tests__/forms/integration.test.ts +444 -0
  83. package/src/__tests__/integration.test.ts +905 -0
  84. package/src/__tests__/keyboard.test.ts +1791 -0
  85. package/src/__tests__/renderer.test.ts +489 -0
  86. package/src/__tests__/renderers/ansi-css.test.ts +948 -0
  87. package/src/__tests__/renderers/ansi.test.ts +1366 -0
  88. package/src/__tests__/renderers/ascii.test.ts +1360 -0
  89. package/src/__tests__/renderers/interactive.test.ts +2353 -0
  90. package/src/__tests__/renderers/markdown.test.ts +1483 -0
  91. package/src/__tests__/renderers/text.test.ts +1369 -0
  92. package/src/__tests__/renderers/unicode.test.ts +1307 -0
  93. package/src/__tests__/theme.test.ts +639 -0
  94. package/src/__tests__/utils/assertions.ts +685 -0
  95. package/src/__tests__/utils/index.ts +115 -0
  96. package/src/__tests__/utils/test-renderer.ts +381 -0
  97. package/src/__tests__/utils/utils.test.ts +560 -0
  98. package/src/components/containers/card.ts +56 -0
  99. package/src/components/containers/dialog.ts +53 -0
  100. package/src/components/containers/index.ts +9 -0
  101. package/src/components/containers/panel.ts +59 -0
  102. package/src/components/feedback/badge.ts +40 -0
  103. package/src/components/feedback/index.ts +8 -0
  104. package/src/components/feedback/spinner.ts +23 -0
  105. package/src/components/helpers.ts +81 -0
  106. package/src/components/index.ts +153 -0
  107. package/src/components/layout/breadcrumb.ts +31 -0
  108. package/src/components/layout/index.ts +10 -0
  109. package/src/components/layout/list.ts +29 -0
  110. package/src/components/layout/sidebar.ts +79 -0
  111. package/src/components/layout/table.ts +62 -0
  112. package/src/components/primitives/box.ts +95 -0
  113. package/src/components/primitives/button.ts +54 -0
  114. package/src/components/primitives/index.ts +11 -0
  115. package/src/components/primitives/input.ts +88 -0
  116. package/src/components/primitives/select.ts +97 -0
  117. package/src/components/primitives/text.ts +60 -0
  118. package/src/components/render.ts +155 -0
  119. package/src/components/templates/app.ts +43 -0
  120. package/src/components/templates/index.ts +8 -0
  121. package/src/components/templates/site.ts +54 -0
  122. package/src/components/types.ts +777 -0
  123. package/src/core/compiler.ts +718 -0
  124. package/src/core/parser.ts +127 -0
  125. package/src/core/tier-switcher.ts +607 -0
  126. package/src/core/types.ts +672 -0
  127. package/src/data/collection.ts +316 -0
  128. package/src/data/collections.ts +50 -0
  129. package/src/data/context.tsx +174 -0
  130. package/src/data/db.ts +127 -0
  131. package/src/data/hooks.ts +532 -0
  132. package/src/data/index.ts +138 -0
  133. package/src/data/reactive.ts +1225 -0
  134. package/src/data/saas-collections.ts +375 -0
  135. package/src/data/sync.ts +1213 -0
  136. package/src/data/types.ts +660 -0
  137. package/src/forms/converters.ts +512 -0
  138. package/src/forms/index.ts +133 -0
  139. package/src/forms/schemas.ts +403 -0
  140. package/src/forms/types.ts +476 -0
  141. package/src/index.ts +542 -0
  142. package/src/keyboard/focus.ts +748 -0
  143. package/src/keyboard/index.ts +96 -0
  144. package/src/keyboard/integration.ts +371 -0
  145. package/src/keyboard/manager.ts +377 -0
  146. package/src/keyboard/presets.ts +90 -0
  147. package/src/renderers/ansi-css.ts +576 -0
  148. package/src/renderers/ansi.ts +802 -0
  149. package/src/renderers/ascii.ts +680 -0
  150. package/src/renderers/breadcrumb.ts +480 -0
  151. package/src/renderers/command-palette.ts +802 -0
  152. package/src/renderers/components/field.ts +210 -0
  153. package/src/renderers/components/form.ts +327 -0
  154. package/src/renderers/components/index.ts +21 -0
  155. package/src/renderers/components/search.ts +449 -0
  156. package/src/renderers/components/select.ts +222 -0
  157. package/src/renderers/index.ts +101 -0
  158. package/src/renderers/interactive/component-handlers.ts +622 -0
  159. package/src/renderers/interactive/cursor-manager.ts +147 -0
  160. package/src/renderers/interactive/focus-manager.ts +279 -0
  161. package/src/renderers/interactive/index.ts +661 -0
  162. package/src/renderers/interactive/input-handler.ts +164 -0
  163. package/src/renderers/interactive/keyboard-handler.ts +212 -0
  164. package/src/renderers/interactive/mouse-handler.ts +167 -0
  165. package/src/renderers/interactive/state-manager.ts +109 -0
  166. package/src/renderers/interactive/types.ts +338 -0
  167. package/src/renderers/interactive-string.ts +299 -0
  168. package/src/renderers/interactive.ts +59 -0
  169. package/src/renderers/markdown.ts +950 -0
  170. package/src/renderers/sidebar.ts +549 -0
  171. package/src/renderers/tabs.ts +682 -0
  172. package/src/renderers/text.ts +791 -0
  173. package/src/renderers/unicode.ts +917 -0
  174. package/src/renderers/utils.ts +942 -0
  175. package/src/router/adapters.ts +383 -0
  176. package/src/router/types.ts +140 -0
  177. package/src/router/utils.ts +452 -0
  178. package/src/schemas.ts +205 -0
  179. package/src/storybook/index.ts +91 -0
  180. package/src/storybook/interactive-decorator.tsx +659 -0
  181. package/src/storybook/keyboard-simulator.ts +501 -0
  182. package/src/theme/ansi-codes.ts +80 -0
  183. package/src/theme/box-drawing.ts +132 -0
  184. package/src/theme/color-convert.ts +254 -0
  185. package/src/theme/color-support.ts +321 -0
  186. package/src/theme/index.ts +134 -0
  187. package/src/theme/strip-ansi.ts +50 -0
  188. package/src/theme/tailwind-map.ts +469 -0
  189. package/src/theme/text-styles.ts +206 -0
  190. package/src/theme/theme-system.ts +568 -0
  191. package/src/types.ts +103 -0
@@ -0,0 +1,905 @@
1
+ /**
2
+ * @mdxui/terminal Integration Tests
3
+ *
4
+ * TDD RED Phase: End-to-end integration tests for the terminal render pipeline.
5
+ * These tests verify that the full system works together correctly:
6
+ * - Components render to string output
7
+ * - Nested components compose properly
8
+ * - Theme applies across component trees
9
+ * - Focus state affects rendering
10
+ * - Keyboard input updates component state
11
+ * - Terminal capabilities are respected
12
+ *
13
+ * All tests should initially define contracts that the implementation must fulfill.
14
+ *
15
+ * NOTE: React hooks (useFocus, useNavigableList, etc.) cannot be tested outside
16
+ * of a React component context. Those tests use the keyboard manager and other
17
+ * non-hook APIs to verify integration behavior.
18
+ */
19
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
20
+ import React from 'react'
21
+
22
+ // ============================================================================
23
+ // Test Utilities
24
+ // ============================================================================
25
+
26
+ /**
27
+ * Simulates rendering a component to terminal strings.
28
+ */
29
+ async function renderToTerminal(element: React.ReactElement): Promise<string[]> {
30
+ const { renderComponent } = await import('../components')
31
+
32
+ if (typeof renderComponent !== 'function') {
33
+ throw new Error('renderComponent function not exported from @mdxui/terminal/components')
34
+ }
35
+
36
+ return renderComponent(element)
37
+ }
38
+
39
+ /**
40
+ * Check if a string contains ANSI escape codes
41
+ */
42
+ function hasAnsiCodes(str: string): boolean {
43
+ // eslint-disable-next-line no-control-regex
44
+ return /\x1b\[[\d;]*m/.test(str)
45
+ }
46
+
47
+ /**
48
+ * Strip ANSI codes from string for content testing
49
+ */
50
+ function stripAnsi(str: string): string {
51
+ // eslint-disable-next-line no-control-regex
52
+ return str.replace(/\x1b\[[\d;]*m/g, '')
53
+ }
54
+
55
+ /**
56
+ * Join and strip ANSI from rendered lines
57
+ */
58
+ function getPlainContent(lines: string[]): string {
59
+ return stripAnsi(lines.join('\n'))
60
+ }
61
+
62
+ // ============================================================================
63
+ // 1. Full Render Pipeline Tests
64
+ // ============================================================================
65
+
66
+ describe('Integration', () => {
67
+ describe('Full Render Pipeline', () => {
68
+ beforeEach(() => {
69
+ vi.resetModules()
70
+ })
71
+
72
+ it('TerminalApp renders to string output', async () => {
73
+ const { TerminalApp, Text } = await import('..')
74
+
75
+ const element = React.createElement(
76
+ TerminalApp,
77
+ { children: React.createElement(Text, {}, 'Hello Terminal') }
78
+ )
79
+
80
+ const lines = await renderToTerminal(element)
81
+
82
+ // Should produce at least one line of output
83
+ expect(lines.length).toBeGreaterThan(0)
84
+
85
+ // Should contain the text content
86
+ const content = getPlainContent(lines)
87
+ expect(content).toContain('Hello Terminal')
88
+ })
89
+
90
+ it('nested components render correctly', async () => {
91
+ const { Box, Text } = await import('../components')
92
+
93
+ // Create nested structure: Box > Box > Text
94
+ const innerText = React.createElement(Text, { color: 'cyan' }, 'Inner content')
95
+ const innerBox = React.createElement(Box, { border: 'single', padding: 1 }, innerText)
96
+ const outerBox = React.createElement(Box, { border: 'double', padding: 1 }, innerBox)
97
+
98
+ const lines = await renderToTerminal(outerBox)
99
+
100
+ const content = getPlainContent(lines)
101
+
102
+ // Should contain the nested text
103
+ expect(content).toContain('Inner content')
104
+
105
+ // Should have double border characters from outer box
106
+ expect(lines.join('')).toMatch(/[\u2550\u2554\u2557\u255A\u255D]/)
107
+
108
+ // Should have single border characters from inner box
109
+ expect(lines.join('')).toMatch(/[\u2500\u250C\u2510\u2514\u2518]/)
110
+ })
111
+
112
+ it('theme applies to all children', async () => {
113
+ const { Box, Text } = await import('../components')
114
+
115
+ // Test that styled text renders with ANSI codes
116
+ const textElement = React.createElement(Text, { color: 'red', bold: true }, 'Styled text')
117
+ const boxElement = React.createElement(
118
+ Box,
119
+ { border: 'single' },
120
+ textElement
121
+ )
122
+
123
+ const lines = await renderToTerminal(boxElement)
124
+
125
+ // Output should contain ANSI codes (theme colors applied)
126
+ const output = lines.join('')
127
+ expect(hasAnsiCodes(output)).toBe(true)
128
+
129
+ // Content should still be present
130
+ expect(getPlainContent(lines)).toContain('Styled text')
131
+ })
132
+
133
+ it('focus state affects rendering', async () => {
134
+ const { Box, Input } = await import('../components')
135
+
136
+ // Unfocused input
137
+ const unfocusedInput = React.createElement(Input, {
138
+ value: 'test',
139
+ focused: false,
140
+ onChange: () => {},
141
+ })
142
+ const unfocusedLines = await renderToTerminal(
143
+ React.createElement(Box, {}, unfocusedInput)
144
+ )
145
+
146
+ // Focused input
147
+ const focusedInput = React.createElement(Input, {
148
+ value: 'test',
149
+ focused: true,
150
+ onChange: () => {},
151
+ })
152
+ const focusedLines = await renderToTerminal(
153
+ React.createElement(Box, {}, focusedInput)
154
+ )
155
+
156
+ // Focused input should have different styling (more ANSI codes for cursor/highlight)
157
+ const unfocusedOutput = unfocusedLines.join('')
158
+ const focusedOutput = focusedLines.join('')
159
+
160
+ // Both should contain the value
161
+ expect(stripAnsi(unfocusedOutput)).toContain('test')
162
+ expect(stripAnsi(focusedOutput)).toContain('test')
163
+
164
+ // Focused should have cursor indicator (inverse video or underline)
165
+ expect(focusedOutput).toMatch(/\x1b\[7m|\x1b\[4m/)
166
+ })
167
+ })
168
+
169
+ // ============================================================================
170
+ // 2. Keyboard Integration Tests
171
+ // ============================================================================
172
+
173
+ describe('Keyboard Integration', () => {
174
+ beforeEach(() => {
175
+ vi.resetModules()
176
+ })
177
+
178
+ it('key press updates component state', async () => {
179
+ const { createKeyboardManager, VIM_BINDINGS } = await import('..')
180
+
181
+ // Track state changes
182
+ let currentIndex = 0
183
+ const onAction = vi.fn((action: string) => {
184
+ if (action === 'move-down') currentIndex++
185
+ if (action === 'move-up') currentIndex--
186
+ })
187
+
188
+ const manager = createKeyboardManager({
189
+ bindings: VIM_BINDINGS,
190
+ onAction,
191
+ })
192
+
193
+ // Simulate key presses
194
+ manager.handleKey('j') // move down
195
+ expect(currentIndex).toBe(1)
196
+ expect(onAction).toHaveBeenCalledWith('move-down', expect.any(Object))
197
+
198
+ manager.handleKey('j') // move down
199
+ expect(currentIndex).toBe(2)
200
+
201
+ manager.handleKey('k') // move up
202
+ expect(currentIndex).toBe(1)
203
+ expect(onAction).toHaveBeenCalledWith('move-up', expect.any(Object))
204
+ })
205
+
206
+ it('focus changes update rendering via props', async () => {
207
+ const { Input } = await import('../components')
208
+
209
+ // Test that focus prop changes rendering
210
+ const unfocusedInput = React.createElement(Input, {
211
+ value: 'test',
212
+ focused: false,
213
+ onChange: () => {},
214
+ })
215
+ const unfocusedLines = await renderToTerminal(unfocusedInput)
216
+
217
+ const focusedInput = React.createElement(Input, {
218
+ value: 'test',
219
+ focused: true,
220
+ onChange: () => {},
221
+ })
222
+ const focusedLines = await renderToTerminal(focusedInput)
223
+
224
+ // Both should contain the value
225
+ expect(getPlainContent(unfocusedLines)).toContain('test')
226
+ expect(getPlainContent(focusedLines)).toContain('test')
227
+
228
+ // Focused state should produce different output
229
+ const unfocusedOutput = unfocusedLines.join('')
230
+ const focusedOutput = focusedLines.join('')
231
+
232
+ // Focused input should have cursor indicator (inverse video or underline)
233
+ expect(focusedOutput).toMatch(/\x1b\[7m|\x1b\[4m/)
234
+ })
235
+
236
+ it('navigation state tracked via keyboard manager', async () => {
237
+ const { createKeyboardManager, VIM_BINDINGS } = await import('..')
238
+
239
+ // Simulate navigable list state management
240
+ const items = ['Apple', 'Banana', 'Cherry', 'Date']
241
+ let currentIndex = 0
242
+
243
+ const onAction = vi.fn((action: string) => {
244
+ if (action === 'move-down') {
245
+ currentIndex = Math.min(currentIndex + 1, items.length - 1)
246
+ }
247
+ if (action === 'move-up') {
248
+ currentIndex = Math.max(currentIndex - 1, 0)
249
+ }
250
+ if (action === 'move-first') {
251
+ currentIndex = 0
252
+ }
253
+ if (action === 'move-last') {
254
+ currentIndex = items.length - 1
255
+ }
256
+ })
257
+
258
+ const manager = createKeyboardManager({
259
+ bindings: VIM_BINDINGS,
260
+ onAction,
261
+ })
262
+
263
+ // Initially at first item
264
+ expect(currentIndex).toBe(0)
265
+ expect(items[currentIndex]).toBe('Apple')
266
+
267
+ // Move down
268
+ manager.handleKey('j')
269
+ expect(currentIndex).toBe(1)
270
+ expect(items[currentIndex]).toBe('Banana')
271
+
272
+ // Move to last (gg sequence for move-first, G for move-last)
273
+ manager.handleKey('G')
274
+ expect(currentIndex).toBe(3)
275
+ expect(items[currentIndex]).toBe('Date')
276
+
277
+ // Move to first
278
+ manager.handleKey('g')
279
+ manager.handleKey('g')
280
+ expect(currentIndex).toBe(0)
281
+ expect(items[currentIndex]).toBe('Apple')
282
+ })
283
+
284
+ it('actions trigger callbacks', async () => {
285
+ const { createKeyboardManager } = await import('..')
286
+
287
+ const onSelect = vi.fn()
288
+ const onBack = vi.fn()
289
+ const onQuit = vi.fn()
290
+
291
+ const manager = createKeyboardManager({
292
+ bindings: {
293
+ enter: 'select',
294
+ escape: 'back',
295
+ q: 'quit',
296
+ },
297
+ onAction: (action) => {
298
+ if (action === 'select') onSelect()
299
+ if (action === 'back') onBack()
300
+ if (action === 'quit') onQuit()
301
+ },
302
+ })
303
+
304
+ manager.handleKey('enter')
305
+ expect(onSelect).toHaveBeenCalled()
306
+
307
+ manager.handleKey('escape')
308
+ expect(onBack).toHaveBeenCalled()
309
+
310
+ manager.handleKey('q')
311
+ expect(onQuit).toHaveBeenCalled()
312
+ })
313
+ })
314
+
315
+ // ============================================================================
316
+ // 3. Real Components Tests
317
+ // ============================================================================
318
+
319
+ describe('Real Components', () => {
320
+ beforeEach(() => {
321
+ vi.resetModules()
322
+ })
323
+
324
+ it('dashboard with sidebar and content renders', async () => {
325
+ const { Box, SidebarItem, Text } = await import('../components')
326
+
327
+ // Build a dashboard layout - render sidebar items individually to verify they work
328
+ const sidebarItem1 = React.createElement(SidebarItem, { label: 'Dashboard', icon: '\u2302', active: true })
329
+ const sidebarItem2 = React.createElement(SidebarItem, { label: 'Settings', icon: '\u2699' })
330
+
331
+ const lines1 = await renderToTerminal(sidebarItem1)
332
+ const lines2 = await renderToTerminal(sidebarItem2)
333
+
334
+ // Each sidebar item should render its label
335
+ expect(getPlainContent(lines1)).toContain('Dashboard')
336
+ expect(getPlainContent(lines2)).toContain('Settings')
337
+
338
+ // Now test the main content area
339
+ const content = React.createElement(
340
+ Box,
341
+ { flexDirection: 'column' },
342
+ React.createElement(Text, { bold: true }, 'Welcome to Dashboard'),
343
+ React.createElement(Text, {}, 'Select an option from the sidebar')
344
+ )
345
+
346
+ const contentLines = await renderToTerminal(content)
347
+ const plainContent = getPlainContent(contentLines)
348
+
349
+ // Should contain main content
350
+ expect(plainContent).toContain('Welcome to Dashboard')
351
+ expect(plainContent).toContain('Select an option from the sidebar')
352
+ })
353
+
354
+ it('form with inputs and buttons renders', async () => {
355
+ const { Input, Button, Text } = await import('../components')
356
+
357
+ // Test individual form components to verify they render correctly
358
+ const title = React.createElement(Text, { bold: true }, 'Login Form')
359
+ const titleLines = await renderToTerminal(title)
360
+ expect(getPlainContent(titleLines)).toContain('Login Form')
361
+
362
+ // Test input with label
363
+ const usernameInput = React.createElement(Input, {
364
+ label: 'Username',
365
+ value: '',
366
+ placeholder: 'Enter username',
367
+ onChange: () => {},
368
+ })
369
+ const inputLines = await renderToTerminal(usernameInput)
370
+ const inputContent = getPlainContent(inputLines)
371
+ // Input should contain label and/or placeholder
372
+ expect(inputContent).toMatch(/Username|Enter username/)
373
+
374
+ // Test password input (masked)
375
+ const passwordInput = React.createElement(Input, {
376
+ value: 'secret123',
377
+ type: 'password',
378
+ onChange: () => {},
379
+ })
380
+ const passwordLines = await renderToTerminal(passwordInput)
381
+ const passwordContent = getPlainContent(passwordLines)
382
+ // Password should be masked (not showing actual value)
383
+ expect(passwordContent).not.toContain('secret123')
384
+
385
+ // Test buttons
386
+ const loginButton = React.createElement(Button, { variant: 'primary', onPress: () => {} }, 'Login')
387
+ const cancelButton = React.createElement(Button, { variant: 'default', onPress: () => {} }, 'Cancel')
388
+
389
+ const loginLines = await renderToTerminal(loginButton)
390
+ const cancelLines = await renderToTerminal(cancelButton)
391
+
392
+ expect(getPlainContent(loginLines)).toContain('Login')
393
+ expect(getPlainContent(cancelLines)).toContain('Cancel')
394
+ })
395
+
396
+ it('table with data renders', async () => {
397
+ const { Table } = await import('../components')
398
+
399
+ interface User {
400
+ name: string
401
+ email: string
402
+ role: string
403
+ }
404
+
405
+ const data: User[] = [
406
+ { name: 'Alice', email: 'alice@example.com', role: 'Admin' },
407
+ { name: 'Bob', email: 'bob@example.com', role: 'User' },
408
+ { name: 'Charlie', email: 'charlie@example.com', role: 'User' },
409
+ ]
410
+
411
+ const columns = [
412
+ { key: 'name' as const, header: 'Name', width: 15 },
413
+ { key: 'email' as const, header: 'Email', width: 25 },
414
+ { key: 'role' as const, header: 'Role', width: 10 },
415
+ ]
416
+
417
+ const table = React.createElement(Table, { data, columns })
418
+
419
+ const lines = await renderToTerminal(table)
420
+ const plainContent = getPlainContent(lines)
421
+
422
+ // Should contain headers
423
+ expect(plainContent).toContain('Name')
424
+ expect(plainContent).toContain('Email')
425
+ expect(plainContent).toContain('Role')
426
+
427
+ // Should contain data
428
+ expect(plainContent).toContain('Alice')
429
+ expect(plainContent).toContain('alice@example.com')
430
+ expect(plainContent).toContain('Admin')
431
+ expect(plainContent).toContain('Bob')
432
+ expect(plainContent).toContain('Charlie')
433
+ })
434
+
435
+ it('dialog modal renders', async () => {
436
+ const { Dialog, Box, Text, Button } = await import('../components')
437
+
438
+ const dialogContent = React.createElement(
439
+ Box,
440
+ { flexDirection: 'column', gap: 1 },
441
+ React.createElement(Text, {}, 'Are you sure you want to delete this item?'),
442
+ React.createElement(
443
+ Box,
444
+ { flexDirection: 'row', gap: 2 },
445
+ React.createElement(Button, { variant: 'destructive', onPress: () => {} }, 'Delete'),
446
+ React.createElement(Button, { variant: 'default', onPress: () => {} }, 'Cancel')
447
+ )
448
+ )
449
+
450
+ const dialog = React.createElement(Dialog, {
451
+ open: true,
452
+ title: 'Confirm Deletion',
453
+ onClose: () => {},
454
+ children: dialogContent,
455
+ })
456
+
457
+ const lines = await renderToTerminal(dialog)
458
+ const plainContent = getPlainContent(lines)
459
+
460
+ // Should contain dialog title
461
+ expect(plainContent).toContain('Confirm Deletion')
462
+
463
+ // Should contain dialog content
464
+ expect(plainContent).toContain('Are you sure you want to delete this item?')
465
+ expect(plainContent).toContain('Delete')
466
+ expect(plainContent).toContain('Cancel')
467
+
468
+ // Should have modal styling (border)
469
+ const output = lines.join('')
470
+ expect(output).toMatch(/[\u250C\u2554\u256D]/) // top-left border corners
471
+ })
472
+ })
473
+
474
+ // ============================================================================
475
+ // 4. Terminal Capabilities Tests
476
+ // ============================================================================
477
+
478
+ describe('Terminal Capabilities', () => {
479
+ beforeEach(() => {
480
+ vi.resetModules()
481
+ })
482
+
483
+ it('color degradation works (256 to 16)', async () => {
484
+ const { degradeColor } = await import('../theme')
485
+
486
+ // ANSI 256 blue (color 33)
487
+ const color256 = '\x1b[38;5;33m'
488
+
489
+ // Degrade to 16 colors
490
+ const color16 = degradeColor(color256, '16')
491
+
492
+ // Should be a basic 16-color code (30-37 or 90-97)
493
+ expect(color16).toMatch(/\x1b\[3[0-7]m|\x1b\[9[0-7]m/)
494
+
495
+ // Should not be empty
496
+ expect(color16.length).toBeGreaterThan(0)
497
+ })
498
+
499
+ it('color degradation returns empty for none support', async () => {
500
+ const { degradeColor } = await import('../theme')
501
+
502
+ const color256 = '\x1b[38;5;33m'
503
+ const colorNone = degradeColor(color256, 'none')
504
+
505
+ // Should return empty string for no color support
506
+ expect(colorNone).toBe('')
507
+ })
508
+
509
+ it('truecolor to 256 conversion works', async () => {
510
+ const { degradeColor } = await import('../theme')
511
+
512
+ // Truecolor blue
513
+ const truecolor = '\x1b[38;2;59;130;246m'
514
+
515
+ // Degrade to 256 colors
516
+ const color256 = degradeColor(truecolor, '256')
517
+
518
+ // Should be a valid 256-color code
519
+ expect(color256).toMatch(/\x1b\[38;5;\d+m/)
520
+ })
521
+
522
+ it('size constraints respected', async () => {
523
+ const { Box, Text } = await import('../components')
524
+
525
+ // Create a box with fixed dimensions
526
+ const constrainedBox = React.createElement(
527
+ Box,
528
+ { width: 30, height: 5, border: 'single' },
529
+ React.createElement(Text, {}, 'Short content')
530
+ )
531
+
532
+ const lines = await renderToTerminal(constrainedBox)
533
+
534
+ // Should have exactly 5 lines (the specified height)
535
+ expect(lines.length).toBe(5)
536
+
537
+ // Content should be within the box
538
+ const content = getPlainContent(lines)
539
+ expect(content).toContain('Short content')
540
+ })
541
+
542
+ it('ANSI codes properly formatted in output', async () => {
543
+ const { Text } = await import('../components')
544
+
545
+ // Create styled text
546
+ const styledText = React.createElement(
547
+ Text,
548
+ { bold: true, color: 'red', underline: true },
549
+ 'Styled content'
550
+ )
551
+
552
+ const lines = await renderToTerminal(styledText)
553
+ const output = lines.join('')
554
+
555
+ // Should contain bold ANSI code
556
+ expect(output).toContain('\x1b[1m')
557
+
558
+ // Should contain red foreground ANSI code
559
+ expect(output).toContain('\x1b[31m')
560
+
561
+ // Should contain underline ANSI code
562
+ expect(output).toContain('\x1b[4m')
563
+
564
+ // Should contain reset ANSI code at end
565
+ expect(output).toContain('\x1b[0m')
566
+
567
+ // Content should still be present
568
+ expect(stripAnsi(output)).toContain('Styled content')
569
+ })
570
+
571
+ it('detectColorSupport returns valid level', async () => {
572
+ const { detectColorSupport } = await import('../theme')
573
+
574
+ const support = detectColorSupport()
575
+
576
+ // Should return one of the valid levels
577
+ expect(['none', '16', '256', 'truecolor']).toContain(support)
578
+ })
579
+ })
580
+
581
+ // ============================================================================
582
+ // 5. Component Composition Tests
583
+ // ============================================================================
584
+
585
+ describe('Component Composition', () => {
586
+ beforeEach(() => {
587
+ vi.resetModules()
588
+ })
589
+
590
+ it('Badge inside Text renders correctly', async () => {
591
+ const { Box, Text, Badge } = await import('../components')
592
+
593
+ const element = React.createElement(
594
+ Box,
595
+ {},
596
+ React.createElement(Text, {}, 'Status: '),
597
+ React.createElement(Badge, { variant: 'success' }, 'Active')
598
+ )
599
+
600
+ const lines = await renderToTerminal(element)
601
+ const content = getPlainContent(lines)
602
+
603
+ expect(content).toContain('Status:')
604
+ expect(content).toContain('Active')
605
+
606
+ // Badge should have some styling (ANSI codes)
607
+ // Success variant should have color styling
608
+ const output = lines.join('')
609
+ // Verify badge renders content - styling is implementation detail
610
+ expect(content).toMatch(/Active/)
611
+ })
612
+
613
+ it('Breadcrumb navigation renders', async () => {
614
+ const { Breadcrumb } = await import('../components')
615
+
616
+ const breadcrumb = React.createElement(Breadcrumb, {
617
+ items: [
618
+ { label: 'Home', path: '/' },
619
+ { label: 'Products', path: '/products' },
620
+ { label: 'Electronics', path: '/products/electronics' },
621
+ { label: 'Phones' }, // Current page (no path)
622
+ ],
623
+ })
624
+
625
+ const lines = await renderToTerminal(breadcrumb)
626
+ const content = getPlainContent(lines)
627
+
628
+ // All items should be rendered
629
+ expect(content).toContain('Home')
630
+ expect(content).toContain('Products')
631
+ expect(content).toContain('Electronics')
632
+ expect(content).toContain('Phones')
633
+
634
+ // Should have separators
635
+ expect(content).toMatch(/[\/>\u203A\u2192]/)
636
+ })
637
+
638
+ it('Spinner animation frame renders', async () => {
639
+ const { Spinner } = await import('../components')
640
+
641
+ const spinner = React.createElement(Spinner, { label: 'Loading...' })
642
+
643
+ const lines = await renderToTerminal(spinner)
644
+ const content = getPlainContent(lines)
645
+
646
+ // Should contain the label
647
+ expect(content).toContain('Loading...')
648
+
649
+ // Should contain a spinner frame character
650
+ // Common spinner chars: dots, braille patterns, bars
651
+ expect(lines.join('')).toMatch(/[\u280B\u2819\u2839\u28B9\u28F9\u28FC\u28E4\u2846|\\/-]/)
652
+ })
653
+
654
+ it('List with selection renders', async () => {
655
+ const { List } = await import('../components')
656
+
657
+ const list = React.createElement(List, {
658
+ items: ['First item', 'Second item', 'Third item'],
659
+ selectedIndex: 1,
660
+ onSelect: () => {},
661
+ })
662
+
663
+ const lines = await renderToTerminal(list)
664
+ const content = getPlainContent(lines)
665
+
666
+ // All items should be present
667
+ expect(content).toContain('First item')
668
+ expect(content).toContain('Second item')
669
+ expect(content).toContain('Third item')
670
+
671
+ // Selected item should have highlight styling
672
+ const output = lines.join('')
673
+ expect(hasAnsiCodes(output)).toBe(true)
674
+ })
675
+ })
676
+
677
+ // ============================================================================
678
+ // 6. Theme Provider Integration Tests
679
+ // ============================================================================
680
+
681
+ describe('Theme Provider Integration', () => {
682
+ beforeEach(() => {
683
+ vi.resetModules()
684
+ })
685
+
686
+ it('dark theme provides dark mode colors', async () => {
687
+ const { createTerminalTheme } = await import('../theme')
688
+
689
+ const darkTheme = createTerminalTheme({ mode: 'dark' })
690
+
691
+ expect(darkTheme.mode).toBe('dark')
692
+ expect(darkTheme.colors.foreground).toBeDefined()
693
+ expect(darkTheme.colors.background).toBeDefined()
694
+ expect(darkTheme.colors.primary).toBeDefined()
695
+ })
696
+
697
+ it('light theme provides light mode colors', async () => {
698
+ const { createTerminalTheme } = await import('../theme')
699
+
700
+ const lightTheme = createTerminalTheme({ mode: 'light' })
701
+
702
+ expect(lightTheme.mode).toBe('light')
703
+ expect(lightTheme.colors.foreground).toBeDefined()
704
+ expect(lightTheme.colors.background).toBeDefined()
705
+ })
706
+
707
+ it('createTerminalTheme creates usable theme', async () => {
708
+ const { createTerminalTheme } = await import('../theme')
709
+
710
+ const darkTheme = createTerminalTheme({ mode: 'dark' })
711
+
712
+ expect(darkTheme.mode).toBe('dark')
713
+ expect(darkTheme.colors.foreground).toBeDefined()
714
+ expect(typeof darkTheme.colors.primary).toBe('string')
715
+ expect(darkTheme.colors.primary).toContain('\x1b[') // ANSI code
716
+ })
717
+ })
718
+
719
+ // ============================================================================
720
+ // 7. Focus Provider Integration Tests
721
+ // ============================================================================
722
+
723
+ describe('Focus Provider Integration', () => {
724
+ beforeEach(() => {
725
+ vi.resetModules()
726
+ })
727
+
728
+ it('FocusContext is exported and available', async () => {
729
+ const { FocusContext } = await import('..')
730
+
731
+ // FocusContext should be a valid React context
732
+ expect(FocusContext).toBeDefined()
733
+ expect(FocusContext.Provider).toBeDefined()
734
+ })
735
+
736
+ it('createKeyboardManager can track focus state externally', async () => {
737
+ const { createKeyboardManager, COMMON_BINDINGS } = await import('..')
738
+
739
+ // Simulate focus management with keyboard manager
740
+ let focusedIndex = 0
741
+ const focusableElements = ['input-1', 'input-2', 'button-1']
742
+
743
+ const onAction = vi.fn((action: string) => {
744
+ if (action === 'focus-next') {
745
+ focusedIndex = (focusedIndex + 1) % focusableElements.length
746
+ }
747
+ if (action === 'focus-prev') {
748
+ focusedIndex = (focusedIndex - 1 + focusableElements.length) % focusableElements.length
749
+ }
750
+ })
751
+
752
+ const manager = createKeyboardManager({
753
+ bindings: COMMON_BINDINGS,
754
+ onAction,
755
+ })
756
+
757
+ // Tab should move focus forward
758
+ manager.handleKey('tab')
759
+ expect(focusedIndex).toBe(1)
760
+
761
+ manager.handleKey('tab')
762
+ expect(focusedIndex).toBe(2)
763
+
764
+ // Shift+Tab should move focus backward
765
+ manager.handleKey('shift+tab')
766
+ expect(focusedIndex).toBe(1)
767
+ })
768
+ })
769
+
770
+ // ============================================================================
771
+ // 8. Grid Navigation Tests
772
+ // ============================================================================
773
+
774
+ describe('Grid Navigation', () => {
775
+ beforeEach(() => {
776
+ vi.resetModules()
777
+ })
778
+
779
+ it('keyboard manager supports 2D grid navigation', async () => {
780
+ const { createKeyboardManager, VIM_BINDINGS, ARROW_BINDINGS } = await import('..')
781
+
782
+ // Simulate 2D grid state
783
+ const rows = 3
784
+ const cols = 4
785
+ let row = 0
786
+ let col = 0
787
+
788
+ const onAction = vi.fn((action: string) => {
789
+ switch (action) {
790
+ case 'move-up':
791
+ row = Math.max(0, row - 1)
792
+ break
793
+ case 'move-down':
794
+ row = Math.min(rows - 1, row + 1)
795
+ break
796
+ case 'move-left':
797
+ col = Math.max(0, col - 1)
798
+ break
799
+ case 'move-right':
800
+ col = Math.min(cols - 1, col + 1)
801
+ break
802
+ }
803
+ })
804
+
805
+ const manager = createKeyboardManager({
806
+ bindings: { ...VIM_BINDINGS, ...ARROW_BINDINGS },
807
+ onAction,
808
+ })
809
+
810
+ // Initial position
811
+ expect(row).toBe(0)
812
+ expect(col).toBe(0)
813
+
814
+ // Move right (l or right arrow)
815
+ manager.handleKey('l')
816
+ expect(col).toBe(1)
817
+
818
+ manager.handleKey('right')
819
+ expect(col).toBe(2)
820
+
821
+ // Move down (j or down arrow)
822
+ manager.handleKey('j')
823
+ expect(row).toBe(1)
824
+
825
+ manager.handleKey('down')
826
+ expect(row).toBe(2)
827
+
828
+ // Move left (h or left arrow)
829
+ manager.handleKey('h')
830
+ expect(col).toBe(1)
831
+
832
+ // Move up (k or up arrow)
833
+ manager.handleKey('k')
834
+ expect(row).toBe(1)
835
+
836
+ // Boundary checking - should not go past edges
837
+ manager.handleKey('up') // row = 0
838
+ manager.handleKey('up') // should stay at 0
839
+ expect(row).toBe(0)
840
+
841
+ manager.handleKey('left') // col = 0
842
+ manager.handleKey('left') // should stay at 0
843
+ expect(col).toBe(0)
844
+ })
845
+ })
846
+
847
+ // ============================================================================
848
+ // 9. Error Handling Tests
849
+ // ============================================================================
850
+
851
+ describe('Error Handling', () => {
852
+ beforeEach(() => {
853
+ vi.resetModules()
854
+ })
855
+
856
+ it('renders gracefully with empty children', async () => {
857
+ const { Box } = await import('../components')
858
+
859
+ const emptyBox = React.createElement(Box, { border: 'single', width: 10, height: 3 })
860
+
861
+ const lines = await renderToTerminal(emptyBox)
862
+
863
+ // Should still render the box structure
864
+ expect(lines.length).toBeGreaterThan(0)
865
+ })
866
+
867
+ it('handles null children without crashing', async () => {
868
+ const { Box, Text } = await import('../components')
869
+
870
+ const element = React.createElement(
871
+ Box,
872
+ {},
873
+ React.createElement(Text, {}, 'Before'),
874
+ null,
875
+ React.createElement(Text, {}, 'After')
876
+ )
877
+
878
+ const lines = await renderToTerminal(element)
879
+ const content = getPlainContent(lines)
880
+
881
+ expect(content).toContain('Before')
882
+ expect(content).toContain('After')
883
+ })
884
+
885
+ it('disabled keyboard manager ignores keys', async () => {
886
+ const { createKeyboardManager } = await import('..')
887
+
888
+ const onAction = vi.fn()
889
+ const manager = createKeyboardManager({
890
+ bindings: { j: 'move-down' },
891
+ onAction,
892
+ enabled: false,
893
+ })
894
+
895
+ // Should not trigger when disabled
896
+ manager.handleKey('j')
897
+ expect(onAction).not.toHaveBeenCalled()
898
+
899
+ // Should work after enabling
900
+ manager.enable()
901
+ manager.handleKey('j')
902
+ expect(onAction).toHaveBeenCalledWith('move-down', expect.any(Object))
903
+ })
904
+ })
905
+ })