@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,240 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
+
import { tapState } from "../../hooks/tap-state";
|
|
3
|
+
import { tapEffect } from "../../hooks/tap-effect";
|
|
4
|
+
import {
|
|
5
|
+
createTestResource,
|
|
6
|
+
renderTest,
|
|
7
|
+
cleanupAllResources,
|
|
8
|
+
waitForNextTick,
|
|
9
|
+
getCommittedState,
|
|
10
|
+
} from "../test-utils";
|
|
11
|
+
|
|
12
|
+
describe("tapState - Basic Functionality", () => {
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
cleanupAllResources();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("Initialization", () => {
|
|
18
|
+
it("should initialize with direct value", () => {
|
|
19
|
+
const testFiber = createTestResource(() => {
|
|
20
|
+
const [count] = tapState(42);
|
|
21
|
+
return count;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const result = renderTest(testFiber, undefined);
|
|
25
|
+
expect(result).toBe(42);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should initialize with lazy value function", () => {
|
|
29
|
+
let initCalled = 0;
|
|
30
|
+
|
|
31
|
+
const testFiber = createTestResource(() => {
|
|
32
|
+
const [count] = tapState(() => {
|
|
33
|
+
initCalled++;
|
|
34
|
+
return 100;
|
|
35
|
+
});
|
|
36
|
+
return count;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// First render
|
|
40
|
+
const result = renderTest(testFiber, undefined);
|
|
41
|
+
expect(result).toBe(100);
|
|
42
|
+
expect(initCalled).toBe(1);
|
|
43
|
+
|
|
44
|
+
// Re-render should not call initializer again
|
|
45
|
+
renderTest(testFiber, undefined);
|
|
46
|
+
expect(initCalled).toBe(1);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should handle undefined initial state", () => {
|
|
50
|
+
const testFiber = createTestResource(() => {
|
|
51
|
+
const [value] = tapState<string>();
|
|
52
|
+
return value;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const result = renderTest(testFiber, undefined);
|
|
56
|
+
expect(result).toBeUndefined();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("State Updates", () => {
|
|
61
|
+
it("should update state and trigger re-render", async () => {
|
|
62
|
+
let renderCount = 0;
|
|
63
|
+
let setCountFn: ((value: number) => void) | null = null;
|
|
64
|
+
|
|
65
|
+
const testFiber = createTestResource(() => {
|
|
66
|
+
renderCount++;
|
|
67
|
+
const [count, setCount] = tapState(0);
|
|
68
|
+
|
|
69
|
+
// Capture setter on first render
|
|
70
|
+
if (!setCountFn) {
|
|
71
|
+
setCountFn = setCount;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { count, renderCount };
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Initial render
|
|
78
|
+
const result1 = renderTest(testFiber, undefined);
|
|
79
|
+
expect(result1).toEqual({ count: 0, renderCount: 1 });
|
|
80
|
+
|
|
81
|
+
// Update state
|
|
82
|
+
setCountFn!(10);
|
|
83
|
+
|
|
84
|
+
// Wait for re-render
|
|
85
|
+
await waitForNextTick();
|
|
86
|
+
|
|
87
|
+
// Check that state was updated
|
|
88
|
+
expect(getCommittedState(testFiber)).toEqual({
|
|
89
|
+
count: 10,
|
|
90
|
+
renderCount: 2,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should not re-render for same value (Object.is comparison)", async () => {
|
|
95
|
+
let renderCount = 0;
|
|
96
|
+
let setCountFn: ((value: number) => void) | null = null;
|
|
97
|
+
|
|
98
|
+
const testFiber = createTestResource(() => {
|
|
99
|
+
renderCount++;
|
|
100
|
+
const [count, setCount] = tapState(42);
|
|
101
|
+
|
|
102
|
+
tapEffect(() => {
|
|
103
|
+
setCountFn = setCount;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return { count, renderCount };
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Initial render
|
|
110
|
+
renderTest(testFiber, undefined);
|
|
111
|
+
expect(renderCount).toBe(1);
|
|
112
|
+
|
|
113
|
+
// Set same value
|
|
114
|
+
setCountFn!(42);
|
|
115
|
+
|
|
116
|
+
// Wait to ensure no re-render happens
|
|
117
|
+
await waitForNextTick();
|
|
118
|
+
|
|
119
|
+
// Should not trigger re-render
|
|
120
|
+
expect(renderCount).toBe(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should handle functional updates", async () => {
|
|
124
|
+
let setCountFn: ((updater: (prev: number) => number) => void) | null =
|
|
125
|
+
null;
|
|
126
|
+
|
|
127
|
+
const testFiber = createTestResource(() => {
|
|
128
|
+
const [count, setCount] = tapState(10);
|
|
129
|
+
|
|
130
|
+
tapEffect(() => {
|
|
131
|
+
setCountFn = setCount;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return count;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Initial render
|
|
138
|
+
renderTest(testFiber, undefined);
|
|
139
|
+
expect(getCommittedState(testFiber)).toBe(10);
|
|
140
|
+
|
|
141
|
+
// Functional update
|
|
142
|
+
setCountFn!((prev) => prev * 2);
|
|
143
|
+
|
|
144
|
+
await waitForNextTick();
|
|
145
|
+
expect(getCommittedState(testFiber)).toBe(20);
|
|
146
|
+
|
|
147
|
+
// Another functional update
|
|
148
|
+
setCountFn!((prev) => prev + 5);
|
|
149
|
+
|
|
150
|
+
await waitForNextTick();
|
|
151
|
+
expect(getCommittedState(testFiber)).toBe(25);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("Multiple States", () => {
|
|
156
|
+
it("should handle multiple state hooks independently", () => {
|
|
157
|
+
const testFiber = createTestResource(() => {
|
|
158
|
+
const [count1, setCount1] = tapState(1);
|
|
159
|
+
const [count2, setCount2] = tapState(2);
|
|
160
|
+
const [text, setText] = tapState("hello");
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
count1,
|
|
164
|
+
count2,
|
|
165
|
+
text,
|
|
166
|
+
setters: { setCount1, setCount2, setText },
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const result = renderTest(testFiber, undefined);
|
|
171
|
+
expect(result).toMatchObject({
|
|
172
|
+
count1: 1,
|
|
173
|
+
count2: 2,
|
|
174
|
+
text: "hello",
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should update multiple states independently", async () => {
|
|
179
|
+
let setters: any = null;
|
|
180
|
+
|
|
181
|
+
const testFiber = createTestResource(() => {
|
|
182
|
+
const [a, setA] = tapState("a");
|
|
183
|
+
const [b, setB] = tapState("b");
|
|
184
|
+
const [c, setC] = tapState("c");
|
|
185
|
+
|
|
186
|
+
tapEffect(() => {
|
|
187
|
+
setters = { setA, setB, setC };
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return { a, b, c };
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Initial render
|
|
194
|
+
renderTest(testFiber, undefined);
|
|
195
|
+
expect(getCommittedState(testFiber)).toEqual({ a: "a", b: "b", c: "c" });
|
|
196
|
+
|
|
197
|
+
// Update only B
|
|
198
|
+
setters.setB("B");
|
|
199
|
+
await waitForNextTick();
|
|
200
|
+
expect(getCommittedState(testFiber)).toEqual({ a: "a", b: "B", c: "c" });
|
|
201
|
+
|
|
202
|
+
// Update A and C
|
|
203
|
+
setters.setA("A");
|
|
204
|
+
setters.setC("C");
|
|
205
|
+
await waitForNextTick();
|
|
206
|
+
expect(getCommittedState(testFiber)).toEqual({ a: "A", b: "B", c: "C" });
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("State Persistence", () => {
|
|
211
|
+
it("should persist state across prop changes", async () => {
|
|
212
|
+
let setCountFn: ((value: number) => void) | null = null;
|
|
213
|
+
|
|
214
|
+
const testFiber = createTestResource((props: { multiplier: number }) => {
|
|
215
|
+
const [count, setCount] = tapState(10);
|
|
216
|
+
|
|
217
|
+
tapEffect(() => {
|
|
218
|
+
setCountFn = setCount;
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
count,
|
|
223
|
+
multiplied: count * props.multiplier,
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Initial render
|
|
228
|
+
const result1 = renderTest(testFiber, { multiplier: 2 });
|
|
229
|
+
expect(result1).toEqual({ count: 10, multiplied: 20 });
|
|
230
|
+
|
|
231
|
+
// Update state
|
|
232
|
+
setCountFn!(15);
|
|
233
|
+
await waitForNextTick();
|
|
234
|
+
|
|
235
|
+
// Re-render with different props
|
|
236
|
+
const result2 = renderTest(testFiber, { multiplier: 3 });
|
|
237
|
+
expect(result2).toEqual({ count: 15, multiplied: 45 });
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { tapEffect } from "../../hooks/tap-effect";
|
|
3
|
+
import { tapState } from "../../hooks/tap-state";
|
|
4
|
+
import { createTestResource, renderTest, unmountResource } from "../test-utils";
|
|
5
|
+
import {
|
|
6
|
+
renderResourceFiber,
|
|
7
|
+
commitResourceFiber,
|
|
8
|
+
} from "../../core/ResourceFiber";
|
|
9
|
+
|
|
10
|
+
describe("Errors - Effect Errors", () => {
|
|
11
|
+
it("should propagate errors from effects", () => {
|
|
12
|
+
const error = new Error("Effect error");
|
|
13
|
+
|
|
14
|
+
const resource = createTestResource(() => {
|
|
15
|
+
tapEffect(() => {
|
|
16
|
+
throw error;
|
|
17
|
+
});
|
|
18
|
+
return null;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
expect(() => renderTest(resource, undefined)).toThrow(error);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should propagate errors from cleanup functions", () => {
|
|
25
|
+
const error = new Error("Cleanup error");
|
|
26
|
+
let dep = 0;
|
|
27
|
+
|
|
28
|
+
const resource = createTestResource(() => {
|
|
29
|
+
tapEffect(() => {
|
|
30
|
+
return () => {
|
|
31
|
+
if (dep > 0) {
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}, [dep]); // Cleanup will run when dep changes
|
|
36
|
+
|
|
37
|
+
return dep;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// First render and commit - establishes the effect
|
|
41
|
+
const ctx1 = renderResourceFiber(resource, undefined);
|
|
42
|
+
commitResourceFiber(resource, ctx1);
|
|
43
|
+
|
|
44
|
+
// Change dep to trigger cleanup on next render
|
|
45
|
+
dep = 1;
|
|
46
|
+
|
|
47
|
+
// Second render with different dep should trigger cleanup that throws
|
|
48
|
+
const ctx2 = renderResourceFiber(resource, undefined);
|
|
49
|
+
expect(() => commitResourceFiber(resource, ctx2)).toThrow(error);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should throw on invalid effect return value", () => {
|
|
53
|
+
const resource = createTestResource(() => {
|
|
54
|
+
tapEffect(() => {
|
|
55
|
+
return "not a function" as any; // Invalid return
|
|
56
|
+
});
|
|
57
|
+
return null;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(() => renderTest(resource, undefined)).toThrow(
|
|
61
|
+
"An effect function must either return a cleanup function or nothing",
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should handle multiple effect errors", () => {
|
|
66
|
+
const error1 = new Error("First error");
|
|
67
|
+
const error2 = new Error("Second error");
|
|
68
|
+
const goodEffect = vi.fn();
|
|
69
|
+
|
|
70
|
+
const resource = createTestResource(() => {
|
|
71
|
+
tapEffect(() => {
|
|
72
|
+
throw error1;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
tapEffect(goodEffect); // This won't run
|
|
76
|
+
|
|
77
|
+
tapEffect(() => {
|
|
78
|
+
throw error2;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return null;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Should throw first error
|
|
85
|
+
expect(() => renderTest(resource, undefined)).toThrow(error1);
|
|
86
|
+
expect(goodEffect).not.toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should continue cleanup on unmount despite errors", () => {
|
|
90
|
+
const cleanupError = new Error("Cleanup failed");
|
|
91
|
+
const cleanup1 = vi.fn(() => {
|
|
92
|
+
throw cleanupError;
|
|
93
|
+
});
|
|
94
|
+
const cleanup2 = vi.fn();
|
|
95
|
+
const cleanup3 = vi.fn();
|
|
96
|
+
|
|
97
|
+
const resource = createTestResource(() => {
|
|
98
|
+
tapEffect(() => cleanup1);
|
|
99
|
+
tapEffect(() => cleanup2);
|
|
100
|
+
tapEffect(() => cleanup3);
|
|
101
|
+
return null;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
renderTest(resource, undefined);
|
|
105
|
+
|
|
106
|
+
// Unmount should throw the error but should still run all cleanups
|
|
107
|
+
expect(() => unmountResource(resource)).toThrow(cleanupError);
|
|
108
|
+
expect(cleanup1).toHaveBeenCalled();
|
|
109
|
+
expect(cleanup2).toHaveBeenCalled();
|
|
110
|
+
expect(cleanup3).toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should handle errors in effect with dependencies", () => {
|
|
114
|
+
const error = new Error("Dep effect error");
|
|
115
|
+
let shouldThrow = false;
|
|
116
|
+
|
|
117
|
+
const resource = createTestResource(() => {
|
|
118
|
+
const [dep, setDep] = tapState(0);
|
|
119
|
+
|
|
120
|
+
tapEffect(() => {
|
|
121
|
+
if (shouldThrow) {
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}, [dep]);
|
|
125
|
+
|
|
126
|
+
// Use effect to trigger state change
|
|
127
|
+
tapEffect(() => {
|
|
128
|
+
if (dep === 0) {
|
|
129
|
+
shouldThrow = true;
|
|
130
|
+
setDep(1); // Trigger effect re-run
|
|
131
|
+
}
|
|
132
|
+
}, [dep]);
|
|
133
|
+
|
|
134
|
+
return dep;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// The initial render will trigger setState which causes flushSync
|
|
138
|
+
// The flushed re-render will throw the error
|
|
139
|
+
expect(() => renderTest(resource, undefined)).toThrow(error);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should handle async errors in effects", async () => {
|
|
143
|
+
// Set up a promise to catch the async error
|
|
144
|
+
let asyncErrorPromise: Promise<void>;
|
|
145
|
+
|
|
146
|
+
const resource = createTestResource(() => {
|
|
147
|
+
tapEffect(() => {
|
|
148
|
+
// Async errors are not caught by the framework
|
|
149
|
+
asyncErrorPromise = new Promise((_, reject) => {
|
|
150
|
+
setTimeout(() => {
|
|
151
|
+
reject(new Error("Async error"));
|
|
152
|
+
}, 0);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Catch the error to prevent unhandled rejection
|
|
156
|
+
asyncErrorPromise.catch(() => {
|
|
157
|
+
// Expected - async errors are not caught by the framework
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
return null;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// This won't throw synchronously
|
|
164
|
+
expect(() => renderTest(resource, undefined)).not.toThrow();
|
|
165
|
+
|
|
166
|
+
// Wait for the async error to be handled
|
|
167
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("should properly clean up state after effect error", () => {
|
|
171
|
+
const error = new Error("Effect error");
|
|
172
|
+
let effectRan = false;
|
|
173
|
+
|
|
174
|
+
const resource = createTestResource(() => {
|
|
175
|
+
const [value] = tapState("initial");
|
|
176
|
+
|
|
177
|
+
tapEffect(() => {
|
|
178
|
+
effectRan = true;
|
|
179
|
+
throw error;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return value;
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(() => renderTest(resource, undefined)).toThrow(error);
|
|
186
|
+
expect(effectRan).toBe(true);
|
|
187
|
+
|
|
188
|
+
// Resource should not have committed state since commit failed
|
|
189
|
+
// Since commit failed, we can't check the state through normal means
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should handle errors in effect cleanup during re-render", () => {
|
|
193
|
+
const cleanupError = new Error("Cleanup during re-render");
|
|
194
|
+
let throwOnCleanup = false;
|
|
195
|
+
|
|
196
|
+
const resource = createTestResource(() => {
|
|
197
|
+
const [count, setCount] = tapState(0);
|
|
198
|
+
|
|
199
|
+
tapEffect(() => {
|
|
200
|
+
return () => {
|
|
201
|
+
if (throwOnCleanup) {
|
|
202
|
+
throw cleanupError;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}, [count]);
|
|
206
|
+
|
|
207
|
+
// Use effect to trigger state change
|
|
208
|
+
tapEffect(() => {
|
|
209
|
+
if (count === 0) {
|
|
210
|
+
throwOnCleanup = true;
|
|
211
|
+
setCount(1);
|
|
212
|
+
}
|
|
213
|
+
}, [count]);
|
|
214
|
+
|
|
215
|
+
return count;
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// The initial render will trigger setState which causes flushSync
|
|
219
|
+
// During the flush, the cleanup will run and throw
|
|
220
|
+
expect(() => renderTest(resource, undefined)).toThrow(cleanupError);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { tapEffect } from "../../hooks/tap-effect";
|
|
3
|
+
import { tapState } from "../../hooks/tap-state";
|
|
4
|
+
import { createTestResource, renderTest } from "../test-utils";
|
|
5
|
+
import {
|
|
6
|
+
renderResourceFiber,
|
|
7
|
+
commitResourceFiber,
|
|
8
|
+
unmountResourceFiber,
|
|
9
|
+
} from "../../core/ResourceFiber";
|
|
10
|
+
|
|
11
|
+
describe("Errors - Render Errors", () => {
|
|
12
|
+
it("should propagate errors during render", () => {
|
|
13
|
+
const error = new Error("Render error");
|
|
14
|
+
|
|
15
|
+
const resource = createTestResource(() => {
|
|
16
|
+
throw error;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(() => renderResourceFiber(resource, undefined)).toThrow(error);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should throw when hooks are called outside render context", () => {
|
|
23
|
+
// Try to call hook outside of resource render
|
|
24
|
+
expect(() => {
|
|
25
|
+
tapState(0);
|
|
26
|
+
}).toThrow("No resource fiber available");
|
|
27
|
+
|
|
28
|
+
expect(() => {
|
|
29
|
+
tapEffect(() => {});
|
|
30
|
+
}).toThrow("No resource fiber available");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should handle errors in state initializers", () => {
|
|
34
|
+
const error = new Error("Initializer error");
|
|
35
|
+
|
|
36
|
+
const resource = createTestResource(() => {
|
|
37
|
+
const [value] = tapState(() => {
|
|
38
|
+
throw error;
|
|
39
|
+
});
|
|
40
|
+
return value;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(() => renderResourceFiber(resource, undefined)).toThrow(error);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should detect render during render", () => {
|
|
47
|
+
const resource = createTestResource(() => {
|
|
48
|
+
const [count, setCount] = tapState(0);
|
|
49
|
+
|
|
50
|
+
// This violates the rules - no state updates during render
|
|
51
|
+
if (count < 5) {
|
|
52
|
+
expect(() => setCount(count + 1)).toThrow(
|
|
53
|
+
"Resource updated during render",
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return count;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
renderResourceFiber(resource, undefined);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should allow setState during commit (effects)", () => {
|
|
64
|
+
const resource = createTestResource(() => {
|
|
65
|
+
const [count, setCount] = tapState(0);
|
|
66
|
+
|
|
67
|
+
tapEffect(() => {
|
|
68
|
+
// setState during effects (commit phase) is allowed
|
|
69
|
+
if (count < 5) {
|
|
70
|
+
setCount(count + 1);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return count;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const ctx = renderResourceFiber(resource, undefined);
|
|
78
|
+
// This should not throw - setState in effects is allowed
|
|
79
|
+
expect(() => commitResourceFiber(resource, ctx)).not.toThrow();
|
|
80
|
+
unmountResourceFiber(resource);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should handle errors in hook order validation", () => {
|
|
84
|
+
let useStateFirst = true;
|
|
85
|
+
|
|
86
|
+
const resource = createTestResource(() => {
|
|
87
|
+
if (useStateFirst) {
|
|
88
|
+
tapState(1);
|
|
89
|
+
tapEffect(() => {});
|
|
90
|
+
} else {
|
|
91
|
+
tapEffect(() => {});
|
|
92
|
+
tapState(1);
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
renderResourceFiber(resource, undefined);
|
|
98
|
+
|
|
99
|
+
useStateFirst = false;
|
|
100
|
+
|
|
101
|
+
expect(() => renderResourceFiber(resource, undefined)).toThrow(
|
|
102
|
+
"Hook order changed between renders",
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should maintain resource state after render error", () => {
|
|
107
|
+
let shouldThrow = false;
|
|
108
|
+
|
|
109
|
+
const resource = createTestResource(() => {
|
|
110
|
+
const [count, _setCount] = tapState(42);
|
|
111
|
+
|
|
112
|
+
if (shouldThrow) {
|
|
113
|
+
throw new Error("Render failed");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return count;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// First successful render
|
|
120
|
+
const result = renderTest(resource, undefined);
|
|
121
|
+
expect(result).toBe(42);
|
|
122
|
+
|
|
123
|
+
// Failed render
|
|
124
|
+
shouldThrow = true;
|
|
125
|
+
expect(() => renderTest(resource, undefined)).toThrow("Render failed");
|
|
126
|
+
|
|
127
|
+
// State should be unchanged after failed render
|
|
128
|
+
// The resource state is preserved
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should handle complex error scenarios", () => {
|
|
132
|
+
let phase = "render";
|
|
133
|
+
|
|
134
|
+
const resource = createTestResource(() => {
|
|
135
|
+
if (phase === "hook-order") {
|
|
136
|
+
// Wrong hook order
|
|
137
|
+
tapEffect(() => {});
|
|
138
|
+
tapState(1);
|
|
139
|
+
} else {
|
|
140
|
+
tapState(1);
|
|
141
|
+
tapEffect(() => {
|
|
142
|
+
if (phase === "effect-error") {
|
|
143
|
+
throw new Error("Effect error");
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (phase === "render-error") {
|
|
149
|
+
throw new Error("Render error");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return phase;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Successful render
|
|
156
|
+
renderTest(resource, undefined);
|
|
157
|
+
|
|
158
|
+
// Render error
|
|
159
|
+
phase = "render-error";
|
|
160
|
+
expect(() => renderTest(resource, undefined)).toThrow("Render error");
|
|
161
|
+
|
|
162
|
+
// Hook order error
|
|
163
|
+
phase = "hook-order";
|
|
164
|
+
expect(() => renderTest(resource, undefined)).toThrow("Hook order changed");
|
|
165
|
+
|
|
166
|
+
// Effect error
|
|
167
|
+
phase = "effect-error";
|
|
168
|
+
expect(() => renderTest(resource, undefined)).toThrow("Effect error");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should handle errors in nested hook calls", () => {
|
|
172
|
+
const useFeature = () => {
|
|
173
|
+
// This will fail if called outside render
|
|
174
|
+
const [value] = tapState("feature");
|
|
175
|
+
return value;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Outside render context
|
|
179
|
+
expect(() => useFeature()).toThrow("No resource fiber available");
|
|
180
|
+
|
|
181
|
+
// Inside render context
|
|
182
|
+
const resource = createTestResource(() => {
|
|
183
|
+
const feature = useFeature(); // This works
|
|
184
|
+
return feature;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const result = renderTest(resource, undefined);
|
|
188
|
+
expect(result).toBe("feature");
|
|
189
|
+
});
|
|
190
|
+
});
|