@ippon-ui/ui 0.0.2

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 (246) hide show
  1. package/.agents/agents/component-library-create-from-pattern-library.agent.md +37 -0
  2. package/.agents/agents/patten-library-create-component.agent.md +30 -0
  3. package/.agents/skills/component-library/SKILL.md +169 -0
  4. package/.agents/skills/pattern-library/SKILL.md +277 -0
  5. package/.github/workflows/build.yml +34 -0
  6. package/.gitlab-ci.yml +12 -0
  7. package/.prettierignore +6 -0
  8. package/AGENTS.md +50 -0
  9. package/LICENSE +202 -0
  10. package/ci/build.yml +15 -0
  11. package/ci/common.yml +42 -0
  12. package/ci/deploy.yml +20 -0
  13. package/icons/LICENCE +202 -0
  14. package/icons/index.ts +69 -0
  15. package/icons/package.json +25 -0
  16. package/icons/tsconfig.json +11 -0
  17. package/lefthook.yml +10 -0
  18. package/mise.toml +55 -0
  19. package/package.json +26 -0
  20. package/pnpm-workspace.yaml +9 -0
  21. package/prettier.config.mts +8 -0
  22. package/react/LICENCE +202 -0
  23. package/react/README.md +75 -0
  24. package/react/eslint.config.js +22 -0
  25. package/react/package.json +63 -0
  26. package/react/src/CAP.ts +14 -0
  27. package/react/src/Card.ts +2 -0
  28. package/react/src/DataSelectable.ts +7 -0
  29. package/react/src/Grid.ts +33 -0
  30. package/react/src/IpponBadge.tsx +62 -0
  31. package/react/src/IpponButton.tsx +93 -0
  32. package/react/src/IpponButtonCard.tsx +34 -0
  33. package/react/src/IpponCard.tsx +30 -0
  34. package/react/src/IpponContainer.tsx +15 -0
  35. package/react/src/IpponGrid.tsx +56 -0
  36. package/react/src/IpponHSpace.tsx +56 -0
  37. package/react/src/IpponIcon.tsx +15 -0
  38. package/react/src/IpponImportFile.tsx +128 -0
  39. package/react/src/IpponIon.tsx +45 -0
  40. package/react/src/IpponMeter.tsx +43 -0
  41. package/react/src/IpponProgress.tsx +45 -0
  42. package/react/src/IpponText.tsx +56 -0
  43. package/react/src/IpponTitle.tsx +45 -0
  44. package/react/src/IpponVSpace.tsx +43 -0
  45. package/react/src/Optional.ts +177 -0
  46. package/react/src/Tokens.ts +36 -0
  47. package/react/src/index.ts +16 -0
  48. package/react/test/File.fixture.ts +13 -0
  49. package/react/test/IpponBadge.spec.tsx +245 -0
  50. package/react/test/IpponButton.spec.tsx +666 -0
  51. package/react/test/IpponButtonCard.spec.tsx +162 -0
  52. package/react/test/IpponCard.spec.tsx +133 -0
  53. package/react/test/IpponContainer.spec.tsx +56 -0
  54. package/react/test/IpponGrid.spec.tsx +140 -0
  55. package/react/test/IpponHSpace.spec.tsx +107 -0
  56. package/react/test/IpponIcon.spec.tsx +37 -0
  57. package/react/test/IpponImportFile.spec.tsx +431 -0
  58. package/react/test/IpponIon.spec.tsx +52 -0
  59. package/react/test/IpponMeter.spec.tsx +59 -0
  60. package/react/test/IpponProgress.spec.tsx +68 -0
  61. package/react/test/IpponText.spec.tsx +149 -0
  62. package/react/test/IpponTitle.spec.tsx +242 -0
  63. package/react/test/IpponVSpace.spec.tsx +91 -0
  64. package/react/tsconfig.app.json +24 -0
  65. package/react/tsconfig.json +4 -0
  66. package/react/tsconfig.node.json +23 -0
  67. package/react/vite.config.ts +30 -0
  68. package/react/vitest.config.ts +21 -0
  69. package/styles/.editorconfig +12 -0
  70. package/styles/.stylelintrc.json +75 -0
  71. package/styles/LICENCE +202 -0
  72. package/styles/README.md +107 -0
  73. package/styles/logo.svg +26 -0
  74. package/styles/package.json +67 -0
  75. package/styles/src/atom/_atom.scss +9 -0
  76. package/styles/src/atom/atom.pug +34 -0
  77. package/styles/src/atom/badge/_badge.scss +108 -0
  78. package/styles/src/atom/badge/badge.code.pug +29 -0
  79. package/styles/src/atom/badge/badge.md +1 -0
  80. package/styles/src/atom/badge/badge.mixin.pug +24 -0
  81. package/styles/src/atom/badge/badge.render.pug +7 -0
  82. package/styles/src/atom/button/_button.scss +242 -0
  83. package/styles/src/atom/button/button.code.pug +38 -0
  84. package/styles/src/atom/button/button.md +31 -0
  85. package/styles/src/atom/button/button.mixin.pug +30 -0
  86. package/styles/src/atom/button/button.render.pug +13 -0
  87. package/styles/src/atom/icon/_icon.scss +8 -0
  88. package/styles/src/atom/icon/icon.code.pug +5 -0
  89. package/styles/src/atom/icon/icon.md +11 -0
  90. package/styles/src/atom/icon/icon.mixin.pug +8 -0
  91. package/styles/src/atom/icon/icon.render.pug +7 -0
  92. package/styles/src/atom/ion/ion.code.pug +8 -0
  93. package/styles/src/atom/ion/ion.md +11 -0
  94. package/styles/src/atom/ion/ion.mixin.pug +8 -0
  95. package/styles/src/atom/ion/ion.render.pug +7 -0
  96. package/styles/src/atom/meter/_meter.scss +23 -0
  97. package/styles/src/atom/meter/meter.code.pug +8 -0
  98. package/styles/src/atom/meter/meter.md +7 -0
  99. package/styles/src/atom/meter/meter.mixin.pug +12 -0
  100. package/styles/src/atom/meter/meter.render.pug +5 -0
  101. package/styles/src/atom/progress/_progress.scss +23 -0
  102. package/styles/src/atom/progress/progress.code.pug +8 -0
  103. package/styles/src/atom/progress/progress.md +7 -0
  104. package/styles/src/atom/progress/progress.mixin.pug +14 -0
  105. package/styles/src/atom/progress/progress.render.pug +5 -0
  106. package/styles/src/atom/tab/_tab.scss +48 -0
  107. package/styles/src/atom/tab/tab.code.pug +5 -0
  108. package/styles/src/atom/tab/tab.md +1 -0
  109. package/styles/src/atom/tab/tab.mixin.pug +14 -0
  110. package/styles/src/atom/tab/tab.render.pug +4 -0
  111. package/styles/src/atom/text/_text.scss +74 -0
  112. package/styles/src/atom/text/text.code.pug +19 -0
  113. package/styles/src/atom/text/text.md +5 -0
  114. package/styles/src/atom/text/text.mixin.pug +12 -0
  115. package/styles/src/atom/text/text.render.pug +7 -0
  116. package/styles/src/atom/title/_title.scss +68 -0
  117. package/styles/src/atom/title/title.code.pug +25 -0
  118. package/styles/src/atom/title/title.md +9 -0
  119. package/styles/src/atom/title/title.mixin.pug +12 -0
  120. package/styles/src/atom/title/title.render.pug +5 -0
  121. package/styles/src/atom/title-display/_title-display.scss +26 -0
  122. package/styles/src/atom/title-display/title-display.code.pug +9 -0
  123. package/styles/src/atom/title-display/title-display.md +5 -0
  124. package/styles/src/atom/title-display/title-display.mixin.pug +6 -0
  125. package/styles/src/atom/title-display/title-display.render.pug +4 -0
  126. package/styles/src/doc.scss +2 -0
  127. package/styles/src/favicon.ico +0 -0
  128. package/styles/src/function/_conversion.scss +9 -0
  129. package/styles/src/index.pug +59 -0
  130. package/styles/src/layout-documentation.pug +14 -0
  131. package/styles/src/layout.pug +17 -0
  132. package/styles/src/molecule/_molecule.scss +2 -0
  133. package/styles/src/molecule/import-file/_import-file.scss +38 -0
  134. package/styles/src/molecule/import-file/import-file.code.pug +4 -0
  135. package/styles/src/molecule/import-file/import-file.md +1 -0
  136. package/styles/src/molecule/import-file/import-file.mixin.pug +15 -0
  137. package/styles/src/molecule/import-file/import-file.render.pug +5 -0
  138. package/styles/src/molecule/molecule.pug +20 -0
  139. package/styles/src/molecule/tabs/_tabs.scss +4 -0
  140. package/styles/src/molecule/tabs/tabs.code.pug +9 -0
  141. package/styles/src/molecule/tabs/tabs.md +1 -0
  142. package/styles/src/molecule/tabs/tabs.mixin.pug +4 -0
  143. package/styles/src/molecule/tabs/tabs.render.pug +4 -0
  144. package/styles/src/molecule/toggle/_toggle.scss +68 -0
  145. package/styles/src/molecule/toggle/toggle.code.pug +26 -0
  146. package/styles/src/molecule/toggle/toggle.md +1 -0
  147. package/styles/src/molecule/toggle/toggle.mixin.pug +36 -0
  148. package/styles/src/molecule/toggle/toggle.render.pug +5 -0
  149. package/styles/src/organism/_abstract-card.scss +36 -0
  150. package/styles/src/organism/_docorganism.scss +1 -0
  151. package/styles/src/organism/_organism.scss +8 -0
  152. package/styles/src/organism/button-card/_button-card.scss +22 -0
  153. package/styles/src/organism/button-card/button-card.code.pug +31 -0
  154. package/styles/src/organism/button-card/button-card.md +28 -0
  155. package/styles/src/organism/button-card/button-card.mixin.pug +8 -0
  156. package/styles/src/organism/button-card/button-card.render.pug +7 -0
  157. package/styles/src/organism/card/_card.scss +6 -0
  158. package/styles/src/organism/card/card.code.pug +9 -0
  159. package/styles/src/organism/card/card.md +23 -0
  160. package/styles/src/organism/card/card.mixin.pug +7 -0
  161. package/styles/src/organism/card/card.render.pug +7 -0
  162. package/styles/src/organism/container/_container.scss +3 -0
  163. package/styles/src/organism/container/container.code.pug +13 -0
  164. package/styles/src/organism/container/container.md +5 -0
  165. package/styles/src/organism/container/container.mixin.pug +3 -0
  166. package/styles/src/organism/container/container.render.pug +4 -0
  167. package/styles/src/organism/grid/_docgrid.scss +11 -0
  168. package/styles/src/organism/grid/_grid.scss +84 -0
  169. package/styles/src/organism/grid/grid.code.pug +25 -0
  170. package/styles/src/organism/grid/grid.md +1 -0
  171. package/styles/src/organism/grid/grid.mixin.pug +7 -0
  172. package/styles/src/organism/grid/grid.render.pug +5 -0
  173. package/styles/src/organism/h-space/_h-space.scss +49 -0
  174. package/styles/src/organism/h-space/h-space.code.pug +56 -0
  175. package/styles/src/organism/h-space/h-space.md +22 -0
  176. package/styles/src/organism/h-space/h-space.mixin.pug +14 -0
  177. package/styles/src/organism/h-space/h-space.render.pug +5 -0
  178. package/styles/src/organism/header/_header.scss +8 -0
  179. package/styles/src/organism/header/header.code.pug +14 -0
  180. package/styles/src/organism/header/header.md +1 -0
  181. package/styles/src/organism/header/header.mixin.pug +7 -0
  182. package/styles/src/organism/header/header.render.pug +4 -0
  183. package/styles/src/organism/modal/_modal.scss +58 -0
  184. package/styles/src/organism/modal/modal.code.pug +68 -0
  185. package/styles/src/organism/modal/modal.md +1 -0
  186. package/styles/src/organism/modal/modal.mixin.pug +25 -0
  187. package/styles/src/organism/modal/modal.render.pug +4 -0
  188. package/styles/src/organism/organism.pug +30 -0
  189. package/styles/src/organism/v-space/_v-space.scss +45 -0
  190. package/styles/src/organism/v-space/v-space.code.pug +41 -0
  191. package/styles/src/organism/v-space/v-space.md +20 -0
  192. package/styles/src/organism/v-space/v-space.mixin.pug +7 -0
  193. package/styles/src/organism/v-space/v-space.render.pug +5 -0
  194. package/styles/src/quark/_breakpoint.scss +12 -0
  195. package/styles/src/quark/_font.scss +38 -0
  196. package/styles/src/quark/_gap.scss +34 -0
  197. package/styles/src/quark/_placeholder.scss +27 -0
  198. package/styles/src/quark/_shadow.scss +13 -0
  199. package/styles/src/quark/_typography.scss +146 -0
  200. package/styles/src/template/_template.scss +1 -0
  201. package/styles/src/template/layout/_layout.scss +20 -0
  202. package/styles/src/template/layout/layout.code.pug +11 -0
  203. package/styles/src/template/layout/layout.md +1 -0
  204. package/styles/src/template/layout/layout.mixin.pug +11 -0
  205. package/styles/src/template/layout/layout.render.pug +4 -0
  206. package/styles/src/template/template.pug +16 -0
  207. package/styles/src/tikui.scss +5 -0
  208. package/styles/src/token/_doctable.scss +14 -0
  209. package/styles/src/token/_doctoken.scss +1 -0
  210. package/styles/src/token/_size.scss +9 -0
  211. package/styles/src/token/_token.scss +5 -0
  212. package/styles/src/token/color/_color.scss +9 -0
  213. package/styles/src/token/color/color/_base.scss +65 -0
  214. package/styles/src/token/color/color/_brand.scss +13 -0
  215. package/styles/src/token/color/color/_error.scss +13 -0
  216. package/styles/src/token/color/color/_information-2.scss +13 -0
  217. package/styles/src/token/color/color/_information.scss +13 -0
  218. package/styles/src/token/color/color/_neutral.scss +20 -0
  219. package/styles/src/token/color/color/_semantic.scss +69 -0
  220. package/styles/src/token/color/color/_success.scss +13 -0
  221. package/styles/src/token/color/color/_warning.scss +13 -0
  222. package/styles/src/token/color/color.js +31 -0
  223. package/styles/src/token/color/color.mixin.pug +19 -0
  224. package/styles/src/token/color/color.pug +9 -0
  225. package/styles/src/token/color/color.render.pug +13 -0
  226. package/styles/src/token/radius/_radius.scss +8 -0
  227. package/styles/src/token/radius/radius.js +54 -0
  228. package/styles/src/token/radius/radius.mixin.pug +14 -0
  229. package/styles/src/token/radius/radius.pug +9 -0
  230. package/styles/src/token/radius/radius.render.pug +11 -0
  231. package/styles/src/token/shadow/_shadow.scss +22 -0
  232. package/styles/src/token/shadow/shadow.js +45 -0
  233. package/styles/src/token/shadow/shadow.mixin.pug +13 -0
  234. package/styles/src/token/shadow/shadow.pug +9 -0
  235. package/styles/src/token/shadow/shadow.render.pug +9 -0
  236. package/styles/src/token/token.js +38 -0
  237. package/styles/src/token/token.pug +25 -0
  238. package/styles/src/token/typography/_typography.scss +103 -0
  239. package/styles/src/token/typography/typography.js +32 -0
  240. package/styles/src/token/typography/typography.mixin.pug +17 -0
  241. package/styles/src/token/typography/typography.pug +9 -0
  242. package/styles/src/token/typography/typography.render.pug +19 -0
  243. package/styles/test/function/conversion.test.scss +20 -0
  244. package/styles/test/function/sass.spec.ts +6 -0
  245. package/styles/tikuiconfig.json +14 -0
  246. package/styles/tsconfig.json +10 -0
