@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.
- package/dist/ktui.js +11061 -10799
- package/dist/ktui.min.js +1 -1
- package/dist/ktui.min.js.map +1 -1
- package/dist/styles.css +33 -27
- package/lib/cjs/components/collapse/collapse.js +0 -2
- package/lib/cjs/components/collapse/collapse.js.map +1 -1
- package/lib/cjs/components/component.js +11 -0
- package/lib/cjs/components/component.js.map +1 -1
- package/lib/cjs/components/datatable/datatable-sort.js +80 -11
- package/lib/cjs/components/datatable/datatable-sort.js.map +1 -1
- package/lib/cjs/components/datatable/datatable.js +77 -24
- package/lib/cjs/components/datatable/datatable.js.map +1 -1
- package/lib/cjs/components/drawer/drawer.js +63 -42
- package/lib/cjs/components/drawer/drawer.js.map +1 -1
- package/lib/cjs/components/dropdown/dropdown.js +6 -0
- package/lib/cjs/components/dropdown/dropdown.js.map +1 -1
- package/lib/cjs/components/scrollto/scrollto.js +0 -2
- package/lib/cjs/components/scrollto/scrollto.js.map +1 -1
- package/lib/cjs/components/select/combobox.js.map +1 -1
- package/lib/cjs/components/select/dropdown.js.map +1 -1
- package/lib/cjs/components/select/remote.js.map +1 -1
- package/lib/cjs/components/select/search.js +9 -5
- package/lib/cjs/components/select/search.js.map +1 -1
- package/lib/cjs/components/select/select.js +29 -9
- package/lib/cjs/components/select/select.js.map +1 -1
- package/lib/cjs/components/select/tags.js.map +1 -1
- package/lib/cjs/components/select/templates.js.map +1 -1
- package/lib/cjs/components/select/utils.js +10 -0
- package/lib/cjs/components/select/utils.js.map +1 -1
- package/lib/cjs/components/sticky/sticky.js +104 -24
- package/lib/cjs/components/sticky/sticky.js.map +1 -1
- package/lib/cjs/components/theme-switch/theme-switch.js +0 -2
- package/lib/cjs/components/theme-switch/theme-switch.js.map +1 -1
- package/lib/cjs/components/toast/toast.js +1 -2
- package/lib/cjs/components/toast/toast.js.map +1 -1
- package/lib/cjs/helpers/dom.js +0 -2
- package/lib/cjs/helpers/dom.js.map +1 -1
- package/lib/esm/components/collapse/collapse.js +0 -2
- package/lib/esm/components/collapse/collapse.js.map +1 -1
- package/lib/esm/components/component.js +11 -0
- package/lib/esm/components/component.js.map +1 -1
- package/lib/esm/components/datatable/datatable-sort.js +80 -11
- package/lib/esm/components/datatable/datatable-sort.js.map +1 -1
- package/lib/esm/components/datatable/datatable.js +77 -24
- package/lib/esm/components/datatable/datatable.js.map +1 -1
- package/lib/esm/components/drawer/drawer.js +63 -42
- package/lib/esm/components/drawer/drawer.js.map +1 -1
- package/lib/esm/components/dropdown/dropdown.js +6 -0
- package/lib/esm/components/dropdown/dropdown.js.map +1 -1
- package/lib/esm/components/scrollto/scrollto.js +0 -2
- package/lib/esm/components/scrollto/scrollto.js.map +1 -1
- package/lib/esm/components/select/combobox.js.map +1 -1
- package/lib/esm/components/select/dropdown.js.map +1 -1
- package/lib/esm/components/select/remote.js.map +1 -1
- package/lib/esm/components/select/search.js +9 -5
- package/lib/esm/components/select/search.js.map +1 -1
- package/lib/esm/components/select/select.js +29 -9
- package/lib/esm/components/select/select.js.map +1 -1
- package/lib/esm/components/select/tags.js.map +1 -1
- package/lib/esm/components/select/templates.js.map +1 -1
- package/lib/esm/components/select/utils.js +10 -0
- package/lib/esm/components/select/utils.js.map +1 -1
- package/lib/esm/components/sticky/sticky.js +104 -24
- package/lib/esm/components/sticky/sticky.js.map +1 -1
- package/lib/esm/components/theme-switch/theme-switch.js +0 -2
- package/lib/esm/components/theme-switch/theme-switch.js.map +1 -1
- package/lib/esm/components/toast/toast.js +1 -2
- package/lib/esm/components/toast/toast.js.map +1 -1
- package/lib/esm/helpers/dom.js +0 -2
- package/lib/esm/helpers/dom.js.map +1 -1
- package/package.json +14 -7
- package/src/components/collapse/collapse.ts +0 -3
- package/src/components/component.ts +14 -4
- package/src/components/datatable/__tests__/currency-sort.test.ts +108 -0
- package/src/components/datatable/__tests__/multi-row-headers.test.ts +121 -0
- package/src/components/datatable/__tests__/pagination-reset.test.ts +13 -5
- package/src/components/datatable/__tests__/race-conditions.test.ts +138 -78
- package/src/components/datatable/__tests__/setup.ts +9 -4
- package/src/components/datatable/datatable-sort.ts +88 -10
- package/src/components/datatable/datatable.css +4 -4
- package/src/components/datatable/datatable.ts +91 -30
- package/src/components/datatable/types.ts +16 -0
- package/src/components/drawer/drawer.ts +97 -57
- package/src/components/drawer/types.ts +4 -2
- package/src/components/dropdown/dropdown.ts +8 -1
- package/src/components/scrollto/scrollto.ts +0 -3
- package/src/components/select/__tests__/ux-behaviors.test.ts +274 -8
- package/src/components/select/combobox.ts +0 -1
- package/src/components/select/dropdown.ts +0 -2
- package/src/components/select/remote.ts +1 -6
- package/src/components/select/search.ts +14 -7
- package/src/components/select/select.ts +29 -29
- package/src/components/select/tags.ts +0 -1
- package/src/components/select/templates.ts +8 -8
- package/src/components/select/utils.ts +15 -2
- package/src/components/sticky/__tests__/sticky.test.ts +205 -0
- package/src/components/sticky/sticky.ts +119 -21
- package/src/components/sticky/types.ts +3 -0
- package/src/components/theme-switch/theme-switch.ts +0 -3
- package/src/components/toast/toast.ts +3 -2
- 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)
|
|
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({
|
|
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
|
-
|
|
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, {
|
|
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 {
|
|
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(
|
|
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
|
-
{
|
|
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(
|
|
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(
|
|
87
|
-
|
|
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(
|
|
99
|
-
|
|
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(
|
|
113
|
-
|
|
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(
|
|
131
|
-
|
|
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
|
-
|
|
184
|
+
const requestIds: number[] = [];
|
|
158
185
|
let callCount = 0;
|
|
159
186
|
|
|
160
187
|
// Mock to capture request sequence
|
|
161
|
-
mockFetch.mockImplementation(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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(
|
|
182
|
-
|
|
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(
|
|
201
|
-
|
|
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(
|
|
224
|
-
|
|
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(
|
|
240
|
-
|
|
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(
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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(
|
|
303
|
+
new Response(
|
|
304
|
+
JSON.stringify({
|
|
305
|
+
data: [{ id: 1, name: 'Success' }],
|
|
306
|
+
totalCount: 1,
|
|
307
|
+
}),
|
|
308
|
+
{ status: 200 },
|
|
309
|
+
),
|
|
260
310
|
);
|
|
261
|
-
}
|
|
262
|
-
|
|
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(
|
|
274
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
438
|
-
|
|
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
|
|
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) =>
|
|
60
|
+
export const waitFor = (ms: number) =>
|
|
61
|
+
new Promise((resolve) => setTimeout(resolve, ms));
|
|
59
62
|
|
|
60
|
-
export const createMockElement = (
|
|
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
|
-
|