ywana-core8 0.1.75 → 0.1.77

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 (122) hide show
  1. package/ACCORDION_EVALUATION.md +583 -0
  2. package/CHECKBOX_EVALUATION.md +273 -0
  3. package/CHIP_EVALUATION.md +542 -0
  4. package/COLOR_EVALUATION.md +524 -0
  5. package/COMPONENTS_EVALUATION.md +477 -0
  6. package/FORM_EVALUATION.md +459 -0
  7. package/HEADER_EVALUATION.md +436 -0
  8. package/ICON_EVALUATION.md +254 -0
  9. package/LIST_EVALUATION.md +574 -0
  10. package/PROGRESS_EVALUATION.md +450 -0
  11. package/RADIO_EVALUATION.md +439 -0
  12. package/RADIO_VISUAL_FIX.md +183 -0
  13. package/SECTION_IMPROVEMENTS.md +153 -0
  14. package/SWITCH_EVALUATION.md +335 -0
  15. package/SWITCH_VISUAL_FIX.md +232 -0
  16. package/TAB_EVALUATION.md +626 -0
  17. package/TEXTFIELD_EVALUATION.md +747 -0
  18. package/TOOLTIP_FIX.md +157 -0
  19. package/TREE_EVALUATION.md +708 -0
  20. package/dist/index.cjs +10893 -1969
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.css +7768 -1096
  23. package/dist/index.css.map +1 -1
  24. package/dist/index.modern.js +10921 -2005
  25. package/dist/index.modern.js.map +1 -1
  26. package/dist/index.umd.js +10893 -1969
  27. package/dist/index.umd.js.map +1 -1
  28. package/jest.config.js +24 -0
  29. package/package.json +10 -1
  30. package/src/html/accordion.css +208 -4
  31. package/src/html/accordion.example.js +390 -0
  32. package/src/html/accordion.js +284 -28
  33. package/src/html/accordion.unit.test.js +334 -0
  34. package/src/html/button.css +157 -16
  35. package/src/html/button.example.js +374 -0
  36. package/src/html/button.js +240 -60
  37. package/src/html/button.test.js +422 -0
  38. package/src/html/checkbox.css +74 -2
  39. package/src/html/checkbox.example.js +316 -0
  40. package/src/html/checkbox.js +113 -26
  41. package/src/html/checkbox.test.js +285 -0
  42. package/src/html/chip.css +230 -19
  43. package/src/html/chip.example.js +355 -0
  44. package/src/html/chip.js +321 -25
  45. package/src/html/chip.test.js +425 -0
  46. package/src/html/color.css +435 -6
  47. package/src/html/color.example.js +527 -0
  48. package/src/html/color.js +458 -9
  49. package/src/html/color.test.js +362 -4
  50. package/src/html/components.example.js +492 -0
  51. package/src/html/components_enhanced.test.js +581 -0
  52. package/src/html/form.css +70 -3
  53. package/src/html/form.example.js +385 -0
  54. package/src/html/form.js +232 -34
  55. package/src/html/form.test.js +369 -0
  56. package/src/html/header2.css +264 -0
  57. package/src/html/header2.example.js +411 -0
  58. package/src/html/header2.js +203 -0
  59. package/src/html/header2.test.js +377 -0
  60. package/src/html/icon.css +20 -2
  61. package/src/html/icon.example.js +268 -0
  62. package/src/html/icon.js +86 -16
  63. package/src/html/icon.test.js +231 -0
  64. package/src/html/index.js +4 -1
  65. package/src/html/list.css +393 -1
  66. package/src/html/list.example.js +404 -0
  67. package/src/html/list.js +583 -40
  68. package/src/html/list.test.js +383 -0
  69. package/src/html/progress.css +707 -17
  70. package/src/html/progress.example.js +424 -0
  71. package/src/html/progress.js +906 -9
  72. package/src/html/progress.test.js +313 -0
  73. package/src/html/property.css +399 -0
  74. package/src/html/property.example.js +553 -0
  75. package/src/html/property.js +393 -15
  76. package/src/html/property.test.js +351 -2
  77. package/src/html/radio-visual-test.js +289 -0
  78. package/src/html/radio.css +137 -11
  79. package/src/html/radio.example.js +389 -0
  80. package/src/html/radio.js +234 -10
  81. package/src/html/radio.test.js +318 -0
  82. package/src/html/section.example.js +99 -0
  83. package/src/html/section.js +40 -3
  84. package/src/html/section.test.js +131 -0
  85. package/src/html/selector.css +329 -3
  86. package/src/html/selector.js +369 -23
  87. package/src/html/switch-debug.js +197 -0
  88. package/src/html/switch-test-visual.js +294 -0
  89. package/src/html/switch.css +200 -0
  90. package/src/html/switch.example.js +461 -0
  91. package/src/html/switch.js +283 -23
  92. package/src/html/switch.test.js +355 -0
  93. package/src/html/tab.css +289 -0
  94. package/src/html/tab.example.js +446 -0
  95. package/src/html/tab.js +387 -22
  96. package/src/html/tab_enhanced.js +378 -0
  97. package/src/html/tab_enhanced.test.js +504 -0
  98. package/src/html/table2.css +576 -0
  99. package/src/html/table2.example.js +703 -0
  100. package/src/html/table2.js +1252 -0
  101. package/src/html/table2.migration.md +328 -0
  102. package/src/html/table2.test.js +582 -0
  103. package/src/html/text.css +375 -0
  104. package/src/html/text.js +311 -20
  105. package/src/html/textfield2.css +841 -0
  106. package/src/html/textfield2.example.js +1370 -0
  107. package/src/html/textfield2.js +1143 -0
  108. package/src/html/textfield2.test.js +950 -0
  109. package/src/html/thumbnail.css +289 -2
  110. package/src/html/thumbnail.js +214 -9
  111. package/src/html/tokenfield.css +449 -1
  112. package/src/html/tokenfield.example.js +503 -0
  113. package/src/html/tokenfield.js +561 -56
  114. package/src/html/tokenfield.test.js +423 -0
  115. package/src/html/tooltip-positioning-demo.js +187 -0
  116. package/src/html/tooltip.css +25 -2
  117. package/src/html/tree.css +240 -10
  118. package/src/html/tree.example.js +475 -0
  119. package/src/html/tree.js +714 -28
  120. package/src/html/tree_enhanced.test.js +495 -0
  121. package/table2.test.js +454 -0
  122. package/src/html/button.tsx +0 -38
