@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,555 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal Field Component Tests (RED phase)
|
|
3
|
+
*
|
|
4
|
+
* TDD RED Phase: These tests define the contract for the Field component
|
|
5
|
+
* that renders individual input fields with labels, values, errors, and type-specific display.
|
|
6
|
+
*
|
|
7
|
+
* Field component responsibilities:
|
|
8
|
+
* - Render label with proper formatting
|
|
9
|
+
* - Display current value or placeholder
|
|
10
|
+
* - Show validation errors
|
|
11
|
+
* - Handle different input types (text, password, email, etc.)
|
|
12
|
+
* - Indicate field state (focused, disabled, required)
|
|
13
|
+
*
|
|
14
|
+
* Rendering across tiers:
|
|
15
|
+
* - TEXT: Plain "Label: value" format
|
|
16
|
+
* - MARKDOWN: **Label**: `value` format
|
|
17
|
+
* - ASCII: ASCII box around focused field
|
|
18
|
+
* - UNICODE: Unicode underline or box for input
|
|
19
|
+
* - ANSI: Colors for states (error red, focus highlight)
|
|
20
|
+
* - INTERACTIVE: Cursor display, real-time typing
|
|
21
|
+
*
|
|
22
|
+
* NOTE: These tests are expected to FAIL until implementation is complete.
|
|
23
|
+
* Run: pnpm --filter @mdxui/terminal test -- --run src/__tests__/components/input/field.test.ts
|
|
24
|
+
*/
|
|
25
|
+
import { describe, it, expect } from 'vitest'
|
|
26
|
+
import type { UINode, RenderTier, RenderContext, ThemeTokens } from '../../../core/types'
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Test Utilities
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
const RENDER_TIERS: RenderTier[] = ['text', 'markdown', 'ascii', 'unicode', 'ansi', 'interactive']
|
|
33
|
+
|
|
34
|
+
const mockTheme: ThemeTokens = {
|
|
35
|
+
primary: '\x1b[34m',
|
|
36
|
+
secondary: '\x1b[36m',
|
|
37
|
+
muted: '\x1b[90m',
|
|
38
|
+
foreground: '\x1b[37m',
|
|
39
|
+
background: '\x1b[40m',
|
|
40
|
+
border: '\x1b[90m',
|
|
41
|
+
success: '\x1b[32m',
|
|
42
|
+
warning: '\x1b[33m',
|
|
43
|
+
error: '\x1b[31m',
|
|
44
|
+
info: '\x1b[34m',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createContext(tier: RenderTier, overrides: Partial<RenderContext> = {}): RenderContext {
|
|
48
|
+
return {
|
|
49
|
+
tier,
|
|
50
|
+
width: 80,
|
|
51
|
+
height: 24,
|
|
52
|
+
depth: 0,
|
|
53
|
+
theme: mockTheme,
|
|
54
|
+
interactive: tier === 'interactive',
|
|
55
|
+
...overrides,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createNode(
|
|
60
|
+
type: string,
|
|
61
|
+
props: Record<string, unknown> = {},
|
|
62
|
+
children?: UINode[]
|
|
63
|
+
): UINode {
|
|
64
|
+
return { type, props, children }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createFieldNode(props: Record<string, unknown>): UINode {
|
|
68
|
+
return createNode('field', props)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Basic Rendering Tests
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
describe('Field Component', () => {
|
|
76
|
+
describe('function signature', () => {
|
|
77
|
+
it('exports renderField function', async () => {
|
|
78
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
79
|
+
expect(typeof renderField).toBe('function')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('accepts UINode and RenderContext and returns string', async () => {
|
|
83
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
84
|
+
const node = createFieldNode({ name: 'test', label: 'Test' })
|
|
85
|
+
const ctx = createContext('text')
|
|
86
|
+
const result = renderField(node, ctx)
|
|
87
|
+
expect(typeof result).toBe('string')
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// ============================================================================
|
|
92
|
+
// Label Rendering Tests
|
|
93
|
+
// ============================================================================
|
|
94
|
+
|
|
95
|
+
describe('label rendering', () => {
|
|
96
|
+
RENDER_TIERS.forEach((tier) => {
|
|
97
|
+
describe(`[${tier}] tier`, () => {
|
|
98
|
+
it('renders field label', async () => {
|
|
99
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
100
|
+
const node = createFieldNode({ name: 'username', label: 'Username' })
|
|
101
|
+
const ctx = createContext(tier)
|
|
102
|
+
const result = renderField(node, ctx)
|
|
103
|
+
|
|
104
|
+
expect(result).toContain('Username')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('renders required indicator', async () => {
|
|
108
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
109
|
+
const node = createFieldNode({ name: 'email', label: 'Email', required: true })
|
|
110
|
+
const ctx = createContext(tier)
|
|
111
|
+
const result = renderField(node, ctx)
|
|
112
|
+
|
|
113
|
+
expect(result).toContain('Email')
|
|
114
|
+
expect(result).toMatch(/\*|required/i)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('renders label with colon separator', async () => {
|
|
118
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
119
|
+
const node = createFieldNode({ name: 'name', label: 'Full Name', value: 'John' })
|
|
120
|
+
const ctx = createContext(tier)
|
|
121
|
+
const result = renderField(node, ctx)
|
|
122
|
+
|
|
123
|
+
expect(result).toContain('Full Name')
|
|
124
|
+
expect(result).toContain('John')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('handles empty label', async () => {
|
|
128
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
129
|
+
const node = createFieldNode({ name: 'hidden', value: 'value' })
|
|
130
|
+
const ctx = createContext(tier)
|
|
131
|
+
const result = renderField(node, ctx)
|
|
132
|
+
|
|
133
|
+
expect(result).toContain('value')
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// Value Display Tests
|
|
141
|
+
// ============================================================================
|
|
142
|
+
|
|
143
|
+
describe('value display', () => {
|
|
144
|
+
RENDER_TIERS.forEach((tier) => {
|
|
145
|
+
describe(`[${tier}] tier`, () => {
|
|
146
|
+
it('renders field value', async () => {
|
|
147
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
148
|
+
const node = createFieldNode({
|
|
149
|
+
name: 'username',
|
|
150
|
+
label: 'Username',
|
|
151
|
+
value: 'john_doe',
|
|
152
|
+
})
|
|
153
|
+
const ctx = createContext(tier)
|
|
154
|
+
const result = renderField(node, ctx)
|
|
155
|
+
|
|
156
|
+
expect(result).toContain('john_doe')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('renders placeholder when no value', async () => {
|
|
160
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
161
|
+
const node = createFieldNode({
|
|
162
|
+
name: 'email',
|
|
163
|
+
label: 'Email',
|
|
164
|
+
placeholder: 'Enter your email',
|
|
165
|
+
})
|
|
166
|
+
const ctx = createContext(tier)
|
|
167
|
+
const result = renderField(node, ctx)
|
|
168
|
+
|
|
169
|
+
expect(result).toContain('Enter your email')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('renders numeric value', async () => {
|
|
173
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
174
|
+
const node = createFieldNode({
|
|
175
|
+
name: 'age',
|
|
176
|
+
label: 'Age',
|
|
177
|
+
value: 25,
|
|
178
|
+
})
|
|
179
|
+
const ctx = createContext(tier)
|
|
180
|
+
const result = renderField(node, ctx)
|
|
181
|
+
|
|
182
|
+
expect(result).toContain('25')
|
|
183
|
+
})
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
describe('[ansi] tier value styling', () => {
|
|
188
|
+
it('renders placeholder in muted color', async () => {
|
|
189
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
190
|
+
const node = createFieldNode({
|
|
191
|
+
name: 'email',
|
|
192
|
+
label: 'Email',
|
|
193
|
+
placeholder: 'Enter email',
|
|
194
|
+
})
|
|
195
|
+
const ctx = createContext('ansi')
|
|
196
|
+
const result = renderField(node, ctx)
|
|
197
|
+
|
|
198
|
+
expect(result).toContain('\x1b[90m')
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// ============================================================================
|
|
204
|
+
// Error Display Tests
|
|
205
|
+
// ============================================================================
|
|
206
|
+
|
|
207
|
+
describe('error display', () => {
|
|
208
|
+
RENDER_TIERS.forEach((tier) => {
|
|
209
|
+
describe(`[${tier}] tier`, () => {
|
|
210
|
+
it('renders error message', async () => {
|
|
211
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
212
|
+
const node = createFieldNode({
|
|
213
|
+
name: 'email',
|
|
214
|
+
label: 'Email',
|
|
215
|
+
value: 'invalid',
|
|
216
|
+
error: 'Please enter a valid email address',
|
|
217
|
+
})
|
|
218
|
+
const ctx = createContext(tier)
|
|
219
|
+
const result = renderField(node, ctx)
|
|
220
|
+
|
|
221
|
+
expect(result).toContain('Please enter a valid email address')
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('renders error below field value', async () => {
|
|
225
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
226
|
+
const node = createFieldNode({
|
|
227
|
+
name: 'email',
|
|
228
|
+
label: 'Email',
|
|
229
|
+
value: 'invalid',
|
|
230
|
+
error: 'Invalid email',
|
|
231
|
+
})
|
|
232
|
+
const ctx = createContext(tier)
|
|
233
|
+
const result = renderField(node, ctx)
|
|
234
|
+
|
|
235
|
+
const valuePos = result.indexOf('invalid')
|
|
236
|
+
const errorPos = result.indexOf('Invalid email')
|
|
237
|
+
|
|
238
|
+
expect(errorPos).toBeGreaterThan(valuePos)
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
describe('[ansi] tier error styling', () => {
|
|
244
|
+
it('renders error in red', async () => {
|
|
245
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
246
|
+
const node = createFieldNode({
|
|
247
|
+
name: 'email',
|
|
248
|
+
label: 'Email',
|
|
249
|
+
error: 'Invalid',
|
|
250
|
+
})
|
|
251
|
+
const ctx = createContext('ansi')
|
|
252
|
+
const result = renderField(node, ctx)
|
|
253
|
+
|
|
254
|
+
expect(result).toContain('\x1b[31m')
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('renders valid field in green', async () => {
|
|
258
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
259
|
+
const node = createFieldNode({
|
|
260
|
+
name: 'email',
|
|
261
|
+
label: 'Email',
|
|
262
|
+
valid: true,
|
|
263
|
+
})
|
|
264
|
+
const ctx = createContext('ansi')
|
|
265
|
+
const result = renderField(node, ctx)
|
|
266
|
+
|
|
267
|
+
expect(result).toContain('\x1b[32m')
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
// ============================================================================
|
|
273
|
+
// Type-Specific Rendering Tests
|
|
274
|
+
// ============================================================================
|
|
275
|
+
|
|
276
|
+
describe('type-specific rendering', () => {
|
|
277
|
+
describe('password field', () => {
|
|
278
|
+
RENDER_TIERS.forEach((tier) => {
|
|
279
|
+
it(`[${tier}] masks password value`, async () => {
|
|
280
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
281
|
+
const node = createFieldNode({
|
|
282
|
+
name: 'password',
|
|
283
|
+
label: 'Password',
|
|
284
|
+
type: 'password',
|
|
285
|
+
value: 'secret123',
|
|
286
|
+
})
|
|
287
|
+
const ctx = createContext(tier)
|
|
288
|
+
const result = renderField(node, ctx)
|
|
289
|
+
|
|
290
|
+
expect(result).toContain('Password')
|
|
291
|
+
expect(result).not.toContain('secret123')
|
|
292
|
+
expect(result).toMatch(/[*•●]+/)
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
describe('checkbox field', () => {
|
|
298
|
+
RENDER_TIERS.forEach((tier) => {
|
|
299
|
+
it(`[${tier}] renders checked checkbox`, async () => {
|
|
300
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
301
|
+
const node = createFieldNode({
|
|
302
|
+
name: 'agree',
|
|
303
|
+
label: 'I agree to terms',
|
|
304
|
+
type: 'checkbox',
|
|
305
|
+
value: true,
|
|
306
|
+
})
|
|
307
|
+
const ctx = createContext(tier)
|
|
308
|
+
const result = renderField(node, ctx)
|
|
309
|
+
|
|
310
|
+
expect(result).toContain('I agree to terms')
|
|
311
|
+
expect(result).toMatch(/[✓✔☑x\[x\]]/i)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it(`[${tier}] renders unchecked checkbox`, async () => {
|
|
315
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
316
|
+
const node = createFieldNode({
|
|
317
|
+
name: 'newsletter',
|
|
318
|
+
label: 'Subscribe to newsletter',
|
|
319
|
+
type: 'checkbox',
|
|
320
|
+
value: false,
|
|
321
|
+
})
|
|
322
|
+
const ctx = createContext(tier)
|
|
323
|
+
const result = renderField(node, ctx)
|
|
324
|
+
|
|
325
|
+
expect(result).toContain('Subscribe to newsletter')
|
|
326
|
+
expect(result).toMatch(/[☐□\[\s\]]/i)
|
|
327
|
+
})
|
|
328
|
+
})
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
describe('textarea field', () => {
|
|
332
|
+
RENDER_TIERS.forEach((tier) => {
|
|
333
|
+
it(`[${tier}] renders textarea with multiple lines`, async () => {
|
|
334
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
335
|
+
const node = createFieldNode({
|
|
336
|
+
name: 'description',
|
|
337
|
+
label: 'Description',
|
|
338
|
+
type: 'textarea',
|
|
339
|
+
value: 'Line 1\nLine 2\nLine 3',
|
|
340
|
+
})
|
|
341
|
+
const ctx = createContext(tier)
|
|
342
|
+
const result = renderField(node, ctx)
|
|
343
|
+
|
|
344
|
+
expect(result).toContain('Line 1')
|
|
345
|
+
expect(result).toContain('Line 2')
|
|
346
|
+
expect(result).toContain('Line 3')
|
|
347
|
+
})
|
|
348
|
+
})
|
|
349
|
+
})
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
// ============================================================================
|
|
353
|
+
// Field State Tests
|
|
354
|
+
// ============================================================================
|
|
355
|
+
|
|
356
|
+
describe('field states', () => {
|
|
357
|
+
describe('focused state', () => {
|
|
358
|
+
RENDER_TIERS.forEach((tier) => {
|
|
359
|
+
it(`[${tier}] renders focused field differently`, async () => {
|
|
360
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
361
|
+
const node = createFieldNode({
|
|
362
|
+
name: 'email',
|
|
363
|
+
label: 'Email',
|
|
364
|
+
focused: true,
|
|
365
|
+
})
|
|
366
|
+
const ctx = createContext(tier)
|
|
367
|
+
const result = renderField(node, ctx)
|
|
368
|
+
|
|
369
|
+
expect(result).toContain('Email')
|
|
370
|
+
})
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
describe('[ansi] tier focus styling', () => {
|
|
374
|
+
it('renders focused field with highlight color', async () => {
|
|
375
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
376
|
+
const node = createFieldNode({
|
|
377
|
+
name: 'email',
|
|
378
|
+
label: 'Email',
|
|
379
|
+
focused: true,
|
|
380
|
+
})
|
|
381
|
+
const ctx = createContext('ansi')
|
|
382
|
+
const result = renderField(node, ctx)
|
|
383
|
+
|
|
384
|
+
expect(result).toMatch(/\x1b\[(34|4|7)m/)
|
|
385
|
+
})
|
|
386
|
+
})
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
describe('disabled state', () => {
|
|
390
|
+
RENDER_TIERS.forEach((tier) => {
|
|
391
|
+
it(`[${tier}] renders disabled field`, async () => {
|
|
392
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
393
|
+
const node = createFieldNode({
|
|
394
|
+
name: 'locked',
|
|
395
|
+
label: 'Locked Field',
|
|
396
|
+
disabled: true,
|
|
397
|
+
value: 'Cannot edit',
|
|
398
|
+
})
|
|
399
|
+
const ctx = createContext(tier)
|
|
400
|
+
const result = renderField(node, ctx)
|
|
401
|
+
|
|
402
|
+
expect(result).toContain('Locked Field')
|
|
403
|
+
expect(result).toContain('Cannot edit')
|
|
404
|
+
})
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
describe('[ansi] tier disabled styling', () => {
|
|
408
|
+
it('renders disabled field as dimmed', async () => {
|
|
409
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
410
|
+
const node = createFieldNode({
|
|
411
|
+
name: 'disabled',
|
|
412
|
+
label: 'Disabled',
|
|
413
|
+
disabled: true,
|
|
414
|
+
})
|
|
415
|
+
const ctx = createContext('ansi')
|
|
416
|
+
const result = renderField(node, ctx)
|
|
417
|
+
|
|
418
|
+
expect(result).toContain('\x1b[2m')
|
|
419
|
+
})
|
|
420
|
+
})
|
|
421
|
+
})
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
// ============================================================================
|
|
425
|
+
// Interactive Tier Tests
|
|
426
|
+
// ============================================================================
|
|
427
|
+
|
|
428
|
+
describe('[interactive] tier', () => {
|
|
429
|
+
it('shows cursor in text field', async () => {
|
|
430
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
431
|
+
const node = createFieldNode({
|
|
432
|
+
name: 'text',
|
|
433
|
+
label: 'Text',
|
|
434
|
+
value: 'Hello',
|
|
435
|
+
focused: true,
|
|
436
|
+
cursorPosition: 5,
|
|
437
|
+
})
|
|
438
|
+
const ctx = createContext('interactive')
|
|
439
|
+
const result = renderField(node, ctx)
|
|
440
|
+
|
|
441
|
+
expect(result).toMatch(/\x1b\[(7|4)m|[|_▌█]/)
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
it('shows text selection', async () => {
|
|
445
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
446
|
+
const node = createFieldNode({
|
|
447
|
+
name: 'text',
|
|
448
|
+
label: 'Text',
|
|
449
|
+
value: 'Hello World',
|
|
450
|
+
focused: true,
|
|
451
|
+
selectionStart: 0,
|
|
452
|
+
selectionEnd: 5,
|
|
453
|
+
})
|
|
454
|
+
const ctx = createContext('interactive')
|
|
455
|
+
const result = renderField(node, ctx)
|
|
456
|
+
|
|
457
|
+
expect(result).toMatch(/\x1b\[7m/)
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
it('shows keyboard hints', async () => {
|
|
461
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
462
|
+
const node = createFieldNode({
|
|
463
|
+
name: 'email',
|
|
464
|
+
label: 'Email',
|
|
465
|
+
focused: true,
|
|
466
|
+
})
|
|
467
|
+
const ctx = createContext('interactive')
|
|
468
|
+
const result = renderField(node, ctx)
|
|
469
|
+
|
|
470
|
+
expect(result).toMatch(/Tab|Enter|Esc/i)
|
|
471
|
+
})
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
// ============================================================================
|
|
475
|
+
// Helper Text Tests
|
|
476
|
+
// ============================================================================
|
|
477
|
+
|
|
478
|
+
describe('helper text', () => {
|
|
479
|
+
RENDER_TIERS.forEach((tier) => {
|
|
480
|
+
it(`[${tier}] renders helper text below field`, async () => {
|
|
481
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
482
|
+
const node = createFieldNode({
|
|
483
|
+
name: 'password',
|
|
484
|
+
label: 'Password',
|
|
485
|
+
helperText: 'Must be at least 8 characters',
|
|
486
|
+
})
|
|
487
|
+
const ctx = createContext(tier)
|
|
488
|
+
const result = renderField(node, ctx)
|
|
489
|
+
|
|
490
|
+
expect(result).toContain('Must be at least 8 characters')
|
|
491
|
+
})
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
describe('[ansi] tier helper styling', () => {
|
|
495
|
+
it('renders helper text in muted color', async () => {
|
|
496
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
497
|
+
const node = createFieldNode({
|
|
498
|
+
name: 'email',
|
|
499
|
+
label: 'Email',
|
|
500
|
+
helperText: 'We will never share your email',
|
|
501
|
+
})
|
|
502
|
+
const ctx = createContext('ansi')
|
|
503
|
+
const result = renderField(node, ctx)
|
|
504
|
+
|
|
505
|
+
expect(result).toContain('\x1b[90m')
|
|
506
|
+
})
|
|
507
|
+
})
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
// ============================================================================
|
|
511
|
+
// Edge Cases
|
|
512
|
+
// ============================================================================
|
|
513
|
+
|
|
514
|
+
describe('edge cases', () => {
|
|
515
|
+
it('handles null value', async () => {
|
|
516
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
517
|
+
const node = createFieldNode({
|
|
518
|
+
name: 'nullable',
|
|
519
|
+
label: 'Nullable',
|
|
520
|
+
value: null,
|
|
521
|
+
})
|
|
522
|
+
const ctx = createContext('text')
|
|
523
|
+
const result = renderField(node, ctx)
|
|
524
|
+
|
|
525
|
+
expect(result).toContain('Nullable')
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
it('handles unicode characters', async () => {
|
|
529
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
530
|
+
const node = createFieldNode({
|
|
531
|
+
name: 'unicode',
|
|
532
|
+
label: '名前',
|
|
533
|
+
value: 'こんにちは',
|
|
534
|
+
})
|
|
535
|
+
const ctx = createContext('text')
|
|
536
|
+
const result = renderField(node, ctx)
|
|
537
|
+
|
|
538
|
+
expect(result).toContain('名前')
|
|
539
|
+
expect(result).toContain('こんにちは')
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
it('handles emoji in value', async () => {
|
|
543
|
+
const { renderField } = await import('../../../renderers/components/field')
|
|
544
|
+
const node = createFieldNode({
|
|
545
|
+
name: 'emoji',
|
|
546
|
+
label: 'Status',
|
|
547
|
+
value: 'Working hard 💪🔥',
|
|
548
|
+
})
|
|
549
|
+
const ctx = createContext('text')
|
|
550
|
+
const result = renderField(node, ctx)
|
|
551
|
+
|
|
552
|
+
expect(result).toContain('Working hard')
|
|
553
|
+
})
|
|
554
|
+
})
|
|
555
|
+
})
|