rask-ui 0.16.0 → 0.18.0

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.
@@ -4,451 +4,464 @@ import { createEffect } from "../createEffect";
4
4
  import { createState } from "../createState";
5
5
  import { render } from "../index";
6
6
  describe("createEffect", () => {
7
- it("should run immediately on creation", () => {
8
- const effectFn = vi.fn();
9
- createEffect(effectFn);
10
- expect(effectFn).toHaveBeenCalledTimes(1);
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
11
16
  });
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);
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);
22
28
  });
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]);
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);
36
42
  });
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]);
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);
47
54
  });
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]);
55
+ createEffect(() => {
56
+ results2.push(state.count * 2);
64
57
  });
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
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'
80
69
  });
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
- }
87
- else {
88
- state.b;
89
- }
90
- });
91
- createEffect(effectFn);
92
- expect(effectFn).toHaveBeenCalledTimes(1);
93
- // Change 'a' (currently tracked)
94
- state.a = 10;
95
- await new Promise((resolve) => setTimeout(resolve, 0));
96
- expect(effectFn).toHaveBeenCalledTimes(2);
97
- // Change 'b' (not tracked)
98
- state.b = 20;
99
- await new Promise((resolve) => setTimeout(resolve, 0));
100
- expect(effectFn).toHaveBeenCalledTimes(2); // No change
101
- // Switch to using 'b'
102
- state.useA = false;
103
- await new Promise((resolve) => setTimeout(resolve, 0));
104
- expect(effectFn).toHaveBeenCalledTimes(3);
105
- // Change 'a' (no longer tracked)
106
- state.a = 100;
107
- await new Promise((resolve) => setTimeout(resolve, 0));
108
- expect(effectFn).toHaveBeenCalledTimes(3); // No change
109
- // Change 'b' (now tracked)
110
- state.b = 200;
111
- await new Promise((resolve) => setTimeout(resolve, 0));
112
- expect(effectFn).toHaveBeenCalledTimes(4);
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
+ }
113
89
  });
114
- it("should handle effects that modify state", async () => {
115
- const state = createState({ input: 1, output: 0 });
116
- createEffect(() => {
117
- state.output = state.input * 2;
118
- });
119
- expect(state.output).toBe(2);
120
- state.input = 5;
121
- await new Promise((resolve) => setTimeout(resolve, 0));
122
- expect(state.output).toBe(10);
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;
123
117
  });
124
- it("should handle nested state access", async () => {
125
- const state = createState({
126
- user: {
127
- profile: {
128
- name: "Alice",
129
- },
130
- },
131
- });
132
- const results = [];
133
- createEffect(() => {
134
- results.push(state.user.profile.name);
135
- });
136
- expect(results).toEqual(["Alice"]);
137
- state.user.profile.name = "Bob";
138
- await new Promise((resolve) => setTimeout(resolve, 0));
139
- expect(results).toEqual(["Alice", "Bob"]);
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
+ },
140
130
  });
141
- it("should handle array access", async () => {
142
- const state = createState({ items: [1, 2, 3] });
143
- const results = [];
144
- createEffect(() => {
145
- results.push(state.items.length);
146
- });
147
- expect(results).toEqual([3]);
148
- state.items.push(4);
149
- await new Promise((resolve) => setTimeout(resolve, 0));
150
- expect(results).toEqual([3, 4]);
151
- state.items.pop();
152
- await new Promise((resolve) => setTimeout(resolve, 0));
153
- expect(results).toEqual([3, 4, 3]);
131
+ const results = [];
132
+ createEffect(() => {
133
+ results.push(state.user.profile.name);
154
134
  });
155
- it("should handle effects accessing array elements", async () => {
156
- const state = createState({ items: [1, 2, 3] });
157
- const results = [];
158
- createEffect(() => {
159
- const sum = state.items.reduce((acc, val) => acc + val, 0);
160
- results.push(sum);
161
- });
162
- expect(results).toEqual([6]);
163
- state.items.push(4);
164
- await new Promise((resolve) => setTimeout(resolve, 0));
165
- expect(results).toEqual([6, 10]);
166
- state.items[0] = 10;
167
- await new Promise((resolve) => setTimeout(resolve, 0));
168
- expect(results).toEqual([6, 10, 19]);
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);
169
145
  });
170
- it("should batch multiple state changes before re-running", async () => {
171
- const state = createState({ a: 1, b: 2 });
172
- const effectFn = vi.fn(() => {
173
- state.a;
174
- state.b;
175
- });
176
- createEffect(effectFn);
177
- expect(effectFn).toHaveBeenCalledTimes(1);
178
- // Multiple changes in same turn
179
- state.a = 10;
180
- state.b = 20;
181
- state.a = 15;
182
- await new Promise((resolve) => setTimeout(resolve, 0));
183
- // Should only run once for all changes
184
- expect(effectFn).toHaveBeenCalledTimes(2);
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);
185
160
  });
