@osimatic/helpers-js 1.4.25 → 1.4.26

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 (110) hide show
  1. package/.claude/settings.local.json +1 -1
  2. package/duration.js +174 -125
  3. package/file.js +19 -4
  4. package/google_charts.js +2 -1
  5. package/location.js +5 -1
  6. package/media.js +6 -6
  7. package/multi_files_input.js +3 -1
  8. package/package.json +2 -1
  9. package/paging.js +2 -2
  10. package/tests/__mocks__/socket.io-client.js +13 -0
  11. package/tests/count_down.test.js +580 -0
  12. package/tests/details_sub_array.test.js +367 -0
  13. package/tests/file.test.js +210 -0
  14. package/tests/flash_message.test.js +297 -0
  15. package/tests/form_date.test.js +1142 -0
  16. package/tests/form_helper.test.js +780 -130
  17. package/tests/google_charts.test.js +768 -0
  18. package/tests/google_maps.test.js +655 -0
  19. package/tests/google_recaptcha.test.js +441 -0
  20. package/tests/import_from_csv.test.js +797 -0
  21. package/tests/list_box.test.js +255 -0
  22. package/tests/location.test.js +86 -0
  23. package/tests/media.test.js +15 -0
  24. package/tests/multi_files_input.test.js +1015 -0
  25. package/tests/multiple_action_in_table.test.js +477 -0
  26. package/tests/paging.test.js +646 -0
  27. package/tests/select_all.test.js +360 -0
  28. package/tests/sortable_list.test.js +602 -0
  29. package/tests/string.test.js +16 -0
  30. package/tests/web_rtc.test.js +458 -0
  31. package/tests/web_socket.test.js +538 -0
  32. package/tmpclaude-00a6-cwd +0 -1
  33. package/tmpclaude-0526-cwd +0 -1
  34. package/tmpclaude-0973-cwd +0 -1
  35. package/tmpclaude-0b61-cwd +0 -1
  36. package/tmpclaude-0fa4-cwd +0 -1
  37. package/tmpclaude-104f-cwd +0 -1
  38. package/tmpclaude-1468-cwd +0 -1
  39. package/tmpclaude-146f-cwd +0 -1
  40. package/tmpclaude-223d-cwd +0 -1
  41. package/tmpclaude-2330-cwd +0 -1
  42. package/tmpclaude-282a-cwd +0 -1
  43. package/tmpclaude-2846-cwd +0 -1
  44. package/tmpclaude-28a6-cwd +0 -1
  45. package/tmpclaude-2b5a-cwd +0 -1
  46. package/tmpclaude-2def-cwd +0 -1
  47. package/tmpclaude-324b-cwd +0 -1
  48. package/tmpclaude-35d3-cwd +0 -1
  49. package/tmpclaude-3906-cwd +0 -1
  50. package/tmpclaude-3b32-cwd +0 -1
  51. package/tmpclaude-3da9-cwd +0 -1
  52. package/tmpclaude-3dc3-cwd +0 -1
  53. package/tmpclaude-3e3b-cwd +0 -1
  54. package/tmpclaude-43b6-cwd +0 -1
  55. package/tmpclaude-4495-cwd +0 -1
  56. package/tmpclaude-462f-cwd +0 -1
  57. package/tmpclaude-4aa8-cwd +0 -1
  58. package/tmpclaude-4b29-cwd +0 -1
  59. package/tmpclaude-4db5-cwd +0 -1
  60. package/tmpclaude-4e01-cwd +0 -1
  61. package/tmpclaude-5101-cwd +0 -1
  62. package/tmpclaude-524f-cwd +0 -1
  63. package/tmpclaude-5636-cwd +0 -1
  64. package/tmpclaude-5cdd-cwd +0 -1
  65. package/tmpclaude-5f1f-cwd +0 -1
  66. package/tmpclaude-6078-cwd +0 -1
  67. package/tmpclaude-622e-cwd +0 -1
  68. package/tmpclaude-6802-cwd +0 -1
  69. package/tmpclaude-6e36-cwd +0 -1
  70. package/tmpclaude-7793-cwd +0 -1
  71. package/tmpclaude-7f96-cwd +0 -1
  72. package/tmpclaude-8566-cwd +0 -1
  73. package/tmpclaude-8874-cwd +0 -1
  74. package/tmpclaude-8915-cwd +0 -1
  75. package/tmpclaude-8c8b-cwd +0 -1
  76. package/tmpclaude-94df-cwd +0 -1
  77. package/tmpclaude-9859-cwd +0 -1
  78. package/tmpclaude-9ac5-cwd +0 -1
  79. package/tmpclaude-9f18-cwd +0 -1
  80. package/tmpclaude-a202-cwd +0 -1
  81. package/tmpclaude-a741-cwd +0 -1
  82. package/tmpclaude-ab5f-cwd +0 -1
  83. package/tmpclaude-b008-cwd +0 -1
  84. package/tmpclaude-b0a1-cwd +0 -1
  85. package/tmpclaude-b63d-cwd +0 -1
  86. package/tmpclaude-b681-cwd +0 -1
  87. package/tmpclaude-b72d-cwd +0 -1
  88. package/tmpclaude-b92f-cwd +0 -1
  89. package/tmpclaude-bc49-cwd +0 -1
  90. package/tmpclaude-bc50-cwd +0 -1
  91. package/tmpclaude-bccf-cwd +0 -1
  92. package/tmpclaude-be55-cwd +0 -1
  93. package/tmpclaude-c228-cwd +0 -1
  94. package/tmpclaude-c717-cwd +0 -1
  95. package/tmpclaude-c7ce-cwd +0 -1
  96. package/tmpclaude-cf3e-cwd +0 -1
  97. package/tmpclaude-d142-cwd +0 -1
  98. package/tmpclaude-d5bc-cwd +0 -1
  99. package/tmpclaude-d6ae-cwd +0 -1
  100. package/tmpclaude-d77a-cwd +0 -1
  101. package/tmpclaude-d8da-cwd +0 -1
  102. package/tmpclaude-dbdb-cwd +0 -1
  103. package/tmpclaude-de61-cwd +0 -1
  104. package/tmpclaude-de81-cwd +0 -1
  105. package/tmpclaude-df9d-cwd +0 -1
  106. package/tmpclaude-e786-cwd +0 -1
  107. package/tmpclaude-f01d-cwd +0 -1
  108. package/tmpclaude-f2a9-cwd +0 -1
  109. package/tmpclaude-fc36-cwd +0 -1
  110. package/tmpclaude-ffef-cwd +0 -1
