@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,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
|
+
})
|