186
- it("should handle effects with no dependencies", async () => {
187
- let runCount = 0;
188
- createEffect(() => {
189
- runCount++;
190
- // No reactive state accessed
191
- });
192
- expect(runCount).toBe(1);
193
- // Wait to ensure it doesn't run again
194
- await new Promise((resolve) => setTimeout(resolve, 10));
195
- expect(runCount).toBe(1);
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;
196
174
  });
197
- it("should handle effects that conditionally access state", async () => {
198
- const state = createState({ enabled: true, value: 5 });
199
- const effectFn = vi.fn(() => {
200
- if (state.enabled) {
201
- state.value;
202
- }
203
- });
204
- createEffect(effectFn);
205
- expect(effectFn).toHaveBeenCalledTimes(1);
206
- // Change value (tracked because enabled is true)
207
- state.value = 10;
208
- await new Promise((resolve) => setTimeout(resolve, 0));
209
- expect(effectFn).toHaveBeenCalledTimes(2);
210
- // Disable
211
- state.enabled = false;
212
- await new Promise((resolve) => setTimeout(resolve, 0));
213
- expect(effectFn).toHaveBeenCalledTimes(3);
214
- // Change value (not tracked because enabled is false)
215
- state.value = 20;
216
- await new Promise((resolve) => setTimeout(resolve, 0));
217
- expect(effectFn).toHaveBeenCalledTimes(3); // No change
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
218
190
  });
219
- it("should handle complex dependency graphs", async () => {
220
- const state = createState({
221
- multiplier: 2,
222
- values: [1, 2, 3],
223
- });
224
- const results = [];
225
- createEffect(() => {
226
- const sum = state.values.reduce((acc, val) => acc + val, 0);
227
- results.push(sum * state.multiplier);
228
- });
229
- expect(results).toEqual([12]); // (1+2+3) * 2
230
- state.multiplier = 3;
231
- await new Promise((resolve) => setTimeout(resolve, 0));
232
- expect(results).toEqual([12, 18]); // (1+2+3) * 3
233
- state.values.push(4);
234
- await new Promise((resolve) => setTimeout(resolve, 0));
235
- expect(results).toEqual([12, 18, 30]); // (1+2+3+4) * 3
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
+ }
236
202
  });
237
- it("should handle effects that run other synchronous code", async () => {
238
- const state = createState({ count: 0 });
239
- const sideEffects = [];
240
- createEffect(() => {
241
- sideEffects.push("effect-start");
242
- const value = state.count;
243
- sideEffects.push(`value-${value}`);
244
- sideEffects.push("effect-end");
245
- });
246
- expect(sideEffects).toEqual(["effect-start", "value-0", "effect-end"]);
247
- state.count = 1;
248
- await new Promise((resolve) => setTimeout(resolve, 0));
249
- expect(sideEffects).toEqual([
250
- "effect-start",
251
- "value-0",
252
- "effect-end",
253
- "effect-start",
254
- "value-1",
255
- "effect-end",
256
- ]);
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],
257
222
  });
