@assistant-ui/tap 0.3.4 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/ResourceFiber.d.ts +1 -1
- package/dist/core/ResourceFiber.d.ts.map +1 -1
- package/dist/core/ResourceFiber.js +35 -40
- package/dist/core/ResourceFiber.js.map +1 -1
- package/dist/core/callResourceFn.js +15 -12
- package/dist/core/callResourceFn.js.map +1 -1
- package/dist/core/commit.d.ts +1 -1
- package/dist/core/commit.d.ts.map +1 -1
- package/dist/core/commit.js +57 -54
- package/dist/core/commit.js.map +1 -1
- package/dist/core/context.js +16 -21
- package/dist/core/context.js.map +1 -1
- package/dist/core/createResource.d.ts +1 -1
- package/dist/core/createResource.d.ts.map +1 -1
- package/dist/core/createResource.js +54 -67
- package/dist/core/createResource.js.map +1 -1
- package/dist/core/execution-context.d.ts +1 -1
- package/dist/core/execution-context.d.ts.map +1 -1
- package/dist/core/execution-context.js +21 -25
- package/dist/core/execution-context.js.map +1 -1
- package/dist/core/resource.d.ts +1 -1
- package/dist/core/resource.d.ts.map +1 -1
- package/dist/core/resource.js +8 -12
- package/dist/core/resource.js.map +1 -1
- package/dist/core/scheduler.js +73 -72
- package/dist/core/scheduler.js.map +1 -1
- package/dist/core/types.d.ts +3 -3
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +1 -0
- package/dist/core/types.js.map +1 -1
- package/dist/hooks/depsShallowEqual.js +8 -10
- package/dist/hooks/depsShallowEqual.js.map +1 -1
- package/dist/hooks/tap-callback.js +2 -6
- package/dist/hooks/tap-callback.js.map +1 -1
- package/dist/hooks/tap-effect-event.js +21 -10
- package/dist/hooks/tap-effect-event.js.map +1 -1
- package/dist/hooks/tap-effect.js +30 -31
- package/dist/hooks/tap-effect.js.map +1 -1
- package/dist/hooks/tap-inline-resource.d.ts +1 -1
- package/dist/hooks/tap-inline-resource.d.ts.map +1 -1
- package/dist/hooks/tap-inline-resource.js +2 -6
- package/dist/hooks/tap-inline-resource.js.map +1 -1
- package/dist/hooks/tap-memo.js +10 -14
- package/dist/hooks/tap-memo.js.map +1 -1
- package/dist/hooks/tap-ref.js +5 -9
- package/dist/hooks/tap-ref.js.map +1 -1
- package/dist/hooks/tap-resource.d.ts +1 -1
- package/dist/hooks/tap-resource.d.ts.map +1 -1
- package/dist/hooks/tap-resource.js +13 -28
- package/dist/hooks/tap-resource.js.map +1 -1
- package/dist/hooks/tap-resources.d.ts +1 -1
- package/dist/hooks/tap-resources.d.ts.map +1 -1
- package/dist/hooks/tap-resources.js +63 -64
- package/dist/hooks/tap-resources.js.map +1 -1
- package/dist/hooks/tap-state.js +47 -44
- package/dist/hooks/tap-state.js.map +1 -1
- package/dist/index.d.ts +14 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -31
- package/dist/index.js.map +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -5
- package/dist/react/index.js.map +1 -1
- package/dist/react/use-resource.d.ts +1 -1
- package/dist/react/use-resource.d.ts.map +1 -1
- package/dist/react/use-resource.js +16 -26
- package/dist/react/use-resource.js.map +1 -1
- package/package.json +44 -30
- package/react/package.json +5 -0
- package/src/__tests__/basic/resourceHandle.test.ts +56 -0
- package/src/__tests__/basic/tapEffect.basic.test.ts +247 -0
- package/src/__tests__/basic/tapResources.basic.test.ts +222 -0
- package/src/__tests__/basic/tapState.basic.test.ts +240 -0
- package/src/__tests__/errors/errors.effect-errors.test.ts +222 -0
- package/src/__tests__/errors/errors.render-errors.test.ts +190 -0
- package/src/__tests__/lifecycle/lifecycle.dependencies.test.ts +241 -0
- package/src/__tests__/lifecycle/lifecycle.mount-unmount.test.ts +211 -0
- package/src/__tests__/rules/rules.hook-count.test.ts +200 -0
- package/src/__tests__/rules/rules.hook-order.test.ts +192 -0
- package/src/__tests__/test-utils.ts +219 -0
- package/src/core/ResourceFiber.ts +58 -0
- package/src/core/callResourceFn.ts +21 -0
- package/src/core/commit.ts +73 -0
- package/src/core/context.ts +28 -0
- package/src/core/createResource.ts +116 -0
- package/src/core/execution-context.ts +34 -0
- package/src/core/resource.ts +16 -0
- package/src/core/scheduler.ts +95 -0
- package/src/core/types.ts +59 -0
- package/src/hooks/depsShallowEqual.ts +10 -0
- package/src/hooks/tap-callback.ts +8 -0
- package/src/hooks/tap-effect-event.ts +29 -0
- package/src/hooks/tap-effect.ts +59 -0
- package/src/hooks/tap-inline-resource.ts +8 -0
- package/src/hooks/tap-memo.ts +16 -0
- package/src/hooks/tap-ref.ts +16 -0
- package/src/hooks/tap-resource.ts +44 -0
- package/src/hooks/tap-resources.ts +112 -0
- package/src/hooks/tap-state.ts +83 -0
- package/src/index.ts +31 -0
- package/src/react/index.ts +1 -0
- package/src/react/use-resource.ts +35 -0
- package/dist/__tests__/test-utils.d.ts +0 -79
- package/dist/__tests__/test-utils.d.ts.map +0 -1
- package/dist/__tests__/test-utils.js +0 -138
- package/dist/__tests__/test-utils.js.map +0 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { createResource } from "../../core/createResource";
|
|
3
|
+
import { resource } from "../../core/resource";
|
|
4
|
+
|
|
5
|
+
describe("ResourceHandle - Basic Usage", () => {
|
|
6
|
+
it("should create a resource handle with const API", () => {
|
|
7
|
+
const TestResource = resource((props: number) => {
|
|
8
|
+
return {
|
|
9
|
+
value: props * 2,
|
|
10
|
+
propsUsed: props,
|
|
11
|
+
};
|
|
12
|
+
});
|
|
13
|
+
const handle = createResource(TestResource(5));
|
|
14
|
+
|
|
15
|
+
// The handle provides a const API
|
|
16
|
+
expect(typeof handle.getState).toBe("function");
|
|
17
|
+
expect(typeof handle.subscribe).toBe("function");
|
|
18
|
+
expect(typeof handle.render).toBe("function");
|
|
19
|
+
|
|
20
|
+
// Initial state
|
|
21
|
+
expect(handle.getState().value).toBe(10);
|
|
22
|
+
expect(handle.getState().propsUsed).toBe(5);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should allow updating props", () => {
|
|
26
|
+
const TestResource = resource((props: { multiplier: number }) => {
|
|
27
|
+
return { result: 10 * props.multiplier };
|
|
28
|
+
});
|
|
29
|
+
const handle = createResource(TestResource({ multiplier: 2 }));
|
|
30
|
+
|
|
31
|
+
// Initial state
|
|
32
|
+
expect(handle.getState().result).toBe(20);
|
|
33
|
+
|
|
34
|
+
// Can call render to update props
|
|
35
|
+
expect(() => handle.render(TestResource({ multiplier: 3 }))).not.toThrow();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should support subscribing and unsubscribing", () => {
|
|
39
|
+
const TestResource = resource(() => ({ timestamp: Date.now() }));
|
|
40
|
+
const handle = createResource(TestResource());
|
|
41
|
+
|
|
42
|
+
const subscriber1 = vi.fn();
|
|
43
|
+
const subscriber2 = vi.fn();
|
|
44
|
+
|
|
45
|
+
// Can subscribe multiple callbacks
|
|
46
|
+
const unsub1 = handle.subscribe(subscriber1);
|
|
47
|
+
const unsub2 = handle.subscribe(subscriber2);
|
|
48
|
+
|
|
49
|
+
// Can unsubscribe individually
|
|
50
|
+
expect(typeof unsub1).toBe("function");
|
|
51
|
+
expect(typeof unsub2).toBe("function");
|
|
52
|
+
|
|
53
|
+
unsub1();
|
|
54
|
+
unsub2();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { tapEffect } from "../../hooks/tap-effect";
|
|
3
|
+
import { tapState } from "../../hooks/tap-state";
|
|
4
|
+
import {
|
|
5
|
+
createTestResource,
|
|
6
|
+
renderTest,
|
|
7
|
+
cleanupAllResources,
|
|
8
|
+
TestResourceManager,
|
|
9
|
+
} from "../test-utils";
|
|
10
|
+
|
|
11
|
+
describe("tapEffect - Basic Functionality", () => {
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
cleanupAllResources();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("Effect Lifecycle", () => {
|
|
17
|
+
it("should run effect after mount and commit", () => {
|
|
18
|
+
const executionOrder: string[] = [];
|
|
19
|
+
|
|
20
|
+
const testFiber = createTestResource(() => {
|
|
21
|
+
executionOrder.push("render");
|
|
22
|
+
|
|
23
|
+
tapEffect(() => {
|
|
24
|
+
executionOrder.push("effect");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return null;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Use TestResourceManager for fine-grained control
|
|
31
|
+
const manager = new TestResourceManager(testFiber);
|
|
32
|
+
|
|
33
|
+
// Mount and render
|
|
34
|
+
manager.renderAndMount(undefined);
|
|
35
|
+
|
|
36
|
+
// Effect should run after commit
|
|
37
|
+
expect(executionOrder).toEqual(["render", "effect"]);
|
|
38
|
+
|
|
39
|
+
manager.cleanup();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("should call cleanup function on unmount", () => {
|
|
43
|
+
const cleanup = vi.fn();
|
|
44
|
+
const effect = vi.fn(() => cleanup);
|
|
45
|
+
|
|
46
|
+
const testFiber = createTestResource(() => {
|
|
47
|
+
tapEffect(effect);
|
|
48
|
+
return null;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const manager = new TestResourceManager(testFiber);
|
|
52
|
+
manager.renderAndMount(undefined);
|
|
53
|
+
|
|
54
|
+
// Effect should be called, but not cleanup
|
|
55
|
+
expect(effect).toHaveBeenCalledTimes(1);
|
|
56
|
+
expect(cleanup).not.toHaveBeenCalled();
|
|
57
|
+
|
|
58
|
+
// Cleanup should be called on unmount
|
|
59
|
+
manager.cleanup();
|
|
60
|
+
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should cleanup effects in reverse order", () => {
|
|
64
|
+
const cleanupOrder: string[] = [];
|
|
65
|
+
|
|
66
|
+
const testFiber = createTestResource(() => {
|
|
67
|
+
tapEffect(() => {
|
|
68
|
+
return () => cleanupOrder.push("first");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
tapEffect(() => {
|
|
72
|
+
return () => cleanupOrder.push("second");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
tapEffect(() => {
|
|
76
|
+
return () => cleanupOrder.push("third");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return null;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const manager = new TestResourceManager(testFiber);
|
|
83
|
+
manager.renderAndMount(undefined);
|
|
84
|
+
manager.cleanup();
|
|
85
|
+
|
|
86
|
+
// Cleanup should run in reverse order (LIFO)
|
|
87
|
+
expect(cleanupOrder).toEqual(["third", "second", "first"]);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("Multiple Effects", () => {
|
|
92
|
+
it("should execute multiple effects in registration order", () => {
|
|
93
|
+
const executionOrder: string[] = [];
|
|
94
|
+
const effects = [
|
|
95
|
+
() => {
|
|
96
|
+
executionOrder.push("effect1");
|
|
97
|
+
},
|
|
98
|
+
() => {
|
|
99
|
+
executionOrder.push("effect2");
|
|
100
|
+
},
|
|
101
|
+
() => {
|
|
102
|
+
executionOrder.push("effect3");
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
const testFiber = createTestResource(() => {
|
|
107
|
+
effects.forEach((fn) => tapEffect(fn));
|
|
108
|
+
return null;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
renderTest(testFiber, undefined);
|
|
112
|
+
expect(executionOrder).toEqual(["effect1", "effect2", "effect3"]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should handle mixed effects with and without dependencies", () => {
|
|
116
|
+
const effectCalls = {
|
|
117
|
+
always: 0,
|
|
118
|
+
once: 0,
|
|
119
|
+
conditional: 0,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const testFiber = createTestResource((props: { value: number }) => {
|
|
123
|
+
// Effect without deps - runs on every render
|
|
124
|
+
tapEffect(() => {
|
|
125
|
+
effectCalls.always++;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Effect with empty deps - runs only once
|
|
129
|
+
tapEffect(() => {
|
|
130
|
+
effectCalls.once++;
|
|
131
|
+
}, []);
|
|
132
|
+
|
|
133
|
+
// Effect with deps - runs when deps change
|
|
134
|
+
tapEffect(() => {
|
|
135
|
+
effectCalls.conditional++;
|
|
136
|
+
}, [props.value]);
|
|
137
|
+
|
|
138
|
+
return effectCalls;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Initial render
|
|
142
|
+
renderTest(testFiber, { value: 1 });
|
|
143
|
+
expect(effectCalls).toEqual({ always: 1, once: 1, conditional: 1 });
|
|
144
|
+
|
|
145
|
+
// Re-render with same props
|
|
146
|
+
renderTest(testFiber, { value: 1 });
|
|
147
|
+
expect(effectCalls).toEqual({ always: 2, once: 1, conditional: 1 });
|
|
148
|
+
|
|
149
|
+
// Re-render with different props
|
|
150
|
+
renderTest(testFiber, { value: 2 });
|
|
151
|
+
expect(effectCalls).toEqual({ always: 3, once: 1, conditional: 2 });
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("Effect Dependencies", () => {
|
|
156
|
+
it("should not re-run effect with empty dependency array", () => {
|
|
157
|
+
const effect = vi.fn();
|
|
158
|
+
let triggerRerender: (() => void) | null = null;
|
|
159
|
+
|
|
160
|
+
const testFiber = createTestResource(() => {
|
|
161
|
+
const [, setState] = tapState(0);
|
|
162
|
+
|
|
163
|
+
tapEffect(() => {
|
|
164
|
+
triggerRerender = () => setState((prev) => prev + 1);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
tapEffect(effect, []);
|
|
168
|
+
|
|
169
|
+
return null;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Initial render
|
|
173
|
+
renderTest(testFiber, undefined);
|
|
174
|
+
expect(effect).toHaveBeenCalledTimes(1);
|
|
175
|
+
|
|
176
|
+
// Trigger re-render
|
|
177
|
+
triggerRerender!();
|
|
178
|
+
|
|
179
|
+
// Effect with empty deps should not re-run
|
|
180
|
+
expect(effect).toHaveBeenCalledTimes(1);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should re-run effect when dependencies change", () => {
|
|
184
|
+
const effect = vi.fn();
|
|
185
|
+
|
|
186
|
+
const testFiber = createTestResource((props: { dep: string }) => {
|
|
187
|
+
tapEffect(() => {
|
|
188
|
+
effect(props.dep);
|
|
189
|
+
}, [props.dep]);
|
|
190
|
+
|
|
191
|
+
return null;
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Initial render
|
|
195
|
+
renderTest(testFiber, { dep: "a" });
|
|
196
|
+
expect(effect).toHaveBeenCalledTimes(1);
|
|
197
|
+
expect(effect).toHaveBeenLastCalledWith("a");
|
|
198
|
+
|
|
199
|
+
// Re-render with same dependency
|
|
200
|
+
renderTest(testFiber, { dep: "a" });
|
|
201
|
+
expect(effect).toHaveBeenCalledTimes(1);
|
|
202
|
+
|
|
203
|
+
// Re-render with different dependency
|
|
204
|
+
renderTest(testFiber, { dep: "b" });
|
|
205
|
+
expect(effect).toHaveBeenCalledTimes(2);
|
|
206
|
+
expect(effect).toHaveBeenLastCalledWith("b");
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("Effect Timing", () => {
|
|
211
|
+
it("should run effects after state updates are committed", () => {
|
|
212
|
+
const events: string[] = [];
|
|
213
|
+
|
|
214
|
+
const testFiber = createTestResource(() => {
|
|
215
|
+
const [count, setCount] = tapState(0);
|
|
216
|
+
|
|
217
|
+
events.push(`render: ${count}`);
|
|
218
|
+
|
|
219
|
+
tapEffect(() => {
|
|
220
|
+
events.push(`effect: ${count}`);
|
|
221
|
+
|
|
222
|
+
// Only update on first effect to avoid infinite loop
|
|
223
|
+
if (count === 0) {
|
|
224
|
+
setCount(1);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return count;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const manager = new TestResourceManager(testFiber);
|
|
232
|
+
|
|
233
|
+
// Initial render
|
|
234
|
+
manager.renderAndMount(undefined);
|
|
235
|
+
// Without mount tracking, the effect runs immediately during commit
|
|
236
|
+
// This triggers setState which causes a synchronous re-render
|
|
237
|
+
expect(events).toEqual([
|
|
238
|
+
"render: 0",
|
|
239
|
+
"effect: 0",
|
|
240
|
+
"render: 1",
|
|
241
|
+
"effect: 1",
|
|
242
|
+
]);
|
|
243
|
+
|
|
244
|
+
manager.cleanup();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
+
import { tapResources } from "../../hooks/tap-resources";
|
|
3
|
+
import { tapState } from "../../hooks/tap-state";
|
|
4
|
+
import { resource } from "../../core/resource";
|
|
5
|
+
import {
|
|
6
|
+
createTestResource,
|
|
7
|
+
renderTest,
|
|
8
|
+
cleanupAllResources,
|
|
9
|
+
createCounterResource,
|
|
10
|
+
} from "../test-utils";
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Test Resources
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
// Simple counter that just returns the value
|
|
17
|
+
const SimpleCounter = resource(createCounterResource());
|
|
18
|
+
|
|
19
|
+
// Stateful counter that tracks its own count
|
|
20
|
+
const StatefulCounter = resource((props: { initial: number }) => {
|
|
21
|
+
const [count] = tapState(props.initial);
|
|
22
|
+
return { count };
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Display component for testing type changes
|
|
26
|
+
const Display = resource((props: { text: string }) => {
|
|
27
|
+
return { type: "display", text: props.text };
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Counter with render tracking for testing instance preservation
|
|
31
|
+
const renderCounts = new Map<string, number>();
|
|
32
|
+
const instances = new Map<string, object>();
|
|
33
|
+
const TrackingCounter = resource((props: { value: number; id: string }) => {
|
|
34
|
+
const currentCount = (renderCounts.get(props.id) || 0) + 1;
|
|
35
|
+
renderCounts.set(props.id, currentCount);
|
|
36
|
+
|
|
37
|
+
if (!instances.has(props.id)) {
|
|
38
|
+
instances.set(props.id, { id: `fiber-${props.id}` });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
value: props.value,
|
|
43
|
+
id: props.id,
|
|
44
|
+
renderCount: currentCount,
|
|
45
|
+
instance: instances.get(props.id),
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Tests
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
describe("tapResources - Basic Functionality", () => {
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
cleanupAllResources();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("Basic Rendering", () => {
|
|
59
|
+
it("should render multiple resources with keys", () => {
|
|
60
|
+
const testFiber = createTestResource(() => {
|
|
61
|
+
const results = tapResources(
|
|
62
|
+
{ a: 10, b: 20, c: 30 },
|
|
63
|
+
(value) => SimpleCounter({ value }),
|
|
64
|
+
[],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return results;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const result = renderTest(testFiber, undefined);
|
|
71
|
+
expect(result).toEqual({
|
|
72
|
+
a: { count: 10 },
|
|
73
|
+
b: { count: 20 },
|
|
74
|
+
c: { count: 30 },
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should work with resource constructor syntax", () => {
|
|
79
|
+
const Counter = resource((props: { value: number }) => {
|
|
80
|
+
const [count] = tapState(props.value);
|
|
81
|
+
return { count, double: count * 2 };
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const testFiber = createTestResource(() => {
|
|
85
|
+
const items = {
|
|
86
|
+
first: { value: 5 },
|
|
87
|
+
second: { value: 10 },
|
|
88
|
+
third: { value: 15 },
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const results = tapResources(
|
|
92
|
+
items,
|
|
93
|
+
(item) => Counter({ value: item.value }),
|
|
94
|
+
[],
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return results;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const result = renderTest(testFiber, undefined);
|
|
101
|
+
expect(result).toEqual({
|
|
102
|
+
first: { count: 5, double: 10 },
|
|
103
|
+
second: { count: 10, double: 20 },
|
|
104
|
+
third: { count: 15, double: 30 },
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("Instance Preservation", () => {
|
|
110
|
+
it("should maintain resource instances when keys remain the same", () => {
|
|
111
|
+
const testFiber = createTestResource(
|
|
112
|
+
(props: { items: Record<string, { value: number; id: string }> }) => {
|
|
113
|
+
return tapResources(
|
|
114
|
+
props.items,
|
|
115
|
+
(item) => TrackingCounter({ value: item.value, id: item.id }),
|
|
116
|
+
|
|
117
|
+
[],
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Initial render
|
|
123
|
+
const result1 = renderTest(testFiber, {
|
|
124
|
+
items: { a: { value: 1, id: "a" }, b: { value: 2, id: "b" } },
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Verify initial state
|
|
128
|
+
expect(result1.a).toMatchObject({
|
|
129
|
+
id: "a",
|
|
130
|
+
value: 1,
|
|
131
|
+
renderCount: 1,
|
|
132
|
+
});
|
|
133
|
+
expect(result1.b).toMatchObject({
|
|
134
|
+
id: "b",
|
|
135
|
+
value: 2,
|
|
136
|
+
renderCount: 1,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Re-render with same keys but different values
|
|
140
|
+
const result2 = renderTest(testFiber, {
|
|
141
|
+
items: { b: { value: 20, id: "b" }, a: { value: 10, id: "a" } },
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Verify instances are preserved
|
|
145
|
+
expect(result2.b).toMatchObject({
|
|
146
|
+
id: "b",
|
|
147
|
+
value: 20,
|
|
148
|
+
renderCount: 2,
|
|
149
|
+
});
|
|
150
|
+
expect(result2.a).toMatchObject({
|
|
151
|
+
id: "a",
|
|
152
|
+
value: 10,
|
|
153
|
+
renderCount: 2,
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("Dynamic List Management", () => {
|
|
159
|
+
it("should handle adding and removing resources", () => {
|
|
160
|
+
const testFiber = createTestResource(
|
|
161
|
+
(props: { items: Record<string, number> }) => {
|
|
162
|
+
const results = tapResources(
|
|
163
|
+
props.items,
|
|
164
|
+
(value) => SimpleCounter({ value }),
|
|
165
|
+
|
|
166
|
+
[],
|
|
167
|
+
);
|
|
168
|
+
return results;
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Initial render with 3 items
|
|
173
|
+
const result1 = renderTest(testFiber, { items: { a: 0, b: 10, c: 20 } });
|
|
174
|
+
expect(result1).toEqual({
|
|
175
|
+
a: { count: 0 },
|
|
176
|
+
b: { count: 10 },
|
|
177
|
+
c: { count: 20 },
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Remove middle item
|
|
181
|
+
const result2 = renderTest(testFiber, { items: { a: 0, c: 10 } });
|
|
182
|
+
expect(result2).toEqual({
|
|
183
|
+
a: { count: 0 },
|
|
184
|
+
c: { count: 10 },
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Add new item
|
|
188
|
+
const result3 = renderTest(testFiber, { items: { a: 0, c: 10, d: 20 } });
|
|
189
|
+
expect(result3).toEqual({
|
|
190
|
+
a: { count: 0 },
|
|
191
|
+
c: { count: 10 },
|
|
192
|
+
d: { count: 20 },
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("should handle changing resource types for the same key", () => {
|
|
197
|
+
const testFiber = createTestResource((props: { useCounter: boolean }) => {
|
|
198
|
+
const results = tapResources(
|
|
199
|
+
{ item: props.useCounter },
|
|
200
|
+
(useCounter) =>
|
|
201
|
+
useCounter
|
|
202
|
+
? StatefulCounter({ initial: 42 })
|
|
203
|
+
: Display({ text: "Hello" }),
|
|
204
|
+
[],
|
|
205
|
+
);
|
|
206
|
+
return results;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Start with Counter
|
|
210
|
+
const result1 = renderTest(testFiber, { useCounter: true });
|
|
211
|
+
expect(result1).toEqual({ item: { count: 42 } });
|
|
212
|
+
|
|
213
|
+
// Switch to Display
|
|
214
|
+
const result2 = renderTest(testFiber, { useCounter: false });
|
|
215
|
+
expect(result2).toEqual({ item: { type: "display", text: "Hello" } });
|
|
216
|
+
|
|
217
|
+
// Switch back to Counter (new instance)
|
|
218
|
+
const result3 = renderTest(testFiber, { useCounter: true });
|
|
219
|
+
expect(result3).toEqual({ item: { count: 42 } });
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|