@osimatic/helpers-js 1.5.2 → 1.5.4

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