258
- it("should handle rapid state changes", async () => {
259
- const state = createState({ count: 0 });
260
- const effectFn = vi.fn(() => {
261
- state.count;
262
- });
263
- createEffect(effectFn);
264
- expect(effectFn).toHaveBeenCalledTimes(1);
265
- // Rapid changes
266
- for (let i = 1; i <= 10; i++) {
267
- state.count = i;
268
- }
269
- await new Promise((resolve) => setTimeout(resolve, 0));
270
- // Should batch all changes into one effect run
271
- expect(effectFn).toHaveBeenCalledTimes(2);
223
+ const results = [];
224
+ createEffect(() => {
225
+ const sum = state.values.reduce((acc, val) => acc + val, 0);
226
+ results.push(sum * state.multiplier);
272
227
  });
273
- it("should access latest state values when effect runs", async () => {
274
- const state = createState({ count: 0 });
275
- const results = [];
276
- createEffect(() => {
277
- results.push(state.count);
278
- });
279
- state.count = 1;
280
- state.count = 2;
281
- state.count = 3;
282
- await new Promise((resolve) => setTimeout(resolve, 0));
283
- // Should see latest value when effect runs
284
- expect(results).toEqual([0, 3]);
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");
285
244
  });
286
- it("should call dispose function before re-executing", async () => {
287
- const state = createState({ count: 0 });
288
- const disposeCalls = [];
289
- const effectCalls = [];
290
- createEffect(() => {
291
- effectCalls.push(state.count);
292
- return () => {
293
- // Dispose sees the current state at the time it's called
294
- disposeCalls.push(state.count);
295
- };
296
- });
297
- expect(effectCalls).toEqual([0]);
298
- expect(disposeCalls).toEqual([]);
299
- state.count = 1;
300
- await new Promise((resolve) => setTimeout(resolve, 0));
301
- // Dispose is called after state change, before effect re-runs
302
- expect(disposeCalls).toEqual([1]);
303
- expect(effectCalls).toEqual([0, 1]);
304
- state.count = 2;
305
- await new Promise((resolve) => setTimeout(resolve, 0));
306
- expect(disposeCalls).toEqual([1, 2]);
307
- expect(effectCalls).toEqual([0, 1, 2]);
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;
308
261
  });
309
- it("should handle dispose function with cleanup logic", async () => {
310
- const state = createState({ url: "/api/data" });
311
- const subscriptions = [];
312
- createEffect(() => {
313
- const currentUrl = state.url;
314
- subscriptions.push(`subscribe:${currentUrl}`);
315
- return () => {
316
- subscriptions.push(`unsubscribe:${currentUrl}`);
317
- };
318
- });
319
- expect(subscriptions).toEqual(["subscribe:/api/data"]);
320
- state.url = "/api/users";
321
- await new Promise((resolve) => setTimeout(resolve, 0));
322
- expect(subscriptions).toEqual([
323
- "subscribe:/api/data",
324
- "unsubscribe:/api/data",
325
- "subscribe:/api/users",
326
- ]);
327
- state.url = "/api/posts";
328
- await new Promise((resolve) => setTimeout(resolve, 0));
329
- expect(subscriptions).toEqual([
330
- "subscribe:/api/data",
331
- "unsubscribe:/api/data",
332
- "subscribe:/api/users",
333
- "unsubscribe:/api/users",
334
- "subscribe:/api/posts",
335
- ]);
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);
336
277
  });
337
- it("should handle effects without dispose function", async () => {
338
- const state = createState({ count: 0 });
339
- const effectCalls = [];
340
- createEffect(() => {
341
- effectCalls.push(state.count);
342
- // No dispose function returned
343
- });
344
- expect(effectCalls).toEqual([0]);
345
- state.count = 1;
346
- await new Promise((resolve) => setTimeout(resolve, 0));
347
- expect(effectCalls).toEqual([0, 1]);
348
- state.count = 2;
349
- await new Promise((resolve) => setTimeout(resolve, 0));
350
- expect(effectCalls).toEqual([0, 1, 2]);
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
+ };
351
295
  });