@@ -0,0 +1,431 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { render, screen, configure, cleanup, fireEvent } from '@testing-library/react';
3
+ import '@testing-library/jest-dom/vitest';
4
+ import { IpponImportFile } from '../src';
5
+ import { fakeTextFile } from './File.fixture.ts';
6
+
7
+ configure({
8
+ testIdAttribute: 'data-selector',
9
+ });
10
+
11
+ const getIpponImportFile = () => screen.getByTestId('ippon-import-file');
12
+
13
+ const expectToHaveClasses = (...classes: string[]) =>
14
+ expect(getIpponImportFile()).toHaveClass('ippon-import-file', ...classes);
15
+
16
+ const getFileInput = () =>
17
+ getIpponImportFile().querySelector('input[type="file"]') as HTMLInputElement;
18
+
19
+ describe('IpponImportFile', () => {
20
+ beforeEach(cleanup);
21
+
22
+ it('should be minimal', () => {
23
+ render(
24
+ <IpponImportFile
25
+ title="Upload files"
26
+ description="Click to browse"
27
+ dataSelector="ippon-import-file"
28
+ />,
29
+ );
30
+
31
+ expectToHaveClasses();
32
+ expect(getFileInput()).toBeInTheDocument();
33
+ });
34
+
35
+ describe('dragover state', () => {
36
+ it('should have dragover class when dragenter event fires', () => {
37
+ render(
38
+ <IpponImportFile
39
+ title="Upload files"
40
+ description="Click to browse"
41
+ dataSelector="ippon-import-file"
42
+ />,
43
+ );
44
+
45
+ fireEvent.dragEnter(getIpponImportFile());
46
+
47
+ expectToHaveClasses('-dragover');
48
+ });
49
+
50
+ it('should not have dragover class when dragleave event fires and counter reaches 0', () => {
51
+ render(
52
+ <IpponImportFile
53
+ title="Upload files"
54
+ description="Click to browse"
55
+ dataSelector="ippon-import-file"
56
+ />,
57
+ );
58
+
59
+ fireEvent.dragEnter(getIpponImportFile());
60
+ expectToHaveClasses('-dragover');
61
+
62
+ fireEvent.dragLeave(getIpponImportFile());
63
+ expect(getIpponImportFile()).not.toHaveClass('-dragover');
64
+ });
65
+
66
+ it('should not have dragover class after drop event', () => {
67
+ render(
68
+ <IpponImportFile
69
+ title="Upload files"
70
+ description="Click to browse"
71
+ dataSelector="ippon-import-file"
72
+ />,
73
+ );
74
+
75
+ fireEvent.dragEnter(getIpponImportFile());
76
+ expectToHaveClasses('-dragover');
77
+
78
+ fireEvent.drop(getIpponImportFile());
79
+ expect(getIpponImportFile()).not.toHaveClass('-dragover');
80
+ });
81
+
82
+ it('should maintain dragover class with multiple dragenter events', () => {
83
+ render(
84
+ <IpponImportFile
85
+ title="Upload files"
86
+ description="Click to browse"
87
+ dataSelector="ippon-import-file"
88
+ />,
89
+ );
90
+
91
+ fireEvent.dragEnter(getIpponImportFile());
92
+ fireEvent.dragEnter(getIpponImportFile());
93
+ expectToHaveClasses('-dragover');
94
+
95
+ fireEvent.dragLeave(getIpponImportFile());
96
+ expectToHaveClasses('-dragover');
97
+
98
+ fireEvent.dragLeave(getIpponImportFile());
99
+ expect(getIpponImportFile()).not.toHaveClass('-dragover');
100
+ });
101
+
102
+ it('should not throw on dragover event', () => {
103
+ render(
104
+ <IpponImportFile
105
+ title="Upload files"
106
+ description="Click to browse"
107
+ dataSelector="ippon-import-file"
108
+ />,
109
+ );
110
+
111
+ expect(() => {
112
+ fireEvent.dragOver(getIpponImportFile());
113
+ }).not.toThrow();
114
+ });
115
+ });
116
+
117
+ describe('file input', () => {
118
+ it('should render hidden file input', () => {
119
+ render(
120
+ <IpponImportFile
121
+ title="Upload files"
122
+ description="Click to browse"
123
+ dataSelector="ippon-import-file"
124
+ />,
125
+ );
126
+
127
+ const input = getFileInput();
128
+ expect(input).toBeInTheDocument();
129
+ expect(input).toHaveAttribute('type', 'file');
130
+ });
131
+
132
+ it('should accept multiple files when multiple is true', () => {
133
+ render(
134
+ <IpponImportFile
135
+ multiple={true}
136
+ title="Upload files"
137
+ description="Click to browse"
138
+ dataSelector="ippon-import-file"
139
+ />,
140
+ );
141
+
142
+ expect(getFileInput()).toHaveAttribute('multiple');
143
+ });
144
+
145
+ it('should accept specific file types when accept is provided', () => {
146
+ render(
147
+ <IpponImportFile
148
+ accept=".pdf,.doc"
149
+ title="Upload files"
150
+ description="Click to browse"
151
+ dataSelector="ippon-import-file"
152
+ />,
153
+ );
154
+
155
+ expect(getFileInput()).toHaveAttribute('accept', '.pdf,.doc');
156
+ });
157
+
158
+ it('should have data-selector on input with .input suffix', () => {
159
+ render(
160
+ <IpponImportFile
161
+ title="Upload files"
162
+ description="Click to browse"
163
+ dataSelector="ippon-import-file"
164
+ />,
165
+ );
166
+
167
+ expect(getFileInput()).toHaveAttribute('data-selector', 'ippon-import-file.input');
168
+ });
169
+
170
+ it('should not have data-selector on input when dataSelector is not provided', () => {
171
+ render(<IpponImportFile title="Upload files" description="Click to browse" />);
172
+
173
+ const input = screen
174
+ .getByRole('presentation')
175
+ .closest('label')
176
+ ?.querySelector('input[type="file"]');
177
+ expect(input).not.toHaveAttribute('data-selector');
178
+ });
179
+ });
180
+
181
+ describe('icon and text content', () => {
182
+ it('should render icon with cloud-upload name', () => {
183
+ render(
184
+ <IpponImportFile
185
+ title="Upload files"
186
+ description="Click to browse"
187
+ dataSelector="ippon-import-file"
188
+ />,
189
+ );
190
+
191
+ const icon = getIpponImportFile().querySelector('.ippon-icon');
192
+ expect(icon).toBeInTheDocument();
193
+ });
194
+
195
+ it('should render default text', () => {
196
+ render(
197
+ <IpponImportFile
198
+ title="Drag and drop your files here"
199
+ description="Or click to browse"
200
+ dataSelector="ippon-import-file"
201
+ />,
202
+ );
203
+
204
+ expect(getIpponImportFile()).toHaveTextContent('Drag and drop your files here');
205
+ expect(getIpponImportFile()).toHaveTextContent('Or click to browse');
206
+ });
207
+
208
+ it('should render custom title', () => {
209
+ render(
210
+ <IpponImportFile
211
+ title="Custom title text"
212
+ description="Click to browse"
213
+ dataSelector="ippon-import-file"
214
+ />,
215
+ );
216
+
217
+ expect(getIpponImportFile()).toHaveTextContent('Custom title text');
218
+ expect(getIpponImportFile()).toHaveTextContent('Click to browse');
219
+ });
220
+
221
+ it('should render custom description', () => {
222
+ render(
223
+ <IpponImportFile
224
+ title="Upload files"
225
+ description="Custom description text"
226
+ dataSelector="ippon-import-file"
227
+ />,
228
+ );
229
+
230
+ expect(getIpponImportFile()).toHaveTextContent('Upload files');
231
+ expect(getIpponImportFile()).toHaveTextContent('Custom description text');
232
+ });
233
+
234
+ it('should render both custom title and description', () => {
235
+ render(
236
+ <IpponImportFile
237
+ title="Custom title"
238
+ description="Custom description"
239
+ dataSelector="ippon-import-file"
240
+ />,
241
+ );
242
+
243
+ expect(getIpponImportFile()).toHaveTextContent('Custom title');
244
+ expect(getIpponImportFile()).toHaveTextContent('Custom description');
245
+ });
246
+ });
247
+
248
+ describe('onChange', () => {
249
+ it('should call onChange with single File in single mode', () => {
250
+ const onChange = vi.fn();
251
+ render(
252
+ <IpponImportFile
253
+ title="Upload file"
254
+ description="Click to browse"
255
+ onChange={onChange}
256
+ dataSelector="ippon-import-file"
257
+ />,
258
+ );
259
+
260
+ const input = getFileInput();
261
+ const file = fakeTextFile({ content: 'content', name: 'test.txt', type: 'text/plain' });
262
+
263
+ fireEvent.change(input, { target: { files: [file] } });
264
+
265
+ expect(onChange).toHaveBeenCalledOnce();
266
+ expect(onChange).toHaveBeenCalledWith(file);
267
+ });
268
+
269
+ it('should call onChange with File[] in multiple mode', () => {
270
+ const onChange = vi.fn();
271
+ render(
272
+ <IpponImportFile
273
+ title="Upload files"
274
+ description="Click to browse"
275
+ multiple={true}
276
+ onChange={onChange}
277
+ dataSelector="ippon-import-file"
278
+ />,
279
+ );
280
+
281
+ const input = getFileInput();
282
+ const file1 = fakeTextFile({ content: 'content1', name: 'test1.txt', type: 'text/plain' });
283
+ const file2 = fakeTextFile({ content: 'content2', name: 'test2.txt', type: 'text/plain' });
284
+
285
+ fireEvent.change(input, { target: { files: [file1, file2] } });
286
+
287
+ expect(onChange).toHaveBeenCalledOnce();
288
+ expect(onChange).toHaveBeenCalledWith([file1, file2]);
289
+ });
290
+
291
+ it('should not call onChange when files list is empty', () => {
292
+ const onChange = vi.fn();
293
+ render(
294
+ <IpponImportFile
295
+ title="Upload file"
296
+ description="Click to browse"
297
+ onChange={onChange}
298
+ dataSelector="ippon-import-file"
299
+ />,
300
+ );
301
+
302
+ const input = getFileInput();
303
+
304
+ fireEvent.change(input, { target: { files: [] } });
305
+
306
+ expect(onChange).not.toHaveBeenCalled();
307
+ });
308
+
309
+ it('should not throw error when onChange is not provided', () => {
310
+ render(
311
+ <IpponImportFile
312
+ title="Upload file"
313
+ description="Click to browse"
314
+ dataSelector="ippon-import-file"
315
+ />,
316
+ );
317
+
318
+ const input = getFileInput();
319
+ const file = fakeTextFile({ content: 'content', name: 'test.txt', type: 'text/plain' });
320
+
321
+ expect(() => {
322
+ fireEvent.change(input, { target: { files: [file] } });
323
+ }).not.toThrow();
324
+ });
325
+
326
+ it('should call onChange with single File when a file is dropped in single mode', () => {
327
+ const onChange = vi.fn();
328
+ render(
329
+ <IpponImportFile
330
+ title="Upload file"
331
+ description="Click to browse"
332
+ onChange={onChange}
333
+ dataSelector="ippon-import-file"
334
+ />,
335
+ );
336
+
337
+ const file = fakeTextFile({ content: 'content', name: 'test.txt', type: 'text/plain' });
338
+
339
+ fireEvent.drop(getIpponImportFile(), { dataTransfer: { files: [file] } });
340
+
341
+ expect(onChange).toHaveBeenCalledOnce();
342
+ expect(onChange).toHaveBeenCalledWith(file);
343
+ });
344
+
345
+ it('should call onChange with File[] when files are dropped in multiple mode', () => {
346
+ const onChange = vi.fn();
347
+ render(
348
+ <IpponImportFile
349
+ title="Upload files"
350
+ description="Click to browse"
351
+ multiple={true}
352
+ onChange={onChange}
353
+ dataSelector="ippon-import-file"
354
+ />,
355
+ );
356
+
357
+ const file1 = fakeTextFile({ content: 'content1', name: 'test1.txt', type: 'text/plain' });
358
+ const file2 = fakeTextFile({ content: 'content2', name: 'test2.txt', type: 'text/plain' });
359
+
360
+ fireEvent.drop(getIpponImportFile(), { dataTransfer: { files: [file1, file2] } });
361
+
362
+ expect(onChange).toHaveBeenCalledOnce();
363
+ expect(onChange).toHaveBeenCalledWith([file1, file2]);
364
+ });
365
+
366
+ it('should not call onChange when no files are dropped', () => {
367
+ const onChange = vi.fn();
368
+ render(
369
+ <IpponImportFile
370
+ title="Upload file"
371
+ description="Click to browse"
372
+ onChange={onChange}
373
+ dataSelector="ippon-import-file"
374
+ />,
375
+ );
376
+
377
+ fireEvent.drop(getIpponImportFile(), { dataTransfer: { files: [] } });
378
+
379
+ expect(onChange).not.toHaveBeenCalled();
380
+ });
381
+
382
+ it('should call onChange twice when selecting the same file consecutively via input', () => {
383
+ const onChange = vi.fn();
384
+ render(
385
+ <IpponImportFile
386
+ title="Upload file"
387
+ description="Click to browse"
388
+ onChange={onChange}
389
+ dataSelector="ippon-import-file"
390
+ />,
391
+ );
392
+
393
+ const input = getFileInput();
394
+ const file = fakeTextFile({ content: 'content', name: 'test.txt', type: 'text/plain' });
395
+
396
+ // First selection
397
+ fireEvent.change(input, { target: { files: [file] } });
398
+ expect(onChange).toHaveBeenCalledOnce();
399
+ expect(onChange).toHaveBeenCalledWith(file);
400
+
401
+ // Second selection of the same file
402
+ fireEvent.change(input, { target: { files: [file] } });
403
+ expect(onChange).toHaveBeenCalledTimes(2);
404
+ expect(onChange).toHaveBeenLastCalledWith(file);
405
+ });
406
+
407
+ it('should call onChange twice when dropping the same file consecutively via drag & drop', () => {
408
+ const onChange = vi.fn();
409
+ render(
410
+ <IpponImportFile
411
+ title="Upload file"
412
+ description="Click to browse"
413
+ onChange={onChange}
414
+ dataSelector="ippon-import-file"
415
+ />,
416
+ );
417
+
418
+ const file = fakeTextFile({ content: 'content', name: 'test.txt', type: 'text/plain' });
419
+
420
+ // First drop
421
+ fireEvent.drop(getIpponImportFile(), { dataTransfer: { files: [file] } });
422
+ expect(onChange).toHaveBeenCalledOnce();
423
+ expect(onChange).toHaveBeenCalledWith(file);
424
+
425
+ // Second drop of the same file
426
+ fireEvent.drop(getIpponImportFile(), { dataTransfer: { files: [file] } });
427
+ expect(onChange).toHaveBeenCalledTimes(2);
428
+ expect(onChange).toHaveBeenLastCalledWith(file);
429
+ });
430
+ });
431
+ });
@@ -0,0 +1,52 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { cleanup, configure, render, screen } from '@testing-library/react';
3
+ import '@testing-library/jest-dom/vitest';
4
+ import { IpponIon } from '../src';
5
+
6
+ configure({
7
+ testIdAttribute: 'data-selector',
8
+ });
9
+
10
+ describe('IpponIon', () => {
11
+ beforeEach(cleanup);
12
+
13
+ it('should be minimal', () => {
14
+ render(<IpponIon name="home" dataSelector="ippon-icon" />);
15
+
16
+ const ipponIcon = screen.getByTestId('ippon-icon');
17
+
18
+ expect(ipponIcon).toHaveRole('presentation');
19
+ });
20
+
21
+ it('should be filled', () => {
22
+ render(<IpponIon name="home" dataSelector="ippon-icon" />);
23
+
24
+ const ipponIcon = screen.getByTestId('ippon-icon');
25
+
26
+ expect(ipponIcon).toHaveClass('ippon-ion-home');
27
+ });
28
+
29
+ it('should be sharp', () => {
30
+ render(<IpponIon name="home" variant="sharp" dataSelector="ippon-icon" />);
31
+
32
+ const ipponIcon = screen.getByTestId('ippon-icon');
33
+
34
+ expect(ipponIcon).toHaveClass('ippon-ion-home-sharp');
35
+ });
36
+
37
+ it('should be outlined', () => {
38
+ render(<IpponIon name="home" variant="outline" dataSelector="ippon-icon" />);
39
+
40
+ const ipponIcon = screen.getByTestId('ippon-icon');
41
+
42
+ expect(ipponIcon).toHaveClass('ippon-ion-home-outline');
43
+ });
44
+
45
+ it('should be a logo', () => {
46
+ render(<IpponIon name="logo-ionic" dataSelector="ippon-icon" />);
47
+
48
+ const ipponIcon = screen.getByTestId('ippon-icon');
49
+
50
+ expect(ipponIcon).toHaveClass('ippon-ion-logo-ionic');
51
+ });
52
+ });
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { IpponMeter } from '../src';
3
+ import { render, screen, configure, cleanup } from '@testing-library/react';
4
+ import '@testing-library/jest-dom/vitest';
5
+
6
+ configure({
7
+ testIdAttribute: 'data-selector',
8
+ });
9
+
10
+ const expectMeterToHave = ({
11
+ value,
12
+ min,
13
+ max,
14
+ percentage,
15
+ label,
16
+ }: {
17
+ value: string;
18
+ min: string;
19
+ max: string;
20
+ percentage: string;
21
+ label: string;
22
+ }) => {
23
+ const ipponMeter = screen.getByTestId('ippon-meter');
24
+
25
+ expect(ipponMeter).toHaveAttribute('aria-label', label);
26
+ expect(ipponMeter).toHaveClass('ippon-meter');
27
+ expect(ipponMeter).toHaveAttribute('aria-valuenow', value);
28
+ expect(ipponMeter).toHaveAttribute('aria-valuemin', min);
29
+ expect(ipponMeter).toHaveAttribute('aria-valuemax', max);
30
+ expect(ipponMeter).toHaveStyle(`--ippon-meter-percentage: ${percentage}`);
31
+ };
32
+
33
+ describe('IpponMeter', () => {
34
+ afterEach(cleanup);
35
+
36
+ it('Should have 0 everywhere with min, max and value at 0', () => {
37
+ render(<IpponMeter value={0} min={0} max={0} label="Meter" dataSelector="ippon-meter" />);
38
+
39
+ expectMeterToHave({
40
+ label: 'Meter',
41
+ value: '0',
42
+ min: '0',
43
+ max: '0',
44
+ percentage: '0%',
45
+ });
46
+ });
47
+
48
+ it('should have 50% for 11 on 22', () => {
49
+ render(<IpponMeter value={11} min={0} max={22} label="Meter" dataSelector="ippon-meter" />);
50
+
51
+ expectMeterToHave({
52
+ label: 'Meter',
53
+ value: '11',
54
+ min: '0',
55
+ max: '22',
56
+ percentage: '50%',
57
+ });
58
+ });
59
+ });
@@ -0,0 +1,68 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { IpponProgress } from '../src';
3
+ import { render, screen, configure, cleanup } from '@testing-library/react';
4
+ import '@testing-library/jest-dom/vitest';
5
+
6
+ configure({
7
+ testIdAttribute: 'data-selector',
8
+ });
9
+
10
+ const expectProgressToHave = ({
11
+ value,
12
+ min,
13
+ max,
14
+ percentage,
15
+ label,
16
+ busy,
17
+ }: {
18
+ value: string;
19
+ min: string;
20
+ max: string;
21
+ percentage: string;
22
+ label: string;
23
+ busy: string;
24
+ }) => {
25
+ const ipponProgress = screen.getByTestId('ippon-progress');
26
+
27
+ expect(ipponProgress).toHaveAttribute('aria-label', label);
28
+ expect(ipponProgress).toHaveClass('ippon-progress');
29
+ expect(ipponProgress).toHaveAttribute('role', 'progressbar');
30
+ expect(ipponProgress).toHaveAttribute('aria-valuenow', value);
31
+ expect(ipponProgress).toHaveAttribute('aria-valuemin', min);
32
+ expect(ipponProgress).toHaveAttribute('aria-valuemax', max);
33
+ expect(ipponProgress).toHaveAttribute('aria-busy', busy);
34
+ expect(ipponProgress).toHaveStyle(`--ippon-progress-percentage: ${percentage}`);
35
+ };
36
+
37
+ describe('IpponProgress', () => {
38
+ afterEach(cleanup);
39
+
40
+ it.each([
41
+ { value: 0, min: 0, max: 0, expectedPercentage: '0%', expectedBusy: 'false' },
42
+ { value: 11, min: 0, max: 22, expectedPercentage: '50%', expectedBusy: 'true' },
43
+ { value: 22, min: 0, max: 22, expectedPercentage: '100%', expectedBusy: 'false' },
44
+ { value: 30, min: 10, max: 50, expectedPercentage: '50%', expectedBusy: 'true' },
45
+ ])(
46
+ 'should have $expectedPercentage for value $value with min $min and max $max',
47
+ ({ value, min, max, expectedPercentage, expectedBusy }) => {
48
+ render(
49
+ <IpponProgress
50
+ value={value}
51
+ min={min}
52
+ max={max}
53
+ label="Progress"
54
+ dataSelector="ippon-progress"
55
+ />,
56
+ );
57
+
58
+ expectProgressToHave({
59
+ label: 'Progress',
60
+ value: String(value),
61
+ min: String(min),
62
+ max: String(max),
63
+ percentage: expectedPercentage,
64
+ busy: expectedBusy,
65
+ });
66
+ },
67
+ );
68
+ });