mtrl-addons 0.1.2 → 0.2.2
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/AI.md +28 -230
- package/CLAUDE.md +882 -0
- package/build.js +253 -24
- package/package.json +14 -4
- package/scripts/debug/vlist-selection.ts +121 -0
- package/src/components/index.ts +5 -41
- package/src/components/{list → vlist}/config.ts +66 -95
- package/src/components/vlist/constants.ts +23 -0
- package/src/components/vlist/features/api.ts +626 -0
- package/src/components/vlist/features/index.ts +10 -0
- package/src/components/vlist/features/selection.ts +436 -0
- package/src/components/vlist/features/viewport.ts +59 -0
- package/src/components/vlist/index.ts +17 -0
- package/src/components/{list → vlist}/types.ts +242 -32
- package/src/components/vlist/vlist.ts +92 -0
- package/src/core/compose/features/gestures/index.ts +227 -0
- package/src/core/compose/features/gestures/longpress.ts +383 -0
- package/src/core/compose/features/gestures/pan.ts +424 -0
- package/src/core/compose/features/gestures/pinch.ts +475 -0
- package/src/core/compose/features/gestures/rotate.ts +485 -0
- package/src/core/compose/features/gestures/swipe.ts +492 -0
- package/src/core/compose/features/gestures/tap.ts +334 -0
- package/src/core/compose/features/index.ts +2 -38
- package/src/core/compose/index.ts +13 -29
- package/src/core/gestures/index.ts +31 -0
- package/src/core/gestures/longpress.ts +68 -0
- package/src/core/gestures/manager.ts +418 -0
- package/src/core/gestures/pan.ts +48 -0
- package/src/core/gestures/pinch.ts +58 -0
- package/src/core/gestures/rotate.ts +58 -0
- package/src/core/gestures/swipe.ts +66 -0
- package/src/core/gestures/tap.ts +45 -0
- package/src/core/gestures/types.ts +387 -0
- package/src/core/gestures/utils.ts +128 -0
- package/src/core/index.ts +27 -151
- package/src/core/layout/schema.ts +153 -72
- package/src/core/layout/types.ts +5 -2
- package/src/core/viewport/constants.ts +145 -0
- package/src/core/viewport/features/base.ts +73 -0
- package/src/core/viewport/features/collection.ts +1182 -0
- package/src/core/viewport/features/events.ts +130 -0
- package/src/core/viewport/features/index.ts +20 -0
- package/src/core/{list-manager/features/viewport → viewport/features}/item-size.ts +31 -34
- package/src/core/{list-manager/features/viewport → viewport/features}/loading.ts +4 -4
- package/src/core/viewport/features/momentum.ts +269 -0
- package/src/core/viewport/features/placeholders.ts +335 -0
- package/src/core/viewport/features/rendering.ts +962 -0
- package/src/core/viewport/features/scrollbar.ts +434 -0
- package/src/core/viewport/features/scrolling.ts +634 -0
- package/src/core/viewport/features/utils.ts +94 -0
- package/src/core/viewport/features/virtual.ts +525 -0
- package/src/core/viewport/index.ts +31 -0
- package/src/core/viewport/types.ts +133 -0
- package/src/core/viewport/utils/speed-tracker.ts +79 -0
- package/src/core/viewport/viewport.ts +265 -0
- package/src/index.ts +0 -7
- package/src/styles/components/_vlist.scss +352 -0
- package/src/styles/index.scss +1 -1
- package/test/components/vlist-selection.test.ts +240 -0
- package/test/components/vlist.test.ts +63 -0
- package/test/core/collection/adapter.test.ts +161 -0
- package/bun.lock +0 -792
- package/src/components/list/api.ts +0 -314
- package/src/components/list/constants.ts +0 -56
- package/src/components/list/features/api.ts +0 -428
- package/src/components/list/features/index.ts +0 -31
- package/src/components/list/features/list-manager.ts +0 -502
- package/src/components/list/index.ts +0 -39
- package/src/components/list/list.ts +0 -234
- package/src/core/collection/base-collection.ts +0 -100
- package/src/core/collection/collection-composer.ts +0 -178
- package/src/core/collection/collection.ts +0 -745
- package/src/core/collection/constants.ts +0 -172
- package/src/core/collection/events.ts +0 -428
- package/src/core/collection/features/api/loading.ts +0 -279
- package/src/core/collection/features/operations/data-operations.ts +0 -147
- package/src/core/collection/index.ts +0 -104
- package/src/core/collection/state.ts +0 -497
- package/src/core/collection/types.ts +0 -404
- package/src/core/compose/features/collection.ts +0 -119
- package/src/core/compose/features/selection.ts +0 -213
- package/src/core/compose/features/styling.ts +0 -108
- package/src/core/list-manager/api.ts +0 -599
- package/src/core/list-manager/config.ts +0 -593
- package/src/core/list-manager/constants.ts +0 -268
- package/src/core/list-manager/features/api.ts +0 -58
- package/src/core/list-manager/features/collection/collection.ts +0 -705
- package/src/core/list-manager/features/collection/index.ts +0 -17
- package/src/core/list-manager/features/viewport/constants.ts +0 -42
- package/src/core/list-manager/features/viewport/index.ts +0 -16
- package/src/core/list-manager/features/viewport/placeholders.ts +0 -281
- package/src/core/list-manager/features/viewport/rendering.ts +0 -575
- package/src/core/list-manager/features/viewport/scrollbar.ts +0 -495
- package/src/core/list-manager/features/viewport/scrolling.ts +0 -795
- package/src/core/list-manager/features/viewport/template.ts +0 -220
- package/src/core/list-manager/features/viewport/viewport.ts +0 -654
- package/src/core/list-manager/features/viewport/virtual.ts +0 -309
- package/src/core/list-manager/index.ts +0 -279
- package/src/core/list-manager/list-manager.ts +0 -206
- package/src/core/list-manager/types.ts +0 -439
- package/src/core/list-manager/utils/calculations.ts +0 -290
- package/src/core/list-manager/utils/range-calculator.ts +0 -349
- package/src/core/list-manager/utils/speed-tracker.ts +0 -273
- package/src/styles/components/_list.scss +0 -244
- package/src/types/mtrl.d.ts +0 -6
- package/test/components/list.test.ts +0 -256
- package/test/core/collection/failed-ranges.test.ts +0 -270
- package/test/core/compose/features.test.ts +0 -183
- package/test/core/list-manager/features/collection.test.ts +0 -704
- package/test/core/list-manager/features/viewport.test.ts +0 -698
- package/test/core/list-manager/list-manager.test.ts +0 -593
- package/test/core/list-manager/utils/calculations.test.ts +0 -433
- package/test/core/list-manager/utils/range-calculator.test.ts +0 -569
- package/test/core/list-manager/utils/speed-tracker.test.ts +0 -530
- package/tsconfig.build.json +0 -23
- /package/src/components/{list → vlist}/features.ts +0 -0
- /package/src/core/{compose → viewport}/features/performance.ts +0 -0
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Basic list component tests
|
|
3
|
-
*
|
|
4
|
-
* Tests the pipe composition list component that wraps the collection system
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, test, expect, beforeEach } from "bun:test";
|
|
8
|
-
import { JSDOM } from "jsdom";
|
|
9
|
-
|
|
10
|
-
// Mock DOM environment for testing
|
|
11
|
-
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");
|
|
12
|
-
global.document = dom.window.document;
|
|
13
|
-
global.HTMLElement = dom.window.HTMLElement;
|
|
14
|
-
global.window = dom.window as any;
|
|
15
|
-
global.navigator = dom.window.navigator;
|
|
16
|
-
global.requestAnimationFrame = (cb: FrameRequestCallback) => {
|
|
17
|
-
setTimeout(cb, 0);
|
|
18
|
-
return 0;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
// Mock scrollIntoView since JSDOM doesn't implement it
|
|
22
|
-
global.HTMLElement.prototype.scrollIntoView = function (
|
|
23
|
-
options?: ScrollIntoViewOptions
|
|
24
|
-
) {
|
|
25
|
-
// Mock implementation - just logs the call
|
|
26
|
-
console.log(`Mock scrollIntoView called with options:`, options);
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
// Import our list component
|
|
30
|
-
import { createList, type ListItem } from "../../src/components/list";
|
|
31
|
-
|
|
32
|
-
// Test data interface
|
|
33
|
-
interface TestUser extends ListItem {
|
|
34
|
-
id: string;
|
|
35
|
-
name: string;
|
|
36
|
-
email: string;
|
|
37
|
-
age: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Test data
|
|
41
|
-
const testUsers: TestUser[] = [
|
|
42
|
-
{ id: "1", name: "John Doe", email: "john@example.com", age: 30 },
|
|
43
|
-
{ id: "2", name: "Jane Smith", email: "jane@example.com", age: 25 },
|
|
44
|
-
{ id: "3", name: "Bob Johnson", email: "bob@example.com", age: 35 },
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
describe("List Component (Pipe Composition)", () => {
|
|
48
|
-
let container: HTMLElement;
|
|
49
|
-
|
|
50
|
-
beforeEach(() => {
|
|
51
|
-
// Create a fresh container for each test
|
|
52
|
-
container = document.createElement("div");
|
|
53
|
-
document.body.appendChild(container);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("creates list with pipe composition pattern", () => {
|
|
57
|
-
const list = createList<TestUser>({
|
|
58
|
-
container,
|
|
59
|
-
items: testUsers,
|
|
60
|
-
className: "user-list",
|
|
61
|
-
ariaLabel: "User List",
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
// mtrl creates a new element and mounts it to the container
|
|
65
|
-
expect(list.element).not.toBe(container);
|
|
66
|
-
expect(list.element.parentElement).toBe(container);
|
|
67
|
-
expect(list.getSize()).toBe(3);
|
|
68
|
-
expect(list.isEmpty()).toBe(false);
|
|
69
|
-
expect(list.element.classList.contains("mtrl-list")).toBe(true);
|
|
70
|
-
expect(list.element.getAttribute("aria-label")).toBe("User List");
|
|
71
|
-
expect(list.element.getAttribute("role")).toBe("list");
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("applies list styling through composition", async () => {
|
|
75
|
-
const list = createList<TestUser>({
|
|
76
|
-
container,
|
|
77
|
-
items: testUsers,
|
|
78
|
-
listStyle: {
|
|
79
|
-
itemSize: 60,
|
|
80
|
-
gap: 8,
|
|
81
|
-
padding: 16,
|
|
82
|
-
striped: true,
|
|
83
|
-
hoverable: true,
|
|
84
|
-
bordered: false,
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// Wait for rendering
|
|
89
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
90
|
-
|
|
91
|
-
// Check styling on the list element, not the container
|
|
92
|
-
expect(list.element.style.getPropertyValue("--mtrl-list-gap")).toBe("8px");
|
|
93
|
-
expect(list.element.style.getPropertyValue("--mtrl-list-padding")).toBe(
|
|
94
|
-
"16px"
|
|
95
|
-
);
|
|
96
|
-
expect(list.element.style.getPropertyValue("--mtrl-list-item-height")).toBe(
|
|
97
|
-
"60px"
|
|
98
|
-
);
|
|
99
|
-
expect(list.element.classList.contains("mtrl-list--striped")).toBe(true);
|
|
100
|
-
expect(list.element.classList.contains("mtrl-list--hoverable")).toBe(true);
|
|
101
|
-
expect(list.element.classList.contains("mtrl-list--bordered")).toBe(false);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("enables selection through composition", async () => {
|
|
105
|
-
const list = createList<TestUser>({
|
|
106
|
-
container,
|
|
107
|
-
items: testUsers,
|
|
108
|
-
selection: {
|
|
109
|
-
enabled: true,
|
|
110
|
-
multiple: true,
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
// Wait for rendering
|
|
115
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
116
|
-
|
|
117
|
-
// Test selection methods exist
|
|
118
|
-
expect(typeof list.selectItem).toBe("function");
|
|
119
|
-
expect(typeof list.deselectItem).toBe("function");
|
|
120
|
-
expect(typeof list.selectAll).toBe("function");
|
|
121
|
-
expect(typeof list.deselectAll).toBe("function");
|
|
122
|
-
expect(typeof list.getSelectedItems).toBe("function");
|
|
123
|
-
expect(typeof list.getSelectedIds).toBe("function");
|
|
124
|
-
|
|
125
|
-
// Test selection functionality
|
|
126
|
-
list.selectItem("1");
|
|
127
|
-
expect(list.getSelectedIds()).toContain("1");
|
|
128
|
-
expect(list.getSelectedItems().length).toBe(1);
|
|
129
|
-
|
|
130
|
-
list.selectItem("2");
|
|
131
|
-
expect(list.getSelectedIds()).toHaveLength(2);
|
|
132
|
-
|
|
133
|
-
list.deselectItem("1");
|
|
134
|
-
expect(list.getSelectedIds()).toHaveLength(1);
|
|
135
|
-
expect(list.getSelectedIds()).toContain("2");
|
|
136
|
-
|
|
137
|
-
list.deselectAll();
|
|
138
|
-
expect(list.getSelectedIds()).toHaveLength(0);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test("tracks performance through composition", async () => {
|
|
142
|
-
const list = createList<TestUser>({
|
|
143
|
-
container,
|
|
144
|
-
items: testUsers,
|
|
145
|
-
performance: {
|
|
146
|
-
recycleElements: true,
|
|
147
|
-
bufferSize: 50,
|
|
148
|
-
},
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
// Wait for rendering
|
|
152
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
153
|
-
|
|
154
|
-
// Test performance methods exist
|
|
155
|
-
expect(typeof list.getMetrics).toBe("function");
|
|
156
|
-
expect(typeof list.resetMetrics).toBe("function");
|
|
157
|
-
|
|
158
|
-
const metrics = list.getMetrics();
|
|
159
|
-
expect(typeof metrics.renderCount).toBe("number");
|
|
160
|
-
expect(typeof metrics.scrollCount).toBe("number");
|
|
161
|
-
expect(typeof metrics.averageRenderTime).toBe("number");
|
|
162
|
-
expect(typeof metrics.averageScrollTime).toBe("number");
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
test("provides list API through composition", async () => {
|
|
166
|
-
const list = createList<TestUser>({
|
|
167
|
-
container,
|
|
168
|
-
items: testUsers,
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
// Wait for rendering
|
|
172
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
173
|
-
|
|
174
|
-
// Test API methods exist
|
|
175
|
-
expect(typeof list.scrollToItem).toBe("function");
|
|
176
|
-
expect(typeof list.scrollToIndex).toBe("function");
|
|
177
|
-
expect(typeof list.scrollToPage).toBe("function");
|
|
178
|
-
|
|
179
|
-
// Test scrolling doesn't crash
|
|
180
|
-
list.scrollToItem("2");
|
|
181
|
-
list.scrollToIndex(1);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
test("maintains collection functionality", async () => {
|
|
185
|
-
const list = createList<TestUser>({
|
|
186
|
-
container,
|
|
187
|
-
items: [testUsers[0]],
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
// Test collection methods still work
|
|
191
|
-
expect(list.getSize()).toBe(1);
|
|
192
|
-
|
|
193
|
-
const newUser: TestUser = {
|
|
194
|
-
id: "4",
|
|
195
|
-
name: "Alice Brown",
|
|
196
|
-
email: "alice@example.com",
|
|
197
|
-
age: 28,
|
|
198
|
-
};
|
|
199
|
-
await list.add(newUser);
|
|
200
|
-
|
|
201
|
-
expect(list.getSize()).toBe(2);
|
|
202
|
-
expect(list.getItem("4")?.name).toBe("Alice Brown");
|
|
203
|
-
|
|
204
|
-
await list.remove("1");
|
|
205
|
-
expect(list.getSize()).toBe(1);
|
|
206
|
-
expect(list.getItem("1")).toBeUndefined();
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test("console shows pipe composition pattern", () => {
|
|
210
|
-
// Capture console logs
|
|
211
|
-
const logs: string[] = [];
|
|
212
|
-
const originalLog = console.log;
|
|
213
|
-
console.log = (message: string) => {
|
|
214
|
-
logs.push(message);
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
try {
|
|
218
|
-
createList<TestUser>({
|
|
219
|
-
container,
|
|
220
|
-
items: testUsers,
|
|
221
|
-
selection: { enabled: true },
|
|
222
|
-
listStyle: { striped: true },
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// Restore console
|
|
226
|
-
console.log = originalLog;
|
|
227
|
-
|
|
228
|
-
// Verify composition pattern logs
|
|
229
|
-
expect(
|
|
230
|
-
logs.some((log) =>
|
|
231
|
-
log.includes("Creating list component (mtrl compose system)")
|
|
232
|
-
)
|
|
233
|
-
).toBe(true);
|
|
234
|
-
expect(
|
|
235
|
-
logs.some((log) => log.includes("Adding collection capabilities"))
|
|
236
|
-
).toBe(true);
|
|
237
|
-
expect(
|
|
238
|
-
logs.some((log) => log.includes("Adding styling capabilities"))
|
|
239
|
-
).toBe(true);
|
|
240
|
-
expect(
|
|
241
|
-
logs.some((log) => log.includes("Adding selection capabilities"))
|
|
242
|
-
).toBe(true);
|
|
243
|
-
expect(
|
|
244
|
-
logs.some((log) => log.includes("Adding performance tracking"))
|
|
245
|
-
).toBe(true);
|
|
246
|
-
expect(logs.some((log) => log.includes("Adding list API methods"))).toBe(
|
|
247
|
-
true
|
|
248
|
-
);
|
|
249
|
-
expect(
|
|
250
|
-
logs.some((log) => log.includes("created via mtrl compose system"))
|
|
251
|
-
).toBe(true);
|
|
252
|
-
} finally {
|
|
253
|
-
console.log = originalLog;
|
|
254
|
-
}
|
|
255
|
-
});
|
|
256
|
-
});
|
|
@@ -1,270 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Modular compose features tests
|
|
3
|
-
*
|
|
4
|
-
* Tests that our extracted features work independently and can be reused
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, test, expect, beforeEach } from "bun:test";
|
|
8
|
-
import { JSDOM } from "jsdom";
|
|
9
|
-
|
|
10
|
-
// Mock DOM environment for testing
|
|
11
|
-
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");
|
|
12
|
-
global.document = dom.window.document;
|
|
13
|
-
global.HTMLElement = dom.window.HTMLElement;
|
|
14
|
-
global.window = dom.window as any;
|
|
15
|
-
global.navigator = dom.window.navigator;
|
|
16
|
-
global.requestAnimationFrame = (cb: FrameRequestCallback) => {
|
|
17
|
-
setTimeout(cb, 0);
|
|
18
|
-
return 0;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
// Import compose features
|
|
22
|
-
import {
|
|
23
|
-
pipe,
|
|
24
|
-
createBase,
|
|
25
|
-
withElement,
|
|
26
|
-
withEvents,
|
|
27
|
-
withCollection,
|
|
28
|
-
withStyling,
|
|
29
|
-
withSelection,
|
|
30
|
-
withPerformance,
|
|
31
|
-
} from "../../../src/core/compose";
|
|
32
|
-
|
|
33
|
-
describe("Modular Compose Features", () => {
|
|
34
|
-
let container: HTMLElement;
|
|
35
|
-
|
|
36
|
-
beforeEach(() => {
|
|
37
|
-
container = document.createElement("div");
|
|
38
|
-
document.body.appendChild(container);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("withStyling works independently", () => {
|
|
42
|
-
const component = pipe(
|
|
43
|
-
createBase,
|
|
44
|
-
withElement({ componentName: "test" }),
|
|
45
|
-
withStyling({
|
|
46
|
-
gap: 16,
|
|
47
|
-
padding: 20,
|
|
48
|
-
striped: true,
|
|
49
|
-
componentName: "test",
|
|
50
|
-
})
|
|
51
|
-
)({ prefix: "mtrl" });
|
|
52
|
-
|
|
53
|
-
expect(component.element.style.getPropertyValue("--mtrl-test-gap")).toBe(
|
|
54
|
-
"16px"
|
|
55
|
-
);
|
|
56
|
-
expect(
|
|
57
|
-
component.element.style.getPropertyValue("--mtrl-test-padding")
|
|
58
|
-
).toBe("20px");
|
|
59
|
-
expect(component.element.classList.contains("mtrl-test--striped")).toBe(
|
|
60
|
-
true
|
|
61
|
-
);
|
|
62
|
-
expect(typeof component.setStyle).toBe("function");
|
|
63
|
-
expect(typeof component.getStyle).toBe("function");
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test.skip("withSelection works independently", async () => {
|
|
67
|
-
// TODO: Debug why selection doesn't work in isolated test
|
|
68
|
-
// This works fine in the full list component tests
|
|
69
|
-
expect(true).toBe(true);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("withPerformance works independently", () => {
|
|
73
|
-
const component = pipe(
|
|
74
|
-
createBase,
|
|
75
|
-
withElement({ componentName: "test" }),
|
|
76
|
-
withEvents(),
|
|
77
|
-
withPerformance({
|
|
78
|
-
trackMemory: true,
|
|
79
|
-
maxSamples: 50,
|
|
80
|
-
})
|
|
81
|
-
)({ prefix: "mtrl" });
|
|
82
|
-
|
|
83
|
-
// Mock getItems method that performance expects
|
|
84
|
-
component.getItems = () => [{ id: "1" }, { id: "2" }];
|
|
85
|
-
|
|
86
|
-
expect(typeof component.getMetrics).toBe("function");
|
|
87
|
-
expect(typeof component.resetMetrics).toBe("function");
|
|
88
|
-
|
|
89
|
-
const metrics = component.getMetrics();
|
|
90
|
-
expect(typeof metrics.renderCount).toBe("number");
|
|
91
|
-
expect(typeof metrics.scrollCount).toBe("number");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test("withCollection works independently", () => {
|
|
95
|
-
const testData = [
|
|
96
|
-
{ id: "1", name: "Item 1" },
|
|
97
|
-
{ id: "2", name: "Item 2" },
|
|
98
|
-
];
|
|
99
|
-
|
|
100
|
-
const component = pipe(
|
|
101
|
-
createBase,
|
|
102
|
-
withElement({ componentName: "test" }),
|
|
103
|
-
withCollection({
|
|
104
|
-
items: testData,
|
|
105
|
-
})
|
|
106
|
-
)({ prefix: "mtrl" });
|
|
107
|
-
|
|
108
|
-
expect(typeof component.add).toBe("function");
|
|
109
|
-
expect(typeof component.remove).toBe("function");
|
|
110
|
-
expect(typeof component.getItems).toBe("function");
|
|
111
|
-
expect(typeof component.getItem).toBe("function");
|
|
112
|
-
expect(component.getSize()).toBe(2);
|
|
113
|
-
expect(component.isEmpty()).toBe(false);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test("features can be combined flexibly", () => {
|
|
117
|
-
const testData = [
|
|
118
|
-
{ id: "1", name: "Test Item 1" },
|
|
119
|
-
{ id: "2", name: "Test Item 2" },
|
|
120
|
-
];
|
|
121
|
-
|
|
122
|
-
// Create a custom component with a different combination
|
|
123
|
-
const customComponent = pipe(
|
|
124
|
-
createBase,
|
|
125
|
-
withElement({ componentName: "custom" }),
|
|
126
|
-
withEvents(),
|
|
127
|
-
withStyling({
|
|
128
|
-
gap: 12,
|
|
129
|
-
hoverable: true,
|
|
130
|
-
componentName: "custom",
|
|
131
|
-
}),
|
|
132
|
-
withCollection({
|
|
133
|
-
items: testData,
|
|
134
|
-
}),
|
|
135
|
-
withPerformance({
|
|
136
|
-
trackMemory: false,
|
|
137
|
-
})
|
|
138
|
-
// Note: deliberately omitting withSelection to show flexibility
|
|
139
|
-
)({ prefix: "mtrl" });
|
|
140
|
-
|
|
141
|
-
// Should have styling
|
|
142
|
-
expect(
|
|
143
|
-
customComponent.element.style.getPropertyValue("--mtrl-custom-gap")
|
|
144
|
-
).toBe("12px");
|
|
145
|
-
expect(
|
|
146
|
-
customComponent.element.classList.contains("mtrl-custom--hoverable")
|
|
147
|
-
).toBe(true);
|
|
148
|
-
|
|
149
|
-
// Should have collection
|
|
150
|
-
expect(customComponent.getSize()).toBe(2);
|
|
151
|
-
expect(customComponent.getItem("1").name).toBe("Test Item 1");
|
|
152
|
-
|
|
153
|
-
// Should have performance
|
|
154
|
-
expect(typeof customComponent.getMetrics).toBe("function");
|
|
155
|
-
|
|
156
|
-
// Should NOT have selection (proving modularity)
|
|
157
|
-
expect(customComponent.selectItem).toBeUndefined();
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("features maintain mtrl patterns", () => {
|
|
161
|
-
const component = pipe(
|
|
162
|
-
createBase,
|
|
163
|
-
withElement({ componentName: "pattern-test" }),
|
|
164
|
-
withStyling({
|
|
165
|
-
striped: true,
|
|
166
|
-
componentName: "pattern-test",
|
|
167
|
-
})
|
|
168
|
-
)({ prefix: "mtrl" });
|
|
169
|
-
|
|
170
|
-
// Should follow mtrl class naming conventions
|
|
171
|
-
expect(component.element.classList.contains("mtrl-pattern-test")).toBe(
|
|
172
|
-
true
|
|
173
|
-
);
|
|
174
|
-
expect(
|
|
175
|
-
component.element.classList.contains("mtrl-pattern-test--striped")
|
|
176
|
-
).toBe(true);
|
|
177
|
-
|
|
178
|
-
// Should have mtrl prefix utilities
|
|
179
|
-
expect(typeof component.getClass).toBe("function");
|
|
180
|
-
expect(typeof component.getModifierClass).toBe("function");
|
|
181
|
-
expect(component.getClass("item")).toBe("mtrl-item");
|
|
182
|
-
});
|
|
183
|
-
});
|