@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,1197 @@
1
+ /**
2
+ * @mdxui/terminal Component Tests
3
+ *
4
+ * TDD RED Phase: Tests for primitive component mappings to OpenTUI.
5
+ * These tests define the contracts for how MDXUI primitives should render
6
+ * in a terminal environment using OpenTUI components.
7
+ *
8
+ * All tests should FAIL initially because they test actual rendering behavior
9
+ * that doesn't exist yet in the stub implementations.
10
+ */
11
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
12
+ import React from 'react'
13
+
14
+ // ============================================================================
15
+ // Test Utilities
16
+ // ============================================================================
17
+
18
+ /**
19
+ * Simulates rendering a component to terminal strings.
20
+ * In the real implementation, this would use OpenTUI's renderer.
21
+ */
22
+ async function renderToTerminal(element: React.ReactElement): Promise<string[]> {
23
+ // Import the terminal renderer
24
+ const { renderComponent } = await import('../components')
25
+
26
+ // The implementation should provide this function
27
+ if (typeof renderComponent !== 'function') {
28
+ throw new Error('renderComponent function not exported from @mdxui/terminal/components')
29
+ }
30
+
31
+ return renderComponent(element)
32
+ }
33
+
34
+ /**
35
+ * Check if a string contains ANSI escape codes
36
+ */
37
+ function hasAnsiCodes(str: string): boolean {
38
+ // eslint-disable-next-line no-control-regex
39
+ return /\x1b\[[\d;]*m/.test(str)
40
+ }
41
+
42
+ /**
43
+ * Strip ANSI codes from string for content testing
44
+ */
45
+ function stripAnsi(str: string): string {
46
+ // eslint-disable-next-line no-control-regex
47
+ return str.replace(/\x1b\[[\d;]*m/g, '')
48
+ }
49
+
50
+ // ============================================================================
51
+ // 1. Box Component Tests - Terminal Rendering
52
+ // ============================================================================
53
+
54
+ describe('Box component terminal rendering', () => {
55
+ beforeEach(() => {
56
+ vi.resetModules()
57
+ })
58
+
59
+ describe('border rendering', () => {
60
+ it('renders single border with box-drawing characters', async () => {
61
+ const { Box } = await import('../components')
62
+ const element = React.createElement(Box, { border: 'single', width: 10, height: 3 }, 'Hi')
63
+
64
+ const lines = await renderToTerminal(element)
65
+
66
+ // Should render box-drawing characters for single border
67
+ expect(lines[0]).toContain('\u250C') // top-left corner
68
+ expect(lines[0]).toContain('\u2500') // horizontal line
69
+ expect(lines[0]).toContain('\u2510') // top-right corner
70
+ expect(lines[2]).toContain('\u2514') // bottom-left corner
71
+ expect(lines[2]).toContain('\u2518') // bottom-right corner
72
+ expect(lines[1]).toContain('\u2502') // vertical line
73
+ })
74
+
75
+ it('renders double border with box-drawing characters', async () => {
76
+ const { Box } = await import('../components')
77
+ const element = React.createElement(Box, { border: 'double', width: 10, height: 3 }, 'Hi')
78
+
79
+ const lines = await renderToTerminal(element)
80
+
81
+ expect(lines[0]).toContain('\u2554') // double top-left
82
+ expect(lines[0]).toContain('\u2550') // double horizontal
83
+ expect(lines[0]).toContain('\u2557') // double top-right
84
+ })
85
+
86
+ it('renders rounded border with box-drawing characters', async () => {
87
+ const { Box } = await import('../components')
88
+ const element = React.createElement(Box, { border: 'rounded', width: 10, height: 3 }, 'Hi')
89
+
90
+ const lines = await renderToTerminal(element)
91
+
92
+ expect(lines[0]).toContain('\u256D') // rounded top-left
93
+ expect(lines[0]).toContain('\u256E') // rounded top-right
94
+ })
95
+
96
+ it('renders no border when border is none', async () => {
97
+ const { Box } = await import('../components')
98
+ const element = React.createElement(Box, { border: 'none', width: 10, height: 3 }, 'Hi')
99
+
100
+ const lines = await renderToTerminal(element)
101
+
102
+ // Should not contain any box-drawing characters
103
+ const allText = lines.join('')
104
+ expect(allText).not.toContain('\u250C')
105
+ expect(allText).not.toContain('\u2554')
106
+ expect(allText).not.toContain('\u256D')
107
+ })
108
+ })
109
+
110
+ describe('padding', () => {
111
+ it('adds padding space around content', async () => {
112
+ const { Box } = await import('../components')
113
+ const element = React.createElement(Box, { padding: 2, width: 10 }, 'X')
114
+
115
+ const lines = await renderToTerminal(element)
116
+ const contentLine = lines.find(l => stripAnsi(l).includes('X'))
117
+
118
+ // Content should be indented by padding amount
119
+ expect(contentLine).toBeDefined()
120
+ const stripped = stripAnsi(contentLine!)
121
+ const xIndex = stripped.indexOf('X')
122
+ expect(xIndex).toBeGreaterThanOrEqual(2) // at least padding spaces
123
+ })
124
+ })
125
+
126
+ describe('dimensions', () => {
127
+ it('respects fixed width', async () => {
128
+ const { Box } = await import('../components')
129
+ const element = React.createElement(Box, { width: 20, border: 'single' }, 'Content')
130
+
131
+ const lines = await renderToTerminal(element)
132
+
133
+ lines.forEach(line => {
134
+ expect(stripAnsi(line).length).toBeLessThanOrEqual(20)
135
+ })
136
+ })
137
+
138
+ it('respects fixed height', async () => {
139
+ const { Box } = await import('../components')
140
+ const element = React.createElement(Box, { height: 5, border: 'single' }, 'Content')
141
+
142
+ const lines = await renderToTerminal(element)
143
+
144
+ expect(lines.length).toBe(5)
145
+ })
146
+ })
147
+
148
+ describe('background color', () => {
149
+ it('applies ANSI background color code', async () => {
150
+ const { Box } = await import('../components')
151
+ const element = React.createElement(Box, { bg: 'blue' }, 'Content')
152
+
153
+ const lines = await renderToTerminal(element)
154
+
155
+ // Should contain ANSI background color escape code
156
+ const allText = lines.join('')
157
+ expect(hasAnsiCodes(allText)).toBe(true)
158
+ expect(allText).toContain('\x1b[44m') // Blue background
159
+ })
160
+ })
161
+ })
162
+
163
+ // ============================================================================
164
+ // 2. Text Component Tests - Terminal Rendering
165
+ // ============================================================================
166
+
167
+ describe('Text component terminal rendering', () => {
168
+ beforeEach(() => {
169
+ vi.resetModules()
170
+ })
171
+
172
+ describe('basic rendering', () => {
173
+ it('renders text content', async () => {
174
+ const { Text } = await import('../components')
175
+ const element = React.createElement(Text, {}, 'Hello World')
176
+
177
+ const lines = await renderToTerminal(element)
178
+
179
+ const content = stripAnsi(lines.join(''))
180
+ expect(content).toContain('Hello World')
181
+ })
182
+ })
183
+
184
+ describe('text styles with ANSI codes', () => {
185
+ it('renders bold text with ANSI bold code', async () => {
186
+ const { Text } = await import('../components')
187
+ const element = React.createElement(Text, { bold: true }, 'Bold')
188
+
189
+ const lines = await renderToTerminal(element)
190
+
191
+ const output = lines.join('')
192
+ expect(output).toContain('\x1b[1m') // ANSI bold
193
+ expect(stripAnsi(output)).toContain('Bold')
194
+ })
195
+
196
+ it('renders italic text with ANSI italic code', async () => {
197
+ const { Text } = await import('../components')
198
+ const element = React.createElement(Text, { italic: true }, 'Italic')
199
+
200
+ const lines = await renderToTerminal(element)
201
+
202
+ const output = lines.join('')
203
+ expect(output).toContain('\x1b[3m') // ANSI italic
204
+ })
205
+
206
+ it('renders underline text with ANSI underline code', async () => {
207
+ const { Text } = await import('../components')
208
+ const element = React.createElement(Text, { underline: true }, 'Underlined')
209
+
210
+ const lines = await renderToTerminal(element)
211
+
212
+ const output = lines.join('')
213
+ expect(output).toContain('\x1b[4m') // ANSI underline
214
+ })
215
+
216
+ it('renders dim text with ANSI dim code', async () => {
217
+ const { Text } = await import('../components')
218
+ const element = React.createElement(Text, { dim: true }, 'Dimmed')
219
+
220
+ const lines = await renderToTerminal(element)
221
+
222
+ const output = lines.join('')
223
+ expect(output).toContain('\x1b[2m') // ANSI dim
224
+ })
225
+
226
+ it('combines multiple styles', async () => {
227
+ const { Text } = await import('../components')
228
+ const element = React.createElement(Text, { bold: true, underline: true }, 'Both')
229
+
230
+ const lines = await renderToTerminal(element)
231
+
232
+ const output = lines.join('')
233
+ expect(output).toContain('\x1b[1m') // ANSI bold
234
+ expect(output).toContain('\x1b[4m') // ANSI underline
235
+ })
236
+
237
+ it('resets styles at end of text', async () => {
238
+ const { Text } = await import('../components')
239
+ const element = React.createElement(Text, { bold: true }, 'Bold')
240
+
241
+ const lines = await renderToTerminal(element)
242
+
243
+ const output = lines.join('')
244
+ expect(output).toContain('\x1b[0m') // ANSI reset
245
+ })
246
+ })
247
+
248
+ describe('text colors', () => {
249
+ it('applies foreground color with ANSI code', async () => {
250
+ const { Text } = await import('../components')
251
+ const element = React.createElement(Text, { color: 'red' }, 'Red text')
252
+
253
+ const lines = await renderToTerminal(element)
254
+
255
+ const output = lines.join('')
256
+ expect(output).toContain('\x1b[31m') // ANSI red foreground
257
+ })
258
+
259
+ it('applies semantic error color', async () => {
260
+ const { Text } = await import('../components')
261
+ const element = React.createElement(Text, { color: 'error' }, 'Error')
262
+
263
+ const lines = await renderToTerminal(element)
264
+
265
+ const output = lines.join('')
266
+ expect(hasAnsiCodes(output)).toBe(true)
267
+ // Error should map to red
268
+ expect(output).toContain('\x1b[31m')
269
+ })
270
+
271
+ it('applies semantic success color', async () => {
272
+ const { Text } = await import('../components')
273
+ const element = React.createElement(Text, { color: 'success' }, 'Success')
274
+
275
+ const lines = await renderToTerminal(element)
276
+
277
+ const output = lines.join('')
278
+ expect(output).toContain('\x1b[32m') // Green
279
+ })
280
+
281
+ it('applies semantic warning color', async () => {
282
+ const { Text } = await import('../components')
283
+ const element = React.createElement(Text, { color: 'warning' }, 'Warning')
284
+
285
+ const lines = await renderToTerminal(element)
286
+
287
+ const output = lines.join('')
288
+ expect(output).toContain('\x1b[33m') // Yellow
289
+ })
290
+
291
+ it('applies semantic primary color', async () => {
292
+ const { Text } = await import('../components')
293
+ const element = React.createElement(Text, { color: 'primary' }, 'Primary')
294
+
295
+ const lines = await renderToTerminal(element)
296
+
297
+ const output = lines.join('')
298
+ expect(output).toContain('\x1b[36m') // Cyan (default primary)
299
+ })
300
+
301
+ it('applies background color', async () => {
302
+ const { Text } = await import('../components')
303
+ const element = React.createElement(Text, { backgroundColor: 'blue' }, 'Highlighted')
304
+
305
+ const lines = await renderToTerminal(element)
306
+
307
+ const output = lines.join('')
308
+ expect(output).toContain('\x1b[44m') // Blue background
309
+ })
310
+ })
311
+
312
+ describe('text wrapping', () => {
313
+ it('wraps text at specified width', async () => {
314
+ const { Text, Box } = await import('../components')
315
+ const longText = 'This is a very long text that should wrap to multiple lines'
316
+ const element = React.createElement(
317
+ Box,
318
+ { width: 20 },
319
+ React.createElement(Text, { wrap: 'wrap' }, longText)
320
+ )
321
+
322
+ const lines = await renderToTerminal(element)
323
+
324
+ // Should have multiple lines
325
+ expect(lines.length).toBeGreaterThan(1)
326
+ lines.forEach(line => {
327
+ expect(stripAnsi(line).length).toBeLessThanOrEqual(20)
328
+ })
329
+ })
330
+
331
+ it('truncates text with ellipsis', async () => {
332
+ const { Text, Box } = await import('../components')
333
+ const longText = 'This is very long text'
334
+ const element = React.createElement(
335
+ Box,
336
+ { width: 10 },
337
+ React.createElement(Text, { wrap: 'truncate' }, longText)
338
+ )
339
+
340
+ const lines = await renderToTerminal(element)
341
+
342
+ const content = stripAnsi(lines.join(''))
343
+ expect(content.length).toBeLessThanOrEqual(10)
344
+ expect(content).toContain('\u2026') // ellipsis character
345
+ })
346
+ })
347
+ })
348
+
349
+ // ============================================================================
350
+ // 3. Table Component Tests - Terminal Rendering
351
+ // ============================================================================
352
+
353
+ describe('Table component terminal rendering', () => {
354
+ beforeEach(() => {
355
+ vi.resetModules()
356
+ })
357
+
358
+ interface TestRow {
359
+ name: string
360
+ status: string
361
+ count: number
362
+ }
363
+
364
+ const sampleData: TestRow[] = [
365
+ { name: 'Item 1', status: 'active', count: 10 },
366
+ { name: 'Item 2', status: 'inactive', count: 5 },
367
+ ]
368
+
369
+ const sampleColumns = [
370
+ { key: 'name' as const, header: 'Name' },
371
+ { key: 'status' as const, header: 'Status' },
372
+ { key: 'count' as const, header: 'Count' },
373
+ ]
374
+
375
+ describe('header rendering', () => {
376
+ it('renders column headers', async () => {
377
+ const { Table } = await import('../components')
378
+ const element = React.createElement(Table, { data: sampleData, columns: sampleColumns })
379
+
380
+ const lines = await renderToTerminal(element)
381
+
382
+ const allText = stripAnsi(lines.join('\n'))
383
+ expect(allText).toContain('Name')
384
+ expect(allText).toContain('Status')
385
+ expect(allText).toContain('Count')
386
+ })
387
+
388
+ it('renders header separator line', async () => {
389
+ const { Table } = await import('../components')
390
+ const element = React.createElement(Table, { data: sampleData, columns: sampleColumns })
391
+
392
+ const lines = await renderToTerminal(element)
393
+
394
+ // Should have a horizontal line after headers
395
+ const hasHorizontalLine = lines.some(line =>
396
+ line.includes('\u2500') || line.includes('\u2550') || line.includes('-')
397
+ )
398
+ expect(hasHorizontalLine).toBe(true)
399
+ })
400
+ })
401
+
402
+ describe('data row rendering', () => {
403
+ it('renders all data rows', async () => {
404
+ const { Table } = await import('../components')
405
+ const element = React.createElement(Table, { data: sampleData, columns: sampleColumns })
406
+
407
+ const lines = await renderToTerminal(element)
408
+
409
+ const allText = stripAnsi(lines.join('\n'))
410
+ expect(allText).toContain('Item 1')
411
+ expect(allText).toContain('Item 2')
412
+ expect(allText).toContain('active')
413
+ expect(allText).toContain('inactive')
414
+ })
415
+
416
+ it('renders numeric values correctly', async () => {
417
+ const { Table } = await import('../components')
418
+ const element = React.createElement(Table, { data: sampleData, columns: sampleColumns })
419
+
420
+ const lines = await renderToTerminal(element)
421
+
422
+ const allText = stripAnsi(lines.join('\n'))
423
+ expect(allText).toContain('10')
424
+ expect(allText).toContain('5')
425
+ })
426
+ })
427
+
428
+ describe('column alignment', () => {
429
+ it('aligns columns with consistent widths', async () => {
430
+ const { Table } = await import('../components')
431
+ const columns = [
432
+ { key: 'name' as const, header: 'Name', width: 15 },
433
+ { key: 'status' as const, header: 'Status', width: 10 },
434
+ ]
435
+ const element = React.createElement(Table, { data: sampleData, columns })
436
+
437
+ const lines = await renderToTerminal(element)
438
+
439
+ // Each column should maintain consistent width
440
+ const contentLines = lines.filter(l => stripAnsi(l).includes('Item'))
441
+ contentLines.forEach(line => {
442
+ const stripped = stripAnsi(line)
443
+ // Check that structure is consistent
444
+ expect(stripped.length).toBeGreaterThan(20) // At least width of both columns
445
+ })
446
+ })
447
+
448
+ it('right-aligns numeric columns', async () => {
449
+ const { Table } = await import('../components')
450
+ const columns = [
451
+ { key: 'name' as const, header: 'Name' },
452
+ { key: 'count' as const, header: 'Count', align: 'right' as const, width: 10 },
453
+ ]
454
+ const element = React.createElement(Table, { data: sampleData, columns })
455
+
456
+ const lines = await renderToTerminal(element)
457
+
458
+ // Numbers should be right-aligned with padding on left
459
+ const countLine = lines.find(l => stripAnsi(l).includes('10'))
460
+ expect(countLine).toBeDefined()
461
+ })
462
+ })
463
+
464
+ describe('selection', () => {
465
+ it('highlights selected row with ANSI background', async () => {
466
+ const { Table } = await import('../components')
467
+ const element = React.createElement(Table, {
468
+ data: sampleData,
469
+ columns: sampleColumns,
470
+ selectedIndex: 0
471
+ })
472
+
473
+ const lines = await renderToTerminal(element)
474
+
475
+ // Selected row should have background color
476
+ const selectedLine = lines.find(l => stripAnsi(l).includes('Item 1'))
477
+ expect(selectedLine).toBeDefined()
478
+ expect(hasAnsiCodes(selectedLine!)).toBe(true)
479
+ // Should contain a background color code (40-47 or 100-107)
480
+ expect(selectedLine).toMatch(/\x1b\[(4[0-7]|10[0-7])m/)
481
+ })
482
+ })
483
+
484
+ describe('borders', () => {
485
+ it('renders table with borders', async () => {
486
+ const { Table } = await import('../components')
487
+ const element = React.createElement(Table, { data: sampleData, columns: sampleColumns })
488
+
489
+ const lines = await renderToTerminal(element)
490
+
491
+ // Should have vertical separators
492
+ const hasVerticalSep = lines.some(l => l.includes('\u2502') || l.includes('|'))
493
+ expect(hasVerticalSep).toBe(true)
494
+ })
495
+ })
496
+ })
497
+
498
+ // ============================================================================
499
+ // 4. Input Component Tests - Terminal Rendering
500
+ // ============================================================================
501
+
502
+ describe('Input component terminal rendering', () => {
503
+ beforeEach(() => {
504
+ vi.resetModules()
505
+ })
506
+
507
+ describe('value display', () => {
508
+ it('displays current value', async () => {
509
+ const { Input } = await import('../components')
510
+ const element = React.createElement(Input, {
511
+ value: 'test input',
512
+ onChange: () => {}
513
+ })
514
+
515
+ const lines = await renderToTerminal(element)
516
+
517
+ const content = stripAnsi(lines.join(''))
518
+ expect(content).toContain('test input')
519
+ })
520
+
521
+ it('displays placeholder when empty', async () => {
522
+ const { Input } = await import('../components')
523
+ const element = React.createElement(Input, {
524
+ value: '',
525
+ placeholder: 'Enter text...',
526
+ onChange: () => {}
527
+ })
528
+
529
+ const lines = await renderToTerminal(element)
530
+
531
+ const content = lines.join('')
532
+ expect(stripAnsi(content)).toContain('Enter text...')
533
+ // Placeholder should be dimmed
534
+ expect(content).toContain('\x1b[2m') // dim code
535
+ })
536
+ })
537
+
538
+ describe('label', () => {
539
+ it('renders label above input', async () => {
540
+ const { Input } = await import('../components')
541
+ const element = React.createElement(Input, {
542
+ value: '',
543
+ label: 'Username',
544
+ onChange: () => {}
545
+ })
546
+
547
+ const lines = await renderToTerminal(element)
548
+
549
+ expect(stripAnsi(lines[0])).toContain('Username')
550
+ })
551
+ })
552
+
553
+ describe('focused state', () => {
554
+ it('shows cursor indicator when focused', async () => {
555
+ const { Input } = await import('../components')
556
+ const element = React.createElement(Input, {
557
+ value: 'text',
558
+ focused: true,
559
+ onChange: () => {}
560
+ })
561
+
562
+ const lines = await renderToTerminal(element)
563
+
564
+ const content = lines.join('')
565
+ // Should show cursor using inverse video (ANSI code 7) or underline
566
+ expect(content).toMatch(/\x1b\[7m|\x1b\[4m/)
567
+ })
568
+
569
+ it('has highlight color when focused', async () => {
570
+ const { Input } = await import('../components')
571
+ const element = React.createElement(Input, {
572
+ value: '',
573
+ focused: true,
574
+ onChange: () => {}
575
+ })
576
+
577
+ const lines = await renderToTerminal(element)
578
+
579
+ // Should have focus color (cyan border or similar)
580
+ const content = lines.join('')
581
+ expect(hasAnsiCodes(content)).toBe(true)
582
+ })
583
+ })
584
+
585
+ describe('disabled state', () => {
586
+ it('appears dimmed when disabled', async () => {
587
+ const { Input } = await import('../components')
588
+ const element = React.createElement(Input, {
589
+ value: 'disabled input',
590
+ disabled: true,
591
+ onChange: () => {}
592
+ })
593
+
594
+ const lines = await renderToTerminal(element)
595
+
596
+ const content = lines.join('')
597
+ expect(content).toContain('\x1b[2m') // dim code
598
+ })
599
+ })
600
+
601
+ describe('password masking', () => {
602
+ it('masks password with asterisks', async () => {
603
+ const { Input } = await import('../components')
604
+ const element = React.createElement(Input, {
605
+ value: 'secret123',
606
+ type: 'password',
607
+ onChange: () => {}
608
+ })
609
+
610
+ const lines = await renderToTerminal(element)
611
+
612
+ const content = stripAnsi(lines.join(''))
613
+ expect(content).not.toContain('secret123')
614
+ expect(content).toContain('*'.repeat(9)) // 9 asterisks for 9 chars
615
+ })
616
+ })
617
+ })
618
+
619
+ // ============================================================================
620
+ // 5. Select Component Tests - Terminal Rendering
621
+ // ============================================================================
622
+
623
+ describe('Select component terminal rendering', () => {
624
+ beforeEach(() => {
625
+ vi.resetModules()
626
+ })
627
+
628
+ const sampleOptions = [
629
+ { label: 'Option A', value: 'a' },
630
+ { label: 'Option B', value: 'b' },
631
+ { label: 'Option C', value: 'c' },
632
+ ]
633
+
634
+ describe('collapsed state', () => {
635
+ it('shows current selection when collapsed', async () => {
636
+ const { Select } = await import('../components')
637
+ const element = React.createElement(Select, {
638
+ options: sampleOptions,
639
+ value: 'b',
640
+ onChange: () => {}
641
+ })
642
+
643
+ const lines = await renderToTerminal(element)
644
+
645
+ const content = stripAnsi(lines.join(''))
646
+ expect(content).toContain('Option B')
647
+ })
648
+
649
+ it('shows dropdown indicator', async () => {
650
+ const { Select } = await import('../components')
651
+ const element = React.createElement(Select, {
652
+ options: sampleOptions,
653
+ value: 'a',
654
+ onChange: () => {}
655
+ })
656
+
657
+ const lines = await renderToTerminal(element)
658
+
659
+ const content = stripAnsi(lines.join(''))
660
+ // Should show dropdown arrow
661
+ expect(content).toMatch(/[\u25BC\u25BE\u2304>v]/) // down arrow variants
662
+ })
663
+ })
664
+
665
+ describe('expanded state', () => {
666
+ it('shows all options when focused', async () => {
667
+ const { Select } = await import('../components')
668
+ const element = React.createElement(Select, {
669
+ options: sampleOptions,
670
+ value: 'a',
671
+ focused: true,
672
+ onChange: () => {}
673
+ })
674
+
675
+ const lines = await renderToTerminal(element)
676
+
677
+ const content = stripAnsi(lines.join('\n'))
678
+ expect(content).toContain('Option A')
679
+ expect(content).toContain('Option B')
680
+ expect(content).toContain('Option C')
681
+ })
682
+
683
+ it('highlights current selection', async () => {
684
+ const { Select } = await import('../components')
685
+ const element = React.createElement(Select, {
686
+ options: sampleOptions,
687
+ value: 'b',
688
+ focused: true,
689
+ highlightedIndex: 1,
690
+ onChange: () => {}
691
+ })
692
+
693
+ const lines = await renderToTerminal(element)
694
+
695
+ // Option B line should have highlight styling
696
+ const optionBLine = lines.find(l => stripAnsi(l).includes('Option B'))
697
+ expect(optionBLine).toBeDefined()
698
+ expect(hasAnsiCodes(optionBLine!)).toBe(true)
699
+ })
700
+ })
701
+
702
+ describe('label', () => {
703
+ it('renders label', async () => {
704
+ const { Select } = await import('../components')
705
+ const element = React.createElement(Select, {
706
+ options: sampleOptions,
707
+ value: 'a',
708
+ label: 'Choose option',
709
+ onChange: () => {}
710
+ })
711
+
712
+ const lines = await renderToTerminal(element)
713
+
714
+ expect(stripAnsi(lines.join(''))).toContain('Choose option')
715
+ })
716
+ })
717
+ })
718
+
719
+ // ============================================================================
720
+ // 6. Sidebar and SidebarItem Tests - Terminal Rendering
721
+ // ============================================================================
722
+
723
+ describe('Sidebar component terminal rendering', () => {
724
+ beforeEach(() => {
725
+ vi.resetModules()
726
+ })
727
+
728
+ describe('layout', () => {
729
+ it('renders with fixed width', async () => {
730
+ const { Sidebar, SidebarItem } = await import('../components')
731
+ const element = React.createElement(
732
+ Sidebar,
733
+ { width: 20 },
734
+ React.createElement(SidebarItem, { label: 'Dashboard' })
735
+ )
736
+
737
+ const lines = await renderToTerminal(element)
738
+
739
+ lines.forEach(line => {
740
+ expect(stripAnsi(line).length).toBeLessThanOrEqual(20)
741
+ })
742
+ })
743
+
744
+ it('renders collapsed with icons only', async () => {
745
+ const { Sidebar, SidebarItem } = await import('../components')
746
+ const element = React.createElement(
747
+ Sidebar,
748
+ { collapsed: true },
749
+ React.createElement(SidebarItem, { label: 'Dashboard', icon: '\u2302' })
750
+ )
751
+
752
+ const lines = await renderToTerminal(element)
753
+
754
+ const content = stripAnsi(lines.join(''))
755
+ expect(content).toContain('\u2302') // icon
756
+ expect(content).not.toContain('Dashboard') // label hidden
757
+ })
758
+ })
759
+ })
760
+
761
+ describe('SidebarItem component terminal rendering', () => {
762
+ beforeEach(() => {
763
+ vi.resetModules()
764
+ })
765
+
766
+ describe('rendering', () => {
767
+ it('renders label text', async () => {
768
+ const { SidebarItem } = await import('../components')
769
+ const element = React.createElement(SidebarItem, { label: 'Dashboard' })
770
+
771
+ const lines = await renderToTerminal(element)
772
+
773
+ expect(stripAnsi(lines.join(''))).toContain('Dashboard')
774
+ })
775
+
776
+ it('renders icon before label', async () => {
777
+ const { SidebarItem } = await import('../components')
778
+ const element = React.createElement(SidebarItem, { label: 'Home', icon: '\u2302' })
779
+
780
+ const lines = await renderToTerminal(element)
781
+
782
+ const content = stripAnsi(lines.join(''))
783
+ const iconIndex = content.indexOf('\u2302')
784
+ const labelIndex = content.indexOf('Home')
785
+ expect(iconIndex).toBeLessThan(labelIndex)
786
+ })
787
+ })
788
+
789
+ describe('active state', () => {
790
+ it('highlights active item with background', async () => {
791
+ const { SidebarItem } = await import('../components')
792
+ const element = React.createElement(SidebarItem, { label: 'Dashboard', active: true })
793
+
794
+ const lines = await renderToTerminal(element)
795
+
796
+ const content = lines.join('')
797
+ // Should have background color for active state
798
+ expect(content).toMatch(/\x1b\[(4[0-7]|10[0-7])m/)
799
+ })
800
+
801
+ it('shows indicator for active item', async () => {
802
+ const { SidebarItem } = await import('../components')
803
+ const element = React.createElement(SidebarItem, { label: 'Dashboard', active: true })
804
+
805
+ const lines = await renderToTerminal(element)
806
+
807
+ const content = stripAnsi(lines.join(''))
808
+ // Should have visual indicator (arrow, bar, etc.)
809
+ expect(content).toMatch(/[\u25B6\u2023\u2022>|]/)
810
+ })
811
+ })
812
+ })
813
+
814
+ // ============================================================================
815
+ // 7. Breadcrumb Component Tests - Terminal Rendering
816
+ // ============================================================================
817
+
818
+ describe('Breadcrumb component terminal rendering', () => {
819
+ beforeEach(() => {
820
+ vi.resetModules()
821
+ })
822
+
823
+ const sampleItems = [
824
+ { label: 'Home', path: '/' },
825
+ { label: 'Products', path: '/products' },
826
+ { label: 'Item 1' }, // No path = current page
827
+ ]
828
+
829
+ describe('rendering', () => {
830
+ it('renders all breadcrumb items', async () => {
831
+ const { Breadcrumb } = await import('../components')
832
+ const element = React.createElement(Breadcrumb, { items: sampleItems })
833
+
834
+ const lines = await renderToTerminal(element)
835
+
836
+ const content = stripAnsi(lines.join(''))
837
+ expect(content).toContain('Home')
838
+ expect(content).toContain('Products')
839
+ expect(content).toContain('Item 1')
840
+ })
841
+
842
+ it('renders default separator between items', async () => {
843
+ const { Breadcrumb } = await import('../components')
844
+ const element = React.createElement(Breadcrumb, { items: sampleItems })
845
+
846
+ const lines = await renderToTerminal(element)
847
+
848
+ const content = stripAnsi(lines.join(''))
849
+ // Default separator is usually / or >
850
+ expect(content).toMatch(/Home.+Products.+Item 1/)
851
+ expect(content).toMatch(/[\/>\u203A\u2192]/) // various separator chars
852
+ })
853
+
854
+ it('renders custom separator', async () => {
855
+ const { Breadcrumb } = await import('../components')
856
+ const element = React.createElement(Breadcrumb, {
857
+ items: sampleItems,
858
+ separator: '\u00BB' // >>
859
+ })
860
+
861
+ const lines = await renderToTerminal(element)
862
+
863
+ const content = stripAnsi(lines.join(''))
864
+ expect(content).toContain('\u00BB')
865
+ })
866
+ })
867
+
868
+ describe('styling', () => {
869
+ it('dims non-current items', async () => {
870
+ const { Breadcrumb } = await import('../components')
871
+ const element = React.createElement(Breadcrumb, { items: sampleItems })
872
+
873
+ const lines = await renderToTerminal(element)
874
+
875
+ // Previous items should be dimmed or have muted color
876
+ const content = lines.join('')
877
+ expect(hasAnsiCodes(content)).toBe(true)
878
+ })
879
+
880
+ it('emphasizes current (last) item', async () => {
881
+ const { Breadcrumb } = await import('../components')
882
+ const element = React.createElement(Breadcrumb, { items: sampleItems })
883
+
884
+ const lines = await renderToTerminal(element)
885
+
886
+ // Current item should be bold or different color
887
+ const content = lines.join('')
888
+ // Should have bold or bright color for last item
889
+ expect(content).toMatch(/\x1b\[(1|9[0-7])m.*Item 1/)
890
+ })
891
+ })
892
+ })
893
+
894
+ // ============================================================================
895
+ // 8. Badge Component Tests - Terminal Rendering
896
+ // ============================================================================
897
+
898
+ describe('Badge component terminal rendering', () => {
899
+ beforeEach(() => {
900
+ vi.resetModules()
901
+ })
902
+
903
+ describe('rendering', () => {
904
+ it('renders badge text', async () => {
905
+ const { Badge } = await import('../components')
906
+ const element = React.createElement(Badge, {}, 'Active')
907
+
908
+ const lines = await renderToTerminal(element)
909
+
910
+ expect(stripAnsi(lines.join(''))).toContain('Active')
911
+ })
912
+
913
+ it('renders with decorative brackets or padding', async () => {
914
+ const { Badge } = await import('../components')
915
+ const element = React.createElement(Badge, {}, 'New')
916
+
917
+ const lines = await renderToTerminal(element)
918
+
919
+ const content = stripAnsi(lines.join(''))
920
+ // Should have visual container (brackets, padding, etc.)
921
+ expect(content).toMatch(/[\[\](){}]| New /)
922
+ })
923
+ })
924
+
925
+ describe('variants with colors', () => {
926
+ it('renders success variant with green', async () => {
927
+ const { Badge } = await import('../components')
928
+ const element = React.createElement(Badge, { variant: 'success' }, 'Success')
929
+
930
+ const lines = await renderToTerminal(element)
931
+
932
+ const content = lines.join('')
933
+ // Green foreground or background
934
+ expect(content).toMatch(/\x1b\[(32|42)m/)
935
+ })
936
+
937
+ it('renders warning variant with yellow', async () => {
938
+ const { Badge } = await import('../components')
939
+ const element = React.createElement(Badge, { variant: 'warning' }, 'Warning')
940
+
941
+ const lines = await renderToTerminal(element)
942
+
943
+ const content = lines.join('')
944
+ // Yellow foreground or background
945
+ expect(content).toMatch(/\x1b\[(33|43)m/)
946
+ })
947
+
948
+ it('renders error variant with red', async () => {
949
+ const { Badge } = await import('../components')
950
+ const element = React.createElement(Badge, { variant: 'error' }, 'Error')
951
+
952
+ const lines = await renderToTerminal(element)
953
+
954
+ const content = lines.join('')
955
+ // Red foreground or background
956
+ expect(content).toMatch(/\x1b\[(31|41)m/)
957
+ })
958
+
959
+ it('renders info variant with blue', async () => {
960
+ const { Badge } = await import('../components')
961
+ const element = React.createElement(Badge, { variant: 'info' }, 'Info')
962
+
963
+ const lines = await renderToTerminal(element)
964
+
965
+ const content = lines.join('')
966
+ // Blue foreground or background
967
+ expect(content).toMatch(/\x1b\[(34|44)m/)
968
+ })
969
+
970
+ it('renders default variant with muted styling', async () => {
971
+ const { Badge } = await import('../components')
972
+ const element = React.createElement(Badge, { variant: 'default' }, 'Default')
973
+
974
+ const lines = await renderToTerminal(element)
975
+
976
+ const content = lines.join('')
977
+ // Should have some styling
978
+ expect(hasAnsiCodes(content)).toBe(true)
979
+ })
980
+ })
981
+ })
982
+
983
+ // ============================================================================
984
+ // 9. Dialog Component Tests - Terminal Rendering
985
+ // ============================================================================
986
+
987
+ describe('Dialog component terminal rendering', () => {
988
+ beforeEach(() => {
989
+ vi.resetModules()
990
+ })
991
+
992
+ describe('visibility', () => {
993
+ it('renders content when open', async () => {
994
+ const { Dialog } = await import('../components')
995
+ const element = React.createElement(Dialog, { open: true }, 'Dialog content')
996
+
997
+ const lines = await renderToTerminal(element)
998
+
999
+ expect(stripAnsi(lines.join(''))).toContain('Dialog content')
1000
+ })
1001
+
1002
+ it('renders nothing when closed', async () => {
1003
+ const { Dialog } = await import('../components')
1004
+ const element = React.createElement(Dialog, { open: false }, 'Dialog content')
1005
+
1006
+ const lines = await renderToTerminal(element)
1007
+
1008
+ const content = stripAnsi(lines.join(''))
1009
+ expect(content.trim()).toBe('')
1010
+ })
1011
+ })
1012
+
1013
+ describe('modal appearance', () => {
1014
+ it('renders with border', async () => {
1015
+ const { Dialog } = await import('../components')
1016
+ const element = React.createElement(Dialog, { open: true }, 'Content')
1017
+
1018
+ const lines = await renderToTerminal(element)
1019
+
1020
+ const content = lines.join('')
1021
+ // Should have box-drawing border
1022
+ expect(content).toMatch(/[\u250C\u2554\u256D]/) // top-left corners
1023
+ })
1024
+
1025
+ it('renders title in header', async () => {
1026
+ const { Dialog } = await import('../components')
1027
+ const element = React.createElement(Dialog, {
1028
+ open: true,
1029
+ title: 'Confirm Action'
1030
+ }, 'Are you sure?')
1031
+
1032
+ const lines = await renderToTerminal(element)
1033
+
1034
+ const content = stripAnsi(lines.join('\n'))
1035
+ expect(content).toContain('Confirm Action')
1036
+ expect(content).toContain('Are you sure?')
1037
+ })
1038
+
1039
+ it('centers dialog in viewport', async () => {
1040
+ const { Dialog } = await import('../components')
1041
+ const element = React.createElement(Dialog, { open: true }, 'Centered')
1042
+
1043
+ const lines = await renderToTerminal(element)
1044
+
1045
+ // Dialog should have padding/margin indicating centering
1046
+ // First non-empty line should have leading spaces
1047
+ const firstContentLine = lines.find(l => stripAnsi(l).trim().length > 0)
1048
+ if (firstContentLine) {
1049
+ const leadingSpaces = firstContentLine.match(/^\s*/)?.[0].length ?? 0
1050
+ expect(leadingSpaces).toBeGreaterThan(0)
1051
+ }
1052
+ })
1053
+ })
1054
+
1055
+ describe('styling', () => {
1056
+ it('has distinctive background or border color', async () => {
1057
+ const { Dialog } = await import('../components')
1058
+ const element = React.createElement(Dialog, { open: true }, 'Modal')
1059
+
1060
+ const lines = await renderToTerminal(element)
1061
+
1062
+ const content = lines.join('')
1063
+ // Should have ANSI styling for modal appearance
1064
+ expect(hasAnsiCodes(content)).toBe(true)
1065
+ })
1066
+ })
1067
+ })
1068
+
1069
+ // ============================================================================
1070
+ // Export Contract Types (for implementation phase)
1071
+ // ============================================================================
1072
+
1073
+ describe('Contract type exports', () => {
1074
+ it('exports BoxProps type', async () => {
1075
+ const { Box } = await import('../components')
1076
+ // TypeScript will catch if Box doesn't accept these props
1077
+ const _: React.ReactElement = Box({
1078
+ children: 'test',
1079
+ border: 'single',
1080
+ padding: 1,
1081
+ margin: 1,
1082
+ width: 10,
1083
+ height: 5,
1084
+ flexDirection: 'row',
1085
+ justifyContent: 'center',
1086
+ alignItems: 'center',
1087
+ gap: 1,
1088
+ bg: 'blue'
1089
+ })
1090
+ expect(_).toBeDefined()
1091
+ })
1092
+
1093
+ it('exports TextProps type', async () => {
1094
+ const { Text } = await import('../components')
1095
+ const _: React.ReactElement = Text({
1096
+ children: 'test',
1097
+ bold: true,
1098
+ italic: true,
1099
+ underline: true,
1100
+ dim: true,
1101
+ color: 'red',
1102
+ backgroundColor: 'blue',
1103
+ wrap: 'truncate'
1104
+ })
1105
+ expect(_).toBeDefined()
1106
+ })
1107
+
1108
+ it('exports TableProps type', async () => {
1109
+ const { Table } = await import('../components')
1110
+ const _: React.ReactElement = Table({
1111
+ data: [{ name: 'test' }],
1112
+ columns: [{ key: 'name', header: 'Name', width: 10, align: 'left' }],
1113
+ selectedIndex: 0,
1114
+ onSelect: () => {},
1115
+ navigable: true
1116
+ })
1117
+ expect(_).toBeDefined()
1118
+ })
1119
+
1120
+ it('exports InputProps type', async () => {
1121
+ const { Input } = await import('../components')
1122
+ const _: React.ReactElement = Input({
1123
+ value: '',
1124
+ onChange: () => {},
1125
+ placeholder: 'test',
1126
+ label: 'Label',
1127
+ disabled: false,
1128
+ focused: false,
1129
+ type: 'password'
1130
+ })
1131
+ expect(_).toBeDefined()
1132
+ })
1133
+
1134
+ it('exports SelectProps type', async () => {
1135
+ const { Select } = await import('../components')
1136
+ const _: React.ReactElement = Select({
1137
+ options: [{ label: 'A', value: 'a' }],
1138
+ value: 'a',
1139
+ onChange: () => {},
1140
+ label: 'Label',
1141
+ focused: false,
1142
+ highlightedIndex: 0
1143
+ })
1144
+ expect(_).toBeDefined()
1145
+ })
1146
+
1147
+ it('exports SidebarProps type', async () => {
1148
+ const { Sidebar } = await import('../components')
1149
+ const _: React.ReactElement = Sidebar({
1150
+ children: 'test',
1151
+ width: 20,
1152
+ collapsed: false
1153
+ })
1154
+ expect(_).toBeDefined()
1155
+ })
1156
+
1157
+ it('exports SidebarItemProps type', async () => {
1158
+ const { SidebarItem } = await import('../components')
1159
+ const _: React.ReactElement = SidebarItem({
1160
+ label: 'test',
1161
+ icon: '\u2302',
1162
+ active: true,
1163
+ onSelect: () => {},
1164
+ children: null
1165
+ })
1166
+ expect(_).toBeDefined()
1167
+ })
1168
+
1169
+ it('exports BreadcrumbProps type', async () => {
1170
+ const { Breadcrumb } = await import('../components')
1171
+ const _: React.ReactElement = Breadcrumb({
1172
+ items: [{ label: 'Home', path: '/' }],
1173
+ separator: '>'
1174
+ })
1175
+ expect(_).toBeDefined()
1176
+ })
1177
+
1178
+ it('exports BadgeProps type', async () => {
1179
+ const { Badge } = await import('../components')
1180
+ const _: React.ReactElement = Badge({
1181
+ children: 'test',
1182
+ variant: 'success'
1183
+ })
1184
+ expect(_).toBeDefined()
1185
+ })
1186
+
1187
+ it('exports DialogProps type', async () => {
1188
+ const { Dialog } = await import('../components')
1189
+ const _: React.ReactElement = Dialog({
1190
+ open: true,
1191
+ onClose: () => {},
1192
+ title: 'Test',
1193
+ children: 'content'
1194
+ })
1195
+ expect(_).toBeDefined()
1196
+ })
1197
+ })