rask-ui 0.27.0 → 0.28.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/dist/compiler.d.ts +4 -0
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +7 -0
- package/dist/component.d.ts.map +1 -1
- package/dist/component.js +9 -4
- package/dist/createAsync.d.ts +39 -0
- package/dist/createAsync.d.ts.map +1 -0
- package/dist/createAsync.js +47 -0
- package/dist/createComputed.d.ts +4 -0
- package/dist/createComputed.d.ts.map +1 -0
- package/dist/createComputed.js +69 -0
- package/dist/createEffect.d.ts +2 -0
- package/dist/createEffect.d.ts.map +1 -0
- package/dist/createEffect.js +29 -0
- package/dist/createMutation.d.ts +43 -0
- package/dist/createMutation.d.ts.map +1 -0
- package/dist/createMutation.js +76 -0
- package/dist/createQuery.d.ts +42 -0
- package/dist/createQuery.d.ts.map +1 -0
- package/dist/createQuery.js +80 -0
- package/dist/createRouter.d.ts +8 -0
- package/dist/createRouter.d.ts.map +1 -0
- package/dist/createRouter.js +27 -0
- package/dist/createState.d.ts +28 -0
- package/dist/createState.d.ts.map +1 -0
- package/dist/createState.js +129 -0
- package/dist/createTask.d.ts +31 -0
- package/dist/createTask.d.ts.map +1 -0
- package/dist/createTask.js +79 -0
- package/dist/createView.d.ts +28 -0
- package/dist/createView.d.ts.map +1 -0
- package/dist/createView.js +77 -0
- package/dist/error.d.ts +5 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +16 -0
- package/dist/jsx.d.ts +11 -0
- package/dist/patchInferno.d.ts +6 -0
- package/dist/patchInferno.d.ts.map +1 -0
- package/dist/patchInferno.js +53 -0
- package/dist/scheduler.d.ts +4 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +107 -0
- package/dist/tests/batch.test.d.ts +2 -0
- package/dist/tests/batch.test.d.ts.map +1 -0
- package/dist/tests/batch.test.js +244 -0
- package/dist/tests/createComputed.test.d.ts +2 -0
- package/dist/tests/createComputed.test.d.ts.map +1 -0
- package/dist/tests/createComputed.test.js +257 -0
- package/dist/tests/createContext.test.d.ts +2 -0
- package/dist/tests/createContext.test.d.ts.map +1 -0
- package/dist/tests/createContext.test.js +136 -0
- package/dist/tests/createEffect.test.d.ts +2 -0
- package/dist/tests/createEffect.test.d.ts.map +1 -0
- package/dist/tests/createEffect.test.js +467 -0
- package/dist/tests/createState.test.d.ts +2 -0
- package/dist/tests/createState.test.d.ts.map +1 -0
- package/dist/tests/createState.test.js +144 -0
- package/dist/tests/createTask.test.d.ts +2 -0
- package/dist/tests/createTask.test.d.ts.map +1 -0
- package/dist/tests/createTask.test.js +322 -0
- package/dist/tests/createView.test.d.ts +2 -0
- package/dist/tests/createView.test.d.ts.map +1 -0
- package/dist/tests/createView.test.js +203 -0
- package/dist/tests/error.test.d.ts +2 -0
- package/dist/tests/error.test.d.ts.map +1 -0
- package/dist/tests/error.test.js +168 -0
- package/dist/tests/observation.test.d.ts +2 -0
- package/dist/tests/observation.test.d.ts.map +1 -0
- package/dist/tests/observation.test.js +341 -0
- package/dist/transformer.d.ts.map +1 -1
- package/dist/transformer.js +1 -1
- package/dist/types.d.ts +6 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/useComputed.d.ts +5 -0
- package/dist/useComputed.d.ts.map +1 -0
- package/dist/useComputed.js +69 -0
- package/dist/useQuery.d.ts +25 -0
- package/dist/useQuery.d.ts.map +1 -0
- package/dist/useQuery.js +25 -0
- package/dist/useSuspendAsync.d.ts +18 -0
- package/dist/useSuspendAsync.d.ts.map +1 -0
- package/dist/useSuspendAsync.js +37 -0
- package/dist/useTask.d.ts +25 -0
- package/dist/useTask.d.ts.map +1 -0
- package/dist/useTask.js +70 -0
- package/package.json +1 -1
- package/swc-plugin/target/wasm32-wasip1/release/swc_plugin_rask_component.wasm +0 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "./jsx-runtime";
|
|
2
|
+
import { describe, it, expect, vi } from "vitest";
|
|
3
|
+
import { createEffect } from "../createEffect";
|
|
4
|
+
import { createState } from "../createState";
|
|
5
|
+
import { render } from "../index";
|
|
6
|
+
describe("createEffect", () => {
|
|
7
|
+
it("should run immediately on creation", () => {
|
|
8
|
+
const effectFn = vi.fn();
|
|
9
|
+
createEffect(effectFn);
|
|
10
|
+
expect(effectFn).toHaveBeenCalledTimes(1);
|
|
11
|
+
});
|
|
12
|
+
it("should track reactive dependencies", async () => {
|
|
13
|
+
const state = createState({ count: 0 });
|
|
14
|
+
const effectFn = vi.fn(() => {
|
|
15
|
+
state.count; // Access to track
|
|
16
|
+
});
|
|
17
|
+
createEffect(effectFn);
|
|
18
|
+
expect(effectFn).toHaveBeenCalledTimes(1);
|
|
19
|
+
state.count = 1;
|
|
20
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
21
|
+
expect(effectFn).toHaveBeenCalledTimes(2);
|
|
22
|
+
});
|
|
23
|
+
it("should re-run when dependencies change", async () => {
|
|
24
|
+
const state = createState({ count: 0 });
|
|
25
|
+
const results = [];
|
|
26
|
+
createEffect(() => {
|
|
27
|
+
results.push(state.count);
|
|
28
|
+
});
|
|
29
|
+
expect(results).toEqual([0]);
|
|
30
|
+
state.count = 1;
|
|
31
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
32
|
+
expect(results).toEqual([0, 1]);
|
|
33
|
+
state.count = 2;
|
|
34
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
35
|
+
expect(results).toEqual([0, 1, 2]);
|
|
36
|
+
});
|
|
37
|
+
it("should run on microtask, not synchronously", () => {
|
|
38
|
+
const state = createState({ count: 0 });
|
|
39
|
+
const results = [];
|
|
40
|
+
createEffect(() => {
|
|
41
|
+
results.push(state.count);
|
|
42
|
+
});
|
|
43
|
+
expect(results).toEqual([0]); // Initial run is synchronous
|
|
44
|
+
state.count = 1;
|
|
45
|
+
// Should not have run yet (microtask not flushed)
|
|
46
|
+
expect(results).toEqual([0]);
|
|
47
|
+
});
|
|
48
|
+
it("should handle multiple effects on same state", async () => {
|
|
49
|
+
const state = createState({ count: 0 });
|
|
50
|
+
const results1 = [];
|
|
51
|
+
const results2 = [];
|
|
52
|
+
createEffect(() => {
|
|
53
|
+
results1.push(state.count);
|
|
54
|
+
});
|
|
55
|
+
createEffect(() => {
|
|
56
|
+
results2.push(state.count * 2);
|
|
57
|
+
});
|
|
58
|
+
expect(results1).toEqual([0]);
|
|
59
|
+
expect(results2).toEqual([0]);
|
|
60
|
+
state.count = 5;
|
|
61
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
62
|
+
expect(results1).toEqual([0, 5]);
|
|
63
|
+
expect(results2).toEqual([0, 10]);
|
|
64
|
+
});
|
|
65
|
+
it("should only track dependencies accessed during execution", async () => {
|
|
66
|
+
const state = createState({ a: 1, b: 2 });
|
|
67
|
+
const effectFn = vi.fn(() => {
|
|
68
|
+
state.a; // Only track 'a'
|
|
69
|
+
});
|
|
70
|
+
createEffect(effectFn);
|
|
71
|
+
expect(effectFn).toHaveBeenCalledTimes(1);
|
|
72
|
+
// Change 'b' (not tracked)
|
|
73
|
+
state.b = 100;
|
|
74
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
75
|
+
expect(effectFn).toHaveBeenCalledTimes(1); // Should not re-run
|
|
76
|
+
// Change 'a' (tracked)
|
|
77
|
+
state.a = 10;
|
|
78
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
79
|
+
expect(effectFn).toHaveBeenCalledTimes(2); // Should re-run
|
|
80
|
+
});
|
|
81
|
+
it("should re-track dependencies on each run", async () => {
|
|
82
|
+
const state = createState({ useA: true, a: 1, b: 2 });
|
|
83
|
+
const effectFn = vi.fn(() => {
|
|
84
|
+
if (state.useA) {
|
|
85
|
+
state.a;
|
|
86
|
+
} else {
|
|
87
|
+
state.b;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
createEffect(effectFn);
|
|
91
|
+
expect(effectFn).toHaveBeenCalledTimes(1);
|
|
92
|
+
// Change 'a' (currently tracked)
|
|
93
|
+
state.a = 10;
|
|
94
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
95
|
+
expect(effectFn).toHaveBeenCalledTimes(2);
|
|
96
|
+
// Change 'b' (not tracked)
|
|
97
|
+
state.b = 20;
|
|
98
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
99
|
+
expect(effectFn).toHaveBeenCalledTimes(2); // No change
|
|
100
|
+
// Switch to using 'b'
|
|
101
|
+
state.useA = false;
|
|
102
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
103
|
+
expect(effectFn).toHaveBeenCalledTimes(3);
|
|
104
|
+
// Change 'a' (no longer tracked)
|
|
105
|
+
state.a = 100;
|
|
106
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
107
|
+
expect(effectFn).toHaveBeenCalledTimes(3); // No change
|
|
108
|
+
// Change 'b' (now tracked)
|
|
109
|
+
state.b = 200;
|
|
110
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
111
|
+
expect(effectFn).toHaveBeenCalledTimes(4);
|
|
112
|
+
});
|
|
113
|
+
it("should handle effects that modify state", async () => {
|
|
114
|
+
const state = createState({ input: 1, output: 0 });
|
|
115
|
+
createEffect(() => {
|
|
116
|
+
state.output = state.input * 2;
|
|
117
|
+
});
|
|
118
|
+
expect(state.output).toBe(2);
|
|
119
|
+
state.input = 5;
|
|
120
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
121
|
+
expect(state.output).toBe(10);
|
|
122
|
+
});
|
|
123
|
+
it("should handle nested state access", async () => {
|
|
124
|
+
const state = createState({
|
|
125
|
+
user: {
|
|
126
|
+
profile: {
|
|
127
|
+
name: "Alice",
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
const results = [];
|
|
132
|
+
createEffect(() => {
|
|
133
|
+
results.push(state.user.profile.name);
|
|
134
|
+
});
|
|
135
|
+
expect(results).toEqual(["Alice"]);
|
|
136
|
+
state.user.profile.name = "Bob";
|
|
137
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
138
|
+
expect(results).toEqual(["Alice", "Bob"]);
|
|
139
|
+
});
|
|
140
|
+
it("should handle array access", async () => {
|
|
141
|
+
const state = createState({ items: [1, 2, 3] });
|
|
142
|
+
const results = [];
|
|
143
|
+
createEffect(() => {
|
|
144
|
+
results.push(state.items.length);
|
|
145
|
+
});
|
|
146
|
+
expect(results).toEqual([3]);
|
|
147
|
+
state.items.push(4);
|
|
148
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
149
|
+
expect(results).toEqual([3, 4]);
|
|
150
|
+
state.items.pop();
|
|
151
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
152
|
+
expect(results).toEqual([3, 4, 3]);
|
|
153
|
+
});
|
|
154
|
+
it("should handle effects accessing array elements", async () => {
|
|
155
|
+
const state = createState({ items: [1, 2, 3] });
|
|
156
|
+
const results = [];
|
|
157
|
+
createEffect(() => {
|
|
158
|
+
const sum = state.items.reduce((acc, val) => acc + val, 0);
|
|
159
|
+
results.push(sum);
|
|
160
|
+
});
|
|
161
|
+
expect(results).toEqual([6]);
|
|
162
|
+
state.items.push(4);
|
|
163
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
164
|
+
expect(results).toEqual([6, 10]);
|
|
165
|
+
state.items[0] = 10;
|
|
166
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
167
|
+
expect(results).toEqual([6, 10, 19]);
|
|
168
|
+
});
|
|
169
|
+
it("should batch multiple state changes before re-running", async () => {
|
|
170
|
+
const state = createState({ a: 1, b: 2 });
|
|
171
|
+
const effectFn = vi.fn(() => {
|
|
172
|
+
state.a;
|
|
173
|
+
state.b;
|
|
174
|
+
});
|
|
175
|
+
createEffect(effectFn);
|
|
176
|
+
expect(effectFn).toHaveBeenCalledTimes(1);
|
|
177
|
+
// Multiple changes in same turn
|
|
178
|
+
state.a = 10;
|
|
179
|
+
state.b = 20;
|
|
180
|
+
state.a = 15;
|
|
181
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
182
|
+
// Should only run once for all changes
|
|
183
|
+
expect(effectFn).toHaveBeenCalledTimes(2);
|
|
184
|
+
});
|
|
185
|
+
it("should handle effects with no dependencies", async () => {
|
|
186
|
+
let runCount = 0;
|
|
187
|
+
createEffect(() => {
|
|
188
|
+
runCount++;
|
|
189
|
+
// No reactive state accessed
|
|
190
|
+
});
|
|
191
|
+
expect(runCount).toBe(1);
|
|
192
|
+
// Wait to ensure it doesn't run again
|
|
193
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
194
|
+
expect(runCount).toBe(1);
|
|
195
|
+
});
|
|
196
|
+
it("should handle effects that conditionally access state", async () => {
|
|
197
|
+
const state = createState({ enabled: true, value: 5 });
|
|
198
|
+
const effectFn = vi.fn(() => {
|
|
199
|
+
if (state.enabled) {
|
|
200
|
+
state.value;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
createEffect(effectFn);
|
|
204
|
+
expect(effectFn).toHaveBeenCalledTimes(1);
|
|
205
|
+
// Change value (tracked because enabled is true)
|
|
206
|
+
state.value = 10;
|
|
207
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
208
|
+
expect(effectFn).toHaveBeenCalledTimes(2);
|
|
209
|
+
// Disable
|
|
210
|
+
state.enabled = false;
|
|
211
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
212
|
+
expect(effectFn).toHaveBeenCalledTimes(3);
|
|
213
|
+
// Change value (not tracked because enabled is false)
|
|
214
|
+
state.value = 20;
|
|
215
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
216
|
+
expect(effectFn).toHaveBeenCalledTimes(3); // No change
|
|
217
|
+
});
|
|
218
|
+
it("should handle complex dependency graphs", async () => {
|
|
219
|
+
const state = createState({
|
|
220
|
+
multiplier: 2,
|
|
221
|
+
values: [1, 2, 3],
|
|
222
|
+
});
|
|
223
|
+
const results = [];
|
|
224
|
+
createEffect(() => {
|
|
225
|
+
const sum = state.values.reduce((acc, val) => acc + val, 0);
|
|
226
|
+
results.push(sum * state.multiplier);
|
|
227
|
+
});
|
|
228
|
+
expect(results).toEqual([12]); // (1+2+3) * 2
|
|
229
|
+
state.multiplier = 3;
|
|
230
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
231
|
+
expect(results).toEqual([12, 18]); // (1+2+3) * 3
|
|
232
|
+
state.values.push(4);
|
|
233
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
234
|
+
expect(results).toEqual([12, 18, 30]); // (1+2+3+4) * 3
|
|
235
|
+
});
|
|
236
|
+
it("should handle effects that run other synchronous code", async () => {
|
|
237
|
+
const state = createState({ count: 0 });
|
|
238
|
+
const sideEffects = [];
|
|
239
|
+
createEffect(() => {
|
|
240
|
+
sideEffects.push("effect-start");
|
|
241
|
+
const value = state.count;
|
|
242
|
+
sideEffects.push(`value-${value}`);
|
|
243
|
+
sideEffects.push("effect-end");
|
|
244
|
+
});
|
|
245
|
+
expect(sideEffects).toEqual(["effect-start", "value-0", "effect-end"]);
|
|
246
|
+
state.count = 1;
|
|
247
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
248
|
+
expect(sideEffects).toEqual([
|
|
249
|
+
"effect-start",
|
|
250
|
+
"value-0",
|
|
251
|
+
"effect-end",
|
|
252
|
+
"effect-start",
|
|
253
|
+
"value-1",
|
|
254
|
+
"effect-end",
|
|
255
|
+
]);
|
|
256
|
+
});
|
|
257
|
+
it("should handle rapid state changes", async () => {
|
|
258
|
+
const state = createState({ count: 0 });
|
|
259
|
+
const effectFn = vi.fn(() => {
|
|
260
|
+
state.count;
|
|
261
|
+
});
|
|
262
|
+
createEffect(effectFn);
|
|
263
|
+
expect(effectFn).toHaveBeenCalledTimes(1);
|
|
264
|
+
// Rapid changes
|
|
265
|
+
for (let i = 1; i <= 10; i++) {
|
|
266
|
+
state.count = i;
|
|
267
|
+
}
|
|
268
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
269
|
+
// Should batch all changes into one effect run
|
|
270
|
+
expect(effectFn).toHaveBeenCalledTimes(2);
|
|
271
|
+
});
|
|
272
|
+
it("should access latest state values when effect runs", async () => {
|
|
273
|
+
const state = createState({ count: 0 });
|
|
274
|
+
const results = [];
|
|
275
|
+
createEffect(() => {
|
|
276
|
+
results.push(state.count);
|
|
277
|
+
});
|
|
278
|
+
state.count = 1;
|
|
279
|
+
state.count = 2;
|
|
280
|
+
state.count = 3;
|
|
281
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
282
|
+
// Should see latest value when effect runs
|
|
283
|
+
expect(results).toEqual([0, 3]);
|
|
284
|
+
});
|
|
285
|
+
it("should call dispose function before re-executing", async () => {
|
|
286
|
+
const state = createState({ count: 0 });
|
|
287
|
+
const disposeCalls = [];
|
|
288
|
+
const effectCalls = [];
|
|
289
|
+
createEffect(() => {
|
|
290
|
+
effectCalls.push(state.count);
|
|
291
|
+
return () => {
|
|
292
|
+
// Dispose sees the current state at the time it's called
|
|
293
|
+
disposeCalls.push(state.count);
|
|
294
|
+
};
|
|
295
|
+
});
|
|
296
|
+
expect(effectCalls).toEqual([0]);
|
|
297
|
+
expect(disposeCalls).toEqual([]);
|
|
298
|
+
state.count = 1;
|
|
299
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
300
|
+
// Dispose is called after state change, before effect re-runs
|
|
301
|
+
expect(disposeCalls).toEqual([1]);
|
|
302
|
+
expect(effectCalls).toEqual([0, 1]);
|
|
303
|
+
state.count = 2;
|
|
304
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
305
|
+
expect(disposeCalls).toEqual([1, 2]);
|
|
306
|
+
expect(effectCalls).toEqual([0, 1, 2]);
|
|
307
|
+
});
|
|
308
|
+
it("should handle dispose function with cleanup logic", async () => {
|
|
309
|
+
const state = createState({ url: "/api/data" });
|
|
310
|
+
const subscriptions = [];
|
|
311
|
+
createEffect(() => {
|
|
312
|
+
const currentUrl = state.url;
|
|
313
|
+
subscriptions.push(`subscribe:${currentUrl}`);
|
|
314
|
+
return () => {
|
|
315
|
+
subscriptions.push(`unsubscribe:${currentUrl}`);
|
|
316
|
+
};
|
|
317
|
+
});
|
|
318
|
+
expect(subscriptions).toEqual(["subscribe:/api/data"]);
|
|
319
|
+
state.url = "/api/users";
|
|
320
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
321
|
+
expect(subscriptions).toEqual([
|
|
322
|
+
"subscribe:/api/data",
|
|
323
|
+
"unsubscribe:/api/data",
|
|
324
|
+
"subscribe:/api/users",
|
|
325
|
+
]);
|
|
326
|
+
state.url = "/api/posts";
|
|
327
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
328
|
+
expect(subscriptions).toEqual([
|
|
329
|
+
"subscribe:/api/data",
|
|
330
|
+
"unsubscribe:/api/data",
|
|
331
|
+
"subscribe:/api/users",
|
|
332
|
+
"unsubscribe:/api/users",
|
|
333
|
+
"subscribe:/api/posts",
|
|
334
|
+
]);
|
|
335
|
+
});
|
|
336
|
+
it("should handle effects without dispose function", async () => {
|
|
337
|
+
const state = createState({ count: 0 });
|
|
338
|
+
const effectCalls = [];
|
|
339
|
+
createEffect(() => {
|
|
340
|
+
effectCalls.push(state.count);
|
|
341
|
+
// No dispose function returned
|
|
342
|
+
});
|
|
343
|
+
expect(effectCalls).toEqual([0]);
|
|
344
|
+
state.count = 1;
|
|
345
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
346
|
+
expect(effectCalls).toEqual([0, 1]);
|
|
347
|
+
state.count = 2;
|
|
348
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
349
|
+
expect(effectCalls).toEqual([0, 1, 2]);
|
|
350
|
+
});
|
|
351
|
+
it("should handle dispose function that throws error", async () => {
|
|
352
|
+
const state = createState({ count: 0 });
|
|
353
|
+
const effectCalls = [];
|
|
354
|
+
const consoleErrorSpy = vi
|
|
355
|
+
.spyOn(console, "error")
|
|
356
|
+
.mockImplementation(() => {});
|
|
357
|
+
createEffect(() => {
|
|
358
|
+
effectCalls.push(state.count);
|
|
359
|
+
return () => {
|
|
360
|
+
throw new Error("Dispose error");
|
|
361
|
+
};
|
|
362
|
+
});
|
|
363
|
+
expect(effectCalls).toEqual([0]);
|
|
364
|
+
state.count = 1;
|
|
365
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
366
|
+
// Effect should still have run despite dispose throwing
|
|
367
|
+
expect(effectCalls).toEqual([0, 1]);
|
|
368
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
369
|
+
"Error in effect dispose function:",
|
|
370
|
+
expect.any(Error)
|
|
371
|
+
);
|
|
372
|
+
consoleErrorSpy.mockRestore();
|
|
373
|
+
});
|
|
374
|
+
it("should call dispose with latest closure values", async () => {
|
|
375
|
+
const state = createState({ count: 0 });
|
|
376
|
+
const disposeValues = [];
|
|
377
|
+
createEffect(() => {
|
|
378
|
+
const capturedCount = state.count;
|
|
379
|
+
return () => {
|
|
380
|
+
disposeValues.push(capturedCount);
|
|
381
|
+
};
|
|
382
|
+
});
|
|
383
|
+
state.count = 1;
|
|
384
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
385
|
+
expect(disposeValues).toEqual([0]);
|
|
386
|
+
state.count = 5;
|
|
387
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
388
|
+
expect(disposeValues).toEqual([0, 1]);
|
|
389
|
+
state.count = 10;
|
|
390
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
391
|
+
expect(disposeValues).toEqual([0, 1, 5]);
|
|
392
|
+
});
|
|
393
|
+
it("should handle rapid state changes with dispose", async () => {
|
|
394
|
+
const state = createState({ count: 0 });
|
|
395
|
+
const effectFn = vi.fn(() => {
|
|
396
|
+
state.count;
|
|
397
|
+
});
|
|
398
|
+
const disposeFn = vi.fn();
|
|
399
|
+
createEffect(() => {
|
|
400
|
+
effectFn();
|
|
401
|
+
return disposeFn;
|
|
402
|
+
});
|
|
403
|
+
expect(effectFn).toHaveBeenCalledTimes(1);
|
|
404
|
+
expect(disposeFn).toHaveBeenCalledTimes(0);
|
|
405
|
+
// Rapid changes should batch
|
|
406
|
+
state.count = 1;
|
|
407
|
+
state.count = 2;
|
|
408
|
+
state.count = 3;
|
|
409
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
410
|
+
// Effect and dispose should each be called once more
|
|
411
|
+
expect(effectFn).toHaveBeenCalledTimes(2);
|
|
412
|
+
expect(disposeFn).toHaveBeenCalledTimes(1);
|
|
413
|
+
});
|
|
414
|
+
it("should run effects synchronously before render when props change", async () => {
|
|
415
|
+
const renderLog = [];
|
|
416
|
+
function Child(props) {
|
|
417
|
+
const state = createState({ internalValue: 0 });
|
|
418
|
+
createEffect(() => {
|
|
419
|
+
// Update internal state based on prop
|
|
420
|
+
state.internalValue = props.value * 2;
|
|
421
|
+
});
|
|
422
|
+
return () => {
|
|
423
|
+
renderLog.push(`render:${state.internalValue}`);
|
|
424
|
+
return _jsx("div", {
|
|
425
|
+
class: "child-component",
|
|
426
|
+
children: state.internalValue,
|
|
427
|
+
});
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
function Parent() {
|
|
431
|
+
const state = createState({ count: 1 });
|
|
432
|
+
return () => {
|
|
433
|
+
renderLog.push(`parent-render:${state.count}`);
|
|
434
|
+
return _jsxs("div", {
|
|
435
|
+
children: [
|
|
436
|
+
_jsx(Child, { value: state.count }),
|
|
437
|
+
_jsx("button", {
|
|
438
|
+
onClick: () => {
|
|
439
|
+
state.count = 2;
|
|
440
|
+
},
|
|
441
|
+
children: "Increment",
|
|
442
|
+
}),
|
|
443
|
+
],
|
|
444
|
+
});
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
const container = document.createElement("div");
|
|
448
|
+
document.body.appendChild(container);
|
|
449
|
+
render(_jsx(Parent, {}), container);
|
|
450
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
451
|
+
// Initial render: parent renders, then child renders with effect-updated state
|
|
452
|
+
expect(renderLog).toEqual(["parent-render:1", "render:2"]);
|
|
453
|
+
const childDiv = container.querySelector(".child-component");
|
|
454
|
+
expect(childDiv?.textContent).toBe("2");
|
|
455
|
+
// Clear log and trigger update
|
|
456
|
+
renderLog.length = 0;
|
|
457
|
+
const button = container.querySelector("button");
|
|
458
|
+
button.click();
|
|
459
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
460
|
+
|
|
461
|
+
// After prop update: parent renders, child's effect runs synchronously updating state,
|
|
462
|
+
// then child renders once with the updated state
|
|
463
|
+
expect(renderLog).toEqual(["parent-render:2", "render:4"]);
|
|
464
|
+
expect(childDiv?.textContent).toBe("4");
|
|
465
|
+
document.body.removeChild(container);
|
|
466
|
+
});
|
|
467
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createState.test.d.ts","sourceRoot":"","sources":["../../src/tests/createState.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { createState } from "../createState";
|
|
3
|
+
import { Observer } from "../observation";
|
|
4
|
+
describe("createState", () => {
|
|
5
|
+
it("should create a reactive proxy from an object", () => {
|
|
6
|
+
const state = createState({ count: 0 });
|
|
7
|
+
expect(state.count).toBe(0);
|
|
8
|
+
});
|
|
9
|
+
it("should allow mutations", () => {
|
|
10
|
+
const state = createState({ count: 0 });
|
|
11
|
+
state.count = 5;
|
|
12
|
+
expect(state.count).toBe(5);
|
|
13
|
+
});
|
|
14
|
+
it("should return the same proxy for the same object", () => {
|
|
15
|
+
const obj = { count: 0 };
|
|
16
|
+
const proxy1 = createState(obj);
|
|
17
|
+
const proxy2 = createState(obj);
|
|
18
|
+
expect(proxy1).toBe(proxy2);
|
|
19
|
+
});
|
|
20
|
+
it("should create nested proxies for nested objects", () => {
|
|
21
|
+
const state = createState({ user: { name: "Alice", age: 30 } });
|
|
22
|
+
state.user.name = "Bob";
|
|
23
|
+
expect(state.user.name).toBe("Bob");
|
|
24
|
+
});
|
|
25
|
+
it("should handle arrays reactively", () => {
|
|
26
|
+
const state = createState({ items: [1, 2, 3] });
|
|
27
|
+
state.items.push(4);
|
|
28
|
+
expect(state.items).toEqual([1, 2, 3, 4]);
|
|
29
|
+
});
|
|
30
|
+
it("should track property access in observers", async () => {
|
|
31
|
+
const state = createState({ count: 0 });
|
|
32
|
+
let renderCount = 0;
|
|
33
|
+
const observer = new Observer(() => {
|
|
34
|
+
renderCount++;
|
|
35
|
+
});
|
|
36
|
+
const dispose = observer.observe();
|
|
37
|
+
state.count; // Access property to track it
|
|
38
|
+
dispose();
|
|
39
|
+
expect(renderCount).toBe(0);
|
|
40
|
+
// Mutate after observation setup
|
|
41
|
+
const dispose2 = observer.observe();
|
|
42
|
+
const value = state.count; // Track
|
|
43
|
+
dispose2(); // Stop observing, subscriptions are now active
|
|
44
|
+
state.count = 1;
|
|
45
|
+
// Wait for microtasks to complete
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
47
|
+
expect(renderCount).toBeGreaterThan(0);
|
|
48
|
+
});
|
|
49
|
+
it("should handle property deletion", () => {
|
|
50
|
+
const state = createState({ count: 0, temp: "value" });
|
|
51
|
+
delete state.temp;
|
|
52
|
+
expect(state.temp).toBeUndefined();
|
|
53
|
+
expect("temp" in state).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
it("should not create proxies for functions", () => {
|
|
56
|
+
const fn = () => "hello";
|
|
57
|
+
const state = createState({ method: fn });
|
|
58
|
+
expect(state.method).toBe(fn);
|
|
59
|
+
expect(state.method()).toBe("hello");
|
|
60
|
+
});
|
|
61
|
+
it("should handle symbol properties", () => {
|
|
62
|
+
const sym = Symbol("test");
|
|
63
|
+
const state = createState({ [sym]: "value" });
|
|
64
|
+
expect(state[sym]).toBe("value");
|
|
65
|
+
});
|
|
66
|
+
it("should notify observers only on actual changes", async () => {
|
|
67
|
+
const state = createState({ count: 0 });
|
|
68
|
+
let notifyCount = 0;
|
|
69
|
+
const observer = new Observer(() => {
|
|
70
|
+
notifyCount++;
|
|
71
|
+
});
|
|
72
|
+
const dispose = observer.observe();
|
|
73
|
+
state.count; // Track
|
|
74
|
+
dispose();
|
|
75
|
+
state.count = 0; // Same value - should still notify per current implementation
|
|
76
|
+
state.count = 0;
|
|
77
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
78
|
+
// The implementation notifies even for same value, except for optimization cases
|
|
79
|
+
observer.dispose();
|
|
80
|
+
});
|
|
81
|
+
it("should handle deeply nested objects", () => {
|
|
82
|
+
const state = createState({
|
|
83
|
+
level1: {
|
|
84
|
+
level2: {
|
|
85
|
+
level3: {
|
|
86
|
+
value: "deep",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
state.level1.level2.level3.value = "modified";
|
|
92
|
+
expect(state.level1.level2.level3.value).toBe("modified");
|
|
93
|
+
});
|
|
94
|
+
it("should handle array mutations correctly", () => {
|
|
95
|
+
const state = createState({ items: [1, 2, 3] });
|
|
96
|
+
state.items.pop();
|
|
97
|
+
expect(state.items).toEqual([1, 2]);
|
|
98
|
+
state.items.unshift(0);
|
|
99
|
+
expect(state.items).toEqual([0, 1, 2]);
|
|
100
|
+
state.items.splice(1, 1, 99);
|
|
101
|
+
expect(state.items).toEqual([0, 99, 2]);
|
|
102
|
+
});
|
|
103
|
+
it("should cache proxies for array elements to prevent double-wrapping", () => {
|
|
104
|
+
const state = createState({
|
|
105
|
+
data: [
|
|
106
|
+
{ id: 1, label: "Item 1" },
|
|
107
|
+
{ id: 2, label: "Item 2" },
|
|
108
|
+
{ id: 3, label: "Item 3" },
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
// Access the same array element multiple times
|
|
112
|
+
const firstAccess = state.data[0];
|
|
113
|
+
const secondAccess = state.data[0];
|
|
114
|
+
// Should return the exact same proxy reference
|
|
115
|
+
expect(firstAccess).toBe(secondAccess);
|
|
116
|
+
// Test with array iteration methods
|
|
117
|
+
const mapped = state.data.map((item) => item);
|
|
118
|
+
const firstItem = mapped[0];
|
|
119
|
+
const directAccess = state.data[0];
|
|
120
|
+
// The proxy returned from iteration should be the same as direct access
|
|
121
|
+
expect(firstItem).toBe(directAccess);
|
|
122
|
+
// Test that we don't double-wrap when iterating multiple times
|
|
123
|
+
const mapped2 = state.data.map((item) => item);
|
|
124
|
+
expect(mapped[0]).toBe(mapped2[0]);
|
|
125
|
+
});
|
|
126
|
+
it("should maintain proxy identity after filter operations", () => {
|
|
127
|
+
const state = createState({
|
|
128
|
+
data: [
|
|
129
|
+
{ id: 1, label: "Item 1" },
|
|
130
|
+
{ id: 2, label: "Item 2" },
|
|
131
|
+
{ id: 3, label: "Item 3" },
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
// Get reference to an item before filtering
|
|
135
|
+
const originalItem = state.data[0];
|
|
136
|
+
// Simulate the remove operation: filter creates a new array but reuses proxies
|
|
137
|
+
state.data = state.data.filter((row) => row.id !== 2);
|
|
138
|
+
// After filter, the first item should still be the same proxy reference
|
|
139
|
+
const afterFilter = state.data[0];
|
|
140
|
+
expect(afterFilter).toBe(originalItem);
|
|
141
|
+
// And accessing it multiple times should return the same reference
|
|
142
|
+
expect(state.data[0]).toBe(state.data[0]);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createTask.test.d.ts","sourceRoot":"","sources":["../../src/tests/createTask.test.ts"],"names":[],"mappings":""}
|