@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,685 @@
1
+ /**
2
+ * @mdxui/terminal Test Assertions
3
+ *
4
+ * Custom assertion helpers for testing terminal output. These helpers
5
+ * understand ANSI codes and terminal rendering conventions.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * import {
10
+ * expectTerminalContains,
11
+ * expectTerminalMatches,
12
+ * expectAnsiCode,
13
+ * expectBoxDrawing,
14
+ * } from './assertions'
15
+ *
16
+ * const output = renderer.render(<Box border="single"><Text>Hello</Text></Box>)
17
+ *
18
+ * expectTerminalContains(output, 'Hello')
19
+ * expectBoxDrawing(output, 'single')
20
+ * ```
21
+ */
22
+
23
+ import { expect } from 'vitest'
24
+ import { ANSI } from '../../theme/ansi-codes'
25
+ import { boxChars, type BoxStyle } from '../../theme/box-drawing'
26
+
27
+ // ============================================================================
28
+ // Types
29
+ // ============================================================================
30
+
31
+ /** ANSI style names for assertion helpers */
32
+ export type AnsiStyleName =
33
+ | 'bold'
34
+ | 'dim'
35
+ | 'italic'
36
+ | 'underline'
37
+ | 'inverse'
38
+ | 'strikethrough'
39
+ | 'reset'
40
+
41
+ /** ANSI color names for assertion helpers */
42
+ export type AnsiColorName =
43
+ | 'black'
44
+ | 'red'
45
+ | 'green'
46
+ | 'yellow'
47
+ | 'blue'
48
+ | 'magenta'
49
+ | 'cyan'
50
+ | 'white'
51
+ | 'brightBlack'
52
+ | 'brightRed'
53
+ | 'brightGreen'
54
+ | 'brightYellow'
55
+ | 'brightBlue'
56
+ | 'brightMagenta'
57
+ | 'brightCyan'
58
+ | 'brightWhite'
59
+
60
+ /** Background color names */
61
+ export type AnsiBgColorName =
62
+ | 'bgBlack'
63
+ | 'bgRed'
64
+ | 'bgGreen'
65
+ | 'bgYellow'
66
+ | 'bgBlue'
67
+ | 'bgMagenta'
68
+ | 'bgCyan'
69
+ | 'bgWhite'
70
+ | 'bgBrightBlack'
71
+ | 'bgBrightRed'
72
+ | 'bgBrightGreen'
73
+ | 'bgBrightYellow'
74
+ | 'bgBrightBlue'
75
+ | 'bgBrightMagenta'
76
+ | 'bgBrightCyan'
77
+ | 'bgBrightWhite'
78
+
79
+ // ============================================================================
80
+ // ANSI Code Utilities
81
+ // ============================================================================
82
+
83
+ /**
84
+ * Strip all ANSI escape codes from a string.
85
+ *
86
+ * @param str - The string containing ANSI codes
87
+ * @returns The string with ANSI codes removed
88
+ */
89
+ export function stripAnsi(str: string): string {
90
+ // eslint-disable-next-line no-control-regex
91
+ return str.replace(/\x1b\[[\d;]*m/g, '')
92
+ }
93
+
94
+ /**
95
+ * Check if a string contains any ANSI escape codes.
96
+ *
97
+ * @param str - The string to check
98
+ * @returns True if the string contains ANSI codes
99
+ */
100
+ export function hasAnsiCodes(str: string): boolean {
101
+ // eslint-disable-next-line no-control-regex
102
+ return /\x1b\[[\d;]*m/.test(str)
103
+ }
104
+
105
+ /**
106
+ * Get the visible length of a string (excluding ANSI codes).
107
+ *
108
+ * @param str - The string to measure
109
+ * @returns The visible character count
110
+ */
111
+ export function visibleLength(str: string): number {
112
+ return stripAnsi(str).length
113
+ }
114
+
115
+ /**
116
+ * Extract all ANSI codes from a string.
117
+ *
118
+ * @param str - The string containing ANSI codes
119
+ * @returns Array of ANSI escape sequences found
120
+ */
121
+ export function extractAnsiCodes(str: string): string[] {
122
+ // eslint-disable-next-line no-control-regex
123
+ const matches = str.match(/\x1b\[[\d;]*m/g)
124
+ return matches ?? []
125
+ }
126
+
127
+ /**
128
+ * Get the plain text content from terminal output.
129
+ *
130
+ * @param output - Array of output lines or single string
131
+ * @returns Plain text with ANSI codes stripped
132
+ */
133
+ export function getPlainContent(output: string | string[]): string {
134
+ const text = Array.isArray(output) ? output.join('\n') : output
135
+ return stripAnsi(text)
136
+ }
137
+
138
+ // ============================================================================
139
+ // Content Assertions
140
+ // ============================================================================
141
+
142
+ /**
143
+ * Assert that terminal output contains specific text (ignoring ANSI codes).
144
+ *
145
+ * @param output - Terminal output as string array or string
146
+ * @param text - The text to search for
147
+ * @throws AssertionError if text is not found
148
+ *
149
+ * @example
150
+ * ```tsx
151
+ * const output = renderer.render(<Text>Hello World</Text>)
152
+ * expectTerminalContains(output, 'Hello')
153
+ * expectTerminalContains(output, 'World')
154
+ * ```
155
+ */
156
+ export function expectTerminalContains(
157
+ output: string | string[],
158
+ text: string
159
+ ): void {
160
+ const plainContent = getPlainContent(output)
161
+ expect(plainContent).toContain(text)
162
+ }
163
+
164
+ /**
165
+ * Assert that terminal output does NOT contain specific text.
166
+ *
167
+ * @param output - Terminal output as string array or string
168
+ * @param text - The text that should not be present
169
+ * @throws AssertionError if text is found
170
+ *
171
+ * @example
172
+ * ```tsx
173
+ * const output = renderer.render(<Input type="password" value="secret" />)
174
+ * expectTerminalNotContains(output, 'secret')
175
+ * ```
176
+ */
177
+ export function expectTerminalNotContains(
178
+ output: string | string[],
179
+ text: string
180
+ ): void {
181
+ const plainContent = getPlainContent(output)
182
+ expect(plainContent).not.toContain(text)
183
+ }
184
+
185
+ /**
186
+ * Assert that terminal output matches a regular expression.
187
+ *
188
+ * @param output - Terminal output as string array or string
189
+ * @param pattern - The regex pattern to match
190
+ * @param options - Options for matching
191
+ * @throws AssertionError if pattern doesn't match
192
+ *
193
+ * @example
194
+ * ```tsx
195
+ * expectTerminalMatches(output, /Hello \w+/)
196
+ * expectTerminalMatches(output, /error/i, { stripAnsi: true })
197
+ * ```
198
+ */
199
+ export function expectTerminalMatches(
200
+ output: string | string[],
201
+ pattern: RegExp,
202
+ options: { stripAnsi?: boolean } = {}
203
+ ): void {
204
+ let text = Array.isArray(output) ? output.join('\n') : output
205
+
206
+ if (options.stripAnsi !== false) {
207
+ text = stripAnsi(text)
208
+ }
209
+
210
+ expect(text).toMatch(pattern)
211
+ }
212
+
213
+ /**
214
+ * Assert that a specific line of output contains text.
215
+ *
216
+ * @param output - Array of output lines
217
+ * @param lineIndex - The line index to check (0-based)
218
+ * @param text - The text to search for
219
+ * @throws AssertionError if line doesn't contain text
220
+ *
221
+ * @example
222
+ * ```tsx
223
+ * expectLineContains(output, 0, 'Header')
224
+ * expectLineContains(output, 1, 'Content')
225
+ * ```
226
+ */
227
+ export function expectLineContains(
228
+ output: string[],
229
+ lineIndex: number,
230
+ text: string
231
+ ): void {
232
+ expect(output.length).toBeGreaterThan(lineIndex)
233
+ expect(stripAnsi(output[lineIndex])).toContain(text)
234
+ }
235
+
236
+ /**
237
+ * Assert that output has a specific number of lines.
238
+ *
239
+ * @param output - Array of output lines
240
+ * @param count - Expected number of lines
241
+ * @throws AssertionError if line count doesn't match
242
+ */
243
+ export function expectLineCount(output: string[], count: number): void {
244
+ expect(output).toHaveLength(count)
245
+ }
246
+
247
+ /**
248
+ * Assert that output has at least a minimum number of lines.
249
+ *
250
+ * @param output - Array of output lines
251
+ * @param minCount - Minimum expected lines
252
+ * @throws AssertionError if fewer lines than minimum
253
+ */
254
+ export function expectMinLines(output: string[], minCount: number): void {
255
+ expect(output.length).toBeGreaterThanOrEqual(minCount)
256
+ }
257
+
258
+ // ============================================================================
259
+ // ANSI Style Assertions
260
+ // ============================================================================
261
+
262
+ /**
263
+ * Assert that output contains a specific ANSI code.
264
+ *
265
+ * @param output - Terminal output as string array or string
266
+ * @param code - The ANSI code to check for (e.g., '\x1b[1m' for bold)
267
+ * @throws AssertionError if code is not found
268
+ *
269
+ * @example
270
+ * ```tsx
271
+ * expectAnsiCode(output, ANSI.bold)
272
+ * expectAnsiCode(output, '\x1b[31m') // red
273
+ * ```
274
+ */
275
+ export function expectAnsiCode(
276
+ output: string | string[],
277
+ code: string
278
+ ): void {
279
+ const text = Array.isArray(output) ? output.join('') : output
280
+ expect(text).toContain(code)
281
+ }
282
+
283
+ /**
284
+ * Assert that output does NOT contain a specific ANSI code.
285
+ *
286
+ * @param output - Terminal output as string array or string
287
+ * @param code - The ANSI code that should not be present
288
+ * @throws AssertionError if code is found
289
+ */
290
+ export function expectNoAnsiCode(
291
+ output: string | string[],
292
+ code: string
293
+ ): void {
294
+ const text = Array.isArray(output) ? output.join('') : output
295
+ expect(text).not.toContain(code)
296
+ }
297
+
298
+ /**
299
+ * Assert that output contains a specific ANSI style by name.
300
+ *
301
+ * @param output - Terminal output
302
+ * @param styleName - The style name (bold, dim, italic, underline, etc.)
303
+ * @throws AssertionError if style is not found
304
+ *
305
+ * @example
306
+ * ```tsx
307
+ * expectStyle(output, 'bold')
308
+ * expectStyle(output, 'underline')
309
+ * ```
310
+ */
311
+ export function expectStyle(
312
+ output: string | string[],
313
+ styleName: AnsiStyleName
314
+ ): void {
315
+ const styleCode = ANSI[styleName]
316
+ expectAnsiCode(output, styleCode)
317
+ }
318
+
319
+ /**
320
+ * Assert that output contains a specific foreground color.
321
+ *
322
+ * @param output - Terminal output
323
+ * @param colorName - The color name
324
+ * @throws AssertionError if color is not found
325
+ *
326
+ * @example
327
+ * ```tsx
328
+ * expectForegroundColor(output, 'red')
329
+ * expectForegroundColor(output, 'cyan')
330
+ * ```
331
+ */
332
+ export function expectForegroundColor(
333
+ output: string | string[],
334
+ colorName: AnsiColorName
335
+ ): void {
336
+ const colorCode = ANSI[colorName]
337
+ expectAnsiCode(output, colorCode)
338
+ }
339
+
340
+ /**
341
+ * Assert that output contains a specific background color.
342
+ *
343
+ * @param output - Terminal output
344
+ * @param colorName - The background color name
345
+ * @throws AssertionError if color is not found
346
+ *
347
+ * @example
348
+ * ```tsx
349
+ * expectBackgroundColor(output, 'bgBlue')
350
+ * expectBackgroundColor(output, 'bgRed')
351
+ * ```
352
+ */
353
+ export function expectBackgroundColor(
354
+ output: string | string[],
355
+ colorName: AnsiBgColorName
356
+ ): void {
357
+ const colorCode = ANSI[colorName]
358
+ expectAnsiCode(output, colorCode)
359
+ }
360
+
361
+ /**
362
+ * Assert that output contains an ANSI reset code.
363
+ *
364
+ * This is important to verify that styles are properly cleaned up.
365
+ *
366
+ * @param output - Terminal output
367
+ * @throws AssertionError if reset is not found
368
+ */
369
+ export function expectReset(output: string | string[]): void {
370
+ expectAnsiCode(output, ANSI.reset)
371
+ }
372
+
373
+ /**
374
+ * Assert that output contains 256-color ANSI code.
375
+ *
376
+ * @param output - Terminal output
377
+ * @param colorNumber - The 256-color code (0-255)
378
+ * @param isForeground - True for foreground, false for background
379
+ * @throws AssertionError if color code is not found
380
+ *
381
+ * @example
382
+ * ```tsx
383
+ * expectAnsi256Color(output, 33) // Blue in 256 palette
384
+ * expectAnsi256Color(output, 196, false) // Red background
385
+ * ```
386
+ */
387
+ export function expectAnsi256Color(
388
+ output: string | string[],
389
+ colorNumber: number,
390
+ isForeground: boolean = true
391
+ ): void {
392
+ const prefix = isForeground ? '38' : '48'
393
+ const code = `\x1b[${prefix};5;${colorNumber}m`
394
+ expectAnsiCode(output, code)
395
+ }
396
+
397
+ /**
398
+ * Assert that output contains truecolor RGB ANSI code.
399
+ *
400
+ * @param output - Terminal output
401
+ * @param r - Red component (0-255)
402
+ * @param g - Green component (0-255)
403
+ * @param b - Blue component (0-255)
404
+ * @param isForeground - True for foreground, false for background
405
+ * @throws AssertionError if RGB code is not found
406
+ *
407
+ * @example
408
+ * ```tsx
409
+ * expectTrueColor(output, 255, 0, 0) // Red foreground
410
+ * expectTrueColor(output, 59, 130, 246, false) // Blue-500 background
411
+ * ```
412
+ */
413
+ export function expectTrueColor(
414
+ output: string | string[],
415
+ r: number,
416
+ g: number,
417
+ b: number,
418
+ isForeground: boolean = true
419
+ ): void {
420
+ const prefix = isForeground ? '38' : '48'
421
+ const code = `\x1b[${prefix};2;${r};${g};${b}m`
422
+ expectAnsiCode(output, code)
423
+ }
424
+
425
+ // ============================================================================
426
+ // Box Drawing Assertions
427
+ // ============================================================================
428
+
429
+ /**
430
+ * Assert that output contains box drawing characters for a specific style.
431
+ *
432
+ * @param output - Terminal output
433
+ * @param style - The box style (single, double, rounded, etc.)
434
+ * @throws AssertionError if box characters are not found
435
+ *
436
+ * @example
437
+ * ```tsx
438
+ * expectBoxDrawing(output, 'single')
439
+ * expectBoxDrawing(output, 'double')
440
+ * expectBoxDrawing(output, 'rounded')
441
+ * ```
442
+ */
443
+ export function expectBoxDrawing(
444
+ output: string | string[],
445
+ style: BoxStyle
446
+ ): void {
447
+ const text = Array.isArray(output) ? output.join('') : output
448
+ const chars = boxChars[style]
449
+
450
+ // Check for corner characters
451
+ expect(text).toContain(chars.topLeft)
452
+ expect(text).toContain(chars.topRight)
453
+ expect(text).toContain(chars.bottomLeft)
454
+ expect(text).toContain(chars.bottomRight)
455
+ }
456
+
457
+ /**
458
+ * Assert that output contains horizontal box lines.
459
+ *
460
+ * @param output - Terminal output
461
+ * @param style - The box style
462
+ * @throws AssertionError if horizontal lines are not found
463
+ */
464
+ export function expectHorizontalLine(
465
+ output: string | string[],
466
+ style: BoxStyle
467
+ ): void {
468
+ const text = Array.isArray(output) ? output.join('') : output
469
+ expect(text).toContain(boxChars[style].horizontal)
470
+ }
471
+
472
+ /**
473
+ * Assert that output contains vertical box lines.
474
+ *
475
+ * @param output - Terminal output
476
+ * @param style - The box style
477
+ * @throws AssertionError if vertical lines are not found
478
+ */
479
+ export function expectVerticalLine(
480
+ output: string | string[],
481
+ style: BoxStyle
482
+ ): void {
483
+ const text = Array.isArray(output) ? output.join('') : output
484
+ expect(text).toContain(boxChars[style].vertical)
485
+ }
486
+
487
+ /**
488
+ * Assert that output does NOT contain box drawing characters.
489
+ *
490
+ * @param output - Terminal output
491
+ * @throws AssertionError if any box characters are found
492
+ */
493
+ export function expectNoBoxDrawing(output: string | string[]): void {
494
+ const text = stripAnsi(Array.isArray(output) ? output.join('') : output)
495
+
496
+ // Check all box styles
497
+ for (const style of ['single', 'double', 'rounded'] as const) {
498
+ const chars = boxChars[style]
499
+ expect(text).not.toContain(chars.topLeft)
500
+ expect(text).not.toContain(chars.topRight)
501
+ expect(text).not.toContain(chars.bottomLeft)
502
+ expect(text).not.toContain(chars.bottomRight)
503
+ }
504
+ }
505
+
506
+ // ============================================================================
507
+ // Dimension Assertions
508
+ // ============================================================================
509
+
510
+ /**
511
+ * Assert that all lines have a maximum width.
512
+ *
513
+ * @param output - Array of output lines
514
+ * @param maxWidth - Maximum allowed visible width
515
+ * @throws AssertionError if any line exceeds maxWidth
516
+ *
517
+ * @example
518
+ * ```tsx
519
+ * expectMaxWidth(output, 80)
520
+ * ```
521
+ */
522
+ export function expectMaxWidth(output: string[], maxWidth: number): void {
523
+ for (const line of output) {
524
+ expect(visibleLength(line)).toBeLessThanOrEqual(maxWidth)
525
+ }
526
+ }
527
+
528
+ /**
529
+ * Assert that all lines have an exact width.
530
+ *
531
+ * @param output - Array of output lines
532
+ * @param width - Expected visible width
533
+ * @throws AssertionError if any line doesn't match width
534
+ */
535
+ export function expectExactWidth(output: string[], width: number): void {
536
+ for (const line of output) {
537
+ expect(visibleLength(line)).toBe(width)
538
+ }
539
+ }
540
+
541
+ /**
542
+ * Assert that output fits within specified dimensions.
543
+ *
544
+ * @param output - Array of output lines
545
+ * @param width - Maximum width
546
+ * @param height - Maximum height (line count)
547
+ * @throws AssertionError if output exceeds dimensions
548
+ */
549
+ export function expectFitsDimensions(
550
+ output: string[],
551
+ width: number,
552
+ height: number
553
+ ): void {
554
+ expect(output.length).toBeLessThanOrEqual(height)
555
+ expectMaxWidth(output, width)
556
+ }
557
+
558
+ // ============================================================================
559
+ // Snapshot Utilities
560
+ // ============================================================================
561
+
562
+ /**
563
+ * Create a snapshot-friendly representation of terminal output.
564
+ *
565
+ * This normalizes the output for consistent snapshot comparison:
566
+ * - Optionally strips ANSI codes
567
+ * - Trims trailing whitespace
568
+ * - Normalizes line endings
569
+ *
570
+ * @param output - Terminal output
571
+ * @param options - Snapshot options
572
+ * @returns Normalized string for snapshot comparison
573
+ *
574
+ * @example
575
+ * ```tsx
576
+ * expect(toSnapshot(output)).toMatchSnapshot()
577
+ * expect(toSnapshot(output, { stripAnsi: true })).toMatchSnapshot()
578
+ * ```
579
+ */
580
+ export function toSnapshot(
581
+ output: string | string[],
582
+ options: {
583
+ stripAnsi?: boolean
584
+ trimLines?: boolean
585
+ } = {}
586
+ ): string {
587
+ const { stripAnsi: strip = false, trimLines = true } = options
588
+
589
+ let text = Array.isArray(output) ? output.join('\n') : output
590
+
591
+ if (strip) {
592
+ text = stripAnsi(text)
593
+ }
594
+
595
+ if (trimLines) {
596
+ text = text
597
+ .split('\n')
598
+ .map(line => line.trimEnd())
599
+ .join('\n')
600
+ }
601
+
602
+ return text
603
+ }
604
+
605
+ /**
606
+ * Compare two terminal outputs for equality (ignoring ANSI codes).
607
+ *
608
+ * @param actual - Actual output
609
+ * @param expected - Expected output
610
+ * @returns True if outputs are equal (after stripping ANSI)
611
+ */
612
+ export function terminalOutputEquals(
613
+ actual: string | string[],
614
+ expected: string | string[]
615
+ ): boolean {
616
+ const actualText = getPlainContent(actual)
617
+ const expectedText = getPlainContent(expected)
618
+ return actualText === expectedText
619
+ }
620
+
621
+ /**
622
+ * Assert that two terminal outputs are equal (ignoring ANSI codes).
623
+ *
624
+ * @param actual - Actual output
625
+ * @param expected - Expected output
626
+ * @throws AssertionError if outputs don't match
627
+ */
628
+ export function expectTerminalEquals(
629
+ actual: string | string[],
630
+ expected: string | string[]
631
+ ): void {
632
+ expect(getPlainContent(actual)).toBe(getPlainContent(expected))
633
+ }
634
+
635
+ // ============================================================================
636
+ // Component-Specific Assertions
637
+ // ============================================================================
638
+
639
+ /**
640
+ * Assert that output shows a cursor indicator (inverse video or underline).
641
+ *
642
+ * Used for testing focused input fields.
643
+ *
644
+ * @param output - Terminal output
645
+ * @throws AssertionError if no cursor indicator found
646
+ */
647
+ export function expectCursor(output: string | string[]): void {
648
+ const text = Array.isArray(output) ? output.join('') : output
649
+ // Cursor is typically shown as inverse video or underline
650
+ expect(text).toMatch(/\x1b\[7m|\x1b\[4m/)
651
+ }
652
+
653
+ /**
654
+ * Assert that output shows focus styling.
655
+ *
656
+ * @param output - Terminal output
657
+ * @throws AssertionError if no focus styling found
658
+ */
659
+ export function expectFocused(output: string | string[]): void {
660
+ const text = Array.isArray(output) ? output.join('') : output
661
+ // Focus typically includes inverse, underline, or background color
662
+ expect(text).toMatch(/\x1b\[(7|4|4[0-7]|10[0-7])m/)
663
+ }
664
+
665
+ /**
666
+ * Assert that output shows disabled styling (typically dim).
667
+ *
668
+ * @param output - Terminal output
669
+ * @throws AssertionError if no disabled styling found
670
+ */
671
+ export function expectDisabled(output: string | string[]): void {
672
+ expectAnsiCode(output, ANSI.dim)
673
+ }
674
+
675
+ /**
676
+ * Assert that output shows selection/highlight styling.
677
+ *
678
+ * @param output - Terminal output
679
+ * @throws AssertionError if no selection styling found
680
+ */
681
+ export function expectSelected(output: string | string[]): void {
682
+ const text = Array.isArray(output) ? output.join('') : output
683
+ // Selection typically includes background color or inverse
684
+ expect(text).toMatch(/\x1b\[(7|4[0-7]|10[0-7])m/)
685
+ }