@mdxui/terminal 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/README.md +571 -0
  2. package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
  3. package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
  4. package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
  5. package/dist/chunk-3EFDH7PK.js +5235 -0
  6. package/dist/chunk-3RG5ZIWI.js +10 -0
  7. package/dist/chunk-3X5IR6WE.js +884 -0
  8. package/dist/chunk-4FV5ZDCE.js +5236 -0
  9. package/dist/chunk-4OVMSF2J.js +243 -0
  10. package/dist/chunk-63FEETIS.js +4048 -0
  11. package/dist/chunk-B43KP7XJ.js +884 -0
  12. package/dist/chunk-BMTJXWUV.js +655 -0
  13. package/dist/chunk-C3SVH4N7.js +882 -0
  14. package/dist/chunk-EVWR7Y47.js +874 -0
  15. package/dist/chunk-F6A5VWUC.js +1285 -0
  16. package/dist/chunk-FD7KW7GE.js +882 -0
  17. package/dist/chunk-GBQ6UD6I.js +655 -0
  18. package/dist/chunk-GMDD3M6U.js +5227 -0
  19. package/dist/chunk-JBHRXOXM.js +1058 -0
  20. package/dist/chunk-JFOO3EYO.js +1182 -0
  21. package/dist/chunk-JQ5H3WXL.js +1291 -0
  22. package/dist/chunk-JQD5NASE.js +234 -0
  23. package/dist/chunk-KRHJP5R7.js +592 -0
  24. package/dist/chunk-KWF6WVJE.js +962 -0
  25. package/dist/chunk-LHYQVN3H.js +1038 -0
  26. package/dist/chunk-M3TLQLGC.js +1032 -0
  27. package/dist/chunk-MVW4Q5OP.js +240 -0
  28. package/dist/chunk-NXCZSWLU.js +1294 -0
  29. package/dist/chunk-O25TNRO6.js +607 -0
  30. package/dist/chunk-PNECDA2I.js +884 -0
  31. package/dist/chunk-QIHWRLJR.js +962 -0
  32. package/dist/chunk-QW5YMQ7K.js +882 -0
  33. package/dist/chunk-R5U7XKVJ.js +16 -0
  34. package/dist/chunk-RP2MVQLR.js +962 -0
  35. package/dist/chunk-TP6RXGXA.js +1087 -0
  36. package/dist/chunk-TQQSTITZ.js +655 -0
  37. package/dist/chunk-X24GWXQV.js +1281 -0
  38. package/dist/components/index.d.ts +802 -0
  39. package/dist/components/index.js +149 -0
  40. package/dist/data/index.d.ts +2554 -0
  41. package/dist/data/index.js +51 -0
  42. package/dist/forms/index.d.ts +1596 -0
  43. package/dist/forms/index.js +464 -0
  44. package/dist/index-CQRFZntR.d.ts +867 -0
  45. package/dist/index.d.ts +579 -0
  46. package/dist/index.js +786 -0
  47. package/dist/interactive-D0JkWosD.d.ts +217 -0
  48. package/dist/keyboard/index.d.ts +2 -0
  49. package/dist/keyboard/index.js +43 -0
  50. package/dist/renderers/index.d.ts +546 -0
  51. package/dist/renderers/index.js +2157 -0
  52. package/dist/storybook/index.d.ts +396 -0
  53. package/dist/storybook/index.js +641 -0
  54. package/dist/theme/index.d.ts +1339 -0
  55. package/dist/theme/index.js +123 -0
  56. package/dist/types-Bxu5PAgA.d.ts +710 -0
  57. package/dist/types-CIlop5Ji.d.ts +701 -0
  58. package/dist/types-Ca8p_p5X.d.ts +710 -0
  59. package/package.json +90 -0
  60. package/src/__tests__/components/data/card.test.ts +458 -0
  61. package/src/__tests__/components/data/list.test.ts +473 -0
  62. package/src/__tests__/components/data/metrics.test.ts +541 -0
  63. package/src/__tests__/components/data/table.test.ts +448 -0
  64. package/src/__tests__/components/input/field.test.ts +555 -0
  65. package/src/__tests__/components/input/form.test.ts +870 -0
  66. package/src/__tests__/components/input/search.test.ts +1238 -0
  67. package/src/__tests__/components/input/select.test.ts +658 -0
  68. package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
  69. package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
  70. package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
  71. package/src/__tests__/components/navigation/tabs.test.ts +995 -0
  72. package/src/__tests__/components.test.tsx +1197 -0
  73. package/src/__tests__/core/compiler.test.ts +986 -0
  74. package/src/__tests__/core/parser.test.ts +785 -0
  75. package/src/__tests__/core/tier-switcher.test.ts +1103 -0
  76. package/src/__tests__/core/types.test.ts +1398 -0
  77. package/src/__tests__/data/collections.test.ts +1337 -0
  78. package/src/__tests__/data/db.test.ts +1265 -0
  79. package/src/__tests__/data/reactive.test.ts +1010 -0
  80. package/src/__tests__/data/sync.test.ts +1614 -0
  81. package/src/__tests__/errors.test.ts +660 -0
  82. package/src/__tests__/forms/integration.test.ts +444 -0
  83. package/src/__tests__/integration.test.ts +905 -0
  84. package/src/__tests__/keyboard.test.ts +1791 -0
  85. package/src/__tests__/renderer.test.ts +489 -0
  86. package/src/__tests__/renderers/ansi-css.test.ts +948 -0
  87. package/src/__tests__/renderers/ansi.test.ts +1366 -0
  88. package/src/__tests__/renderers/ascii.test.ts +1360 -0
  89. package/src/__tests__/renderers/interactive.test.ts +2353 -0
  90. package/src/__tests__/renderers/markdown.test.ts +1483 -0
  91. package/src/__tests__/renderers/text.test.ts +1369 -0
  92. package/src/__tests__/renderers/unicode.test.ts +1307 -0
  93. package/src/__tests__/theme.test.ts +639 -0
  94. package/src/__tests__/utils/assertions.ts +685 -0
  95. package/src/__tests__/utils/index.ts +115 -0
  96. package/src/__tests__/utils/test-renderer.ts +381 -0
  97. package/src/__tests__/utils/utils.test.ts +560 -0
  98. package/src/components/containers/card.ts +56 -0
  99. package/src/components/containers/dialog.ts +53 -0
  100. package/src/components/containers/index.ts +9 -0
  101. package/src/components/containers/panel.ts +59 -0
  102. package/src/components/feedback/badge.ts +40 -0
  103. package/src/components/feedback/index.ts +8 -0
  104. package/src/components/feedback/spinner.ts +23 -0
  105. package/src/components/helpers.ts +81 -0
  106. package/src/components/index.ts +153 -0
  107. package/src/components/layout/breadcrumb.ts +31 -0
  108. package/src/components/layout/index.ts +10 -0
  109. package/src/components/layout/list.ts +29 -0
  110. package/src/components/layout/sidebar.ts +79 -0
  111. package/src/components/layout/table.ts +62 -0
  112. package/src/components/primitives/box.ts +95 -0
  113. package/src/components/primitives/button.ts +54 -0
  114. package/src/components/primitives/index.ts +11 -0
  115. package/src/components/primitives/input.ts +88 -0
  116. package/src/components/primitives/select.ts +97 -0
  117. package/src/components/primitives/text.ts +60 -0
  118. package/src/components/render.ts +155 -0
  119. package/src/components/templates/app.ts +43 -0
  120. package/src/components/templates/index.ts +8 -0
  121. package/src/components/templates/site.ts +54 -0
  122. package/src/components/types.ts +777 -0
  123. package/src/core/compiler.ts +718 -0
  124. package/src/core/parser.ts +127 -0
  125. package/src/core/tier-switcher.ts +607 -0
  126. package/src/core/types.ts +672 -0
  127. package/src/data/collection.ts +316 -0
  128. package/src/data/collections.ts +50 -0
  129. package/src/data/context.tsx +174 -0
  130. package/src/data/db.ts +127 -0
  131. package/src/data/hooks.ts +532 -0
  132. package/src/data/index.ts +138 -0
  133. package/src/data/reactive.ts +1225 -0
  134. package/src/data/saas-collections.ts +375 -0
  135. package/src/data/sync.ts +1213 -0
  136. package/src/data/types.ts +660 -0
  137. package/src/forms/converters.ts +512 -0
  138. package/src/forms/index.ts +133 -0
  139. package/src/forms/schemas.ts +403 -0
  140. package/src/forms/types.ts +476 -0
  141. package/src/index.ts +542 -0
  142. package/src/keyboard/focus.ts +748 -0
  143. package/src/keyboard/index.ts +96 -0
  144. package/src/keyboard/integration.ts +371 -0
  145. package/src/keyboard/manager.ts +377 -0
  146. package/src/keyboard/presets.ts +90 -0
  147. package/src/renderers/ansi-css.ts +576 -0
  148. package/src/renderers/ansi.ts +802 -0
  149. package/src/renderers/ascii.ts +680 -0
  150. package/src/renderers/breadcrumb.ts +480 -0
  151. package/src/renderers/command-palette.ts +802 -0
  152. package/src/renderers/components/field.ts +210 -0
  153. package/src/renderers/components/form.ts +327 -0
  154. package/src/renderers/components/index.ts +21 -0
  155. package/src/renderers/components/search.ts +449 -0
  156. package/src/renderers/components/select.ts +222 -0
  157. package/src/renderers/index.ts +101 -0
  158. package/src/renderers/interactive/component-handlers.ts +622 -0
  159. package/src/renderers/interactive/cursor-manager.ts +147 -0
  160. package/src/renderers/interactive/focus-manager.ts +279 -0
  161. package/src/renderers/interactive/index.ts +661 -0
  162. package/src/renderers/interactive/input-handler.ts +164 -0
  163. package/src/renderers/interactive/keyboard-handler.ts +212 -0
  164. package/src/renderers/interactive/mouse-handler.ts +167 -0
  165. package/src/renderers/interactive/state-manager.ts +109 -0
  166. package/src/renderers/interactive/types.ts +338 -0
  167. package/src/renderers/interactive-string.ts +299 -0
  168. package/src/renderers/interactive.ts +59 -0
  169. package/src/renderers/markdown.ts +950 -0
  170. package/src/renderers/sidebar.ts +549 -0
  171. package/src/renderers/tabs.ts +682 -0
  172. package/src/renderers/text.ts +791 -0
  173. package/src/renderers/unicode.ts +917 -0
  174. package/src/renderers/utils.ts +942 -0
  175. package/src/router/adapters.ts +383 -0
  176. package/src/router/types.ts +140 -0
  177. package/src/router/utils.ts +452 -0
  178. package/src/schemas.ts +205 -0
  179. package/src/storybook/index.ts +91 -0
  180. package/src/storybook/interactive-decorator.tsx +659 -0
  181. package/src/storybook/keyboard-simulator.ts +501 -0
  182. package/src/theme/ansi-codes.ts +80 -0
  183. package/src/theme/box-drawing.ts +132 -0
  184. package/src/theme/color-convert.ts +254 -0
  185. package/src/theme/color-support.ts +321 -0
  186. package/src/theme/index.ts +134 -0
  187. package/src/theme/strip-ansi.ts +50 -0
  188. package/src/theme/tailwind-map.ts +469 -0
  189. package/src/theme/text-styles.ts +206 -0
  190. package/src/theme/theme-system.ts +568 -0
  191. package/src/types.ts +103 -0
@@ -0,0 +1,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
+ })