@easyops-cn/a2ui-react 0.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/.claude/commands/speckit.analyze.md +184 -0
- package/.claude/commands/speckit.checklist.md +294 -0
- package/.claude/commands/speckit.clarify.md +181 -0
- package/.claude/commands/speckit.constitution.md +82 -0
- package/.claude/commands/speckit.implement.md +135 -0
- package/.claude/commands/speckit.plan.md +89 -0
- package/.claude/commands/speckit.specify.md +256 -0
- package/.claude/commands/speckit.tasks.md +137 -0
- package/.claude/commands/speckit.taskstoissues.md +30 -0
- package/.github/workflows/deploy.yml +69 -0
- package/.husky/pre-commit +1 -0
- package/.prettierignore +4 -0
- package/.prettierrc +7 -0
- package/.specify/memory/constitution.md +73 -0
- package/.specify/scripts/bash/check-prerequisites.sh +166 -0
- package/.specify/scripts/bash/common.sh +156 -0
- package/.specify/scripts/bash/create-new-feature.sh +297 -0
- package/.specify/scripts/bash/setup-plan.sh +61 -0
- package/.specify/scripts/bash/update-agent-context.sh +799 -0
- package/.specify/templates/agent-file-template.md +28 -0
- package/.specify/templates/checklist-template.md +40 -0
- package/.specify/templates/plan-template.md +105 -0
- package/.specify/templates/spec-template.md +115 -0
- package/.specify/templates/tasks-template.md +250 -0
- package/CLAUDE.md +105 -0
- package/CONTRIBUTING.md +97 -0
- package/README.md +126 -0
- package/components.json +21 -0
- package/eslint.config.js +25 -0
- package/netlify.toml +50 -0
- package/package.json +94 -0
- package/playground/README.md +75 -0
- package/playground/index.html +22 -0
- package/playground/package.json +32 -0
- package/playground/public/favicon.svg +8 -0
- package/playground/src/App.css +256 -0
- package/playground/src/App.tsx +115 -0
- package/playground/src/assets/react.svg +1 -0
- package/playground/src/components/ErrorDisplay.tsx +13 -0
- package/playground/src/components/ExampleSelector.tsx +64 -0
- package/playground/src/components/Header.tsx +47 -0
- package/playground/src/components/JsonEditor.tsx +32 -0
- package/playground/src/components/Preview.tsx +78 -0
- package/playground/src/components/ThemeToggle.tsx +19 -0
- package/playground/src/data/examples.ts +1571 -0
- package/playground/src/hooks/useTheme.ts +55 -0
- package/playground/src/index.css +220 -0
- package/playground/src/main.tsx +10 -0
- package/playground/tsconfig.app.json +34 -0
- package/playground/tsconfig.json +13 -0
- package/playground/tsconfig.node.json +26 -0
- package/playground/vite.config.ts +31 -0
- package/specs/001-a2ui-renderer/checklists/requirements.md +41 -0
- package/specs/001-a2ui-renderer/data-model.md +140 -0
- package/specs/001-a2ui-renderer/plan.md +123 -0
- package/specs/001-a2ui-renderer/quickstart.md +141 -0
- package/specs/001-a2ui-renderer/research.md +140 -0
- package/specs/001-a2ui-renderer/spec.md +165 -0
- package/specs/001-a2ui-renderer/tasks.md +310 -0
- package/specs/002-playground/checklists/requirements.md +37 -0
- package/specs/002-playground/contracts/components.md +120 -0
- package/specs/002-playground/data-model.md +149 -0
- package/specs/002-playground/plan.md +73 -0
- package/specs/002-playground/quickstart.md +158 -0
- package/specs/002-playground/research.md +117 -0
- package/specs/002-playground/spec.md +109 -0
- package/specs/002-playground/tasks.md +224 -0
- package/src/0.8/A2UIRender.test.tsx +793 -0
- package/src/0.8/A2UIRender.tsx +142 -0
- package/src/0.8/components/ComponentRenderer.test.tsx +373 -0
- package/src/0.8/components/ComponentRenderer.tsx +163 -0
- package/src/0.8/components/UnknownComponent.tsx +49 -0
- package/src/0.8/components/display/AudioPlayerComponent.tsx +37 -0
- package/src/0.8/components/display/DividerComponent.tsx +23 -0
- package/src/0.8/components/display/IconComponent.tsx +137 -0
- package/src/0.8/components/display/ImageComponent.tsx +57 -0
- package/src/0.8/components/display/TextComponent.tsx +56 -0
- package/src/0.8/components/display/VideoComponent.tsx +31 -0
- package/src/0.8/components/display/display.test.tsx +660 -0
- package/src/0.8/components/display/index.ts +10 -0
- package/src/0.8/components/index.ts +14 -0
- package/src/0.8/components/interactive/ButtonComponent.tsx +44 -0
- package/src/0.8/components/interactive/CheckBoxComponent.tsx +45 -0
- package/src/0.8/components/interactive/DateTimeInputComponent.tsx +176 -0
- package/src/0.8/components/interactive/MultipleChoiceComponent.tsx +157 -0
- package/src/0.8/components/interactive/SliderComponent.tsx +53 -0
- package/src/0.8/components/interactive/TextFieldComponent.tsx +65 -0
- package/src/0.8/components/interactive/index.ts +10 -0
- package/src/0.8/components/interactive/interactive.test.tsx +618 -0
- package/src/0.8/components/layout/CardComponent.tsx +30 -0
- package/src/0.8/components/layout/ColumnComponent.tsx +93 -0
- package/src/0.8/components/layout/ListComponent.tsx +81 -0
- package/src/0.8/components/layout/ModalComponent.tsx +41 -0
- package/src/0.8/components/layout/RowComponent.tsx +94 -0
- package/src/0.8/components/layout/TabsComponent.tsx +59 -0
- package/src/0.8/components/layout/index.ts +10 -0
- package/src/0.8/components/layout/layout.test.tsx +558 -0
- package/src/0.8/contexts/A2UIProvider.test.tsx +226 -0
- package/src/0.8/contexts/A2UIProvider.tsx +54 -0
- package/src/0.8/contexts/ActionContext.test.tsx +242 -0
- package/src/0.8/contexts/ActionContext.tsx +105 -0
- package/src/0.8/contexts/ComponentsMapContext.tsx +125 -0
- package/src/0.8/contexts/DataModelContext.test.tsx +335 -0
- package/src/0.8/contexts/DataModelContext.tsx +184 -0
- package/src/0.8/contexts/SurfaceContext.test.tsx +339 -0
- package/src/0.8/contexts/SurfaceContext.tsx +197 -0
- package/src/0.8/hooks/useA2UIMessageHandler.test.tsx +399 -0
- package/src/0.8/hooks/useA2UIMessageHandler.ts +123 -0
- package/src/0.8/hooks/useComponent.test.tsx +148 -0
- package/src/0.8/hooks/useComponent.ts +39 -0
- package/src/0.8/hooks/useDataBinding.test.tsx +334 -0
- package/src/0.8/hooks/useDataBinding.ts +99 -0
- package/src/0.8/hooks/useDispatchAction.test.tsx +83 -0
- package/src/0.8/hooks/useDispatchAction.ts +35 -0
- package/src/0.8/hooks/useSurface.test.tsx +114 -0
- package/src/0.8/hooks/useSurface.ts +34 -0
- package/src/0.8/index.ts +38 -0
- package/src/0.8/schemas/client_to_server.json +50 -0
- package/src/0.8/schemas/server_to_client.json +148 -0
- package/src/0.8/schemas/standard_catalog_definition.json +661 -0
- package/src/0.8/types/index.ts +448 -0
- package/src/0.8/utils/dataBinding.test.ts +443 -0
- package/src/0.8/utils/dataBinding.ts +212 -0
- package/src/0.8/utils/pathUtils.test.ts +353 -0
- package/src/0.8/utils/pathUtils.ts +200 -0
- package/src/components/ui/button.tsx +62 -0
- package/src/components/ui/calendar.tsx +220 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/dialog.tsx +141 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +22 -0
- package/src/components/ui/native-select.tsx +53 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/select.tsx +188 -0
- package/src/components/ui/separator.tsx +26 -0
- package/src/components/ui/slider.tsx +61 -0
- package/src/components/ui/tabs.tsx +64 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/index.ts +1 -0
- package/src/lib/utils.ts +6 -0
- package/tsconfig.json +28 -0
- package/vite.config.ts +29 -0
- package/vitest.config.ts +22 -0
- package/vitest.setup.ts +8 -0
- package/website/README.md +4 -0
- package/website/assets/favicon.svg +8 -0
- package/website/content/.gitkeep +0 -0
- package/website/content/index.md +122 -0
- package/website/global.d.ts +9 -0
- package/website/package.json +17 -0
- package/website/plain.config.js +28 -0
- package/website/serve.json +6 -0
- package/website/src/client/color-mode-switch.css +47 -0
- package/website/src/client/index.js +61 -0
- package/website/src/client/moon.svg +1 -0
- package/website/src/client/sun.svg +1 -0
- package/website/src/components/Footer.jsx +9 -0
- package/website/src/components/Header.jsx +44 -0
- package/website/src/components/Page.jsx +28 -0
- package/website/src/global.css +423 -0
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive Components Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for ButtonComponent, CheckBoxComponent, TextFieldComponent,
|
|
5
|
+
* DateTimeInputComponent, MultipleChoiceComponent, and SliderComponent.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
9
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
|
10
|
+
import userEvent from '@testing-library/user-event'
|
|
11
|
+
import { ButtonComponent } from './ButtonComponent'
|
|
12
|
+
import { CheckBoxComponent } from './CheckBoxComponent'
|
|
13
|
+
import { TextFieldComponent } from './TextFieldComponent'
|
|
14
|
+
import { DateTimeInputComponent } from './DateTimeInputComponent'
|
|
15
|
+
import { MultipleChoiceComponent } from './MultipleChoiceComponent'
|
|
16
|
+
import { SliderComponent } from './SliderComponent'
|
|
17
|
+
import { DataModelProvider } from '../../contexts/DataModelContext'
|
|
18
|
+
import { ActionProvider } from '../../contexts/ActionContext'
|
|
19
|
+
import type { ReactNode } from 'react'
|
|
20
|
+
import type { ActionPayload } from '@/0.8/types'
|
|
21
|
+
|
|
22
|
+
// Mock ComponentRenderer
|
|
23
|
+
vi.mock('../ComponentRenderer', () => ({
|
|
24
|
+
ComponentRenderer: vi.fn(({ componentId }) => (
|
|
25
|
+
<span data-testid={`component-${componentId}`}>{componentId}</span>
|
|
26
|
+
)),
|
|
27
|
+
}))
|
|
28
|
+
|
|
29
|
+
// Wrapper with providers
|
|
30
|
+
const createWrapper = (onAction?: (action: ActionPayload) => void) => {
|
|
31
|
+
return ({ children }: { children: ReactNode }) => (
|
|
32
|
+
<DataModelProvider>
|
|
33
|
+
<ActionProvider onAction={onAction}>{children}</ActionProvider>
|
|
34
|
+
</DataModelProvider>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const wrapper = createWrapper()
|
|
39
|
+
|
|
40
|
+
describe('ButtonComponent', () => {
|
|
41
|
+
it('should render button with default text', () => {
|
|
42
|
+
render(<ButtonComponent surfaceId="surface-1" componentId="button-1" />, {
|
|
43
|
+
wrapper,
|
|
44
|
+
})
|
|
45
|
+
expect(screen.getByRole('button')).toHaveTextContent('Button')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should render child component when provided', async () => {
|
|
49
|
+
render(
|
|
50
|
+
<ButtonComponent
|
|
51
|
+
surfaceId="surface-1"
|
|
52
|
+
componentId="button-1"
|
|
53
|
+
child="button-text"
|
|
54
|
+
/>,
|
|
55
|
+
{ wrapper }
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
await waitFor(() => {
|
|
59
|
+
expect(screen.getByTestId('component-button-text')).toBeInTheDocument()
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should render with outline variant by default', () => {
|
|
64
|
+
render(<ButtonComponent surfaceId="surface-1" componentId="button-1" />, {
|
|
65
|
+
wrapper,
|
|
66
|
+
})
|
|
67
|
+
// Outline variant has specific classes
|
|
68
|
+
const button = screen.getByRole('button')
|
|
69
|
+
expect(button).toBeInTheDocument()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should render with default variant when primary is true', () => {
|
|
73
|
+
render(
|
|
74
|
+
<ButtonComponent
|
|
75
|
+
surfaceId="surface-1"
|
|
76
|
+
componentId="button-1"
|
|
77
|
+
primary={true}
|
|
78
|
+
/>,
|
|
79
|
+
{ wrapper }
|
|
80
|
+
)
|
|
81
|
+
const button = screen.getByRole('button')
|
|
82
|
+
expect(button).toBeInTheDocument()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('should dispatch action on click', async () => {
|
|
86
|
+
const onAction = vi.fn()
|
|
87
|
+
const actionWrapper = createWrapper(onAction)
|
|
88
|
+
const user = userEvent.setup()
|
|
89
|
+
|
|
90
|
+
render(
|
|
91
|
+
<ButtonComponent
|
|
92
|
+
surfaceId="surface-1"
|
|
93
|
+
componentId="button-1"
|
|
94
|
+
action={{ name: 'submit' }}
|
|
95
|
+
/>,
|
|
96
|
+
{ wrapper: actionWrapper }
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
await user.click(screen.getByRole('button'))
|
|
100
|
+
|
|
101
|
+
expect(onAction).toHaveBeenCalledWith({
|
|
102
|
+
surfaceId: 'surface-1',
|
|
103
|
+
name: 'submit',
|
|
104
|
+
context: {},
|
|
105
|
+
sourceComponentId: 'button-1',
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('should dispatch action with context', async () => {
|
|
110
|
+
const onAction = vi.fn()
|
|
111
|
+
const actionWrapper = createWrapper(onAction)
|
|
112
|
+
const user = userEvent.setup()
|
|
113
|
+
|
|
114
|
+
render(
|
|
115
|
+
<ButtonComponent
|
|
116
|
+
surfaceId="surface-1"
|
|
117
|
+
componentId="button-1"
|
|
118
|
+
action={{
|
|
119
|
+
name: 'submit',
|
|
120
|
+
context: [{ key: 'type', value: { literalString: 'form' } }],
|
|
121
|
+
}}
|
|
122
|
+
/>,
|
|
123
|
+
{ wrapper: actionWrapper }
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
await user.click(screen.getByRole('button'))
|
|
127
|
+
|
|
128
|
+
expect(onAction).toHaveBeenCalledWith({
|
|
129
|
+
surfaceId: 'surface-1',
|
|
130
|
+
name: 'submit',
|
|
131
|
+
context: { type: 'form' },
|
|
132
|
+
sourceComponentId: 'button-1',
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('should not dispatch when no action is provided', async () => {
|
|
137
|
+
const onAction = vi.fn()
|
|
138
|
+
const actionWrapper = createWrapper(onAction)
|
|
139
|
+
const user = userEvent.setup()
|
|
140
|
+
|
|
141
|
+
render(<ButtonComponent surfaceId="surface-1" componentId="button-1" />, {
|
|
142
|
+
wrapper: actionWrapper,
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
await user.click(screen.getByRole('button'))
|
|
146
|
+
|
|
147
|
+
expect(onAction).not.toHaveBeenCalled()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('should have correct displayName', () => {
|
|
151
|
+
expect(ButtonComponent.displayName).toBe('A2UI.Button')
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
describe('CheckBoxComponent', () => {
|
|
156
|
+
it('should render checkbox', () => {
|
|
157
|
+
render(
|
|
158
|
+
<CheckBoxComponent surfaceId="surface-1" componentId="checkbox-1" />,
|
|
159
|
+
{ wrapper }
|
|
160
|
+
)
|
|
161
|
+
expect(screen.getByRole('checkbox')).toBeInTheDocument()
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('should render label when provided', () => {
|
|
165
|
+
render(
|
|
166
|
+
<CheckBoxComponent
|
|
167
|
+
surfaceId="surface-1"
|
|
168
|
+
componentId="checkbox-1"
|
|
169
|
+
label={{ literalString: 'Accept terms' }}
|
|
170
|
+
/>,
|
|
171
|
+
{ wrapper }
|
|
172
|
+
)
|
|
173
|
+
expect(screen.getByText('Accept terms')).toBeInTheDocument()
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('should not render label when empty', () => {
|
|
177
|
+
const { container } = render(
|
|
178
|
+
<CheckBoxComponent
|
|
179
|
+
surfaceId="surface-1"
|
|
180
|
+
componentId="checkbox-1"
|
|
181
|
+
label={{ literalString: '' }}
|
|
182
|
+
/>,
|
|
183
|
+
{ wrapper }
|
|
184
|
+
)
|
|
185
|
+
expect(container.querySelector('label')).toBeNull()
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('should be unchecked by default', () => {
|
|
189
|
+
render(
|
|
190
|
+
<CheckBoxComponent surfaceId="surface-1" componentId="checkbox-1" />,
|
|
191
|
+
{ wrapper }
|
|
192
|
+
)
|
|
193
|
+
const checkbox = screen.getByRole('checkbox')
|
|
194
|
+
expect(checkbox).not.toBeChecked()
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('should toggle checked state on click', async () => {
|
|
198
|
+
const user = userEvent.setup()
|
|
199
|
+
|
|
200
|
+
render(
|
|
201
|
+
<CheckBoxComponent
|
|
202
|
+
surfaceId="surface-1"
|
|
203
|
+
componentId="checkbox-1"
|
|
204
|
+
value={{ path: '/form/accepted' }}
|
|
205
|
+
/>,
|
|
206
|
+
{ wrapper }
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
const checkbox = screen.getByRole('checkbox')
|
|
210
|
+
expect(checkbox).not.toBeChecked()
|
|
211
|
+
|
|
212
|
+
await user.click(checkbox)
|
|
213
|
+
expect(checkbox).toBeChecked()
|
|
214
|
+
|
|
215
|
+
await user.click(checkbox)
|
|
216
|
+
expect(checkbox).not.toBeChecked()
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('should have correct displayName', () => {
|
|
220
|
+
expect(CheckBoxComponent.displayName).toBe('A2UI.CheckBox')
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
describe('TextFieldComponent', () => {
|
|
225
|
+
it('should render input field', () => {
|
|
226
|
+
render(<TextFieldComponent surfaceId="surface-1" componentId="field-1" />, {
|
|
227
|
+
wrapper,
|
|
228
|
+
})
|
|
229
|
+
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('should render label when provided', () => {
|
|
233
|
+
render(
|
|
234
|
+
<TextFieldComponent
|
|
235
|
+
surfaceId="surface-1"
|
|
236
|
+
componentId="field-1"
|
|
237
|
+
label={{ literalString: 'Name' }}
|
|
238
|
+
/>,
|
|
239
|
+
{ wrapper }
|
|
240
|
+
)
|
|
241
|
+
expect(screen.getByText('Name')).toBeInTheDocument()
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('should not render label when empty', () => {
|
|
245
|
+
const { container } = render(
|
|
246
|
+
<TextFieldComponent
|
|
247
|
+
surfaceId="surface-1"
|
|
248
|
+
componentId="field-1"
|
|
249
|
+
label={{ literalString: '' }}
|
|
250
|
+
/>,
|
|
251
|
+
{ wrapper }
|
|
252
|
+
)
|
|
253
|
+
expect(container.querySelector('label')).toBeNull()
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('should render text input for shortText type', () => {
|
|
257
|
+
render(
|
|
258
|
+
<TextFieldComponent
|
|
259
|
+
surfaceId="surface-1"
|
|
260
|
+
componentId="field-1"
|
|
261
|
+
textFieldType="shortText"
|
|
262
|
+
/>,
|
|
263
|
+
{ wrapper }
|
|
264
|
+
)
|
|
265
|
+
const input = screen.getByRole('textbox')
|
|
266
|
+
expect(input.tagName).toBe('INPUT')
|
|
267
|
+
expect(input).toHaveAttribute('type', 'text')
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('should render textarea for longText type', () => {
|
|
271
|
+
render(
|
|
272
|
+
<TextFieldComponent
|
|
273
|
+
surfaceId="surface-1"
|
|
274
|
+
componentId="field-1"
|
|
275
|
+
textFieldType="longText"
|
|
276
|
+
/>,
|
|
277
|
+
{ wrapper }
|
|
278
|
+
)
|
|
279
|
+
const textarea = screen.getByRole('textbox')
|
|
280
|
+
expect(textarea.tagName).toBe('TEXTAREA')
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('should render number input for number type', () => {
|
|
284
|
+
render(
|
|
285
|
+
<TextFieldComponent
|
|
286
|
+
surfaceId="surface-1"
|
|
287
|
+
componentId="field-1"
|
|
288
|
+
textFieldType="number"
|
|
289
|
+
/>,
|
|
290
|
+
{ wrapper }
|
|
291
|
+
)
|
|
292
|
+
const input = screen.getByRole('spinbutton')
|
|
293
|
+
expect(input).toHaveAttribute('type', 'number')
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('should render password input for obscured type', () => {
|
|
297
|
+
const { container } = render(
|
|
298
|
+
<TextFieldComponent
|
|
299
|
+
surfaceId="surface-1"
|
|
300
|
+
componentId="field-1"
|
|
301
|
+
textFieldType="obscured"
|
|
302
|
+
/>,
|
|
303
|
+
{ wrapper }
|
|
304
|
+
)
|
|
305
|
+
const input = container.querySelector('input[type="password"]')
|
|
306
|
+
expect(input).toBeInTheDocument()
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('should render date input for date type', () => {
|
|
310
|
+
const { container } = render(
|
|
311
|
+
<TextFieldComponent
|
|
312
|
+
surfaceId="surface-1"
|
|
313
|
+
componentId="field-1"
|
|
314
|
+
textFieldType="date"
|
|
315
|
+
/>,
|
|
316
|
+
{ wrapper }
|
|
317
|
+
)
|
|
318
|
+
const input = container.querySelector('input[type="date"]')
|
|
319
|
+
expect(input).toBeInTheDocument()
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('should update value on input', async () => {
|
|
323
|
+
const user = userEvent.setup()
|
|
324
|
+
|
|
325
|
+
render(
|
|
326
|
+
<TextFieldComponent
|
|
327
|
+
surfaceId="surface-1"
|
|
328
|
+
componentId="field-1"
|
|
329
|
+
text={{ path: '/form/name' }}
|
|
330
|
+
/>,
|
|
331
|
+
{ wrapper }
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
const input = screen.getByRole('textbox')
|
|
335
|
+
await user.type(input, 'John')
|
|
336
|
+
|
|
337
|
+
expect(input).toHaveValue('John')
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('should have correct displayName', () => {
|
|
341
|
+
expect(TextFieldComponent.displayName).toBe('A2UI.TextField')
|
|
342
|
+
})
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
describe('DateTimeInputComponent', () => {
|
|
346
|
+
it('should render date picker button by default', () => {
|
|
347
|
+
render(
|
|
348
|
+
<DateTimeInputComponent surfaceId="surface-1" componentId="datetime-1" />,
|
|
349
|
+
{ wrapper }
|
|
350
|
+
)
|
|
351
|
+
// Component uses Calendar/Popover UI, renders a button trigger
|
|
352
|
+
expect(
|
|
353
|
+
screen.getByRole('button', { name: /选择日期/i })
|
|
354
|
+
).toBeInTheDocument()
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('should render date picker button when enableDate is true', () => {
|
|
358
|
+
render(
|
|
359
|
+
<DateTimeInputComponent
|
|
360
|
+
surfaceId="surface-1"
|
|
361
|
+
componentId="datetime-1"
|
|
362
|
+
enableDate={true}
|
|
363
|
+
enableTime={false}
|
|
364
|
+
/>,
|
|
365
|
+
{ wrapper }
|
|
366
|
+
)
|
|
367
|
+
expect(
|
|
368
|
+
screen.getByRole('button', { name: /选择日期/i })
|
|
369
|
+
).toBeInTheDocument()
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
it('should render time input when only enableTime is true', () => {
|
|
373
|
+
const { container } = render(
|
|
374
|
+
<DateTimeInputComponent
|
|
375
|
+
surfaceId="surface-1"
|
|
376
|
+
componentId="datetime-1"
|
|
377
|
+
enableDate={false}
|
|
378
|
+
enableTime={true}
|
|
379
|
+
/>,
|
|
380
|
+
{ wrapper }
|
|
381
|
+
)
|
|
382
|
+
const input = container.querySelector('input[type="time"]')
|
|
383
|
+
expect(input).toBeInTheDocument()
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('should render datetime picker button when both are enabled', () => {
|
|
387
|
+
render(
|
|
388
|
+
<DateTimeInputComponent
|
|
389
|
+
surfaceId="surface-1"
|
|
390
|
+
componentId="datetime-1"
|
|
391
|
+
enableDate={true}
|
|
392
|
+
enableTime={true}
|
|
393
|
+
/>,
|
|
394
|
+
{ wrapper }
|
|
395
|
+
)
|
|
396
|
+
// Component uses Calendar/Popover UI with time input inside
|
|
397
|
+
expect(
|
|
398
|
+
screen.getByRole('button', { name: /选择日期和时间/i })
|
|
399
|
+
).toBeInTheDocument()
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it('should update time value on change', async () => {
|
|
403
|
+
const { container } = render(
|
|
404
|
+
<DateTimeInputComponent
|
|
405
|
+
surfaceId="surface-1"
|
|
406
|
+
componentId="datetime-1"
|
|
407
|
+
enableDate={false}
|
|
408
|
+
enableTime={true}
|
|
409
|
+
value={{ path: '/form/time' }}
|
|
410
|
+
/>,
|
|
411
|
+
{ wrapper }
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
const input = container.querySelector(
|
|
415
|
+
'input[type="time"]'
|
|
416
|
+
) as HTMLInputElement
|
|
417
|
+
fireEvent.change(input, { target: { value: '14:30' } })
|
|
418
|
+
|
|
419
|
+
expect(input).toHaveValue('14:30')
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
it('should have correct displayName', () => {
|
|
423
|
+
expect(DateTimeInputComponent.displayName).toBe('A2UI.DateTimeInput')
|
|
424
|
+
})
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
describe('MultipleChoiceComponent', () => {
|
|
428
|
+
it('should return null when no options', () => {
|
|
429
|
+
const { container } = render(
|
|
430
|
+
<MultipleChoiceComponent surfaceId="surface-1" componentId="choice-1" />,
|
|
431
|
+
{ wrapper }
|
|
432
|
+
)
|
|
433
|
+
expect(container.firstChild).toBeNull()
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
it('should return null when options is empty', () => {
|
|
437
|
+
const { container } = render(
|
|
438
|
+
<MultipleChoiceComponent
|
|
439
|
+
surfaceId="surface-1"
|
|
440
|
+
componentId="choice-1"
|
|
441
|
+
options={[]}
|
|
442
|
+
/>,
|
|
443
|
+
{ wrapper }
|
|
444
|
+
)
|
|
445
|
+
expect(container.firstChild).toBeNull()
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
it('should render select with options when maxAllowedSelections is 1', async () => {
|
|
449
|
+
render(
|
|
450
|
+
<MultipleChoiceComponent
|
|
451
|
+
surfaceId="surface-1"
|
|
452
|
+
componentId="choice-1"
|
|
453
|
+
maxAllowedSelections={1}
|
|
454
|
+
options={[
|
|
455
|
+
{ label: { literalString: 'Option A' }, value: 'a' },
|
|
456
|
+
{ label: { literalString: 'Option B' }, value: 'b' },
|
|
457
|
+
]}
|
|
458
|
+
/>,
|
|
459
|
+
{ wrapper }
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
// Select trigger is a combobox role
|
|
463
|
+
expect(screen.getByRole('combobox')).toBeInTheDocument()
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
it('should show placeholder when no selection and maxAllowedSelections is 1', () => {
|
|
467
|
+
render(
|
|
468
|
+
<MultipleChoiceComponent
|
|
469
|
+
surfaceId="surface-1"
|
|
470
|
+
componentId="choice-1"
|
|
471
|
+
maxAllowedSelections={1}
|
|
472
|
+
options={[{ label: { literalString: 'Option A' }, value: 'a' }]}
|
|
473
|
+
/>,
|
|
474
|
+
{ wrapper }
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
expect(screen.getByText('Select an option')).toBeInTheDocument()
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it('should render checkboxes for multi-select by default', () => {
|
|
481
|
+
render(
|
|
482
|
+
<MultipleChoiceComponent
|
|
483
|
+
surfaceId="surface-1"
|
|
484
|
+
componentId="choice-1"
|
|
485
|
+
options={[
|
|
486
|
+
{ label: { literalString: 'Option A' }, value: 'a' },
|
|
487
|
+
{ label: { literalString: 'Option B' }, value: 'b' },
|
|
488
|
+
]}
|
|
489
|
+
/>,
|
|
490
|
+
{ wrapper }
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
// Should render checkboxes instead of combobox
|
|
494
|
+
const checkboxes = screen.getAllByRole('checkbox')
|
|
495
|
+
expect(checkboxes).toHaveLength(2)
|
|
496
|
+
expect(screen.getByText('Option A')).toBeInTheDocument()
|
|
497
|
+
expect(screen.getByText('Option B')).toBeInTheDocument()
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
it('should allow multiple selections', async () => {
|
|
501
|
+
const user = userEvent.setup()
|
|
502
|
+
|
|
503
|
+
render(
|
|
504
|
+
<MultipleChoiceComponent
|
|
505
|
+
surfaceId="surface-1"
|
|
506
|
+
componentId="choice-1"
|
|
507
|
+
selections={{ path: '/form/selections' }}
|
|
508
|
+
options={[
|
|
509
|
+
{ label: { literalString: 'Option A' }, value: 'a' },
|
|
510
|
+
{ label: { literalString: 'Option B' }, value: 'b' },
|
|
511
|
+
]}
|
|
512
|
+
/>,
|
|
513
|
+
{ wrapper }
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
const checkboxes = screen.getAllByRole('checkbox')
|
|
517
|
+
await user.click(checkboxes[0])
|
|
518
|
+
await user.click(checkboxes[1])
|
|
519
|
+
|
|
520
|
+
// Re-query after clicks since component re-renders
|
|
521
|
+
const updatedCheckboxes = screen.getAllByRole('checkbox')
|
|
522
|
+
expect(updatedCheckboxes[0]).toHaveAttribute('data-state', 'checked')
|
|
523
|
+
expect(updatedCheckboxes[1]).toHaveAttribute('data-state', 'checked')
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
it('should respect maxAllowedSelections limit', async () => {
|
|
527
|
+
const user = userEvent.setup()
|
|
528
|
+
|
|
529
|
+
render(
|
|
530
|
+
<MultipleChoiceComponent
|
|
531
|
+
surfaceId="surface-1"
|
|
532
|
+
componentId="choice-1"
|
|
533
|
+
selections={{ path: '/form/selections' }}
|
|
534
|
+
maxAllowedSelections={2}
|
|
535
|
+
options={[
|
|
536
|
+
{ label: { literalString: 'Option A' }, value: 'a' },
|
|
537
|
+
{ label: { literalString: 'Option B' }, value: 'b' },
|
|
538
|
+
{ label: { literalString: 'Option C' }, value: 'c' },
|
|
539
|
+
]}
|
|
540
|
+
/>,
|
|
541
|
+
{ wrapper }
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
const checkboxes = screen.getAllByRole('checkbox')
|
|
545
|
+
await user.click(checkboxes[0])
|
|
546
|
+
await user.click(checkboxes[1])
|
|
547
|
+
|
|
548
|
+
// Re-query after clicks since component re-renders
|
|
549
|
+
const updatedCheckboxes = screen.getAllByRole('checkbox')
|
|
550
|
+
// Third checkbox should be disabled after reaching max
|
|
551
|
+
expect(updatedCheckboxes[2]).toBeDisabled()
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
it('should have correct displayName', () => {
|
|
555
|
+
expect(MultipleChoiceComponent.displayName).toBe('A2UI.MultipleChoice')
|
|
556
|
+
})
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
describe('SliderComponent', () => {
|
|
560
|
+
it('should render slider', () => {
|
|
561
|
+
render(<SliderComponent surfaceId="surface-1" componentId="slider-1" />, {
|
|
562
|
+
wrapper,
|
|
563
|
+
})
|
|
564
|
+
expect(screen.getByRole('slider')).toBeInTheDocument()
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
it('should display min and max values', () => {
|
|
568
|
+
render(
|
|
569
|
+
<SliderComponent
|
|
570
|
+
surfaceId="surface-1"
|
|
571
|
+
componentId="slider-1"
|
|
572
|
+
minValue={0}
|
|
573
|
+
maxValue={100}
|
|
574
|
+
/>,
|
|
575
|
+
{ wrapper }
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
// Min and max values are displayed, but min value may appear twice (as min label and current value)
|
|
579
|
+
expect(screen.getAllByText('0').length).toBeGreaterThanOrEqual(1)
|
|
580
|
+
expect(screen.getByText('100')).toBeInTheDocument()
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
it('should display custom min and max values', () => {
|
|
584
|
+
render(
|
|
585
|
+
<SliderComponent
|
|
586
|
+
surfaceId="surface-1"
|
|
587
|
+
componentId="slider-1"
|
|
588
|
+
minValue={10}
|
|
589
|
+
maxValue={50}
|
|
590
|
+
/>,
|
|
591
|
+
{ wrapper }
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
// Multiple elements may have the same text (min value and current value)
|
|
595
|
+
expect(screen.getAllByText('10').length).toBeGreaterThanOrEqual(1)
|
|
596
|
+
expect(screen.getByText('50')).toBeInTheDocument()
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
it('should display current value', () => {
|
|
600
|
+
render(
|
|
601
|
+
<SliderComponent
|
|
602
|
+
surfaceId="surface-1"
|
|
603
|
+
componentId="slider-1"
|
|
604
|
+
minValue={0}
|
|
605
|
+
maxValue={100}
|
|
606
|
+
/>,
|
|
607
|
+
{ wrapper }
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
// Default value should be minValue (0)
|
|
611
|
+
const valueDisplay = screen.getAllByText('0')
|
|
612
|
+
expect(valueDisplay.length).toBeGreaterThanOrEqual(1)
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
it('should have correct displayName', () => {
|
|
616
|
+
expect(SliderComponent.displayName).toBe('A2UI.Slider')
|
|
617
|
+
})
|
|
618
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CardComponent - Card container.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { memo } from 'react'
|
|
6
|
+
import type { CardComponentProps } from '@/0.8/types'
|
|
7
|
+
import { Card, CardContent } from '@/components/ui/card'
|
|
8
|
+
import { ComponentRenderer } from '../ComponentRenderer'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Card component - container with card styling.
|
|
12
|
+
*/
|
|
13
|
+
export const CardComponent = memo(function CardComponent({
|
|
14
|
+
surfaceId,
|
|
15
|
+
child,
|
|
16
|
+
}: CardComponentProps) {
|
|
17
|
+
if (!child) {
|
|
18
|
+
return <Card />
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Card>
|
|
23
|
+
<CardContent className="p-4">
|
|
24
|
+
<ComponentRenderer surfaceId={surfaceId} componentId={child} />
|
|
25
|
+
</CardContent>
|
|
26
|
+
</Card>
|
|
27
|
+
)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
CardComponent.displayName = 'A2UI.Card'
|