@keenthemes/ktui 1.1.4 → 1.1.6

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 (101) hide show
  1. package/dist/ktui.js +11061 -10799
  2. package/dist/ktui.min.js +1 -1
  3. package/dist/ktui.min.js.map +1 -1
  4. package/dist/styles.css +33 -27
  5. package/lib/cjs/components/collapse/collapse.js +0 -2
  6. package/lib/cjs/components/collapse/collapse.js.map +1 -1
  7. package/lib/cjs/components/component.js +11 -0
  8. package/lib/cjs/components/component.js.map +1 -1
  9. package/lib/cjs/components/datatable/datatable-sort.js +80 -11
  10. package/lib/cjs/components/datatable/datatable-sort.js.map +1 -1
  11. package/lib/cjs/components/datatable/datatable.js +77 -24
  12. package/lib/cjs/components/datatable/datatable.js.map +1 -1
  13. package/lib/cjs/components/drawer/drawer.js +63 -42
  14. package/lib/cjs/components/drawer/drawer.js.map +1 -1
  15. package/lib/cjs/components/dropdown/dropdown.js +6 -0
  16. package/lib/cjs/components/dropdown/dropdown.js.map +1 -1
  17. package/lib/cjs/components/scrollto/scrollto.js +0 -2
  18. package/lib/cjs/components/scrollto/scrollto.js.map +1 -1
  19. package/lib/cjs/components/select/combobox.js.map +1 -1
  20. package/lib/cjs/components/select/dropdown.js.map +1 -1
  21. package/lib/cjs/components/select/remote.js.map +1 -1
  22. package/lib/cjs/components/select/search.js +9 -5
  23. package/lib/cjs/components/select/search.js.map +1 -1
  24. package/lib/cjs/components/select/select.js +29 -9
  25. package/lib/cjs/components/select/select.js.map +1 -1
  26. package/lib/cjs/components/select/tags.js.map +1 -1
  27. package/lib/cjs/components/select/templates.js.map +1 -1
  28. package/lib/cjs/components/select/utils.js +10 -0
  29. package/lib/cjs/components/select/utils.js.map +1 -1
  30. package/lib/cjs/components/sticky/sticky.js +104 -24
  31. package/lib/cjs/components/sticky/sticky.js.map +1 -1
  32. package/lib/cjs/components/theme-switch/theme-switch.js +0 -2
  33. package/lib/cjs/components/theme-switch/theme-switch.js.map +1 -1
  34. package/lib/cjs/components/toast/toast.js +1 -2
  35. package/lib/cjs/components/toast/toast.js.map +1 -1
  36. package/lib/cjs/helpers/dom.js +0 -2
  37. package/lib/cjs/helpers/dom.js.map +1 -1
  38. package/lib/esm/components/collapse/collapse.js +0 -2
  39. package/lib/esm/components/collapse/collapse.js.map +1 -1
  40. package/lib/esm/components/component.js +11 -0
  41. package/lib/esm/components/component.js.map +1 -1
  42. package/lib/esm/components/datatable/datatable-sort.js +80 -11
  43. package/lib/esm/components/datatable/datatable-sort.js.map +1 -1
  44. package/lib/esm/components/datatable/datatable.js +77 -24
  45. package/lib/esm/components/datatable/datatable.js.map +1 -1
  46. package/lib/esm/components/drawer/drawer.js +63 -42
  47. package/lib/esm/components/drawer/drawer.js.map +1 -1
  48. package/lib/esm/components/dropdown/dropdown.js +6 -0
  49. package/lib/esm/components/dropdown/dropdown.js.map +1 -1
  50. package/lib/esm/components/scrollto/scrollto.js +0 -2
  51. package/lib/esm/components/scrollto/scrollto.js.map +1 -1
  52. package/lib/esm/components/select/combobox.js.map +1 -1
  53. package/lib/esm/components/select/dropdown.js.map +1 -1
  54. package/lib/esm/components/select/remote.js.map +1 -1
  55. package/lib/esm/components/select/search.js +9 -5
  56. package/lib/esm/components/select/search.js.map +1 -1
  57. package/lib/esm/components/select/select.js +29 -9
  58. package/lib/esm/components/select/select.js.map +1 -1
  59. package/lib/esm/components/select/tags.js.map +1 -1
  60. package/lib/esm/components/select/templates.js.map +1 -1
  61. package/lib/esm/components/select/utils.js +10 -0
  62. package/lib/esm/components/select/utils.js.map +1 -1
  63. package/lib/esm/components/sticky/sticky.js +104 -24
  64. package/lib/esm/components/sticky/sticky.js.map +1 -1
  65. package/lib/esm/components/theme-switch/theme-switch.js +0 -2
  66. package/lib/esm/components/theme-switch/theme-switch.js.map +1 -1
  67. package/lib/esm/components/toast/toast.js +1 -2
  68. package/lib/esm/components/toast/toast.js.map +1 -1
  69. package/lib/esm/helpers/dom.js +0 -2
  70. package/lib/esm/helpers/dom.js.map +1 -1
  71. package/package.json +14 -7
  72. package/src/components/collapse/collapse.ts +0 -3
  73. package/src/components/component.ts +14 -4
  74. package/src/components/datatable/__tests__/currency-sort.test.ts +108 -0
  75. package/src/components/datatable/__tests__/multi-row-headers.test.ts +121 -0
  76. package/src/components/datatable/__tests__/pagination-reset.test.ts +13 -5
  77. package/src/components/datatable/__tests__/race-conditions.test.ts +138 -78
  78. package/src/components/datatable/__tests__/setup.ts +9 -4
  79. package/src/components/datatable/datatable-sort.ts +88 -10
  80. package/src/components/datatable/datatable.css +4 -4
  81. package/src/components/datatable/datatable.ts +91 -30
  82. package/src/components/datatable/types.ts +16 -0
  83. package/src/components/drawer/drawer.ts +97 -57
  84. package/src/components/drawer/types.ts +4 -2
  85. package/src/components/dropdown/dropdown.ts +8 -1
  86. package/src/components/scrollto/scrollto.ts +0 -3
  87. package/src/components/select/__tests__/ux-behaviors.test.ts +274 -8
  88. package/src/components/select/combobox.ts +0 -1
  89. package/src/components/select/dropdown.ts +0 -2
  90. package/src/components/select/remote.ts +1 -6
  91. package/src/components/select/search.ts +14 -7
  92. package/src/components/select/select.ts +29 -29
  93. package/src/components/select/tags.ts +0 -1
  94. package/src/components/select/templates.ts +8 -8
  95. package/src/components/select/utils.ts +15 -2
  96. package/src/components/sticky/__tests__/sticky.test.ts +205 -0
  97. package/src/components/sticky/sticky.ts +119 -21
  98. package/src/components/sticky/types.ts +3 -0
  99. package/src/components/theme-switch/theme-switch.ts +0 -3
  100. package/src/components/toast/toast.ts +3 -2
  101. package/src/helpers/dom.ts +0 -3
