@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.
Files changed (161) hide show
  1. package/.claude/commands/speckit.analyze.md +184 -0
  2. package/.claude/commands/speckit.checklist.md +294 -0
  3. package/.claude/commands/speckit.clarify.md +181 -0
  4. package/.claude/commands/speckit.constitution.md +82 -0
  5. package/.claude/commands/speckit.implement.md +135 -0
  6. package/.claude/commands/speckit.plan.md +89 -0
  7. package/.claude/commands/speckit.specify.md +256 -0
  8. package/.claude/commands/speckit.tasks.md +137 -0
  9. package/.claude/commands/speckit.taskstoissues.md +30 -0
  10. package/.github/workflows/deploy.yml +69 -0
  11. package/.husky/pre-commit +1 -0
  12. package/.prettierignore +4 -0
  13. package/.prettierrc +7 -0
  14. package/.specify/memory/constitution.md +73 -0
  15. package/.specify/scripts/bash/check-prerequisites.sh +166 -0
  16. package/.specify/scripts/bash/common.sh +156 -0
  17. package/.specify/scripts/bash/create-new-feature.sh +297 -0
  18. package/.specify/scripts/bash/setup-plan.sh +61 -0
  19. package/.specify/scripts/bash/update-agent-context.sh +799 -0
  20. package/.specify/templates/agent-file-template.md +28 -0
  21. package/.specify/templates/checklist-template.md +40 -0
  22. package/.specify/templates/plan-template.md +105 -0
  23. package/.specify/templates/spec-template.md +115 -0
  24. package/.specify/templates/tasks-template.md +250 -0
  25. package/CLAUDE.md +105 -0
  26. package/CONTRIBUTING.md +97 -0
  27. package/README.md +126 -0
  28. package/components.json +21 -0
  29. package/eslint.config.js +25 -0
  30. package/netlify.toml +50 -0
  31. package/package.json +94 -0
  32. package/playground/README.md +75 -0
  33. package/playground/index.html +22 -0
  34. package/playground/package.json +32 -0
  35. package/playground/public/favicon.svg +8 -0
  36. package/playground/src/App.css +256 -0
  37. package/playground/src/App.tsx +115 -0
  38. package/playground/src/assets/react.svg +1 -0
  39. package/playground/src/components/ErrorDisplay.tsx +13 -0
  40. package/playground/src/components/ExampleSelector.tsx +64 -0
  41. package/playground/src/components/Header.tsx +47 -0
  42. package/playground/src/components/JsonEditor.tsx +32 -0
  43. package/playground/src/components/Preview.tsx +78 -0
  44. package/playground/src/components/ThemeToggle.tsx +19 -0
  45. package/playground/src/data/examples.ts +1571 -0
  46. package/playground/src/hooks/useTheme.ts +55 -0
  47. package/playground/src/index.css +220 -0
  48. package/playground/src/main.tsx +10 -0
  49. package/playground/tsconfig.app.json +34 -0
  50. package/playground/tsconfig.json +13 -0
  51. package/playground/tsconfig.node.json +26 -0
  52. package/playground/vite.config.ts +31 -0
  53. package/specs/001-a2ui-renderer/checklists/requirements.md +41 -0
  54. package/specs/001-a2ui-renderer/data-model.md +140 -0
  55. package/specs/001-a2ui-renderer/plan.md +123 -0
  56. package/specs/001-a2ui-renderer/quickstart.md +141 -0
  57. package/specs/001-a2ui-renderer/research.md +140 -0
  58. package/specs/001-a2ui-renderer/spec.md +165 -0
  59. package/specs/001-a2ui-renderer/tasks.md +310 -0
  60. package/specs/002-playground/checklists/requirements.md +37 -0
  61. package/specs/002-playground/contracts/components.md +120 -0
  62. package/specs/002-playground/data-model.md +149 -0
  63. package/specs/002-playground/plan.md +73 -0
  64. package/specs/002-playground/quickstart.md +158 -0
  65. package/specs/002-playground/research.md +117 -0
  66. package/specs/002-playground/spec.md +109 -0
  67. package/specs/002-playground/tasks.md +224 -0
  68. package/src/0.8/A2UIRender.test.tsx +793 -0
  69. package/src/0.8/A2UIRender.tsx +142 -0
  70. package/src/0.8/components/ComponentRenderer.test.tsx +373 -0
  71. package/src/0.8/components/ComponentRenderer.tsx +163 -0
  72. package/src/0.8/components/UnknownComponent.tsx +49 -0
  73. package/src/0.8/components/display/AudioPlayerComponent.tsx +37 -0
  74. package/src/0.8/components/display/DividerComponent.tsx +23 -0
  75. package/src/0.8/components/display/IconComponent.tsx +137 -0
  76. package/src/0.8/components/display/ImageComponent.tsx +57 -0
  77. package/src/0.8/components/display/TextComponent.tsx +56 -0
  78. package/src/0.8/components/display/VideoComponent.tsx +31 -0
  79. package/src/0.8/components/display/display.test.tsx +660 -0
  80. package/src/0.8/components/display/index.ts +10 -0
  81. package/src/0.8/components/index.ts +14 -0
  82. package/src/0.8/components/interactive/ButtonComponent.tsx +44 -0
  83. package/src/0.8/components/interactive/CheckBoxComponent.tsx +45 -0
  84. package/src/0.8/components/interactive/DateTimeInputComponent.tsx +176 -0
  85. package/src/0.8/components/interactive/MultipleChoiceComponent.tsx +157 -0
  86. package/src/0.8/components/interactive/SliderComponent.tsx +53 -0
  87. package/src/0.8/components/interactive/TextFieldComponent.tsx +65 -0
  88. package/src/0.8/components/interactive/index.ts +10 -0
  89. package/src/0.8/components/interactive/interactive.test.tsx +618 -0
  90. package/src/0.8/components/layout/CardComponent.tsx +30 -0
  91. package/src/0.8/components/layout/ColumnComponent.tsx +93 -0
  92. package/src/0.8/components/layout/ListComponent.tsx +81 -0
  93. package/src/0.8/components/layout/ModalComponent.tsx +41 -0
  94. package/src/0.8/components/layout/RowComponent.tsx +94 -0
  95. package/src/0.8/components/layout/TabsComponent.tsx +59 -0
  96. package/src/0.8/components/layout/index.ts +10 -0
  97. package/src/0.8/components/layout/layout.test.tsx +558 -0
  98. package/src/0.8/contexts/A2UIProvider.test.tsx +226 -0
  99. package/src/0.8/contexts/A2UIProvider.tsx +54 -0
  100. package/src/0.8/contexts/ActionContext.test.tsx +242 -0
  101. package/src/0.8/contexts/ActionContext.tsx +105 -0
  102. package/src/0.8/contexts/ComponentsMapContext.tsx +125 -0
  103. package/src/0.8/contexts/DataModelContext.test.tsx +335 -0
  104. package/src/0.8/contexts/DataModelContext.tsx +184 -0
  105. package/src/0.8/contexts/SurfaceContext.test.tsx +339 -0
  106. package/src/0.8/contexts/SurfaceContext.tsx +197 -0
  107. package/src/0.8/hooks/useA2UIMessageHandler.test.tsx +399 -0
  108. package/src/0.8/hooks/useA2UIMessageHandler.ts +123 -0
  109. package/src/0.8/hooks/useComponent.test.tsx +148 -0
  110. package/src/0.8/hooks/useComponent.ts +39 -0
  111. package/src/0.8/hooks/useDataBinding.test.tsx +334 -0
  112. package/src/0.8/hooks/useDataBinding.ts +99 -0
  113. package/src/0.8/hooks/useDispatchAction.test.tsx +83 -0
  114. package/src/0.8/hooks/useDispatchAction.ts +35 -0
  115. package/src/0.8/hooks/useSurface.test.tsx +114 -0
  116. package/src/0.8/hooks/useSurface.ts +34 -0
  117. package/src/0.8/index.ts +38 -0
  118. package/src/0.8/schemas/client_to_server.json +50 -0
  119. package/src/0.8/schemas/server_to_client.json +148 -0
  120. package/src/0.8/schemas/standard_catalog_definition.json +661 -0
  121. package/src/0.8/types/index.ts +448 -0
  122. package/src/0.8/utils/dataBinding.test.ts +443 -0
  123. package/src/0.8/utils/dataBinding.ts +212 -0
  124. package/src/0.8/utils/pathUtils.test.ts +353 -0
  125. package/src/0.8/utils/pathUtils.ts +200 -0
  126. package/src/components/ui/button.tsx +62 -0
  127. package/src/components/ui/calendar.tsx +220 -0
  128. package/src/components/ui/card.tsx +92 -0
  129. package/src/components/ui/checkbox.tsx +30 -0
  130. package/src/components/ui/dialog.tsx +141 -0
  131. package/src/components/ui/input.tsx +21 -0
  132. package/src/components/ui/label.tsx +22 -0
  133. package/src/components/ui/native-select.tsx +53 -0
  134. package/src/components/ui/popover.tsx +46 -0
  135. package/src/components/ui/select.tsx +188 -0
  136. package/src/components/ui/separator.tsx +26 -0
  137. package/src/components/ui/slider.tsx +61 -0
  138. package/src/components/ui/tabs.tsx +64 -0
  139. package/src/components/ui/textarea.tsx +18 -0
  140. package/src/index.ts +1 -0
  141. package/src/lib/utils.ts +6 -0
  142. package/tsconfig.json +28 -0
  143. package/vite.config.ts +29 -0
  144. package/vitest.config.ts +22 -0
  145. package/vitest.setup.ts +8 -0
  146. package/website/README.md +4 -0
  147. package/website/assets/favicon.svg +8 -0
  148. package/website/content/.gitkeep +0 -0
  149. package/website/content/index.md +122 -0
  150. package/website/global.d.ts +9 -0
  151. package/website/package.json +17 -0
  152. package/website/plain.config.js +28 -0
  153. package/website/serve.json +6 -0
  154. package/website/src/client/color-mode-switch.css +47 -0
  155. package/website/src/client/index.js +61 -0
  156. package/website/src/client/moon.svg +1 -0
  157. package/website/src/client/sun.svg +1 -0
  158. package/website/src/components/Footer.jsx +9 -0
  159. package/website/src/components/Header.jsx +44 -0
  160. package/website/src/components/Page.jsx +28 -0
  161. package/website/src/global.css +423 -0
