@osimatic/helpers-js 1.5.3 → 1.5.5

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.
@@ -4,277 +4,239 @@
4
4
  require('../array'); // For removeEmptyValues method
5
5
  const { FormHelper, ArrayField, EditValue } = require('../form_helper');
6
6
 
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
- });
7
+ // Helper functions
8
+ function setupForm() {
9
+ const form = document.createElement('form');
10
+ document.body.appendChild(form);
11
+ return form;
12
+ }
13
+
14
+ function addButton(form, name = 'validate', label = 'Submit') {
15
+ const btn = document.createElement('button');
16
+ btn.type = 'submit';
17
+ btn.name = name;
18
+ btn.textContent = label;
19
+ form.appendChild(btn);
20
+ return btn;
21
+ }
22
+
23
+ function addInput(form, name, type = 'text', value = '') {
24
+ const input = document.createElement('input');
25
+ input.name = name;
26
+ input.type = type;
27
+ input.value = value;
28
+ form.appendChild(input);
29
+ return input;
30
+ }
31
+
32
+ function addSelect(form, name, options = []) {
33
+ const select = document.createElement('select');
34
+ select.name = name;
35
+ options.forEach(({ value, text, disabled }) => {
36
+ const opt = document.createElement('option');
37
+ opt.value = value;
38
+ opt.textContent = text;
39
+ if (disabled) opt.disabled = true;
40
+ select.appendChild(opt);
84
41
  });
42
+ form.appendChild(select);
43
+ return select;
44
+ }
85
45
 
86
- afterEach(() => {
87
- delete global.$;
88
- jest.clearAllMocks();
89
- });
46
+ afterEach(() => {
47
+ document.body.innerHTML = '';
48
+ jest.clearAllMocks();
49
+ });
90
50
 
