mtrl-addons 0.1.0 → 0.1.1
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/.cursorrules +117 -0
- package/AI.md +241 -0
- package/build.js +170 -0
- package/bun.lock +792 -0
- package/index.ts +7 -0
- package/package.json +10 -17
- package/scripts/analyze-orphaned-functions.ts +387 -0
- package/src/components/index.ts +45 -0
- package/src/components/list/api.ts +314 -0
- package/src/components/list/config.ts +352 -0
- package/src/components/list/constants.ts +56 -0
- package/src/components/list/features/api.ts +428 -0
- package/src/components/list/features/index.ts +31 -0
- package/src/components/list/features/list-manager.ts +502 -0
- package/src/components/list/features.ts +112 -0
- package/src/components/list/index.ts +39 -0
- package/src/components/list/list.ts +234 -0
- package/src/components/list/types.ts +513 -0
- package/src/core/collection/base-collection.ts +100 -0
- package/src/core/collection/collection-composer.ts +178 -0
- package/src/core/collection/collection.ts +745 -0
- package/src/core/collection/constants.ts +172 -0
- package/src/core/collection/events.ts +428 -0
- package/src/core/collection/features/api/loading.ts +279 -0
- package/src/core/collection/features/operations/data-operations.ts +147 -0
- package/src/core/collection/index.ts +104 -0
- package/src/core/collection/state.ts +497 -0
- package/src/core/collection/types.ts +404 -0
- package/src/core/compose/features/collection.ts +119 -0
- package/src/core/compose/features/index.ts +39 -0
- package/src/core/compose/features/performance.ts +161 -0
- package/src/core/compose/features/selection.ts +213 -0
- package/src/core/compose/features/styling.ts +108 -0
- package/src/core/compose/index.ts +31 -0
- package/src/core/index.ts +167 -0
- package/src/core/layout/config.ts +102 -0
- package/src/core/layout/index.ts +168 -0
- package/src/core/layout/jsx.ts +174 -0
- package/src/core/layout/schema.ts +963 -0
- package/src/core/layout/types.ts +92 -0
- package/src/core/list-manager/api.ts +599 -0
- package/src/core/list-manager/config.ts +593 -0
- package/src/core/list-manager/constants.ts +268 -0
- package/src/core/list-manager/features/api.ts +58 -0
- package/src/core/list-manager/features/collection/collection.ts +705 -0
- package/src/core/list-manager/features/collection/index.ts +17 -0
- package/src/core/list-manager/features/viewport/constants.ts +42 -0
- package/src/core/list-manager/features/viewport/index.ts +16 -0
- package/src/core/list-manager/features/viewport/item-size.ts +274 -0
- package/src/core/list-manager/features/viewport/loading.ts +263 -0
- package/src/core/list-manager/features/viewport/placeholders.ts +281 -0
- package/src/core/list-manager/features/viewport/rendering.ts +575 -0
- package/src/core/list-manager/features/viewport/scrollbar.ts +495 -0
- package/src/core/list-manager/features/viewport/scrolling.ts +795 -0
- package/src/core/list-manager/features/viewport/template.ts +220 -0
- package/src/core/list-manager/features/viewport/viewport.ts +654 -0
- package/src/core/list-manager/features/viewport/virtual.ts +309 -0
- package/src/core/list-manager/index.ts +279 -0
- package/src/core/list-manager/list-manager.ts +206 -0
- package/src/core/list-manager/types.ts +439 -0
- package/src/core/list-manager/utils/calculations.ts +290 -0
- package/src/core/list-manager/utils/range-calculator.ts +349 -0
- package/src/core/list-manager/utils/speed-tracker.ts +273 -0
- package/src/index.ts +17 -0
- package/src/styles/components/_list.scss +244 -0
- package/src/styles/index.scss +12 -0
- package/src/types/mtrl.d.ts +6 -0
- package/test/benchmarks/layout/advanced.test.ts +656 -0
- package/test/benchmarks/layout/comparison.test.ts +519 -0
- package/test/benchmarks/layout/performance-comparison.test.ts +274 -0
- package/test/benchmarks/layout/real-components.test.ts +733 -0
- package/test/benchmarks/layout/simple.test.ts +321 -0
- package/test/benchmarks/layout/stress.test.ts +990 -0
- package/test/collection/basic.test.ts +304 -0
- package/test/components/list.test.ts +256 -0
- package/test/core/collection/collection.test.ts +394 -0
- package/test/core/collection/failed-ranges.test.ts +270 -0
- package/test/core/compose/features.test.ts +183 -0
- package/test/core/layout/layout.test.ts +201 -0
- package/test/core/list-manager/features/collection.test.ts +704 -0
- package/test/core/list-manager/features/viewport.test.ts +698 -0
- package/test/core/list-manager/list-manager.test.ts +593 -0
- package/test/core/list-manager/utils/calculations.test.ts +433 -0
- package/test/core/list-manager/utils/range-calculator.test.ts +569 -0
- package/test/core/list-manager/utils/speed-tracker.test.ts +530 -0
- package/test/utils/dom-helpers.ts +275 -0
- package/test/utils/performance-helpers.ts +392 -0
- package/tsconfig.build.json +14 -0
- package/tsconfig.json +20 -0
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -38
- package/dist/index.mjs +0 -8
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
// test/core/collection/collection.test.ts - Core Collection Tests
|
|
2
|
+
import {
|
|
3
|
+
describe,
|
|
4
|
+
test,
|
|
5
|
+
expect,
|
|
6
|
+
mock,
|
|
7
|
+
beforeAll,
|
|
8
|
+
afterAll,
|
|
9
|
+
beforeEach,
|
|
10
|
+
afterEach,
|
|
11
|
+
} from "bun:test";
|
|
12
|
+
import { JSDOM } from "jsdom";
|
|
13
|
+
|
|
14
|
+
// Setup for DOM testing environment
|
|
15
|
+
let dom: JSDOM;
|
|
16
|
+
let window: Window;
|
|
17
|
+
let document: Document;
|
|
18
|
+
let originalGlobalDocument: any;
|
|
19
|
+
let originalGlobalWindow: any;
|
|
20
|
+
|
|
21
|
+
// Setup DOM environment before importing modules
|
|
22
|
+
beforeAll(() => {
|
|
23
|
+
// Create a new JSDOM instance
|
|
24
|
+
dom = new JSDOM("<!DOCTYPE html><html><body></body></html>", {
|
|
25
|
+
url: "http://localhost/",
|
|
26
|
+
pretendToBeVisual: true,
|
|
27
|
+
resources: "usable",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Get window and document from jsdom
|
|
31
|
+
window = dom.window as any;
|
|
32
|
+
document = window.document;
|
|
33
|
+
|
|
34
|
+
// Store original globals
|
|
35
|
+
originalGlobalDocument = global.document;
|
|
36
|
+
originalGlobalWindow = global.window;
|
|
37
|
+
|
|
38
|
+
// Set globals to use jsdom
|
|
39
|
+
global.document = document;
|
|
40
|
+
global.window = window as any;
|
|
41
|
+
global.Element = (window as any).Element;
|
|
42
|
+
global.HTMLElement = (window as any).HTMLElement;
|
|
43
|
+
global.DocumentFragment = (window as any).DocumentFragment;
|
|
44
|
+
global.requestAnimationFrame = (window as any).requestAnimationFrame;
|
|
45
|
+
global.cancelAnimationFrame = (window as any).cancelAnimationFrame;
|
|
46
|
+
|
|
47
|
+
// Add missing DOM APIs
|
|
48
|
+
global.getComputedStyle =
|
|
49
|
+
(window as any).getComputedStyle ||
|
|
50
|
+
(() => ({
|
|
51
|
+
position: "static",
|
|
52
|
+
getPropertyValue: () => "",
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
// Add IntersectionObserver mock
|
|
56
|
+
if (!(window as any).IntersectionObserver) {
|
|
57
|
+
const IntersectionObserverMock = class {
|
|
58
|
+
constructor() {}
|
|
59
|
+
observe() {}
|
|
60
|
+
disconnect() {}
|
|
61
|
+
unobserve() {}
|
|
62
|
+
};
|
|
63
|
+
(window as any).IntersectionObserver = IntersectionObserverMock as any;
|
|
64
|
+
(global as any).IntersectionObserver = IntersectionObserverMock as any;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Add ResizeObserver mock
|
|
68
|
+
if (!(window as any).ResizeObserver) {
|
|
69
|
+
const ResizeObserverMock = class {
|
|
70
|
+
constructor() {}
|
|
71
|
+
observe() {}
|
|
72
|
+
disconnect() {}
|
|
73
|
+
unobserve() {}
|
|
74
|
+
};
|
|
75
|
+
(window as any).ResizeObserver = ResizeObserverMock as any;
|
|
76
|
+
(global as any).ResizeObserver = ResizeObserverMock as any;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
afterAll(() => {
|
|
81
|
+
// Restore original globals
|
|
82
|
+
global.document = originalGlobalDocument;
|
|
83
|
+
global.window = originalGlobalWindow;
|
|
84
|
+
|
|
85
|
+
// Clean up jsdom
|
|
86
|
+
window.close();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Import the collection after DOM setup
|
|
90
|
+
// TODO: Implement these imports as we build the system
|
|
91
|
+
// import { createCollection, pipe, withEvents, withLifecycle } from "../../../src/core/collection";
|
|
92
|
+
|
|
93
|
+
// Test data interfaces
|
|
94
|
+
interface TestItem {
|
|
95
|
+
id: string;
|
|
96
|
+
name: string;
|
|
97
|
+
email: string;
|
|
98
|
+
age: number;
|
|
99
|
+
active: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface TestProduct {
|
|
103
|
+
id: string;
|
|
104
|
+
title: string;
|
|
105
|
+
price: number;
|
|
106
|
+
category: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Helper functions for creating test data
|
|
110
|
+
function createTestItem(
|
|
111
|
+
id: string,
|
|
112
|
+
overrides: Partial<TestItem> = {}
|
|
113
|
+
): TestItem {
|
|
114
|
+
return {
|
|
115
|
+
id,
|
|
116
|
+
name: `User ${id}`,
|
|
117
|
+
email: `user${id}@example.com`,
|
|
118
|
+
age: 20 + parseInt(id),
|
|
119
|
+
active: true,
|
|
120
|
+
...overrides,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function createTestProduct(
|
|
125
|
+
id: string,
|
|
126
|
+
overrides: Partial<TestProduct> = {}
|
|
127
|
+
): TestProduct {
|
|
128
|
+
return {
|
|
129
|
+
id,
|
|
130
|
+
title: `Product ${id}`,
|
|
131
|
+
price: parseFloat(id) * 10,
|
|
132
|
+
category: "electronics",
|
|
133
|
+
...overrides,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Mock template engine
|
|
138
|
+
const mockTemplateEngine = {
|
|
139
|
+
render: mock((template: any, data: any) => {
|
|
140
|
+
const element = document.createElement("div");
|
|
141
|
+
element.className = "test-item";
|
|
142
|
+
element.setAttribute("data-id", data.id);
|
|
143
|
+
element.innerHTML = `
|
|
144
|
+
<div class="item-name">${data.name}</div>
|
|
145
|
+
<div class="item-email">${data.email}</div>
|
|
146
|
+
`;
|
|
147
|
+
return element;
|
|
148
|
+
}),
|
|
149
|
+
|
|
150
|
+
compile: mock((template: any) => {
|
|
151
|
+
return (data: any) => mockTemplateEngine.render(template, data);
|
|
152
|
+
}),
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Mock adapter for data loading
|
|
156
|
+
const mockAdapter = {
|
|
157
|
+
read: mock(async (params: any) => {
|
|
158
|
+
const page = params.page || 1;
|
|
159
|
+
const limit = params.limit || params.per_page || 20;
|
|
160
|
+
const startId = (page - 1) * limit + 1;
|
|
161
|
+
|
|
162
|
+
const items: TestItem[] = [];
|
|
163
|
+
for (let i = 0; i < limit; i++) {
|
|
164
|
+
const id = startId + i;
|
|
165
|
+
if (id <= 100) {
|
|
166
|
+
items.push(createTestItem(id.toString()));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
items,
|
|
172
|
+
meta: {
|
|
173
|
+
total: 100,
|
|
174
|
+
page,
|
|
175
|
+
hasNext: page * limit < 100,
|
|
176
|
+
nextCursor: items.length > 0 ? items[items.length - 1].id : null,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}),
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
describe("Collection System", () => {
|
|
183
|
+
let container: HTMLElement;
|
|
184
|
+
|
|
185
|
+
beforeEach(() => {
|
|
186
|
+
// Create fresh container for each test
|
|
187
|
+
container = document.createElement("div");
|
|
188
|
+
container.style.height = "400px";
|
|
189
|
+
container.style.overflow = "auto";
|
|
190
|
+
document.body.appendChild(container);
|
|
191
|
+
|
|
192
|
+
// Reset mocks
|
|
193
|
+
mockTemplateEngine.render.mockClear();
|
|
194
|
+
mockTemplateEngine.compile.mockClear();
|
|
195
|
+
mockAdapter.read.mockClear();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
afterEach(() => {
|
|
199
|
+
// Cleanup container
|
|
200
|
+
if (container && container.parentNode) {
|
|
201
|
+
container.parentNode.removeChild(container);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("Basic Infrastructure", () => {
|
|
206
|
+
test("DOM environment is properly set up", () => {
|
|
207
|
+
expect(document).toBeDefined();
|
|
208
|
+
expect(document.createElement).toBeDefined();
|
|
209
|
+
expect(global.IntersectionObserver).toBeDefined();
|
|
210
|
+
expect(global.ResizeObserver).toBeDefined();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("test container is created correctly", () => {
|
|
214
|
+
expect(container).toBeDefined();
|
|
215
|
+
expect(container.style.height).toBe("400px");
|
|
216
|
+
expect(container.style.overflow).toBe("auto");
|
|
217
|
+
expect(container.parentNode).toBe(document.body);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("test data helpers work correctly", () => {
|
|
221
|
+
const item = createTestItem("123");
|
|
222
|
+
expect(item.id).toBe("123");
|
|
223
|
+
expect(item.name).toBe("User 123");
|
|
224
|
+
expect(item.email).toBe("user123@example.com");
|
|
225
|
+
expect(item.age).toBe(143);
|
|
226
|
+
expect(item.active).toBe(true);
|
|
227
|
+
|
|
228
|
+
const customItem = createTestItem("456", { name: "Custom User" });
|
|
229
|
+
expect(customItem.name).toBe("Custom User");
|
|
230
|
+
expect(customItem.id).toBe("456");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("mock template engine renders correctly", () => {
|
|
234
|
+
const template = { tag: "div", text: "{{name}}" };
|
|
235
|
+
const data = { id: "1", name: "John Doe", email: "john@example.com" };
|
|
236
|
+
|
|
237
|
+
const element = mockTemplateEngine.render(template, data);
|
|
238
|
+
|
|
239
|
+
expect(element.tagName).toBe("DIV");
|
|
240
|
+
expect(element.className).toBe("test-item");
|
|
241
|
+
expect(element.getAttribute("data-id")).toBe("1");
|
|
242
|
+
expect(element.querySelector(".item-name")?.textContent).toBe("John Doe");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("mock adapter generates test data correctly", async () => {
|
|
246
|
+
const result = await mockAdapter.read({ page: 1, per_page: 5 });
|
|
247
|
+
|
|
248
|
+
expect(result.items).toHaveLength(5);
|
|
249
|
+
expect(result.meta.total).toBe(100);
|
|
250
|
+
expect(result.meta.page).toBe(1);
|
|
251
|
+
expect(result.meta.hasNext).toBe(true);
|
|
252
|
+
|
|
253
|
+
// Check first item
|
|
254
|
+
expect(result.items[0].id).toBe("1");
|
|
255
|
+
expect(result.items[0].name).toBe("User 1");
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe("Collection Core (Placeholder)", () => {
|
|
260
|
+
test("placeholder for collection creation", () => {
|
|
261
|
+
// TODO: Implement when createCollection is available
|
|
262
|
+
// const collection = createCollection({ items: [] });
|
|
263
|
+
// expect(collection).toBeDefined();
|
|
264
|
+
|
|
265
|
+
// Placeholder assertion
|
|
266
|
+
expect(true).toBe(true);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("placeholder for functional composition", () => {
|
|
270
|
+
// TODO: Implement when pipe and features are available
|
|
271
|
+
// const collection = pipe(
|
|
272
|
+
// createCollection(config),
|
|
273
|
+
// withEvents(),
|
|
274
|
+
// withLifecycle()
|
|
275
|
+
// );
|
|
276
|
+
// expect(collection).toBeDefined();
|
|
277
|
+
|
|
278
|
+
// Placeholder assertion
|
|
279
|
+
expect(true).toBe(true);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe("Template Engine Integration (Placeholder)", () => {
|
|
284
|
+
test("placeholder for template compilation", () => {
|
|
285
|
+
// TODO: Implement when template engine is available
|
|
286
|
+
// const engine = createTemplateEngine('object');
|
|
287
|
+
// const template = { tag: 'div', text: '{{name}}' };
|
|
288
|
+
// const compiled = engine.compile(template);
|
|
289
|
+
// expect(compiled).toBeDefined();
|
|
290
|
+
|
|
291
|
+
// Test mock for now
|
|
292
|
+
const compiled = mockTemplateEngine.compile({
|
|
293
|
+
tag: "div",
|
|
294
|
+
text: "{{name}}",
|
|
295
|
+
});
|
|
296
|
+
expect(compiled).toBeDefined();
|
|
297
|
+
expect(mockTemplateEngine.compile).toHaveBeenCalled();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("placeholder for element recycling", () => {
|
|
301
|
+
// TODO: Implement when recycling pool is available
|
|
302
|
+
// const pool = createRecyclingPool({ maxSize: 10 });
|
|
303
|
+
// const element = pool.getElement();
|
|
304
|
+
// expect(element).toBeDefined();
|
|
305
|
+
|
|
306
|
+
// Placeholder assertion
|
|
307
|
+
expect(true).toBe(true);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe("Performance Testing Framework", () => {
|
|
312
|
+
test("measures render time accurately", async () => {
|
|
313
|
+
const start = performance.now();
|
|
314
|
+
|
|
315
|
+
// Simulate some work
|
|
316
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
317
|
+
|
|
318
|
+
const end = performance.now();
|
|
319
|
+
const duration = end - start;
|
|
320
|
+
|
|
321
|
+
expect(duration).toBeGreaterThan(5);
|
|
322
|
+
expect(duration).toBeLessThan(50);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("creates large datasets for performance testing", () => {
|
|
326
|
+
const items = Array.from({ length: 1000 }, (_, i) =>
|
|
327
|
+
createTestItem(i.toString())
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
expect(items).toHaveLength(1000);
|
|
331
|
+
expect(items[0].id).toBe("0");
|
|
332
|
+
expect(items[999].id).toBe("999");
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("simulates scroll events correctly", () => {
|
|
336
|
+
let scrollEventFired = false;
|
|
337
|
+
|
|
338
|
+
container.addEventListener("scroll", () => {
|
|
339
|
+
scrollEventFired = true;
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
container.scrollTop = 100;
|
|
343
|
+
const scrollEvent = new (window as any).Event("scroll", {
|
|
344
|
+
bubbles: true,
|
|
345
|
+
});
|
|
346
|
+
container.dispatchEvent(scrollEvent);
|
|
347
|
+
|
|
348
|
+
expect(scrollEventFired).toBe(true);
|
|
349
|
+
expect(container.scrollTop).toBe(100);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
describe("Memory Management Testing", () => {
|
|
354
|
+
test("cleans up event listeners", () => {
|
|
355
|
+
const listener = mock(() => {});
|
|
356
|
+
|
|
357
|
+
container.addEventListener("scroll", listener);
|
|
358
|
+
|
|
359
|
+
// Simulate cleanup
|
|
360
|
+
container.removeEventListener("scroll", listener);
|
|
361
|
+
|
|
362
|
+
// Fire event - should not call listener
|
|
363
|
+
const scrollEvent = new (window as any).Event("scroll", {
|
|
364
|
+
bubbles: true,
|
|
365
|
+
});
|
|
366
|
+
container.dispatchEvent(scrollEvent);
|
|
367
|
+
|
|
368
|
+
expect(listener).not.toHaveBeenCalled();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("handles rapid element creation/destruction", () => {
|
|
372
|
+
const elements: HTMLElement[] = [];
|
|
373
|
+
|
|
374
|
+
// Create many elements
|
|
375
|
+
for (let i = 0; i < 100; i++) {
|
|
376
|
+
const element = document.createElement("div");
|
|
377
|
+
element.textContent = `Item ${i}`;
|
|
378
|
+
elements.push(element);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
expect(elements).toHaveLength(100);
|
|
382
|
+
|
|
383
|
+
// Clean up
|
|
384
|
+
elements.forEach((element) => {
|
|
385
|
+
element.remove();
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
expect(elements[0].isConnected).toBe(false);
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Export test utilities for use in other test files
|
|
394
|
+
export { createTestItem, createTestProduct, mockTemplateEngine, mockAdapter };
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// test/core/collection/failed-ranges.test.ts
|
|
2
|
+
|
|
3
|
+
import { describe, test, expect, beforeEach, mock, afterEach } from "bun:test";
|
|
4
|
+
import { createListManager } from "../../../src/core/list-manager/list-manager";
|
|
5
|
+
import { withCollection } from "../../../src/core/list-manager/features/collection/collection";
|
|
6
|
+
import type { ListManagerConfig } from "../../../src/core/list-manager/types";
|
|
7
|
+
|
|
8
|
+
describe("Collection - Failed Range Tracking", () => {
|
|
9
|
+
let container: HTMLElement;
|
|
10
|
+
let config: ListManagerConfig;
|
|
11
|
+
let mockAdapter: any;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
container = document.createElement("div");
|
|
15
|
+
container.style.height = "600px";
|
|
16
|
+
container.style.width = "400px";
|
|
17
|
+
document.body.appendChild(container);
|
|
18
|
+
|
|
19
|
+
// Mock adapter that can simulate failures
|
|
20
|
+
mockAdapter = {
|
|
21
|
+
read: mock(),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
config = {
|
|
25
|
+
container,
|
|
26
|
+
items: [],
|
|
27
|
+
template: {
|
|
28
|
+
template: (item: any) => `<div>Item ${item.id}</div>`,
|
|
29
|
+
},
|
|
30
|
+
virtual: {
|
|
31
|
+
enabled: true,
|
|
32
|
+
itemSize: 84,
|
|
33
|
+
estimatedItemSize: 84,
|
|
34
|
+
overscan: 5,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
container.remove();
|
|
41
|
+
mock.restore();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("should track failed ranges when data loading fails", async () => {
|
|
45
|
+
// Configure adapter to fail
|
|
46
|
+
mockAdapter.read.mockRejectedValueOnce(new Error("Network error"));
|
|
47
|
+
|
|
48
|
+
const manager = createListManager(config);
|
|
49
|
+
const enhancedManager = withCollection({
|
|
50
|
+
collection: mockAdapter,
|
|
51
|
+
rangeSize: 20,
|
|
52
|
+
})(manager);
|
|
53
|
+
|
|
54
|
+
// Try to load a range
|
|
55
|
+
try {
|
|
56
|
+
await enhancedManager.collection.loadRange(0, 20);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// Expected to fail
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check that failed range is tracked
|
|
62
|
+
const failedRanges = enhancedManager.collection.getFailedRanges();
|
|
63
|
+
expect(failedRanges.size).toBe(1);
|
|
64
|
+
expect(failedRanges.has(0)).toBe(true);
|
|
65
|
+
|
|
66
|
+
const failedInfo = failedRanges.get(0);
|
|
67
|
+
expect(failedInfo?.attempts).toBe(1);
|
|
68
|
+
expect(failedInfo?.error.message).toBe("Network error");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("should not reload failed ranges immediately", async () => {
|
|
72
|
+
// Configure adapter to fail
|
|
73
|
+
mockAdapter.read.mockRejectedValue(new Error("Network error"));
|
|
74
|
+
|
|
75
|
+
const manager = createListManager(config);
|
|
76
|
+
const enhancedManager = withCollection({
|
|
77
|
+
collection: mockAdapter,
|
|
78
|
+
rangeSize: 20,
|
|
79
|
+
})(manager);
|
|
80
|
+
|
|
81
|
+
// First attempt - should fail
|
|
82
|
+
try {
|
|
83
|
+
await enhancedManager.collection.loadRange(0, 20);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
// Expected
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Reset mock
|
|
89
|
+
mockAdapter.read.mockClear();
|
|
90
|
+
|
|
91
|
+
// Try to load missing ranges immediately - should skip failed range
|
|
92
|
+
await enhancedManager.collection.loadMissingRanges({ start: 0, end: 19 });
|
|
93
|
+
|
|
94
|
+
// Should not have tried to load again
|
|
95
|
+
expect(mockAdapter.read).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("should retry failed ranges after clearing lastAttempt", async () => {
|
|
99
|
+
// Configure adapter to fail first, then succeed
|
|
100
|
+
mockAdapter.read
|
|
101
|
+
.mockRejectedValueOnce(new Error("Network error"))
|
|
102
|
+
.mockResolvedValueOnce({
|
|
103
|
+
items: Array.from({ length: 20 }, (_, i) => ({
|
|
104
|
+
id: i,
|
|
105
|
+
name: `Item ${i}`,
|
|
106
|
+
})),
|
|
107
|
+
meta: { total: 100 },
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const manager = createListManager(config);
|
|
111
|
+
const enhancedManager = withCollection({
|
|
112
|
+
collection: mockAdapter,
|
|
113
|
+
rangeSize: 20,
|
|
114
|
+
})(manager);
|
|
115
|
+
|
|
116
|
+
// First attempt - should fail
|
|
117
|
+
try {
|
|
118
|
+
await enhancedManager.collection.loadRange(0, 20);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
// Expected
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Manually retry failed ranges
|
|
124
|
+
await enhancedManager.collection.retryFailedRanges();
|
|
125
|
+
|
|
126
|
+
// Check that failed range was cleared after success
|
|
127
|
+
const failedRanges = enhancedManager.collection.getFailedRanges();
|
|
128
|
+
expect(failedRanges.size).toBe(0);
|
|
129
|
+
|
|
130
|
+
// Check that data was loaded
|
|
131
|
+
const loadedRanges = enhancedManager.collection.getLoadedRanges();
|
|
132
|
+
expect(loadedRanges.has(0)).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("should track multiple attempts", async () => {
|
|
136
|
+
// Configure adapter to fail multiple times
|
|
137
|
+
mockAdapter.read.mockRejectedValue(new Error("Network error"));
|
|
138
|
+
|
|
139
|
+
const manager = createListManager(config);
|
|
140
|
+
const enhancedManager = withCollection({
|
|
141
|
+
collection: mockAdapter,
|
|
142
|
+
rangeSize: 20,
|
|
143
|
+
})(manager);
|
|
144
|
+
|
|
145
|
+
// Multiple attempts
|
|
146
|
+
for (let i = 0; i < 3; i++) {
|
|
147
|
+
try {
|
|
148
|
+
// Clear lastAttempt to allow immediate retry
|
|
149
|
+
const failedRanges = enhancedManager.collection.getFailedRanges();
|
|
150
|
+
failedRanges.forEach((info) => {
|
|
151
|
+
info.lastAttempt = 0;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await enhancedManager.collection.loadRange(0, 20);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
// Expected
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check attempt count
|
|
161
|
+
const failedRanges = enhancedManager.collection.getFailedRanges();
|
|
162
|
+
const failedInfo = failedRanges.get(0);
|
|
163
|
+
expect(failedInfo?.attempts).toBe(3);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("should clear failed ranges", async () => {
|
|
167
|
+
// Configure adapter to fail
|
|
168
|
+
mockAdapter.read.mockRejectedValue(new Error("Network error"));
|
|
169
|
+
|
|
170
|
+
const manager = createListManager(config);
|
|
171
|
+
const enhancedManager = withCollection({
|
|
172
|
+
collection: mockAdapter,
|
|
173
|
+
rangeSize: 20,
|
|
174
|
+
})(manager);
|
|
175
|
+
|
|
176
|
+
// Fail to load some ranges
|
|
177
|
+
try {
|
|
178
|
+
await enhancedManager.collection.loadRange(0, 20);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
// Expected
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
await enhancedManager.collection.loadRange(40, 20);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
// Expected
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Should have 2 failed ranges
|
|
189
|
+
expect(enhancedManager.collection.getFailedRanges().size).toBe(2);
|
|
190
|
+
|
|
191
|
+
// Clear failed ranges
|
|
192
|
+
enhancedManager.collection.clearFailedRanges();
|
|
193
|
+
|
|
194
|
+
// Should have no failed ranges
|
|
195
|
+
expect(enhancedManager.collection.getFailedRanges().size).toBe(0);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("should handle concurrent failures correctly", async () => {
|
|
199
|
+
// Configure adapter to fail
|
|
200
|
+
mockAdapter.read.mockRejectedValue(new Error("Network error"));
|
|
201
|
+
|
|
202
|
+
const manager = createListManager(config);
|
|
203
|
+
const enhancedManager = withCollection({
|
|
204
|
+
collection: mockAdapter,
|
|
205
|
+
rangeSize: 20,
|
|
206
|
+
})(manager);
|
|
207
|
+
|
|
208
|
+
// Try to load multiple ranges concurrently
|
|
209
|
+
const promises = [
|
|
210
|
+
enhancedManager.collection.loadRange(0, 20),
|
|
211
|
+
enhancedManager.collection.loadRange(20, 20),
|
|
212
|
+
enhancedManager.collection.loadRange(40, 20),
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
// All should fail
|
|
216
|
+
const results = await Promise.allSettled(promises);
|
|
217
|
+
expect(results.every((r) => r.status === "rejected")).toBe(true);
|
|
218
|
+
|
|
219
|
+
// Should have 3 failed ranges
|
|
220
|
+
const failedRanges = enhancedManager.collection.getFailedRanges();
|
|
221
|
+
expect(failedRanges.size).toBe(3);
|
|
222
|
+
expect(failedRanges.has(0)).toBe(true);
|
|
223
|
+
expect(failedRanges.has(1)).toBe(true);
|
|
224
|
+
expect(failedRanges.has(2)).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("should respect exponential backoff", async () => {
|
|
228
|
+
// Configure adapter to fail
|
|
229
|
+
mockAdapter.read.mockRejectedValue(new Error("Network error"));
|
|
230
|
+
|
|
231
|
+
const manager = createListManager(config);
|
|
232
|
+
const enhancedManager = withCollection({
|
|
233
|
+
collection: mockAdapter,
|
|
234
|
+
rangeSize: 20,
|
|
235
|
+
})(manager);
|
|
236
|
+
|
|
237
|
+
// First failure
|
|
238
|
+
try {
|
|
239
|
+
await enhancedManager.collection.loadRange(0, 20);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
// Expected
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Try again immediately - should be blocked
|
|
245
|
+
mockAdapter.read.mockClear();
|
|
246
|
+
try {
|
|
247
|
+
await enhancedManager.collection.loadRange(0, 20);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
// Should re-throw the same error without calling adapter
|
|
250
|
+
expect(error.message).toBe("Network error");
|
|
251
|
+
}
|
|
252
|
+
expect(mockAdapter.read).not.toHaveBeenCalled();
|
|
253
|
+
|
|
254
|
+
// Simulate time passing (clear lastAttempt)
|
|
255
|
+
const failedRanges = enhancedManager.collection.getFailedRanges();
|
|
256
|
+
const failedInfo = failedRanges.get(0);
|
|
257
|
+
if (failedInfo) {
|
|
258
|
+
failedInfo.lastAttempt = Date.now() - 2000; // 2 seconds ago
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Now it should try again
|
|
262
|
+
mockAdapter.read.mockRejectedValueOnce(new Error("Still failing"));
|
|
263
|
+
try {
|
|
264
|
+
await enhancedManager.collection.loadRange(0, 20);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
expect(error.message).toBe("Still failing");
|
|
267
|
+
}
|
|
268
|
+
expect(mockAdapter.read).toHaveBeenCalledTimes(1);
|
|
269
|
+
});
|
|
270
|
+
});
|