@datarailsshared/dr_renderer 1.5.150 → 1.5.159

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 (54) hide show
  1. package/package.json +5 -2
  2. package/src/charts/dr_donut_chart.d.ts +79 -0
  3. package/src/charts/dr_donut_chart.js +7 -2
  4. package/src/charts/dr_gauge_categories_summary_chart.d.ts +136 -0
  5. package/src/charts/dr_gauge_chart.d.ts +18 -0
  6. package/src/charts/dr_gauge_chart.js +31 -0
  7. package/src/dr-renderer-helpers.d.ts +18 -0
  8. package/src/dr-renderer-helpers.js +2 -2
  9. package/src/dr_pivottable.d.ts +2 -0
  10. package/src/dr_pivottable.js +32 -75
  11. package/src/errors.js +1 -0
  12. package/src/{types/graph-table-renderer.d.ts → graph-table-renderer.d.ts} +57 -4
  13. package/src/graph-table-renderer.js +74 -2
  14. package/src/highcharts_renderer.d.ts +5 -0
  15. package/src/highcharts_renderer.js +1 -0
  16. package/src/index.d.ts +83 -86
  17. package/src/index.js +77 -3
  18. package/src/novix_renderer.d.ts +2 -0
  19. package/src/novix_renderer.js +7 -0
  20. package/src/options/builders.js +1 -0
  21. package/src/options/constants.js +1 -0
  22. package/src/options/elements.js +1 -0
  23. package/src/options/helpers.js +1 -0
  24. package/src/options/index.js +1 -0
  25. package/src/options/presets.js +1 -0
  26. package/src/pivot-table/freeze-panes/constants.d.ts +26 -0
  27. package/src/pivot-table/freeze-panes/constants.js +42 -0
  28. package/src/pivot-table/freeze-panes/freeze-panes.css +282 -0
  29. package/src/pivot-table/freeze-panes/index.d.ts +115 -0
  30. package/src/pivot-table/freeze-panes/index.js +143 -0
  31. package/src/pivot-table/freeze-panes/sticky-strategy.d.ts +38 -0
  32. package/src/pivot-table/freeze-panes/sticky-strategy.js +247 -0
  33. package/src/pivot-table/freeze-panes/transform-strategy.d.ts +61 -0
  34. package/src/pivot-table/freeze-panes/transform-strategy.js +131 -0
  35. package/src/pivot.css +2 -98
  36. package/src/published_items_renderer.d.ts +10 -0
  37. package/src/seriesPointStyles-helper.d.ts +2 -0
  38. package/src/smart_queries_helper.d.ts +12 -0
  39. package/src/value.formatter.d.ts +3 -0
  40. package/tests/dr-renderer-helpers.test.js +29 -0
  41. package/tests/pivot-table/freeze-panes/constants.test.js +92 -0
  42. package/tests/pivot-table/freeze-panes/index.test.js +193 -0
  43. package/tests/pivot-table/freeze-panes/sticky-strategy.test.js +542 -0
  44. package/tests/pivot-table/freeze-panes/transform-strategy.test.js +304 -0
  45. package/tsconfig.json +7 -10
  46. package/src/types/index.d.ts +0 -12
  47. package/tsconfig.tsbuildinfo +0 -1
  48. /package/src/{types/errors.d.ts → errors.d.ts} +0 -0
  49. /package/src/{types/options → options}/builders.d.ts +0 -0
  50. /package/src/{types/options → options}/constants.d.ts +0 -0
  51. /package/src/{types/options → options}/elements.d.ts +0 -0
  52. /package/src/{types/options → options}/helpers.d.ts +0 -0
  53. /package/src/{types/options → options}/index.d.ts +0 -0
  54. /package/src/{types/options → options}/presets.d.ts +0 -0
