@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
@@ -0,0 +1,646 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ const { Pagination, Navigation } = require('../paging');
6
+
7
+ describe('Pagination', () => {
8
+ let mockDiv, mockTable, mockSelect, mockItems, mockUl, mockLi;
9
+
10
+ beforeEach(() => {
11
+ // Mock items
12
+ mockItems = [];
13
+ for (let i = 0; i < 5; i++) {
14
+ const item = {
15
+ show: jest.fn().mockReturnThis(),
16
+ hide: jest.fn().mockReturnThis()
17
+ };
18
+ mockItems.push(item);
19
+ }
20
+
21
+ const itemsWithEach = Object.assign(mockItems, {
22
+ length: 5,
23
+ each: jest.fn(function(callback) {
24
+ this.forEach((item, idx) => callback.call(item, idx, item));
25
+ })
26
+ });
27
+
28
+ // Mock li elements
29
+ mockLi = {
30
+ addClass: jest.fn().mockReturnThis(),
31
+ removeClass: jest.fn().mockReturnThis(),
32
+ data: jest.fn((key) => key === 'page' ? 1 : undefined),
33
+ click: jest.fn().mockReturnThis()
34
+ };
35
+
36
+ // Mock ul element
37
+ const mockLiList = {
38
+ remove: jest.fn().mockReturnThis(),
39
+ click: jest.fn().mockReturnThis(),
40
+ removeClass: jest.fn().mockReturnThis(),
41
+ addClass: jest.fn().mockReturnThis()
42
+ };
43
+
44
+ mockUl = {
45
+ find: jest.fn((selector) => {
46
+ if (selector === 'li') {
47
+ return mockLiList;
48
+ }
49
+ if (selector === 'li:first-child') {
50
+ return mockLi;
51
+ }
52
+ return mockLi;
53
+ }),
54
+ append: jest.fn().mockReturnThis(),
55
+ show: jest.fn().mockReturnThis(),
56
+ addClass: jest.fn().mockReturnThis(),
57
+ removeClass: jest.fn().mockReturnThis(),
58
+ remove: jest.fn().mockReturnThis(),
59
+ each: jest.fn(function(callback) {
60
+ callback.call(this, 0, this);
61
+ }),
62
+ length: 1
63
+ };
64
+
65
+ // Mock select element
66
+ mockSelect = {
67
+ children: jest.fn(() => ({ length: 0 })),
68
+ append: jest.fn().mockReturnThis(),
69
+ data: jest.fn((key) => {
70
+ if (key === 'nb_rows_list') return '5,10,25,50';
71
+ if (key === 'default_nb_rows') return '10';
72
+ return undefined;
73
+ }),
74
+ val: jest.fn((value) => {
75
+ if (value === undefined) return '10';
76
+ return mockSelect;
77
+ }),
78
+ change: jest.fn().mockReturnThis(),
79
+ length: 1
80
+ };
81
+
82
+ // Mock div element
83
+ mockDiv = {
84
+ find: jest.fn((selector) => {
85
+ if (selector === '.pagination_item') return itemsWithEach;
86
+ if (selector === '.pagination_links') return { length: 1, prepend: jest.fn(), append: jest.fn() };
87
+ return { length: 0 };
88
+ }),
89
+ before: jest.fn().mockReturnThis(),
90
+ after: jest.fn().mockReturnThis(),
91
+ data: jest.fn((key) => key === 'max_rows' ? '10' : undefined),
92
+ length: 1
93
+ };
94
+
95
+ // Mock table element
96
+ mockTable = {
97
+ find: jest.fn((selector) => {
98
+ if (selector === 'tbody tr:not(.hide)') return itemsWithEach;
99
+ return { length: 0 };
100
+ }),
101
+ data: jest.fn((key) => key === 'max_rows' ? '10' : undefined),
102
+ before: jest.fn().mockReturnThis(),
103
+ after: jest.fn().mockReturnThis(),
104
+ length: 1
105
+ };
106
+
107
+ // Mock jQuery global
108
+ global.$ = jest.fn((selector) => {
109
+ if (selector === 'ul.pagination') return mockUl;
110
+ if (typeof selector === 'string' && selector.startsWith('<ul')) return mockUl;
111
+ if (typeof selector === 'string' && selector.startsWith('li[data-page')) {
112
+ return {
113
+ each: jest.fn(function(callback) {
114
+ callback.call(mockLi, 0, mockLi);
115
+ })
116
+ };
117
+ }
118
+ // Handle $(ul) calls where ul is mockUl - return mockUl itself
119
+ if (selector === mockUl) {
120
+ return mockUl;
121
+ }
122
+ // Handle $(this) calls inside .each() - return an object with all jQuery methods
123
+ if (typeof selector === 'object' && selector !== null) {
124
+ return {
125
+ show: jest.fn().mockReturnThis(),
126
+ hide: jest.fn().mockReturnThis(),
127
+ addClass: jest.fn().mockReturnThis(),
128
+ removeClass: jest.fn().mockReturnThis(),
129
+ data: jest.fn((key) => key === 'page' ? 1 : undefined),
130
+ find: jest.fn(() => ({
131
+ remove: jest.fn().mockReturnThis(),
132
+ addClass: jest.fn().mockReturnThis(),
133
+ removeClass: jest.fn().mockReturnThis()
134
+ })),
135
+ remove: jest.fn().mockReturnThis(),
136
+ append: jest.fn().mockReturnThis()
137
+ };
138
+ }
139
+ return mockDiv;
140
+ });
141
+
142
+ global.$.each = jest.fn((obj, callback) => {
143
+ if (Array.isArray(obj)) {
144
+ obj.forEach((item, idx) => callback(idx, item));
145
+ } else {
146
+ Object.keys(obj).forEach(key => callback(key, obj[key]));
147
+ }
148
+ });
149
+
150
+ // Mock global labelDisplayAll
151
+ global.labelDisplayAll = 'Display all';
152
+ });
153
+
154
+ afterEach(() => {
155
+ jest.clearAllMocks();
156
+ delete global.$;
157
+ delete global.labelDisplayAll;
158
+ });
159
+
160
+ describe('paginateCards', () => {
161
+ test('should call paginate with correct parameters', () => {
162
+ const spyPaginate = jest.spyOn(Pagination, 'paginate').mockImplementation(() => {});
163
+
164
+ Pagination.paginateCards(mockDiv, 10, false);
165
+
166
+ expect(spyPaginate).toHaveBeenCalledWith(
167
+ mockDiv,
168
+ expect.anything(),
169
+ 10,
170
+ undefined,
171
+ false
172
+ );
173
+
174
+ spyPaginate.mockRestore();
175
+ });
176
+
177
+ test('should call paginate with double pagination', () => {
178
+ const spyPaginate = jest.spyOn(Pagination, 'paginate').mockImplementation(() => {});
179
+
180
+ Pagination.paginateCards(mockDiv, 20, true);
181
+
182
+ expect(spyPaginate).toHaveBeenCalledWith(
183
+ mockDiv,
184
+ expect.anything(),
185
+ 20,
186
+ undefined,
187
+ true
188
+ );
189
+
190
+ spyPaginate.mockRestore();
191
+ });
192
+ });
193
+
194
+ describe('paginateTable', () => {
195
+ test('should call paginate with table rows', () => {
196
+ const spyPaginate = jest.spyOn(Pagination, 'paginate').mockImplementation(() => {});
197
+
198
+ Pagination.paginateTable(mockTable, mockSelect, false);
199
+
200
+ expect(mockTable.find).toHaveBeenCalledWith('tbody tr:not(.hide)');
201
+ expect(mockTable.data).toHaveBeenCalledWith('max_rows');
202
+ expect(spyPaginate).toHaveBeenCalled();
203
+
204
+ spyPaginate.mockRestore();
205
+ });
206
+ });
207
+
208
+ describe('paginate', () => {
209
+ test('should return early if div is undefined', () => {
210
+ Pagination.paginate(undefined, mockItems, 10, undefined, false);
211
+ // Should not throw
212
+ });
213
+
214
+ test('should return early if div has no length', () => {
215
+ const emptyDiv = { length: 0 };
216
+ Pagination.paginate(emptyDiv, mockItems, 10, undefined, false);
217
+ // Should not throw
218
+ });
219
+
220
+ test('should initialize select with options when empty', () => {
221
+ const spyInitPaginationDiv = jest.spyOn(Pagination, 'initPaginationDiv').mockImplementation(() => {});
222
+ const spyInitPaginationItems = jest.spyOn(Pagination, 'initPaginationItems').mockImplementation(() => {});
223
+
224
+ Pagination.paginate(mockDiv, mockItems, 10, mockSelect, false);
225
+
226
+ expect(mockSelect.children).toHaveBeenCalled();
227
+ expect(mockSelect.append).toHaveBeenCalled();
228
+ expect(mockSelect.data).toHaveBeenCalledWith('nb_rows_list');
229
+
230
+ spyInitPaginationDiv.mockRestore();
231
+ spyInitPaginationItems.mockRestore();
232
+ });
233
+
234
+ test('should set default value when data-default_nb_rows is present', () => {
235
+ const spyInitPaginationDiv = jest.spyOn(Pagination, 'initPaginationDiv').mockImplementation(() => {});
236
+ const spyInitPaginationItems = jest.spyOn(Pagination, 'initPaginationItems').mockImplementation(() => {});
237
+
238
+ Pagination.paginate(mockDiv, mockItems, 10, mockSelect, false);
239
+
240
+ expect(mockSelect.val).toHaveBeenCalled();
241
+
242
+ spyInitPaginationDiv.mockRestore();
243
+ spyInitPaginationItems.mockRestore();
244
+ });
245
+
246
+ test('should initialize double pagination when requested', () => {
247
+ const spyInitPaginationDiv = jest.spyOn(Pagination, 'initPaginationDiv').mockImplementation(() => {});
248
+ const spyInitPaginationItems = jest.spyOn(Pagination, 'initPaginationItems').mockImplementation(() => {});
249
+
250
+ Pagination.paginate(mockDiv, mockItems, 10, undefined, true);
251
+
252
+ expect(spyInitPaginationDiv).toHaveBeenCalledTimes(2);
253
+ expect(spyInitPaginationDiv).toHaveBeenCalledWith(mockDiv, mockUl, true, true); // top
254
+ expect(spyInitPaginationDiv).toHaveBeenCalledWith(mockDiv, mockUl, false, true); // bottom
255
+
256
+ spyInitPaginationDiv.mockRestore();
257
+ spyInitPaginationItems.mockRestore();
258
+ });
259
+
260
+ test('should initialize single pagination when not requested', () => {
261
+ const spyInitPaginationDiv = jest.spyOn(Pagination, 'initPaginationDiv').mockImplementation(() => {});
262
+ const spyInitPaginationItems = jest.spyOn(Pagination, 'initPaginationItems').mockImplementation(() => {});
263
+
264
+ Pagination.paginate(mockDiv, mockItems, 10, undefined, false);
265
+
266
+ expect(spyInitPaginationDiv).toHaveBeenCalledTimes(1);
267
+ expect(spyInitPaginationDiv).toHaveBeenCalledWith(mockDiv, mockUl, false, false); // bottom
268
+
269
+ spyInitPaginationDiv.mockRestore();
270
+ spyInitPaginationItems.mockRestore();
271
+ });
272
+ });
273
+
274
+ describe('initPaginationDiv', () => {
275
+ test('should create new pagination div when not present', () => {
276
+ const emptyUl = { length: 0 };
277
+
278
+ Pagination.initPaginationDiv(mockDiv, emptyUl, false, true);
279
+
280
+ expect(global.$).toHaveBeenCalledWith('<ul class="pagination"></ul>');
281
+ });
282
+
283
+ test('should append pagination when onTop is false and pagination_links exists', () => {
284
+ const paginationLinks = {
285
+ length: 1,
286
+ prepend: jest.fn(),
287
+ append: jest.fn()
288
+ };
289
+
290
+ mockDiv.find = jest.fn((selector) => {
291
+ if (selector === '.pagination_links') return paginationLinks;
292
+ return { length: 0 };
293
+ });
294
+
295
+ Pagination.initPaginationDiv(mockDiv, { length: 0 }, false, true);
296
+
297
+ expect(paginationLinks.append).toHaveBeenCalled();
298
+ });
299
+
300
+ test('should prepend pagination when onTop is true and pagination_links exists', () => {
301
+ const paginationLinks = {
302
+ length: 1,
303
+ prepend: jest.fn(),
304
+ append: jest.fn()
305
+ };
306
+
307
+ mockDiv.find = jest.fn((selector) => {
308
+ if (selector === '.pagination_links') return paginationLinks;
309
+ return { length: 0 };
310
+ });
311
+
312
+ Pagination.initPaginationDiv(mockDiv, { length: 0 }, true, true);
313
+
314
+ expect(paginationLinks.prepend).toHaveBeenCalled();
315
+ });
316
+
317
+ test('should place pagination after div when pagination_links not present and onTop is false', () => {
318
+ mockDiv.find = jest.fn(() => ({ length: 0 }));
319
+
320
+ Pagination.initPaginationDiv(mockDiv, { length: 0 }, false, true);
321
+
322
+ expect(mockDiv.after).toHaveBeenCalled();
323
+ });
324
+
325
+ test('should place pagination before div when pagination_links not present and onTop is true', () => {
326
+ mockDiv.find = jest.fn(() => ({ length: 0 }));
327
+
328
+ Pagination.initPaginationDiv(mockDiv, { length: 0 }, true, true);
329
+
330
+ expect(mockDiv.before).toHaveBeenCalled();
331
+ });
332
+ });
333
+
334
+ describe('initPaginationItems', () => {
335
+ test('should show items up to maxItems', () => {
336
+ const $Spy = jest.spyOn(global, '$');
337
+
338
+ Pagination.initPaginationItems(mockItems, 3, false);
339
+
340
+ // Verify that $ was called with items (items.each iterates over them)
341
+ expect(mockItems.each).toHaveBeenCalled();
342
+ // Verify that $ was called (for $(this).show() and $(this).hide() calls)
343
+ expect($Spy).toHaveBeenCalled();
344
+
345
+ $Spy.mockRestore();
346
+ });
347
+
348
+ test('should show all items when maxItems is 0', () => {
349
+ Pagination.initPaginationItems(mockItems, 0, false);
350
+
351
+ // With maxItems = 0, all items should be shown
352
+ expect(mockItems.each).toHaveBeenCalled();
353
+ });
354
+
355
+ test('should hide pagination when maxItems is 0', () => {
356
+ Pagination.initPaginationItems(mockItems, 0, false);
357
+
358
+ // When maxItems is 0 or totalItems < maxItems, pagination is hidden
359
+ expect(mockUl.each).toHaveBeenCalled();
360
+ });
361
+
362
+ test('should hide pagination when totalItems < maxItems', () => {
363
+ Pagination.initPaginationItems(mockItems, 10, false);
364
+
365
+ // 5 items < 10 maxItems, so pagination should be hidden
366
+ expect(mockUl.each).toHaveBeenCalled();
367
+ });
368
+
369
+ test('should create pagination pages', () => {
370
+ const manyItems = [];
371
+ for (let i = 0; i < 25; i++) {
372
+ manyItems.push({
373
+ show: jest.fn().mockReturnThis(),
374
+ hide: jest.fn().mockReturnThis()
375
+ });
376
+ }
377
+ manyItems.each = jest.fn(function(callback) {
378
+ this.forEach((item, idx) => callback.call(item, idx, item));
379
+ });
380
+ manyItems.length = 25;
381
+
382
+ Pagination.initPaginationItems(manyItems, 10, false);
383
+
384
+ // Should create 3 pages (25 items / 10 per page = 3 pages)
385
+ // Verify that ul.append was called to create page items
386
+ expect(mockUl.append).toHaveBeenCalled();
387
+ // Verify pagination is not hidden
388
+ expect(mockUl.removeClass).toHaveBeenCalledWith('hide');
389
+ });
390
+
391
+ test('should set first page as active', () => {
392
+ const manyItems = [];
393
+ for (let i = 0; i < 25; i++) {
394
+ manyItems.push({
395
+ show: jest.fn().mockReturnThis(),
396
+ hide: jest.fn().mockReturnThis()
397
+ });
398
+ }
399
+ manyItems.each = jest.fn(function(callback) {
400
+ this.forEach((item, idx) => callback.call(item, idx, item));
401
+ });
402
+ manyItems.length = 25;
403
+
404
+ Pagination.initPaginationItems(manyItems, 10, false);
405
+
406
+ // Verify that find('li:first-child') was called to set first page active
407
+ expect(mockUl.find).toHaveBeenCalledWith('li:first-child');
408
+ expect(mockLi.addClass).toHaveBeenCalledWith('active');
409
+ });
410
+
411
+ test('should handle page clicks', () => {
412
+ const manyItems = [];
413
+ for (let i = 0; i < 25; i++) {
414
+ manyItems.push({
415
+ show: jest.fn().mockReturnThis(),
416
+ hide: jest.fn().mockReturnThis()
417
+ });
418
+ }
419
+ manyItems.each = jest.fn(function(callback) {
420
+ this.forEach((item, idx) => callback.call(item, idx, item));
421
+ });
422
+ manyItems.length = 25;
423
+
424
+ Pagination.initPaginationItems(manyItems, 10, false);
425
+
426
+ // Verify that click handler was attached to pagination items
427
+ expect(mockUl.find).toHaveBeenCalledWith('li');
428
+ });
429
+ });
430
+ });
431
+
432
+ describe('Navigation', () => {
433
+ let mockA, mockUlNav, mockTabContent, mockNavLink, mockTabPane;
434
+
435
+ beforeEach(() => {
436
+ // Mock tab pane
437
+ mockTabPane = {
438
+ addClass: jest.fn().mockReturnThis(),
439
+ removeClass: jest.fn().mockReturnThis()
440
+ };
441
+
442
+ // Mock tab content
443
+ mockTabContent = {
444
+ find: jest.fn((selector) => mockTabPane)
445
+ };
446
+
447
+ // Mock nav link
448
+ mockNavLink = {
449
+ removeClass: jest.fn().mockReturnThis(),
450
+ attr: jest.fn((key) => key === 'href' ? '#tab1' : undefined)
451
+ };
452
+
453
+ // Mock ul nav
454
+ mockUlNav = {
455
+ find: jest.fn((selector) => {
456
+ if (selector === 'a.nav-link') {
457
+ return {
458
+ each: jest.fn(function(callback) {
459
+ callback.call(mockNavLink, 0, mockNavLink);
460
+ })
461
+ };
462
+ }
463
+ return mockNavLink;
464
+ }),
465
+ parent: jest.fn(() => ({
466
+ find: jest.fn((selector) => mockTabContent)
467
+ }))
468
+ };
469
+
470
+ // Mock a element
471
+ mockA = {
472
+ closest: jest.fn((selector) => mockUlNav),
473
+ addClass: jest.fn().mockReturnThis(),
474
+ attr: jest.fn((key) => key === 'href' ? '#tab2' : undefined),
475
+ length: 1
476
+ };
477
+
478
+ // Mock jQuery
479
+ global.$ = jest.fn((selector) => {
480
+ if (selector === mockNavLink || (typeof selector === 'object' && selector === mockNavLink)) {
481
+ return mockNavLink;
482
+ }
483
+ if (typeof selector === 'object' && selector !== null) {
484
+ // Handle $(element) calls
485
+ return {
486
+ removeClass: jest.fn().mockReturnThis(),
487
+ addClass: jest.fn().mockReturnThis(),
488
+ attr: jest.fn((key) => key === 'href' ? '#tab1' : undefined),
489
+ ...selector
490
+ };
491
+ }
492
+ return mockA;
493
+ });
494
+
495
+ // Simple mock of window and document for these tests
496
+ // Note: window.location.href will be jsdom default
497
+ if (!global.window.history) {
498
+ global.window.history = {};
499
+ }
500
+ global.window.history.replaceState = jest.fn();
501
+ global.window.history.pushState = jest.fn();
502
+
503
+ if (!global.document) {
504
+ global.document = {};
505
+ }
506
+ global.document.title = 'Test Page';
507
+ });
508
+
509
+ afterEach(() => {
510
+ jest.clearAllMocks();
511
+ delete global.$;
512
+ delete global.bootstrap;
513
+ delete global.UrlAndQueryString;
514
+ });
515
+
516
+ describe('activateTab', () => {
517
+ test('should remove active class from all nav links', () => {
518
+ Navigation.activateTab(mockA);
519
+
520
+ expect(mockUlNav.find).toHaveBeenCalledWith('a.nav-link');
521
+ expect(mockNavLink.removeClass).toHaveBeenCalledWith('active');
522
+ });
523
+
524
+ test('should remove active and show classes from tab panes', () => {
525
+ Navigation.activateTab(mockA);
526
+
527
+ expect(mockTabPane.removeClass).toHaveBeenCalledWith('active');
528
+ expect(mockTabPane.removeClass).toHaveBeenCalledWith('show');
529
+ });
530
+
531
+ test('should add active class to clicked link', () => {
532
+ Navigation.activateTab(mockA);
533
+
534
+ expect(mockA.addClass).toHaveBeenCalledWith('active');
535
+ });
536
+
537
+ test('should show corresponding tab pane', () => {
538
+ Navigation.activateTab(mockA);
539
+
540
+ expect(mockTabPane.addClass).toHaveBeenCalledWith('active');
541
+ expect(mockTabPane.addClass).toHaveBeenCalledWith('show');
542
+ });
543
+
544
+ test('should only process links with # href', () => {
545
+ mockNavLink.attr = jest.fn((key) => key === 'href' ? 'http://example.com' : undefined);
546
+
547
+ Navigation.activateTab(mockA);
548
+
549
+ // Should not find tab content for non-# hrefs
550
+ expect(mockNavLink.removeClass).toHaveBeenCalledWith('active');
551
+ });
552
+ });
553
+
554
+ describe('showTab', () => {
555
+ test('should return early if bootstrap is undefined', () => {
556
+ delete global.bootstrap;
557
+
558
+ Navigation.showTab(mockA);
559
+
560
+ // Should not throw
561
+ });
562
+
563
+ test('should create and show bootstrap tab when available', () => {
564
+ const mockTab = {
565
+ show: jest.fn()
566
+ };
567
+
568
+ global.bootstrap = {
569
+ Tab: jest.fn(() => mockTab)
570
+ };
571
+
572
+ Navigation.showTab(mockA);
573
+
574
+ expect(global.bootstrap.Tab).toHaveBeenCalledWith(mockA[0]);
575
+ expect(mockTab.show).toHaveBeenCalled();
576
+ });
577
+ });
578
+
579
+ describe('addTabInHistory', () => {
580
+ beforeEach(() => {
581
+ global.UrlAndQueryString = {
582
+ setParamOfUrl: jest.fn((key, value, url) => `${url}&${key}=${value}`)
583
+ };
584
+ });
585
+
586
+ test('should call UrlAndQueryString.setParamOfUrl with correct parameters', () => {
587
+ Navigation.addTabInHistory('tab1', 'tab', true);
588
+
589
+ expect(global.UrlAndQueryString.setParamOfUrl).toHaveBeenCalledWith(
590
+ 'tab',
591
+ 'tab1',
592
+ expect.any(String)
593
+ );
594
+ });
595
+
596
+ test('should use default queryStringKey if not provided', () => {
597
+ Navigation.addTabInHistory('tab1');
598
+
599
+ expect(global.UrlAndQueryString.setParamOfUrl).toHaveBeenCalledWith(
600
+ 'tab',
601
+ 'tab1',
602
+ expect.any(String)
603
+ );
604
+ });
605
+
606
+ test('should use replaceState by default', () => {
607
+ const replaceStateSpy = jest.spyOn(global.window.history, 'replaceState');
608
+ const pushStateSpy = jest.spyOn(global.window.history, 'pushState');
609
+
610
+ Navigation.addTabInHistory('tab1', 'tab', true);
611
+
612
+ expect(replaceStateSpy).toHaveBeenCalled();
613
+ expect(pushStateSpy).not.toHaveBeenCalled();
614
+
615
+ replaceStateSpy.mockRestore();
616
+ pushStateSpy.mockRestore();
617
+ });
618
+
619
+ test('should use pushState when replace is false', () => {
620
+ const replaceStateSpy = jest.spyOn(global.window.history, 'replaceState');
621
+ const pushStateSpy = jest.spyOn(global.window.history, 'pushState');
622
+
623
+ Navigation.addTabInHistory('tab1', 'tab', false);
624
+
625
+ expect(pushStateSpy).toHaveBeenCalled();
626
+ expect(replaceStateSpy).not.toHaveBeenCalled();
627
+
628
+ replaceStateSpy.mockRestore();
629
+ pushStateSpy.mockRestore();
630
+ });
631
+
632
+ test('should update URL with tab parameter', () => {
633
+ const replaceStateSpy = jest.spyOn(global.window.history, 'replaceState');
634
+
635
+ Navigation.addTabInHistory('tab2', 'activeTab', true);
636
+
637
+ expect(replaceStateSpy).toHaveBeenCalledWith(
638
+ '',
639
+ 'Test Page',
640
+ expect.stringContaining('activeTab=tab2')
641
+ );
642
+
643
+ replaceStateSpy.mockRestore();
644
+ });
645
+ });
646
+ });