352
- it("should handle dispose function that throws error", async () => {
353
- const state = createState({ count: 0 });
354
- const effectCalls = [];
355
- const consoleErrorSpy = vi
356
- .spyOn(console, "error")
357
- .mockImplementation(() => { });
358
- createEffect(() => {
359
- effectCalls.push(state.count);
360
- return () => {
361
- throw new Error("Dispose error");
362
- };
363
- });
364
- expect(effectCalls).toEqual([0]);
365
- state.count = 1;
366
- await new Promise((resolve) => setTimeout(resolve, 0));
367
- // Effect should still have run despite dispose throwing
368
- expect(effectCalls).toEqual([0, 1]);
369
- expect(consoleErrorSpy).toHaveBeenCalledWith("Error in effect dispose function:", expect.any(Error));
370
- consoleErrorSpy.mockRestore();
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
+ };
371
317
  });
372
- it("should call dispose with latest closure values", async () => {
373
- const state = createState({ count: 0 });
374
- const disposeValues = [];
375
- createEffect(() => {
376
- const capturedCount = state.count;
377
- return () => {
378
- disposeValues.push(capturedCount);
379
- };
380
- });
381
- state.count = 1;
382
- await new Promise((resolve) => setTimeout(resolve, 0));
383
- expect(disposeValues).toEqual([0]);
384
- state.count = 5;
385
- await new Promise((resolve) => setTimeout(resolve, 0));
386
- expect(disposeValues).toEqual([0, 1]);
387
- state.count = 10;
388
- await new Promise((resolve) => setTimeout(resolve, 0));
389
- expect(disposeValues).toEqual([0, 1, 5]);
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
390
342
  });
391
- it("should handle rapid state changes with dispose", async () => {
392
- const state = createState({ count: 0 });
393
- const effectFn = vi.fn(() => {
394
- state.count;
395
- });
396
- const disposeFn = vi.fn();
397
- createEffect(() => {
398
- effectFn();
399
- return disposeFn;
400
- });
401
- expect(effectFn).toHaveBeenCalledTimes(1);
402
- expect(disposeFn).toHaveBeenCalledTimes(0);
403
- // Rapid changes should batch
404
- state.count = 1;
405
- state.count = 2;
406
- state.count = 3;
407
- await new Promise((resolve) => setTimeout(resolve, 0));
408
- // Effect and dispose should each be called once more
409
- expect(effectFn).toHaveBeenCalledTimes(2);
410
- expect(disposeFn).toHaveBeenCalledTimes(1);
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
+ };
411
382
  });
412
- it("should run effects synchronously before render when props change", async () => {
413
- const renderLog = [];
414
- function Child(props) {
415
- const state = createState({ internalValue: 0 });
416
- createEffect(() => {
417
- // Update internal state based on prop
418
- state.internalValue = props.value * 2;
419
- });
420
- return () => {
421
- renderLog.push(`render:${state.internalValue}`);
422
- return _jsx("div", { class: "child-component", children: state.internalValue });
423
- };
424
- }
425
- function Parent() {
426
- const state = createState({ count: 1 });
427
- return () => {
428
- renderLog.push(`parent-render:${state.count}`);
429
- return (_jsxs("div", { children: [_jsx(Child, { value: state.count }), _jsx("button", { onClick: () => {
430
- state.count = 2;
431
- }, children: "Increment" })] }));
432
- };
433
- }
434
- const container = document.createElement("div");
435
- document.body.appendChild(container);
436
- render(_jsx(Parent, {}), container);
437
- await new Promise((resolve) => setTimeout(resolve, 10));
438
- // Initial render: parent renders, then child renders with effect-updated state
439
- expect(renderLog).toEqual(["parent-render:1", "render:2"]);
440
- const childDiv = container.querySelector(".child-component");
441
- expect(childDiv?.textContent).toBe("2");
442
- // Clear log and trigger update
443
- renderLog.length = 0;
444
- const button = container.querySelector("button");
445
- button.click();
446
- await new Promise((resolve) => setTimeout(resolve, 10));
447
- console.log(renderLog);
448
- // After prop update: parent renders, child's effect runs synchronously updating state,
449
- // then child renders once with the updated state
450
- expect(renderLog).toEqual(["parent-render:2", "render:4"]);
451
- expect(childDiv?.textContent).toBe("4");
452
- document.body.removeChild(container);
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;
453
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
+ });
454
467
  });