@keenthemes/ktui 1.2.5 → 1.2.7
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/README.md +14 -5
- package/dist/ktui.js +1538 -786
- package/dist/ktui.min.js +1 -1
- package/dist/ktui.min.js.map +1 -1
- package/dist/styles.css +85 -5
- package/lib/cjs/components/datatable/datatable-checkbox.d.ts +37 -1
- package/lib/cjs/components/datatable/datatable-checkbox.d.ts.map +1 -1
- package/lib/cjs/components/datatable/datatable-checkbox.js +143 -156
- package/lib/cjs/components/datatable/datatable-checkbox.js.map +1 -1
- package/lib/cjs/components/datatable/datatable-column-utils.d.ts +30 -0
- package/lib/cjs/components/datatable/datatable-column-utils.d.ts.map +1 -0
- package/lib/cjs/components/datatable/datatable-column-utils.js +42 -0
- package/lib/cjs/components/datatable/datatable-column-utils.js.map +1 -0
- package/lib/cjs/components/datatable/datatable-contracts.d.ts +2 -4
- package/lib/cjs/components/datatable/datatable-contracts.d.ts.map +1 -1
- package/lib/cjs/components/datatable/datatable-defaults.d.ts +20 -0
- package/lib/cjs/components/datatable/datatable-defaults.d.ts.map +1 -0
- package/lib/cjs/components/datatable/datatable-defaults.js +193 -0
- package/lib/cjs/components/datatable/datatable-defaults.js.map +1 -0
- package/lib/cjs/components/datatable/datatable-layout-plugin.d.ts +7 -0
- package/lib/cjs/components/datatable/datatable-layout-plugin.d.ts.map +1 -0
- package/lib/cjs/components/datatable/datatable-layout-plugin.js +338 -0
- package/lib/cjs/components/datatable/datatable-layout-plugin.js.map +1 -0
- package/lib/cjs/components/datatable/datatable-local-provider.d.ts +2 -2
- package/lib/cjs/components/datatable/datatable-local-provider.d.ts.map +1 -1
- package/lib/cjs/components/datatable/datatable-local-provider.js +85 -27
- package/lib/cjs/components/datatable/datatable-local-provider.js.map +1 -1
- package/lib/cjs/components/datatable/datatable-pagination-renderer.d.ts.map +1 -1
- package/lib/cjs/components/datatable/datatable-pagination-renderer.js +13 -13
- package/lib/cjs/components/datatable/datatable-pagination-renderer.js.map +1 -1
- package/lib/cjs/components/datatable/datatable-registry.d.ts +18 -0
- package/lib/cjs/components/datatable/datatable-registry.d.ts.map +1 -0
- package/lib/cjs/components/datatable/datatable-registry.js +66 -0
- package/lib/cjs/components/datatable/datatable-registry.js.map +1 -0
- package/lib/cjs/components/datatable/datatable-remote-provider.d.ts.map +1 -1
- package/lib/cjs/components/datatable/datatable-remote-provider.js +1 -2
- package/lib/cjs/components/datatable/datatable-remote-provider.js.map +1 -1
- package/lib/cjs/components/datatable/datatable-search-handler.d.ts +10 -0
- package/lib/cjs/components/datatable/datatable-search-handler.d.ts.map +1 -0
- package/lib/cjs/components/datatable/datatable-search-handler.js +65 -0
- package/lib/cjs/components/datatable/datatable-search-handler.js.map +1 -0
- package/lib/cjs/components/datatable/datatable-sort.d.ts +31 -4
- package/lib/cjs/components/datatable/datatable-sort.d.ts.map +1 -1
- package/lib/cjs/components/datatable/datatable-sort.js +86 -58
- package/lib/cjs/components/datatable/datatable-sort.js.map +1 -1
- package/lib/cjs/components/datatable/datatable-spinner.d.ts +30 -0
- package/lib/cjs/components/datatable/datatable-spinner.d.ts.map +1 -0
- package/lib/cjs/components/datatable/datatable-spinner.js +54 -0
- package/lib/cjs/components/datatable/datatable-spinner.js.map +1 -0
- package/lib/cjs/components/datatable/datatable-state-persistence.d.ts +19 -0
- package/lib/cjs/components/datatable/datatable-state-persistence.d.ts.map +1 -0
- package/lib/cjs/components/datatable/datatable-state-persistence.js +59 -0
- package/lib/cjs/components/datatable/datatable-state-persistence.js.map +1 -0
- package/lib/cjs/components/datatable/datatable-table-renderer.d.ts +2 -0
- package/lib/cjs/components/datatable/datatable-table-renderer.d.ts.map +1 -1
- package/lib/cjs/components/datatable/datatable-table-renderer.js +75 -16
- package/lib/cjs/components/datatable/datatable-table-renderer.js.map +1 -1
- package/lib/cjs/components/datatable/datatable-utils.d.ts +10 -0
- package/lib/cjs/components/datatable/datatable-utils.d.ts.map +1 -0
- package/lib/cjs/components/datatable/datatable-utils.js +15 -0
- package/lib/cjs/components/datatable/datatable-utils.js.map +1 -0
- package/lib/cjs/components/datatable/datatable.d.ts +35 -34
- package/lib/cjs/components/datatable/datatable.d.ts.map +1 -1
- package/lib/cjs/components/datatable/datatable.js +233 -497
- package/lib/cjs/components/datatable/datatable.js.map +1 -1
- package/lib/cjs/components/datatable/index.d.ts +1 -1
- package/lib/cjs/components/datatable/index.d.ts.map +1 -1
- package/lib/cjs/components/datatable/types.d.ts +127 -11
- package/lib/cjs/components/datatable/types.d.ts.map +1 -1
- package/lib/cjs/index.d.ts +1 -1
- package/lib/cjs/index.d.ts.map +1 -1
- package/lib/cjs/index.js +6 -0
- package/lib/cjs/index.js.map +1 -1
- package/lib/esm/components/datatable/datatable-checkbox.d.ts +37 -1
- package/lib/esm/components/datatable/datatable-checkbox.d.ts.map +1 -1
- package/lib/esm/components/datatable/datatable-checkbox.js +142 -155
- package/lib/esm/components/datatable/datatable-checkbox.js.map +1 -1
- package/lib/esm/components/datatable/datatable-column-utils.d.ts +30 -0
- package/lib/esm/components/datatable/datatable-column-utils.d.ts.map +1 -0
- package/lib/esm/components/datatable/datatable-column-utils.js +38 -0
- package/lib/esm/components/datatable/datatable-column-utils.js.map +1 -0
- package/lib/esm/components/datatable/datatable-contracts.d.ts +2 -4
- package/lib/esm/components/datatable/datatable-contracts.d.ts.map +1 -1
- package/lib/esm/components/datatable/datatable-defaults.d.ts +20 -0
- package/lib/esm/components/datatable/datatable-defaults.d.ts.map +1 -0
- package/lib/esm/components/datatable/datatable-defaults.js +190 -0
- package/lib/esm/components/datatable/datatable-defaults.js.map +1 -0
- package/lib/esm/components/datatable/datatable-layout-plugin.d.ts +7 -0
- package/lib/esm/components/datatable/datatable-layout-plugin.d.ts.map +1 -0
- package/lib/esm/components/datatable/datatable-layout-plugin.js +334 -0
- package/lib/esm/components/datatable/datatable-layout-plugin.js.map +1 -0
- package/lib/esm/components/datatable/datatable-local-provider.d.ts +2 -2
- package/lib/esm/components/datatable/datatable-local-provider.d.ts.map +1 -1
- package/lib/esm/components/datatable/datatable-local-provider.js +85 -27
- package/lib/esm/components/datatable/datatable-local-provider.js.map +1 -1
- package/lib/esm/components/datatable/datatable-pagination-renderer.d.ts.map +1 -1
- package/lib/esm/components/datatable/datatable-pagination-renderer.js +13 -13
- package/lib/esm/components/datatable/datatable-pagination-renderer.js.map +1 -1
- package/lib/esm/components/datatable/datatable-registry.d.ts +18 -0
- package/lib/esm/components/datatable/datatable-registry.d.ts.map +1 -0
- package/lib/esm/components/datatable/datatable-registry.js +63 -0
- package/lib/esm/components/datatable/datatable-registry.js.map +1 -0
- package/lib/esm/components/datatable/datatable-remote-provider.d.ts.map +1 -1
- package/lib/esm/components/datatable/datatable-remote-provider.js +1 -2
- package/lib/esm/components/datatable/datatable-remote-provider.js.map +1 -1
- package/lib/esm/components/datatable/datatable-search-handler.d.ts +10 -0
- package/lib/esm/components/datatable/datatable-search-handler.d.ts.map +1 -0
- package/lib/esm/components/datatable/datatable-search-handler.js +62 -0
- package/lib/esm/components/datatable/datatable-search-handler.js.map +1 -0
- package/lib/esm/components/datatable/datatable-sort.d.ts +31 -4
- package/lib/esm/components/datatable/datatable-sort.d.ts.map +1 -1
- package/lib/esm/components/datatable/datatable-sort.js +85 -57
- package/lib/esm/components/datatable/datatable-sort.js.map +1 -1
- package/lib/esm/components/datatable/datatable-spinner.d.ts +30 -0
- package/lib/esm/components/datatable/datatable-spinner.d.ts.map +1 -0
- package/lib/esm/components/datatable/datatable-spinner.js +51 -0
- package/lib/esm/components/datatable/datatable-spinner.js.map +1 -0
- package/lib/esm/components/datatable/datatable-state-persistence.d.ts +19 -0
- package/lib/esm/components/datatable/datatable-state-persistence.d.ts.map +1 -0
- package/lib/esm/components/datatable/datatable-state-persistence.js +55 -0
- package/lib/esm/components/datatable/datatable-state-persistence.js.map +1 -0
- package/lib/esm/components/datatable/datatable-table-renderer.d.ts +2 -0
- package/lib/esm/components/datatable/datatable-table-renderer.d.ts.map +1 -1
- package/lib/esm/components/datatable/datatable-table-renderer.js +75 -16
- package/lib/esm/components/datatable/datatable-table-renderer.js.map +1 -1
- package/lib/esm/components/datatable/datatable-utils.d.ts +10 -0
- package/lib/esm/components/datatable/datatable-utils.d.ts.map +1 -0
- package/lib/esm/components/datatable/datatable-utils.js +12 -0
- package/lib/esm/components/datatable/datatable-utils.js.map +1 -0
- package/lib/esm/components/datatable/datatable.d.ts +35 -34
- package/lib/esm/components/datatable/datatable.d.ts.map +1 -1
- package/lib/esm/components/datatable/datatable.js +235 -499
- package/lib/esm/components/datatable/datatable.js.map +1 -1
- package/lib/esm/components/datatable/index.d.ts +1 -1
- package/lib/esm/components/datatable/index.d.ts.map +1 -1
- package/lib/esm/components/datatable/types.d.ts +127 -11
- package/lib/esm/components/datatable/types.d.ts.map +1 -1
- package/lib/esm/index.d.ts +1 -1
- package/lib/esm/index.d.ts.map +1 -1
- package/lib/esm/index.js +6 -0
- package/lib/esm/index.js.map +1 -1
- package/package.json +5 -1
- package/skills/ktui/SKILL.md +711 -0
- package/skills/ktui-datatable/SKILL.md +302 -0
- package/skills/ktui-install/SKILL.md +150 -0
- package/skills/ktui-select/SKILL.md +271 -0
- package/src/components/__tests__/component.test.ts +347 -0
- package/src/components/collapse/collapse.css +2 -2
- package/src/components/datatable/__tests__/architecture-boundaries.test.ts +56 -8
- package/src/components/datatable/__tests__/currency-sort.test.ts +25 -28
- package/src/components/datatable/__tests__/datatable-checkbox.test.ts +527 -0
- package/src/components/datatable/__tests__/datatable-column-utils.test.ts +117 -0
- package/src/components/datatable/__tests__/datatable-defaults.test.ts +57 -0
- package/src/components/datatable/__tests__/datatable-finalize-extended.test.ts +361 -0
- package/src/components/datatable/__tests__/datatable-fixed-layout.test.ts +427 -0
- package/src/components/datatable/__tests__/datatable-improvements.test.ts +484 -0
- package/src/components/datatable/__tests__/datatable-pagination-extended.test.ts +508 -0
- package/src/components/datatable/__tests__/datatable-public-api.test.ts +269 -0
- package/src/components/datatable/__tests__/datatable-registry.test.ts +172 -0
- package/src/components/datatable/__tests__/datatable-remote-provider.test.ts +468 -0
- package/src/components/datatable/__tests__/datatable-search-handler.test.ts +124 -0
- package/src/components/datatable/__tests__/datatable-sort-extended.test.ts +417 -0
- package/src/components/datatable/__tests__/datatable-spinner.test.ts +95 -0
- package/src/components/datatable/__tests__/datatable-table-renderer-extended.test.ts +425 -0
- package/src/components/datatable/__tests__/datatable-types.test.ts +117 -0
- package/src/components/datatable/__tests__/datatable-utils.test.ts +52 -0
- package/src/components/datatable/__tests__/locked-layout.test.ts +257 -0
- package/src/components/datatable/__tests__/multi-row-headers.test.ts +7 -7
- package/src/components/datatable/__tests__/pagination-reset.test.ts +147 -6
- package/src/components/datatable/__tests__/race-conditions.test.ts +11 -11
- package/src/components/datatable/__tests__/setup.ts +12 -4
- package/src/components/datatable/datatable-checkbox.ts +139 -143
- package/src/components/datatable/datatable-column-utils.ts +63 -0
- package/src/components/datatable/datatable-contracts.ts +2 -3
- package/src/components/datatable/datatable-defaults.ts +204 -0
- package/src/components/datatable/datatable-layout-plugin.ts +459 -0
- package/src/components/datatable/datatable-local-provider.ts +106 -35
- package/src/components/datatable/datatable-pagination-renderer.ts +13 -15
- package/src/components/datatable/datatable-registry.ts +89 -0
- package/src/components/datatable/datatable-remote-provider.ts +1 -3
- package/src/components/datatable/datatable-search-handler.ts +97 -0
- package/src/components/datatable/datatable-sort.ts +111 -66
- package/src/components/datatable/datatable-spinner.ts +103 -0
- package/src/components/datatable/datatable-state-persistence.ts +67 -0
- package/src/components/datatable/datatable-table-renderer.ts +81 -18
- package/src/components/datatable/datatable-utils.ts +12 -0
- package/src/components/datatable/datatable.css +98 -0
- package/src/components/datatable/datatable.ts +288 -583
- package/src/components/datatable/index.ts +8 -0
- package/src/components/datatable/types.ts +157 -23
- package/src/helpers/__tests__/dom.test.ts +776 -0
- package/src/helpers/__tests__/utils.test.ts +332 -0
- package/src/index.ts +15 -0
- package/skills/ktui-components/SKILL.md +0 -41
- package/skills/ktui-theming/SKILL.md +0 -50
- package/src/components/datatable/datatable-event-adapter.ts +0 -21
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { KTDataTableRemoteDataProvider } from '../datatable-remote-provider';
|
|
3
|
+
import type { KTDataTableEventAdapter, KTDataTableStateStore } from '../datatable-contracts';
|
|
4
|
+
import type { KTDataTableConfigInterface } from '../types';
|
|
5
|
+
|
|
6
|
+
function createMockStateStore(overrides = {}) {
|
|
7
|
+
return {
|
|
8
|
+
getState: vi.fn(() => ({
|
|
9
|
+
page: 1,
|
|
10
|
+
pageSize: 10,
|
|
11
|
+
sortField: undefined,
|
|
12
|
+
sortOrder: undefined,
|
|
13
|
+
filters: [],
|
|
14
|
+
search: '',
|
|
15
|
+
...overrides,
|
|
16
|
+
})),
|
|
17
|
+
setState: vi.fn(),
|
|
18
|
+
setPage: vi.fn(),
|
|
19
|
+
setPageSize: vi.fn(),
|
|
20
|
+
setSort: vi.fn(),
|
|
21
|
+
setSearch: vi.fn(),
|
|
22
|
+
setFilter: vi.fn(),
|
|
23
|
+
replaceState: vi.fn(),
|
|
24
|
+
patchState: vi.fn(),
|
|
25
|
+
setOriginalData: vi.fn(),
|
|
26
|
+
} as unknown as KTDataTableStateStore;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createMockEventAdapter() {
|
|
30
|
+
return {
|
|
31
|
+
emit: vi.fn(),
|
|
32
|
+
} as unknown as KTDataTableEventAdapter;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createMockConfig(
|
|
36
|
+
overrides: Partial<KTDataTableConfigInterface> = {},
|
|
37
|
+
): KTDataTableConfigInterface {
|
|
38
|
+
return {
|
|
39
|
+
requestMethod: 'GET',
|
|
40
|
+
apiEndpoint: 'https://api.example.com/data',
|
|
41
|
+
...overrides,
|
|
42
|
+
} as KTDataTableConfigInterface;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function createProvider(options: {
|
|
46
|
+
config?: Partial<KTDataTableConfigInterface>;
|
|
47
|
+
stateOverrides?: Record<string, unknown>;
|
|
48
|
+
} = {}) {
|
|
49
|
+
const stateStore = createMockStateStore(options.stateOverrides);
|
|
50
|
+
const eventAdapter = createMockEventAdapter();
|
|
51
|
+
const noticeOnTable = vi.fn();
|
|
52
|
+
const config = createMockConfig(options.config);
|
|
53
|
+
const provider = new KTDataTableRemoteDataProvider({
|
|
54
|
+
config,
|
|
55
|
+
createUrl: (path: string) => new URL(path, 'https://api.example.com'),
|
|
56
|
+
eventAdapter,
|
|
57
|
+
noticeOnTable,
|
|
58
|
+
stateStore,
|
|
59
|
+
});
|
|
60
|
+
return { provider, stateStore, eventAdapter, noticeOnTable };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('KTDataTableRemoteDataProvider', () => {
|
|
64
|
+
let fetchMock: ReturnType<typeof vi.fn>;
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
fetchMock = vi.fn();
|
|
68
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
vi.restoreAllMocks();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('Successful fetch (GET)', () => {
|
|
76
|
+
it('fetch() with GET method appends query params to URL', async () => {
|
|
77
|
+
const mockResponse = { data: [{ id: 1 }], totalCount: 1 };
|
|
78
|
+
fetchMock.mockResolvedValue({
|
|
79
|
+
json: () => Promise.resolve(mockResponse),
|
|
80
|
+
ok: true,
|
|
81
|
+
status: 200,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const { provider } = createProvider({
|
|
85
|
+
config: { requestMethod: 'GET', apiEndpoint: 'https://api.example.com/data' },
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await provider.fetch();
|
|
89
|
+
|
|
90
|
+
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
91
|
+
expect(calledUrl).toContain('page=');
|
|
92
|
+
expect(calledUrl).toContain('size=');
|
|
93
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
94
|
+
expect.any(String),
|
|
95
|
+
expect.objectContaining({ method: 'GET' }),
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('fetch() returns data and totalCount from response', async () => {
|
|
100
|
+
const mockResponse = { data: [{ id: 1 }, { id: 2 }], totalCount: 100 };
|
|
101
|
+
fetchMock.mockResolvedValue({
|
|
102
|
+
json: () => Promise.resolve(mockResponse),
|
|
103
|
+
ok: true,
|
|
104
|
+
status: 200,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const { provider } = createProvider();
|
|
108
|
+
const result = await provider.fetch();
|
|
109
|
+
|
|
110
|
+
expect(result.data).toEqual([{ id: 1 }, { id: 2 }]);
|
|
111
|
+
expect(result.totalItems).toBe(100);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('fetch() applies mapResponse when provided', async () => {
|
|
115
|
+
const mockResponse = { data: [{ id: 1 }], totalCount: 1 };
|
|
116
|
+
const mappedResponse = { data: [{ id: 1, mapped: true }], totalCount: 1 };
|
|
117
|
+
fetchMock.mockResolvedValue({
|
|
118
|
+
json: () => Promise.resolve(mockResponse),
|
|
119
|
+
ok: true,
|
|
120
|
+
status: 200,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const { provider } = createProvider({
|
|
124
|
+
config: {
|
|
125
|
+
requestMethod: 'GET',
|
|
126
|
+
apiEndpoint: 'https://api.example.com/data',
|
|
127
|
+
mapResponse: () => mappedResponse as any,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const result = await provider.fetch();
|
|
132
|
+
expect(result.data).toEqual([{ id: 1, mapped: true }]);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('Successful fetch (POST)', () => {
|
|
137
|
+
it('fetch() with POST method sends queryParams as body', async () => {
|
|
138
|
+
const mockResponse = { data: [], totalCount: 0 };
|
|
139
|
+
fetchMock.mockResolvedValue({
|
|
140
|
+
json: () => Promise.resolve(mockResponse),
|
|
141
|
+
ok: true,
|
|
142
|
+
status: 200,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const { provider } = createProvider({
|
|
146
|
+
config: { requestMethod: 'POST', apiEndpoint: 'https://api.example.com/data' },
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await provider.fetch();
|
|
150
|
+
|
|
151
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
152
|
+
'https://api.example.com/data',
|
|
153
|
+
expect.objectContaining({
|
|
154
|
+
method: 'POST',
|
|
155
|
+
body: expect.any(URLSearchParams),
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('Query params', () => {
|
|
162
|
+
it('getQueryParams includes page, size from state', async () => {
|
|
163
|
+
const mockResponse = { data: [], totalCount: 0 };
|
|
164
|
+
fetchMock.mockResolvedValue({
|
|
165
|
+
json: () => Promise.resolve(mockResponse),
|
|
166
|
+
ok: true,
|
|
167
|
+
status: 200,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const { provider } = createProvider({
|
|
171
|
+
stateOverrides: { page: 3, pageSize: 25 },
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
await provider.fetch();
|
|
175
|
+
|
|
176
|
+
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
177
|
+
expect(calledUrl).toContain('page=3');
|
|
178
|
+
expect(calledUrl).toContain('size=25');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('getQueryParams includes sortField, sortOrder when defined', async () => {
|
|
182
|
+
const mockResponse = { data: [], totalCount: 0 };
|
|
183
|
+
fetchMock.mockResolvedValue({
|
|
184
|
+
json: () => Promise.resolve(mockResponse),
|
|
185
|
+
ok: true,
|
|
186
|
+
status: 200,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const { provider } = createProvider({
|
|
190
|
+
stateOverrides: { sortField: 'name', sortOrder: 'asc' },
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await provider.fetch();
|
|
194
|
+
|
|
195
|
+
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
196
|
+
expect(calledUrl).toContain('sortField=name');
|
|
197
|
+
expect(calledUrl).toContain('sortOrder=asc');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('getQueryParams includes filters as JSON string when present', async () => {
|
|
201
|
+
const mockResponse = { data: [], totalCount: 0 };
|
|
202
|
+
fetchMock.mockResolvedValue({
|
|
203
|
+
json: () => Promise.resolve(mockResponse),
|
|
204
|
+
ok: true,
|
|
205
|
+
status: 200,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const { provider } = createProvider({
|
|
209
|
+
stateOverrides: {
|
|
210
|
+
filters: [{ column: 'status', type: 'text', value: 'active' }],
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
await provider.fetch();
|
|
215
|
+
|
|
216
|
+
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
217
|
+
const url = new URL(calledUrl);
|
|
218
|
+
const filters = url.searchParams.get('filters');
|
|
219
|
+
expect(filters).toBeTruthy();
|
|
220
|
+
expect(JSON.parse(filters!)).toEqual([
|
|
221
|
+
{ column: 'status', type: 'text', value: 'active' },
|
|
222
|
+
]);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('getQueryParams includes search as string', async () => {
|
|
226
|
+
const mockResponse = { data: [], totalCount: 0 };
|
|
227
|
+
fetchMock.mockResolvedValue({
|
|
228
|
+
json: () => Promise.resolve(mockResponse),
|
|
229
|
+
ok: true,
|
|
230
|
+
status: 200,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const { provider } = createProvider({
|
|
234
|
+
stateOverrides: { search: 'hello' },
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await provider.fetch();
|
|
238
|
+
|
|
239
|
+
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
240
|
+
expect(calledUrl).toContain('search=hello');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('getQueryParams includes search as JSON.stringify when object', async () => {
|
|
244
|
+
const mockResponse = { data: [], totalCount: 0 };
|
|
245
|
+
fetchMock.mockResolvedValue({
|
|
246
|
+
json: () => Promise.resolve(mockResponse),
|
|
247
|
+
ok: true,
|
|
248
|
+
status: 200,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const searchObj = { query: 'test', category: 'all' };
|
|
252
|
+
const { provider } = createProvider({
|
|
253
|
+
stateOverrides: { search: searchObj },
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
await provider.fetch();
|
|
257
|
+
|
|
258
|
+
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
259
|
+
const url = new URL(calledUrl);
|
|
260
|
+
expect(url.searchParams.get('search')).toEqual(
|
|
261
|
+
JSON.stringify(searchObj),
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('getQueryParams applies mapRequest when provided', async () => {
|
|
266
|
+
const mockResponse = { data: [], totalCount: 0 };
|
|
267
|
+
fetchMock.mockResolvedValue({
|
|
268
|
+
json: () => Promise.resolve(mockResponse),
|
|
269
|
+
ok: true,
|
|
270
|
+
status: 200,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const { provider } = createProvider({
|
|
274
|
+
config: {
|
|
275
|
+
requestMethod: 'GET',
|
|
276
|
+
apiEndpoint: 'https://api.example.com/data',
|
|
277
|
+
mapRequest: (params: URLSearchParams) => {
|
|
278
|
+
params.set('custom', 'value');
|
|
279
|
+
return params;
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
await provider.fetch();
|
|
285
|
+
|
|
286
|
+
const calledUrl = fetchMock.mock.calls[0][0] as string;
|
|
287
|
+
expect(calledUrl).toContain('custom=value');
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe('Error handling', () => {
|
|
292
|
+
it('fetch() network error fires error event and calls noticeOnTable', async () => {
|
|
293
|
+
const networkError = new TypeError('Failed to fetch');
|
|
294
|
+
fetchMock.mockRejectedValue(networkError);
|
|
295
|
+
|
|
296
|
+
const { provider, eventAdapter, noticeOnTable } = createProvider();
|
|
297
|
+
|
|
298
|
+
await expect(provider.fetch()).rejects.toThrow();
|
|
299
|
+
expect(eventAdapter.emit).toHaveBeenCalledWith('error', {
|
|
300
|
+
error: networkError,
|
|
301
|
+
});
|
|
302
|
+
expect(noticeOnTable).toHaveBeenCalledWith(
|
|
303
|
+
expect.stringContaining('Error performing fetch request'),
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('fetch() JSON parse error fires fetchError event with response details', async () => {
|
|
308
|
+
fetchMock.mockResolvedValue({
|
|
309
|
+
json: () => Promise.reject(new SyntaxError('Unexpected token')),
|
|
310
|
+
ok: true,
|
|
311
|
+
status: 200,
|
|
312
|
+
statusText: 'OK',
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const { provider, eventAdapter } = createProvider();
|
|
316
|
+
const result = await provider.fetch();
|
|
317
|
+
|
|
318
|
+
expect(result.skipped).toBe(true);
|
|
319
|
+
expect(eventAdapter.emit).toHaveBeenCalledWith(
|
|
320
|
+
'fetchError',
|
|
321
|
+
expect.objectContaining({
|
|
322
|
+
status: 200,
|
|
323
|
+
statusText: 'OK',
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('fetch() AbortError returns { data: [], totalItems: 0, skipped: true }', async () => {
|
|
329
|
+
const abortError = new DOMException('The operation was aborted', 'AbortError');
|
|
330
|
+
fetchMock.mockRejectedValue(abortError);
|
|
331
|
+
|
|
332
|
+
const { provider } = createProvider();
|
|
333
|
+
const result = await provider.fetch();
|
|
334
|
+
|
|
335
|
+
expect(result).toEqual({ data: [], totalItems: 0, skipped: true });
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe('Request ID race conditions', () => {
|
|
340
|
+
it('second fetch() before first completes → first returns skipped', async () => {
|
|
341
|
+
let resolveFirst: (v: any) => void;
|
|
342
|
+
const firstPromise = new Promise((r) => {
|
|
343
|
+
resolveFirst = r;
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
fetchMock
|
|
347
|
+
.mockReturnValueOnce(
|
|
348
|
+
firstPromise.then(() => ({
|
|
349
|
+
json: () => Promise.resolve({ data: ['first'], totalCount: 1 }),
|
|
350
|
+
ok: true,
|
|
351
|
+
status: 200,
|
|
352
|
+
})),
|
|
353
|
+
)
|
|
354
|
+
.mockResolvedValue({
|
|
355
|
+
json: () => Promise.resolve({ data: ['second'], totalCount: 1 }),
|
|
356
|
+
ok: true,
|
|
357
|
+
status: 200,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const { provider } = createProvider();
|
|
361
|
+
|
|
362
|
+
// Start first fetch, don't await
|
|
363
|
+
const firstFetch = provider.fetch();
|
|
364
|
+
// Start second fetch immediately
|
|
365
|
+
const secondFetch = provider.fetch();
|
|
366
|
+
|
|
367
|
+
resolveFirst!(null);
|
|
368
|
+
|
|
369
|
+
const [firstResult, secondResult] = await Promise.all([
|
|
370
|
+
firstFetch,
|
|
371
|
+
secondFetch,
|
|
372
|
+
]);
|
|
373
|
+
|
|
374
|
+
expect(firstResult.skipped).toBe(true);
|
|
375
|
+
expect(secondResult.data).toEqual(['second']);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('requestId check after JSON parse → returns skipped if mismatched', async () => {
|
|
379
|
+
let resolveJson: (v: any) => void;
|
|
380
|
+
const jsonPromise = new Promise((r) => {
|
|
381
|
+
resolveJson = r;
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
fetchMock
|
|
385
|
+
.mockResolvedValueOnce({
|
|
386
|
+
json: () => jsonPromise,
|
|
387
|
+
ok: true,
|
|
388
|
+
status: 200,
|
|
389
|
+
})
|
|
390
|
+
.mockResolvedValue({
|
|
391
|
+
json: () => Promise.resolve({ data: ['new'], totalCount: 1 }),
|
|
392
|
+
ok: true,
|
|
393
|
+
status: 200,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const { provider } = createProvider();
|
|
397
|
+
|
|
398
|
+
const firstFetch = provider.fetch();
|
|
399
|
+
const secondFetch = provider.fetch();
|
|
400
|
+
|
|
401
|
+
// Resolve JSON after second fetch started
|
|
402
|
+
resolveJson!({ data: ['old'], totalCount: 1 });
|
|
403
|
+
|
|
404
|
+
const firstResult = await firstFetch;
|
|
405
|
+
expect(firstResult.skipped).toBe(true);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
describe('Dispose', () => {
|
|
410
|
+
it('dispose() aborts pending request', async () => {
|
|
411
|
+
fetchMock.mockResolvedValue({
|
|
412
|
+
json: () => Promise.resolve({ data: [], totalCount: 0 }),
|
|
413
|
+
ok: true,
|
|
414
|
+
status: 200,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const { provider } = createProvider();
|
|
418
|
+
await provider.fetch(); // Creates an AbortController
|
|
419
|
+
provider.dispose();
|
|
420
|
+
|
|
421
|
+
// No assertion needed - just verifying dispose doesn't throw on a provider that has been used
|
|
422
|
+
expect(true).toBe(true);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('dispose() with no pending request does not throw', () => {
|
|
426
|
+
const { provider } = createProvider();
|
|
427
|
+
expect(() => provider.dispose()).not.toThrow();
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
describe('Edge cases', () => {
|
|
432
|
+
it('fetch() without apiEndpoint throws Error', async () => {
|
|
433
|
+
const { provider } = createProvider({
|
|
434
|
+
config: { requestMethod: 'GET', apiEndpoint: undefined } as any,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
await expect(provider.fetch()).rejects.toThrow(
|
|
438
|
+
'KTDataTable: apiEndpoint is required',
|
|
439
|
+
);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('performFetchRequest with requestCredentials includes credentials option', async () => {
|
|
443
|
+
const mockResponse = { data: [], totalCount: 0 };
|
|
444
|
+
fetchMock.mockResolvedValue({
|
|
445
|
+
json: () => Promise.resolve(mockResponse),
|
|
446
|
+
ok: true,
|
|
447
|
+
status: 200,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
const { provider } = createProvider({
|
|
451
|
+
config: {
|
|
452
|
+
requestMethod: 'GET',
|
|
453
|
+
apiEndpoint: 'https://api.example.com/data',
|
|
454
|
+
requestCredentials: 'include',
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
await provider.fetch();
|
|
459
|
+
|
|
460
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
461
|
+
expect.any(String),
|
|
462
|
+
expect.objectContaining({
|
|
463
|
+
credentials: 'include',
|
|
464
|
+
}),
|
|
465
|
+
);
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { createSearchHandler } from '../datatable-search-handler';
|
|
3
|
+
|
|
4
|
+
describe('createSearchHandler', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
document.body.innerHTML = '';
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
function createSearchInput(tableId: string): HTMLInputElement {
|
|
10
|
+
const input = document.createElement('input');
|
|
11
|
+
input.setAttribute('data-kt-datatable-search', `#${tableId}`);
|
|
12
|
+
document.body.appendChild(input);
|
|
13
|
+
return input;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
it('should return an object with attach and detach methods', () => {
|
|
17
|
+
const handler = createSearchHandler();
|
|
18
|
+
expect(handler).toHaveProperty('attach');
|
|
19
|
+
expect(handler).toHaveProperty('detach');
|
|
20
|
+
expect(typeof handler.attach).toBe('function');
|
|
21
|
+
expect(typeof handler.detach).toBe('function');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should call onSearch when keyup event fires after delay', () => {
|
|
25
|
+
vi.useFakeTimers();
|
|
26
|
+
const input = createSearchInput('test-table');
|
|
27
|
+
const onSearch = vi.fn();
|
|
28
|
+
const handler = createSearchHandler();
|
|
29
|
+
|
|
30
|
+
handler.attach('test-table', undefined, 300, onSearch);
|
|
31
|
+
|
|
32
|
+
input.value = 'test';
|
|
33
|
+
input.dispatchEvent(new Event('keyup'));
|
|
34
|
+
|
|
35
|
+
expect(onSearch).not.toHaveBeenCalled();
|
|
36
|
+
vi.advanceTimersByTime(300);
|
|
37
|
+
expect(onSearch).toHaveBeenCalledWith('test');
|
|
38
|
+
|
|
39
|
+
handler.detach('test-table');
|
|
40
|
+
vi.useRealTimers();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should not call onSearch after detach', () => {
|
|
44
|
+
vi.useFakeTimers();
|
|
45
|
+
const input = createSearchInput('test-table');
|
|
46
|
+
const onSearch = vi.fn();
|
|
47
|
+
const handler = createSearchHandler();
|
|
48
|
+
|
|
49
|
+
handler.attach('test-table', undefined, 300, onSearch);
|
|
50
|
+
handler.detach('test-table');
|
|
51
|
+
|
|
52
|
+
input.value = 'test';
|
|
53
|
+
input.dispatchEvent(new Event('keyup'));
|
|
54
|
+
|
|
55
|
+
vi.advanceTimersByTime(300);
|
|
56
|
+
expect(onSearch).not.toHaveBeenCalled();
|
|
57
|
+
|
|
58
|
+
vi.useRealTimers();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should debounce rapid keyup events', () => {
|
|
62
|
+
vi.useFakeTimers();
|
|
63
|
+
const input = createSearchInput('test-table');
|
|
64
|
+
const onSearch = vi.fn();
|
|
65
|
+
const handler = createSearchHandler();
|
|
66
|
+
|
|
67
|
+
handler.attach('test-table', undefined, 300, onSearch);
|
|
68
|
+
|
|
69
|
+
// Fire multiple keyup events rapidly
|
|
70
|
+
for (const val of ['t', 'te', 'tes', 'test']) {
|
|
71
|
+
input.value = val;
|
|
72
|
+
input.dispatchEvent(new Event('keyup'));
|
|
73
|
+
vi.advanceTimersByTime(100);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Wait for the final debounce
|
|
77
|
+
vi.advanceTimersByTime(300);
|
|
78
|
+
|
|
79
|
+
// Should only be called once with the last value
|
|
80
|
+
expect(onSearch).toHaveBeenCalledTimes(1);
|
|
81
|
+
expect(onSearch).toHaveBeenCalledWith('test');
|
|
82
|
+
|
|
83
|
+
handler.detach('test-table');
|
|
84
|
+
vi.useRealTimers();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should restore search value from currentSearch', () => {
|
|
88
|
+
const input = createSearchInput('test-table');
|
|
89
|
+
const handler = createSearchHandler();
|
|
90
|
+
|
|
91
|
+
handler.attach('test-table', 'previous search', 300, vi.fn());
|
|
92
|
+
expect(input.value).toBe('previous search');
|
|
93
|
+
|
|
94
|
+
handler.detach('test-table');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should handle missing search element gracefully', () => {
|
|
98
|
+
const handler = createSearchHandler();
|
|
99
|
+
expect(() => handler.attach('nonexistent', undefined, 300, vi.fn())).not.toThrow();
|
|
100
|
+
expect(() => handler.detach('nonexistent')).not.toThrow();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should replace existing debounced listener on re-attach', () => {
|
|
104
|
+
vi.useFakeTimers();
|
|
105
|
+
const input = createSearchInput('test-table');
|
|
106
|
+
const onSearch1 = vi.fn();
|
|
107
|
+
const onSearch2 = vi.fn();
|
|
108
|
+
const handler = createSearchHandler();
|
|
109
|
+
|
|
110
|
+
handler.attach('test-table', undefined, 300, onSearch1);
|
|
111
|
+
handler.attach('test-table', undefined, 300, onSearch2);
|
|
112
|
+
|
|
113
|
+
input.value = 'test';
|
|
114
|
+
input.dispatchEvent(new Event('keyup'));
|
|
115
|
+
vi.advanceTimersByTime(300);
|
|
116
|
+
|
|
117
|
+
// Only the second callback should be called
|
|
118
|
+
expect(onSearch1).not.toHaveBeenCalled();
|
|
119
|
+
expect(onSearch2).toHaveBeenCalledWith('test');
|
|
120
|
+
|
|
121
|
+
handler.detach('test-table');
|
|
122
|
+
vi.useRealTimers();
|
|
123
|
+
});
|
|
124
|
+
});
|