@@ -0,0 +1,108 @@
1
+ /**
2
+ * currency-sort.test.ts
3
+ * Tests that price/currency columns sort by numeric value, not lexicographically.
4
+ *
5
+ * Spec: fix-datatable-sort-arrow-and-numeric-sort
6
+ * Requirement: Numeric and Custom Column Sort Configuration
7
+ */
8
+
9
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
10
+ import { createSortHandler } from '../datatable-sort';
11
+
12
+ describe('KTDataTable - Currency/numeric sort', () => {
13
+ let thead: HTMLTableSectionElement;
14
+ const noop = vi.fn();
15
+
16
+ beforeEach(() => {
17
+ thead = document.createElement('thead');
18
+ const tr = document.createElement('tr');
19
+ const th = document.createElement('th');
20
+ th.setAttribute('data-kt-datatable-column', 'price');
21
+ tr.appendChild(th);
22
+ thead.appendChild(tr);
23
+ });
24
+
25
+ it('sorts currency column by numeric value ascending (e.g. £5, £20, £123)', () => {
26
+ const config = {
27
+ columns: {
28
+ price: { sortType: 'numeric' as const },
29
+ },
30
+ };
31
+ const handler = createSortHandler(
32
+ config as any,
33
+ thead,
34
+ () => ({ sortField: null, sortOrder: '' }),
35
+ noop,
36
+ noop,
37
+ noop,
38
+ noop,
39
+ );
40
+
41
+ const data = [
42
+ { price: '£123' },
43
+ { price: '£20' },
44
+ { price: '£5' },
45
+ { price: '£9.99' },
46
+ ];
47
+ const sorted = handler.sortData(data, 'price', 'asc');
48
+
49
+ // Must be numeric order: 5, 9.99, 20, 123 (not lexicographic £123, £20, £5)
50
+ const values = sorted.map((row) => row.price);
51
+ expect(values).toEqual(['£5', '£9.99', '£20', '£123']);
52
+ });
53
+
54
+ it('sorts currency column by numeric value descending', () => {
55
+ const config = {
56
+ columns: {
57
+ price: { sortType: 'numeric' as const },
58
+ },
59
+ };
60
+ const handler = createSortHandler(
61
+ config as any,
62
+ thead,
63
+ () => ({ sortField: null, sortOrder: '' }),
64
+ noop,
65
+ noop,
66
+ noop,
67
+ noop,
68
+ );
69
+
70
+ const data = [
71
+ { price: '£5' },
72
+ { price: '£20' },
73
+ { price: '£123' },
74
+ ];
75
+ const sorted = handler.sortData(data, 'price', 'desc');
76
+
77
+ const numericOrder = sorted.map((row) =>
78
+ parseFloat(String(row.price).replace(/[^0-9.-]/g, '')),
79
+ );
80
+ expect(numericOrder).toEqual([123, 20, 5]);
81
+ });
82
+
83
+ it('without sortType numeric, sorts lexicographically (e.g. £123 before £20)', () => {
84
+ const config = { columns: {} };
85
+ const handler = createSortHandler(
86
+ config as any,
87
+ thead,
88
+ () => ({ sortField: null, sortOrder: '' }),
89
+ noop,
90
+ noop,
91
+ noop,
92
+ noop,
93
+ );
94
+
95
+ const data = [
96
+ { price: '£123' },
97
+ { price: '£20' },
98
+ { price: '£5' },
99
+ ];
100
+ const sorted = handler.sortData(data, 'price', 'asc');
101
+
102
+ // String sort: "£123" < "£20" < "£5" (1 < 2 < 5)
103
+ const values = sorted.map((row) => row.price);
104
+ expect(values[0]).toBe('£123');
105
+ expect(values[1]).toBe('£20');
106
+ expect(values[2]).toBe('£5');
107
+ });
108
+ });
@@ -0,0 +1,121 @@
1
+ /**
2
+ * multi-row-headers.test.ts
3
+ * Tests for datatable column count with multi-row (grouped) headers.
4
+ *
5
+ * Spec: openspec/changes/fix-datatable-multi-row-header-column-count
6
+ * Requirement: Multi-Row (Grouped) Header Column Count
7
+ */
8
+
9
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
10
+ import { KTDataTable } from '../datatable';
11
+
12
+ describe('KTDataTable - Multi-row header column count', () => {
13
+ let container: HTMLElement;
14
+ let tableElement: HTMLTableElement;
15
+
16
+ /**
17
+ * Create a table with multi-row thead (no data-kt-datatable-column) and 17 data columns.
18
+ * Row 1: Person (rowspan=2), Backlog (colspan=3), Floater2 (colspan=3), Floater1 (colspan=3), CL2025 (colspan=3), LWP (colspan=3), Action (rowspan=2) = 7 th
19
+ * Row 2: 15 leaf th (Assigned, Used, Balance × 5)
20
+ * Total 22 th in DOM but only 17 data columns.
21
+ */
22
+ const createMultiRowHeaderTable = (bodyRowCount: number = 2) => {
23
+ container = document.createElement('div');
24
+ container.id = 'kt_datatable_multirow';
25
+ container.setAttribute('data-kt-datatable', 'true');
26
+
27
+ tableElement = document.createElement('table');
28
+ tableElement.setAttribute('data-kt-datatable-table', 'true');
29
+
30
+ const thead = document.createElement('thead');
31
+ const row1 = document.createElement('tr');
32
+ row1.innerHTML = `
33
+ <th rowspan="2">Person</th>
34
+ <th colspan="3">Backlog</th>
35
+ <th colspan="3">Floater (2) 2025</th>
36
+ <th colspan="3">Floater (1) 2025</th>
37
+ <th colspan="3">CL2025</th>
38
+ <th colspan="3">LWP</th>
39
+ <th rowspan="2">Action</th>
40
+ `;
41
+ thead.appendChild(row1);
42
+
43
+ const row2 = document.createElement('tr');
44
+ row2.innerHTML = `
45
+ <th>Assigned</th><th>Used</th><th>Balance</th>
46
+ <th>Assigned</th><th>Used</th><th>Balance</th>
47
+ <th>Assigned</th><th>Used</th><th>Balance</th>
48
+ <th>Assigned</th><th>Used</th><th>Balance</th>
49
+ <th>Assigned</th><th>Used</th><th>Balance</th>
50
+ `;
51
+ thead.appendChild(row2);
52
+ tableElement.appendChild(thead);
53
+
54
+ const tbody = document.createElement('tbody');
55
+ const tdCount = 17;
56
+ for (let r = 0; r < bodyRowCount; r++) {
57
+ const tr = document.createElement('tr');
58
+ for (let c = 0; c < tdCount; c++) {
59
+ const td = document.createElement('td');
60
+ td.textContent = c === 0 ? `Person ${r + 1}` : String(c);
61
+ tr.appendChild(td);
62
+ }
63
+ tbody.appendChild(tr);
64
+ }
65
+ tableElement.appendChild(tbody);
66
+
67
+ const wrapper = document.createElement('div');
68
+ wrapper.appendChild(tableElement);
69
+
70
+ const infoElement = document.createElement('span');
71
+ infoElement.setAttribute('data-kt-datatable-info', 'true');
72
+ const sizeElement = document.createElement('select');
73
+ sizeElement.setAttribute('data-kt-datatable-size', 'true');
74
+ const paginationElement = document.createElement('div');
75
+ paginationElement.setAttribute('data-kt-datatable-pagination', 'true');
76
+
77
+ container.appendChild(wrapper);
78
+ container.appendChild(infoElement);
79
+ container.appendChild(sizeElement);
80
+ container.appendChild(paginationElement);
81
+ document.body.appendChild(container);
82
+
83
+ return { container, tableElement, tbody };
84
+ };
85
+
86
+ beforeEach(() => {
87
+ vi.useFakeTimers();
88
+ });
89
+
90
+ it('should render exactly 17 columns when thead has multi-row headers and no data-kt-datatable-column', async () => {
91
+ createMultiRowHeaderTable(2);
92
+ const datatable = new KTDataTable(container, { stateSave: false });
93
+ await vi.runAllTimersAsync();
94
+
95
+ const tbody = tableElement.tBodies[0];
96
+ expect(tbody).toBeDefined();
97
+ const rows = tbody.querySelectorAll('tr');
98
+ // Two data rows
99
+ expect(rows.length).toBe(2);
100
+ rows.forEach((row) => {
101
+ const cells = row.querySelectorAll('td');
102
+ expect(cells.length).toBe(17);
103
+ });
104
+ });
105
+
106
+ it('should use logical column count for empty-state row colspan', async () => {
107
+ createMultiRowHeaderTable(0);
108
+ const tbody = tableElement.querySelector('tbody');
109
+ expect(tbody).toBeDefined();
110
+ const datatable = new KTDataTable(container, { stateSave: false });
111
+ await vi.runAllTimersAsync();
112
+
113
+ const noticeRow = tableElement.tBodies[0].querySelector('tr');
114
+ expect(noticeRow).toBeDefined();
115
+ const cell = noticeRow?.querySelector('td');
116
+ expect(cell).toBeDefined();
117
+ // Should span 17 (logical columns from originalData) or 1 if no data; after extract we have 0 rows so logicalCount could be 0 -> we use 1
118
+ // With 0 body rows we never have originalData, so _getLogicalColumnCount() returns first tbody row td count (0) or 0; we set colspan to 1
119
+ expect(cell!.colSpan).toBeGreaterThanOrEqual(1);
120
+ });
121
+ });
@@ -184,7 +184,9 @@ describe('KTDataTable - Pagination Reset', () => {
184
184
  // String search
185
185
  return data.filter((item: any) =>
186
186
  Object.values(item).some((value: any) =>
187
- String(value).toLowerCase().includes((search as string).toLowerCase()),
187
+ String(value)
188
+ .toLowerCase()
189
+ .includes((search as string).toLowerCase()),
188
190
  ),
189
191
  );
190
192
  },
@@ -326,7 +328,11 @@ describe('KTDataTable - Pagination Reset', () => {
326
328
  expect(datatable.getState().page).toBe(1);
327
329
 
328
330
  // Replace filter on same column
329
- datatable.setFilter({ column: 'status', type: 'text', value: 'inactive' });
331
+ datatable.setFilter({
332
+ column: 'status',
333
+ type: 'text',
334
+ value: 'inactive',
335
+ });
330
336
  expect(datatable.getState().page).toBe(1);
331
337
 
332
338
  // Should only have one filter for 'status' column
@@ -498,7 +504,7 @@ describe('KTDataTable - Pagination Reset', () => {
498
504
  const namespace = 'test-datatable-restore';
499
505
 
500
506
  // First instance
501
- let table1 = new KTDataTable(container, {
507
+ const table1 = new KTDataTable(container, {
502
508
  pageSize: 10,
503
509
  stateSave: true,
504
510
  stateNamespace: namespace,
@@ -637,7 +643,10 @@ describe('KTDataTable - Pagination Reset', () => {
637
643
 
638
644
  it('should not break existing event handlers', async () => {
639
645
  const { container } = createMockDataTable(25);
640
- datatable = new KTDataTable(container, { pageSize: 10, stateSave: false });
646
+ datatable = new KTDataTable(container, {
647
+ pageSize: 10,
648
+ stateSave: false,
649
+ });
641
650
 
642
651
  const reloadSpy = vi.fn();
643
652
  // Listen for 'reload' event directly (CustomEvent)
@@ -654,4 +663,3 @@ describe('KTDataTable - Pagination Reset', () => {
654
663
  });
655
664
  });
656
665
  });
657
-
@@ -3,7 +3,15 @@
3
3
  * Tests the fixes for concurrent request handling, request cancellation, and stale response detection
4
4
  */
5
5
 
6
- import { describe, it, expect, beforeEach, afterEach, vi, type MockedFunction } from 'vitest';
6
+ import {
7
+ describe,
8
+ it,
9
+ expect,
10
+ beforeEach,
11
+ afterEach,
12
+ vi,
13
+ type MockedFunction,
14
+ } from 'vitest';
7
15
  import { KTDataTable } from '../datatable';
8
16
  import { waitFor } from './setup';
9
17
 
@@ -45,7 +53,9 @@ describe('KTDataTable Race Condition Fixes', () => {
45
53
  return new Promise<Response>((resolve, reject) => {
46
54
  const timeout = setTimeout(() => {
47
55
  if (options?.signal?.aborted) {
48
- reject(new DOMException('The operation was aborted.', 'AbortError'));
56
+ reject(
57
+ new DOMException('The operation was aborted.', 'AbortError'),
58
+ );
49
59
  } else {
50
60
  resolve(
51
61
  new Response(
@@ -56,7 +66,10 @@ describe('KTDataTable Race Condition Fixes', () => {
56
66
  ],
57
67
  totalCount: 2,
58
68
  }),
59
- { status: 200, headers: { 'Content-Type': 'application/json' } },
69
+ {
70
+ status: 200,
71
+ headers: { 'Content-Type': 'application/json' },
72
+ },
60
73
  ),
61
74
  );
62
75
  }
@@ -66,7 +79,9 @@ describe('KTDataTable Race Condition Fixes', () => {
66
79
  if (options?.signal) {
67
80
  options.signal.addEventListener('abort', () => {
68
81
  clearTimeout(timeout);
69
- reject(new DOMException('The operation was aborted.', 'AbortError'));
82
+ reject(
83
+ new DOMException('The operation was aborted.', 'AbortError'),
84
+ );
70
85
  });
71
86
  }
72
87
  });
@@ -83,9 +98,12 @@ describe('KTDataTable Race Condition Fixes', () => {
83
98
 
84
99
  describe('AbortController Integration', () => {
85
100
  it('should create AbortController for remote data requests', async () => {
86
- const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
87
- apiEndpoint: '/api/data',
88
- });
101
+ const datatable = new KTDataTable(
102
+ container.querySelector('[data-kt-datatable="true"]')!,
103
+ {
104
+ apiEndpoint: '/api/data',
105
+ },
106
+ );
89
107
 
90
108
  await waitFor(150);
91
109
 
@@ -95,9 +113,12 @@ describe('KTDataTable Race Condition Fixes', () => {
95
113
  });
96
114
 
97
115
  it('should use _isFetching flag to prevent concurrent requests', async () => {
98
- const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
99
- apiEndpoint: '/api/data',
100
- });
116
+ const datatable = new KTDataTable(
117
+ container.querySelector('[data-kt-datatable="true"]')!,
118
+ {
119
+ apiEndpoint: '/api/data',
120
+ },
121
+ );
101
122
 
102
123
  // Try to trigger search during initial fetch
103
124
  datatable.search('test'); // Should be blocked by _isFetching
@@ -109,9 +130,12 @@ describe('KTDataTable Race Condition Fixes', () => {
109
130
  });
110
131
 
111
132
  it('should allow new request after previous completes', async () => {
112
- const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
113
- apiEndpoint: '/api/data',
114
- });
133
+ const datatable = new KTDataTable(
134
+ container.querySelector('[data-kt-datatable="true"]')!,
135
+ {
136
+ apiEndpoint: '/api/data',
137
+ },
138
+ );
115
139
 
116
140
  // Wait for initial fetch to complete
117
141
  await waitFor(150);
@@ -127,9 +151,12 @@ describe('KTDataTable Race Condition Fixes', () => {
127
151
 
128
152
  it('should abort previous request when _performFetchRequest is called again', async () => {
129
153
  // This tests the AbortController logic directly by making multiple sequential requests
130
- const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
131
- apiEndpoint: '/api/data',
132
- });
154
+ const datatable = new KTDataTable(
155
+ container.querySelector('[data-kt-datatable="true"]')!,
156
+ {
157
+ apiEndpoint: '/api/data',
158
+ },
159
+ );
133
160
 
134
161
  await waitFor(150); // Complete initial request
135
162
 
@@ -154,33 +181,38 @@ describe('KTDataTable Race Condition Fixes', () => {
154
181
 
155
182
  describe('Request ID Sequencing', () => {
156
183
  it('should assign incremental request IDs for sequential requests', async () => {
157
- let requestIds: number[] = [];
184
+ const requestIds: number[] = [];
158
185
  let callCount = 0;
159
186
 
160
187
  // Mock to capture request sequence
161
- mockFetch.mockImplementation((url: RequestInfo | URL, options?: RequestInit) => {
162
- callCount++;
163
- const id = callCount;
164
- requestIds.push(id);
165
-
166
- return new Promise((resolve) => {
167
- setTimeout(() => {
168
- resolve(
169
- new Response(
170
- JSON.stringify({
171
- data: [{ id: id, name: `Item ${id}` }],
172
- totalCount: 1,
173
- }),
174
- { status: 200 },
175
- ),
176
- );
177
- }, 50);
178
- });
179
- });
188
+ mockFetch.mockImplementation(
189
+ (url: RequestInfo | URL, options?: RequestInit) => {
190
+ callCount++;
191
+ const id = callCount;
192
+ requestIds.push(id);
193
+
194
+ return new Promise((resolve) => {
195
+ setTimeout(() => {
196
+ resolve(
197
+ new Response(
198
+ JSON.stringify({
199
+ data: [{ id: id, name: `Item ${id}` }],
200
+ totalCount: 1,
201
+ }),
202
+ { status: 200 },
203
+ ),
204
+ );
205
+ }, 50);
206
+ });
207
+ },
208
+ );
180
209
 
181
- const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
182
- apiEndpoint: '/api/data',
183
- });
210
+ const datatable = new KTDataTable(
211
+ container.querySelector('[data-kt-datatable="true"]')!,
212
+ {
213
+ apiEndpoint: '/api/data',
214
+ },
215
+ );
184
216
 
185
217
  await waitFor(100); // Complete initial request
186
218
 
@@ -197,9 +229,12 @@ describe('KTDataTable Race Condition Fixes', () => {
197
229
  it('should have request ID validation logic in place', async () => {
198
230
  // This tests that request IDs are tracked internally
199
231
  // The actual stale response scenario is prevented by _isFetching flag
200
- const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
201
- apiEndpoint: '/api/data',
202
- });
232
+ const datatable = new KTDataTable(
233
+ container.querySelector('[data-kt-datatable="true"]')!,
234
+ {
235
+ apiEndpoint: '/api/data',
236
+ },
237
+ );
203
238
 
204
239
  await waitFor(150); // Complete initial
205
240
 
@@ -220,9 +255,12 @@ describe('KTDataTable Race Condition Fixes', () => {
220
255
 
221
256
  describe('_isFetching Flag Management', () => {
222
257
  it('should prevent concurrent fetch executions', async () => {
223
- const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
224
- apiEndpoint: '/api/data',
225
- });
258
+ const datatable = new KTDataTable(
259
+ container.querySelector('[data-kt-datatable="true"]')!,
260
+ {
261
+ apiEndpoint: '/api/data',
262
+ },
263
+ );
226
264
 
227
265
  // Try to trigger reload immediately (should be blocked by initial fetch)
228
266
  datatable.reload(); // Blocked by _isFetching
@@ -236,9 +274,12 @@ describe('KTDataTable Race Condition Fixes', () => {
236
274
  });
237
275
 
238
276
  it('should reset _isFetching flag after fetch completes', async () => {
239
- const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
240
- apiEndpoint: '/api/data',
241
- });
277
+ const datatable = new KTDataTable(
278
+ container.querySelector('[data-kt-datatable="true"]')!,
279
+ {
280
+ apiEndpoint: '/api/data',
281
+ },
282
+ );
242
283
 
243
284
  await waitFor(150); // Wait for initial fetch
244
285
 
@@ -251,28 +292,31 @@ describe('KTDataTable Race Condition Fixes', () => {
251
292
 
252
293
  it('should reset _isFetching flag even after fetch error', async () => {
253
294
  let callCount = 0;
254
- mockFetch.mockImplementation((url: RequestInfo | URL, options?: RequestInit) => {
255
- callCount++;
256
- if (callCount === 1) {
257
- // Return invalid JSON to trigger parse error
295
+ mockFetch.mockImplementation(
296
+ (url: RequestInfo | URL, options?: RequestInit) => {
297
+ callCount++;
298
+ if (callCount === 1) {
299
+ // Return invalid JSON to trigger parse error
300
+ return Promise.resolve(new Response('Not JSON', { status: 200 }));
301
+ }
258
302
  return Promise.resolve(
259
- new Response('Not JSON', { status: 200 }),
303
+ new Response(
304
+ JSON.stringify({
305
+ data: [{ id: 1, name: 'Success' }],
306
+ totalCount: 1,
307
+ }),
308
+ { status: 200 },
309
+ ),
260
310
  );
261
- }
262
- return Promise.resolve(
263
- new Response(
264
- JSON.stringify({
265
- data: [{ id: 1, name: 'Success' }],
266
- totalCount: 1,
267
- }),
268
- { status: 200 },
269
- ),
270
- );
271
- });
311
+ },
312
+ );
272
313
 
273
- const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
274
- apiEndpoint: '/api/data',
275
- });
314
+ const datatable = new KTDataTable(
315
+ container.querySelector('[data-kt-datatable="true"]')!,
316
+ {
317
+ apiEndpoint: '/api/data',
318
+ },
319
+ );
276
320
 
277
321
  await waitFor(150); // Initial request triggers parse error
278
322
 
@@ -286,7 +330,9 @@ describe('KTDataTable Race Condition Fixes', () => {
286
330
 
287
331
  describe('Loading Spinner Management', () => {
288
332
  it('should show spinner during fetch', async () => {
289
- const element = container.querySelector('[data-kt-datatable="true"]') as HTMLElement;
333
+ const element = container.querySelector(
334
+ '[data-kt-datatable="true"]',
335
+ ) as HTMLElement;
290
336
  const datatable = new KTDataTable(element, {
291
337
  apiEndpoint: '/api/data',
292
338
  });
@@ -301,7 +347,9 @@ describe('KTDataTable Race Condition Fixes', () => {
301
347
  });
302
348
 
303
349
  it('should keep spinner visible during overlapping requests', async () => {
304
- const element = container.querySelector('[data-kt-datatable="true"]') as HTMLElement;
350
+ const element = container.querySelector(
351
+ '[data-kt-datatable="true"]',
352
+ ) as HTMLElement;
305
353
  const datatable = new KTDataTable(element, {
306
354
  apiEndpoint: '/api/data',
307
355
  });
@@ -323,7 +371,9 @@ describe('KTDataTable Race Condition Fixes', () => {
323
371
  });
324
372
 
325
373
  it('should not flicker spinner during rapid interactions', async () => {
326
- const element = container.querySelector('[data-kt-datatable="true"]') as HTMLElement;
374
+ const element = container.querySelector(
375
+ '[data-kt-datatable="true"]',
376
+ ) as HTMLElement;
327
377
  const datatable = new KTDataTable(element, {
328
378
  apiEndpoint: '/api/data',
329
379
  });
@@ -362,7 +412,9 @@ describe('KTDataTable Race Condition Fixes', () => {
362
412
  it('should fire fetch event for successful requests', async () => {
363
413
  const fetchEvents: any[] = [];
364
414
 
365
- const element = container.querySelector('[data-kt-datatable="true"]') as HTMLElement;
415
+ const element = container.querySelector(
416
+ '[data-kt-datatable="true"]',
417
+ ) as HTMLElement;
366
418
  element.addEventListener('fetch', (e) => {
367
419
  fetchEvents.push(e);
368
420
  });
@@ -383,7 +435,9 @@ describe('KTDataTable Race Condition Fixes', () => {
383
435
  it('should fire fetched event after successful data load', async () => {
384
436
  const fetchedEvents: any[] = [];
385
437
 
386
- const element = container.querySelector('[data-kt-datatable="true"]') as HTMLElement;
438
+ const element = container.querySelector(
439
+ '[data-kt-datatable="true"]',
440
+ ) as HTMLElement;
387
441
  element.addEventListener('fetched', (e) => {
388
442
  fetchedEvents.push(e);
389
443
  });
@@ -401,7 +455,9 @@ describe('KTDataTable Race Condition Fixes', () => {
401
455
  it('should not fire error events for AbortError', async () => {
402
456
  const errorEvents: any[] = [];
403
457
 
404
- const element = container.querySelector('[data-kt-datatable="true"]') as HTMLElement;
458
+ const element = container.querySelector(
459
+ '[data-kt-datatable="true"]',
460
+ ) as HTMLElement;
405
461
  element.addEventListener('error.kt.datatable', (e) => {
406
462
  errorEvents.push(e);
407
463
  });
@@ -422,7 +478,9 @@ describe('KTDataTable Race Condition Fixes', () => {
422
478
 
423
479
  describe('Backward Compatibility', () => {
424
480
  it('should work with local data mode (no AbortController needed)', async () => {
425
- const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!);
481
+ const datatable = new KTDataTable(
482
+ container.querySelector('[data-kt-datatable="true"]')!,
483
+ );
426
484
 
427
485
  // Should not call fetch for local data
428
486
  expect(mockFetch).not.toHaveBeenCalled();
@@ -434,9 +492,12 @@ describe('KTDataTable Race Condition Fixes', () => {
434
492
  });
435
493
 
436
494
  it('should maintain existing API compatibility', async () => {
437
- const datatable = new KTDataTable(container.querySelector('[data-kt-datatable="true"]')!, {
438
- apiEndpoint: '/api/data',
439
- });
495
+ const datatable = new KTDataTable(
496
+ container.querySelector('[data-kt-datatable="true"]')!,
497
+ {
498
+ apiEndpoint: '/api/data',
499
+ },
500
+ );
440
501
 
441
502
  await waitFor(150);
442
503
 
@@ -452,4 +513,3 @@ describe('KTDataTable Race Condition Fixes', () => {
452
513
  });
453
514
  });
454
515
  });
455
-
@@ -45,7 +45,9 @@ Object.defineProperty(window, 'matchMedia', {
45
45
  value: (query: string) => ({
46
46
  matches: false,
47
47
  media: query,
48
- onchange: null as ((this: MediaQueryList, ev: MediaQueryListEvent) => any) | null,
48
+ onchange: null as
49
+ | ((this: MediaQueryList, ev: MediaQueryListEvent) => any)
50
+ | null,
49
51
  addListener: () => {}, // deprecated
50
52
  removeListener: () => {}, // deprecated
51
53
  addEventListener: () => {},
@@ -55,13 +57,16 @@ Object.defineProperty(window, 'matchMedia', {
55
57
  });
56
58
 
57
59
  // Export utilities that tests can use
58
- export const waitFor = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
60
+ export const waitFor = (ms: number) =>
61
+ new Promise((resolve) => setTimeout(resolve, ms));
59
62
 
60
- export const createMockElement = (tag: string, attributes: Record<string, string> = {}) => {
63
+ export const createMockElement = (
64
+ tag: string,
65
+ attributes: Record<string, string> = {},
66
+ ) => {
61
67
  const el = document.createElement(tag);
62
68
  Object.entries(attributes).forEach(([key, value]) => {
63
69
  el.setAttribute(key, value);
64
70
  });
65
71
  return el;
66
72
  };
67
-