@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,658 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal Select Component Tests (RED phase)
|
|
3
|
+
*
|
|
4
|
+
* TDD RED Phase: These tests define the contract for the Select component
|
|
5
|
+
* that renders dropdown/list selection with options and keyboard navigation.
|
|
6
|
+
*
|
|
7
|
+
* Rendering across tiers:
|
|
8
|
+
* - TEXT: Plain list with selected indicator
|
|
9
|
+
* - MARKDOWN: Markdown list with selection marker
|
|
10
|
+
* - ASCII: ASCII box with scrollable list
|
|
11
|
+
* - UNICODE: Unicode box with scroll indicators
|
|
12
|
+
* - ANSI: Colors for selection and hover states
|
|
13
|
+
* - INTERACTIVE: Full keyboard navigation, type-to-search
|
|
14
|
+
*
|
|
15
|
+
* NOTE: These tests are expected to FAIL until implementation is complete.
|
|
16
|
+
*/
|
|
17
|
+
import { describe, it, expect } from 'vitest'
|
|
18
|
+
import type { UINode, RenderTier, RenderContext, ThemeTokens } from '../../../core/types'
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Test Utilities
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
const RENDER_TIERS: RenderTier[] = ['text', 'markdown', 'ascii', 'unicode', 'ansi', 'interactive']
|
|
25
|
+
|
|
26
|
+
const mockTheme: ThemeTokens = {
|
|
27
|
+
primary: '\x1b[34m',
|
|
28
|
+
secondary: '\x1b[36m',
|
|
29
|
+
muted: '\x1b[90m',
|
|
30
|
+
foreground: '\x1b[37m',
|
|
31
|
+
background: '\x1b[40m',
|
|
32
|
+
border: '\x1b[90m',
|
|
33
|
+
success: '\x1b[32m',
|
|
34
|
+
warning: '\x1b[33m',
|
|
35
|
+
error: '\x1b[31m',
|
|
36
|
+
info: '\x1b[34m',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createContext(tier: RenderTier, overrides: Partial<RenderContext> = {}): RenderContext {
|
|
40
|
+
return {
|
|
41
|
+
tier,
|
|
42
|
+
width: 80,
|
|
43
|
+
height: 24,
|
|
44
|
+
depth: 0,
|
|
45
|
+
theme: mockTheme,
|
|
46
|
+
interactive: tier === 'interactive',
|
|
47
|
+
...overrides,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createNode(
|
|
52
|
+
type: string,
|
|
53
|
+
props: Record<string, unknown> = {},
|
|
54
|
+
children?: UINode[]
|
|
55
|
+
): UINode {
|
|
56
|
+
return { type, props, children }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createSelectNode(props: Record<string, unknown>): UINode {
|
|
60
|
+
return createNode('select', props)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// Basic Rendering Tests
|
|
65
|
+
// ============================================================================
|
|
66
|
+
|
|
67
|
+
describe('Select Component', () => {
|
|
68
|
+
describe('function signature', () => {
|
|
69
|
+
it('exports renderSelect function', async () => {
|
|
70
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
71
|
+
expect(typeof renderSelect).toBe('function')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('accepts UINode and RenderContext and returns string', async () => {
|
|
75
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
76
|
+
const node = createSelectNode({
|
|
77
|
+
options: [{ label: 'Option 1', value: 'opt1' }],
|
|
78
|
+
})
|
|
79
|
+
const ctx = createContext('text')
|
|
80
|
+
const result = renderSelect(node, ctx)
|
|
81
|
+
expect(typeof result).toBe('string')
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// Options Rendering Tests
|
|
87
|
+
// ============================================================================
|
|
88
|
+
|
|
89
|
+
describe('options rendering', () => {
|
|
90
|
+
RENDER_TIERS.forEach((tier) => {
|
|
91
|
+
describe(`[${tier}] tier`, () => {
|
|
92
|
+
it('renders single option', async () => {
|
|
93
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
94
|
+
const node = createSelectNode({
|
|
95
|
+
options: [{ label: 'First Option', value: 'first' }],
|
|
96
|
+
})
|
|
97
|
+
const ctx = createContext(tier)
|
|
98
|
+
const result = renderSelect(node, ctx)
|
|
99
|
+
|
|
100
|
+
expect(result).toContain('First Option')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('renders multiple options', async () => {
|
|
104
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
105
|
+
const node = createSelectNode({
|
|
106
|
+
options: [
|
|
107
|
+
{ label: 'Option A', value: 'a' },
|
|
108
|
+
{ label: 'Option B', value: 'b' },
|
|
109
|
+
{ label: 'Option C', value: 'c' },
|
|
110
|
+
],
|
|
111
|
+
})
|
|
112
|
+
const ctx = createContext(tier)
|
|
113
|
+
const result = renderSelect(node, ctx)
|
|
114
|
+
|
|
115
|
+
expect(result).toContain('Option A')
|
|
116
|
+
expect(result).toContain('Option B')
|
|
117
|
+
expect(result).toContain('Option C')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('preserves option order', async () => {
|
|
121
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
122
|
+
const node = createSelectNode({
|
|
123
|
+
options: [
|
|
124
|
+
{ label: 'First', value: '1' },
|
|
125
|
+
{ label: 'Second', value: '2' },
|
|
126
|
+
{ label: 'Third', value: '3' },
|
|
127
|
+
],
|
|
128
|
+
})
|
|
129
|
+
const ctx = createContext(tier)
|
|
130
|
+
const result = renderSelect(node, ctx)
|
|
131
|
+
|
|
132
|
+
const firstPos = result.indexOf('First')
|
|
133
|
+
const secondPos = result.indexOf('Second')
|
|
134
|
+
const thirdPos = result.indexOf('Third')
|
|
135
|
+
|
|
136
|
+
expect(firstPos).toBeLessThan(secondPos)
|
|
137
|
+
expect(secondPos).toBeLessThan(thirdPos)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('renders empty select gracefully', async () => {
|
|
141
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
142
|
+
const node = createSelectNode({ options: [] })
|
|
143
|
+
const ctx = createContext(tier)
|
|
144
|
+
const result = renderSelect(node, ctx)
|
|
145
|
+
|
|
146
|
+
expect(typeof result).toBe('string')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('renders select with label', async () => {
|
|
150
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
151
|
+
const node = createSelectNode({
|
|
152
|
+
label: 'Choose Country',
|
|
153
|
+
options: [{ label: 'USA', value: 'us' }],
|
|
154
|
+
})
|
|
155
|
+
const ctx = createContext(tier)
|
|
156
|
+
const result = renderSelect(node, ctx)
|
|
157
|
+
|
|
158
|
+
expect(result).toContain('Choose Country')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('renders placeholder when no selection', async () => {
|
|
162
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
163
|
+
const node = createSelectNode({
|
|
164
|
+
placeholder: 'Select an option',
|
|
165
|
+
options: [{ label: 'Option 1', value: '1' }],
|
|
166
|
+
})
|
|
167
|
+
const ctx = createContext(tier)
|
|
168
|
+
const result = renderSelect(node, ctx)
|
|
169
|
+
|
|
170
|
+
expect(result).toContain('Select an option')
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
// ============================================================================
|
|
177
|
+
// Selection Indicator Tests
|
|
178
|
+
// ============================================================================
|
|
179
|
+
|
|
180
|
+
describe('selection indicator', () => {
|
|
181
|
+
RENDER_TIERS.forEach((tier) => {
|
|
182
|
+
describe(`[${tier}] tier`, () => {
|
|
183
|
+
it('shows selected option', async () => {
|
|
184
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
185
|
+
const node = createSelectNode({
|
|
186
|
+
value: 'b',
|
|
187
|
+
options: [
|
|
188
|
+
{ label: 'Option A', value: 'a' },
|
|
189
|
+
{ label: 'Option B', value: 'b' },
|
|
190
|
+
{ label: 'Option C', value: 'c' },
|
|
191
|
+
],
|
|
192
|
+
})
|
|
193
|
+
const ctx = createContext(tier)
|
|
194
|
+
const result = renderSelect(node, ctx)
|
|
195
|
+
|
|
196
|
+
expect(result).toContain('Option B')
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('distinguishes selected from unselected', async () => {
|
|
200
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
201
|
+
const node = createSelectNode({
|
|
202
|
+
value: 'selected',
|
|
203
|
+
options: [
|
|
204
|
+
{ label: 'Selected Item', value: 'selected' },
|
|
205
|
+
{ label: 'Other Item', value: 'other' },
|
|
206
|
+
],
|
|
207
|
+
})
|
|
208
|
+
const ctx = createContext(tier)
|
|
209
|
+
const result = renderSelect(node, ctx)
|
|
210
|
+
|
|
211
|
+
expect(result).toMatch(/[✓✔●◉>*\[\*\]]/i)
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
describe('[ansi] tier selection styling', () => {
|
|
217
|
+
it('highlights selected option', async () => {
|
|
218
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
219
|
+
const node = createSelectNode({
|
|
220
|
+
value: 'selected',
|
|
221
|
+
options: [
|
|
222
|
+
{ label: 'Selected', value: 'selected' },
|
|
223
|
+
{ label: 'Other', value: 'other' },
|
|
224
|
+
],
|
|
225
|
+
})
|
|
226
|
+
const ctx = createContext('ansi')
|
|
227
|
+
const result = renderSelect(node, ctx)
|
|
228
|
+
|
|
229
|
+
expect(result).toMatch(/\x1b\[(34|32)m/)
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
// ============================================================================
|
|
235
|
+
// Dropdown State Tests
|
|
236
|
+
// ============================================================================
|
|
237
|
+
|
|
238
|
+
describe('dropdown state', () => {
|
|
239
|
+
RENDER_TIERS.forEach((tier) => {
|
|
240
|
+
describe(`[${tier}] tier`, () => {
|
|
241
|
+
it('renders collapsed state showing only selected', async () => {
|
|
242
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
243
|
+
const node = createSelectNode({
|
|
244
|
+
open: false,
|
|
245
|
+
value: 'selected',
|
|
246
|
+
options: [
|
|
247
|
+
{ label: 'Selected Option', value: 'selected' },
|
|
248
|
+
{ label: 'Other Option', value: 'other' },
|
|
249
|
+
],
|
|
250
|
+
})
|
|
251
|
+
const ctx = createContext(tier)
|
|
252
|
+
const result = renderSelect(node, ctx)
|
|
253
|
+
|
|
254
|
+
expect(result).toContain('Selected Option')
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('renders expanded state showing all options', async () => {
|
|
258
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
259
|
+
const node = createSelectNode({
|
|
260
|
+
open: true,
|
|
261
|
+
value: 'b',
|
|
262
|
+
options: [
|
|
263
|
+
{ label: 'Option A', value: 'a' },
|
|
264
|
+
{ label: 'Option B', value: 'b' },
|
|
265
|
+
{ label: 'Option C', value: 'c' },
|
|
266
|
+
],
|
|
267
|
+
})
|
|
268
|
+
const ctx = createContext(tier)
|
|
269
|
+
const result = renderSelect(node, ctx)
|
|
270
|
+
|
|
271
|
+
expect(result).toContain('Option A')
|
|
272
|
+
expect(result).toContain('Option B')
|
|
273
|
+
expect(result).toContain('Option C')
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('shows dropdown indicator', async () => {
|
|
277
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
278
|
+
const node = createSelectNode({
|
|
279
|
+
open: false,
|
|
280
|
+
options: [{ label: 'Option', value: 'opt' }],
|
|
281
|
+
})
|
|
282
|
+
const ctx = createContext(tier)
|
|
283
|
+
const result = renderSelect(node, ctx)
|
|
284
|
+
|
|
285
|
+
expect(result).toMatch(/[▼▾↓v>]/i)
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
// ============================================================================
|
|
292
|
+
// Disabled Options Tests
|
|
293
|
+
// ============================================================================
|
|
294
|
+
|
|
295
|
+
describe('disabled options', () => {
|
|
296
|
+
RENDER_TIERS.forEach((tier) => {
|
|
297
|
+
describe(`[${tier}] tier`, () => {
|
|
298
|
+
it('renders disabled option', async () => {
|
|
299
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
300
|
+
const node = createSelectNode({
|
|
301
|
+
options: [
|
|
302
|
+
{ label: 'Available', value: 'available' },
|
|
303
|
+
{ label: 'Unavailable', value: 'unavailable', disabled: true },
|
|
304
|
+
],
|
|
305
|
+
})
|
|
306
|
+
const ctx = createContext(tier)
|
|
307
|
+
const result = renderSelect(node, ctx)
|
|
308
|
+
|
|
309
|
+
expect(result).toContain('Available')
|
|
310
|
+
expect(result).toContain('Unavailable')
|
|
311
|
+
})
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
describe('[ansi] tier disabled styling', () => {
|
|
316
|
+
it('renders disabled option as dimmed', async () => {
|
|
317
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
318
|
+
const node = createSelectNode({
|
|
319
|
+
options: [
|
|
320
|
+
{ label: 'Disabled', value: 'disabled', disabled: true },
|
|
321
|
+
],
|
|
322
|
+
})
|
|
323
|
+
const ctx = createContext('ansi')
|
|
324
|
+
const result = renderSelect(node, ctx)
|
|
325
|
+
|
|
326
|
+
expect(result).toContain('\x1b[2m')
|
|
327
|
+
})
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
// ============================================================================
|
|
332
|
+
// Keyboard Navigation Tests (Interactive Tier)
|
|
333
|
+
// ============================================================================
|
|
334
|
+
|
|
335
|
+
describe('[interactive] tier keyboard navigation', () => {
|
|
336
|
+
it('shows currently highlighted option', async () => {
|
|
337
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
338
|
+
const node = createSelectNode({
|
|
339
|
+
open: true,
|
|
340
|
+
highlightedIndex: 1,
|
|
341
|
+
options: [
|
|
342
|
+
{ label: 'Option A', value: 'a' },
|
|
343
|
+
{ label: 'Option B', value: 'b' },
|
|
344
|
+
{ label: 'Option C', value: 'c' },
|
|
345
|
+
],
|
|
346
|
+
})
|
|
347
|
+
const ctx = createContext('interactive')
|
|
348
|
+
const result = renderSelect(node, ctx)
|
|
349
|
+
|
|
350
|
+
expect(result).toContain('Option B')
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
it('shows keyboard navigation hints', async () => {
|
|
354
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
355
|
+
const node = createSelectNode({
|
|
356
|
+
open: true,
|
|
357
|
+
options: [{ label: 'Option', value: 'opt' }],
|
|
358
|
+
})
|
|
359
|
+
const ctx = createContext('interactive')
|
|
360
|
+
const result = renderSelect(node, ctx)
|
|
361
|
+
|
|
362
|
+
expect(result).toMatch(/↑|↓|Enter|Esc|Arrow/i)
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it('renders search input when searchable', async () => {
|
|
366
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
367
|
+
const node = createSelectNode({
|
|
368
|
+
open: true,
|
|
369
|
+
searchable: true,
|
|
370
|
+
searchValue: 'app',
|
|
371
|
+
options: [
|
|
372
|
+
{ label: 'Apple', value: 'apple' },
|
|
373
|
+
{ label: 'Banana', value: 'banana' },
|
|
374
|
+
],
|
|
375
|
+
})
|
|
376
|
+
const ctx = createContext('interactive')
|
|
377
|
+
const result = renderSelect(node, ctx)
|
|
378
|
+
|
|
379
|
+
expect(result).toContain('app')
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
it('shows no results message when filter has no matches', async () => {
|
|
383
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
384
|
+
const node = createSelectNode({
|
|
385
|
+
open: true,
|
|
386
|
+
searchable: true,
|
|
387
|
+
searchValue: 'xyz',
|
|
388
|
+
filteredOptions: [],
|
|
389
|
+
options: [
|
|
390
|
+
{ label: 'Apple', value: 'apple' },
|
|
391
|
+
],
|
|
392
|
+
})
|
|
393
|
+
const ctx = createContext('interactive')
|
|
394
|
+
const result = renderSelect(node, ctx)
|
|
395
|
+
|
|
396
|
+
expect(result).toMatch(/no.*results|no.*matches|not found/i)
|
|
397
|
+
})
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
// ============================================================================
|
|
401
|
+
// ASCII/Unicode Border Tests
|
|
402
|
+
// ============================================================================
|
|
403
|
+
|
|
404
|
+
describe('border rendering', () => {
|
|
405
|
+
describe('[ascii] tier', () => {
|
|
406
|
+
it('renders ASCII box when open', async () => {
|
|
407
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
408
|
+
const node = createSelectNode({
|
|
409
|
+
open: true,
|
|
410
|
+
options: [
|
|
411
|
+
{ label: 'Option 1', value: '1' },
|
|
412
|
+
{ label: 'Option 2', value: '2' },
|
|
413
|
+
],
|
|
414
|
+
})
|
|
415
|
+
const ctx = createContext('ascii')
|
|
416
|
+
const result = renderSelect(node, ctx)
|
|
417
|
+
|
|
418
|
+
expect(result).toMatch(/[+\-|]/)
|
|
419
|
+
})
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
describe('[unicode] tier', () => {
|
|
423
|
+
it('renders Unicode box when open', async () => {
|
|
424
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
425
|
+
const node = createSelectNode({
|
|
426
|
+
open: true,
|
|
427
|
+
options: [
|
|
428
|
+
{ label: 'Option 1', value: '1' },
|
|
429
|
+
{ label: 'Option 2', value: '2' },
|
|
430
|
+
],
|
|
431
|
+
})
|
|
432
|
+
const ctx = createContext('unicode')
|
|
433
|
+
const result = renderSelect(node, ctx)
|
|
434
|
+
|
|
435
|
+
expect(result).toMatch(/[┌┐└┘─│]/)
|
|
436
|
+
})
|
|
437
|
+
})
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
// ============================================================================
|
|
441
|
+
// Multi-Select Tests
|
|
442
|
+
// ============================================================================
|
|
443
|
+
|
|
444
|
+
describe('multi-select mode', () => {
|
|
445
|
+
RENDER_TIERS.forEach((tier) => {
|
|
446
|
+
describe(`[${tier}] tier`, () => {
|
|
447
|
+
it('renders checkboxes for multi-select', async () => {
|
|
448
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
449
|
+
const node = createSelectNode({
|
|
450
|
+
multiple: true,
|
|
451
|
+
options: [
|
|
452
|
+
{ label: 'Option A', value: 'a' },
|
|
453
|
+
{ label: 'Option B', value: 'b' },
|
|
454
|
+
],
|
|
455
|
+
})
|
|
456
|
+
const ctx = createContext(tier)
|
|
457
|
+
const result = renderSelect(node, ctx)
|
|
458
|
+
|
|
459
|
+
expect(result).toMatch(/[☐☑✓✔\[\s\]\[x\]]/i)
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
it('shows multiple selected values', async () => {
|
|
463
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
464
|
+
const node = createSelectNode({
|
|
465
|
+
multiple: true,
|
|
466
|
+
value: ['a', 'c'],
|
|
467
|
+
options: [
|
|
468
|
+
{ label: 'Option A', value: 'a' },
|
|
469
|
+
{ label: 'Option B', value: 'b' },
|
|
470
|
+
{ label: 'Option C', value: 'c' },
|
|
471
|
+
],
|
|
472
|
+
})
|
|
473
|
+
const ctx = createContext(tier)
|
|
474
|
+
const result = renderSelect(node, ctx)
|
|
475
|
+
|
|
476
|
+
expect(result).toContain('Option A')
|
|
477
|
+
expect(result).toContain('Option C')
|
|
478
|
+
})
|
|
479
|
+
})
|
|
480
|
+
})
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
// ============================================================================
|
|
484
|
+
// Validation Tests
|
|
485
|
+
// ============================================================================
|
|
486
|
+
|
|
487
|
+
describe('validation', () => {
|
|
488
|
+
RENDER_TIERS.forEach((tier) => {
|
|
489
|
+
describe(`[${tier}] tier`, () => {
|
|
490
|
+
it('renders required indicator', async () => {
|
|
491
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
492
|
+
const node = createSelectNode({
|
|
493
|
+
label: 'Country',
|
|
494
|
+
required: true,
|
|
495
|
+
options: [{ label: 'USA', value: 'us' }],
|
|
496
|
+
})
|
|
497
|
+
const ctx = createContext(tier)
|
|
498
|
+
const result = renderSelect(node, ctx)
|
|
499
|
+
|
|
500
|
+
expect(result).toMatch(/\*|required/i)
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
it('renders error message', async () => {
|
|
504
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
505
|
+
const node = createSelectNode({
|
|
506
|
+
options: [{ label: 'Option', value: 'opt' }],
|
|
507
|
+
error: 'Please select an option',
|
|
508
|
+
})
|
|
509
|
+
const ctx = createContext(tier)
|
|
510
|
+
const result = renderSelect(node, ctx)
|
|
511
|
+
|
|
512
|
+
expect(result).toContain('Please select an option')
|
|
513
|
+
})
|
|
514
|
+
})
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
describe('[ansi] tier validation styling', () => {
|
|
518
|
+
it('renders error in red', async () => {
|
|
519
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
520
|
+
const node = createSelectNode({
|
|
521
|
+
options: [{ label: 'Option', value: 'opt' }],
|
|
522
|
+
error: 'Error message',
|
|
523
|
+
})
|
|
524
|
+
const ctx = createContext('ansi')
|
|
525
|
+
const result = renderSelect(node, ctx)
|
|
526
|
+
|
|
527
|
+
expect(result).toContain('\x1b[31m')
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
it('renders valid state in green', async () => {
|
|
531
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
532
|
+
const node = createSelectNode({
|
|
533
|
+
value: 'opt',
|
|
534
|
+
valid: true,
|
|
535
|
+
options: [{ label: 'Option', value: 'opt' }],
|
|
536
|
+
})
|
|
537
|
+
const ctx = createContext('ansi')
|
|
538
|
+
const result = renderSelect(node, ctx)
|
|
539
|
+
|
|
540
|
+
expect(result).toContain('\x1b[32m')
|
|
541
|
+
})
|
|
542
|
+
})
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
// ============================================================================
|
|
546
|
+
// Disabled State Tests
|
|
547
|
+
// ============================================================================
|
|
548
|
+
|
|
549
|
+
describe('disabled state', () => {
|
|
550
|
+
RENDER_TIERS.forEach((tier) => {
|
|
551
|
+
it(`[${tier}] renders disabled select`, async () => {
|
|
552
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
553
|
+
const node = createSelectNode({
|
|
554
|
+
disabled: true,
|
|
555
|
+
value: 'selected',
|
|
556
|
+
options: [
|
|
557
|
+
{ label: 'Selected Option', value: 'selected' },
|
|
558
|
+
{ label: 'Other', value: 'other' },
|
|
559
|
+
],
|
|
560
|
+
})
|
|
561
|
+
const ctx = createContext(tier)
|
|
562
|
+
const result = renderSelect(node, ctx)
|
|
563
|
+
|
|
564
|
+
expect(result).toContain('Selected Option')
|
|
565
|
+
})
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
describe('[ansi] tier disabled styling', () => {
|
|
569
|
+
it('renders disabled select as dimmed', async () => {
|
|
570
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
571
|
+
const node = createSelectNode({
|
|
572
|
+
disabled: true,
|
|
573
|
+
options: [{ label: 'Option', value: 'opt' }],
|
|
574
|
+
})
|
|
575
|
+
const ctx = createContext('ansi')
|
|
576
|
+
const result = renderSelect(node, ctx)
|
|
577
|
+
|
|
578
|
+
expect(result).toContain('\x1b[2m')
|
|
579
|
+
})
|
|
580
|
+
})
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
// ============================================================================
|
|
584
|
+
// Loading State Tests
|
|
585
|
+
// ============================================================================
|
|
586
|
+
|
|
587
|
+
describe('loading state', () => {
|
|
588
|
+
RENDER_TIERS.forEach((tier) => {
|
|
589
|
+
it(`[${tier}] renders loading indicator`, async () => {
|
|
590
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
591
|
+
const node = createSelectNode({
|
|
592
|
+
loading: true,
|
|
593
|
+
options: [],
|
|
594
|
+
})
|
|
595
|
+
const ctx = createContext(tier)
|
|
596
|
+
const result = renderSelect(node, ctx)
|
|
597
|
+
|
|
598
|
+
expect(result).toMatch(/loading|\.{3}|⠋|spinner/i)
|
|
599
|
+
})
|
|
600
|
+
})
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
// ============================================================================
|
|
604
|
+
// Edge Cases
|
|
605
|
+
// ============================================================================
|
|
606
|
+
|
|
607
|
+
describe('edge cases', () => {
|
|
608
|
+
it('handles missing options array', async () => {
|
|
609
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
610
|
+
const node = createSelectNode({})
|
|
611
|
+
const ctx = createContext('text')
|
|
612
|
+
const result = renderSelect(node, ctx)
|
|
613
|
+
|
|
614
|
+
expect(typeof result).toBe('string')
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
it('handles null value', async () => {
|
|
618
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
619
|
+
const node = createSelectNode({
|
|
620
|
+
value: null,
|
|
621
|
+
options: [{ label: 'Option', value: 'opt' }],
|
|
622
|
+
})
|
|
623
|
+
const ctx = createContext('text')
|
|
624
|
+
const result = renderSelect(node, ctx)
|
|
625
|
+
|
|
626
|
+
expect(result).toContain('Option')
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
it('handles unicode in options', async () => {
|
|
630
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
631
|
+
const node = createSelectNode({
|
|
632
|
+
options: [
|
|
633
|
+
{ label: '日本語', value: 'ja' },
|
|
634
|
+
{ label: 'العربية', value: 'ar' },
|
|
635
|
+
],
|
|
636
|
+
})
|
|
637
|
+
const ctx = createContext('text')
|
|
638
|
+
const result = renderSelect(node, ctx)
|
|
639
|
+
|
|
640
|
+
expect(result).toContain('日本語')
|
|
641
|
+
expect(result).toContain('العربية')
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
it('handles emoji in option labels', async () => {
|
|
645
|
+
const { renderSelect } = await import('../../../renderers/components/select')
|
|
646
|
+
const node = createSelectNode({
|
|
647
|
+
options: [
|
|
648
|
+
{ label: '🇺🇸 United States', value: 'us' },
|
|
649
|
+
{ label: '🇬🇧 United Kingdom', value: 'uk' },
|
|
650
|
+
],
|
|
651
|
+
})
|
|
652
|
+
const ctx = createContext('text')
|
|
653
|
+
const result = renderSelect(node, ctx)
|
|
654
|
+
|
|
655
|
+
expect(result).toContain('United States')
|
|
656
|
+
})
|
|
657
|
+
})
|
|
658
|
+
})
|