@@ -1,7 +1,627 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
1
4
  require('../array'); // For removeEmptyValues method
2
5
  const { FormHelper, ArrayField, EditValue } = require('../form_helper');
3
6
 
4
7
  describe('FormHelper', () => {
8
+ let mockForm;
9
+ let mockButton;
10
+ let mockInput;
11
+
12
+ beforeEach(() => {
13
+ // Setup common jQuery mocks
14
+ mockInput = {
15
+ val: jest.fn().mockReturnThis(),
16
+ prop: jest.fn().mockReturnThis(),
17
+ attr: jest.fn().mockReturnThis(),
18
+ off: jest.fn().mockReturnThis(),
19
+ click: jest.fn().mockReturnThis(),
20
+ on: jest.fn().mockReturnThis(),
21
+ each: jest.fn().mockReturnThis(),
22
+ filter: jest.fn().mockReturnThis(),
23
+ data: jest.fn().mockReturnThis(),
24
+ parent: jest.fn().mockReturnThis(),
25
+ closest: jest.fn().mockReturnThis(),
26
+ addClass: jest.fn().mockReturnThis(),
27
+ removeClass: jest.fn().mockReturnThis(),
28
+ find: jest.fn().mockReturnThis(),
29
+ remove: jest.fn().mockReturnThis(),
30
+ append: jest.fn().mockReturnThis(),
31
+ prepend: jest.fn().mockReturnThis(),
32
+ before: jest.fn().mockReturnThis(),
33
+ after: jest.fn().mockReturnThis(),
34
+ wrap: jest.fn().mockReturnThis(),
35
+ html: jest.fn().mockReturnThis(),
36
+ text: jest.fn().mockReturnThis(),
37
+ css: jest.fn().mockReturnThis(),
38
+ hasClass: jest.fn(() => false),
39
+ length: 1,
40
+ serialize: jest.fn(() => 'field1=value1&field2=value2'),
41
+ 0: document.createElement('form')
42
+ };
43
+
44
+ mockButton = {
45
+ ...mockInput,
46
+ attr: jest.fn((key, value) => {
47
+ if (key === 'disabled' && value === undefined) return false;
48
+ return mockButton;
49
+ })
50
+ };
51
+
52
+ mockForm = {
53
+ ...mockInput,
54
+ find: jest.fn((selector) => {
55
+ if (selector.includes('button[name="validate"]')) return mockButton;
56
+ if (selector.includes('input[name]:not')) return mockInput;
57
+ if (selector.includes('select.selectpicker')) return mockInput;
58
+ if (selector.includes('div.form_errors')) return mockInput;
59
+ if (selector.includes('.form_errors_content')) return { length: 0 };
60
+ if (selector.includes('.modal-body')) return { length: 0 };
61
+ if (selector.includes('.form-group:first')) return mockInput;
62
+ return mockInput;
63
+ })
64
+ };
65
+
66
+ global.$ = jest.fn((selector) => {
67
+ if (typeof selector === 'function') {
68
+ // Document ready
69
+ return mockInput;
70
+ }
71
+ if (selector === mockButton || selector === mockInput || selector === mockForm) {
72
+ return selector;
73
+ }
74
+ return mockInput;
75
+ });
76
+
77
+ global.$.each = jest.fn((obj, callback) => {
78
+ if (Array.isArray(obj)) {
79
+ obj.forEach((item, idx) => callback(idx, item));
80
+ } else {
81
+ Object.keys(obj).forEach(key => callback(key, obj[key]));
82
+ }
83
+ });
84
+ });
85
+
86
+ afterEach(() => {
87
+ delete global.$;
88
+ jest.clearAllMocks();
89
+ });
90
+
91
+ describe('init', () => {
92
+ test('should initialize form with default submit button', () => {
93
+ const onSubmitCallback = jest.fn();
94
+
95
+ const result = FormHelper.init(mockForm, onSubmitCallback);
96
+
97
+ expect(mockForm.find).toHaveBeenCalledWith('button[name="validate"]');
98
+ expect(mockButton.off).toHaveBeenCalledWith('click');
99
+ expect(mockButton.click).toHaveBeenCalled();
100
+ expect(result).toBe(mockForm);
101
+ });
102
+
103
+ test('should initialize form with custom submit button', () => {
104
+ const onSubmitCallback = jest.fn();
105
+ const customButton = { ...mockButton };
106
+
107
+ FormHelper.init(mockForm, onSubmitCallback, customButton);
108
+
109
+ expect(customButton.off).toHaveBeenCalledWith('click');
110
+ expect(customButton.click).toHaveBeenCalled();
111
+ });
112
+
113
+ test('should call callback on submit', () => {
114
+ const onSubmitCallback = jest.fn();
115
+ let clickHandler;
116
+
117
+ mockButton.click.mockImplementation((handler) => {
118
+ clickHandler = handler;
119
+ return mockButton;
120
+ });
121
+
122
+ FormHelper.init(mockForm, onSubmitCallback);
123
+
124
+ // Simulate button click
125
+ const mockEvent = { preventDefault: jest.fn() };
126
+ clickHandler.call(mockButton, mockEvent);
127
+
128
+ expect(mockButton.data).toHaveBeenCalled();
129
+ expect(onSubmitCallback).toHaveBeenCalledWith(mockForm, mockButton);
130
+ });
131
+
132
+ test('should prevent default on submit', () => {
133
+ const onSubmitCallback = jest.fn();
134
+ let clickHandler;
135
+ const mockEvent = { preventDefault: jest.fn() };
136
+
137
+ mockButton.click.mockImplementation((handler) => {
138
+ clickHandler = handler;
139
+ return mockButton;
140
+ });
141
+
142
+ FormHelper.init(mockForm, onSubmitCallback);
143
+ clickHandler.call(mockButton, { preventDefault: () => {} });
144
+
145
+ expect(onSubmitCallback).toHaveBeenCalledWith(mockForm, mockButton);
146
+ });
147
+ });
148
+
149
+ describe('reset', () => {
150
+ test('should reset form fields', () => {
151
+ mockInput.each.mockImplementation((callback) => {
152
+ callback(0, {});
153
+ return mockInput;
154
+ });
155
+
156
+ const result = FormHelper.reset(mockForm);
157
+
158
+ expect(mockInput.val).toHaveBeenCalledWith('');
159
+ expect(mockInput.off).toHaveBeenCalledWith('change');
160
+ expect(result).toBe(mockForm);
161
+ });
162
+
163
+ test('should reset form with custom submit button', () => {
164
+ const customButton = { ...mockButton };
165
+
166
+ FormHelper.reset(mockForm, customButton);
167
+
168
+ expect(mockForm.find).toHaveBeenCalled();
169
+ });
170
+ });
171
+
172
+ describe('populateForm', () => {
173
+ test('should skip null values', () => {
174
+ const data = { field1: 'value1', field2: null };
175
+
176
+ FormHelper.populateForm(mockForm, data);
177
+
178
+ // Should only process non-null values
179
+ expect(mockForm.find).toHaveBeenCalled();
180
+ });
181
+
182
+ test('should handle object values as array select', () => {
183
+ const selectMock = {
184
+ find: jest.fn().mockReturnThis(),
185
+ prop: jest.fn().mockReturnThis(),
186
+ data: jest.fn().mockReturnThis()
187
+ };
188
+
189
+ mockForm.find = jest.fn((selector) => {
190
+ if (selector.includes('employees_display_type')) return mockInput;
191
+ if (selector.includes('[]')) return selectMock;
192
+ return mockInput;
193
+ });
194
+
195
+ const data = { tags: ['tag1', 'tag2'] };
196
+
197
+ FormHelper.populateForm(mockForm, data);
198
+
199
+ expect(selectMock.find).toHaveBeenCalled();
200
+ expect(selectMock.data).toHaveBeenCalledWith('default_id', 'tag1,tag2');
201
+ });
202
+
203
+ test('should handle radio button values', () => {
204
+ mockInput.prop = jest.fn((key) => {
205
+ if (key === 'type') return 'radio';
206
+ return mockInput;
207
+ });
208
+
209
+ const data = { gender: 'male' };
210
+
211
+ FormHelper.populateForm(mockForm, data);
212
+
213
+ expect(mockInput.filter).toHaveBeenCalledWith('[value="male"]');
214
+ });
215
+
216
+ test('should handle checkbox values', () => {
217
+ mockInput.prop = jest.fn((key) => {
218
+ if (key === 'type') return 'checkbox';
219
+ return mockInput;
220
+ });
221
+
222
+ const data = { terms: 'accepted' };
223
+
224
+ FormHelper.populateForm(mockForm, data);
225
+
226
+ expect(mockInput.filter).toHaveBeenCalledWith('[value="accepted"]');
227
+ });
228
+
229
+ test('should handle regular input values', () => {
230
+ mockInput.prop = jest.fn(() => 'text');
231
+
232
+ const data = { name: 'John' };
233
+
234
+ FormHelper.populateForm(mockForm, data);
235
+
236
+ expect(mockInput.val).toHaveBeenCalled();
237
+ });
238
+ });
239
+
240
+ describe('reset', () => {
241
+ test('should reset form fields', () => {
242
+ mockInput.each.mockImplementation((callback) => {
243
+ callback(0, document.createElement('input'));
244
+ return mockInput;
245
+ });
246
+
247
+ const result = FormHelper.reset(mockForm);
248
+
249
+ expect(mockForm.find).toHaveBeenCalled();
250
+ expect(mockInput.each).toHaveBeenCalled();
251
+ expect(result).toBe(mockForm);
252
+ });
253
+
254
+ test('should reset with custom button', () => {
255
+ const customButton = { ...mockButton };
256
+ mockInput.each.mockImplementation(() => mockInput);
257
+
258
+ FormHelper.reset(mockForm, customButton);
259
+
260
+ expect(mockInput.each).toHaveBeenCalled();
261
+ });
262
+ });
263
+
264
+ describe('getFormData', () => {
265
+ test('should return FormData from form element', () => {
266
+ const result = FormHelper.getFormData(mockForm);
267
+
268
+ expect(result).toBeInstanceOf(FormData);
269
+ });
270
+ });
271
+
272
+ describe('getFormDataQueryString', () => {
273
+ test('should return serialized form data', () => {
274
+ const result = FormHelper.getFormDataQueryString(mockForm);
275
+
276
+ expect(result).toBe('field1=value1&field2=value2');
277
+ expect(mockForm.serialize).toHaveBeenCalled();
278
+ });
279
+ });
280
+
281
+ describe('setOnInputChange', () => {
282
+ beforeEach(() => {
283
+ jest.useFakeTimers();
284
+ });
285
+
286
+ afterEach(() => {
287
+ jest.useRealTimers();
288
+ });
289
+
290
+ test('should setup keyup handler with timeout', () => {
291
+ const callback = jest.fn();
292
+ let keyupHandler;
293
+
294
+ mockInput.on.mockImplementation((event, handler) => {
295
+ if (event === 'keyup') keyupHandler = handler;
296
+ return mockInput;
297
+ });
298
+
299
+ FormHelper.setOnInputChange(mockInput, callback, 500);
300
+
301
+ expect(mockInput.on).toHaveBeenCalledWith('keyup', expect.any(Function));
302
+
303
+ keyupHandler();
304
+ jest.advanceTimersByTime(500);
305
+
306
+ expect(callback).toHaveBeenCalled();
307
+ });
308
+
309
+ test('should clear timeout on keydown', () => {
310
+ const callback = jest.fn();
311
+ let keyupHandler, keydownHandler;
312
+
313
+ mockInput.on.mockImplementation((event, handler) => {
314
+ if (event === 'keyup') keyupHandler = handler;
315
+ if (event === 'keydown') keydownHandler = handler;
316
+ return mockInput;
317
+ });
318
+
319
+ FormHelper.setOnInputChange(mockInput, callback, 500);
320
+
321
+ keyupHandler();
322
+ keydownHandler();
323
+ jest.advanceTimersByTime(500);
324
+
325
+ expect(callback).not.toHaveBeenCalled();
326
+ });
327
+
328
+ test('should call callback on focusout', () => {
329
+ const callback = jest.fn();
330
+ let focusoutHandler;
331
+
332
+ mockInput.on.mockImplementation((event, handler) => {
333
+ if (event === 'focusout') focusoutHandler = handler;
334
+ return mockInput;
335
+ });
336
+
337
+ FormHelper.setOnInputChange(mockInput, callback);
338
+
339
+ focusoutHandler();
340
+
341
+ expect(callback).toHaveBeenCalled();
342
+ });
343
+
344
+ test('should use default interval of 700ms', () => {
345
+ const callback = jest.fn();
346
+ let keyupHandler;
347
+
348
+ mockInput.on.mockImplementation((event, handler) => {
349
+ if (event === 'keyup') keyupHandler = handler;
350
+ return mockInput;
351
+ });
352
+
353
+ FormHelper.setOnInputChange(mockInput, callback);
354
+
355
+ keyupHandler();
356
+ jest.advanceTimersByTime(700);
357
+
358
+ expect(callback).toHaveBeenCalled();
359
+ });
360
+ });
361
+
362
+ describe('Select methods', () => {
363
+ test('resetSelectOption should reset select options', () => {
364
+ const optionMock = {
365
+ prop: jest.fn().mockReturnThis()
366
+ };
367
+
368
+ mockForm.find = jest.fn(() => optionMock);
369
+
370
+ FormHelper.resetSelectOption(mockForm, 'category');
371
+
372
+ expect(mockForm.find).toHaveBeenCalledWith('select[name="category"] option');
373
+ expect(optionMock.prop).toHaveBeenCalledWith('disabled', false);
374
+ expect(optionMock.prop).toHaveBeenCalledWith('selected', false);
375
+ });
376
+
377
+ test('setSelectedSelectOption should select option', () => {
378
+ const optionMock = {
379
+ prop: jest.fn().mockReturnThis()
380
+ };
381
+
382
+ mockForm.find = jest.fn(() => optionMock);
383
+
384
+ FormHelper.setSelectedSelectOption(mockForm, 'category', '5');
385
+
386
+ expect(mockForm.find).toHaveBeenCalledWith('select[name="category"] option[value="5"]');
387
+ expect(optionMock.prop).toHaveBeenCalledWith('selected', true);
388
+ });
389
+
390
+ test('setSelectedSelectOptions should select multiple options', () => {
391
+ const optionMock = {
392
+ prop: jest.fn().mockReturnThis()
393
+ };
394
+
395
+ mockForm.find = jest.fn(() => optionMock);
396
+
397
+ FormHelper.setSelectedSelectOptions(mockForm, 'tags', ['1', '2', '3']);
398
+
399
+ expect(mockForm.find).toHaveBeenCalledTimes(3);
400
+ expect(optionMock.prop).toHaveBeenCalledWith('selected', true);
401
+ });
402
+
403
+ test('disableSelectOption should disable option', () => {
404
+ const optionMock = {
405
+ prop: jest.fn().mockReturnThis()
406
+ };
407
+
408
+ mockForm.find = jest.fn(() => optionMock);
409
+
410
+ FormHelper.disableSelectOption(mockForm, 'category', '5');
411
+
412
+ expect(mockForm.find).toHaveBeenCalledWith('select[name="category"] option[value="5"]');
413
+ expect(optionMock.prop).toHaveBeenCalledWith('disabled', true);
414
+ });
415
+
416
+ test('disableSelectOptions should disable multiple options', () => {
417
+ const optionMock = {
418
+ prop: jest.fn().mockReturnThis()
419
+ };
420
+
421
+ mockForm.find = jest.fn(() => optionMock);
422
+
423
+ FormHelper.disableSelectOptions(mockForm, 'category', ['1', '2']);
424
+
425
+ expect(mockForm.find).toHaveBeenCalledTimes(2);
426
+ expect(optionMock.prop).toHaveBeenCalledWith('disabled', true);
427
+ });
428
+
429
+ test('countSelectOptions should count non-disabled options', () => {
430
+ const optionMock = {
431
+ length: 5
432
+ };
433
+
434
+ mockForm.find = jest.fn(() => optionMock);
435
+
436
+ const count = FormHelper.countSelectOptions(mockForm, 'category');
437
+
438
+ expect(mockForm.find).toHaveBeenCalledWith('select[name="category"] option:not([disabled])');
439
+ expect(count).toBe(5);
440
+ });
441
+ });
442
+
443
+ describe('Checkbox methods', () => {
444
+ test('getCheckedValues should return checked values', () => {
445
+ const mockCheckboxes = {
446
+ map: jest.fn((callback) => {
447
+ const results = [
448
+ callback.call({ checked: true, value: 'option1' }),
449
+ callback.call({ checked: false, value: 'option2' }),
450
+ callback.call({ checked: true, value: 'option3' })
451
+ ];
452
+ return { get: () => results.filter(r => r !== undefined) };
453
+ })
454
+ };
455
+
456
+ const result = FormHelper.getCheckedValues(mockCheckboxes);
457
+
458
+ expect(result).toEqual(['option1', 'option3']);
459
+ });
460
+
461
+ test('setCheckedValues should check specified values', () => {
462
+ const parentMock = {
463
+ find: jest.fn().mockReturnThis(),
464
+ prop: jest.fn().mockReturnThis()
465
+ };
466
+
467
+ mockInput.parent = jest.fn(() => parentMock);
468
+
469
+ FormHelper.setCheckedValues(mockInput, ['value1', 'value2']);
470
+
471
+ expect(parentMock.find).toHaveBeenCalledWith('[value="value1"]');
472
+ expect(parentMock.find).toHaveBeenCalledWith('[value="value2"]');
473
+ expect(parentMock.prop).toHaveBeenCalledWith('checked', true);
474
+ });
475
+
476
+ test('getInputListValues should return all input values', () => {
477
+ const mockInputs = {
478
+ map: jest.fn((callback) => {
479
+ const results = [
480
+ callback.call({ value: 'value1' }),
481
+ callback.call({ value: 'value2' }),
482
+ callback.call({ value: '' })
483
+ ];
484
+ return {
485
+ get: () => results.filter(r => r.length > 0)
486
+ };
487
+ })
488
+ };
489
+
490
+ const result = FormHelper.getInputListValues(mockInputs);
491
+
492
+ expect(result).toEqual(['value1', 'value2']);
493
+ });
494
+ });
495
+
496
+ describe('initTypeFields', () => {
497
+ test('should wrap password fields with toggle button', () => {
498
+ const passwordInput = {
499
+ wrap: jest.fn().mockReturnThis(),
500
+ after: jest.fn().mockReturnThis()
501
+ };
502
+
503
+ mockForm.find = jest.fn((selector) => {
504
+ if (selector === 'input[type="password"]') return passwordInput;
505
+ if (selector.includes('input[type="date"]')) return { length: 0 };
506
+ if (selector.includes('input[type="time"]')) return { length: 0 };
507
+ return mockInput;
508
+ });
509
+
510
+ FormHelper.initTypeFields(mockForm);
511
+
512
+ expect(passwordInput.wrap).toHaveBeenCalled();
513
+ expect(passwordInput.after).toHaveBeenCalled();
514
+ });
515
+
516
+ test('should handle date inputs when Modernizr not available', () => {
517
+ const dateInput = {
518
+ css: jest.fn().mockReturnThis(),
519
+ length: 1
520
+ };
521
+
522
+ const passwordInput = {
523
+ wrap: jest.fn().mockReturnThis(),
524
+ after: jest.fn().mockReturnThis(),
525
+ length: 0
526
+ };
527
+
528
+ mockForm.find = jest.fn((selector) => {
529
+ if (selector.includes('input[type="date"]')) return dateInput;
530
+ if (selector.includes('input[type="time"]')) return { length: 0 };
531
+ if (selector === 'input[type="password"]') return passwordInput;
532
+ return mockInput;
533
+ });
534
+
535
+ global.Modernizr = {
536
+ inputtypes: {
537
+ date: false,
538
+ time: true
539
+ }
540
+ };
541
+
542
+ FormHelper.initTypeFields(mockForm);
543
+
544
+ expect(dateInput.css).toHaveBeenCalledWith('max-width', '120px');
545
+
546
+ delete global.Modernizr;
547
+ });
548
+
549
+ test('should handle time inputs when not supported', () => {
550
+ const timeInput = {
551
+ css: jest.fn().mockReturnThis(),
552
+ attr: jest.fn().mockReturnThis(),
553
+ length: 1
554
+ };
555
+
556
+ const passwordInput = {
557
+ wrap: jest.fn().mockReturnThis(),
558
+ after: jest.fn().mockReturnThis(),
559
+ length: 0
560
+ };
561
+
562
+ mockForm.find = jest.fn((selector) => {
563
+ if (selector.includes('input[type="time"]')) return timeInput;
564
+ if (selector.includes('[step="1"]')) return timeInput;
565
+ if (selector.includes('input[type="date"]')) return { length: 0 };
566
+ if (selector === 'input[type="password"]') return passwordInput;
567
+ return mockInput;
568
+ });
569
+
570
+ global.Modernizr = {
571
+ inputtypes: {
572
+ date: true,
573
+ time: false
574
+ }
575
+ };
576
+
577
+ FormHelper.initTypeFields(mockForm);
578
+
579
+ expect(timeInput.css).toHaveBeenCalledWith('max-width', '100px');
580
+ expect(timeInput.attr).toHaveBeenCalledWith('placeholder', 'hh:mm');
581
+
582
+ delete global.Modernizr;
583
+ });
584
+
585
+ test('should skip when Modernizr not defined', () => {
586
+ const passwordInput = {
587
+ wrap: jest.fn().mockReturnThis(),
588
+ after: jest.fn().mockReturnThis(),
589
+ length: 1
590
+ };
591
+
592
+ const emptyElement = {
593
+ length: 0,
594
+ css: jest.fn().mockReturnThis(),
595
+ attr: jest.fn().mockReturnThis()
596
+ };
597
+
598
+ mockForm.find = jest.fn((selector) => {
599
+ if (selector === 'input[type="password"]') return passwordInput;
600
+ return emptyElement;
601
+ });
602
+
603
+ FormHelper.initTypeFields(mockForm);
604
+
605
+ // Should still wrap password fields
606
+ expect(passwordInput.wrap).toHaveBeenCalled();
607
+ });
608
+ });
609
+
610
+ describe('hideField', () => {
611
+ test('should hide field form-group', () => {
612
+ const formGroupMock = {
613
+ addClass: jest.fn().mockReturnThis()
614
+ };
615
+
616
+ mockInput.closest = jest.fn(() => formGroupMock);
617
+
618
+ FormHelper.hideField(mockInput);
619
+
620
+ expect(mockInput.closest).toHaveBeenCalledWith('.form-group');
621
+ expect(formGroupMock.addClass).toHaveBeenCalledWith('hide');
622
+ });
623
+ });
624
+
5
625
  describe('extractErrorKeyOfJson', () => {
6
626
  test('should return null for undefined', () => {
7
627
  expect(FormHelper.extractErrorKeyOfJson(undefined)).toBeNull();
@@ -86,26 +706,6 @@ describe('FormHelper', () => {
86
706
 
87
707
  expect(result).toEqual({});
88
708
  });
89
-
90
- test('should handle multiple values for same key (last value wins)', () => {
91
- const formData = new FormData();
92
- formData.append('tag', 'tag1');
93
- formData.append('tag', 'tag2');
94
-
95
- const result = FormHelper.getDataFromFormData(formData);
96
-
97
- // FormData entries() with same key will have last value
98
- expect(result.tag).toBe('tag2');
99
- });
100
-
101
- test('should handle special characters', () => {
102
- const formData = new FormData();
103
- formData.append('field', 'value with spaces & special!');
104
-
105
- const result = FormHelper.getDataFromFormData(formData);
106
-
107
- expect(result.field).toBe('value with spaces & special!');
108
- });
109
709
  });
110
710
 
111
711
  describe('getFormErrorText', () => {
@@ -152,69 +752,167 @@ describe('FormHelper', () => {
152
752
 
153
753
  expect(result).toBe('<span>Error 1</span>');
154
754
  });
755
+ });
155
756
 
156
- test('should remove empty values', () => {
157
- const errors = {
158
- field1: 'Error 1',
159
- field2: '',
160
- field3: null,
161
- field4: 'Error 4'
757
+ describe('displayFormErrors', () => {
758
+ test('should display errors and reset button', () => {
759
+ const errors = { field: 'Error message' };
760
+ const displaySpy = jest.spyOn(FormHelper, 'displayFormErrorsFromText');
761
+ const getTextSpy = jest.spyOn(FormHelper, 'getFormErrorText').mockReturnValue('<span>Error</span>');
762
+ const buttonLoaderSpy = jest.spyOn(FormHelper, 'buttonLoader');
763
+
764
+ FormHelper.displayFormErrors(mockForm, mockButton, errors);
765
+
766
+ expect(getTextSpy).toHaveBeenCalledWith(errors);
767
+ expect(displaySpy).toHaveBeenCalledWith(mockForm, '<span>Error</span>', null);
768
+ expect(buttonLoaderSpy).toHaveBeenCalledWith(mockButton, 'reset');
769
+
770
+ displaySpy.mockRestore();
771
+ getTextSpy.mockRestore();
772
+ buttonLoaderSpy.mockRestore();
773
+ });
774
+
775
+ test('should work without button', () => {
776
+ const errors = { field: 'Error message' };
777
+ const displaySpy = jest.spyOn(FormHelper, 'displayFormErrorsFromText');
778
+ const getTextSpy = jest.spyOn(FormHelper, 'getFormErrorText').mockReturnValue('<span>Error</span>');
779
+
780
+ FormHelper.displayFormErrors(mockForm, null, errors);
781
+
782
+ expect(displaySpy).toHaveBeenCalledWith(mockForm, '<span>Error</span>', null);
783
+
784
+ displaySpy.mockRestore();
785
+ getTextSpy.mockRestore();
786
+ });
787
+
788
+ test('should use custom error wrapper', () => {
789
+ const errors = { field: 'Error message' };
790
+ const wrapper = { append: jest.fn() };
791
+ const displaySpy = jest.spyOn(FormHelper, 'displayFormErrorsFromText');
792
+ const getTextSpy = jest.spyOn(FormHelper, 'getFormErrorText').mockReturnValue('<span>Error</span>');
793
+
794
+ FormHelper.displayFormErrors(mockForm, mockButton, errors, wrapper);
795
+
796
+ expect(displaySpy).toHaveBeenCalledWith(mockForm, '<span>Error</span>', wrapper);
797
+
798
+ displaySpy.mockRestore();
799
+ getTextSpy.mockRestore();
800
+ });
801
+ });
802
+
803
+ describe('displayFormErrorsFromText', () => {
804
+ test('should append to errorWrapperDiv when provided', () => {
805
+ const wrapper = {
806
+ append: jest.fn()
162
807
  };
163
808
 
164
- const result = FormHelper.getFormErrorText(errors);
809
+ FormHelper.displayFormErrorsFromText(mockForm, 'Error text', wrapper);
165
810
 
166
- expect(result).toContain('<span>Error 1</span>');
167
- expect(result).toContain('<span>Error 4</span>');
168
- expect(result).not.toContain('<span></span>');
811
+ expect(wrapper.append).toHaveBeenCalledWith('<div class="alert alert-danger form_errors">Error text</div>');
169
812
  });
170
813
 
171
- test('should handle empty errors object', () => {
172
- const result = FormHelper.getFormErrorText({});
814
+ test('should append to .form_errors_content when exists', () => {
815
+ const errorContent = {
816
+ append: jest.fn(),
817
+ length: 1
818
+ };
173
819
 
174
- expect(result).toBe('');
820
+ mockForm.find = jest.fn((selector) => {
821
+ if (selector === '.form_errors_content') return errorContent;
822
+ return { length: 0 };
823
+ });
824
+
825
+ FormHelper.displayFormErrorsFromText(mockForm, 'Error text');
826
+
827
+ expect(errorContent.append).toHaveBeenCalled();
175
828
  });
176
829
 
177
- test('should handle mixed error formats', () => {
178
- const errors = {
179
- field1: 'Simple error',
180
- field2: { error_description: 'Described error' },
181
- field3: ['key', 'Array error']
830
+ test('should prepend to modal-body when exists', () => {
831
+ const formGroupMock = {
832
+ parent: jest.fn().mockReturnThis(),
833
+ hasClass: jest.fn(() => false),
834
+ before: jest.fn(),
835
+ length: 1
182
836
  };
183
837
 
184
- const result = FormHelper.getFormErrorText(errors);
838
+ const modalBody = {
839
+ find: jest.fn((selector) => {
840
+ if (selector === '.form-group:first') return formGroupMock;
841
+ return { length: 0 };
842
+ }),
843
+ length: 1
844
+ };
845
+
846
+ mockForm.find = jest.fn((selector) => {
847
+ if (selector === '.form_errors_content') return { length: 0 };
848
+ if (selector === '.modal-body') return modalBody;
849
+ if (selector === '.form-group:first') return { length: 0 };
850
+ return mockInput;
851
+ });
852
+
853
+ FormHelper.displayFormErrorsFromText(mockForm, 'Error text');
185
854
 
186
- expect(result).toContain('<span>Simple error</span>');
187
- expect(result).toContain('<span>Described error</span>');
188
- expect(result).toContain('<span>Array error</span>');
855
+ expect(mockForm.find).toHaveBeenCalledWith('.modal-body');
189
856
  });
190
- });
191
857
 
192
- describe('buttonLoader with jQuery mock', () => {
193
- let mockButton;
858
+ test('should insert before first form-group when exists', () => {
859
+ const formGroup = {
860
+ parent: jest.fn().mockReturnThis(),
861
+ hasClass: jest.fn(() => false),
862
+ before: jest.fn(),
863
+ length: 1
864
+ };
194
865
 
195
- beforeEach(() => {
196
- // Mock jQuery button
197
- mockButton = {
198
- attr: jest.fn().mockReturnThis(),
199
- data: jest.fn().mockReturnThis(),
200
- html: jest.fn().mockReturnThis(),
201
- addClass: jest.fn().mockReturnThis(),
202
- removeClass: jest.fn().mockReturnThis(),
866
+ mockForm.find = jest.fn((selector) => {
867
+ if (selector === '.form_errors_content') return { length: 0 };
868
+ if (selector === '.modal-body') return { length: 0 };
869
+ if (selector === '.form-group:first') return formGroup;
870
+ return mockInput;
871
+ });
872
+
873
+ FormHelper.displayFormErrorsFromText(mockForm, 'Error text');
874
+
875
+ expect(formGroup.before).toHaveBeenCalled();
876
+ });
877
+
878
+ test('should insert before row parent when form-group is in row', () => {
879
+ const parentRow = {
880
+ parent: jest.fn().mockReturnThis(),
881
+ hasClass: jest.fn(() => true),
882
+ before: jest.fn()
203
883
  };
204
884
 
205
- // Mock $ to return mockButton
206
- global.$ = jest.fn(() => mockButton);
885
+ const formGroup = {
886
+ parent: jest.fn(() => parentRow),
887
+ length: 1
888
+ };
889
+
890
+ mockForm.find = jest.fn((selector) => {
891
+ if (selector === '.form_errors_content') return { length: 0 };
892
+ if (selector === '.modal-body') return { length: 0 };
893
+ if (selector === '.form-group:first') return formGroup;
894
+ return mockInput;
895
+ });
896
+
897
+ FormHelper.displayFormErrorsFromText(mockForm, 'Error text');
898
+
899
+ expect(parentRow.before).toHaveBeenCalled();
207
900
  });
208
901
 
209
- afterEach(() => {
210
- delete global.$;
902
+ test('should prepend to form when no other location found', () => {
903
+ mockForm.find = jest.fn(() => ({ length: 0 }));
904
+ mockForm.prepend = jest.fn();
905
+
906
+ FormHelper.displayFormErrorsFromText(mockForm, 'Error text');
907
+
908
+ expect(mockForm.prepend).toHaveBeenCalledWith('<div class="alert alert-danger form_errors">Error text</div>');
211
909
  });
910
+ });
212
911
 
912
+ describe('buttonLoader', () => {
213
913
  test('should disable button on loading', () => {
214
914
  mockButton.attr.mockImplementation((key, value) => {
215
- if (key === 'disabled' && value === undefined) {
216
- return false; // button not disabled initially
217
- }
915
+ if (key === 'disabled' && value === undefined) return false;
218
916
  return mockButton;
219
917
  });
220
918
  mockButton.data.mockImplementation((key, value) => {
@@ -228,6 +926,17 @@ describe('FormHelper', () => {
228
926
  expect(mockButton.addClass).toHaveBeenCalledWith('disabled');
229
927
  });
230
928
 
929
+ test('should not process if already disabled', () => {
930
+ mockButton.attr.mockImplementation((key) => {
931
+ if (key === 'disabled') return true;
932
+ return mockButton;
933
+ });
934
+
935
+ FormHelper.buttonLoader(mockButton, 'loading');
936
+
937
+ expect(mockButton.html).not.toHaveBeenCalled();
938
+ });
939
+
231
940
  test('should enable button on reset', () => {
232
941
  mockButton.data.mockReturnValue('Original Text');
233
942
 
@@ -238,39 +947,34 @@ describe('FormHelper', () => {
238
947
  expect(mockButton.attr).toHaveBeenCalledWith('disabled', false);
239
948
  });
240
949
 
241
- test('should use custom loading text if provided', () => {
950
+ test('should use load-text when provided', () => {
242
951
  mockButton.attr.mockImplementation((key) => {
243
952
  if (key === 'disabled') return false;
244
953
  return mockButton;
245
954
  });
246
955
  mockButton.data.mockImplementation((key) => {
247
- if (key === 'loading-text') return 'Custom Loading...';
248
- return mockButton;
956
+ if (key === 'load-text') return 'Loading...';
957
+ return null;
249
958
  });
250
959
 
251
960
  FormHelper.buttonLoader(mockButton, 'start');
252
961
 
253
- expect(mockButton.html).toHaveBeenCalledWith('Custom Loading...');
962
+ expect(mockButton.html).toHaveBeenCalledWith('Loading...');
254
963
  });
255
964
 
256
- test('should handle "start" action same as "loading"', () => {
965
+ test('should use loading-text when provided', () => {
257
966
  mockButton.attr.mockImplementation((key) => {
258
967
  if (key === 'disabled') return false;
259
968
  return mockButton;
260
969
  });
261
- mockButton.data.mockReturnValue(null);
970
+ mockButton.data.mockImplementation((key) => {
971
+ if (key === 'loading-text') return 'Custom Loading...';
972
+ return null;
973
+ });
262
974
 
263
975
  FormHelper.buttonLoader(mockButton, 'start');
264
976
 
265
- expect(mockButton.attr).toHaveBeenCalledWith('disabled', true);
266
- });
267
-
268
- test('should handle "stop" action same as "reset"', () => {
269
- mockButton.data.mockReturnValue('Original');
270
-
271
- FormHelper.buttonLoader(mockButton, 'stop');
272
-
273
- expect(mockButton.attr).toHaveBeenCalledWith('disabled', false);
977
+ expect(mockButton.html).toHaveBeenCalledWith('Custom Loading...');
274
978
  });
275
979
 
276
980
  test('should return button object', () => {
@@ -295,15 +999,6 @@ describe('FormHelper', () => {
295
999
  delete global.$;
296
1000
  });
297
1001
 
298
- test('should return null for null value', () => {
299
- const mockInput = { val: jest.fn(() => null) };
300
- global.$ = jest.fn(() => mockInput);
301
-
302
- expect(FormHelper.getInputValue(mockInput)).toBeNull();
303
-
304
- delete global.$;
305
- });
306
-
307
1002
  test('should return value when present', () => {
308
1003
  const mockInput = { val: jest.fn(() => 'test value') };
309
1004
  global.$ = jest.fn(() => mockInput);
@@ -334,59 +1029,14 @@ describe('FormHelper', () => {
334
1029
 
335
1030
  expect(result).toEqual(['line1', 'line2', 'line3']);
336
1031
  });
337
-
338
- test('should filter empty lines', () => {
339
- const mockTextarea = {
340
- val: jest.fn(() => 'line1\n\nline2\n\n\nline3')
341
- };
342
-
343
- const result = FormHelper.getLinesOfTextarea(mockTextarea);
344
-
345
- expect(result).toEqual(['line1', 'line2', 'line3']);
346
- });
347
-
348
- test('should handle Mac line endings', () => {
349
- const mockTextarea = {
350
- val: jest.fn(() => 'line1\rline2\rline3')
351
- };
352
-
353
- const result = FormHelper.getLinesOfTextarea(mockTextarea);
354
-
355
- expect(result).toEqual(['line1', 'line2', 'line3']);
356
- });
357
-
358
- test('should handle empty textarea', () => {
359
- const mockTextarea = {
360
- val: jest.fn(() => '')
361
- };
362
-
363
- const result = FormHelper.getLinesOfTextarea(mockTextarea);
364
-
365
- expect(result).toEqual([]);
366
- });
367
-
368
- test('should handle single line', () => {
369
- const mockTextarea = {
370
- val: jest.fn(() => 'single line')
371
- };
372
-
373
- const result = FormHelper.getLinesOfTextarea(mockTextarea);
374
-
375
- expect(result).toEqual(['single line']);
376
- });
377
1032
  });
378
1033
 
379
1034
  describe('hideFormErrors', () => {
380
1035
  test('should remove form_errors div', () => {
381
- const mockForm = {
382
- find: jest.fn().mockReturnThis(),
383
- remove: jest.fn().mockReturnThis()
384
- };
385
-
386
1036
  const result = FormHelper.hideFormErrors(mockForm);
387
1037
 
388
1038
  expect(mockForm.find).toHaveBeenCalledWith('div.form_errors');
389
- expect(mockForm.remove).toHaveBeenCalled();
1039
+ expect(mockInput.remove).toHaveBeenCalled();
390
1040
  expect(result).toBe(mockForm);
391
1041
  });
392
1042
  });