@@ -0,0 +1,950 @@
1
+ import React from 'react'
2
+ import { TextField2, TextArea2, PasswordField2, DropDown2, DateRange2 } from './textfield2'
3
+
4
+ // Pruebas unitarias para los componentes TextField2
5
+ describe('TextField2 Components', () => {
6
+ // Mock de los componentes dependientes
7
+ const mockText = jest.fn()
8
+ const mockIcon = jest.fn()
9
+ const mockSiteContext = {
10
+ translate: jest.fn((text) => text),
11
+ changeFocus: jest.fn()
12
+ }
13
+
14
+ beforeEach(() => {
15
+ jest.clearAllMocks()
16
+
17
+ // Mock del componente Text
18
+ jest.doMock('./text', () => ({
19
+ Text: mockText
20
+ }))
21
+
22
+ // Mock del componente Icon
23
+ jest.doMock('./icon', () => ({
24
+ Icon: mockIcon
25
+ }))
26
+
27
+ // Mock del SiteContext
28
+ jest.doMock('../site/siteContext', () => ({
29
+ SiteContext: {
30
+ Provider: ({ children }) => children,
31
+ Consumer: ({ children }) => children(mockSiteContext)
32
+ }
33
+ }))
34
+
35
+ // Mock de console.warn para las pruebas
36
+ jest.spyOn(console, 'warn').mockImplementation(() => {})
37
+ })
38
+
39
+ afterEach(() => {
40
+ console.warn.mockRestore()
41
+ })
42
+
43
+ describe('TextField2 Component', () => {
44
+ test('component exports correctly', () => {
45
+ expect(TextField2).toBeDefined()
46
+ expect(typeof TextField2).toBe('function')
47
+ })
48
+
49
+ test('component has correct PropTypes', () => {
50
+ expect(TextField2.propTypes).toBeDefined()
51
+ expect(TextField2.propTypes.id).toBeDefined()
52
+ expect(TextField2.propTypes.type).toBeDefined()
53
+ expect(TextField2.propTypes.className).toBeDefined()
54
+ expect(TextField2.propTypes.label).toBeDefined()
55
+ expect(TextField2.propTypes.labelPosition).toBeDefined()
56
+ expect(TextField2.propTypes.placeholder).toBeDefined()
57
+ expect(TextField2.propTypes.value).toBeDefined()
58
+ expect(TextField2.propTypes.outlined).toBeDefined()
59
+ expect(TextField2.propTypes.readOnly).toBeDefined()
60
+ expect(TextField2.propTypes.disabled).toBeDefined()
61
+ expect(TextField2.propTypes.required).toBeDefined()
62
+ expect(TextField2.propTypes.canClear).toBeDefined()
63
+ expect(TextField2.propTypes.showPasswordToggle).toBeDefined()
64
+ expect(TextField2.propTypes.autoComplete).toBeDefined()
65
+ expect(TextField2.propTypes.error).toBeDefined()
66
+ expect(TextField2.propTypes.helperText).toBeDefined()
67
+ expect(TextField2.propTypes.validation).toBeDefined()
68
+ expect(TextField2.propTypes.onChange).toBeDefined()
69
+ })
70
+
71
+ test('component has correct defaultProps', () => {
72
+ expect(TextField2.defaultProps).toBeDefined()
73
+ expect(TextField2.defaultProps.type).toBe('text')
74
+ expect(TextField2.defaultProps.labelPosition).toBe('top')
75
+ expect(TextField2.defaultProps.outlined).toBe(false)
76
+ expect(TextField2.defaultProps.readOnly).toBe(false)
77
+ expect(TextField2.defaultProps.disabled).toBe(false)
78
+ expect(TextField2.defaultProps.required).toBe(false)
79
+ expect(TextField2.defaultProps.canClear).toBe(true)
80
+ expect(TextField2.defaultProps.showPasswordToggle).toBe(true)
81
+ expect(TextField2.defaultProps.autoComplete).toBe('off')
82
+ expect(TextField2.defaultProps.debounceMs).toBe(0)
83
+ expect(TextField2.defaultProps.className).toBe('')
84
+ })
85
+
86
+ test('warns when id prop is missing', () => {
87
+ const validateId = (id) => {
88
+ if (!id) {
89
+ console.warn('TextField2 component: id prop is required')
90
+ }
91
+ }
92
+
93
+ validateId(null)
94
+ expect(console.warn).toHaveBeenCalledWith('TextField2 component: id prop is required')
95
+
96
+ console.warn.mockClear()
97
+ validateId('test-id')
98
+ expect(console.warn).not.toHaveBeenCalled()
99
+ })
100
+
101
+ test('validation logic works correctly', () => {
102
+ const validateField = (value, validation, required) => {
103
+ if (validation && value !== undefined) {
104
+ const validationResult = validation(value)
105
+ const valid = typeof validationResult === 'boolean' ? validationResult : validationResult.valid
106
+ const errorMessage = typeof validationResult === 'object' ? validationResult.message : ''
107
+
108
+ return {
109
+ isValid: valid,
110
+ internalError: valid ? '' : errorMessage || (required && !value ? 'This field is required' : 'Invalid value')
111
+ }
112
+ } else if (required && !value) {
113
+ return {
114
+ isValid: false,
115
+ internalError: 'This field is required'
116
+ }
117
+ } else {
118
+ return {
119
+ isValid: true,
120
+ internalError: ''
121
+ }
122
+ }
123
+ }
124
+
125
+ // Valid value with validation
126
+ const result1 = validateField('test@email.com', (v) => v.includes('@'), false)
127
+ expect(result1.isValid).toBe(true)
128
+ expect(result1.internalError).toBe('')
129
+
130
+ // Invalid value with validation
131
+ const result2 = validateField('invalid-email', (v) => v.includes('@'), false)
132
+ expect(result2.isValid).toBe(false)
133
+ expect(result2.internalError).toBe('Invalid value')
134
+
135
+ // Required field empty
136
+ const result3 = validateField('', null, true)
137
+ expect(result3.isValid).toBe(false)
138
+ expect(result3.internalError).toBe('This field is required')
139
+
140
+ // Valid required field
141
+ const result4 = validateField('value', null, true)
142
+ expect(result4.isValid).toBe(true)
143
+ expect(result4.internalError).toBe('')
144
+
145
+ // Validation with custom error message
146
+ const result5 = validateField('short', (v) => ({ valid: v.length >= 6, message: 'Must be at least 6 characters' }), false)
147
+ expect(result5.isValid).toBe(false)
148
+ expect(result5.internalError).toBe('Must be at least 6 characters')
149
+ })
150
+
151
+ test('CSS classes generation works correctly', () => {
152
+ const generateClasses = (outlined, labelPosition, label, type, isFocused, disabled, readOnly, error, internalError, isValid, className, id) => {
153
+ const borderStyle = outlined ? 'textfield2-outlined' : 'textfield2'
154
+ const labelStyle = label ? '' : 'no-label'
155
+ const labelPositionStyle = labelPosition === 'left' ? 'label-left' : 'label-top'
156
+
157
+ return [
158
+ 'textfield2',
159
+ borderStyle,
160
+ labelStyle,
161
+ labelPositionStyle,
162
+ `textfield2-${type}`,
163
+ isFocused && 'focused',
164
+ disabled && 'disabled',
165
+ readOnly && 'readonly',
166
+ error || internalError ? 'error' : '',
167
+ !isValid && 'invalid',
168
+ className || '',
169
+ id
170
+ ].filter(Boolean).join(' ')
171
+ }
172
+
173
+ expect(generateClasses(false, 'top', 'Label', 'text', false, false, false, '', '', true, '', 'test-id'))
174
+ .toBe('textfield2 textfield2 label-top textfield2-text test-id')
175
+
176
+ expect(generateClasses(true, 'left', '', 'email', true, false, false, '', '', true, 'custom', 'test-id'))
177
+ .toBe('textfield2 textfield2-outlined no-label label-left textfield2-email focused custom test-id')
178
+
179
+ expect(generateClasses(false, 'top', 'Label', 'password', false, true, false, '', '', true, '', 'test-id'))
180
+ .toBe('textfield2 textfield2 label-top textfield2-password disabled test-id')
181
+
182
+ expect(generateClasses(false, 'top', 'Label', 'text', false, false, false, 'Error message', '', false, '', 'test-id'))
183
+ .toBe('textfield2 textfield2 label-top textfield2-text error invalid test-id')
184
+ })
185
+
186
+ test('accessibility attributes generation works correctly', () => {
187
+ const generateAriaAttributes = (ariaLabel, label, ariaDescribedBy, error, internalError, helperText, id, isValid, required, disabled, readOnly) => {
188
+ return {
189
+ 'aria-label': ariaLabel || label,
190
+ 'aria-describedby': ariaDescribedBy || (error || internalError || helperText ? `${id}-helper` : undefined),
191
+ 'aria-invalid': !isValid || !!(error || internalError),
192
+ 'aria-required': required,
193
+ 'aria-disabled': disabled,
194
+ 'aria-readonly': readOnly
195
+ }
196
+ }
197
+
198
+ const basic = generateAriaAttributes(null, 'Test Label', null, '', '', '', 'test-id', true, false, false, false)
199
+ expect(basic['aria-label']).toBe('Test Label')
200
+ expect(basic['aria-describedby']).toBeUndefined()
201
+ expect(basic['aria-invalid']).toBe(false)
202
+ expect(basic['aria-required']).toBe(false)
203
+ expect(basic['aria-disabled']).toBe(false)
204
+ expect(basic['aria-readonly']).toBe(false)
205
+
206
+ const withError = generateAriaAttributes(null, 'Test Label', null, 'Error message', '', '', 'test-id', false, true, false, false)
207
+ expect(withError['aria-describedby']).toBe('test-id-helper')
208
+ expect(withError['aria-invalid']).toBe(true)
209
+ expect(withError['aria-required']).toBe(true)
210
+
211
+ const disabled = generateAriaAttributes(null, 'Test Label', null, '', '', '', 'test-id', true, false, true, true)
212
+ expect(disabled['aria-disabled']).toBe(true)
213
+ expect(disabled['aria-readonly']).toBe(true)
214
+ })
215
+
216
+ test('input attributes generation works correctly', () => {
217
+ const generateInputAttributes = (id, type, isPasswordVisible, placeholder, value, required, disabled, readOnly, maxLength, minLength, pattern, step, min, max, autoComplete, ariaAttributes, restProps) => {
218
+ return {
219
+ id,
220
+ type: type === 'password' && isPasswordVisible ? 'text' : type,
221
+ placeholder,
222
+ value: value || '',
223
+ required,
224
+ disabled,
225
+ readOnly,
226
+ maxLength,
227
+ minLength,
228
+ pattern,
229
+ step,
230
+ min,
231
+ max,
232
+ autoComplete,
233
+ ...ariaAttributes,
234
+ ...restProps
235
+ }
236
+ }
237
+
238
+ const basic = generateInputAttributes('test-id', 'text', false, 'Enter text', 'value', false, false, false, null, null, null, null, null, null, 'off', {}, {})
239
+ expect(basic.id).toBe('test-id')
240
+ expect(basic.type).toBe('text')
241
+ expect(basic.placeholder).toBe('Enter text')
242
+ expect(basic.value).toBe('value')
243
+
244
+ const password = generateInputAttributes('pwd-id', 'password', true, 'Enter password', 'secret', true, false, false, null, null, null, null, null, null, 'off', {}, {})
245
+ expect(password.type).toBe('text') // Password visible
246
+ expect(password.required).toBe(true)
247
+
248
+ const passwordHidden = generateInputAttributes('pwd-id', 'password', false, 'Enter password', 'secret', true, false, false, null, null, null, null, null, null, 'off', {}, {})
249
+ expect(passwordHidden.type).toBe('password') // Password hidden
250
+ })
251
+
252
+ test('debounce logic works correctly', () => {
253
+ jest.useFakeTimers()
254
+
255
+ const mockOnChange = jest.fn()
256
+ let debounceRef = { current: null }
257
+
258
+ const simulateDebounce = (debounceMs, id, value, onChange) => {
259
+ // Clear previous debounce
260
+ if (debounceRef.current) {
261
+ clearTimeout(debounceRef.current)
262
+ }
263
+
264
+ if (debounceMs > 0) {
265
+ debounceRef.current = setTimeout(() => {
266
+ if (onChange) onChange(id, value, {})
267
+ }, debounceMs)
268
+ } else {
269
+ if (onChange) onChange(id, value, {})
270
+ }
271
+ }
272
+
273
+ // No debounce
274
+ simulateDebounce(0, 'test-id', 'value', mockOnChange)
275
+ expect(mockOnChange).toHaveBeenCalledWith('test-id', 'value', {})
276
+
277
+ mockOnChange.mockClear()
278
+
279
+ // With debounce
280
+ simulateDebounce(300, 'test-id', 'value1', mockOnChange)
281
+ expect(mockOnChange).not.toHaveBeenCalled()
282
+
283
+ jest.advanceTimersByTime(300)
284
+ expect(mockOnChange).toHaveBeenCalledWith('test-id', 'value1', {})
285
+
286
+ mockOnChange.mockClear()
287
+
288
+ // Debounce cancellation
289
+ simulateDebounce(300, 'test-id', 'value2', mockOnChange)
290
+ simulateDebounce(300, 'test-id', 'value3', mockOnChange)
291
+
292
+ jest.advanceTimersByTime(300)
293
+ expect(mockOnChange).toHaveBeenCalledTimes(1)
294
+ expect(mockOnChange).toHaveBeenCalledWith('test-id', 'value3', {})
295
+
296
+ jest.useRealTimers()
297
+ })
298
+
299
+ test('password visibility toggle works correctly', () => {
300
+ const togglePassword = (isPasswordVisible) => !isPasswordVisible
301
+
302
+ expect(togglePassword(false)).toBe(true)
303
+ expect(togglePassword(true)).toBe(false)
304
+ })
305
+
306
+ test('clear functionality works correctly', () => {
307
+ const mockOnChange = jest.fn()
308
+
309
+ const simulateClear = (disabled, readOnly, id, onChange) => {
310
+ if (disabled || readOnly) return
311
+
312
+ if (onChange) onChange(id, '', { target: { value: '' } })
313
+ }
314
+
315
+ // Normal clear
316
+ simulateClear(false, false, 'test-id', mockOnChange)
317
+ expect(mockOnChange).toHaveBeenCalledWith('test-id', '', { target: { value: '' } })
318
+
319
+ mockOnChange.mockClear()
320
+
321
+ // Disabled clear
322
+ simulateClear(true, false, 'test-id', mockOnChange)
323
+ expect(mockOnChange).not.toHaveBeenCalled()
324
+
325
+ // ReadOnly clear
326
+ simulateClear(false, true, 'test-id', mockOnChange)
327
+ expect(mockOnChange).not.toHaveBeenCalled()
328
+ })
329
+
330
+ test('focus management works correctly', () => {
331
+ const mockSite = {
332
+ changeFocus: jest.fn()
333
+ }
334
+
335
+ const simulateFocus = (disabled, onFocus, site) => {
336
+ if (disabled) return
337
+
338
+ if (onFocus) onFocus({})
339
+
340
+ if (site && site.changeFocus) {
341
+ site.changeFocus({
342
+ lose: () => {} // setIsFocused(false)
343
+ })
344
+ }
345
+ }
346
+
347
+ const mockOnFocus = jest.fn()
348
+
349
+ simulateFocus(false, mockOnFocus, mockSite)
350
+ expect(mockOnFocus).toHaveBeenCalled()
351
+ expect(mockSite.changeFocus).toHaveBeenCalled()
352
+
353
+ mockOnFocus.mockClear()
354
+ mockSite.changeFocus.mockClear()
355
+
356
+ // Disabled focus
357
+ simulateFocus(true, mockOnFocus, mockSite)
358
+ expect(mockOnFocus).not.toHaveBeenCalled()
359
+ expect(mockSite.changeFocus).not.toHaveBeenCalled()
360
+ })
361
+
362
+ test('error and helper text display logic works correctly', () => {
363
+ const getDisplayText = (error, internalError, helperText) => {
364
+ const displayError = error || internalError
365
+ const displayHelperText = helperText && !displayError
366
+
367
+ return {
368
+ displayError,
369
+ displayHelperText,
370
+ showHelper: !!(displayError || displayHelperText)
371
+ }
372
+ }
373
+
374
+ const result1 = getDisplayText('External error', '', 'Helper text')
375
+ expect(result1.displayError).toBe('External error')
376
+ expect(result1.displayHelperText).toBe(false)
377
+ expect(result1.showHelper).toBe(true)
378
+
379
+ const result2 = getDisplayText('', 'Internal error', 'Helper text')
380
+ expect(result2.displayError).toBe('Internal error')
381
+ expect(result2.displayHelperText).toBe(false)
382
+ expect(result2.showHelper).toBe(true)
383
+
384
+ const result3 = getDisplayText('', '', 'Helper text')
385
+ expect(result3.displayError).toBe('')
386
+ expect(result3.displayHelperText).toBe(true)
387
+ expect(result3.showHelper).toBe(true)
388
+
389
+ const result4 = getDisplayText('', '', '')
390
+ expect(result4.displayError).toBe('')
391
+ expect(result4.displayHelperText).toBe('')
392
+ expect(result4.showHelper).toBe(false)
393
+ })
394
+ })
395
+
396
+ describe('TextArea2 Component', () => {
397
+ test('component exports correctly', () => {
398
+ expect(TextArea2).toBeDefined()
399
+ expect(typeof TextArea2).toBe('function')
400
+ })
401
+
402
+ test('component has correct PropTypes', () => {
403
+ expect(TextArea2.propTypes).toBeDefined()
404
+ // Should inherit from TextField2
405
+ expect(TextArea2.propTypes.id).toBeDefined()
406
+ expect(TextArea2.propTypes.rows).toBeDefined()
407
+ })
408
+
409
+ test('component has correct defaultProps', () => {
410
+ expect(TextArea2.defaultProps).toBeDefined()
411
+ expect(TextArea2.defaultProps.type).toBe('textarea')
412
+ })
413
+ })
414
+
415
+ describe('PasswordField2 Component', () => {
416
+ test('component exports correctly', () => {
417
+ expect(PasswordField2).toBeDefined()
418
+ expect(typeof PasswordField2).toBe('function')
419
+ })
420
+
421
+ test('component has correct PropTypes', () => {
422
+ expect(PasswordField2.propTypes).toBeDefined()
423
+ // Should inherit from TextField2
424
+ expect(PasswordField2.propTypes.id).toBeDefined()
425
+ expect(PasswordField2.propTypes.showPasswordToggle).toBeDefined()
426
+ })
427
+
428
+ test('component has correct defaultProps', () => {
429
+ expect(PasswordField2.defaultProps).toBeDefined()
430
+ expect(PasswordField2.defaultProps.type).toBe('password')
431
+ })
432
+ })
433
+
434
+ describe('DropDown2 Component', () => {
435
+ test('component exports correctly', () => {
436
+ expect(DropDown2).toBeDefined()
437
+ expect(typeof DropDown2).toBe('function')
438
+ })
439
+
440
+ test('component has correct PropTypes', () => {
441
+ expect(DropDown2.propTypes).toBeDefined()
442
+ expect(DropDown2.propTypes.id).toBeDefined()
443
+ expect(DropDown2.propTypes.options).toBeDefined()
444
+ expect(DropDown2.propTypes.value).toBeDefined()
445
+ expect(DropDown2.propTypes.placeholder).toBeDefined()
446
+ expect(DropDown2.propTypes.label).toBeDefined()
447
+ expect(DropDown2.propTypes.outlined).toBeDefined()
448
+ expect(DropDown2.propTypes.disabled).toBeDefined()
449
+ expect(DropDown2.propTypes.readOnly).toBeDefined()
450
+ expect(DropDown2.propTypes.required).toBeDefined()
451
+ expect(DropDown2.propTypes.searchable).toBeDefined()
452
+ expect(DropDown2.propTypes.clearable).toBeDefined()
453
+ expect(DropDown2.propTypes.multiple).toBeDefined()
454
+ expect(DropDown2.propTypes.onChange).toBeDefined()
455
+ })
456
+
457
+ test('component has correct defaultProps', () => {
458
+ expect(DropDown2.defaultProps).toBeDefined()
459
+ expect(DropDown2.defaultProps.options).toEqual([])
460
+ expect(DropDown2.defaultProps.outlined).toBe(false)
461
+ expect(DropDown2.defaultProps.disabled).toBe(false)
462
+ expect(DropDown2.defaultProps.readOnly).toBe(false)
463
+ expect(DropDown2.defaultProps.required).toBe(false)
464
+ expect(DropDown2.defaultProps.searchable).toBe(false)
465
+ expect(DropDown2.defaultProps.clearable).toBe(false)
466
+ expect(DropDown2.defaultProps.multiple).toBe(false)
467
+ expect(DropDown2.defaultProps.position).toBe('bottom')
468
+ expect(DropDown2.defaultProps.maxHeight).toBe('200px')
469
+ expect(DropDown2.defaultProps.className).toBe('')
470
+ })
471
+
472
+ test('warns when id prop is missing', () => {
473
+ const validateId = (id) => {
474
+ if (!id) {
475
+ console.warn('DropDown2 component: id prop is required')
476
+ }
477
+ }
478
+
479
+ validateId(null)
480
+ expect(console.warn).toHaveBeenCalledWith('DropDown2 component: id prop is required')
481
+
482
+ console.warn.mockClear()
483
+ validateId('test-id')
484
+ expect(console.warn).not.toHaveBeenCalled()
485
+ })
486
+
487
+ test('warns when options is not an array', () => {
488
+ const validateOptions = (options) => {
489
+ if (!Array.isArray(options)) {
490
+ console.warn('DropDown2 component: options must be an array')
491
+ }
492
+ }
493
+
494
+ validateOptions('not an array')
495
+ expect(console.warn).toHaveBeenCalledWith('DropDown2 component: options must be an array')
496
+
497
+ console.warn.mockClear()
498
+ validateOptions([])
499
+ expect(console.warn).not.toHaveBeenCalled()
500
+ })
501
+
502
+ test('display value generation works correctly', () => {
503
+ const options = [
504
+ { value: 'option1', label: 'Option 1' },
505
+ { value: 'option2', label: 'Option 2' },
506
+ { value: 'option3', label: 'Option 3' }
507
+ ]
508
+
509
+ const getDisplayValue = (value, multiple, options, renderValue) => {
510
+ if (!value) return ''
511
+
512
+ if (multiple && Array.isArray(value)) {
513
+ if (value.length === 0) return ''
514
+ if (value.length === 1) {
515
+ const option = options.find(opt => opt.value === value[0])
516
+ return option ? option.label : value[0]
517
+ }
518
+ return `${value.length} items selected`
519
+ }
520
+
521
+ const option = options.find(opt => opt.value === value)
522
+ if (renderValue && option) {
523
+ return renderValue(option)
524
+ }
525
+ return option ? option.label : value
526
+ }
527
+
528
+ // Single selection
529
+ expect(getDisplayValue('option1', false, options)).toBe('Option 1')
530
+ expect(getDisplayValue('unknown', false, options)).toBe('unknown')
531
+ expect(getDisplayValue('', false, options)).toBe('')
532
+
533
+ // Multiple selection
534
+ expect(getDisplayValue([], true, options)).toBe('')
535
+ expect(getDisplayValue(['option1'], true, options)).toBe('Option 1')
536
+ expect(getDisplayValue(['option1', 'option2'], true, options)).toBe('2 items selected')
537
+
538
+ // Custom render
539
+ const customRender = (option) => `Custom: ${option.label}`
540
+ expect(getDisplayValue('option1', false, options, customRender)).toBe('Custom: Option 1')
541
+ })
542
+
543
+ test('option filtering works correctly', () => {
544
+ const options = [
545
+ { value: 'apple', label: 'Apple' },
546
+ { value: 'banana', label: 'Banana' },
547
+ { value: 'cherry', label: 'Cherry' }
548
+ ]
549
+
550
+ const filterOptions = (options, searchTerm, searchable, filterFunction) => {
551
+ if (!searchTerm || !searchable) return options
552
+
553
+ if (filterFunction) {
554
+ return options.filter(option => filterFunction(option, searchTerm))
555
+ }
556
+
557
+ return options.filter(option =>
558
+ option.label.toLowerCase().includes(searchTerm.toLowerCase())
559
+ )
560
+ }
561
+
562
+ // No search
563
+ expect(filterOptions(options, 'app', false)).toEqual(options)
564
+ expect(filterOptions(options, '', true)).toEqual(options)
565
+
566
+ // Default filtering
567
+ expect(filterOptions(options, 'app', true)).toEqual([{ value: 'apple', label: 'Apple' }])
568
+ expect(filterOptions(options, 'an', true)).toEqual([{ value: 'banana', label: 'Banana' }])
569
+
570
+ // Custom filtering
571
+ const customFilter = (option, term) => option.value.startsWith(term)
572
+ expect(filterOptions(options, 'a', true, customFilter)).toEqual([{ value: 'apple', label: 'Apple' }])
573
+ })
574
+
575
+ test('option grouping works correctly', () => {
576
+ const options = [
577
+ { value: 'apple', label: 'Apple', category: 'Fruits' },
578
+ { value: 'banana', label: 'Banana', category: 'Fruits' },
579
+ { value: 'carrot', label: 'Carrot', category: 'Vegetables' }
580
+ ]
581
+
582
+ const groupOptions = (options, groupBy) => {
583
+ if (!groupBy) return [{ options }]
584
+
585
+ const groups = options.reduce((acc, option) => {
586
+ const groupKey = typeof groupBy === 'function' ? groupBy(option) : option[groupBy]
587
+ if (!acc[groupKey]) {
588
+ acc[groupKey] = []
589
+ }
590
+ acc[groupKey].push(option)
591
+ return acc
592
+ }, {})
593
+
594
+ return Object.entries(groups).map(([label, options]) => ({ label, options }))
595
+ }
596
+
597
+ // No grouping
598
+ expect(groupOptions(options)).toEqual([{ options }])
599
+
600
+ // Group by property
601
+ const grouped = groupOptions(options, 'category')
602
+ expect(grouped).toHaveLength(2)
603
+ expect(grouped[0].label).toBe('Fruits')
604
+ expect(grouped[0].options).toHaveLength(2)
605
+ expect(grouped[1].label).toBe('Vegetables')
606
+ expect(grouped[1].options).toHaveLength(1)
607
+
608
+ // Group by function
609
+ const groupByFirstLetter = (option) => option.label[0]
610
+ const groupedByLetter = groupOptions(options, groupByFirstLetter)
611
+ expect(groupedByLetter).toHaveLength(3) // A, B, C
612
+ })
613
+
614
+ test('selection logic works correctly', () => {
615
+ const handleSelection = (selectedValue, multiple, currentValue) => {
616
+ if (multiple) {
617
+ const currentValues = Array.isArray(currentValue) ? currentValue : []
618
+ if (currentValues.includes(selectedValue)) {
619
+ return currentValues.filter(v => v !== selectedValue)
620
+ } else {
621
+ return [...currentValues, selectedValue]
622
+ }
623
+ } else {
624
+ return selectedValue
625
+ }
626
+ }
627
+
628
+ // Single selection
629
+ expect(handleSelection('option1', false, '')).toBe('option1')
630
+ expect(handleSelection('option2', false, 'option1')).toBe('option2')
631
+
632
+ // Multiple selection - add
633
+ expect(handleSelection('option1', true, [])).toEqual(['option1'])
634
+ expect(handleSelection('option2', true, ['option1'])).toEqual(['option1', 'option2'])
635
+
636
+ // Multiple selection - remove
637
+ expect(handleSelection('option1', true, ['option1', 'option2'])).toEqual(['option2'])
638
+ })
639
+
640
+ test('keyboard navigation logic works correctly', () => {
641
+ const handleKeyNavigation = (key, isOpen, focusedIndex, optionsLength) => {
642
+ switch (key) {
643
+ case 'ArrowDown':
644
+ if (!isOpen) {
645
+ return { isOpen: true, focusedIndex: -1 }
646
+ } else {
647
+ return {
648
+ isOpen: true,
649
+ focusedIndex: focusedIndex < optionsLength - 1 ? focusedIndex + 1 : 0
650
+ }
651
+ }
652
+ case 'ArrowUp':
653
+ if (isOpen) {
654
+ return {
655
+ isOpen: true,
656
+ focusedIndex: focusedIndex > 0 ? focusedIndex - 1 : optionsLength - 1
657
+ }
658
+ }
659
+ return { isOpen, focusedIndex }
660
+ case 'Escape':
661
+ return { isOpen: false, focusedIndex: -1 }
662
+ default:
663
+ return { isOpen, focusedIndex }
664
+ }
665
+ }
666
+
667
+ // Arrow down when closed
668
+ expect(handleKeyNavigation('ArrowDown', false, -1, 3)).toEqual({ isOpen: true, focusedIndex: -1 })
669
+
670
+ // Arrow down when open
671
+ expect(handleKeyNavigation('ArrowDown', true, -1, 3)).toEqual({ isOpen: true, focusedIndex: 0 })
672
+ expect(handleKeyNavigation('ArrowDown', true, 0, 3)).toEqual({ isOpen: true, focusedIndex: 1 })
673
+ expect(handleKeyNavigation('ArrowDown', true, 2, 3)).toEqual({ isOpen: true, focusedIndex: 0 }) // Wrap around
674
+
675
+ // Arrow up
676
+ expect(handleKeyNavigation('ArrowUp', true, 1, 3)).toEqual({ isOpen: true, focusedIndex: 0 })
677
+ expect(handleKeyNavigation('ArrowUp', true, 0, 3)).toEqual({ isOpen: true, focusedIndex: 2 }) // Wrap around
678
+
679
+ // Escape
680
+ expect(handleKeyNavigation('Escape', true, 1, 3)).toEqual({ isOpen: false, focusedIndex: -1 })
681
+ })
682
+
683
+ test('CSS classes generation works correctly', () => {
684
+ const generateClasses = (outlined, disabled, readOnly, isOpen, error, internalError, multiple, className) => {
685
+ return [
686
+ 'dropdown2',
687
+ outlined && 'outlined',
688
+ disabled && 'disabled',
689
+ readOnly && 'readonly',
690
+ isOpen && 'open',
691
+ error || internalError ? 'error' : '',
692
+ multiple && 'multiple',
693
+ className || ''
694
+ ].filter(Boolean).join(' ')
695
+ }
696
+
697
+ expect(generateClasses(false, false, false, false, '', '', false, '')).toBe('dropdown2')
698
+ expect(generateClasses(true, false, false, false, '', '', false, '')).toBe('dropdown2 outlined')
699
+ expect(generateClasses(false, true, false, false, '', '', false, '')).toBe('dropdown2 disabled')
700
+ expect(generateClasses(false, false, true, false, '', '', false, '')).toBe('dropdown2 readonly')
701
+ expect(generateClasses(false, false, false, true, '', '', false, '')).toBe('dropdown2 open')
702
+ expect(generateClasses(false, false, false, false, 'Error', '', false, '')).toBe('dropdown2 error')
703
+ expect(generateClasses(false, false, false, false, '', '', true, '')).toBe('dropdown2 multiple')
704
+ expect(generateClasses(false, false, false, false, '', '', false, 'custom')).toBe('dropdown2 custom')
705
+ })
706
+
707
+ test('accessibility attributes generation works correctly', () => {
708
+ const generateAriaAttributes = (ariaLabel, label, isOpen, disabled, readOnly, required, error, internalError, helperText, id) => {
709
+ return {
710
+ 'aria-label': ariaLabel || label,
711
+ 'aria-expanded': isOpen,
712
+ 'aria-haspopup': 'listbox',
713
+ 'aria-disabled': disabled,
714
+ 'aria-readonly': readOnly,
715
+ 'aria-required': required,
716
+ 'aria-invalid': !!(error || internalError),
717
+ 'aria-describedby': error || internalError || helperText ? `${id}-helper` : undefined
718
+ }
719
+ }
720
+
721
+ const basic = generateAriaAttributes(null, 'Test Label', false, false, false, false, '', '', '', 'test-id')
722
+ expect(basic['aria-label']).toBe('Test Label')
723
+ expect(basic['aria-expanded']).toBe(false)
724
+ expect(basic['aria-haspopup']).toBe('listbox')
725
+ expect(basic['aria-disabled']).toBe(false)
726
+ expect(basic['aria-readonly']).toBe(false)
727
+ expect(basic['aria-required']).toBe(false)
728
+ expect(basic['aria-invalid']).toBe(false)
729
+ expect(basic['aria-describedby']).toBeUndefined()
730
+
731
+ const withError = generateAriaAttributes(null, 'Test Label', true, false, false, true, 'Error message', '', '', 'test-id')
732
+ expect(withError['aria-expanded']).toBe(true)
733
+ expect(withError['aria-required']).toBe(true)
734
+ expect(withError['aria-invalid']).toBe(true)
735
+ expect(withError['aria-describedby']).toBe('test-id-helper')
736
+ })
737
+ })
738
+
739
+ describe('DateRange2 Component', () => {
740
+ test('component exports correctly', () => {
741
+ expect(DateRange2).toBeDefined()
742
+ expect(typeof DateRange2).toBe('function')
743
+ })
744
+
745
+ test('component has correct PropTypes', () => {
746
+ expect(DateRange2.propTypes).toBeDefined()
747
+ expect(DateRange2.propTypes.id).toBeDefined()
748
+ expect(DateRange2.propTypes.label).toBeDefined()
749
+ expect(DateRange2.propTypes.value).toBeDefined()
750
+ expect(DateRange2.propTypes.outlined).toBeDefined()
751
+ expect(DateRange2.propTypes.disabled).toBeDefined()
752
+ expect(DateRange2.propTypes.readOnly).toBeDefined()
753
+ expect(DateRange2.propTypes.required).toBeDefined()
754
+ expect(DateRange2.propTypes.minDate).toBeDefined()
755
+ expect(DateRange2.propTypes.maxDate).toBeDefined()
756
+ expect(DateRange2.propTypes.error).toBeDefined()
757
+ expect(DateRange2.propTypes.helperText).toBeDefined()
758
+ expect(DateRange2.propTypes.onChange).toBeDefined()
759
+ expect(DateRange2.propTypes.onValidation).toBeDefined()
760
+ })
761
+
762
+ test('component has correct defaultProps', () => {
763
+ expect(DateRange2.defaultProps).toBeDefined()
764
+ expect(DateRange2.defaultProps.outlined).toBe(false)
765
+ expect(DateRange2.defaultProps.disabled).toBe(false)
766
+ expect(DateRange2.defaultProps.readOnly).toBe(false)
767
+ expect(DateRange2.defaultProps.required).toBe(false)
768
+ expect(DateRange2.defaultProps.className).toBe('')
769
+ })
770
+
771
+ test('warns when id prop is missing', () => {
772
+ const validateId = (id) => {
773
+ if (!id) {
774
+ console.warn('DateRange2 component: id prop is required')
775
+ }
776
+ }
777
+
778
+ validateId(null)
779
+ expect(console.warn).toHaveBeenCalledWith('DateRange2 component: id prop is required')
780
+
781
+ console.warn.mockClear()
782
+ validateId('test-id')
783
+ expect(console.warn).not.toHaveBeenCalled()
784
+ })
785
+
786
+ test('form initialization from value works correctly', () => {
787
+ const initializeForm = (value) => {
788
+ if (value && typeof value === 'object') {
789
+ return {
790
+ from: value.from || '',
791
+ to: value.to || ''
792
+ }
793
+ }
794
+ return { from: '', to: '' }
795
+ }
796
+
797
+ expect(initializeForm(null)).toEqual({ from: '', to: '' })
798
+ expect(initializeForm({})).toEqual({ from: '', to: '' })
799
+ expect(initializeForm({ from: '2023-01-01' })).toEqual({ from: '2023-01-01', to: '' })
800
+ expect(initializeForm({ from: '2023-01-01', to: '2023-12-31' })).toEqual({ from: '2023-01-01', to: '2023-12-31' })
801
+ })
802
+
803
+ test('date range validation works correctly', () => {
804
+ const validateDateRange = (form, required, minDate, maxDate) => {
805
+ let valid = true
806
+ let errorMessage = ''
807
+
808
+ if (required && (!form.from || !form.to)) {
809
+ valid = false
810
+ errorMessage = 'Both dates are required'
811
+ } else if (form.from && form.to) {
812
+ const fromDate = new Date(form.from)
813
+ const toDate = new Date(form.to)
814
+
815
+ if (fromDate > toDate) {
816
+ valid = false
817
+ errorMessage = 'From date must be before To date'
818
+ } else if (minDate && fromDate < new Date(minDate)) {
819
+ valid = false
820
+ errorMessage = `From date must be after ${minDate}`
821
+ } else if (maxDate && toDate > new Date(maxDate)) {
822
+ valid = false
823
+ errorMessage = `To date must be before ${maxDate}`
824
+ }
825
+ }
826
+
827
+ return { valid, errorMessage }
828
+ }
829
+
830
+ // Valid range
831
+ const result1 = validateDateRange({ from: '2023-01-01', to: '2023-12-31' }, false, null, null)
832
+ expect(result1.valid).toBe(true)
833
+ expect(result1.errorMessage).toBe('')
834
+
835
+ // Required but empty
836
+ const result2 = validateDateRange({ from: '', to: '' }, true, null, null)
837
+ expect(result2.valid).toBe(false)
838
+ expect(result2.errorMessage).toBe('Both dates are required')
839
+
840
+ // From after To
841
+ const result3 = validateDateRange({ from: '2023-12-31', to: '2023-01-01' }, false, null, null)
842
+ expect(result3.valid).toBe(false)
843
+ expect(result3.errorMessage).toBe('From date must be before To date')
844
+
845
+ // Before min date
846
+ const result4 = validateDateRange({ from: '2022-01-01', to: '2023-12-31' }, false, '2023-01-01', null)
847
+ expect(result4.valid).toBe(false)
848
+ expect(result4.errorMessage).toBe('From date must be after 2023-01-01')
849
+
850
+ // After max date
851
+ const result5 = validateDateRange({ from: '2023-01-01', to: '2024-12-31' }, false, null, '2024-01-01')
852
+ expect(result5.valid).toBe(false)
853
+ expect(result5.errorMessage).toBe('To date must be before 2024-01-01')
854
+ })
855
+
856
+ test('form change handling works correctly', () => {
857
+ const handleFormChange = (prevForm, fieldId, fieldValue) => {
858
+ return {
859
+ ...prevForm,
860
+ [fieldId]: fieldValue
861
+ }
862
+ }
863
+
864
+ const initialForm = { from: '', to: '' }
865
+
866
+ const result1 = handleFormChange(initialForm, 'from', '2023-01-01')
867
+ expect(result1).toEqual({ from: '2023-01-01', to: '' })
868
+
869
+ const result2 = handleFormChange(result1, 'to', '2023-12-31')
870
+ expect(result2).toEqual({ from: '2023-01-01', to: '2023-12-31' })
871
+ })
872
+
873
+ test('CSS classes generation works correctly', () => {
874
+ const generateClasses = (outlined, disabled, readOnly, error, internalError, isValid, className) => {
875
+ return [
876
+ 'date-range2',
877
+ outlined && 'outlined',
878
+ disabled && 'disabled',
879
+ readOnly && 'readonly',
880
+ error || internalError ? 'error' : '',
881
+ !isValid && 'invalid',
882
+ className || ''
883
+ ].filter(Boolean).join(' ')
884
+ }
885
+
886
+ expect(generateClasses(false, false, false, '', '', true, '')).toBe('date-range2')
887
+ expect(generateClasses(true, false, false, '', '', true, '')).toBe('date-range2 outlined')
888
+ expect(generateClasses(false, true, false, '', '', true, '')).toBe('date-range2 disabled')
889
+ expect(generateClasses(false, false, true, '', '', true, '')).toBe('date-range2 readonly')
890
+ expect(generateClasses(false, false, false, 'Error', '', true, '')).toBe('date-range2 error')
891
+ expect(generateClasses(false, false, false, '', '', false, '')).toBe('date-range2 invalid')
892
+ expect(generateClasses(false, false, false, '', '', true, 'custom')).toBe('date-range2 custom')
893
+ })
894
+
895
+ test('accessibility attributes generation works correctly', () => {
896
+ const generateAriaAttributes = (ariaLabel, label, isValid, error, internalError, required, disabled, readOnly, helperText, id) => {
897
+ return {
898
+ 'aria-label': ariaLabel || label,
899
+ 'aria-invalid': !isValid || !!(error || internalError),
900
+ 'aria-required': required,
901
+ 'aria-disabled': disabled,
902
+ 'aria-readonly': readOnly,
903
+ 'aria-describedby': error || internalError || helperText ? `${id}-helper` : undefined
904
+ }
905
+ }
906
+
907
+ const basic = generateAriaAttributes(null, 'Date Range', true, '', '', false, false, false, '', 'test-id')
908
+ expect(basic['aria-label']).toBe('Date Range')
909
+ expect(basic['aria-invalid']).toBe(false)
910
+ expect(basic['aria-required']).toBe(false)
911
+ expect(basic['aria-disabled']).toBe(false)
912
+ expect(basic['aria-readonly']).toBe(false)
913
+ expect(basic['aria-describedby']).toBeUndefined()
914
+
915
+ const withError = generateAriaAttributes(null, 'Date Range', false, 'Error message', '', true, false, false, '', 'test-id')
916
+ expect(withError['aria-invalid']).toBe(true)
917
+ expect(withError['aria-required']).toBe(true)
918
+ expect(withError['aria-describedby']).toBe('test-id-helper')
919
+ })
920
+
921
+ test('min/max date constraints work correctly', () => {
922
+ const getDateConstraints = (form, minDate, maxDate) => {
923
+ return {
924
+ fromMin: minDate,
925
+ fromMax: form.to || maxDate,
926
+ toMin: form.from || minDate,
927
+ toMax: maxDate
928
+ }
929
+ }
930
+
931
+ const form1 = { from: '', to: '' }
932
+ const constraints1 = getDateConstraints(form1, '2023-01-01', '2023-12-31')
933
+ expect(constraints1).toEqual({
934
+ fromMin: '2023-01-01',
935
+ fromMax: '2023-12-31',
936
+ toMin: '2023-01-01',
937
+ toMax: '2023-12-31'
938
+ })
939
+
940
+ const form2 = { from: '2023-06-01', to: '2023-09-01' }
941
+ const constraints2 = getDateConstraints(form2, '2023-01-01', '2023-12-31')
942
+ expect(constraints2).toEqual({
943
+ fromMin: '2023-01-01',
944
+ fromMax: '2023-09-01',
945
+ toMin: '2023-06-01',
946
+ toMax: '2023-12-31'
947
+ })
948
+ })
949
+ })
950
+ })