@furystack/shades-common-components 10.0.35 → 11.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 (295) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/esm/components/animations.spec.d.ts +2 -0
  3. package/esm/components/animations.spec.d.ts.map +1 -0
  4. package/esm/components/animations.spec.js +201 -0
  5. package/esm/components/animations.spec.js.map +1 -0
  6. package/esm/components/app-bar-link.js +21 -20
  7. package/esm/components/app-bar-link.js.map +1 -1
  8. package/esm/components/app-bar-link.spec.d.ts +2 -0
  9. package/esm/components/app-bar-link.spec.d.ts.map +1 -0
  10. package/esm/components/app-bar-link.spec.js +252 -0
  11. package/esm/components/app-bar-link.spec.js.map +1 -0
  12. package/esm/components/app-bar.js +21 -21
  13. package/esm/components/app-bar.js.map +1 -1
  14. package/esm/components/app-bar.spec.d.ts +2 -0
  15. package/esm/components/app-bar.spec.d.ts.map +1 -0
  16. package/esm/components/app-bar.spec.js +117 -0
  17. package/esm/components/app-bar.spec.js.map +1 -0
  18. package/esm/components/avatar.d.ts.map +1 -1
  19. package/esm/components/avatar.js +15 -19
  20. package/esm/components/avatar.js.map +1 -1
  21. package/esm/components/avatar.spec.d.ts +2 -0
  22. package/esm/components/avatar.spec.d.ts.map +1 -0
  23. package/esm/components/avatar.spec.js +114 -0
  24. package/esm/components/avatar.spec.js.map +1 -0
  25. package/esm/components/button.d.ts.map +1 -1
  26. package/esm/components/button.js +145 -156
  27. package/esm/components/button.js.map +1 -1
  28. package/esm/components/button.spec.d.ts +2 -0
  29. package/esm/components/button.spec.d.ts.map +1 -0
  30. package/esm/components/button.spec.js +155 -0
  31. package/esm/components/button.spec.js.map +1 -0
  32. package/esm/components/command-palette/command-palette-input.d.ts.map +1 -1
  33. package/esm/components/command-palette/command-palette-input.js +18 -16
  34. package/esm/components/command-palette/command-palette-input.js.map +1 -1
  35. package/esm/components/command-palette/command-palette-input.spec.d.ts +2 -0
  36. package/esm/components/command-palette/command-palette-input.spec.d.ts.map +1 -0
  37. package/esm/components/command-palette/command-palette-input.spec.js +233 -0
  38. package/esm/components/command-palette/command-palette-input.spec.js.map +1 -0
  39. package/esm/components/command-palette/command-palette-manager.spec.d.ts +2 -0
  40. package/esm/components/command-palette/command-palette-manager.spec.d.ts.map +1 -0
  41. package/esm/components/command-palette/command-palette-manager.spec.js +362 -0
  42. package/esm/components/command-palette/command-palette-manager.spec.js.map +1 -0
  43. package/esm/components/command-palette/command-palette-suggestion-list.d.ts.map +1 -1
  44. package/esm/components/command-palette/command-palette-suggestion-list.js +42 -46
  45. package/esm/components/command-palette/command-palette-suggestion-list.js.map +1 -1
  46. package/esm/components/command-palette/command-palette-suggestion-list.spec.d.ts +2 -0
  47. package/esm/components/command-palette/command-palette-suggestion-list.spec.d.ts.map +1 -0
  48. package/esm/components/command-palette/command-palette-suggestion-list.spec.js +376 -0
  49. package/esm/components/command-palette/command-palette-suggestion-list.spec.js.map +1 -0
  50. package/esm/components/command-palette/index.d.ts.map +1 -1
  51. package/esm/components/command-palette/index.js +100 -110
  52. package/esm/components/command-palette/index.js.map +1 -1
  53. package/esm/components/command-palette/index.spec.d.ts +2 -0
  54. package/esm/components/command-palette/index.spec.d.ts.map +1 -0
  55. package/esm/components/command-palette/index.spec.js +509 -0
  56. package/esm/components/command-palette/index.spec.js.map +1 -0
  57. package/esm/components/data-grid/body.js +1 -1
  58. package/esm/components/data-grid/body.js.map +1 -1
  59. package/esm/components/data-grid/body.spec.d.ts +2 -0
  60. package/esm/components/data-grid/body.spec.d.ts.map +1 -0
  61. package/esm/components/data-grid/body.spec.js +228 -0
  62. package/esm/components/data-grid/body.spec.js.map +1 -0
  63. package/esm/components/data-grid/data-grid-row.d.ts.map +1 -1
  64. package/esm/components/data-grid/data-grid-row.js +49 -73
  65. package/esm/components/data-grid/data-grid-row.js.map +1 -1
  66. package/esm/components/data-grid/data-grid-row.spec.d.ts +2 -0
  67. package/esm/components/data-grid/data-grid-row.spec.d.ts.map +1 -0
  68. package/esm/components/data-grid/data-grid-row.spec.js +296 -0
  69. package/esm/components/data-grid/data-grid-row.spec.js.map +1 -0
  70. package/esm/components/data-grid/data-grid.d.ts.map +1 -1
  71. package/esm/components/data-grid/data-grid.js +35 -28
  72. package/esm/components/data-grid/data-grid.js.map +1 -1
  73. package/esm/components/data-grid/data-grid.spec.d.ts +2 -0
  74. package/esm/components/data-grid/data-grid.spec.d.ts.map +1 -0
  75. package/esm/components/data-grid/data-grid.spec.js +544 -0
  76. package/esm/components/data-grid/data-grid.spec.js.map +1 -0
  77. package/esm/components/data-grid/footer.js +21 -15
  78. package/esm/components/data-grid/footer.js.map +1 -1
  79. package/esm/components/data-grid/footer.spec.d.ts +2 -0
  80. package/esm/components/data-grid/footer.spec.d.ts.map +1 -0
  81. package/esm/components/data-grid/footer.spec.js +264 -0
  82. package/esm/components/data-grid/footer.spec.js.map +1 -0
  83. package/esm/components/data-grid/header.d.ts.map +1 -1
  84. package/esm/components/data-grid/header.js +55 -33
  85. package/esm/components/data-grid/header.js.map +1 -1
  86. package/esm/components/data-grid/header.spec.d.ts +2 -0
  87. package/esm/components/data-grid/header.spec.d.ts.map +1 -0
  88. package/esm/components/data-grid/header.spec.js +421 -0
  89. package/esm/components/data-grid/header.spec.js.map +1 -0
  90. package/esm/components/data-grid/selection-cell.d.ts.map +1 -1
  91. package/esm/components/data-grid/selection-cell.js +13 -6
  92. package/esm/components/data-grid/selection-cell.js.map +1 -1
  93. package/esm/components/data-grid/selection-cell.spec.d.ts +2 -0
  94. package/esm/components/data-grid/selection-cell.spec.d.ts.map +1 -0
  95. package/esm/components/data-grid/selection-cell.spec.js +118 -0
  96. package/esm/components/data-grid/selection-cell.spec.js.map +1 -0
  97. package/esm/components/fab.d.ts.map +1 -1
  98. package/esm/components/fab.js +10 -1
  99. package/esm/components/fab.js.map +1 -1
  100. package/esm/components/fab.spec.d.ts +2 -0
  101. package/esm/components/fab.spec.d.ts.map +1 -0
  102. package/esm/components/fab.spec.js +95 -0
  103. package/esm/components/fab.spec.js.map +1 -0
  104. package/esm/components/form.spec.d.ts +2 -0
  105. package/esm/components/form.spec.d.ts.map +1 -0
  106. package/esm/components/form.spec.js +314 -0
  107. package/esm/components/form.spec.js.map +1 -0
  108. package/esm/components/grid.d.ts.map +1 -1
  109. package/esm/components/grid.js +40 -37
  110. package/esm/components/grid.js.map +1 -1
  111. package/esm/components/grid.spec.d.ts +2 -0
  112. package/esm/components/grid.spec.d.ts.map +1 -0
  113. package/esm/components/grid.spec.js +316 -0
  114. package/esm/components/grid.spec.js.map +1 -0
  115. package/esm/components/inputs/autocomplete.spec.d.ts +2 -0
  116. package/esm/components/inputs/autocomplete.spec.d.ts.map +1 -0
  117. package/esm/components/inputs/autocomplete.spec.js +194 -0
  118. package/esm/components/inputs/autocomplete.spec.js.map +1 -0
  119. package/esm/components/inputs/input.d.ts.map +1 -1
  120. package/esm/components/inputs/input.js +141 -109
  121. package/esm/components/inputs/input.js.map +1 -1
  122. package/esm/components/inputs/input.spec.d.ts +2 -0
  123. package/esm/components/inputs/input.spec.d.ts.map +1 -0
  124. package/esm/components/inputs/input.spec.js +577 -0
  125. package/esm/components/inputs/input.spec.js.map +1 -0
  126. package/esm/components/inputs/text-area.d.ts.map +1 -1
  127. package/esm/components/inputs/text-area.js +54 -58
  128. package/esm/components/inputs/text-area.js.map +1 -1
  129. package/esm/components/inputs/text-area.spec.d.ts +2 -0
  130. package/esm/components/inputs/text-area.spec.d.ts.map +1 -0
  131. package/esm/components/inputs/text-area.spec.js +214 -0
  132. package/esm/components/inputs/text-area.spec.js.map +1 -0
  133. package/esm/components/loader.js +1 -1
  134. package/esm/components/loader.js.map +1 -1
  135. package/esm/components/loader.spec.d.ts +2 -0
  136. package/esm/components/loader.spec.d.ts.map +1 -0
  137. package/esm/components/loader.spec.js +251 -0
  138. package/esm/components/loader.spec.js.map +1 -0
  139. package/esm/components/modal.d.ts.map +1 -1
  140. package/esm/components/modal.js +11 -9
  141. package/esm/components/modal.js.map +1 -1
  142. package/esm/components/modal.spec.d.ts +2 -0
  143. package/esm/components/modal.spec.d.ts.map +1 -0
  144. package/esm/components/modal.spec.js +227 -0
  145. package/esm/components/modal.spec.js.map +1 -0
  146. package/esm/components/noty-list.d.ts.map +1 -1
  147. package/esm/components/noty-list.js +39 -40
  148. package/esm/components/noty-list.js.map +1 -1
  149. package/esm/components/noty-list.spec.d.ts +2 -0
  150. package/esm/components/noty-list.spec.d.ts.map +1 -0
  151. package/esm/components/noty-list.spec.js +486 -0
  152. package/esm/components/noty-list.spec.js.map +1 -0
  153. package/esm/components/paper.d.ts.map +1 -1
  154. package/esm/components/paper.js +15 -12
  155. package/esm/components/paper.js.map +1 -1
  156. package/esm/components/paper.spec.d.ts +2 -0
  157. package/esm/components/paper.spec.d.ts.map +1 -0
  158. package/esm/components/paper.spec.js +63 -0
  159. package/esm/components/paper.spec.js.map +1 -0
  160. package/esm/components/skeleton.js +1 -1
  161. package/esm/components/skeleton.js.map +1 -1
  162. package/esm/components/skeleton.spec.d.ts +2 -0
  163. package/esm/components/skeleton.spec.d.ts.map +1 -0
  164. package/esm/components/skeleton.spec.js +159 -0
  165. package/esm/components/skeleton.spec.js.map +1 -0
  166. package/esm/components/styles.spec.d.ts +2 -0
  167. package/esm/components/styles.spec.d.ts.map +1 -0
  168. package/esm/components/styles.spec.js +56 -0
  169. package/esm/components/styles.spec.js.map +1 -0
  170. package/esm/components/suggest/index.d.ts.map +1 -1
  171. package/esm/components/suggest/index.js +74 -83
  172. package/esm/components/suggest/index.js.map +1 -1
  173. package/esm/components/suggest/index.spec.d.ts +2 -0
  174. package/esm/components/suggest/index.spec.d.ts.map +1 -0
  175. package/esm/components/suggest/index.spec.js +515 -0
  176. package/esm/components/suggest/index.spec.js.map +1 -0
  177. package/esm/components/suggest/suggest-input.d.ts.map +1 -1
  178. package/esm/components/suggest/suggest-input.js +16 -17
  179. package/esm/components/suggest/suggest-input.js.map +1 -1
  180. package/esm/components/suggest/suggest-input.spec.d.ts +2 -0
  181. package/esm/components/suggest/suggest-input.spec.d.ts.map +1 -0
  182. package/esm/components/suggest/suggest-input.spec.js +138 -0
  183. package/esm/components/suggest/suggest-input.spec.js.map +1 -0
  184. package/esm/components/suggest/suggest-manager.spec.d.ts +2 -0
  185. package/esm/components/suggest/suggest-manager.spec.d.ts.map +1 -0
  186. package/esm/components/suggest/suggest-manager.spec.js +308 -0
  187. package/esm/components/suggest/suggest-manager.spec.js.map +1 -0
  188. package/esm/components/suggest/suggestion-list.d.ts.map +1 -1
  189. package/esm/components/suggest/suggestion-list.js +43 -48
  190. package/esm/components/suggest/suggestion-list.js.map +1 -1
  191. package/esm/components/suggest/suggestion-list.spec.d.ts +2 -0
  192. package/esm/components/suggest/suggestion-list.spec.d.ts.map +1 -0
  193. package/esm/components/suggest/suggestion-list.spec.js +252 -0
  194. package/esm/components/suggest/suggestion-list.spec.js.map +1 -0
  195. package/esm/components/tabs.d.ts.map +1 -1
  196. package/esm/components/tabs.js +32 -18
  197. package/esm/components/tabs.js.map +1 -1
  198. package/esm/components/tabs.spec.d.ts +2 -0
  199. package/esm/components/tabs.spec.d.ts.map +1 -0
  200. package/esm/components/tabs.spec.js +187 -0
  201. package/esm/components/tabs.spec.js.map +1 -0
  202. package/esm/components/wizard/index.d.ts.map +1 -1
  203. package/esm/components/wizard/index.js +10 -7
  204. package/esm/components/wizard/index.js.map +1 -1
  205. package/esm/components/wizard/index.spec.d.ts +2 -0
  206. package/esm/components/wizard/index.spec.d.ts.map +1 -0
  207. package/esm/components/wizard/index.spec.js +171 -0
  208. package/esm/components/wizard/index.spec.js.map +1 -0
  209. package/esm/services/collection-service.spec.js +391 -2
  210. package/esm/services/collection-service.spec.js.map +1 -1
  211. package/esm/services/css-variable-theme.d.ts.map +1 -1
  212. package/esm/services/css-variable-theme.js +21 -1
  213. package/esm/services/css-variable-theme.js.map +1 -1
  214. package/esm/services/css-variable-theme.spec.d.ts +2 -0
  215. package/esm/services/css-variable-theme.spec.d.ts.map +1 -0
  216. package/esm/services/css-variable-theme.spec.js +169 -0
  217. package/esm/services/css-variable-theme.spec.js.map +1 -0
  218. package/esm/services/default-palette.d.ts +4 -0
  219. package/esm/services/default-palette.d.ts.map +1 -1
  220. package/esm/services/default-palette.js +22 -0
  221. package/esm/services/default-palette.js.map +1 -1
  222. package/esm/services/theme-provider-service.d.ts +59 -1
  223. package/esm/services/theme-provider-service.d.ts.map +1 -1
  224. package/esm/services/theme-provider-service.js.map +1 -1
  225. package/esm/services/theme-provider-service.spec.d.ts +2 -0
  226. package/esm/services/theme-provider-service.spec.d.ts.map +1 -0
  227. package/esm/services/theme-provider-service.spec.js +166 -0
  228. package/esm/services/theme-provider-service.spec.js.map +1 -0
  229. package/package.json +2 -2
  230. package/src/components/animations.spec.ts +299 -0
  231. package/src/components/app-bar-link.spec.tsx +341 -0
  232. package/src/components/app-bar-link.tsx +21 -21
  233. package/src/components/app-bar.spec.tsx +142 -0
  234. package/src/components/app-bar.tsx +22 -22
  235. package/src/components/avatar.spec.tsx +146 -0
  236. package/src/components/avatar.tsx +17 -20
  237. package/src/components/button.spec.tsx +193 -0
  238. package/src/components/button.tsx +162 -197
  239. package/src/components/command-palette/command-palette-input.spec.tsx +320 -0
  240. package/src/components/command-palette/command-palette-input.tsx +19 -22
  241. package/src/components/command-palette/command-palette-manager.spec.ts +470 -0
  242. package/src/components/command-palette/command-palette-suggestion-list.spec.tsx +499 -0
  243. package/src/components/command-palette/command-palette-suggestion-list.tsx +42 -46
  244. package/src/components/command-palette/index.spec.tsx +684 -0
  245. package/src/components/command-palette/index.tsx +107 -136
  246. package/src/components/data-grid/body.spec.tsx +340 -0
  247. package/src/components/data-grid/body.tsx +1 -1
  248. package/src/components/data-grid/data-grid-row.spec.tsx +382 -0
  249. package/src/components/data-grid/data-grid-row.tsx +50 -82
  250. package/src/components/data-grid/data-grid.spec.tsx +939 -0
  251. package/src/components/data-grid/data-grid.tsx +38 -35
  252. package/src/components/data-grid/footer.spec.tsx +344 -0
  253. package/src/components/data-grid/footer.tsx +19 -19
  254. package/src/components/data-grid/header.spec.tsx +563 -0
  255. package/src/components/data-grid/header.tsx +53 -44
  256. package/src/components/data-grid/selection-cell.spec.tsx +150 -0
  257. package/src/components/data-grid/selection-cell.tsx +12 -6
  258. package/src/components/fab.spec.tsx +108 -0
  259. package/src/components/fab.tsx +10 -1
  260. package/src/components/form.spec.tsx +481 -0
  261. package/src/components/grid.spec.tsx +334 -0
  262. package/src/components/grid.tsx +57 -63
  263. package/src/components/inputs/autocomplete.spec.tsx +258 -0
  264. package/src/components/inputs/input.spec.tsx +808 -0
  265. package/src/components/inputs/input.tsx +153 -139
  266. package/src/components/inputs/text-area.spec.tsx +285 -0
  267. package/src/components/inputs/text-area.tsx +53 -79
  268. package/src/components/loader.spec.tsx +346 -0
  269. package/src/components/loader.tsx +1 -1
  270. package/src/components/modal.spec.tsx +304 -0
  271. package/src/components/modal.tsx +11 -9
  272. package/src/components/noty-list.spec.tsx +631 -0
  273. package/src/components/noty-list.tsx +39 -50
  274. package/src/components/paper.spec.tsx +72 -0
  275. package/src/components/paper.tsx +15 -13
  276. package/src/components/skeleton.spec.tsx +219 -0
  277. package/src/components/skeleton.tsx +1 -1
  278. package/src/components/styles.spec.ts +70 -0
  279. package/src/components/suggest/index.spec.tsx +861 -0
  280. package/src/components/suggest/index.tsx +74 -101
  281. package/src/components/suggest/suggest-input.spec.tsx +181 -0
  282. package/src/components/suggest/suggest-input.tsx +16 -24
  283. package/src/components/suggest/suggest-manager.spec.ts +409 -0
  284. package/src/components/suggest/suggestion-list.spec.tsx +334 -0
  285. package/src/components/suggest/suggestion-list.tsx +43 -48
  286. package/src/components/tabs.spec.tsx +236 -0
  287. package/src/components/tabs.tsx +33 -21
  288. package/src/components/wizard/index.spec.tsx +224 -0
  289. package/src/components/wizard/index.tsx +10 -9
  290. package/src/services/collection-service.spec.ts +492 -3
  291. package/src/services/css-variable-theme.spec.ts +204 -0
  292. package/src/services/css-variable-theme.ts +21 -1
  293. package/src/services/default-palette.ts +22 -0
  294. package/src/services/theme-provider-service.spec.ts +195 -0
  295. package/src/services/theme-provider-service.ts +60 -2