51
+ describe('FormHelper', () => {
91
52
  describe('init', () => {
92
- test('should initialize form with default submit button', () => {
53
+ test('should initialize form with default submit button and return form', () => {
54
+ const form = setupForm();
55
+ addButton(form, 'validate', 'Submit');
93
56
  const onSubmitCallback = jest.fn();
94
57
 
95
- const result = FormHelper.init(mockForm, onSubmitCallback);
58
+ const result = FormHelper.init(form, onSubmitCallback);
96
59
 
97
- expect(mockForm.find).toHaveBeenCalledWith('button[name="validate"]');
98
- expect(mockButton.off).toHaveBeenCalledWith('click');
99
- expect(mockButton.click).toHaveBeenCalled();
100
- expect(result).toBe(mockForm);
60
+ expect(result).toBe(form);
101
61
  });
102
62
 
103
- test('should initialize form with custom submit button', () => {
63
+ test('should call callback on button click', () => {
64
+ const form = setupForm();
65
+ const btn = addButton(form, 'validate', 'Submit');
104
66
  const onSubmitCallback = jest.fn();
105
- const customButton = { ...mockButton };
106
67
 
107
- FormHelper.init(mockForm, onSubmitCallback, customButton);
68
+ FormHelper.init(form, onSubmitCallback);
108
69
 
109
- expect(customButton.off).toHaveBeenCalledWith('click');
110
- expect(customButton.click).toHaveBeenCalled();
70
+ btn.click();
71
+
72
+ expect(onSubmitCallback).toHaveBeenCalledWith(form, btn);
111
73
  });
112
74
 
113
- test('should call callback on submit', () => {
75
+ test('should use custom submit button when provided', () => {
76
+ const form = setupForm();
77
+ addInput(form, 'name');
78
+ const customBtn = document.createElement('button');
79
+ customBtn.type = 'button';
80
+ customBtn.name = 'custom';
81
+ customBtn.textContent = 'Custom';
82
+ document.body.appendChild(customBtn);
114
83
  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
84
 
124
- // Simulate button click
125
- const mockEvent = { preventDefault: jest.fn() };
126
- clickHandler.call(mockButton, mockEvent);
85
+ FormHelper.init(form, onSubmitCallback, customBtn);
86
+ customBtn.click();
127
87
 
128
- expect(mockButton.data).toHaveBeenCalled();
129
- expect(onSubmitCallback).toHaveBeenCalledWith(mockForm, mockButton);
88
+ expect(onSubmitCallback).toHaveBeenCalledWith(form, customBtn);
130
89
  });
131
90
 
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
- });
91
+ test('should call buttonLoader with loading on submit', () => {
92
+ const form = setupForm();
93
+ const btn = addButton(form, 'validate', 'Submit');
94
+ const loaderSpy = jest.spyOn(FormHelper, 'buttonLoader');
141
95
 
142
- FormHelper.init(mockForm, onSubmitCallback);
143
- clickHandler.call(mockButton, { preventDefault: () => {} });
96
+ FormHelper.init(form, jest.fn());
97
+ btn.click();
144
98
 
145
- expect(onSubmitCallback).toHaveBeenCalledWith(mockForm, mockButton);
99
+ expect(loaderSpy).toHaveBeenCalledWith(btn, 'loading');
100
+ loaderSpy.mockRestore();
146
101
  });
147
102
  });
148
103
 
149
104
  describe('reset', () => {
150
- test('should reset form fields', () => {
151
- mockInput.each.mockImplementation((callback) => {
152
- callback(0, {});
153
- return mockInput;
154
- });
105
+ test('should reset text input values', () => {
106
+ const form = setupForm();
107
+ addButton(form, 'validate', 'Submit');
108
+ const input = addInput(form, 'name', 'text', 'hello');
155
109
 
156
- const result = FormHelper.reset(mockForm);
110
+ const result = FormHelper.reset(form);
157
111
 
158
- expect(mockInput.val).toHaveBeenCalledWith('');
159
- expect(mockInput.off).toHaveBeenCalledWith('change');
160
- expect(result).toBe(mockForm);
112
+ expect(input.value).toBe('');
113
+ expect(result).toBe(form);
161
114
  });
162
115
 
163
- test('should reset form with custom submit button', () => {
164
- const customButton = { ...mockButton };
116
+ test('should reset textarea values', () => {
117
+ const form = setupForm();
118
+ addButton(form, 'validate', 'Submit');
119
+ const textarea = document.createElement('textarea');
120
+ textarea.name = 'bio';
121
+ textarea.value = 'some text';
122
+ form.appendChild(textarea);
165
123
 
166
- FormHelper.reset(mockForm, customButton);
124
+ FormHelper.reset(form);
167
125
 
168
- expect(mockForm.find).toHaveBeenCalled();
126
+ expect(textarea.value).toBe('');
169
127
  });
170
- });
171
128
 
172
- describe('populateForm', () => {
173
- test('should skip null values', () => {
174
- const data = { field1: 'value1', field2: null };
129
+ test('should not reset checkbox/radio inputs', () => {
130
+ const form = setupForm();
131
+ addButton(form, 'validate', 'Submit');
132
+ const checkbox = addInput(form, 'agree', 'checkbox');
133
+ checkbox.checked = true;
175
134
 
176
- FormHelper.populateForm(mockForm, data);
135
+ FormHelper.reset(form);
177
136
 
178
- // Should only process non-null values
179
- expect(mockForm.find).toHaveBeenCalled();
137
+ // checkboxes should not be cleared by reset (they match :not([type="checkbox"]) exclusion)
138
+ // The selector explicitly excludes them
139
+ expect(checkbox.checked).toBe(true);
180
140
  });
181
141
 
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'] };
142
+ test('should reset with custom submit button', () => {
143
+ const form = setupForm();
144
+ const customBtn = document.createElement('button');
145
+ customBtn.type = 'button';
146
+ customBtn.innerHTML = 'Save';
147
+ document.body.appendChild(customBtn);
148
+ const input = addInput(form, 'name', 'text', 'hello');
196
149
 
197
- FormHelper.populateForm(mockForm, data);
150
+ FormHelper.reset(form, customBtn);
198
151
 
199
- expect(selectMock.find).toHaveBeenCalled();
200
- expect(selectMock.data).toHaveBeenCalledWith('default_id', 'tag1,tag2');
152
+ expect(input.value).toBe('');
201
153
  });
154
+ });
202
155
 
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' };
156
+ describe('populateForm', () => {
157
+ test('should skip null values', () => {
158
+ const form = setupForm();
159
+ const input = addInput(form, 'field1', 'text', 'original');
210
160
 
211
- FormHelper.populateForm(mockForm, data);
161
+ FormHelper.populateForm(form, { field1: null });
212
162
 
213
- expect(mockInput.filter).toHaveBeenCalledWith('[value="male"]');
163
+ expect(input.value).toBe('original');
214
164
  });
215
165
 
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' };
166
+ test('should set text input value', () => {
167
+ const form = setupForm();
168
+ const input = addInput(form, 'name', 'text', '');
223
169
 
224
- FormHelper.populateForm(mockForm, data);
170
+ FormHelper.populateForm(form, { name: 'John' });
225
171
 
226
- expect(mockInput.filter).toHaveBeenCalledWith('[value="accepted"]');
172
+ expect(input.value).toBe('John');
227
173
  });
228
174
 
229
- test('should handle regular input values', () => {
230
- mockInput.prop = jest.fn(() => 'text');
231
-
232
- const data = { name: 'John' };
175
+ test('should handle radio button values', () => {
176
+ const form = setupForm();
177
+ const radio1 = addInput(form, 'gender', 'radio', 'male');
178
+ const radio2 = addInput(form, 'gender', 'radio', 'female');
233
179
 
234
- FormHelper.populateForm(mockForm, data);
180
+ FormHelper.populateForm(form, { gender: 'female' });
235
181
 
236
- expect(mockInput.val).toHaveBeenCalled();
182
+ expect(radio1.checked).toBe(false);
183
+ expect(radio2.checked).toBe(true);
237
184
  });
238
- });
239
185
 
240
- describe('reset', () => {
241
- test('should reset form fields', () => {
242
- mockInput.each.mockImplementation((callback) => {
243
- callback(0, document.createElement('input'));
244
- return mockInput;
245
- });
186
+ test('should handle checkbox values', () => {
187
+ const form = setupForm();
188
+ const cb1 = addInput(form, 'terms', 'checkbox', 'accepted');
189
+ const cb2 = addInput(form, 'terms', 'checkbox', 'declined');
246
190
 
247
- const result = FormHelper.reset(mockForm);
191
+ FormHelper.populateForm(form, { terms: 'accepted' });
248
192
 
249
- expect(mockForm.find).toHaveBeenCalled();
250
- expect(mockInput.each).toHaveBeenCalled();
251
- expect(result).toBe(mockForm);
193
+ expect(cb1.checked).toBe(true);
194
+ expect(cb2.checked).toBe(false);
252
195
  });
253
196
 
254
- test('should reset with custom button', () => {
255
- const customButton = { ...mockButton };
256
- mockInput.each.mockImplementation(() => mockInput);
197
+ test('should handle object values as array select', () => {
198
+ const form = setupForm();
199
+ const select = document.createElement('select');
200
+ select.name = 'tags[]';
201
+ select.multiple = true;
202
+ ['tag1', 'tag2', 'tag3'].forEach(v => {
203
+ const opt = document.createElement('option');
204
+ opt.value = v;
205
+ opt.textContent = v;
206
+ select.appendChild(opt);
207
+ });
208
+ form.appendChild(select);
257
209
 
258
- FormHelper.reset(mockForm, customButton);
210
+ FormHelper.populateForm(form, { tags: ['tag1', 'tag3'] });
259
211
 
260
- expect(mockInput.each).toHaveBeenCalled();
212
+ expect(select.querySelector('option[value="tag1"]').selected).toBe(true);
213
+ expect(select.querySelector('option[value="tag2"]').selected).toBe(false);
214
+ expect(select.querySelector('option[value="tag3"]').selected).toBe(true);
215
+ expect(select.dataset.default_id).toBe('tag1,tag3');
261
216
  });
262
217
  });
263
218
 
264
219
  describe('getFormData', () => {
265
- test('should return FormData from form element', () => {
266
- const result = FormHelper.getFormData(mockForm);
220
+ test('should return FormData from native form element', () => {
221
+ const form = setupForm();
222
+ addInput(form, 'name', 'text', 'John');
223
+
224
+ const result = FormHelper.getFormData(form);
267
225
 
268
226
  expect(result).toBeInstanceOf(FormData);
269
227
  });
270
228
  });
271
229
 
272
230
  describe('getFormDataQueryString', () => {
273
- test('should return serialized form data', () => {
274
- const result = FormHelper.getFormDataQueryString(mockForm);
231
+ test('should return query string from form data', () => {
232
+ const form = setupForm();
233
+ addInput(form, 'name', 'text', 'John');
234
+ addInput(form, 'age', 'text', '30');
275
235
 
276
- expect(result).toBe('field1=value1&field2=value2');
277
- expect(mockForm.serialize).toHaveBeenCalled();
236
+ const result = FormHelper.getFormDataQueryString(form);
237
+
238
+ expect(result).toContain('name=John');
239
+ expect(result).toContain('age=30');
278
240
  });
279
241
  });
280
242
 
@@ -287,338 +249,491 @@ describe('FormHelper', () => {
287
249
  jest.useRealTimers();
288
250
  });
289
251
 
290
- test('should setup keyup handler with timeout', () => {
252
+ test('should call callback after keyup + timeout', () => {
253
+ const form = setupForm();
254
+ const input = addInput(form, 'search');
291
255
  const callback = jest.fn();
292
- let keyupHandler;
293
256
 
294
- mockInput.on.mockImplementation((event, handler) => {
295
- if (event === 'keyup') keyupHandler = handler;
296
- return mockInput;
297
- });
257
+ FormHelper.setOnInputChange(input, callback, 500);
298
258
 
299
- FormHelper.setOnInputChange(mockInput, callback, 500);
300
-
301
- expect(mockInput.on).toHaveBeenCalledWith('keyup', expect.any(Function));
302
-
303
- keyupHandler();
259
+ input.dispatchEvent(new KeyboardEvent('keyup'));
304
260
  jest.advanceTimersByTime(500);
305
261
 
306
262
  expect(callback).toHaveBeenCalled();
307
263
  });
308
264
 
309
- test('should clear timeout on keydown', () => {
265
+ test('should clear timeout on keydown after keyup', () => {
266
+ const form = setupForm();
267
+ const input = addInput(form, 'search');
310
268
  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
269
 
319
- FormHelper.setOnInputChange(mockInput, callback, 500);
270
+ FormHelper.setOnInputChange(input, callback, 500);
320
271
 
321
- keyupHandler();
322
- keydownHandler();
272
+ input.dispatchEvent(new KeyboardEvent('keyup'));
273
+ input.dispatchEvent(new KeyboardEvent('keydown'));
323
274
  jest.advanceTimersByTime(500);
324
275
 
325
276
  expect(callback).not.toHaveBeenCalled();
326
277
  });
327
278
 
328
- test('should call callback on focusout', () => {
279
+ test('should call callback immediately on focusout', () => {
280
+ const form = setupForm();
281
+ const input = addInput(form, 'search');
329
282
  const callback = jest.fn();
330
- let focusoutHandler;
331
283
 
332
- mockInput.on.mockImplementation((event, handler) => {
333
- if (event === 'focusout') focusoutHandler = handler;
334
- return mockInput;
335
- });
336
-
337
- FormHelper.setOnInputChange(mockInput, callback);
284
+ FormHelper.setOnInputChange(input, callback);
338
285
 
339
- focusoutHandler();
286
+ input.dispatchEvent(new Event('focusout'));
340
287
 
341
288
  expect(callback).toHaveBeenCalled();
342
289
  });
343
290
 
344
291
  test('should use default interval of 700ms', () => {
292
+ const form = setupForm();
293
+ const input = addInput(form, 'search');
345
294
  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
295
 
355
- keyupHandler();
356
- jest.advanceTimersByTime(700);
296
+ FormHelper.setOnInputChange(input, callback);
357
297
 
298
+ input.dispatchEvent(new KeyboardEvent('keyup'));
299
+ jest.advanceTimersByTime(699);
300
+ expect(callback).not.toHaveBeenCalled();
301
+ jest.advanceTimersByTime(1);
358
302
  expect(callback).toHaveBeenCalled();
359
303
  });
360
304
  });
361
305
 
362
306
  describe('Select methods', () => {
363
- test('resetSelectOption should reset select options', () => {
364
- const optionMock = {
365
- prop: jest.fn().mockReturnThis()
366
- };
307
+ test('resetSelectOption should reset all options', () => {
308
+ const form = setupForm();
309
+ const select = addSelect(form, 'category', [
310
+ { value: '', text: '-- choose --' },
311
+ { value: '1', text: 'One' },
312
+ { value: '2', text: 'Two', disabled: true },
313
+ ]);
314
+ select.querySelector('option[value="1"]').selected = true;
315
+ select.querySelector('option[value="2"]').disabled = true;
367
316
 
368
- mockForm.find = jest.fn(() => optionMock);
317
+ FormHelper.resetSelectOption(form, 'category');
369
318
 
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);
319
+ expect(select.querySelector('option[value="1"]').disabled).toBe(false);
320
+ expect(select.querySelector('option[value="1"]').selected).toBe(false);
321
+ expect(select.querySelector('option[value="2"]').disabled).toBe(false);
375
322
  });
376
323
 
377
- test('setSelectedSelectOption should select option', () => {
378
- const optionMock = {
379
- prop: jest.fn().mockReturnThis()
380
- };
381
-
382
- mockForm.find = jest.fn(() => optionMock);
324
+ test('setSelectedSelectOption should select specific option', () => {
325
+ const form = setupForm();
326
+ addSelect(form, 'category', [
327
+ { value: '1', text: 'One' },
328
+ { value: '5', text: 'Five' },
329
+ ]);
383
330
 
384
- FormHelper.setSelectedSelectOption(mockForm, 'category', '5');
331
+ FormHelper.setSelectedSelectOption(form, 'category', '5');
385
332
 
386
- expect(mockForm.find).toHaveBeenCalledWith('select[name="category"] option[value="5"]');
387
- expect(optionMock.prop).toHaveBeenCalledWith('selected', true);
333
+ expect(form.querySelector('select[name="category"] option[value="5"]').selected).toBe(true);
388
334
  });
389
335
 
390
336
  test('setSelectedSelectOptions should select multiple options', () => {
391
- const optionMock = {
392
- prop: jest.fn().mockReturnThis()
393
- };
394
-
395
- mockForm.find = jest.fn(() => optionMock);
337
+ const form = setupForm();
338
+ const select = document.createElement('select');
339
+ select.name = 'tags';
340
+ select.multiple = true;
341
+ ['1', '2', '3'].forEach(v => {
342
+ const opt = document.createElement('option');
343
+ opt.value = v;
344
+ select.appendChild(opt);
345
+ });
346
+ form.appendChild(select);
396
347
 
397
- FormHelper.setSelectedSelectOptions(mockForm, 'tags', ['1', '2', '3']);
348
+ FormHelper.setSelectedSelectOptions(form, 'tags', ['1', '3']);
398
349
 
399
- expect(mockForm.find).toHaveBeenCalledTimes(3);
400
- expect(optionMock.prop).toHaveBeenCalledWith('selected', true);
350
+ expect(select.querySelector('option[value="1"]').selected).toBe(true);
351
+ expect(select.querySelector('option[value="2"]').selected).toBe(false);
352
+ expect(select.querySelector('option[value="3"]').selected).toBe(true);
401
353
  });
402
354
 
403
- test('disableSelectOption should disable option', () => {
404
- const optionMock = {
405
- prop: jest.fn().mockReturnThis()
406
- };
355
+ test('disableSelectOption should disable specific option', () => {
356
+ const form = setupForm();
357
+ addSelect(form, 'category', [
358
+ { value: '1', text: 'One' },
359
+ { value: '5', text: 'Five' },
360
+ ]);
407
361
 
408
- mockForm.find = jest.fn(() => optionMock);
362
+ FormHelper.disableSelectOption(form, 'category', '5');
409
363
 
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);
364
+ expect(form.querySelector('select[name="category"] option[value="5"]').disabled).toBe(true);
365
+ expect(form.querySelector('select[name="category"] option[value="1"]').disabled).toBe(false);
414
366
  });
415
367
 
416
368
  test('disableSelectOptions should disable multiple options', () => {
417
- const optionMock = {
418
- prop: jest.fn().mockReturnThis()
419
- };
369
+ const form = setupForm();
370
+ addSelect(form, 'category', [
371
+ { value: '1', text: 'One' },
372
+ { value: '2', text: 'Two' },
373
+ { value: '3', text: 'Three' },
374
+ ]);
420
375
 
421
- mockForm.find = jest.fn(() => optionMock);
376
+ FormHelper.disableSelectOptions(form, 'category', ['1', '2']);
422
377
 
423
- FormHelper.disableSelectOptions(mockForm, 'category', ['1', '2']);
424
-
425
- expect(mockForm.find).toHaveBeenCalledTimes(2);
426
- expect(optionMock.prop).toHaveBeenCalledWith('disabled', true);
378
+ expect(form.querySelector('option[value="1"]').disabled).toBe(true);
379
+ expect(form.querySelector('option[value="2"]').disabled).toBe(true);
380
+ expect(form.querySelector('option[value="3"]').disabled).toBe(false);
427
381
  });
428
382
 
429
383
  test('countSelectOptions should count non-disabled options', () => {
430
- const optionMock = {
431
- length: 5
432
- };
384
+ const form = setupForm();
385
+ addSelect(form, 'category', [
386
+ { value: '1', text: 'One' },
387
+ { value: '2', text: 'Two', disabled: true },
388
+ { value: '3', text: 'Three' },
389
+ { value: '4', text: 'Four', disabled: true },
390
+ { value: '5', text: 'Five' },
391
+ ]);
433
392
 
434
- mockForm.find = jest.fn(() => optionMock);
393
+ const count = FormHelper.countSelectOptions(form, 'category');
435
394
 
436
- const count = FormHelper.countSelectOptions(mockForm, 'category');
437
-
438
- expect(mockForm.find).toHaveBeenCalledWith('select[name="category"] option:not([disabled])');
439
- expect(count).toBe(5);
395
+ expect(count).toBe(3);
440
396
  });
441
397
  });
442
398
 
443
399
  describe('Checkbox methods', () => {
444
400
  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
- };
401
+ const form = setupForm();
402
+ const cb1 = addInput(form, 'opt', 'checkbox', 'option1');
403
+ const cb2 = addInput(form, 'opt', 'checkbox', 'option2');
404
+ const cb3 = addInput(form, 'opt', 'checkbox', 'option3');
405
+ cb1.checked = true;
406
+ cb3.checked = true;
455
407
 
456
- const result = FormHelper.getCheckedValues(mockCheckboxes);
408
+ const result = FormHelper.getCheckedValues(form.querySelectorAll('[name="opt"]'));
457
409
 
458
410
  expect(result).toEqual(['option1', 'option3']);
459
411
  });
460
412
 
461
413
  test('setCheckedValues should check specified values', () => {
462
- const parentMock = {
463
- find: jest.fn().mockReturnThis(),
464
- prop: jest.fn().mockReturnThis()
465
- };
414
+ const form = setupForm();
415
+ const container = document.createElement('div');
416
+ form.appendChild(container);
417
+ const cb1 = document.createElement('input');
418
+ cb1.type = 'checkbox'; cb1.value = 'value1';
419
+ const cb2 = document.createElement('input');
420
+ cb2.type = 'checkbox'; cb2.value = 'value2';
421
+ const cb3 = document.createElement('input');
422
+ cb3.type = 'checkbox'; cb3.value = 'value3';
423
+ container.appendChild(cb1);
424
+ container.appendChild(cb2);
425
+ container.appendChild(cb3);
426
+
427
+ FormHelper.setCheckedValues(container.querySelectorAll('input'), ['value1', 'value3']);
428
+
429
+ expect(cb1.checked).toBe(true);
430
+ expect(cb2.checked).toBe(false);
431
+ expect(cb3.checked).toBe(true);
432
+ });
433
+
434
+ test('getInputListValues should return non-empty input values', () => {
435
+ const form = setupForm();
436
+ const i1 = addInput(form, 'item', 'text', 'value1');
437
+ const i2 = addInput(form, 'item', 'text', 'value2');
438
+ const i3 = addInput(form, 'item', 'text', '');
439
+
440
+ const result = FormHelper.getInputListValues(form.querySelectorAll('[name="item"]'));
466
441
 
467
- mockInput.parent = jest.fn(() => parentMock);
442
+ expect(result).toEqual(['value1', 'value2']);
443
+ });
444
+ });
445
+
446
+ describe('initTypeFields', () => {
447
+ test('should wrap password fields with input-group.password', () => {
448
+ const form = setupForm();
449
+ const input = addInput(form, 'password', 'password');
468
450
 
469
- FormHelper.setCheckedValues(mockInput, ['value1', 'value2']);
451
+ FormHelper.initTypeFields(form);
470
452
 
471
- expect(parentMock.find).toHaveBeenCalledWith('[value="value1"]');
472
- expect(parentMock.find).toHaveBeenCalledWith('[value="value2"]');
473
- expect(parentMock.prop).toHaveBeenCalledWith('checked', true);
453
+ const wrapper = form.querySelector('.input-group.password');
454
+ expect(wrapper).not.toBeNull();
455
+ expect(wrapper.querySelector('input[type="password"]')).not.toBeNull();
474
456
  });
475
457
 
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
- };
458
+ test('should add toggle button span inside wrapper', () => {
459
+ const form = setupForm();
460
+ addInput(form, 'password', 'password');
489
461
 
490
- const result = FormHelper.getInputListValues(mockInputs);
462
+ FormHelper.initTypeFields(form);
491
463
 
492
- expect(result).toEqual(['value1', 'value2']);
464
+ const span = form.querySelector('.input-group.password .input-group-text');
465
+ expect(span).not.toBeNull();
466
+ expect(span.querySelector('i.fa-eye')).not.toBeNull();
467
+ });
468
+
469
+ test('should toggle password visibility on icon click', () => {
470
+ const form = setupForm();
471
+ addInput(form, 'password', 'password');
472
+
473
+ FormHelper.initTypeFields(form);
474
+
475
+ const icon = form.querySelector('.input-group.password i.fa-eye');
476
+ const passwordInput = form.querySelector('.input-group.password input');
477
+
478
+ expect(passwordInput.type).toBe('password');
479
+ icon.click();
480
+ expect(passwordInput.type).toBe('text');
481
+ icon.click();
482
+ expect(passwordInput.type).toBe('password');
483
+ });
484
+
485
+ test('should set max-width on date inputs when Modernizr.inputtypes.date is false', () => {
486
+ const form = setupForm();
487
+ const dateInput = addInput(form, 'mydate', 'date');
488
+ global.Modernizr = { inputtypes: { date: false, time: true } };
489
+
490
+ FormHelper.initTypeFields(form);
491
+
492
+ expect(dateInput.style.maxWidth).toBe('120px');
493
+ delete global.Modernizr;
494
+ });
495
+
496
+ test('should set placeholder on time inputs when Modernizr.inputtypes.time is false', () => {
497
+ const form = setupForm();
498
+ const timeInput = addInput(form, 'mytime', 'time');
499
+ global.Modernizr = { inputtypes: { date: true, time: false } };
500
+
501
+ FormHelper.initTypeFields(form);
502
+
503
+ expect(timeInput.style.maxWidth).toBe('100px');
504
+ expect(timeInput.placeholder).toBe('hh:mm');
505
+ delete global.Modernizr;
506
+ });
507
+
508
+ test('should skip Modernizr block when Modernizr not defined', () => {
509
+ const form = setupForm();
510
+ addInput(form, 'password', 'password');
511
+
512
+ // Should not throw when Modernizr is undefined
513
+ expect(() => FormHelper.initTypeFields(form)).not.toThrow();
493
514
  });
494
515
  });
495
516
 
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
- };
517
+ describe('hideField', () => {
518
+ test('should add hide class to closest .form-group', () => {
519
+ const form = setupForm();
520
+ const formGroup = document.createElement('div');
521
+ formGroup.className = 'form-group';
522
+ form.appendChild(formGroup);
523
+ const input = document.createElement('input');
524
+ formGroup.appendChild(input);
502
525
 
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
- });
526
+ FormHelper.hideField(input);
527
+
528
+ expect(formGroup.classList.contains('hide')).toBe(true);
529
+ });
530
+ });
531
+
532
+ describe('hideFormErrors', () => {
533
+ test('should remove all div.form_errors elements', () => {
534
+ const form = setupForm();
535
+ const err1 = document.createElement('div');
536
+ err1.className = 'form_errors';
537
+ const err2 = document.createElement('div');
538
+ err2.className = 'form_errors';
539
+ form.appendChild(err1);
540
+ form.appendChild(err2);
509
541
 
510
- FormHelper.initTypeFields(mockForm);
542
+ const result = FormHelper.hideFormErrors(form);
511
543
 
512
- expect(passwordInput.wrap).toHaveBeenCalled();
513
- expect(passwordInput.after).toHaveBeenCalled();
544
+ expect(form.querySelectorAll('div.form_errors').length).toBe(0);
545
+ expect(result).toBe(form);
514
546
  });
547
+ });
515
548
 
516
- test('should handle date inputs when Modernizr not available', () => {
517
- const dateInput = {
518
- css: jest.fn().mockReturnThis(),
519
- length: 1
520
- };
549
+ describe('displayFormErrorsFromText', () => {
550
+ test('should insert into errorWrapperDiv when provided', () => {
551
+ const form = setupForm();
552
+ const wrapper = document.createElement('div');
553
+ document.body.appendChild(wrapper);
521
554
 
522
- const passwordInput = {
523
- wrap: jest.fn().mockReturnThis(),
524
- after: jest.fn().mockReturnThis(),
525
- length: 0
526
- };
555
+ FormHelper.displayFormErrorsFromText(form, 'Error text', wrapper);
527
556
 
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
- });
557
+ expect(wrapper.querySelector('.form_errors')).not.toBeNull();
558
+ expect(wrapper.querySelector('.form_errors').textContent).toBe('Error text');
559
+ });
534
560
 
535
- global.Modernizr = {
536
- inputtypes: {
537
- date: false,
538
- time: true
539
- }
540
- };
561
+ test('should insert into .form_errors_content when exists', () => {
562
+ const form = setupForm();
563
+ const container = document.createElement('div');
564
+ container.className = 'form_errors_content';
565
+ form.appendChild(container);
541
566
 
542
- FormHelper.initTypeFields(mockForm);
567
+ FormHelper.displayFormErrorsFromText(form, 'Error text');
543
568
 
544
- expect(dateInput.css).toHaveBeenCalledWith('max-width', '120px');
569
+ expect(container.querySelector('.form_errors')).not.toBeNull();
570
+ });
545
571
 
546
- delete global.Modernizr;
572
+ test('should insert before first .form-group when present', () => {
573
+ const form = setupForm();
574
+ const fg = document.createElement('div');
575
+ fg.className = 'form-group';
576
+ form.appendChild(fg);
577
+
578
+ FormHelper.displayFormErrorsFromText(form, 'Error text');
579
+
580
+ const errors = form.querySelector('.form_errors');
581
+ expect(errors).not.toBeNull();
582
+ // The error div should be before the form-group
583
+ expect(form.children[0].classList.contains('form_errors')).toBe(true);
547
584
  });
548
585
 
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
- };
586
+ test('should insert before row grandparent when form-group is inside a .row', () => {
587
+ const form = setupForm();
588
+ const row = document.createElement('div');
589
+ row.className = 'row';
590
+ form.appendChild(row);
591
+ const col = document.createElement('div');
592
+ col.className = 'col';
593
+ row.appendChild(col);
594
+ const fg = document.createElement('div');
595
+ fg.className = 'form-group';
596
+ col.appendChild(fg);
555
597
 
556
- const passwordInput = {
557
- wrap: jest.fn().mockReturnThis(),
558
- after: jest.fn().mockReturnThis(),
559
- length: 0
560
- };
598
+ FormHelper.displayFormErrorsFromText(form, 'Error text');
561
599
 
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
- });
600
+ // Error should be inserted before the .row
601
+ const errors = form.querySelector('.form_errors');
602
+ expect(errors).not.toBeNull();
603
+ expect(form.children[0].classList.contains('form_errors')).toBe(true);
604
+ });
569
605
 
570
- global.Modernizr = {
571
- inputtypes: {
572
- date: true,
573
- time: false
574
- }
575
- };
606
+ test('should prepend to form when no other location found', () => {
607
+ const form = setupForm();
576
608
 
577
- FormHelper.initTypeFields(mockForm);
609
+ FormHelper.displayFormErrorsFromText(form, 'Error text');
578
610
 
579
- expect(timeInput.css).toHaveBeenCalledWith('max-width', '100px');
580
- expect(timeInput.attr).toHaveBeenCalledWith('placeholder', 'hh:mm');
611
+ expect(form.children[0].classList.contains('form_errors')).toBe(true);
612
+ });
581
613
 
582
- delete global.Modernizr;
614
+ test('should place errors inside modal-body when present', () => {
615
+ const form = setupForm();
616
+ const modalBody = document.createElement('div');
617
+ modalBody.className = 'modal-body';
618
+ form.appendChild(modalBody);
619
+
620
+ FormHelper.displayFormErrorsFromText(form, 'Error text');
621
+
622
+ // The form_errors should be inside modal-body (prepended)
623
+ expect(modalBody.querySelector('.form_errors')).not.toBeNull();
583
624
  });
625
+ });
584
626
 
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
- };
627
+ describe('buttonLoader', () => {
628
+ test('should disable button and set loading text on loading action', () => {
629
+ const form = setupForm();
630
+ const btn = addButton(form, 'validate', 'Submit');
591
631
 
592
- const emptyElement = {
593
- length: 0,
594
- css: jest.fn().mockReturnThis(),
595
- attr: jest.fn().mockReturnThis()
596
- };
632
+ FormHelper.buttonLoader(btn, 'loading');
597
633
 
598
- mockForm.find = jest.fn((selector) => {
599
- if (selector === 'input[type="password"]') return passwordInput;
600
- return emptyElement;
601
- });
634
+ expect(btn.disabled).toBe(true);
635
+ expect(btn.classList.contains('disabled')).toBe(true);
636
+ expect(btn.innerHTML).toContain('fa-spin');
637
+ });
638
+
639
+ test('should not process if already disabled', () => {
640
+ const form = setupForm();
641
+ const btn = addButton(form, 'validate', 'Submit');
642
+ btn.disabled = true;
643
+ const originalHTML = btn.innerHTML;
602
644
 
603
- FormHelper.initTypeFields(mockForm);
645
+ FormHelper.buttonLoader(btn, 'loading');
604
646
 
605
- // Should still wrap password fields
606
- expect(passwordInput.wrap).toHaveBeenCalled();
647
+ expect(btn.innerHTML).toBe(originalHTML);
648
+ });
649
+
650
+ test('should restore original text on reset', () => {
651
+ const form = setupForm();
652
+ const btn = addButton(form, 'validate', 'Submit');
653
+
654
+ FormHelper.buttonLoader(btn, 'loading');
655
+ FormHelper.buttonLoader(btn, 'reset');
656
+
657
+ expect(btn.innerHTML).toBe('Submit');
658
+ expect(btn.disabled).toBe(false);
659
+ expect(btn.classList.contains('disabled')).toBe(false);
660
+ });
661
+
662
+ test('should use data-load-text when provided', () => {
663
+ const form = setupForm();
664
+ const btn = addButton(form, 'validate', 'Submit');
665
+ btn.dataset.loadText = 'Loading...';
666
+
667
+ FormHelper.buttonLoader(btn, 'start');
668
+
669
+ expect(btn.innerHTML).toBe('Loading...');
670
+ });
671
+
672
+ test('should use data-loading-text when provided', () => {
673
+ const form = setupForm();
674
+ const btn = addButton(form, 'validate', 'Submit');
675
+ btn.dataset.loadingText = 'Custom...';
676
+
677
+ FormHelper.buttonLoader(btn, 'start');
678
+
679
+ expect(btn.innerHTML).toBe('Custom...');
680
+ });
681
+
682
+ test('should return the button element', () => {
683
+ const form = setupForm();
684
+ const btn = addButton(form, 'validate', 'Submit');
685
+
686
+ const result = FormHelper.buttonLoader(btn, 'reset');
687
+
688
+ expect(result).toBe(btn);
607
689
  });
608
690
  });
609
691
 
610
- describe('hideField', () => {
611
- test('should hide field form-group', () => {
612
- const formGroupMock = {
613
- addClass: jest.fn().mockReturnThis()
614
- };
692
+ describe('getInputValue', () => {
693
+ test('should return null for undefined input', () => {
694
+ expect(FormHelper.getInputValue(undefined)).toBeNull();
695
+ });
696
+
697
+ test('should return null for null input', () => {
698
+ expect(FormHelper.getInputValue(null)).toBeNull();
699
+ });
700
+
701
+ test('should return null for empty string value', () => {
702
+ const form = setupForm();
703
+ const input = addInput(form, 'name', 'text', '');
704
+
705
+ expect(FormHelper.getInputValue(input)).toBeNull();
706
+ });
707
+
708
+ test('should return value when present', () => {
709
+ const form = setupForm();
710
+ const input = addInput(form, 'name', 'text', 'test value');
711
+
712
+ expect(FormHelper.getInputValue(input)).toBe('test value');
713
+ });
714
+ });
715
+
716
+ describe('getLinesOfTextarea', () => {
717
+ test('should split textarea by newlines and filter empty', () => {
718
+ const form = setupForm();
719
+ const textarea = document.createElement('textarea');
720
+ textarea.value = 'line1\nline2\nline3';
721
+ form.appendChild(textarea);
722
+
723
+ const result = FormHelper.getLinesOfTextarea(textarea);
615
724
 
616
- mockInput.closest = jest.fn(() => formGroupMock);
725
+ expect(result).toEqual(['line1', 'line2', 'line3']);
726
+ });
727
+
728
+ test('should handle Windows line endings', () => {
729
+ const form = setupForm();
730
+ const textarea = document.createElement('textarea');
731
+ textarea.value = 'line1\r\nline2\r\nline3';
732
+ form.appendChild(textarea);
617
733
 
618
- FormHelper.hideField(mockInput);
734
+ const result = FormHelper.getLinesOfTextarea(textarea);
619
735
 
620
- expect(mockInput.closest).toHaveBeenCalledWith('.form-group');
621
- expect(formGroupMock.addClass).toHaveBeenCalledWith('hide');
736
+ expect(result).toEqual(['line1', 'line2', 'line3']);
622
737
  });
623
738
  });
624
739
 
@@ -756,16 +871,18 @@ describe('FormHelper', () => {
756
871
 
757
872
  describe('displayFormErrors', () => {
758
873
  test('should display errors and reset button', () => {
874
+ const form = setupForm();
875
+ const btn = addButton(form, 'validate', 'Submit');
759
876
  const errors = { field: 'Error message' };
760
877
  const displaySpy = jest.spyOn(FormHelper, 'displayFormErrorsFromText');
761
878
  const getTextSpy = jest.spyOn(FormHelper, 'getFormErrorText').mockReturnValue('<span>Error</span>');
762
879
  const buttonLoaderSpy = jest.spyOn(FormHelper, 'buttonLoader');
763
880
 
764
- FormHelper.displayFormErrors(mockForm, mockButton, errors);
881
+ FormHelper.displayFormErrors(form, btn, errors);
765
882
 
766
883
  expect(getTextSpy).toHaveBeenCalledWith(errors);
767
- expect(displaySpy).toHaveBeenCalledWith(mockForm, '<span>Error</span>', null);
768
- expect(buttonLoaderSpy).toHaveBeenCalledWith(mockButton, 'reset');
884
+ expect(displaySpy).toHaveBeenCalledWith(form, '<span>Error</span>', null);
885
+ expect(buttonLoaderSpy).toHaveBeenCalledWith(btn, 'reset');
769
886
 
770
887
  displaySpy.mockRestore();
771
888
  getTextSpy.mockRestore();
@@ -773,273 +890,36 @@ describe('FormHelper', () => {
773
890
  });
774
891
 
775
892
  test('should work without button', () => {
893
+ const form = setupForm();
776
894
  const errors = { field: 'Error message' };
777
895
  const displaySpy = jest.spyOn(FormHelper, 'displayFormErrorsFromText');
778
896
  const getTextSpy = jest.spyOn(FormHelper, 'getFormErrorText').mockReturnValue('<span>Error</span>');
779
897
 
780
- FormHelper.displayFormErrors(mockForm, null, errors);
898
+ FormHelper.displayFormErrors(form, null, errors);
781
899
 
782
- expect(displaySpy).toHaveBeenCalledWith(mockForm, '<span>Error</span>', null);
900
+ expect(displaySpy).toHaveBeenCalledWith(form, '<span>Error</span>', null);
783
901
 
784
902
  displaySpy.mockRestore();
785
903
  getTextSpy.mockRestore();
786
904
  });
787
905
 
788
906
  test('should use custom error wrapper', () => {
907
+ const form = setupForm();
908
+ const btn = addButton(form, 'validate', 'Submit');
789
909
  const errors = { field: 'Error message' };
790
- const wrapper = { append: jest.fn() };
910
+ const wrapper = document.createElement('div');
911
+ document.body.appendChild(wrapper);
791
912
  const displaySpy = jest.spyOn(FormHelper, 'displayFormErrorsFromText');
792
913
  const getTextSpy = jest.spyOn(FormHelper, 'getFormErrorText').mockReturnValue('<span>Error</span>');
793
914
 
794
- FormHelper.displayFormErrors(mockForm, mockButton, errors, wrapper);
915
+ FormHelper.displayFormErrors(form, btn, errors, wrapper);
795
916
 
796
- expect(displaySpy).toHaveBeenCalledWith(mockForm, '<span>Error</span>', wrapper);
917
+ expect(displaySpy).toHaveBeenCalledWith(form, '<span>Error</span>', wrapper);
797
918
 
798
919
  displaySpy.mockRestore();
799
920
  getTextSpy.mockRestore();
800
921
  });
801
922
  });
802
-
803
- describe('displayFormErrorsFromText', () => {
804
- test('should append to errorWrapperDiv when provided', () => {
805
- const wrapper = {
806
- append: jest.fn()
807
- };
808
-
809
- FormHelper.displayFormErrorsFromText(mockForm, 'Error text', wrapper);
810
-
811
- expect(wrapper.append).toHaveBeenCalledWith('<div class="alert alert-danger form_errors">Error text</div>');
812
- });
813
-
814
- test('should append to .form_errors_content when exists', () => {
815
- const errorContent = {
816
- append: jest.fn(),
817
- length: 1
818
- };
819
-
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();
828
- });
829
-
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
836
- };
837
-
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');
854
-
855
- expect(mockForm.find).toHaveBeenCalledWith('.modal-body');
856
- });
857
-
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
- };
865
-
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()
883
- };
884
-
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();
900
- });
901
-
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>');
909
- });
910
- });
911
-
912
- describe('buttonLoader', () => {
913
- test('should disable button on loading', () => {
914
- mockButton.attr.mockImplementation((key, value) => {
915
- if (key === 'disabled' && value === undefined) return false;
916
- return mockButton;
917
- });
918
- mockButton.data.mockImplementation((key, value) => {
919
- if (key === 'btn-text') return 'Original Text';
920
- return mockButton;
921
- });
922
-
923
- FormHelper.buttonLoader(mockButton, 'loading');
924
-
925
- expect(mockButton.attr).toHaveBeenCalledWith('disabled', true);
926
- expect(mockButton.addClass).toHaveBeenCalledWith('disabled');
927
- });
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
-
940
- test('should enable button on reset', () => {
941
- mockButton.data.mockReturnValue('Original Text');
942
-
943
- FormHelper.buttonLoader(mockButton, 'reset');
944
-
945
- expect(mockButton.html).toHaveBeenCalledWith('Original Text');
946
- expect(mockButton.removeClass).toHaveBeenCalledWith('disabled');
947
- expect(mockButton.attr).toHaveBeenCalledWith('disabled', false);
948
- });
949
-
950
- test('should use load-text when provided', () => {
951
- mockButton.attr.mockImplementation((key) => {
952
- if (key === 'disabled') return false;
953
- return mockButton;
954
- });
955
- mockButton.data.mockImplementation((key) => {
956
- if (key === 'load-text') return 'Loading...';
957
- return null;
958
- });
959
-
960
- FormHelper.buttonLoader(mockButton, 'start');
961
-
962
- expect(mockButton.html).toHaveBeenCalledWith('Loading...');
963
- });
964
-
965
- test('should use loading-text when provided', () => {
966
- mockButton.attr.mockImplementation((key) => {
967
- if (key === 'disabled') return false;
968
- return mockButton;
969
- });
970
- mockButton.data.mockImplementation((key) => {
971
- if (key === 'loading-text') return 'Custom Loading...';
972
- return null;
973
- });
974
-
975
- FormHelper.buttonLoader(mockButton, 'start');
976
-
977
- expect(mockButton.html).toHaveBeenCalledWith('Custom Loading...');
978
- });
979
-
980
- test('should return button object', () => {
981
- mockButton.data.mockReturnValue('Text');
982
- const result = FormHelper.buttonLoader(mockButton, 'reset');
983
-
984
- expect(result).toBe(mockButton);
985
- });
986
- });
987
-
988
- describe('getInputValue', () => {
989
- test('should return null for undefined input', () => {
990
- expect(FormHelper.getInputValue(undefined)).toBeNull();
991
- });
992
-
993
- test('should return null for empty string value', () => {
994
- const mockInput = { val: jest.fn(() => '') };
995
- global.$ = jest.fn(() => mockInput);
996
-
997
- expect(FormHelper.getInputValue(mockInput)).toBeNull();
998
-
999
- delete global.$;
1000
- });
1001
-
1002
- test('should return value when present', () => {
1003
- const mockInput = { val: jest.fn(() => 'test value') };
1004
- global.$ = jest.fn(() => mockInput);
1005
-
1006
- expect(FormHelper.getInputValue(mockInput)).toBe('test value');
1007
-
1008
- delete global.$;
1009
- });
1010
- });
1011
-
1012
- describe('getLinesOfTextarea', () => {
1013
- test('should split textarea by newlines and filter empty', () => {
1014
- const mockTextarea = {
1015
- val: jest.fn(() => 'line1\nline2\nline3')
1016
- };
1017
-
1018
- const result = FormHelper.getLinesOfTextarea(mockTextarea);
1019
-
1020
- expect(result).toEqual(['line1', 'line2', 'line3']);
1021
- });
1022
-
1023
- test('should handle Windows line endings', () => {
1024
- const mockTextarea = {
1025
- val: jest.fn(() => 'line1\r\nline2\r\nline3')
1026
- };
1027
-
1028
- const result = FormHelper.getLinesOfTextarea(mockTextarea);
1029
-
1030
- expect(result).toEqual(['line1', 'line2', 'line3']);
1031
- });
1032
- });
1033
-
1034
- describe('hideFormErrors', () => {
1035
- test('should remove form_errors div', () => {
1036
- const result = FormHelper.hideFormErrors(mockForm);
1037
-
1038
- expect(mockForm.find).toHaveBeenCalledWith('div.form_errors');
1039
- expect(mockInput.remove).toHaveBeenCalled();
1040
- expect(result).toBe(mockForm);
1041
- });
1042
- });
1043
923
  });
1044
924
 
1045
925
  describe('ArrayField', () => {