@mdxui/terminal 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +571 -0
- package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
- package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
- package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
- package/dist/chunk-3EFDH7PK.js +5235 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-3X5IR6WE.js +884 -0
- package/dist/chunk-4FV5ZDCE.js +5236 -0
- package/dist/chunk-4OVMSF2J.js +243 -0
- package/dist/chunk-63FEETIS.js +4048 -0
- package/dist/chunk-B43KP7XJ.js +884 -0
- package/dist/chunk-BMTJXWUV.js +655 -0
- package/dist/chunk-C3SVH4N7.js +882 -0
- package/dist/chunk-EVWR7Y47.js +874 -0
- package/dist/chunk-F6A5VWUC.js +1285 -0
- package/dist/chunk-FD7KW7GE.js +882 -0
- package/dist/chunk-GBQ6UD6I.js +655 -0
- package/dist/chunk-GMDD3M6U.js +5227 -0
- package/dist/chunk-JBHRXOXM.js +1058 -0
- package/dist/chunk-JFOO3EYO.js +1182 -0
- package/dist/chunk-JQ5H3WXL.js +1291 -0
- package/dist/chunk-JQD5NASE.js +234 -0
- package/dist/chunk-KRHJP5R7.js +592 -0
- package/dist/chunk-KWF6WVJE.js +962 -0
- package/dist/chunk-LHYQVN3H.js +1038 -0
- package/dist/chunk-M3TLQLGC.js +1032 -0
- package/dist/chunk-MVW4Q5OP.js +240 -0
- package/dist/chunk-NXCZSWLU.js +1294 -0
- package/dist/chunk-O25TNRO6.js +607 -0
- package/dist/chunk-PNECDA2I.js +884 -0
- package/dist/chunk-QIHWRLJR.js +962 -0
- package/dist/chunk-QW5YMQ7K.js +882 -0
- package/dist/chunk-R5U7XKVJ.js +16 -0
- package/dist/chunk-RP2MVQLR.js +962 -0
- package/dist/chunk-TP6RXGXA.js +1087 -0
- package/dist/chunk-TQQSTITZ.js +655 -0
- package/dist/chunk-X24GWXQV.js +1281 -0
- package/dist/components/index.d.ts +802 -0
- package/dist/components/index.js +149 -0
- package/dist/data/index.d.ts +2554 -0
- package/dist/data/index.js +51 -0
- package/dist/forms/index.d.ts +1596 -0
- package/dist/forms/index.js +464 -0
- package/dist/index-CQRFZntR.d.ts +867 -0
- package/dist/index.d.ts +579 -0
- package/dist/index.js +786 -0
- package/dist/interactive-D0JkWosD.d.ts +217 -0
- package/dist/keyboard/index.d.ts +2 -0
- package/dist/keyboard/index.js +43 -0
- package/dist/renderers/index.d.ts +546 -0
- package/dist/renderers/index.js +2157 -0
- package/dist/storybook/index.d.ts +396 -0
- package/dist/storybook/index.js +641 -0
- package/dist/theme/index.d.ts +1339 -0
- package/dist/theme/index.js +123 -0
- package/dist/types-Bxu5PAgA.d.ts +710 -0
- package/dist/types-CIlop5Ji.d.ts +701 -0
- package/dist/types-Ca8p_p5X.d.ts +710 -0
- package/package.json +90 -0
- package/src/__tests__/components/data/card.test.ts +458 -0
- package/src/__tests__/components/data/list.test.ts +473 -0
- package/src/__tests__/components/data/metrics.test.ts +541 -0
- package/src/__tests__/components/data/table.test.ts +448 -0
- package/src/__tests__/components/input/field.test.ts +555 -0
- package/src/__tests__/components/input/form.test.ts +870 -0
- package/src/__tests__/components/input/search.test.ts +1238 -0
- package/src/__tests__/components/input/select.test.ts +658 -0
- package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
- package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
- package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
- package/src/__tests__/components/navigation/tabs.test.ts +995 -0
- package/src/__tests__/components.test.tsx +1197 -0
- package/src/__tests__/core/compiler.test.ts +986 -0
- package/src/__tests__/core/parser.test.ts +785 -0
- package/src/__tests__/core/tier-switcher.test.ts +1103 -0
- package/src/__tests__/core/types.test.ts +1398 -0
- package/src/__tests__/data/collections.test.ts +1337 -0
- package/src/__tests__/data/db.test.ts +1265 -0
- package/src/__tests__/data/reactive.test.ts +1010 -0
- package/src/__tests__/data/sync.test.ts +1614 -0
- package/src/__tests__/errors.test.ts +660 -0
- package/src/__tests__/forms/integration.test.ts +444 -0
- package/src/__tests__/integration.test.ts +905 -0
- package/src/__tests__/keyboard.test.ts +1791 -0
- package/src/__tests__/renderer.test.ts +489 -0
- package/src/__tests__/renderers/ansi-css.test.ts +948 -0
- package/src/__tests__/renderers/ansi.test.ts +1366 -0
- package/src/__tests__/renderers/ascii.test.ts +1360 -0
- package/src/__tests__/renderers/interactive.test.ts +2353 -0
- package/src/__tests__/renderers/markdown.test.ts +1483 -0
- package/src/__tests__/renderers/text.test.ts +1369 -0
- package/src/__tests__/renderers/unicode.test.ts +1307 -0
- package/src/__tests__/theme.test.ts +639 -0
- package/src/__tests__/utils/assertions.ts +685 -0
- package/src/__tests__/utils/index.ts +115 -0
- package/src/__tests__/utils/test-renderer.ts +381 -0
- package/src/__tests__/utils/utils.test.ts +560 -0
- package/src/components/containers/card.ts +56 -0
- package/src/components/containers/dialog.ts +53 -0
- package/src/components/containers/index.ts +9 -0
- package/src/components/containers/panel.ts +59 -0
- package/src/components/feedback/badge.ts +40 -0
- package/src/components/feedback/index.ts +8 -0
- package/src/components/feedback/spinner.ts +23 -0
- package/src/components/helpers.ts +81 -0
- package/src/components/index.ts +153 -0
- package/src/components/layout/breadcrumb.ts +31 -0
- package/src/components/layout/index.ts +10 -0
- package/src/components/layout/list.ts +29 -0
- package/src/components/layout/sidebar.ts +79 -0
- package/src/components/layout/table.ts +62 -0
- package/src/components/primitives/box.ts +95 -0
- package/src/components/primitives/button.ts +54 -0
- package/src/components/primitives/index.ts +11 -0
- package/src/components/primitives/input.ts +88 -0
- package/src/components/primitives/select.ts +97 -0
- package/src/components/primitives/text.ts +60 -0
- package/src/components/render.ts +155 -0
- package/src/components/templates/app.ts +43 -0
- package/src/components/templates/index.ts +8 -0
- package/src/components/templates/site.ts +54 -0
- package/src/components/types.ts +777 -0
- package/src/core/compiler.ts +718 -0
- package/src/core/parser.ts +127 -0
- package/src/core/tier-switcher.ts +607 -0
- package/src/core/types.ts +672 -0
- package/src/data/collection.ts +316 -0
- package/src/data/collections.ts +50 -0
- package/src/data/context.tsx +174 -0
- package/src/data/db.ts +127 -0
- package/src/data/hooks.ts +532 -0
- package/src/data/index.ts +138 -0
- package/src/data/reactive.ts +1225 -0
- package/src/data/saas-collections.ts +375 -0
- package/src/data/sync.ts +1213 -0
- package/src/data/types.ts +660 -0
- package/src/forms/converters.ts +512 -0
- package/src/forms/index.ts +133 -0
- package/src/forms/schemas.ts +403 -0
- package/src/forms/types.ts +476 -0
- package/src/index.ts +542 -0
- package/src/keyboard/focus.ts +748 -0
- package/src/keyboard/index.ts +96 -0
- package/src/keyboard/integration.ts +371 -0
- package/src/keyboard/manager.ts +377 -0
- package/src/keyboard/presets.ts +90 -0
- package/src/renderers/ansi-css.ts +576 -0
- package/src/renderers/ansi.ts +802 -0
- package/src/renderers/ascii.ts +680 -0
- package/src/renderers/breadcrumb.ts +480 -0
- package/src/renderers/command-palette.ts +802 -0
- package/src/renderers/components/field.ts +210 -0
- package/src/renderers/components/form.ts +327 -0
- package/src/renderers/components/index.ts +21 -0
- package/src/renderers/components/search.ts +449 -0
- package/src/renderers/components/select.ts +222 -0
- package/src/renderers/index.ts +101 -0
- package/src/renderers/interactive/component-handlers.ts +622 -0
- package/src/renderers/interactive/cursor-manager.ts +147 -0
- package/src/renderers/interactive/focus-manager.ts +279 -0
- package/src/renderers/interactive/index.ts +661 -0
- package/src/renderers/interactive/input-handler.ts +164 -0
- package/src/renderers/interactive/keyboard-handler.ts +212 -0
- package/src/renderers/interactive/mouse-handler.ts +167 -0
- package/src/renderers/interactive/state-manager.ts +109 -0
- package/src/renderers/interactive/types.ts +338 -0
- package/src/renderers/interactive-string.ts +299 -0
- package/src/renderers/interactive.ts +59 -0
- package/src/renderers/markdown.ts +950 -0
- package/src/renderers/sidebar.ts +549 -0
- package/src/renderers/tabs.ts +682 -0
- package/src/renderers/text.ts +791 -0
- package/src/renderers/unicode.ts +917 -0
- package/src/renderers/utils.ts +942 -0
- package/src/router/adapters.ts +383 -0
- package/src/router/types.ts +140 -0
- package/src/router/utils.ts +452 -0
- package/src/schemas.ts +205 -0
- package/src/storybook/index.ts +91 -0
- package/src/storybook/interactive-decorator.tsx +659 -0
- package/src/storybook/keyboard-simulator.ts +501 -0
- package/src/theme/ansi-codes.ts +80 -0
- package/src/theme/box-drawing.ts +132 -0
- package/src/theme/color-convert.ts +254 -0
- package/src/theme/color-support.ts +321 -0
- package/src/theme/index.ts +134 -0
- package/src/theme/strip-ansi.ts +50 -0
- package/src/theme/tailwind-map.ts +469 -0
- package/src/theme/text-styles.ts +206 -0
- package/src/theme/theme-system.ts +568 -0
- package/src/types.ts +103 -0
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal Error Handling Tests
|
|
3
|
+
*
|
|
4
|
+
* TDD RED Phase: These tests define the contract for proper error handling
|
|
5
|
+
* behavior throughout the terminal package. Tests verify:
|
|
6
|
+
* - Graceful handling of invalid inputs
|
|
7
|
+
* - Proper error throwing vs silent fallbacks
|
|
8
|
+
* - Edge case behavior for boundary conditions
|
|
9
|
+
*
|
|
10
|
+
* NOTE: Some tests define expected behavior that may need implementation fixes.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
13
|
+
import React from 'react'
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Color Conversion Error Handling
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
describe('Error Handling', () => {
|
|
20
|
+
describe('Color Conversion', () => {
|
|
21
|
+
describe('hex()', () => {
|
|
22
|
+
it('throws ZodError for invalid hex string', async () => {
|
|
23
|
+
const { hex } = await import('@mdxui/terminal')
|
|
24
|
+
// With Zod validation, invalid hex throws instead of falling back
|
|
25
|
+
expect(() => hex('not-a-hex')).toThrow(/Invalid hex color format/)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('throws ZodError for empty string', async () => {
|
|
29
|
+
const { hex } = await import('@mdxui/terminal')
|
|
30
|
+
// With Zod validation, empty string throws instead of falling back
|
|
31
|
+
expect(() => hex('')).toThrow(/Invalid hex color format/)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('handles 3-digit hex correctly', async () => {
|
|
35
|
+
const { hex } = await import('@mdxui/terminal')
|
|
36
|
+
// #fff should expand to #ffffff (255, 255, 255)
|
|
37
|
+
const result = hex('#fff')
|
|
38
|
+
expect(result).toBe('\x1b[38;2;255;255;255m')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('handles 6-digit hex correctly', async () => {
|
|
42
|
+
const { hex } = await import('@mdxui/terminal')
|
|
43
|
+
// #3b82f6 is Tailwind blue-500
|
|
44
|
+
const result = hex('#3b82f6')
|
|
45
|
+
expect(result).toBe('\x1b[38;2;59;130;246m')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('handles hex without # prefix', async () => {
|
|
49
|
+
const { hex } = await import('@mdxui/terminal')
|
|
50
|
+
const result = hex('ff0000')
|
|
51
|
+
expect(result).toBe('\x1b[38;2;255;0;0m')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('handles 3-digit hex without # prefix', async () => {
|
|
55
|
+
const { hex } = await import('@mdxui/terminal')
|
|
56
|
+
const result = hex('f00')
|
|
57
|
+
expect(result).toBe('\x1b[38;2;255;0;0m')
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('hexToRgb()', () => {
|
|
62
|
+
it('returns black for invalid hex string', async () => {
|
|
63
|
+
const { hexToRgb } = await import('@mdxui/terminal')
|
|
64
|
+
const result = hexToRgb('invalid')
|
|
65
|
+
expect(result).toEqual({ r: 0, g: 0, b: 0 })
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('returns black for hex with wrong length', async () => {
|
|
69
|
+
const { hexToRgb } = await import('@mdxui/terminal')
|
|
70
|
+
expect(hexToRgb('#12')).toEqual({ r: 0, g: 0, b: 0 })
|
|
71
|
+
expect(hexToRgb('#1234')).toEqual({ r: 0, g: 0, b: 0 })
|
|
72
|
+
expect(hexToRgb('#12345')).toEqual({ r: 0, g: 0, b: 0 })
|
|
73
|
+
expect(hexToRgb('#1234567')).toEqual({ r: 0, g: 0, b: 0 })
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('returns black for empty string', async () => {
|
|
77
|
+
const { hexToRgb } = await import('@mdxui/terminal')
|
|
78
|
+
const result = hexToRgb('')
|
|
79
|
+
expect(result).toEqual({ r: 0, g: 0, b: 0 })
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('returns black for only hash prefix', async () => {
|
|
83
|
+
const { hexToRgb } = await import('@mdxui/terminal')
|
|
84
|
+
const result = hexToRgb('#')
|
|
85
|
+
expect(result).toEqual({ r: 0, g: 0, b: 0 })
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('rgb() validation', () => {
|
|
90
|
+
it('accepts RGB values in valid range (0-255)', async () => {
|
|
91
|
+
const { rgb } = await import('@mdxui/terminal')
|
|
92
|
+
// Valid range should produce valid ANSI codes
|
|
93
|
+
expect(rgb(0, 0, 0)).toBe('\x1b[38;2;0;0;0m')
|
|
94
|
+
expect(rgb(255, 255, 255)).toBe('\x1b[38;2;255;255;255m')
|
|
95
|
+
expect(rgb(128, 128, 128)).toBe('\x1b[38;2;128;128;128m')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('throws ZodError for values outside 0-255 range', async () => {
|
|
99
|
+
const { rgb } = await import('@mdxui/terminal')
|
|
100
|
+
// With Zod validation, out-of-range values throw instead of passing through
|
|
101
|
+
expect(() => rgb(-10, 0, 0)).toThrow(/RGB component must be >= 0/)
|
|
102
|
+
expect(() => rgb(300, 0, 0)).toThrow(/RGB component must be <= 255/)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe('tailwindToTerminal()', () => {
|
|
107
|
+
it('handles unknown colors gracefully by returning empty string', async () => {
|
|
108
|
+
const { tailwindToTerminal } = await import('@mdxui/terminal')
|
|
109
|
+
expect(tailwindToTerminal('text-unknown-999')).toBe('')
|
|
110
|
+
expect(tailwindToTerminal('bg-nonexistent-500')).toBe('')
|
|
111
|
+
expect(tailwindToTerminal('random-class')).toBe('')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('handles empty string gracefully', async () => {
|
|
115
|
+
const { tailwindToTerminal } = await import('@mdxui/terminal')
|
|
116
|
+
expect(tailwindToTerminal('')).toBe('')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('handles null-like input gracefully', async () => {
|
|
120
|
+
const { tailwindToTerminal } = await import('@mdxui/terminal')
|
|
121
|
+
// The function should handle null/undefined gracefully
|
|
122
|
+
expect(tailwindToTerminal(null as unknown as string)).toBe('')
|
|
123
|
+
expect(tailwindToTerminal(undefined as unknown as string)).toBe('')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('handles whitespace-only input', async () => {
|
|
127
|
+
const { tailwindToTerminal } = await import('@mdxui/terminal')
|
|
128
|
+
expect(tailwindToTerminal(' ')).toBe('')
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
describe('tailwindToAnsi()', () => {
|
|
133
|
+
it('returns empty string for unknown classes', async () => {
|
|
134
|
+
const { tailwindToAnsi } = await import('@mdxui/terminal')
|
|
135
|
+
expect(tailwindToAnsi('text-fake-color-999')).toBe('')
|
|
136
|
+
expect(tailwindToAnsi('completely-invalid')).toBe('')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('returns empty string for empty input', async () => {
|
|
140
|
+
const { tailwindToAnsi } = await import('@mdxui/terminal')
|
|
141
|
+
expect(tailwindToAnsi('')).toBe('')
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// ============================================================================
|
|
147
|
+
// Component Rendering Error Handling
|
|
148
|
+
// ============================================================================
|
|
149
|
+
|
|
150
|
+
describe('Component Rendering', () => {
|
|
151
|
+
describe('Box component', () => {
|
|
152
|
+
it('handles width of 0 gracefully', async () => {
|
|
153
|
+
const { renderComponent, Box } = await import('@mdxui/terminal')
|
|
154
|
+
const element = React.createElement(Box, { width: 0, children: 'test' })
|
|
155
|
+
// Should not throw, should return some output (possibly empty or minimal)
|
|
156
|
+
expect(() => renderComponent(element)).not.toThrow()
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('handles negative width gracefully', async () => {
|
|
160
|
+
const { renderComponent, Box } = await import('@mdxui/terminal')
|
|
161
|
+
const element = React.createElement(Box, { width: -10, children: 'test' })
|
|
162
|
+
// Should not throw - implementation should handle gracefully
|
|
163
|
+
expect(() => renderComponent(element)).not.toThrow()
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('handles undefined children gracefully', async () => {
|
|
167
|
+
const { renderComponent, Box } = await import('@mdxui/terminal')
|
|
168
|
+
const element = React.createElement(Box, { width: 20 })
|
|
169
|
+
const result = renderComponent(element)
|
|
170
|
+
expect(Array.isArray(result)).toBe(true)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('handles null children gracefully', async () => {
|
|
174
|
+
const { renderComponent, Box } = await import('@mdxui/terminal')
|
|
175
|
+
const element = React.createElement(Box, { width: 20, children: null })
|
|
176
|
+
const result = renderComponent(element)
|
|
177
|
+
expect(Array.isArray(result)).toBe(true)
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
describe('Table component', () => {
|
|
182
|
+
it('renders empty array for empty data', async () => {
|
|
183
|
+
const { renderComponent, Table } = await import('@mdxui/terminal')
|
|
184
|
+
const element = React.createElement(Table, {
|
|
185
|
+
data: [],
|
|
186
|
+
columns: [{ key: 'name', header: 'Name' }],
|
|
187
|
+
})
|
|
188
|
+
const result = renderComponent(element)
|
|
189
|
+
expect(Array.isArray(result)).toBe(true)
|
|
190
|
+
// Should have at least the header row
|
|
191
|
+
expect(result.length).toBeGreaterThanOrEqual(1)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('handles empty columns array', async () => {
|
|
195
|
+
const { renderComponent, Table } = await import('@mdxui/terminal')
|
|
196
|
+
const element = React.createElement(Table, {
|
|
197
|
+
data: [{ name: 'test' }],
|
|
198
|
+
columns: [],
|
|
199
|
+
})
|
|
200
|
+
// Should not throw with empty columns
|
|
201
|
+
expect(() => renderComponent(element)).not.toThrow()
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('handles missing column key in data', async () => {
|
|
205
|
+
const { renderComponent, Table } = await import('@mdxui/terminal')
|
|
206
|
+
const element = React.createElement(Table, {
|
|
207
|
+
data: [{ name: 'test' }],
|
|
208
|
+
columns: [{ key: 'nonexistent', header: 'Missing' }],
|
|
209
|
+
})
|
|
210
|
+
// Should render without throwing, showing empty/undefined values
|
|
211
|
+
expect(() => renderComponent(element)).not.toThrow()
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
describe('renderComponent()', () => {
|
|
216
|
+
it('throws when given null element', async () => {
|
|
217
|
+
const { renderComponent } = await import('@mdxui/terminal')
|
|
218
|
+
// @ts-expect-error Testing invalid input
|
|
219
|
+
// Current implementation throws on null - does not handle gracefully
|
|
220
|
+
expect(() => renderComponent(null)).toThrow()
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('handles unknown component type gracefully', async () => {
|
|
224
|
+
const { renderComponent } = await import('@mdxui/terminal')
|
|
225
|
+
// Creating an element with an unknown type string
|
|
226
|
+
const unknownElement = React.createElement('unknown-component', {
|
|
227
|
+
children: 'content',
|
|
228
|
+
})
|
|
229
|
+
const result = renderComponent(unknownElement)
|
|
230
|
+
// Should return empty array or the children text for unknown types
|
|
231
|
+
expect(Array.isArray(result)).toBe(true)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('handles deeply nested invalid elements', async () => {
|
|
235
|
+
const { renderComponent, Box } = await import('@mdxui/terminal')
|
|
236
|
+
const element = React.createElement(Box, {
|
|
237
|
+
children: React.createElement('invalid', {
|
|
238
|
+
children: React.createElement('also-invalid', { children: 'text' }),
|
|
239
|
+
}),
|
|
240
|
+
})
|
|
241
|
+
expect(() => renderComponent(element)).not.toThrow()
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
// ============================================================================
|
|
247
|
+
// Keyboard Manager Error Handling
|
|
248
|
+
// ============================================================================
|
|
249
|
+
|
|
250
|
+
describe('Keyboard Manager', () => {
|
|
251
|
+
describe('binding validation', () => {
|
|
252
|
+
it('handles empty bindings object', async () => {
|
|
253
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
254
|
+
const manager = createKeyboardManager({ bindings: {} })
|
|
255
|
+
expect(manager.getAction('anything')).toBeUndefined()
|
|
256
|
+
expect(manager.handleKey('anything')).toBe(false)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('handles binding with empty string key', async () => {
|
|
260
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
261
|
+
const manager = createKeyboardManager({
|
|
262
|
+
bindings: { '': 'empty-key-action' },
|
|
263
|
+
})
|
|
264
|
+
// Empty string key behavior - should be retrievable
|
|
265
|
+
expect(manager.getAction('')).toBe('empty-key-action')
|
|
266
|
+
})
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
describe('destroyed manager behavior', () => {
|
|
270
|
+
it('clears pending sequence on destroy', async () => {
|
|
271
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
272
|
+
const manager = createKeyboardManager({
|
|
273
|
+
bindings: { gg: 'move-first' },
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
manager.handleKey('g')
|
|
277
|
+
expect(manager.getPendingSequence()).toBe('g')
|
|
278
|
+
|
|
279
|
+
manager.destroy()
|
|
280
|
+
expect(manager.getPendingSequence()).toBe('')
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('can be called multiple times safely', async () => {
|
|
284
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
285
|
+
const manager = createKeyboardManager({
|
|
286
|
+
bindings: { gg: 'move-first' },
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
manager.handleKey('g')
|
|
290
|
+
expect(() => {
|
|
291
|
+
manager.destroy()
|
|
292
|
+
manager.destroy()
|
|
293
|
+
manager.destroy()
|
|
294
|
+
}).not.toThrow()
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('handles calls after destroy without crashing', async () => {
|
|
298
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
299
|
+
const onAction = vi.fn()
|
|
300
|
+
const manager = createKeyboardManager({
|
|
301
|
+
bindings: { j: 'move-down' },
|
|
302
|
+
onAction,
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
manager.destroy()
|
|
306
|
+
|
|
307
|
+
// After destroy, the manager should still be usable but in disabled state
|
|
308
|
+
// or handle calls gracefully
|
|
309
|
+
expect(() => manager.handleKey('j')).not.toThrow()
|
|
310
|
+
expect(() => manager.getAction('j')).not.toThrow()
|
|
311
|
+
expect(() => manager.addBinding('k', 'move-up')).not.toThrow()
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
describe('sequence timeout behavior', () => {
|
|
316
|
+
it('sequence times out and triggers fallback action', async () => {
|
|
317
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
318
|
+
const onAction = vi.fn()
|
|
319
|
+
|
|
320
|
+
const manager = createKeyboardManager({
|
|
321
|
+
bindings: {
|
|
322
|
+
gg: 'move-first',
|
|
323
|
+
g: 'go', // Fallback if only single g
|
|
324
|
+
},
|
|
325
|
+
onAction,
|
|
326
|
+
sequenceTimeout: 100,
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
manager.handleKey('g')
|
|
330
|
+
|
|
331
|
+
// Wait for timeout to fire
|
|
332
|
+
await new Promise((r) => setTimeout(r, 150))
|
|
333
|
+
|
|
334
|
+
// Should have called the fallback 'g' action after timeout
|
|
335
|
+
expect(onAction).toHaveBeenCalledWith('go', { key: 'g' })
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('sequence timeout does nothing when no fallback exists', async () => {
|
|
339
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
340
|
+
const onAction = vi.fn()
|
|
341
|
+
|
|
342
|
+
const manager = createKeyboardManager({
|
|
343
|
+
bindings: {
|
|
344
|
+
gg: 'move-first',
|
|
345
|
+
// No fallback for single 'g'
|
|
346
|
+
},
|
|
347
|
+
onAction,
|
|
348
|
+
sequenceTimeout: 100,
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
manager.handleKey('g')
|
|
352
|
+
await new Promise((r) => setTimeout(r, 150))
|
|
353
|
+
|
|
354
|
+
// Should not have called any action since 'g' alone has no binding
|
|
355
|
+
expect(onAction).not.toHaveBeenCalled()
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
it('destroy clears pending sequence timer to prevent memory leak', async () => {
|
|
359
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
360
|
+
const onAction = vi.fn()
|
|
361
|
+
|
|
362
|
+
const manager = createKeyboardManager({
|
|
363
|
+
bindings: {
|
|
364
|
+
gg: 'move-first',
|
|
365
|
+
g: 'go',
|
|
366
|
+
},
|
|
367
|
+
onAction,
|
|
368
|
+
sequenceTimeout: 100,
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
// Start a sequence which creates a timer
|
|
372
|
+
manager.handleKey('g')
|
|
373
|
+
|
|
374
|
+
// Immediately destroy before timeout fires
|
|
375
|
+
manager.destroy()
|
|
376
|
+
|
|
377
|
+
const callCountBeforeWait = onAction.mock.calls.length
|
|
378
|
+
|
|
379
|
+
// Wait for what would have been the timeout
|
|
380
|
+
await new Promise((r) => setTimeout(r, 200))
|
|
381
|
+
|
|
382
|
+
// The timeout handler should NOT have been called because destroy cleared it
|
|
383
|
+
expect(onAction.mock.calls.length).toBe(callCountBeforeWait)
|
|
384
|
+
})
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
describe('disabled manager', () => {
|
|
388
|
+
it('ignores key handling when disabled', async () => {
|
|
389
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
390
|
+
const onAction = vi.fn()
|
|
391
|
+
|
|
392
|
+
const manager = createKeyboardManager({
|
|
393
|
+
bindings: { j: 'move-down' },
|
|
394
|
+
onAction,
|
|
395
|
+
enabled: false,
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
const result = manager.handleKey('j')
|
|
399
|
+
expect(result).toBe(false)
|
|
400
|
+
expect(onAction).not.toHaveBeenCalled()
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
it('enable/disable toggles work correctly', async () => {
|
|
404
|
+
const { createKeyboardManager } = await import('@mdxui/terminal')
|
|
405
|
+
const onAction = vi.fn()
|
|
406
|
+
|
|
407
|
+
const manager = createKeyboardManager({
|
|
408
|
+
bindings: { j: 'move-down' },
|
|
409
|
+
onAction,
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
manager.disable()
|
|
413
|
+
manager.handleKey('j')
|
|
414
|
+
expect(onAction).not.toHaveBeenCalled()
|
|
415
|
+
|
|
416
|
+
manager.enable()
|
|
417
|
+
manager.handleKey('j')
|
|
418
|
+
expect(onAction).toHaveBeenCalledWith('move-down', { key: 'j' })
|
|
419
|
+
})
|
|
420
|
+
})
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
// ============================================================================
|
|
424
|
+
// Theme System Error Handling
|
|
425
|
+
// ============================================================================
|
|
426
|
+
|
|
427
|
+
describe('Theme System', () => {
|
|
428
|
+
describe('createTheme()', () => {
|
|
429
|
+
it('throws ZodError for invalid ANSI color values', async () => {
|
|
430
|
+
const { createTheme } = await import('@mdxui/terminal')
|
|
431
|
+
// With Zod validation, the error message comes from the schema
|
|
432
|
+
expect(() =>
|
|
433
|
+
createTheme({
|
|
434
|
+
primary: 'not-an-ansi-code',
|
|
435
|
+
})
|
|
436
|
+
).toThrow(/Invalid ANSI escape sequence/)
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
it('accepts empty string as valid color (no color)', async () => {
|
|
440
|
+
const { createTheme } = await import('@mdxui/terminal')
|
|
441
|
+
expect(() =>
|
|
442
|
+
createTheme({
|
|
443
|
+
background: '',
|
|
444
|
+
})
|
|
445
|
+
).not.toThrow()
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
it('accepts valid ANSI codes', async () => {
|
|
449
|
+
const { createTheme } = await import('@mdxui/terminal')
|
|
450
|
+
expect(() =>
|
|
451
|
+
createTheme({
|
|
452
|
+
primary: '\x1b[34m',
|
|
453
|
+
secondary: '\x1b[38;5;45m',
|
|
454
|
+
})
|
|
455
|
+
).not.toThrow()
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
it('creates theme with defaults when no options provided', async () => {
|
|
459
|
+
const { createTerminalTheme } = await import('@mdxui/terminal')
|
|
460
|
+
const theme = createTerminalTheme({})
|
|
461
|
+
expect(theme.mode).toBe('dark')
|
|
462
|
+
expect(theme.colors).toBeDefined()
|
|
463
|
+
expect(theme.colors.primary).toBeDefined()
|
|
464
|
+
})
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
describe('extendTheme()', () => {
|
|
468
|
+
it('merges partial themes safely without modifying original', async () => {
|
|
469
|
+
const { extendTheme, defaultTheme } = await import('@mdxui/terminal')
|
|
470
|
+
|
|
471
|
+
const originalPrimary = defaultTheme.primary
|
|
472
|
+
const extended = extendTheme(defaultTheme, {
|
|
473
|
+
primary: '\x1b[35m',
|
|
474
|
+
})
|
|
475
|
+
|
|
476
|
+
// Original should not be modified
|
|
477
|
+
expect(defaultTheme.primary).toBe(originalPrimary)
|
|
478
|
+
// Extended should have new value
|
|
479
|
+
expect(extended.primary).toBe('\x1b[35m')
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
it('handles empty override object', async () => {
|
|
483
|
+
const { extendTheme, defaultTheme } = await import('@mdxui/terminal')
|
|
484
|
+
const extended = extendTheme(defaultTheme, {})
|
|
485
|
+
expect(extended).toEqual(defaultTheme)
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
it('deep merges nested objects like typography and spacing', async () => {
|
|
489
|
+
const { extendTheme, defaultTheme } = await import('@mdxui/terminal')
|
|
490
|
+
const extended = extendTheme(defaultTheme, {
|
|
491
|
+
typography: { headingWeight: 'normal' },
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
// Should have merged the typography object
|
|
495
|
+
expect(extended.typography?.headingWeight).toBe('normal')
|
|
496
|
+
// Other typography properties should be preserved
|
|
497
|
+
expect(extended.typography?.bodyWeight).toBe('normal')
|
|
498
|
+
expect(extended.typography?.codeFont).toBe('monospace')
|
|
499
|
+
})
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
describe('getThemeColor()', () => {
|
|
503
|
+
it('throws when unknown key and no fallback provided', async () => {
|
|
504
|
+
const { getThemeColor, createTerminalTheme } = await import('@mdxui/terminal')
|
|
505
|
+
const theme = createTerminalTheme({ mode: 'dark' })
|
|
506
|
+
|
|
507
|
+
expect(() =>
|
|
508
|
+
getThemeColor(theme, 'nonexistent' as unknown as 'primary')
|
|
509
|
+
).toThrow()
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
it('returns fallback for unknown key when provided', async () => {
|
|
513
|
+
const { getThemeColor, createTerminalTheme } = await import('@mdxui/terminal')
|
|
514
|
+
const theme = createTerminalTheme({ mode: 'dark' })
|
|
515
|
+
const fallback = '\x1b[37m'
|
|
516
|
+
|
|
517
|
+
const color = getThemeColor(
|
|
518
|
+
theme,
|
|
519
|
+
'nonexistent' as unknown as 'primary',
|
|
520
|
+
fallback
|
|
521
|
+
)
|
|
522
|
+
expect(color).toBe(fallback)
|
|
523
|
+
})
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
describe('degradeColor()', () => {
|
|
527
|
+
it('returns empty string for support level none', async () => {
|
|
528
|
+
const { degradeColor } = await import('@mdxui/terminal')
|
|
529
|
+
const color = '\x1b[38;5;33m'
|
|
530
|
+
expect(degradeColor(color, 'none')).toBe('')
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
it('passes through valid color when support matches', async () => {
|
|
534
|
+
const { degradeColor } = await import('@mdxui/terminal')
|
|
535
|
+
const color256 = '\x1b[38;5;33m'
|
|
536
|
+
expect(degradeColor(color256, '256')).toBe(color256)
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
it('handles empty string input', async () => {
|
|
540
|
+
const { degradeColor } = await import('@mdxui/terminal')
|
|
541
|
+
expect(degradeColor('', '256')).toBe('')
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
it('handles invalid ANSI code gracefully', async () => {
|
|
545
|
+
const { degradeColor } = await import('@mdxui/terminal')
|
|
546
|
+
// Invalid ANSI should pass through unchanged
|
|
547
|
+
expect(degradeColor('not-ansi', '256')).toBe('not-ansi')
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
it('converts truecolor to 256 correctly', async () => {
|
|
551
|
+
const { degradeColor } = await import('@mdxui/terminal')
|
|
552
|
+
const truecolor = '\x1b[38;2;59;130;246m' // RGB for blue-500
|
|
553
|
+
const degraded = degradeColor(truecolor, '256')
|
|
554
|
+
// Should be a valid 256-color code
|
|
555
|
+
expect(degraded).toMatch(/\x1b\[38;5;\d+m/)
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
it('converts 256 to 16 color correctly', async () => {
|
|
559
|
+
const { degradeColor } = await import('@mdxui/terminal')
|
|
560
|
+
const color256 = '\x1b[38;5;33m' // Blue in 256 palette
|
|
561
|
+
const degraded = degradeColor(color256, '16')
|
|
562
|
+
// Should be a basic 16-color code (30-37 or 90-97)
|
|
563
|
+
expect(degraded).toMatch(/\x1b\[(3[0-7]|9[0-7])m/)
|
|
564
|
+
})
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
describe('cssVarToAnsi()', () => {
|
|
568
|
+
it('returns empty string for unknown CSS variables', async () => {
|
|
569
|
+
const { cssVarToAnsi, createTerminalTheme } = await import('@mdxui/terminal')
|
|
570
|
+
const theme = createTerminalTheme({ mode: 'dark' })
|
|
571
|
+
expect(cssVarToAnsi('--unknown-variable', theme)).toBe('')
|
|
572
|
+
expect(cssVarToAnsi('not-a-css-var', theme)).toBe('')
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
it('handles empty string CSS variable', async () => {
|
|
576
|
+
const { cssVarToAnsi, createTerminalTheme } = await import('@mdxui/terminal')
|
|
577
|
+
const theme = createTerminalTheme({ mode: 'dark' })
|
|
578
|
+
expect(cssVarToAnsi('', theme)).toBe('')
|
|
579
|
+
})
|
|
580
|
+
})
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
// ============================================================================
|
|
584
|
+
// Input Normalization Edge Cases
|
|
585
|
+
// ============================================================================
|
|
586
|
+
|
|
587
|
+
describe('Input Normalization', () => {
|
|
588
|
+
describe('normalizeReadlineKey()', () => {
|
|
589
|
+
it('handles undefined string and key', async () => {
|
|
590
|
+
const { normalizeReadlineKey } = await import('@mdxui/terminal')
|
|
591
|
+
const result = normalizeReadlineKey(undefined, undefined)
|
|
592
|
+
expect(result.name).toBe('')
|
|
593
|
+
expect(result.ctrl).toBe(false)
|
|
594
|
+
expect(result.alt).toBe(false)
|
|
595
|
+
expect(result.shift).toBe(false)
|
|
596
|
+
expect(result.meta).toBe(false)
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
it('handles empty string input', async () => {
|
|
600
|
+
const { normalizeReadlineKey } = await import('@mdxui/terminal')
|
|
601
|
+
const result = normalizeReadlineKey('', undefined)
|
|
602
|
+
expect(result.name).toBe('')
|
|
603
|
+
})
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
describe('keyToBindingString()', () => {
|
|
607
|
+
it('handles key with no modifiers', async () => {
|
|
608
|
+
const { keyToBindingString } = await import('@mdxui/terminal')
|
|
609
|
+
const result = keyToBindingString({
|
|
610
|
+
name: 'a',
|
|
611
|
+
ctrl: false,
|
|
612
|
+
alt: false,
|
|
613
|
+
shift: false,
|
|
614
|
+
meta: false,
|
|
615
|
+
})
|
|
616
|
+
expect(result).toBe('a')
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
it('handles empty key name', async () => {
|
|
620
|
+
const { keyToBindingString } = await import('@mdxui/terminal')
|
|
621
|
+
const result = keyToBindingString({
|
|
622
|
+
name: '',
|
|
623
|
+
ctrl: false,
|
|
624
|
+
alt: false,
|
|
625
|
+
shift: false,
|
|
626
|
+
meta: false,
|
|
627
|
+
})
|
|
628
|
+
expect(result).toBe('')
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
it('handles empty key name with modifiers', async () => {
|
|
632
|
+
const { keyToBindingString } = await import('@mdxui/terminal')
|
|
633
|
+
const result = keyToBindingString({
|
|
634
|
+
name: '',
|
|
635
|
+
ctrl: true,
|
|
636
|
+
alt: false,
|
|
637
|
+
shift: false,
|
|
638
|
+
meta: false,
|
|
639
|
+
})
|
|
640
|
+
// Should produce 'ctrl+' with empty key
|
|
641
|
+
expect(result).toBe('ctrl+')
|
|
642
|
+
})
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
describe('attachKeyboardManager()', () => {
|
|
646
|
+
it('throws error for non-TTY stdin', async () => {
|
|
647
|
+
const { createKeyboardManager, attachKeyboardManager } = await import('@mdxui/terminal')
|
|
648
|
+
const manager = createKeyboardManager({ bindings: {} })
|
|
649
|
+
|
|
650
|
+
const mockStream = {
|
|
651
|
+
isTTY: false,
|
|
652
|
+
} as NodeJS.ReadStream
|
|
653
|
+
|
|
654
|
+
expect(() => attachKeyboardManager(manager, { input: mockStream })).toThrow(
|
|
655
|
+
/requires a TTY stdin/
|
|
656
|
+
)
|
|
657
|
+
})
|
|
658
|
+
})
|
|
659
|
+
})
|
|
660
|
+
})
|