@@ -0,0 +1,660 @@
1
+ /**
2
+ * Display Components Tests
3
+ *
4
+ * Tests for TextComponent, ImageComponent, IconComponent, VideoComponent,
5
+ * AudioPlayerComponent, and DividerComponent.
6
+ */
7
+
8
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
9
+ import { render, screen } from '@testing-library/react'
10
+ import { TextComponent } from './TextComponent'
11
+ import { ImageComponent } from './ImageComponent'
12
+ import { IconComponent } from './IconComponent'
13
+ import { VideoComponent } from './VideoComponent'
14
+ import { AudioPlayerComponent } from './AudioPlayerComponent'
15
+ import { DividerComponent } from './DividerComponent'
16
+ import { DataModelProvider } from '../../contexts/DataModelContext'
17
+ import type { ReactNode } from 'react'
18
+
19
+ // Wrapper with DataModelProvider
20
+ const wrapper = ({ children }: { children: ReactNode }) => (
21
+ <DataModelProvider>{children}</DataModelProvider>
22
+ )
23
+
24
+ describe('TextComponent', () => {
25
+ it('should render text from literalString', () => {
26
+ render(
27
+ <TextComponent
28
+ surfaceId="surface-1"
29
+ componentId="text-1"
30
+ text={{ literalString: 'Hello World' }}
31
+ />,
32
+ { wrapper }
33
+ )
34
+ expect(screen.getByText('Hello World')).toBeInTheDocument()
35
+ })
36
+
37
+ it('should render empty string when text is undefined', () => {
38
+ const { container } = render(
39
+ <TextComponent surfaceId="surface-1" componentId="text-1" />,
40
+ { wrapper }
41
+ )
42
+ expect(container.querySelector('p')).toBeInTheDocument()
43
+ expect(container.querySelector('p')?.textContent).toBe('')
44
+ })
45
+
46
+ it('should render h1 element for h1 usageHint', () => {
47
+ const { container } = render(
48
+ <TextComponent
49
+ surfaceId="surface-1"
50
+ componentId="text-1"
51
+ text={{ literalString: 'Heading' }}
52
+ usageHint="h1"
53
+ />,
54
+ { wrapper }
55
+ )
56
+ expect(container.querySelector('h1')).toBeInTheDocument()
57
+ expect(container.querySelector('h1')?.textContent).toBe('Heading')
58
+ })
59
+
60
+ it('should render h2 element for h2 usageHint', () => {
61
+ const { container } = render(
62
+ <TextComponent
63
+ surfaceId="surface-1"
64
+ componentId="text-1"
65
+ text={{ literalString: 'Heading 2' }}
66
+ usageHint="h2"
67
+ />,
68
+ { wrapper }
69
+ )
70
+ expect(container.querySelector('h2')).toBeInTheDocument()
71
+ })
72
+
73
+ it('should render h3 element for h3 usageHint', () => {
74
+ const { container } = render(
75
+ <TextComponent
76
+ surfaceId="surface-1"
77
+ componentId="text-1"
78
+ text={{ literalString: 'Heading 3' }}
79
+ usageHint="h3"
80
+ />,
81
+ { wrapper }
82
+ )
83
+ expect(container.querySelector('h3')).toBeInTheDocument()
84
+ })
85
+
86
+ it('should render h4 element for h4 usageHint', () => {
87
+ const { container } = render(
88
+ <TextComponent
89
+ surfaceId="surface-1"
90
+ componentId="text-1"
91
+ text={{ literalString: 'Heading 4' }}
92
+ usageHint="h4"
93
+ />,
94
+ { wrapper }
95
+ )
96
+ expect(container.querySelector('h4')).toBeInTheDocument()
97
+ })
98
+
99
+ it('should render h5 element for h5 usageHint', () => {
100
+ const { container } = render(
101
+ <TextComponent
102
+ surfaceId="surface-1"
103
+ componentId="text-1"
104
+ text={{ literalString: 'Heading 5' }}
105
+ usageHint="h5"
106
+ />,
107
+ { wrapper }
108
+ )
109
+ expect(container.querySelector('h5')).toBeInTheDocument()
110
+ })
111
+
112
+ it('should render span element for caption usageHint', () => {
113
+ const { container } = render(
114
+ <TextComponent
115
+ surfaceId="surface-1"
116
+ componentId="text-1"
117
+ text={{ literalString: 'Caption text' }}
118
+ usageHint="caption"
119
+ />,
120
+ { wrapper }
121
+ )
122
+ expect(container.querySelector('span')).toBeInTheDocument()
123
+ })
124
+
125
+ it('should render p element for body usageHint', () => {
126
+ const { container } = render(
127
+ <TextComponent
128
+ surfaceId="surface-1"
129
+ componentId="text-1"
130
+ text={{ literalString: 'Body text' }}
131
+ usageHint="body"
132
+ />,
133
+ { wrapper }
134
+ )
135
+ expect(container.querySelector('p')).toBeInTheDocument()
136
+ })
137
+
138
+ it('should apply correct CSS classes for h1', () => {
139
+ const { container } = render(
140
+ <TextComponent
141
+ surfaceId="surface-1"
142
+ componentId="text-1"
143
+ text={{ literalString: 'Heading' }}
144
+ usageHint="h1"
145
+ />,
146
+ { wrapper }
147
+ )
148
+ const h1 = container.querySelector('h1')
149
+ expect(h1).toHaveClass('text-3xl', 'font-bold')
150
+ })
151
+
152
+ it('should apply correct CSS classes for caption', () => {
153
+ const { container } = render(
154
+ <TextComponent
155
+ surfaceId="surface-1"
156
+ componentId="text-1"
157
+ text={{ literalString: 'Caption' }}
158
+ usageHint="caption"
159
+ />,
160
+ { wrapper }
161
+ )
162
+ const span = container.querySelector('span')
163
+ expect(span).toHaveClass('text-sm', 'text-muted-foreground')
164
+ })
165
+
166
+ it('should have correct displayName', () => {
167
+ expect(TextComponent.displayName).toBe('A2UI.Text')
168
+ })
169
+ })
170
+
171
+ describe('ImageComponent', () => {
172
+ it('should render image with url', () => {
173
+ render(
174
+ <ImageComponent
175
+ surfaceId="surface-1"
176
+ componentId="image-1"
177
+ url={{ literalString: 'https://example.com/image.jpg' }}
178
+ />,
179
+ { wrapper }
180
+ )
181
+ const img = screen.getByRole('presentation')
182
+ expect(img).toHaveAttribute('src', 'https://example.com/image.jpg')
183
+ })
184
+
185
+ it('should return null when url is empty', () => {
186
+ const { container } = render(
187
+ <ImageComponent
188
+ surfaceId="surface-1"
189
+ componentId="image-1"
190
+ url={{ literalString: '' }}
191
+ />,
192
+ { wrapper }
193
+ )
194
+ expect(container.firstChild).toBeNull()
195
+ })
196
+
197
+ it('should return null when url is undefined', () => {
198
+ const { container } = render(
199
+ <ImageComponent surfaceId="surface-1" componentId="image-1" />,
200
+ { wrapper }
201
+ )
202
+ expect(container.firstChild).toBeNull()
203
+ })
204
+
205
+ it('should apply cover fit by default', () => {
206
+ render(
207
+ <ImageComponent
208
+ surfaceId="surface-1"
209
+ componentId="image-1"
210
+ url={{ literalString: 'https://example.com/image.jpg' }}
211
+ />,
212
+ { wrapper }
213
+ )
214
+ const img = screen.getByRole('presentation')
215
+ expect(img).toHaveClass('object-cover')
216
+ })
217
+
218
+ it('should apply contain fit', () => {
219
+ render(
220
+ <ImageComponent
221
+ surfaceId="surface-1"
222
+ componentId="image-1"
223
+ url={{ literalString: 'https://example.com/image.jpg' }}
224
+ fit="contain"
225
+ />,
226
+ { wrapper }
227
+ )
228
+ const img = screen.getByRole('presentation')
229
+ expect(img).toHaveClass('object-contain')
230
+ })
231
+
232
+ it('should apply fill fit', () => {
233
+ render(
234
+ <ImageComponent
235
+ surfaceId="surface-1"
236
+ componentId="image-1"
237
+ url={{ literalString: 'https://example.com/image.jpg' }}
238
+ fit="fill"
239
+ />,
240
+ { wrapper }
241
+ )
242
+ const img = screen.getByRole('presentation')
243
+ expect(img).toHaveClass('object-fill')
244
+ })
245
+
246
+ it('should apply none fit', () => {
247
+ render(
248
+ <ImageComponent
249
+ surfaceId="surface-1"
250
+ componentId="image-1"
251
+ url={{ literalString: 'https://example.com/image.jpg' }}
252
+ fit="none"
253
+ />,
254
+ { wrapper }
255
+ )
256
+ const img = screen.getByRole('presentation')
257
+ expect(img).toHaveClass('object-none')
258
+ })
259
+
260
+ it('should apply scale-down fit', () => {
261
+ render(
262
+ <ImageComponent
263
+ surfaceId="surface-1"
264
+ componentId="image-1"
265
+ url={{ literalString: 'https://example.com/image.jpg' }}
266
+ fit="scale-down"
267
+ />,
268
+ { wrapper }
269
+ )
270
+ const img = screen.getByRole('presentation')
271
+ expect(img).toHaveClass('object-scale-down')
272
+ })
273
+
274
+ it('should apply icon usageHint styles', () => {
275
+ render(
276
+ <ImageComponent
277
+ surfaceId="surface-1"
278
+ componentId="image-1"
279
+ url={{ literalString: 'https://example.com/image.jpg' }}
280
+ usageHint="icon"
281
+ />,
282
+ { wrapper }
283
+ )
284
+ const img = screen.getByRole('presentation')
285
+ expect(img).toHaveClass('w-6', 'h-6')
286
+ })
287
+
288
+ it('should apply avatar usageHint styles', () => {
289
+ render(
290
+ <ImageComponent
291
+ surfaceId="surface-1"
292
+ componentId="image-1"
293
+ url={{ literalString: 'https://example.com/image.jpg' }}
294
+ usageHint="avatar"
295
+ />,
296
+ { wrapper }
297
+ )
298
+ const img = screen.getByRole('presentation')
299
+ expect(img).toHaveClass('w-10', 'h-10', 'rounded-full')
300
+ })
301
+
302
+ it('should apply smallFeature usageHint styles', () => {
303
+ render(
304
+ <ImageComponent
305
+ surfaceId="surface-1"
306
+ componentId="image-1"
307
+ url={{ literalString: 'https://example.com/image.jpg' }}
308
+ usageHint="smallFeature"
309
+ />,
310
+ { wrapper }
311
+ )
312
+ const img = screen.getByRole('presentation')
313
+ expect(img).toHaveClass('w-16', 'h-16')
314
+ })
315
+
316
+ it('should apply mediumFeature usageHint styles', () => {
317
+ render(
318
+ <ImageComponent
319
+ surfaceId="surface-1"
320
+ componentId="image-1"
321
+ url={{ literalString: 'https://example.com/image.jpg' }}
322
+ usageHint="mediumFeature"
323
+ />,
324
+ { wrapper }
325
+ )
326
+ const img = screen.getByRole('presentation')
327
+ expect(img).toHaveClass('w-32', 'h-32')
328
+ })
329
+
330
+ it('should apply largeFeature usageHint styles', () => {
331
+ render(
332
+ <ImageComponent
333
+ surfaceId="surface-1"
334
+ componentId="image-1"
335
+ url={{ literalString: 'https://example.com/image.jpg' }}
336
+ usageHint="largeFeature"
337
+ />,
338
+ { wrapper }
339
+ )
340
+ const img = screen.getByRole('presentation')
341
+ expect(img).toHaveClass('w-48', 'h-48')
342
+ })
343
+
344
+ it('should apply header usageHint styles', () => {
345
+ render(
346
+ <ImageComponent
347
+ surfaceId="surface-1"
348
+ componentId="image-1"
349
+ url={{ literalString: 'https://example.com/image.jpg' }}
350
+ usageHint="header"
351
+ />,
352
+ { wrapper }
353
+ )
354
+ const img = screen.getByRole('presentation')
355
+ expect(img).toHaveClass('w-full', 'h-48')
356
+ })
357
+
358
+ it('should have lazy loading', () => {
359
+ render(
360
+ <ImageComponent
361
+ surfaceId="surface-1"
362
+ componentId="image-1"
363
+ url={{ literalString: 'https://example.com/image.jpg' }}
364
+ />,
365
+ { wrapper }
366
+ )
367
+ const img = screen.getByRole('presentation')
368
+ expect(img).toHaveAttribute('loading', 'lazy')
369
+ })
370
+
371
+ it('should have correct displayName', () => {
372
+ expect(ImageComponent.displayName).toBe('A2UI.Image')
373
+ })
374
+ })
375
+
376
+ describe('IconComponent', () => {
377
+ beforeEach(() => {
378
+ vi.spyOn(console, 'warn').mockImplementation(() => {})
379
+ })
380
+
381
+ it('should render known icon', () => {
382
+ const { container } = render(
383
+ <IconComponent
384
+ surfaceId="surface-1"
385
+ componentId="icon-1"
386
+ name={{ literalString: 'check' }}
387
+ />,
388
+ { wrapper }
389
+ )
390
+ // Lucide icons render as SVG
391
+ expect(container.querySelector('svg')).toBeInTheDocument()
392
+ })
393
+
394
+ it('should return null for empty icon name', () => {
395
+ const { container } = render(
396
+ <IconComponent
397
+ surfaceId="surface-1"
398
+ componentId="icon-1"
399
+ name={{ literalString: '' }}
400
+ />,
401
+ { wrapper }
402
+ )
403
+ expect(container.firstChild).toBeNull()
404
+ })
405
+
406
+ it('should return null for undefined icon name', () => {
407
+ const { container } = render(
408
+ <IconComponent surfaceId="surface-1" componentId="icon-1" />,
409
+ { wrapper }
410
+ )
411
+ expect(container.firstChild).toBeNull()
412
+ })
413
+
414
+ it('should return null and warn for unknown icon', () => {
415
+ const consoleWarn = vi.spyOn(console, 'warn').mockImplementation(() => {})
416
+ const { container } = render(
417
+ <IconComponent
418
+ surfaceId="surface-1"
419
+ componentId="icon-1"
420
+ name={{ literalString: 'unknownIcon' }}
421
+ />,
422
+ { wrapper }
423
+ )
424
+ expect(container.firstChild).toBeNull()
425
+ expect(consoleWarn).toHaveBeenCalledWith(
426
+ 'A2UI: Unknown icon name: unknownIcon'
427
+ )
428
+ consoleWarn.mockRestore()
429
+ })
430
+
431
+ it('should render various icons', () => {
432
+ const icons = [
433
+ 'add',
434
+ 'close',
435
+ 'edit',
436
+ 'delete',
437
+ 'search',
438
+ 'home',
439
+ 'settings',
440
+ ]
441
+ icons.forEach((iconName) => {
442
+ const { container, unmount } = render(
443
+ <IconComponent
444
+ surfaceId="surface-1"
445
+ componentId="icon-1"
446
+ name={{ literalString: iconName }}
447
+ />,
448
+ { wrapper }
449
+ )
450
+ expect(container.querySelector('svg')).toBeInTheDocument()
451
+ unmount()
452
+ })
453
+ })
454
+
455
+ it('should apply correct size classes', () => {
456
+ const { container } = render(
457
+ <IconComponent
458
+ surfaceId="surface-1"
459
+ componentId="icon-1"
460
+ name={{ literalString: 'check' }}
461
+ />,
462
+ { wrapper }
463
+ )
464
+ const svg = container.querySelector('svg')
465
+ expect(svg).toHaveClass('w-5', 'h-5')
466
+ })
467
+
468
+ it('should have correct displayName', () => {
469
+ expect(IconComponent.displayName).toBe('A2UI.Icon')
470
+ })
471
+ })
472
+
473
+ describe('VideoComponent', () => {
474
+ it('should render video with url', () => {
475
+ const { container } = render(
476
+ <VideoComponent
477
+ surfaceId="surface-1"
478
+ componentId="video-1"
479
+ url={{ literalString: 'https://example.com/video.mp4' }}
480
+ />,
481
+ { wrapper }
482
+ )
483
+ const video = container.querySelector('video')
484
+ expect(video).toBeInTheDocument()
485
+ expect(video).toHaveAttribute('src', 'https://example.com/video.mp4')
486
+ })
487
+
488
+ it('should return null when url is empty', () => {
489
+ const { container } = render(
490
+ <VideoComponent
491
+ surfaceId="surface-1"
492
+ componentId="video-1"
493
+ url={{ literalString: '' }}
494
+ />,
495
+ { wrapper }
496
+ )
497
+ expect(container.firstChild).toBeNull()
498
+ })
499
+
500
+ it('should return null when url is undefined', () => {
501
+ const { container } = render(
502
+ <VideoComponent surfaceId="surface-1" componentId="video-1" />,
503
+ { wrapper }
504
+ )
505
+ expect(container.firstChild).toBeNull()
506
+ })
507
+
508
+ it('should have controls attribute', () => {
509
+ const { container } = render(
510
+ <VideoComponent
511
+ surfaceId="surface-1"
512
+ componentId="video-1"
513
+ url={{ literalString: 'https://example.com/video.mp4' }}
514
+ />,
515
+ { wrapper }
516
+ )
517
+ const video = container.querySelector('video')
518
+ expect(video).toHaveAttribute('controls')
519
+ })
520
+
521
+ it('should have correct CSS classes', () => {
522
+ const { container } = render(
523
+ <VideoComponent
524
+ surfaceId="surface-1"
525
+ componentId="video-1"
526
+ url={{ literalString: 'https://example.com/video.mp4' }}
527
+ />,
528
+ { wrapper }
529
+ )
530
+ const video = container.querySelector('video')
531
+ expect(video).toHaveClass('w-full', 'rounded-lg')
532
+ })
533
+
534
+ it('should have correct displayName', () => {
535
+ expect(VideoComponent.displayName).toBe('A2UI.Video')
536
+ })
537
+ })
538
+
539
+ describe('AudioPlayerComponent', () => {
540
+ it('should render audio player with url', () => {
541
+ const { container } = render(
542
+ <AudioPlayerComponent
543
+ surfaceId="surface-1"
544
+ componentId="audio-1"
545
+ url={{ literalString: 'https://example.com/audio.mp3' }}
546
+ />,
547
+ { wrapper }
548
+ )
549
+ const audio = container.querySelector('audio')
550
+ expect(audio).toBeInTheDocument()
551
+ expect(audio).toHaveAttribute('src', 'https://example.com/audio.mp3')
552
+ })
553
+
554
+ it('should return null when url is empty', () => {
555
+ const { container } = render(
556
+ <AudioPlayerComponent
557
+ surfaceId="surface-1"
558
+ componentId="audio-1"
559
+ url={{ literalString: '' }}
560
+ />,
561
+ { wrapper }
562
+ )
563
+ expect(container.firstChild).toBeNull()
564
+ })
565
+
566
+ it('should return null when url is undefined', () => {
567
+ const { container } = render(
568
+ <AudioPlayerComponent surfaceId="surface-1" componentId="audio-1" />,
569
+ { wrapper }
570
+ )
571
+ expect(container.firstChild).toBeNull()
572
+ })
573
+
574
+ it('should render description when provided', () => {
575
+ render(
576
+ <AudioPlayerComponent
577
+ surfaceId="surface-1"
578
+ componentId="audio-1"
579
+ url={{ literalString: 'https://example.com/audio.mp3' }}
580
+ description={{ literalString: 'Audio description' }}
581
+ />,
582
+ { wrapper }
583
+ )
584
+ expect(screen.getByText('Audio description')).toBeInTheDocument()
585
+ })
586
+
587
+ it('should not render description when empty', () => {
588
+ const { container } = render(
589
+ <AudioPlayerComponent
590
+ surfaceId="surface-1"
591
+ componentId="audio-1"
592
+ url={{ literalString: 'https://example.com/audio.mp3' }}
593
+ description={{ literalString: '' }}
594
+ />,
595
+ { wrapper }
596
+ )
597
+ expect(container.querySelector('p')).toBeNull()
598
+ })
599
+
600
+ it('should have controls attribute', () => {
601
+ const { container } = render(
602
+ <AudioPlayerComponent
603
+ surfaceId="surface-1"
604
+ componentId="audio-1"
605
+ url={{ literalString: 'https://example.com/audio.mp3' }}
606
+ />,
607
+ { wrapper }
608
+ )
609
+ const audio = container.querySelector('audio')
610
+ expect(audio).toHaveAttribute('controls')
611
+ })
612
+
613
+ it('should have correct displayName', () => {
614
+ expect(AudioPlayerComponent.displayName).toBe('A2UI.AudioPlayer')
615
+ })
616
+ })
617
+
618
+ describe('DividerComponent', () => {
619
+ it('should render horizontal divider by default', () => {
620
+ const { container } = render(
621
+ <DividerComponent surfaceId="surface-1" componentId="divider-1" />,
622
+ { wrapper }
623
+ )
624
+ // Separator component renders a div with data-orientation
625
+ const separator = container.querySelector('[data-orientation="horizontal"]')
626
+ expect(separator).toBeInTheDocument()
627
+ })
628
+
629
+ it('should render horizontal divider when axis is horizontal', () => {
630
+ const { container } = render(
631
+ <DividerComponent
632
+ surfaceId="surface-1"
633
+ componentId="divider-1"
634
+ axis="horizontal"
635
+ />,
636
+ { wrapper }
637
+ )
638
+ const separator = container.querySelector('[data-orientation="horizontal"]')
639
+ expect(separator).toBeInTheDocument()
640
+ expect(separator).toHaveClass('w-full')
641
+ })
642
+
643
+ it('should render vertical divider when axis is vertical', () => {
644
+ const { container } = render(
645
+ <DividerComponent
646
+ surfaceId="surface-1"
647
+ componentId="divider-1"
648
+ axis="vertical"
649
+ />,
650
+ { wrapper }
651
+ )
652
+ const separator = container.querySelector('[data-orientation="vertical"]')
653
+ expect(separator).toBeInTheDocument()
654
+ expect(separator).toHaveClass('h-auto')
655
+ })
656
+
657
+ it('should have correct displayName', () => {
658
+ expect(DividerComponent.displayName).toBe('A2UI.Divider')
659
+ })
660
+ })
@@ -0,0 +1,10 @@
1
+ /**
2
+ * A2UI React Renderer - Display Components
3
+ */
4
+
5
+ export { TextComponent } from './TextComponent'
6
+ export { ImageComponent } from './ImageComponent'
7
+ export { IconComponent } from './IconComponent'
8
+ export { VideoComponent } from './VideoComponent'
9
+ export { AudioPlayerComponent } from './AudioPlayerComponent'
10
+ export { DividerComponent } from './DividerComponent'
@@ -0,0 +1,14 @@
1
+ /**
2
+ * A2UI React Renderer - Components Exports
3
+ */
4
+
5
+ export { ComponentRenderer, registerComponent } from './ComponentRenderer'
6
+
7
+ // Display components
8
+ export * from './display'
9
+
10
+ // Layout components
11
+ export * from './layout'
12
+
13
+ // Interactive components
14
+ export * from './interactive'