@@ -0,0 +1,481 @@
1
+ import { Injector } from '@furystack/inject'
2
+ import { createComponent, initializeShadeRoot } from '@furystack/shades'
3
+ import { sleepAsync, using } from '@furystack/utils'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+ import { Form, FormService } from './form.js'
6
+
7
+ describe('FormService', () => {
8
+ describe('initialization', () => {
9
+ it('should initialize with null validatedFormData', () => {
10
+ using(new FormService(), (service) => {
11
+ expect(service.validatedFormData.getValue()).toBeNull()
12
+ })
13
+ })
14
+
15
+ it('should initialize with null rawFormData', () => {
16
+ using(new FormService(), (service) => {
17
+ expect(service.rawFormData.getValue()).toBeNull()
18
+ })
19
+ })
20
+
21
+ it('should initialize with unknown validation result', () => {
22
+ using(new FormService(), (service) => {
23
+ expect(service.validationResult.getValue()).toEqual({ isValid: null })
24
+ })
25
+ })
26
+
27
+ it('should initialize with empty fieldErrors', () => {
28
+ using(new FormService(), (service) => {
29
+ expect(service.fieldErrors.getValue()).toEqual({})
30
+ })
31
+ })
32
+
33
+ it('should initialize with empty inputs set', () => {
34
+ using(new FormService(), (service) => {
35
+ expect(service.inputs.size).toBe(0)
36
+ })
37
+ })
38
+ })
39
+
40
+ describe('setFieldState', () => {
41
+ it('should update field errors with valid result', () => {
42
+ using(new FormService<{ email: string }>(), (service) => {
43
+ const validity = { valid: true } as ValidityState
44
+
45
+ service.setFieldState('email', { isValid: true }, validity)
46
+
47
+ expect(service.fieldErrors.getValue()).toEqual({
48
+ email: { validationResult: { isValid: true }, validity },
49
+ })
50
+ })
51
+ })
52
+
53
+ it('should update field errors with invalid result', () => {
54
+ using(new FormService<{ email: string }>(), (service) => {
55
+ const validity = { valid: false, valueMissing: true } as ValidityState
56
+ const validationResult = { isValid: false as const, message: 'Email is required' }
57
+
58
+ service.setFieldState('email', validationResult, validity)
59
+
60
+ expect(service.fieldErrors.getValue()).toEqual({
61
+ email: { validationResult, validity },
62
+ })
63
+ })
64
+ })
65
+
66
+ it('should merge field errors when updating multiple fields', () => {
67
+ using(new FormService<{ email: string; password: string }>(), (service) => {
68
+ const validity = { valid: true } as ValidityState
69
+
70
+ service.setFieldState('email', { isValid: true }, validity)
71
+ service.setFieldState('password', { isValid: true }, validity)
72
+
73
+ const errors = service.fieldErrors.getValue()
74
+ expect(errors.email).toBeDefined()
75
+ expect(errors.password).toBeDefined()
76
+ })
77
+ })
78
+ })
79
+
80
+ describe('disposal', () => {
81
+ it('should dispose all observables', () => {
82
+ const service = new FormService()
83
+
84
+ const validatedFormDataDisposeSpy = vi.spyOn(service.validatedFormData, Symbol.dispose)
85
+ const rawFormDataDisposeSpy = vi.spyOn(service.rawFormData, Symbol.dispose)
86
+ const validationResultDisposeSpy = vi.spyOn(service.validationResult, Symbol.dispose)
87
+
88
+ service[Symbol.dispose]()
89
+
90
+ expect(validatedFormDataDisposeSpy).toHaveBeenCalled()
91
+ expect(rawFormDataDisposeSpy).toHaveBeenCalled()
92
+ expect(validationResultDisposeSpy).toHaveBeenCalled()
93
+ })
94
+ })
95
+ })
96
+
97
+ describe('Form component', () => {
98
+ beforeEach(() => {
99
+ document.body.innerHTML = '<div id="root"></div>'
100
+ })
101
+
102
+ afterEach(() => {
103
+ document.body.innerHTML = ''
104
+ })
105
+
106
+ it('should render children', async () => {
107
+ const injector = new Injector()
108
+ const rootElement = document.getElementById('root') as HTMLDivElement
109
+
110
+ type FormData = { name: string }
111
+
112
+ initializeShadeRoot({
113
+ injector,
114
+ rootElement,
115
+ jsxElement: (
116
+ <Form<FormData>
117
+ onSubmit={() => {}}
118
+ validate={(data): data is FormData => {
119
+ const d = data as Record<string, unknown>
120
+ return typeof d.name === 'string'
121
+ }}
122
+ >
123
+ <input name="name" type="text" />
124
+ <button type="submit">Submit</button>
125
+ </Form>
126
+ ),
127
+ })
128
+
129
+ await sleepAsync(50)
130
+
131
+ const form = document.querySelector('form[is="shade-form"]')
132
+ expect(form).not.toBeNull()
133
+ expect(form?.querySelector('input[name="name"]')).not.toBeNull()
134
+ expect(form?.querySelector('button[type="submit"]')).not.toBeNull()
135
+ })
136
+
137
+ it('should call onSubmit with validated data when form is valid', async () => {
138
+ const injector = new Injector()
139
+ const rootElement = document.getElementById('root') as HTMLDivElement
140
+ const onSubmit = vi.fn()
141
+
142
+ type FormData = { name: string }
143
+
144
+ initializeShadeRoot({
145
+ injector,
146
+ rootElement,
147
+ jsxElement: (
148
+ <Form<FormData>
149
+ onSubmit={onSubmit}
150
+ validate={(data): data is FormData => {
151
+ const d = data as Record<string, unknown>
152
+ return typeof d.name === 'string'
153
+ }}
154
+ >
155
+ <input name="name" type="text" value="Test Name" />
156
+ <button type="submit">Submit</button>
157
+ </Form>
158
+ ),
159
+ })
160
+
161
+ await sleepAsync(50)
162
+
163
+ const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
164
+ const input = form.querySelector('input[name="name"]') as HTMLInputElement
165
+ input.value = 'Test Name'
166
+
167
+ const submitEvent = new Event('submit', { bubbles: true, cancelable: true })
168
+ form.dispatchEvent(submitEvent)
169
+
170
+ await sleepAsync(50)
171
+
172
+ expect(onSubmit).toHaveBeenCalledWith({ name: 'Test Name' })
173
+ })
174
+
175
+ it('should not call onSubmit when validation fails', async () => {
176
+ const injector = new Injector()
177
+ const rootElement = document.getElementById('root') as HTMLDivElement
178
+ const onSubmit = vi.fn()
179
+
180
+ type FormData = { name: string; email: string }
181
+
182
+ initializeShadeRoot({
183
+ injector,
184
+ rootElement,
185
+ jsxElement: (
186
+ <Form<FormData>
187
+ onSubmit={onSubmit}
188
+ validate={(data): data is FormData => {
189
+ const d = data as Record<string, unknown>
190
+ return typeof d.name === 'string' && typeof d.email === 'string' && d.email.includes('@')
191
+ }}
192
+ >
193
+ <input name="name" type="text" />
194
+ <input name="email" type="text" />
195
+ <button type="submit">Submit</button>
196
+ </Form>
197
+ ),
198
+ })
199
+
200
+ await sleepAsync(50)
201
+
202
+ const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
203
+ const nameInput = form.querySelector('input[name="name"]') as HTMLInputElement
204
+ const emailInput = form.querySelector('input[name="email"]') as HTMLInputElement
205
+
206
+ nameInput.value = 'Test'
207
+ emailInput.value = 'invalid-email'
208
+
209
+ const submitEvent = new Event('submit', { bubbles: true, cancelable: true })
210
+ form.dispatchEvent(submitEvent)
211
+
212
+ await sleepAsync(50)
213
+
214
+ expect(onSubmit).not.toHaveBeenCalled()
215
+ })
216
+
217
+ it('should set validation result to validation-failed when validate returns false', async () => {
218
+ const injector = new Injector()
219
+ const rootElement = document.getElementById('root') as HTMLDivElement
220
+
221
+ type FormData = { email: string }
222
+
223
+ initializeShadeRoot({
224
+ injector,
225
+ rootElement,
226
+ jsxElement: (
227
+ <Form<FormData>
228
+ onSubmit={() => {}}
229
+ validate={(data): data is FormData => {
230
+ const d = data as Record<string, unknown>
231
+ return typeof d.email === 'string' && d.email.includes('@')
232
+ }}
233
+ >
234
+ <input name="email" type="text" />
235
+ <button type="submit">Submit</button>
236
+ </Form>
237
+ ),
238
+ })
239
+
240
+ await sleepAsync(50)
241
+
242
+ const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
243
+ const input = form.querySelector('input[name="email"]') as HTMLInputElement
244
+ input.value = 'no-at-sign'
245
+
246
+ const changeEvent = new Event('change', { bubbles: true })
247
+ form.dispatchEvent(changeEvent)
248
+
249
+ await sleepAsync(50)
250
+
251
+ const formInjector = (form as unknown as { injector: Injector }).injector
252
+ const formService = formInjector.getInstance(FormService)
253
+
254
+ expect(formService.validationResult.getValue()).toEqual({
255
+ isValid: false,
256
+ reason: 'validation-failed',
257
+ })
258
+ })
259
+
260
+ it('should reset form state on reset event', async () => {
261
+ const injector = new Injector()
262
+ const rootElement = document.getElementById('root') as HTMLDivElement
263
+ const onReset = vi.fn()
264
+
265
+ type FormData = { name: string }
266
+
267
+ initializeShadeRoot({
268
+ injector,
269
+ rootElement,
270
+ jsxElement: (
271
+ <Form<FormData>
272
+ onSubmit={() => {}}
273
+ onReset={onReset}
274
+ validate={(data): data is FormData => {
275
+ const d = data as Record<string, unknown>
276
+ return typeof d.name === 'string'
277
+ }}
278
+ >
279
+ <input name="name" type="text" />
280
+ <button type="submit">Submit</button>
281
+ <button type="reset">Reset</button>
282
+ </Form>
283
+ ),
284
+ })
285
+
286
+ await sleepAsync(50)
287
+
288
+ const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
289
+ const input = form.querySelector('input[name="name"]') as HTMLInputElement
290
+ input.value = 'Test'
291
+
292
+ const changeEvent = new Event('change', { bubbles: true })
293
+ form.dispatchEvent(changeEvent)
294
+
295
+ await sleepAsync(50)
296
+
297
+ const formInjector = (form as unknown as { injector: Injector }).injector
298
+ const formService = formInjector.getInstance(FormService)
299
+
300
+ expect(formService.rawFormData.getValue()).toEqual({ name: 'Test' })
301
+
302
+ const resetEvent = new Event('reset', { bubbles: true })
303
+ form.dispatchEvent(resetEvent)
304
+
305
+ await sleepAsync(50)
306
+
307
+ expect(formService.rawFormData.getValue()).toBeNull()
308
+ expect(formService.validationResult.getValue()).toEqual({ isValid: null })
309
+ expect(formService.validatedFormData.getValue()).toBeNull()
310
+ })
311
+
312
+ it('should update rawFormData on change event', async () => {
313
+ const injector = new Injector()
314
+ const rootElement = document.getElementById('root') as HTMLDivElement
315
+
316
+ type FormData = { username: string }
317
+
318
+ initializeShadeRoot({
319
+ injector,
320
+ rootElement,
321
+ jsxElement: (
322
+ <Form<FormData>
323
+ onSubmit={() => {}}
324
+ validate={(data): data is FormData => {
325
+ const d = data as Record<string, unknown>
326
+ return typeof d.username === 'string'
327
+ }}
328
+ >
329
+ <input name="username" type="text" />
330
+ </Form>
331
+ ),
332
+ })
333
+
334
+ await sleepAsync(50)
335
+
336
+ const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
337
+ const input = form.querySelector('input[name="username"]') as HTMLInputElement
338
+ input.value = 'testuser'
339
+
340
+ const changeEvent = new Event('change', { bubbles: true })
341
+ form.dispatchEvent(changeEvent)
342
+
343
+ await sleepAsync(50)
344
+
345
+ const formInjector = (form as unknown as { injector: Injector }).injector
346
+ const formService = formInjector.getInstance(FormService)
347
+
348
+ expect(formService.rawFormData.getValue()).toEqual({ username: 'testuser' })
349
+ })
350
+
351
+ it('should set validatedFormData when validation passes', async () => {
352
+ const injector = new Injector()
353
+ const rootElement = document.getElementById('root') as HTMLDivElement
354
+
355
+ type FormData = { title: string }
356
+
357
+ initializeShadeRoot({
358
+ injector,
359
+ rootElement,
360
+ jsxElement: (
361
+ <Form<FormData>
362
+ onSubmit={() => {}}
363
+ validate={(data): data is FormData => {
364
+ const d = data as Record<string, unknown>
365
+ return typeof d.title === 'string'
366
+ }}
367
+ >
368
+ <input name="title" type="text" />
369
+ </Form>
370
+ ),
371
+ })
372
+
373
+ await sleepAsync(50)
374
+
375
+ const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
376
+ const input = form.querySelector('input[name="title"]') as HTMLInputElement
377
+ input.value = 'My Title'
378
+
379
+ const changeEvent = new Event('change', { bubbles: true })
380
+ form.dispatchEvent(changeEvent)
381
+
382
+ await sleepAsync(50)
383
+
384
+ const formInjector = (form as unknown as { injector: Injector }).injector
385
+ const formService = formInjector.getInstance(FormService)
386
+
387
+ expect(formService.validatedFormData.getValue()).toEqual({ title: 'My Title' })
388
+ expect(formService.validationResult.getValue()).toEqual({ isValid: true })
389
+ })
390
+
391
+ it('should prevent default on submit event', async () => {
392
+ const injector = new Injector()
393
+ const rootElement = document.getElementById('root') as HTMLDivElement
394
+
395
+ type FormData = { field: string }
396
+
397
+ initializeShadeRoot({
398
+ injector,
399
+ rootElement,
400
+ jsxElement: (
401
+ <Form<FormData> onSubmit={() => {}} validate={(_data): _data is FormData => true}>
402
+ <input name="field" type="text" />
403
+ <button type="submit">Submit</button>
404
+ </Form>
405
+ ),
406
+ })
407
+
408
+ await sleepAsync(50)
409
+
410
+ const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
411
+
412
+ const submitEvent = new Event('submit', { bubbles: true, cancelable: true })
413
+ const preventDefaultSpy = vi.spyOn(submitEvent, 'preventDefault')
414
+
415
+ form.dispatchEvent(submitEvent)
416
+
417
+ expect(preventDefaultSpy).toHaveBeenCalled()
418
+ })
419
+
420
+ it('should create child injector with FormService', async () => {
421
+ const injector = new Injector()
422
+ const rootElement = document.getElementById('root') as HTMLDivElement
423
+
424
+ type FormData = { data: string }
425
+
426
+ initializeShadeRoot({
427
+ injector,
428
+ rootElement,
429
+ jsxElement: (
430
+ <Form<FormData> onSubmit={() => {}} validate={(_data): _data is FormData => true}>
431
+ <input name="data" type="text" />
432
+ </Form>
433
+ ),
434
+ })
435
+
436
+ await sleepAsync(50)
437
+
438
+ const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
439
+ const formInjector = (form as unknown as { injector: Injector }).injector
440
+
441
+ expect(formInjector).toBeInstanceOf(Injector)
442
+ expect(formInjector).not.toBe(injector)
443
+
444
+ const formService = formInjector.getInstance(FormService)
445
+ expect(formService).toBeInstanceOf(FormService)
446
+ })
447
+
448
+ it('should handle oninvalid event and trigger validation', async () => {
449
+ const injector = new Injector()
450
+ const rootElement = document.getElementById('root') as HTMLDivElement
451
+
452
+ type FormData = { required: string }
453
+
454
+ initializeShadeRoot({
455
+ injector,
456
+ rootElement,
457
+ jsxElement: (
458
+ <Form<FormData>
459
+ onSubmit={() => {}}
460
+ validate={(data): data is FormData => {
461
+ const d = data as Record<string, unknown>
462
+ return typeof d.required === 'string' && d.required.length > 0
463
+ }}
464
+ >
465
+ <input name="required" type="text" required />
466
+ <button type="submit">Submit</button>
467
+ </Form>
468
+ ),
469
+ })
470
+
471
+ await sleepAsync(50)
472
+
473
+ const form = document.querySelector('form[is="shade-form"]') as HTMLFormElement
474
+ const input = form.querySelector('input[name="required"]') as HTMLInputElement
475
+
476
+ const invalidEvent = new Event('invalid', { bubbles: true })
477
+ input.dispatchEvent(invalidEvent)
478
+
479
+ await sleepAsync(50)
480
+ })
481
+ })