@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,870 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mdxui/terminal Form Component Tests (RED phase)
|
|
3
|
+
*
|
|
4
|
+
* TDD RED Phase: These tests define the contract for the Form component
|
|
5
|
+
* that collects fields, validates input, and handles submission.
|
|
6
|
+
*
|
|
7
|
+
* Form component responsibilities:
|
|
8
|
+
* - Collect multiple fields into a cohesive unit
|
|
9
|
+
* - Validate field values against rules
|
|
10
|
+
* - Display validation errors
|
|
11
|
+
* - Handle form submission
|
|
12
|
+
* - Support different field types (text, email, password, etc.)
|
|
13
|
+
*
|
|
14
|
+
* Rendering across tiers:
|
|
15
|
+
* - TEXT: Plain label: value format, errors as text
|
|
16
|
+
* - MARKDOWN: Labels with formatting, errors in emphasis
|
|
17
|
+
* - ASCII: ASCII box borders around form groups
|
|
18
|
+
* - UNICODE: Unicode box borders for form structure
|
|
19
|
+
* - ANSI: Colors for validation states (red errors, green valid)
|
|
20
|
+
* - INTERACTIVE: Full keyboard navigation, real-time validation
|
|
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/form.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 createFormNode(props: Record<string, unknown>, children?: UINode[]): UINode {
|
|
68
|
+
return createNode('form', props, children)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Basic Rendering Tests
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
describe('Form Component', () => {
|
|
76
|
+
describe('function signature', () => {
|
|
77
|
+
it('exports renderForm function', async () => {
|
|
78
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
79
|
+
expect(typeof renderForm).toBe('function')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('accepts UINode and RenderContext and returns string', async () => {
|
|
83
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
84
|
+
const node = createFormNode({
|
|
85
|
+
fields: [{ name: 'username', label: 'Username' }],
|
|
86
|
+
})
|
|
87
|
+
const ctx = createContext('text')
|
|
88
|
+
const result = renderForm(node, ctx)
|
|
89
|
+
expect(typeof result).toBe('string')
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// Field Collection Tests
|
|
95
|
+
// ============================================================================
|
|
96
|
+
|
|
97
|
+
describe('field collection', () => {
|
|
98
|
+
RENDER_TIERS.forEach((tier) => {
|
|
99
|
+
describe(`[${tier}] tier`, () => {
|
|
100
|
+
it('renders single field', async () => {
|
|
101
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
102
|
+
const node = createFormNode({
|
|
103
|
+
fields: [{ name: 'email', label: 'Email', type: 'email' }],
|
|
104
|
+
})
|
|
105
|
+
const ctx = createContext(tier)
|
|
106
|
+
const result = renderForm(node, ctx)
|
|
107
|
+
|
|
108
|
+
expect(result).toContain('Email')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('renders multiple fields', async () => {
|
|
112
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
113
|
+
const node = createFormNode({
|
|
114
|
+
fields: [
|
|
115
|
+
{ name: 'firstName', label: 'First Name' },
|
|
116
|
+
{ name: 'lastName', label: 'Last Name' },
|
|
117
|
+
{ name: 'email', label: 'Email' },
|
|
118
|
+
],
|
|
119
|
+
})
|
|
120
|
+
const ctx = createContext(tier)
|
|
121
|
+
const result = renderForm(node, ctx)
|
|
122
|
+
|
|
123
|
+
expect(result).toContain('First Name')
|
|
124
|
+
expect(result).toContain('Last Name')
|
|
125
|
+
expect(result).toContain('Email')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('preserves field order', async () => {
|
|
129
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
130
|
+
const node = createFormNode({
|
|
131
|
+
fields: [
|
|
132
|
+
{ name: 'a', label: 'Field A' },
|
|
133
|
+
{ name: 'b', label: 'Field B' },
|
|
134
|
+
{ name: 'c', label: 'Field C' },
|
|
135
|
+
],
|
|
136
|
+
})
|
|
137
|
+
const ctx = createContext(tier)
|
|
138
|
+
const result = renderForm(node, ctx)
|
|
139
|
+
|
|
140
|
+
const aPos = result.indexOf('Field A')
|
|
141
|
+
const bPos = result.indexOf('Field B')
|
|
142
|
+
const cPos = result.indexOf('Field C')
|
|
143
|
+
|
|
144
|
+
expect(aPos).toBeLessThan(bPos)
|
|
145
|
+
expect(bPos).toBeLessThan(cPos)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('renders empty form gracefully', async () => {
|
|
149
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
150
|
+
const node = createFormNode({ fields: [] })
|
|
151
|
+
const ctx = createContext(tier)
|
|
152
|
+
const result = renderForm(node, ctx)
|
|
153
|
+
|
|
154
|
+
expect(typeof result).toBe('string')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('renders form with title', async () => {
|
|
158
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
159
|
+
const node = createFormNode({
|
|
160
|
+
title: 'User Registration',
|
|
161
|
+
fields: [{ name: 'username', label: 'Username' }],
|
|
162
|
+
})
|
|
163
|
+
const ctx = createContext(tier)
|
|
164
|
+
const result = renderForm(node, ctx)
|
|
165
|
+
|
|
166
|
+
expect(result).toContain('User Registration')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('renders form with description', async () => {
|
|
170
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
171
|
+
const node = createFormNode({
|
|
172
|
+
title: 'Sign Up',
|
|
173
|
+
description: 'Create your account to get started',
|
|
174
|
+
fields: [{ name: 'email', label: 'Email' }],
|
|
175
|
+
})
|
|
176
|
+
const ctx = createContext(tier)
|
|
177
|
+
const result = renderForm(node, ctx)
|
|
178
|
+
|
|
179
|
+
expect(result).toContain('Create your account')
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
// ============================================================================
|
|
186
|
+
// Field Types Tests
|
|
187
|
+
// ============================================================================
|
|
188
|
+
|
|
189
|
+
describe('field types', () => {
|
|
190
|
+
RENDER_TIERS.forEach((tier) => {
|
|
191
|
+
describe(`[${tier}] tier`, () => {
|
|
192
|
+
it('renders text field', async () => {
|
|
193
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
194
|
+
const node = createFormNode({
|
|
195
|
+
fields: [{ name: 'name', label: 'Name', type: 'text', value: 'John' }],
|
|
196
|
+
})
|
|
197
|
+
const ctx = createContext(tier)
|
|
198
|
+
const result = renderForm(node, ctx)
|
|
199
|
+
|
|
200
|
+
expect(result).toContain('Name')
|
|
201
|
+
expect(result).toContain('John')
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('renders email field', async () => {
|
|
205
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
206
|
+
const node = createFormNode({
|
|
207
|
+
fields: [{ name: 'email', label: 'Email', type: 'email', value: 'test@example.com' }],
|
|
208
|
+
})
|
|
209
|
+
const ctx = createContext(tier)
|
|
210
|
+
const result = renderForm(node, ctx)
|
|
211
|
+
|
|
212
|
+
expect(result).toContain('Email')
|
|
213
|
+
expect(result).toContain('test@example.com')
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('renders password field with masking', async () => {
|
|
217
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
218
|
+
const node = createFormNode({
|
|
219
|
+
fields: [{ name: 'password', label: 'Password', type: 'password', value: 'secret123' }],
|
|
220
|
+
})
|
|
221
|
+
const ctx = createContext(tier)
|
|
222
|
+
const result = renderForm(node, ctx)
|
|
223
|
+
|
|
224
|
+
expect(result).toContain('Password')
|
|
225
|
+
expect(result).not.toContain('secret123')
|
|
226
|
+
expect(result).toMatch(/[*•]+/)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('renders number field', async () => {
|
|
230
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
231
|
+
const node = createFormNode({
|
|
232
|
+
fields: [{ name: 'age', label: 'Age', type: 'number', value: 25 }],
|
|
233
|
+
})
|
|
234
|
+
const ctx = createContext(tier)
|
|
235
|
+
const result = renderForm(node, ctx)
|
|
236
|
+
|
|
237
|
+
expect(result).toContain('Age')
|
|
238
|
+
expect(result).toContain('25')
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('renders textarea field', async () => {
|
|
242
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
243
|
+
const node = createFormNode({
|
|
244
|
+
fields: [{
|
|
245
|
+
name: 'bio',
|
|
246
|
+
label: 'Biography',
|
|
247
|
+
type: 'textarea',
|
|
248
|
+
value: 'Hello, I am a developer.',
|
|
249
|
+
}],
|
|
250
|
+
})
|
|
251
|
+
const ctx = createContext(tier)
|
|
252
|
+
const result = renderForm(node, ctx)
|
|
253
|
+
|
|
254
|
+
expect(result).toContain('Biography')
|
|
255
|
+
expect(result).toContain('Hello, I am a developer')
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('renders checkbox field', async () => {
|
|
259
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
260
|
+
const node = createFormNode({
|
|
261
|
+
fields: [{ name: 'agree', label: 'I agree to terms', type: 'checkbox', value: true }],
|
|
262
|
+
})
|
|
263
|
+
const ctx = createContext(tier)
|
|
264
|
+
const result = renderForm(node, ctx)
|
|
265
|
+
|
|
266
|
+
expect(result).toContain('I agree to terms')
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('renders select field', async () => {
|
|
270
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
271
|
+
const node = createFormNode({
|
|
272
|
+
fields: [{
|
|
273
|
+
name: 'country',
|
|
274
|
+
label: 'Country',
|
|
275
|
+
type: 'select',
|
|
276
|
+
value: 'us',
|
|
277
|
+
options: [
|
|
278
|
+
{ label: 'United States', value: 'us' },
|
|
279
|
+
{ label: 'Canada', value: 'ca' },
|
|
280
|
+
],
|
|
281
|
+
}],
|
|
282
|
+
})
|
|
283
|
+
const ctx = createContext(tier)
|
|
284
|
+
const result = renderForm(node, ctx)
|
|
285
|
+
|
|
286
|
+
expect(result).toContain('Country')
|
|
287
|
+
expect(result).toContain('United States')
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
// ============================================================================
|
|
294
|
+
// Validation Tests
|
|
295
|
+
// ============================================================================
|
|
296
|
+
|
|
297
|
+
describe('validation', () => {
|
|
298
|
+
RENDER_TIERS.forEach((tier) => {
|
|
299
|
+
describe(`[${tier}] tier`, () => {
|
|
300
|
+
it('displays required field indicator', async () => {
|
|
301
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
302
|
+
const node = createFormNode({
|
|
303
|
+
fields: [{ name: 'email', label: 'Email', required: true }],
|
|
304
|
+
})
|
|
305
|
+
const ctx = createContext(tier)
|
|
306
|
+
const result = renderForm(node, ctx)
|
|
307
|
+
|
|
308
|
+
expect(result).toMatch(/Email.*\*|required/i)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('displays validation error message', async () => {
|
|
312
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
313
|
+
const node = createFormNode({
|
|
314
|
+
fields: [{
|
|
315
|
+
name: 'email',
|
|
316
|
+
label: 'Email',
|
|
317
|
+
value: 'invalid',
|
|
318
|
+
error: 'Please enter a valid email address',
|
|
319
|
+
}],
|
|
320
|
+
})
|
|
321
|
+
const ctx = createContext(tier)
|
|
322
|
+
const result = renderForm(node, ctx)
|
|
323
|
+
|
|
324
|
+
expect(result).toContain('Please enter a valid email address')
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it('displays multiple field errors', async () => {
|
|
328
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
329
|
+
const node = createFormNode({
|
|
330
|
+
fields: [
|
|
331
|
+
{ name: 'email', label: 'Email', error: 'Invalid email' },
|
|
332
|
+
{ name: 'password', label: 'Password', error: 'Too short' },
|
|
333
|
+
],
|
|
334
|
+
})
|
|
335
|
+
const ctx = createContext(tier)
|
|
336
|
+
const result = renderForm(node, ctx)
|
|
337
|
+
|
|
338
|
+
expect(result).toContain('Invalid email')
|
|
339
|
+
expect(result).toContain('Too short')
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
it('displays form-level error', async () => {
|
|
343
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
344
|
+
const node = createFormNode({
|
|
345
|
+
fields: [{ name: 'email', label: 'Email' }],
|
|
346
|
+
error: 'Submission failed. Please try again.',
|
|
347
|
+
})
|
|
348
|
+
const ctx = createContext(tier)
|
|
349
|
+
const result = renderForm(node, ctx)
|
|
350
|
+
|
|
351
|
+
expect(result).toContain('Submission failed')
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('indicates valid field state', async () => {
|
|
355
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
356
|
+
const node = createFormNode({
|
|
357
|
+
fields: [{
|
|
358
|
+
name: 'email',
|
|
359
|
+
label: 'Email',
|
|
360
|
+
value: 'valid@example.com',
|
|
361
|
+
valid: true,
|
|
362
|
+
}],
|
|
363
|
+
})
|
|
364
|
+
const ctx = createContext(tier)
|
|
365
|
+
const result = renderForm(node, ctx)
|
|
366
|
+
|
|
367
|
+
expect(result).toContain('Email')
|
|
368
|
+
expect(result).toContain('valid@example.com')
|
|
369
|
+
})
|
|
370
|
+
})
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
describe('[ansi] validation styling', () => {
|
|
374
|
+
it('renders error in red', async () => {
|
|
375
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
376
|
+
const node = createFormNode({
|
|
377
|
+
fields: [{ name: 'email', label: 'Email', error: 'Invalid' }],
|
|
378
|
+
})
|
|
379
|
+
const ctx = createContext('ansi')
|
|
380
|
+
const result = renderForm(node, ctx)
|
|
381
|
+
|
|
382
|
+
expect(result).toContain('\x1b[31m')
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it('renders valid field in green', async () => {
|
|
386
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
387
|
+
const node = createFormNode({
|
|
388
|
+
fields: [{ name: 'email', label: 'Email', valid: true }],
|
|
389
|
+
})
|
|
390
|
+
const ctx = createContext('ansi')
|
|
391
|
+
const result = renderForm(node, ctx)
|
|
392
|
+
|
|
393
|
+
expect(result).toContain('\x1b[32m')
|
|
394
|
+
})
|
|
395
|
+
})
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
// ============================================================================
|
|
399
|
+
// Submit Button Tests
|
|
400
|
+
// ============================================================================
|
|
401
|
+
|
|
402
|
+
describe('submit button', () => {
|
|
403
|
+
RENDER_TIERS.forEach((tier) => {
|
|
404
|
+
describe(`[${tier}] tier`, () => {
|
|
405
|
+
it('renders submit button', async () => {
|
|
406
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
407
|
+
const node = createFormNode({
|
|
408
|
+
fields: [{ name: 'email', label: 'Email' }],
|
|
409
|
+
submitLabel: 'Submit',
|
|
410
|
+
})
|
|
411
|
+
const ctx = createContext(tier)
|
|
412
|
+
const result = renderForm(node, ctx)
|
|
413
|
+
|
|
414
|
+
expect(result).toContain('Submit')
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
it('renders custom submit label', async () => {
|
|
418
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
419
|
+
const node = createFormNode({
|
|
420
|
+
fields: [{ name: 'email', label: 'Email' }],
|
|
421
|
+
submitLabel: 'Create Account',
|
|
422
|
+
})
|
|
423
|
+
const ctx = createContext(tier)
|
|
424
|
+
const result = renderForm(node, ctx)
|
|
425
|
+
|
|
426
|
+
expect(result).toContain('Create Account')
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
it('renders disabled submit button', async () => {
|
|
430
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
431
|
+
const node = createFormNode({
|
|
432
|
+
fields: [{ name: 'email', label: 'Email', error: 'Invalid' }],
|
|
433
|
+
submitLabel: 'Submit',
|
|
434
|
+
submitDisabled: true,
|
|
435
|
+
})
|
|
436
|
+
const ctx = createContext(tier)
|
|
437
|
+
const result = renderForm(node, ctx)
|
|
438
|
+
|
|
439
|
+
expect(result).toContain('Submit')
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
it('renders secondary action button', async () => {
|
|
443
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
444
|
+
const node = createFormNode({
|
|
445
|
+
fields: [{ name: 'email', label: 'Email' }],
|
|
446
|
+
submitLabel: 'Save',
|
|
447
|
+
cancelLabel: 'Cancel',
|
|
448
|
+
})
|
|
449
|
+
const ctx = createContext(tier)
|
|
450
|
+
const result = renderForm(node, ctx)
|
|
451
|
+
|
|
452
|
+
expect(result).toContain('Save')
|
|
453
|
+
expect(result).toContain('Cancel')
|
|
454
|
+
})
|
|
455
|
+
})
|
|
456
|
+
})
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
// ============================================================================
|
|
460
|
+
// Form Layout Tests
|
|
461
|
+
// ============================================================================
|
|
462
|
+
|
|
463
|
+
describe('form layout', () => {
|
|
464
|
+
describe('[ascii] tier', () => {
|
|
465
|
+
it('renders form with ASCII border', async () => {
|
|
466
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
467
|
+
const node = createFormNode({
|
|
468
|
+
title: 'Login',
|
|
469
|
+
fields: [{ name: 'username', label: 'Username' }],
|
|
470
|
+
border: true,
|
|
471
|
+
})
|
|
472
|
+
const ctx = createContext('ascii')
|
|
473
|
+
const result = renderForm(node, ctx)
|
|
474
|
+
|
|
475
|
+
expect(result).toMatch(/[+\-|]/)
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
it('groups related fields with ASCII border', async () => {
|
|
479
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
480
|
+
const node = createFormNode({
|
|
481
|
+
fields: [],
|
|
482
|
+
groups: [
|
|
483
|
+
{
|
|
484
|
+
title: 'Personal Info',
|
|
485
|
+
fields: [
|
|
486
|
+
{ name: 'firstName', label: 'First Name' },
|
|
487
|
+
{ name: 'lastName', label: 'Last Name' },
|
|
488
|
+
],
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
title: 'Contact',
|
|
492
|
+
fields: [{ name: 'email', label: 'Email' }],
|
|
493
|
+
},
|
|
494
|
+
],
|
|
495
|
+
})
|
|
496
|
+
const ctx = createContext('ascii')
|
|
497
|
+
const result = renderForm(node, ctx)
|
|
498
|
+
|
|
499
|
+
expect(result).toContain('Personal Info')
|
|
500
|
+
expect(result).toContain('Contact')
|
|
501
|
+
})
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
describe('[unicode] tier', () => {
|
|
505
|
+
it('renders form with Unicode border', async () => {
|
|
506
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
507
|
+
const node = createFormNode({
|
|
508
|
+
title: 'Login',
|
|
509
|
+
fields: [{ name: 'username', label: 'Username' }],
|
|
510
|
+
border: true,
|
|
511
|
+
})
|
|
512
|
+
const ctx = createContext('unicode')
|
|
513
|
+
const result = renderForm(node, ctx)
|
|
514
|
+
|
|
515
|
+
expect(result).toMatch(/[┌┐└┘─│]/)
|
|
516
|
+
})
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
describe('[markdown] tier', () => {
|
|
520
|
+
it('renders form fields with markdown formatting', async () => {
|
|
521
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
522
|
+
const node = createFormNode({
|
|
523
|
+
title: 'Registration',
|
|
524
|
+
fields: [{ name: 'email', label: 'Email', required: true }],
|
|
525
|
+
})
|
|
526
|
+
const ctx = createContext('markdown')
|
|
527
|
+
const result = renderForm(node, ctx)
|
|
528
|
+
|
|
529
|
+
expect(result).toMatch(/#+.*Registration/)
|
|
530
|
+
})
|
|
531
|
+
})
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
// ============================================================================
|
|
535
|
+
// Field State Tests
|
|
536
|
+
// ============================================================================
|
|
537
|
+
|
|
538
|
+
describe('field states', () => {
|
|
539
|
+
RENDER_TIERS.forEach((tier) => {
|
|
540
|
+
describe(`[${tier}] tier`, () => {
|
|
541
|
+
it('renders disabled field', async () => {
|
|
542
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
543
|
+
const node = createFormNode({
|
|
544
|
+
fields: [{ name: 'locked', label: 'Locked Field', disabled: true, value: 'Cannot edit' }],
|
|
545
|
+
})
|
|
546
|
+
const ctx = createContext(tier)
|
|
547
|
+
const result = renderForm(node, ctx)
|
|
548
|
+
|
|
549
|
+
expect(result).toContain('Locked Field')
|
|
550
|
+
expect(result).toContain('Cannot edit')
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
it('renders readonly field', async () => {
|
|
554
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
555
|
+
const node = createFormNode({
|
|
556
|
+
fields: [{ name: 'id', label: 'User ID', readonly: true, value: '12345' }],
|
|
557
|
+
})
|
|
558
|
+
const ctx = createContext(tier)
|
|
559
|
+
const result = renderForm(node, ctx)
|
|
560
|
+
|
|
561
|
+
expect(result).toContain('User ID')
|
|
562
|
+
expect(result).toContain('12345')
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
it('renders field with placeholder', async () => {
|
|
566
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
567
|
+
const node = createFormNode({
|
|
568
|
+
fields: [{ name: 'email', label: 'Email', placeholder: 'Enter your email' }],
|
|
569
|
+
})
|
|
570
|
+
const ctx = createContext(tier)
|
|
571
|
+
const result = renderForm(node, ctx)
|
|
572
|
+
|
|
573
|
+
expect(result).toContain('Enter your email')
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
it('renders field with helper text', async () => {
|
|
577
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
578
|
+
const node = createFormNode({
|
|
579
|
+
fields: [{
|
|
580
|
+
name: 'password',
|
|
581
|
+
label: 'Password',
|
|
582
|
+
helperText: 'Must be at least 8 characters',
|
|
583
|
+
}],
|
|
584
|
+
})
|
|
585
|
+
const ctx = createContext(tier)
|
|
586
|
+
const result = renderForm(node, ctx)
|
|
587
|
+
|
|
588
|
+
expect(result).toContain('Must be at least 8 characters')
|
|
589
|
+
})
|
|
590
|
+
})
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
describe('[ansi] field styling', () => {
|
|
594
|
+
it('renders disabled field as dimmed', async () => {
|
|
595
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
596
|
+
const node = createFormNode({
|
|
597
|
+
fields: [{ name: 'locked', label: 'Locked', disabled: true }],
|
|
598
|
+
})
|
|
599
|
+
const ctx = createContext('ansi')
|
|
600
|
+
const result = renderForm(node, ctx)
|
|
601
|
+
|
|
602
|
+
expect(result).toContain('\x1b[2m')
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
it('renders focused field with highlight', async () => {
|
|
606
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
607
|
+
const node = createFormNode({
|
|
608
|
+
fields: [{ name: 'active', label: 'Active', focused: true }],
|
|
609
|
+
})
|
|
610
|
+
const ctx = createContext('ansi')
|
|
611
|
+
const result = renderForm(node, ctx)
|
|
612
|
+
|
|
613
|
+
expect(result).toMatch(/\x1b\[(34|4|7)m/)
|
|
614
|
+
})
|
|
615
|
+
})
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
// ============================================================================
|
|
619
|
+
// Interactive Tier Tests
|
|
620
|
+
// ============================================================================
|
|
621
|
+
|
|
622
|
+
describe('[interactive] tier', () => {
|
|
623
|
+
it('renders form with keyboard hints', async () => {
|
|
624
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
625
|
+
const node = createFormNode({
|
|
626
|
+
fields: [{ name: 'email', label: 'Email' }],
|
|
627
|
+
submitLabel: 'Submit',
|
|
628
|
+
})
|
|
629
|
+
const ctx = createContext('interactive')
|
|
630
|
+
const result = renderForm(node, ctx)
|
|
631
|
+
|
|
632
|
+
expect(result).toMatch(/Tab|Enter|↑|↓|Arrow/i)
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
it('indicates currently focused field', async () => {
|
|
636
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
637
|
+
const node = createFormNode({
|
|
638
|
+
fields: [
|
|
639
|
+
{ name: 'first', label: 'First' },
|
|
640
|
+
{ name: 'second', label: 'Second', focused: true },
|
|
641
|
+
{ name: 'third', label: 'Third' },
|
|
642
|
+
],
|
|
643
|
+
})
|
|
644
|
+
const ctx = createContext('interactive')
|
|
645
|
+
const result = renderForm(node, ctx)
|
|
646
|
+
|
|
647
|
+
expect(result).toContain('Second')
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
it('shows cursor in text field', async () => {
|
|
651
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
652
|
+
const node = createFormNode({
|
|
653
|
+
fields: [{
|
|
654
|
+
name: 'text',
|
|
655
|
+
label: 'Text',
|
|
656
|
+
value: 'Hello',
|
|
657
|
+
focused: true,
|
|
658
|
+
cursorPosition: 5,
|
|
659
|
+
}],
|
|
660
|
+
})
|
|
661
|
+
const ctx = createContext('interactive')
|
|
662
|
+
const result = renderForm(node, ctx)
|
|
663
|
+
|
|
664
|
+
expect(result).toMatch(/\x1b\[(7|4)m|[|_▌]/)
|
|
665
|
+
})
|
|
666
|
+
|
|
667
|
+
it('renders submit button with hotkey', async () => {
|
|
668
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
669
|
+
const node = createFormNode({
|
|
670
|
+
fields: [{ name: 'email', label: 'Email' }],
|
|
671
|
+
submitLabel: 'Submit',
|
|
672
|
+
submitHotkey: 'Ctrl+Enter',
|
|
673
|
+
})
|
|
674
|
+
const ctx = createContext('interactive')
|
|
675
|
+
const result = renderForm(node, ctx)
|
|
676
|
+
|
|
677
|
+
expect(result).toContain('Ctrl+Enter')
|
|
678
|
+
})
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
// ============================================================================
|
|
682
|
+
// Form Groups Tests
|
|
683
|
+
// ============================================================================
|
|
684
|
+
|
|
685
|
+
describe('form groups', () => {
|
|
686
|
+
RENDER_TIERS.forEach((tier) => {
|
|
687
|
+
describe(`[${tier}] tier`, () => {
|
|
688
|
+
it('renders grouped fields with section headers', async () => {
|
|
689
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
690
|
+
const node = createFormNode({
|
|
691
|
+
fields: [],
|
|
692
|
+
groups: [
|
|
693
|
+
{
|
|
694
|
+
title: 'Account Details',
|
|
695
|
+
fields: [
|
|
696
|
+
{ name: 'username', label: 'Username' },
|
|
697
|
+
{ name: 'password', label: 'Password', type: 'password' },
|
|
698
|
+
],
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
title: 'Profile',
|
|
702
|
+
fields: [{ name: 'name', label: 'Full Name' }],
|
|
703
|
+
},
|
|
704
|
+
],
|
|
705
|
+
})
|
|
706
|
+
const ctx = createContext(tier)
|
|
707
|
+
const result = renderForm(node, ctx)
|
|
708
|
+
|
|
709
|
+
expect(result).toContain('Account Details')
|
|
710
|
+
expect(result).toContain('Profile')
|
|
711
|
+
expect(result).toContain('Username')
|
|
712
|
+
expect(result).toContain('Password')
|
|
713
|
+
expect(result).toContain('Full Name')
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
it('preserves group order', async () => {
|
|
717
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
718
|
+
const node = createFormNode({
|
|
719
|
+
fields: [],
|
|
720
|
+
groups: [
|
|
721
|
+
{ title: 'Section 1', fields: [{ name: 'a', label: 'A' }] },
|
|
722
|
+
{ title: 'Section 2', fields: [{ name: 'b', label: 'B' }] },
|
|
723
|
+
{ title: 'Section 3', fields: [{ name: 'c', label: 'C' }] },
|
|
724
|
+
],
|
|
725
|
+
})
|
|
726
|
+
const ctx = createContext(tier)
|
|
727
|
+
const result = renderForm(node, ctx)
|
|
728
|
+
|
|
729
|
+
const pos1 = result.indexOf('Section 1')
|
|
730
|
+
const pos2 = result.indexOf('Section 2')
|
|
731
|
+
const pos3 = result.indexOf('Section 3')
|
|
732
|
+
|
|
733
|
+
expect(pos1).toBeLessThan(pos2)
|
|
734
|
+
expect(pos2).toBeLessThan(pos3)
|
|
735
|
+
})
|
|
736
|
+
|
|
737
|
+
it('renders collapsible group', async () => {
|
|
738
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
739
|
+
const node = createFormNode({
|
|
740
|
+
fields: [],
|
|
741
|
+
groups: [
|
|
742
|
+
{
|
|
743
|
+
title: 'Advanced Options',
|
|
744
|
+
collapsible: true,
|
|
745
|
+
collapsed: false,
|
|
746
|
+
fields: [{ name: 'advanced', label: 'Advanced Setting' }],
|
|
747
|
+
},
|
|
748
|
+
],
|
|
749
|
+
})
|
|
750
|
+
const ctx = createContext(tier)
|
|
751
|
+
const result = renderForm(node, ctx)
|
|
752
|
+
|
|
753
|
+
expect(result).toContain('Advanced Options')
|
|
754
|
+
expect(result).toContain('Advanced Setting')
|
|
755
|
+
})
|
|
756
|
+
|
|
757
|
+
it('hides collapsed group content', async () => {
|
|
758
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
759
|
+
const node = createFormNode({
|
|
760
|
+
fields: [],
|
|
761
|
+
groups: [
|
|
762
|
+
{
|
|
763
|
+
title: 'Hidden Section',
|
|
764
|
+
collapsible: true,
|
|
765
|
+
collapsed: true,
|
|
766
|
+
fields: [{ name: 'hidden', label: 'Hidden Field' }],
|
|
767
|
+
},
|
|
768
|
+
],
|
|
769
|
+
})
|
|
770
|
+
const ctx = createContext(tier)
|
|
771
|
+
const result = renderForm(node, ctx)
|
|
772
|
+
|
|
773
|
+
expect(result).toContain('Hidden Section')
|
|
774
|
+
expect(result).not.toContain('Hidden Field')
|
|
775
|
+
})
|
|
776
|
+
})
|
|
777
|
+
})
|
|
778
|
+
})
|
|
779
|
+
|
|
780
|
+
// ============================================================================
|
|
781
|
+
// Edge Cases
|
|
782
|
+
// ============================================================================
|
|
783
|
+
|
|
784
|
+
describe('edge cases', () => {
|
|
785
|
+
it('handles missing fields array', async () => {
|
|
786
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
787
|
+
const node = createFormNode({})
|
|
788
|
+
const ctx = createContext('text')
|
|
789
|
+
const result = renderForm(node, ctx)
|
|
790
|
+
|
|
791
|
+
expect(typeof result).toBe('string')
|
|
792
|
+
})
|
|
793
|
+
|
|
794
|
+
it('handles null field values', async () => {
|
|
795
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
796
|
+
const node = createFormNode({
|
|
797
|
+
fields: [{ name: 'empty', label: 'Empty', value: null }],
|
|
798
|
+
})
|
|
799
|
+
const ctx = createContext('text')
|
|
800
|
+
const result = renderForm(node, ctx)
|
|
801
|
+
|
|
802
|
+
expect(result).toContain('Empty')
|
|
803
|
+
})
|
|
804
|
+
|
|
805
|
+
it('handles very long field labels', async () => {
|
|
806
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
807
|
+
const longLabel = 'A'.repeat(100)
|
|
808
|
+
const node = createFormNode({
|
|
809
|
+
fields: [{ name: 'long', label: longLabel }],
|
|
810
|
+
})
|
|
811
|
+
const ctx = createContext('text', { width: 40 })
|
|
812
|
+
const result = renderForm(node, ctx)
|
|
813
|
+
|
|
814
|
+
expect(typeof result).toBe('string')
|
|
815
|
+
})
|
|
816
|
+
|
|
817
|
+
it('handles unicode in labels and values', async () => {
|
|
818
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
819
|
+
const node = createFormNode({
|
|
820
|
+
fields: [{ name: 'unicode', label: '用户名', value: 'مرحبا' }],
|
|
821
|
+
})
|
|
822
|
+
const ctx = createContext('text')
|
|
823
|
+
const result = renderForm(node, ctx)
|
|
824
|
+
|
|
825
|
+
expect(result).toContain('用户名')
|
|
826
|
+
expect(result).toContain('مرحبا')
|
|
827
|
+
})
|
|
828
|
+
})
|
|
829
|
+
|
|
830
|
+
// ============================================================================
|
|
831
|
+
// Loading State Tests
|
|
832
|
+
// ============================================================================
|
|
833
|
+
|
|
834
|
+
describe('loading state', () => {
|
|
835
|
+
RENDER_TIERS.forEach((tier) => {
|
|
836
|
+
it(`[${tier}] renders form in loading state`, async () => {
|
|
837
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
838
|
+
const node = createFormNode({
|
|
839
|
+
fields: [{ name: 'email', label: 'Email' }],
|
|
840
|
+
submitLabel: 'Submit',
|
|
841
|
+
loading: true,
|
|
842
|
+
})
|
|
843
|
+
const ctx = createContext(tier)
|
|
844
|
+
const result = renderForm(node, ctx)
|
|
845
|
+
|
|
846
|
+
expect(result).toMatch(/loading|submitting|\.{3}|⠋|spinner/i)
|
|
847
|
+
})
|
|
848
|
+
})
|
|
849
|
+
})
|
|
850
|
+
|
|
851
|
+
// ============================================================================
|
|
852
|
+
// Success State Tests
|
|
853
|
+
// ============================================================================
|
|
854
|
+
|
|
855
|
+
describe('success state', () => {
|
|
856
|
+
RENDER_TIERS.forEach((tier) => {
|
|
857
|
+
it(`[${tier}] renders form success message`, async () => {
|
|
858
|
+
const { renderForm } = await import('../../../renderers/components/form')
|
|
859
|
+
const node = createFormNode({
|
|
860
|
+
fields: [{ name: 'email', label: 'Email' }],
|
|
861
|
+
success: 'Form submitted successfully!',
|
|
862
|
+
})
|
|
863
|
+
const ctx = createContext(tier)
|
|
864
|
+
const result = renderForm(node, ctx)
|
|
865
|
+
|
|
866
|
+
expect(result).toContain('Form submitted successfully')
|
|
867
|
+
})
|
|
868
|
+
})
|
|
869
|
+
})
|
|
870
|
+
})
|