@@ -0,0 +1,542 @@
1
+ const stickyStrategy = require('../../../src/pivot-table/freeze-panes/sticky-strategy');
2
+ const { STICKY_CSS_CLASSES, FREEZE_PANES_THRESHOLD_CHECK_DEBOUNCE_MS } = require('../../../src/pivot-table/freeze-panes/constants');
3
+
4
+ describe('freeze-panes/sticky-strategy', () => {
5
+ describe('getClassStrings', () => {
6
+ it('should return sticky class strings when enabled', () => {
7
+ const result = stickyStrategy.getClassStrings(true);
8
+
9
+ expect(result.horizontal).toBe(' ' + STICKY_CSS_CLASSES.HORIZONTAL);
10
+ expect(result.vertical).toBe(' ' + STICKY_CSS_CLASSES.VERTICAL);
11
+ expect(result.axis).toBe(' ' + STICKY_CSS_CLASSES.AXIS);
12
+ });
13
+
14
+ it('should return empty strings when disabled', () => {
15
+ const result = stickyStrategy.getClassStrings(false);
16
+
17
+ expect(result.horizontal).toBe('');
18
+ expect(result.vertical).toBe('');
19
+ expect(result.axis).toBe('');
20
+ });
21
+
22
+ it('should return class strings with leading space', () => {
23
+ const result = stickyStrategy.getClassStrings(true);
24
+
25
+ expect(result.horizontal.startsWith(' ')).toBe(true);
26
+ expect(result.vertical.startsWith(' ')).toBe(true);
27
+ expect(result.axis.startsWith(' ')).toBe(true);
28
+ });
29
+ });
30
+
31
+ describe('initialize', () => {
32
+ let container;
33
+ let mockObserve;
34
+ let mockDisconnect;
35
+
36
+ beforeEach(() => {
37
+ container = document.createElement('div');
38
+ Object.defineProperty(container, 'clientWidth', { value: 1000, configurable: true });
39
+ Object.defineProperty(container, 'clientHeight', { value: 500, configurable: true });
40
+ document.body.appendChild(container);
41
+
42
+ mockObserve = jest.fn();
43
+ mockDisconnect = jest.fn();
44
+ global.ResizeObserver = jest.fn().mockImplementation(() => ({
45
+ observe: mockObserve,
46
+ disconnect: mockDisconnect,
47
+ }));
48
+ });
49
+
50
+ afterEach(() => {
51
+ document.body.removeChild(container);
52
+ jest.restoreAllMocks();
53
+ });
54
+
55
+ it('should apply container class', () => {
56
+ const table = document.createElement('table');
57
+ table.className = 'pvtTable';
58
+ container.appendChild(table);
59
+
60
+ stickyStrategy.initialize(container, { rowAttrsLength: 1 });
61
+
62
+ expect(container.classList.contains(STICKY_CSS_CLASSES.CONTAINER)).toBe(true);
63
+ });
64
+
65
+ it('should return destroy function', () => {
66
+ const table = document.createElement('table');
67
+ table.className = 'pvtTable';
68
+ container.appendChild(table);
69
+
70
+ const result = stickyStrategy.initialize(container, { rowAttrsLength: 1 });
71
+
72
+ expect(result).toBeDefined();
73
+ expect(result.destroy).toBeInstanceOf(Function);
74
+ });
75
+
76
+ it('should disconnect observer on destroy', () => {
77
+ const table = document.createElement('table');
78
+ table.className = 'pvtTable';
79
+ container.appendChild(table);
80
+
81
+ const result = stickyStrategy.initialize(container, { rowAttrsLength: 1 });
82
+ result.destroy();
83
+
84
+ expect(mockDisconnect).toHaveBeenCalled();
85
+ });
86
+
87
+ it('should observe container for resize', () => {
88
+ const table = document.createElement('table');
89
+ table.className = 'pvtTable';
90
+ container.appendChild(table);
91
+
92
+ stickyStrategy.initialize(container, { rowAttrsLength: 1 });
93
+
94
+ expect(mockObserve).toHaveBeenCalledWith(container);
95
+ });
96
+
97
+ it('should return noop destroy function when no table found', () => {
98
+ const result = stickyStrategy.initialize(container, { rowAttrsLength: 1 });
99
+
100
+ expect(result.destroy).toBeInstanceOf(Function);
101
+ // Should not throw
102
+ result.destroy();
103
+ });
104
+
105
+ it('should handle missing rowAttrsLength', () => {
106
+ const table = document.createElement('table');
107
+ table.className = 'pvtTable';
108
+ container.appendChild(table);
109
+
110
+ const result = stickyStrategy.initialize(container, {});
111
+
112
+ expect(result.destroy).toBeInstanceOf(Function);
113
+ });
114
+
115
+ it('should calculate sticky positions for header cells', () => {
116
+ const table = document.createElement('table');
117
+ table.className = 'pvtTable';
118
+ table.innerHTML = `
119
+ <thead>
120
+ <tr>
121
+ <th class="${STICKY_CSS_CLASSES.AXIS}">Axis</th>
122
+ <th class="${STICKY_CSS_CLASSES.VERTICAL}">Header</th>
123
+ </tr>
124
+ </thead>
125
+ <tbody>
126
+ <tr>
127
+ <td class="${STICKY_CSS_CLASSES.HORIZONTAL}">Row Header</td>
128
+ <td>Data</td>
129
+ </tr>
130
+ </tbody>
131
+ `;
132
+ container.appendChild(table);
133
+
134
+ // Mock offsetHeight and offsetWidth
135
+ const th = table.querySelector('th');
136
+ Object.defineProperty(th, 'offsetHeight', { value: 30, configurable: true });
137
+ Object.defineProperty(th, 'offsetWidth', { value: 100, configurable: true });
138
+
139
+ stickyStrategy.initialize(container, { rowAttrsLength: 1 });
140
+
141
+ // The axis cell should have top style set
142
+ const axisCell = table.querySelector('.' + STICKY_CSS_CLASSES.AXIS);
143
+ expect(axisCell.style.top).toBe('0px');
144
+ });
145
+
146
+ it('should toggle rows disabled class based on threshold', () => {
147
+ const table = document.createElement('table');
148
+ table.className = 'pvtTable';
149
+ table.innerHTML = `
150
+ <thead>
151
+ <tr><th>H1</th></tr>
152
+ <tr><th>H2</th></tr>
153
+ <tr><th>H3</th></tr>
154
+ <tr><th>H4</th></tr>
155
+ </thead>
156
+ <tbody><tr><td>Data</td></tr></tbody>
157
+ `;
158
+ container.appendChild(table);
159
+
160
+ // Set container height small so rows exceed threshold
161
+ Object.defineProperty(container, 'clientHeight', { value: 100, configurable: true });
162
+
163
+ // Mock row heights to exceed 70% of container
164
+ table.querySelectorAll('thead tr').forEach(row => {
165
+ Object.defineProperty(row, 'offsetHeight', { value: 25, configurable: true });
166
+ });
167
+
168
+ stickyStrategy.initialize(container, { rowAttrsLength: 0 });
169
+
170
+ expect(table.classList.contains(STICKY_CSS_CLASSES.ROWS_DISABLED)).toBe(true);
171
+ });
172
+
173
+ it('should toggle columns disabled class based on threshold', () => {
174
+ const table = document.createElement('table');
175
+ table.className = 'pvtTable';
176
+ table.innerHTML = `
177
+ <thead><tr><th>Header</th></tr></thead>
178
+ <tbody>
179
+ <tr>
180
+ <td class="rowcol0 ${STICKY_CSS_CLASSES.HORIZONTAL}">Row 1</td>
181
+ <td>Data</td>
182
+ </tr>
183
+ </tbody>
184
+ `;
185
+ container.appendChild(table);
186
+
187
+ // Set container width small so columns exceed threshold
188
+ Object.defineProperty(container, 'clientWidth', { value: 100, configurable: true });
189
+
190
+ // Mock cell width to exceed 70% of container
191
+ const firstCell = table.querySelector('tbody td');
192
+ Object.defineProperty(firstCell, 'offsetWidth', { value: 80, configurable: true });
193
+
194
+ stickyStrategy.initialize(container, { rowAttrsLength: 1 });
195
+
196
+ expect(table.classList.contains(STICKY_CSS_CLASSES.COLUMNS_DISABLED)).toBe(true);
197
+ });
198
+
199
+ it('should enable freeze when dimensions are zero', () => {
200
+ const table = document.createElement('table');
201
+ table.className = 'pvtTable';
202
+ table.innerHTML = '<thead><tr><th>H</th></tr></thead><tbody><tr><td>D</td></tr></tbody>';
203
+ container.appendChild(table);
204
+
205
+ Object.defineProperty(container, 'clientWidth', { value: 0, configurable: true });
206
+ Object.defineProperty(container, 'clientHeight', { value: 0, configurable: true });
207
+
208
+ stickyStrategy.initialize(container, { rowAttrsLength: 1 });
209
+
210
+ // Should not disable rows/columns when dimensions are zero
211
+ expect(table.classList.contains(STICKY_CSS_CLASSES.ROWS_DISABLED)).toBe(false);
212
+ expect(table.classList.contains(STICKY_CSS_CLASSES.COLUMNS_DISABLED)).toBe(false);
213
+ });
214
+
215
+ it('should enable columns freeze when rowAttrsLength is 0', () => {
216
+ const table = document.createElement('table');
217
+ table.className = 'pvtTable';
218
+ table.innerHTML = '<thead><tr><th>H</th></tr></thead><tbody><tr><td>D</td></tr></tbody>';
219
+ container.appendChild(table);
220
+
221
+ stickyStrategy.initialize(container, { rowAttrsLength: 0 });
222
+
223
+ expect(table.classList.contains(STICKY_CSS_CLASSES.COLUMNS_DISABLED)).toBe(false);
224
+ });
225
+ });
226
+
227
+ describe('debounce behavior', () => {
228
+ let container;
229
+
230
+ beforeEach(() => {
231
+ jest.useFakeTimers();
232
+ container = document.createElement('div');
233
+ Object.defineProperty(container, 'clientWidth', { value: 1000, configurable: true });
234
+ Object.defineProperty(container, 'clientHeight', { value: 500, configurable: true });
235
+ document.body.appendChild(container);
236
+ });
237
+
238
+ afterEach(() => {
239
+ document.body.removeChild(container);
240
+ jest.useRealTimers();
241
+ });
242
+
243
+ it('should debounce resize observer callback', () => {
244
+ let observerCallback;
245
+ global.ResizeObserver = jest.fn().mockImplementation((callback) => {
246
+ observerCallback = callback;
247
+ return {
248
+ observe: jest.fn(),
249
+ disconnect: jest.fn(),
250
+ };
251
+ });
252
+
253
+ const table = document.createElement('table');
254
+ table.className = 'pvtTable';
255
+ table.innerHTML = '<thead><tr><th>H</th></tr></thead><tbody><tr><td>D</td></tr></tbody>';
256
+ container.appendChild(table);
257
+
258
+ stickyStrategy.initialize(container, { rowAttrsLength: 1 });
259
+
260
+ // Trigger multiple resize events
261
+ observerCallback();
262
+ observerCallback();
263
+ observerCallback();
264
+
265
+ // Fast-forward time but not enough for debounce
266
+ jest.advanceTimersByTime(100);
267
+
268
+ // Fast-forward past debounce time
269
+ jest.advanceTimersByTime(FREEZE_PANES_THRESHOLD_CHECK_DEBOUNCE_MS);
270
+
271
+ // The callback should have been called (no assertion needed, just ensuring no errors)
272
+ expect(true).toBe(true);
273
+ });
274
+ });
275
+
276
+ describe('calculateStickyPositions', () => {
277
+ let container;
278
+
279
+ beforeEach(() => {
280
+ container = document.createElement('div');
281
+ Object.defineProperty(container, 'clientWidth', { value: 1000, configurable: true });
282
+ Object.defineProperty(container, 'clientHeight', { value: 500, configurable: true });
283
+ document.body.appendChild(container);
284
+
285
+ global.ResizeObserver = jest.fn().mockImplementation(() => ({
286
+ observe: jest.fn(),
287
+ disconnect: jest.fn(),
288
+ }));
289
+ });
290
+
291
+ afterEach(() => {
292
+ document.body.removeChild(container);
293
+ });
294
+
295
+ it('should set top positions for multiple header rows', () => {
296
+ const table = document.createElement('table');
297
+ table.className = 'pvtTable';
298
+ table.innerHTML = `
299
+ <thead>
300
+ <tr>
301
+ <th class="${STICKY_CSS_CLASSES.VERTICAL}">Row 1</th>
302
+ </tr>
303
+ <tr>
304
+ <th class="${STICKY_CSS_CLASSES.VERTICAL}">Row 2</th>
305
+ </tr>
306
+ </thead>
307
+ <tbody><tr><td>Data</td></tr></tbody>
308
+ `;
309
+ container.appendChild(table);
310
+
311
+ const rows = table.querySelectorAll('thead tr');
312
+ rows.forEach(row => {
313
+ Object.defineProperty(row, 'offsetHeight', { value: 30, configurable: true });
314
+ });
315
+
316
+ stickyStrategy.initialize(container, { rowAttrsLength: 0 });
317
+
318
+ const verticalCells = table.querySelectorAll('.' + STICKY_CSS_CLASSES.VERTICAL);
319
+ expect(verticalCells[0].style.top).toBe('0px');
320
+ expect(verticalCells[1].style.top).toBe('30px');
321
+ });
322
+
323
+ it('should set left positions for row headers in tbody', () => {
324
+ const table = document.createElement('table');
325
+ table.className = 'pvtTable';
326
+ table.innerHTML = `
327
+ <thead><tr><th>Header</th></tr></thead>
328
+ <tbody>
329
+ <tr>
330
+ <td class="rowcol0 ${STICKY_CSS_CLASSES.HORIZONTAL}">Col 1</td>
331
+ <td class="rowcol1 ${STICKY_CSS_CLASSES.HORIZONTAL}">Col 2</td>
332
+ <td>Data</td>
333
+ </tr>
334
+ </tbody>
335
+ `;
336
+ container.appendChild(table);
337
+
338
+ const cells = table.querySelectorAll('.' + STICKY_CSS_CLASSES.HORIZONTAL);
339
+ cells.forEach(cell => {
340
+ Object.defineProperty(cell, 'offsetWidth', { value: 80, configurable: true });
341
+ });
342
+
343
+ stickyStrategy.initialize(container, { rowAttrsLength: 2 });
344
+
345
+ expect(cells[0].style.left).toBe('0px');
346
+ expect(cells[1].style.left).toBe('80px');
347
+ });
348
+
349
+ it('should set left positions for axis cells in thead', () => {
350
+ const table = document.createElement('table');
351
+ table.className = 'pvtTable';
352
+ table.innerHTML = `
353
+ <thead>
354
+ <tr>
355
+ <th class="${STICKY_CSS_CLASSES.AXIS}">Axis 1</th>
356
+ <th class="${STICKY_CSS_CLASSES.AXIS}">Axis 2</th>
357
+ <th class="${STICKY_CSS_CLASSES.VERTICAL}">Header</th>
358
+ </tr>
359
+ </thead>
360
+ <tbody><tr><td>Data</td></tr></tbody>
361
+ `;
362
+ container.appendChild(table);
363
+
364
+ const rows = table.querySelectorAll('thead tr');
365
+ rows.forEach(row => {
366
+ Object.defineProperty(row, 'offsetHeight', { value: 30, configurable: true });
367
+ });
368
+
369
+ const axisCells = table.querySelectorAll('.' + STICKY_CSS_CLASSES.AXIS);
370
+ axisCells.forEach(cell => {
371
+ Object.defineProperty(cell, 'offsetWidth', { value: 100, configurable: true });
372
+ });
373
+
374
+ stickyStrategy.initialize(container, { rowAttrsLength: 2 });
375
+
376
+ expect(axisCells[0].style.left).toBe('0px');
377
+ expect(axisCells[1].style.left).toBe('100px');
378
+ });
379
+
380
+ it('should correctly position cells when subtotals are present', () => {
381
+ const table = document.createElement('table');
382
+ table.className = 'pvtTable';
383
+ // Simulates subtotal structure where one <tr> has multiple <th> cells
384
+ table.innerHTML = `
385
+ <thead><tr><th>Header</th></tr></thead>
386
+ <tbody>
387
+ <tr class="pvtRowSubtotal row0">
388
+ <th class="pvtRowLabel row0 rowcol0 ${STICKY_CSS_CLASSES.HORIZONTAL}">Category A</th>
389
+ <th class="pvtRowLabel pvtRowSubtotal row0 rowcol0">Subtotal</th>
390
+ </tr>
391
+ <tr class="row1">
392
+ <th class="pvtRowLabel row1 rowcol0 ${STICKY_CSS_CLASSES.HORIZONTAL}">Category A</th>
393
+ <th class="pvtRowLabel row1 rowcol1 ${STICKY_CSS_CLASSES.HORIZONTAL}">Item 1</th>
394
+ <td>Data</td>
395
+ </tr>
396
+ <tr class="row2">
397
+ <th class="pvtRowLabel row2 rowcol0 ${STICKY_CSS_CLASSES.HORIZONTAL}">Category A</th>
398
+ <th class="pvtRowLabel row2 rowcol1 ${STICKY_CSS_CLASSES.HORIZONTAL}">Item 2</th>
399
+ <td>Data</td>
400
+ </tr>
401
+ </tbody>
402
+ `;
403
+ container.appendChild(table);
404
+
405
+ // Mock offsetWidth for cells - different widths for different column levels
406
+ const horizontalCells = table.querySelectorAll('.' + STICKY_CSS_CLASSES.HORIZONTAL);
407
+ horizontalCells.forEach(cell => {
408
+ if (cell.classList.contains('rowcol0')) {
409
+ Object.defineProperty(cell, 'offsetWidth', { value: 120, configurable: true });
410
+ } else if (cell.classList.contains('rowcol1')) {
411
+ Object.defineProperty(cell, 'offsetWidth', { value: 80, configurable: true });
412
+ }
413
+ });
414
+
415
+ stickyStrategy.initialize(container, { rowAttrsLength: 2 });
416
+
417
+ // All rowcol0 cells should have left: 0px
418
+ // All rowcol1 cells should have left: 120px (width of rowcol0)
419
+ horizontalCells.forEach(cell => {
420
+ if (cell.classList.contains('rowcol0')) {
421
+ expect(cell.style.left).toBe('0px');
422
+ } else if (cell.classList.contains('rowcol1')) {
423
+ expect(cell.style.left).toBe('120px');
424
+ }
425
+ });
426
+ });
427
+
428
+ it('should skip subtotal placeholder cells when calculating widths', () => {
429
+ const table = document.createElement('table');
430
+ table.className = 'pvtTable';
431
+ table.innerHTML = `
432
+ <thead><tr><th>Header</th></tr></thead>
433
+ <tbody>
434
+ <tr class="pvtRowSubtotal row0">
435
+ <th class="pvtRowLabel row0 rowcol0 ${STICKY_CSS_CLASSES.HORIZONTAL}">Category</th>
436
+ <th class="pvtRowLabel pvtRowSubtotal row0 rowcol0 ${STICKY_CSS_CLASSES.HORIZONTAL}">Subtotal</th>
437
+ </tr>
438
+ </tbody>
439
+ `;
440
+ container.appendChild(table);
441
+
442
+ // Regular cell has width 100, subtotal cell has width 200
443
+ const horizontalCells = table.querySelectorAll('.' + STICKY_CSS_CLASSES.HORIZONTAL);
444
+ Object.defineProperty(horizontalCells[0], 'offsetWidth', { value: 100, configurable: true });
445
+ Object.defineProperty(horizontalCells[1], 'offsetWidth', { value: 200, configurable: true });
446
+
447
+ // The subtotal cell (pvtRowSubtotal class) should be ignored when calculating column widths
448
+ // So the positioning should be based on 100px, not 200px
449
+ stickyStrategy.initialize(container, { rowAttrsLength: 1 });
450
+
451
+ // Both cells are rowcol0, so both should have left: 0px
452
+ expect(horizontalCells[0].style.left).toBe('0px');
453
+ expect(horizontalCells[1].style.left).toBe('0px');
454
+ });
455
+
456
+ it('should handle multiple row attribute levels with subtotals correctly', () => {
457
+ const table = document.createElement('table');
458
+ table.className = 'pvtTable';
459
+ table.innerHTML = `
460
+ <thead><tr><th>Header</th></tr></thead>
461
+ <tbody>
462
+ <tr class="row0">
463
+ <th class="rowcol0 ${STICKY_CSS_CLASSES.HORIZONTAL}">Level 0</th>
464
+ <th class="rowcol1 ${STICKY_CSS_CLASSES.HORIZONTAL}">Level 1</th>
465
+ <th class="rowcol2 ${STICKY_CSS_CLASSES.HORIZONTAL}">Level 2</th>
466
+ <td>Data</td>
467
+ </tr>
468
+ <tr class="pvtRowSubtotal row1">
469
+ <th class="rowcol0 ${STICKY_CSS_CLASSES.HORIZONTAL}">Category</th>
470
+ <th class="pvtRowSubtotal rowcol0">Subtotal</th>
471
+ </tr>
472
+ </tbody>
473
+ `;
474
+ container.appendChild(table);
475
+
476
+ const horizontalCells = table.querySelectorAll('.' + STICKY_CSS_CLASSES.HORIZONTAL);
477
+ horizontalCells.forEach(cell => {
478
+ if (cell.classList.contains('rowcol0')) {
479
+ Object.defineProperty(cell, 'offsetWidth', { value: 50, configurable: true });
480
+ } else if (cell.classList.contains('rowcol1')) {
481
+ Object.defineProperty(cell, 'offsetWidth', { value: 60, configurable: true });
482
+ } else if (cell.classList.contains('rowcol2')) {
483
+ Object.defineProperty(cell, 'offsetWidth', { value: 70, configurable: true });
484
+ }
485
+ });
486
+
487
+ stickyStrategy.initialize(container, { rowAttrsLength: 3 });
488
+
489
+ // Check positions: rowcol0 -> 0, rowcol1 -> 50, rowcol2 -> 110
490
+ horizontalCells.forEach(cell => {
491
+ if (cell.classList.contains('rowcol0')) {
492
+ expect(cell.style.left).toBe('0px');
493
+ } else if (cell.classList.contains('rowcol1')) {
494
+ expect(cell.style.left).toBe('50px');
495
+ } else if (cell.classList.contains('rowcol2')) {
496
+ expect(cell.style.left).toBe('110px');
497
+ }
498
+ });
499
+ });
500
+
501
+ it('should handle empty tbody gracefully', () => {
502
+ const table = document.createElement('table');
503
+ table.className = 'pvtTable';
504
+ table.innerHTML = `
505
+ <thead><tr><th>Header</th></tr></thead>
506
+ <tbody></tbody>
507
+ `;
508
+ container.appendChild(table);
509
+
510
+ // Should not throw
511
+ expect(() => {
512
+ stickyStrategy.initialize(container, { rowAttrsLength: 2 });
513
+ }).not.toThrow();
514
+ });
515
+
516
+ it('should handle cells without rowcol classes', () => {
517
+ const table = document.createElement('table');
518
+ table.className = 'pvtTable';
519
+ table.innerHTML = `
520
+ <thead><tr><th>Header</th></tr></thead>
521
+ <tbody>
522
+ <tr>
523
+ <td class="${STICKY_CSS_CLASSES.HORIZONTAL}">No rowcol class</td>
524
+ <td>Data</td>
525
+ </tr>
526
+ </tbody>
527
+ `;
528
+ container.appendChild(table);
529
+
530
+ const horizontalCell = table.querySelector('.' + STICKY_CSS_CLASSES.HORIZONTAL);
531
+ Object.defineProperty(horizontalCell, 'offsetWidth', { value: 80, configurable: true });
532
+
533
+ // Should not throw, cell without rowcol class should not get positioned
534
+ expect(() => {
535
+ stickyStrategy.initialize(container, { rowAttrsLength: 1 });
536
+ }).not.toThrow();
537
+
538
+ // Cell without rowcol class should not have left style set
539
+ expect(horizontalCell.style.left).toBe('');
540
+ });
541
+ });
542
+ });