rask-ui 0.28.3 → 0.28.4

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.
Files changed (38) hide show
  1. package/dist/tests/batch.test.js +202 -12
  2. package/dist/tests/createContext.test.js +50 -37
  3. package/dist/tests/error.test.js +25 -12
  4. package/dist/tests/renderCount.test.d.ts +2 -0
  5. package/dist/tests/renderCount.test.d.ts.map +1 -0
  6. package/dist/tests/renderCount.test.js +95 -0
  7. package/dist/tests/scopeEnforcement.test.d.ts +2 -0
  8. package/dist/tests/scopeEnforcement.test.d.ts.map +1 -0
  9. package/dist/tests/scopeEnforcement.test.js +157 -0
  10. package/dist/tests/useAction.test.d.ts +2 -0
  11. package/dist/tests/useAction.test.d.ts.map +1 -0
  12. package/dist/tests/useAction.test.js +132 -0
  13. package/dist/tests/useAsync.test.d.ts +2 -0
  14. package/dist/tests/useAsync.test.d.ts.map +1 -0
  15. package/dist/tests/useAsync.test.js +499 -0
  16. package/dist/tests/useDerived.test.d.ts +2 -0
  17. package/dist/tests/useDerived.test.d.ts.map +1 -0
  18. package/dist/tests/useDerived.test.js +407 -0
  19. package/dist/tests/useEffect.test.d.ts +2 -0
  20. package/dist/tests/useEffect.test.d.ts.map +1 -0
  21. package/dist/tests/useEffect.test.js +600 -0
  22. package/dist/tests/useLookup.test.d.ts +2 -0
  23. package/dist/tests/useLookup.test.d.ts.map +1 -0
  24. package/dist/tests/useLookup.test.js +299 -0
  25. package/dist/tests/useRef.test.d.ts +2 -0
  26. package/dist/tests/useRef.test.d.ts.map +1 -0
  27. package/dist/tests/useRef.test.js +189 -0
  28. package/dist/tests/useState.test.d.ts +2 -0
  29. package/dist/tests/useState.test.d.ts.map +1 -0
  30. package/dist/tests/useState.test.js +178 -0
  31. package/dist/tests/useSuspend.test.d.ts +2 -0
  32. package/dist/tests/useSuspend.test.d.ts.map +1 -0
  33. package/dist/tests/useSuspend.test.js +752 -0
  34. package/dist/tests/useView.test.d.ts +2 -0
  35. package/dist/tests/useView.test.d.ts.map +1 -0
  36. package/dist/tests/useView.test.js +305 -0
  37. package/dist/useState.js +4 -2
  38. package/package.json +1 -1
@@ -1,10 +1,10 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { syncBatch } from "../batch";
3
- import { createState } from "../createState";
3
+ import { useState } from "../useState";
4
4
  import { Observer } from "../observation";
5
5
  describe("syncBatch", () => {
6
6
  it("should batch multiple state changes into a single notification", () => {
7
- const state = createState({ count: 0, name: "Alice" });
7
+ const state = useState({ count: 0, name: "Alice" });
8
8
  let notifyCount = 0;
9
9
  const observer = new Observer(() => {
10
10
  notifyCount++;
@@ -26,7 +26,7 @@ describe("syncBatch", () => {
26
26
  observer.dispose();
27
27
  });
28
28
  it("should handle nested batches correctly", () => {
29
- const state = createState({ count: 0 });
29
+ const state = useState({ count: 0 });
30
30
  let notifyCount = 0;
31
31
  const observer = new Observer(() => {
32
32
  notifyCount++;
@@ -47,7 +47,7 @@ describe("syncBatch", () => {
47
47
  observer.dispose();
48
48
  });
49
49
  it("should handle multiple observers with syncBatch", () => {
50
- const state = createState({ count: 0 });
50
+ const state = useState({ count: 0 });
51
51
  let notifyCount1 = 0;
52
52
  let notifyCount2 = 0;
53
53
  const observer1 = new Observer(() => {
@@ -74,7 +74,7 @@ describe("syncBatch", () => {
74
74
  observer2.dispose();
75
75
  });
76
76
  it("should maintain correct state values after syncBatch", () => {
77
- const state = createState({
77
+ const state = useState({
78
78
  count: 0,
79
79
  name: "Alice",
80
80
  items: [1, 2, 3],
@@ -90,7 +90,7 @@ describe("syncBatch", () => {
90
90
  expect(state.items).toEqual([100, 2, 3, 4]);
91
91
  });
92
92
  it("should not flush if exception thrown within syncBatch", () => {
93
- const state = createState({ count: 0 });
93
+ const state = useState({ count: 0 });
94
94
  let notifyCount = 0;
95
95
  const observer = new Observer(() => {
96
96
  notifyCount++;
@@ -114,7 +114,7 @@ describe("syncBatch", () => {
114
114
  observer.dispose();
115
115
  });
116
116
  it("should deduplicate notifications for the same observer", () => {
117
- const state = createState({ count: 0, name: "Alice" });
117
+ const state = useState({ count: 0, name: "Alice" });
118
118
  let notifyCount = 0;
119
119
  const observer = new Observer(() => {
120
120
  notifyCount++;
@@ -135,7 +135,7 @@ describe("syncBatch", () => {
135
135
  });
136
136
  describe("queue (async batching)", () => {
137
137
  it("should queue updates and flush on microtask", async () => {
138
- const state = createState({ count: 0 });
138
+ const state = useState({ count: 0 });
139
139
  let notifyCount = 0;
140
140
  const observer = new Observer(() => {
141
141
  notifyCount++;
@@ -157,7 +157,7 @@ describe("queue (async batching)", () => {
157
157
  observer.dispose();
158
158
  });
159
159
  it("should batch multiple async updates into one notification", async () => {
160
- const state = createState({ count: 0, name: "Alice" });
160
+ const state = useState({ count: 0, name: "Alice" });
161
161
  let notifyCount = 0;
162
162
  const observer = new Observer(() => {
163
163
  notifyCount++;
@@ -175,7 +175,7 @@ describe("queue (async batching)", () => {
175
175
  observer.dispose();
176
176
  });
177
177
  it("should handle separate async batches", async () => {
178
- const state = createState({ count: 0 });
178
+ const state = useState({ count: 0 });
179
179
  let notifyCount = 0;
180
180
  const observer = new Observer(() => {
181
181
  notifyCount++;
@@ -196,7 +196,7 @@ describe("queue (async batching)", () => {
196
196
  });
197
197
  describe("syncBatch with nested async updates", () => {
198
198
  it("should handle syncBatch inside async context", async () => {
199
- const state = createState({ count: 0 });
199
+ const state = useState({ count: 0 });
200
200
  let notifyCount = 0;
201
201
  const observer = new Observer(() => {
202
202
  notifyCount++;
@@ -217,7 +217,7 @@ describe("syncBatch with nested async updates", () => {
217
217
  observer.dispose();
218
218
  });
219
219
  it("should handle async updates inside syncBatch callback", async () => {
220
- const state = createState({ count: 0 });
220
+ const state = useState({ count: 0 });
221
221
  let notifyCount = 0;
222
222
  const observer = new Observer(() => {
223
223
  notifyCount++;
@@ -242,3 +242,193 @@ describe("syncBatch with nested async updates", () => {
242
242
  observer.dispose();
243
243
  });
244
244
  });
245
+ describe("syncBatch with cascading updates", () => {
246
+ it("should handle cascading observer notifications within the same batch", () => {
247
+ const state = useState({ count: 0 });
248
+ const derived = useState({ doubled: 0 });
249
+ let stateNotifyCount = 0;
250
+ let derivedNotifyCount = 0;
251
+ let componentNotifyCount = 0;
252
+ // Observer 1: Watches state, updates derived (simulates useDerived)
253
+ const derivedObserver = new Observer(() => {
254
+ stateNotifyCount++;
255
+ // When state changes, update derived synchronously
256
+ derived.doubled = state.count * 2;
257
+ });
258
+ const dispose1 = derivedObserver.observe();
259
+ state.count; // Track state
260
+ dispose1();
261
+ // Observer 2: Watches derived (simulates component)
262
+ const componentObserver = new Observer(() => {
263
+ derivedNotifyCount++;
264
+ });
265
+ const dispose2 = componentObserver.observe();
266
+ derived.doubled; // Track derived
267
+ dispose2();
268
+ // Observer 3: Also watches derived (another component)
269
+ const component2Observer = new Observer(() => {
270
+ componentNotifyCount++;
271
+ });
272
+ const dispose3 = component2Observer.observe();
273
+ derived.doubled; // Track derived
274
+ dispose3();
275
+ // Make a change in a batch
276
+ syncBatch(() => {
277
+ state.count = 5;
278
+ });
279
+ // All observers should have been notified exactly once
280
+ expect(stateNotifyCount).toBe(1);
281
+ expect(derivedNotifyCount).toBe(1);
282
+ expect(componentNotifyCount).toBe(1);
283
+ expect(state.count).toBe(5);
284
+ expect(derived.doubled).toBe(10);
285
+ derivedObserver.dispose();
286
+ componentObserver.dispose();
287
+ component2Observer.dispose();
288
+ });
289
+ it("should handle multi-level cascading updates", () => {
290
+ const state = useState({ value: 0 });
291
+ const derived1 = useState({ level1: 0 });
292
+ const derived2 = useState({ level2: 0 });
293
+ const derived3 = useState({ level3: 0 });
294
+ const notifyCounts = [0, 0, 0, 0];
295
+ // Level 1: state -> derived1
296
+ const observer1 = new Observer(() => {
297
+ notifyCounts[0]++;
298
+ derived1.level1 = state.value + 1;
299
+ });
300
+ const dispose1 = observer1.observe();
301
+ state.value;
302
+ dispose1();
303
+ // Level 2: derived1 -> derived2
304
+ const observer2 = new Observer(() => {
305
+ notifyCounts[1]++;
306
+ derived2.level2 = derived1.level1 + 1;
307
+ });
308
+ const dispose2 = observer2.observe();
309
+ derived1.level1;
310
+ dispose2();
311
+ // Level 3: derived2 -> derived3
312
+ const observer3 = new Observer(() => {
313
+ notifyCounts[2]++;
314
+ derived3.level3 = derived2.level2 + 1;
315
+ });
316
+ const dispose3 = observer3.observe();
317
+ derived2.level2;
318
+ dispose3();
319
+ // Final observer: watches derived3
320
+ const observer4 = new Observer(() => {
321
+ notifyCounts[3]++;
322
+ });
323
+ const dispose4 = observer4.observe();
324
+ derived3.level3;
325
+ dispose4();
326
+ // Update state in a batch
327
+ syncBatch(() => {
328
+ state.value = 10;
329
+ });
330
+ // All levels should have cascaded and each observer notified exactly once
331
+ expect(notifyCounts).toEqual([1, 1, 1, 1]);
332
+ expect(state.value).toBe(10);
333
+ expect(derived1.level1).toBe(11);
334
+ expect(derived2.level2).toBe(12);
335
+ expect(derived3.level3).toBe(13);
336
+ observer1.dispose();
337
+ observer2.dispose();
338
+ observer3.dispose();
339
+ observer4.dispose();
340
+ });
341
+ it("should handle diamond dependency pattern", () => {
342
+ // Diamond: state -> [derived1, derived2] -> derived3
343
+ const state = useState({ value: 0 });
344
+ const derived1 = useState({ path1: 0 });
345
+ const derived2 = useState({ path2: 0 });
346
+ const derived3 = useState({ combined: 0 });
347
+ let derived3NotifyCount = 0;
348
+ // State -> derived1
349
+ const obs1 = new Observer(() => {
350
+ derived1.path1 = state.value * 2;
351
+ });
352
+ const d1 = obs1.observe();
353
+ state.value;
354
+ d1();
355
+ // State -> derived2
356
+ const obs2 = new Observer(() => {
357
+ derived2.path2 = state.value * 3;
358
+ });
359
+ const d2 = obs2.observe();
360
+ state.value;
361
+ d2();
362
+ // [derived1, derived2] -> derived3
363
+ const obs3 = new Observer(() => {
364
+ derived3.combined = derived1.path1 + derived2.path2;
365
+ });
366
+ const d3 = obs3.observe();
367
+ derived1.path1;
368
+ derived2.path2;
369
+ d3();
370
+ // Watch derived3
371
+ const obs4 = new Observer(() => {
372
+ derived3NotifyCount++;
373
+ });
374
+ const d4 = obs4.observe();
375
+ derived3.combined;
376
+ d4();
377
+ syncBatch(() => {
378
+ state.value = 5;
379
+ });
380
+ // derived3 should only be notified once despite two paths updating
381
+ expect(derived3NotifyCount).toBe(1);
382
+ expect(derived1.path1).toBe(10);
383
+ expect(derived2.path2).toBe(15);
384
+ expect(derived3.combined).toBe(25);
385
+ obs1.dispose();
386
+ obs2.dispose();
387
+ obs3.dispose();
388
+ obs4.dispose();
389
+ });
390
+ it("should not create infinite loops with circular dependencies", () => {
391
+ const state1 = useState({ value: 0 });
392
+ const state2 = useState({ value: 0 });
393
+ let notify1Count = 0;
394
+ let notify2Count = 0;
395
+ // Observer 1: watches state1, updates state2
396
+ const obs1 = new Observer(() => {
397
+ notify1Count++;
398
+ if (notify1Count > 10) {
399
+ throw new Error("Infinite loop detected");
400
+ }
401
+ // Only update if different to break the cycle
402
+ if (state2.value !== state1.value + 1) {
403
+ state2.value = state1.value + 1;
404
+ }
405
+ });
406
+ const d1 = obs1.observe();
407
+ state1.value;
408
+ d1();
409
+ // Observer 2: watches state2, updates state1
410
+ const obs2 = new Observer(() => {
411
+ notify2Count++;
412
+ if (notify2Count > 10) {
413
+ throw new Error("Infinite loop detected");
414
+ }
415
+ // Only update if different to break the cycle
416
+ if (state1.value !== state2.value - 1) {
417
+ state1.value = state2.value - 1;
418
+ }
419
+ });
420
+ const d2 = obs2.observe();
421
+ state2.value;
422
+ d2();
423
+ syncBatch(() => {
424
+ state1.value = 5;
425
+ });
426
+ // Should stabilize without infinite loop
427
+ expect(notify1Count).toBeLessThan(10);
428
+ expect(notify2Count).toBeLessThan(10);
429
+ expect(state1.value).toBe(5);
430
+ expect(state2.value).toBe(6);
431
+ obs1.dispose();
432
+ obs2.dispose();
433
+ });
434
+ });
@@ -1,21 +1,21 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "./jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "rask-ui/jsx-runtime";
2
2
  import { describe, it, expect } from "vitest";
3
3
  import { createContext } from "../createContext";
4
4
  import { render } from "../";
5
5
  describe("createContext", () => {
6
- it("should create a context object", () => {
7
- const context = createContext();
8
- expect(context).toHaveProperty("inject");
9
- expect(context).toHaveProperty("get");
6
+ it("should create a context with use and inject methods", () => {
7
+ const MyContext = createContext(() => ({ value: "test" }));
8
+ expect(typeof MyContext.use).toBe("function");
9
+ expect(typeof MyContext.inject).toBe("function");
10
10
  });
11
11
  it("should allow setting and getting context values", () => {
12
- const ThemeContext = createContext();
12
+ const ThemeContext = createContext(() => ({ theme: "dark" }));
13
13
  function Parent() {
14
- ThemeContext.inject({ theme: "dark" });
14
+ ThemeContext.inject();
15
15
  return () => _jsx(Child, {});
16
16
  }
17
17
  function Child() {
18
- const theme = ThemeContext.get();
18
+ const theme = ThemeContext.use();
19
19
  return () => _jsx("div", { children: theme.theme });
20
20
  }
21
21
  const container = document.createElement("div");
@@ -25,16 +25,16 @@ describe("createContext", () => {
25
25
  document.body.removeChild(container);
26
26
  });
27
27
  it("should traverse parent components to find context", () => {
28
- const ThemeContext = createContext();
28
+ const ThemeContext = createContext(() => ({ theme: "light" }));
29
29
  function GrandParent() {
30
- ThemeContext.inject({ theme: "light" });
30
+ ThemeContext.inject();
31
31
  return () => _jsx(Parent, {});
32
32
  }
33
33
  function Parent() {
34
34
  return () => _jsx(Child, {});
35
35
  }
36
36
  function Child() {
37
- const theme = ThemeContext.get();
37
+ const theme = ThemeContext.use();
38
38
  return () => _jsx("div", { children: theme.theme });
39
39
  }
40
40
  const container = document.createElement("div");
@@ -44,11 +44,11 @@ describe("createContext", () => {
44
44
  document.body.removeChild(container);
45
45
  });
46
46
  it("should throw error when context is not found", () => {
47
- const ThemeContext = createContext();
47
+ const ThemeContext = createContext(() => ({ theme: "dark" }));
48
48
  function Child() {
49
49
  expect(() => {
50
- ThemeContext.get();
51
- }).toThrow("There is no parent context");
50
+ ThemeContext.use();
51
+ }).toThrow("No context available");
52
52
  return () => _jsx("div", { children: "Child" });
53
53
  }
54
54
  const container = document.createElement("div");
@@ -57,33 +57,33 @@ describe("createContext", () => {
57
57
  document.body.removeChild(container);
58
58
  });
59
59
  it("should throw error when setting context outside component", () => {
60
- const ThemeContext = createContext();
60
+ const ThemeContext = createContext(() => ({ theme: "dark" }));
61
61
  expect(() => {
62
- ThemeContext.inject({ theme: "dark" });
63
- }).toThrow("No current component");
62
+ ThemeContext.inject();
63
+ }).toThrow("Only use useInjectContext in component setup");
64
64
  });
65
65
  it("should throw error when getting context outside component", () => {
66
- const ThemeContext = createContext();
66
+ const ThemeContext = createContext(() => ({ theme: "dark" }));
67
67
  expect(() => {
68
- ThemeContext.get();
69
- }).toThrow("No current component");
68
+ ThemeContext.use();
69
+ }).toThrow("Only use useContext in component setup");
70
70
  });
71
71
  it("should allow overriding context in nested components", () => {
72
- const ThemeContext = createContext();
72
+ const ThemeContext = createContext((theme) => ({ theme }));
73
73
  function GrandParent() {
74
- ThemeContext.inject({ theme: "light" });
74
+ ThemeContext.inject("light");
75
75
  return () => (_jsxs("div", { children: [_jsx(Parent, {}), _jsx(ChildOfGrandParent, {})] }));
76
76
  }
77
77
  function Parent() {
78
- ThemeContext.inject({ theme: "dark" });
78
+ ThemeContext.inject("dark");
79
79
  return () => _jsx(ChildOfParent, {});
80
80
  }
81
81
  function ChildOfParent() {
82
- const theme = ThemeContext.get();
82
+ const theme = ThemeContext.use();
83
83
  return () => _jsx("div", { class: "child-of-parent", children: theme.theme });
84
84
  }
85
85
  function ChildOfGrandParent() {
86
- const theme = ThemeContext.get();
86
+ const theme = ThemeContext.use();
87
87
  return () => _jsx("div", { class: "child-of-grandparent", children: theme.theme });
88
88
  }
89
89
  const container = document.createElement("div");
@@ -96,16 +96,16 @@ describe("createContext", () => {
96
96
  document.body.removeChild(container);
97
97
  });
98
98
  it("should support multiple different contexts", () => {
99
- const ThemeContext = createContext();
100
- const UserContext = createContext();
99
+ const ThemeContext = createContext(() => ({ theme: "dark" }));
100
+ const UserContext = createContext(() => ({ name: "Alice" }));
101
101
  function Parent() {
102
- ThemeContext.inject({ theme: "dark" });
103
- UserContext.inject({ name: "Alice" });
102
+ ThemeContext.inject();
103
+ UserContext.inject();
104
104
  return () => _jsx(Child, {});
105
105
  }
106
106
  function Child() {
107
- const theme = ThemeContext.get();
108
- const user = UserContext.get();
107
+ const theme = ThemeContext.use();
108
+ const user = UserContext.use();
109
109
  return () => _jsx("div", { children: `${theme.theme} - ${user.name}` });
110
110
  }
111
111
  const container = document.createElement("div");
@@ -115,16 +115,16 @@ describe("createContext", () => {
115
115
  document.body.removeChild(container);
116
116
  });
117
117
  it("should handle context values of different types", () => {
118
- const NumberContext = createContext();
119
- const ArrayContext = createContext();
118
+ const NumberContext = createContext(() => 42);
119
+ const ArrayContext = createContext(() => ["a", "b", "c"]);
120
120
  function Parent() {
121
- NumberContext.inject(42);
122
- ArrayContext.inject(["a", "b", "c"]);
121
+ NumberContext.inject();
122
+ ArrayContext.inject();
123
123
  return () => _jsx(Child, {});
124
124
  }
125
125
  function Child() {
126
- const num = NumberContext.get();
127
- const arr = ArrayContext.get();
126
+ const num = NumberContext.use();
127
+ const arr = ArrayContext.use();
128
128
  return () => _jsx("div", { children: `${num} - ${arr.join(",")}` });
129
129
  }
130
130
  const container = document.createElement("div");
@@ -133,4 +133,17 @@ describe("createContext", () => {
133
133
  expect(container.textContent).toContain("42 - a,b,c");
134
134
  document.body.removeChild(container);
135
135
  });
136
+ it("should allow using context in the root component that injects it", () => {
137
+ const ThemeContext = createContext(() => ({ theme: "root-theme" }));
138
+ function RootComponent() {
139
+ ThemeContext.inject();
140
+ const theme = ThemeContext.use();
141
+ return () => _jsx("div", { children: theme.theme });
142
+ }
143
+ const container = document.createElement("div");
144
+ document.body.appendChild(container);
145
+ render(_jsx(RootComponent, {}), container);
146
+ expect(container.textContent).toContain("root-theme");
147
+ document.body.removeChild(container);
148
+ });
136
149
  });
@@ -1,14 +1,15 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "./jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "rask-ui/jsx-runtime";
2
2
  import { describe, it, expect } from "vitest";
3
- import { ErrorBoundary } from "../error";
3
+ import { useCatchError } from "../useCatchError";
4
4
  import { render } from "../";
5
- describe("ErrorBoundary", () => {
5
+ describe("useCatchError", () => {
6
6
  it("should render children when no error occurs", async () => {
7
7
  function SafeChild() {
8
8
  return () => _jsx("div", { children: "Safe content" });
9
9
  }
10
10
  function TestComponent() {
11
- return () => (_jsx(ErrorBoundary, { error: (error) => _jsxs("div", { children: ["Error: ", String(error)] }), children: _jsx(SafeChild, {}) }));
11
+ const errorState = useCatchError();
12
+ return () => errorState.error ? (_jsxs("div", { children: ["Error: ", String(errorState.error)] })) : (_jsx(SafeChild, {}));
12
13
  }
13
14
  const container = document.createElement("div");
14
15
  document.body.appendChild(container);
@@ -26,7 +27,8 @@ describe("ErrorBoundary", () => {
26
27
  };
27
28
  }
28
29
  function TestComponent() {
29
- return () => (_jsx(ErrorBoundary, { error: (error) => _jsxs("div", { children: ["Error: ", String(error)] }), children: _jsx(ThrowingChild, {}) }));
30
+ const errorState = useCatchError();
31
+ return () => errorState.error ? (_jsxs("div", { children: ["Error: ", String(errorState.error)] })) : (_jsx(ThrowingChild, {}));
30
32
  }
31
33
  const container = document.createElement("div");
32
34
  document.body.appendChild(container);
@@ -44,7 +46,8 @@ describe("ErrorBoundary", () => {
44
46
  };
45
47
  }
46
48
  function TestComponent() {
47
- return () => (_jsx(ErrorBoundary, { error: (error) => (_jsxs("div", { class: "error-ui", children: [_jsx("h1", { children: "Oops!" }), _jsx("p", { children: String(error) })] })), children: _jsx(ThrowingChild, {}) }));
49
+ const errorState = useCatchError();
50
+ return () => errorState.error ? (_jsxs("div", { class: "error-ui", children: [_jsx("h1", { children: "Oops!" }), _jsx("p", { children: String(errorState.error) })] })) : (_jsx(ThrowingChild, {}));
48
51
  }
49
52
  const container = document.createElement("div");
50
53
  document.body.appendChild(container);
@@ -64,7 +67,8 @@ describe("ErrorBoundary", () => {
64
67
  return () => _jsx("div", { children: "Child 2" });
65
68
  }
66
69
  function TestComponent() {
67
- return () => (_jsxs(ErrorBoundary, { error: (error) => _jsxs("div", { children: ["Error: ", String(error)] }), children: [_jsx(SafeChild1, {}), _jsx(SafeChild2, {})] }));
70
+ const errorState = useCatchError();
71
+ return () => errorState.error ? (_jsxs("div", { children: ["Error: ", String(errorState.error)] })) : (_jsxs(_Fragment, { children: [_jsx(SafeChild1, {}), _jsx(SafeChild2, {})] }));
68
72
  }
69
73
  const container = document.createElement("div");
70
74
  document.body.appendChild(container);
@@ -85,7 +89,8 @@ describe("ErrorBoundary", () => {
85
89
  return () => _jsx(DeepChild, {});
86
90
  }
87
91
  function TestComponent() {
88
- return () => (_jsx(ErrorBoundary, { error: (error) => _jsxs("div", { children: ["Caught: ", String(error)] }), children: _jsx(MiddleChild, {}) }));
92
+ const errorState = useCatchError();
93
+ return () => errorState.error ? (_jsxs("div", { children: ["Caught: ", String(errorState.error)] })) : (_jsx(MiddleChild, {}));
89
94
  }
90
95
  const container = document.createElement("div");
91
96
  document.body.appendChild(container);
@@ -102,8 +107,13 @@ describe("ErrorBoundary", () => {
102
107
  return _jsx("div", {});
103
108
  };
104
109
  }
110
+ function InnerBoundary() {
111
+ const errorState = useCatchError();
112
+ return () => errorState.error ? (_jsxs("div", { children: ["Inner: ", String(errorState.error)] })) : (_jsx(ThrowingChild, {}));
113
+ }
105
114
  function TestComponent() {
106
- return () => (_jsx(ErrorBoundary, { error: (error) => _jsxs("div", { children: ["Outer: ", String(error)] }), children: _jsx(ErrorBoundary, { error: (error) => _jsxs("div", { children: ["Inner: ", String(error)] }), children: _jsx(ThrowingChild, {}) }) }));
115
+ const errorState = useCatchError();
116
+ return () => errorState.error ? (_jsxs("div", { children: ["Outer: ", String(errorState.error)] })) : (_jsx(InnerBoundary, {}));
107
117
  }
108
118
  const container = document.createElement("div");
109
119
  document.body.appendChild(container);
@@ -122,7 +132,8 @@ describe("ErrorBoundary", () => {
122
132
  };
123
133
  }
124
134
  function TestComponent() {
125
- return () => (_jsx(ErrorBoundary, { error: (error) => _jsxs("div", { children: ["Error: ", String(error)] }), children: _jsx(ThrowingChild, {}) }));
135
+ const errorState = useCatchError();
136
+ return () => errorState.error ? (_jsxs("div", { children: ["Error: ", String(errorState.error)] })) : (_jsx(ThrowingChild, {}));
126
137
  }
127
138
  const container = document.createElement("div");
128
139
  document.body.appendChild(container);
@@ -139,7 +150,8 @@ describe("ErrorBoundary", () => {
139
150
  };
140
151
  }
141
152
  function TestComponent() {
142
- return () => (_jsx(ErrorBoundary, { error: (error) => (_jsxs("div", { children: ["Error: ", error.message, " (Code: ", error.code, ")"] })), children: _jsx(ThrowingChild, {}) }));
153
+ const errorState = useCatchError();
154
+ return () => errorState.error ? (_jsxs("div", { children: ["Error: ", errorState.error.message, " (Code:", " ", errorState.error.code, ")"] })) : (_jsx(ThrowingChild, {}));
143
155
  }
144
156
  const container = document.createElement("div");
145
157
  document.body.appendChild(container);
@@ -156,7 +168,8 @@ describe("ErrorBoundary", () => {
156
168
  return () => _jsx("div", { children: "Safe content" });
157
169
  }
158
170
  function TestComponent() {
159
- return () => (_jsx(ErrorBoundary, { error: (error) => _jsxs("div", { children: ["Error: ", String(error)] }), children: _jsx(SafeChild, {}) }));
171
+ const errorState = useCatchError();
172
+ return () => errorState.error ? (_jsxs("div", { children: ["Error: ", String(errorState.error)] })) : (_jsx(SafeChild, {}));
160
173
  }
161
174
  const container = document.createElement("div");
162
175
  document.body.appendChild(container);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=renderCount.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderCount.test.d.ts","sourceRoot":"","sources":["../../src/tests/renderCount.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,95 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "rask-ui/jsx-runtime";
2
+ import { describe, it, expect } from "vitest";
3
+ import { useState } from "../useState";
4
+ import { render } from "../";
5
+ describe("Child render count test", () => {
6
+ it("should track how many times Child renders when count changes", async () => {
7
+ let childRenderCount = 0;
8
+ let state;
9
+ function Child(props) {
10
+ return () => {
11
+ childRenderCount++;
12
+ return (_jsxs("div", { children: ["State count: ", props.state.count, ", Props count: ", props.count] }));
13
+ };
14
+ }
15
+ function MyComp() {
16
+ state = useState({
17
+ count: 0,
18
+ });
19
+ return () => (_jsx("div", { children: _jsx(Child, { state: state, count: state.count }) }));
20
+ }
21
+ const container = document.createElement("div");
22
+ render(_jsx(MyComp, {}), container);
23
+ // Initial render
24
+ expect(childRenderCount).toBe(1);
25
+ expect(container.textContent).toBe("State count: 0, Props count: 0");
26
+ // Simulate click to increment count
27
+ state.count++;
28
+ await new Promise((resolve) => setTimeout(resolve, 10));
29
+ // After first increment
30
+ expect(childRenderCount).toBe(2);
31
+ expect(container.textContent).toBe("State count: 1, Props count: 1");
32
+ state.count++;
33
+ await new Promise((resolve) => setTimeout(resolve, 10));
34
+ // After second increment
35
+ expect(childRenderCount).toBe(3);
36
+ expect(container.textContent).toBe("State count: 2, Props count: 2");
37
+ });
38
+ it("should show Child renders only once when only state prop changes", async () => {
39
+ let childRenderCount = 0;
40
+ let state;
41
+ function Child(props) {
42
+ childRenderCount++;
43
+ return () => _jsxs("div", { children: ["State count: ", props.state.count] });
44
+ }
45
+ function MyComp() {
46
+ state = useState({
47
+ count: 0,
48
+ });
49
+ return () => (_jsx("div", { children: _jsx(Child, { state: state }) }));
50
+ }
51
+ const container = document.createElement("div");
52
+ render(_jsx(MyComp, {}), container);
53
+ // Initial render
54
+ expect(childRenderCount).toBe(1);
55
+ expect(container.textContent).toBe("State count: 0");
56
+ // Simulate click to increment count
57
+ state.count++;
58
+ await new Promise((resolve) => setTimeout(resolve, 10));
59
+ // Child shouldn't re-render because state object reference didn't change
60
+ expect(childRenderCount).toBe(1);
61
+ expect(container.textContent).toBe("State count: 1");
62
+ });
63
+ it("should show Child renders when primitive prop changes", async () => {
64
+ let childRenderCount = 0;
65
+ let state;
66
+ function Child(props) {
67
+ return () => {
68
+ childRenderCount++;
69
+ return _jsxs("div", { children: ["Props count: ", props.count] });
70
+ };
71
+ }
72
+ function MyComp() {
73
+ state = useState({
74
+ count: 0,
75
+ });
76
+ return () => (_jsx("div", { children: _jsx(Child, { count: state.count }) }));
77
+ }
78
+ const container = document.createElement("div");
79
+ render(_jsx(MyComp, {}), container);
80
+ // Initial render
81
+ expect(childRenderCount).toBe(1);
82
+ expect(container.textContent).toBe("Props count: 0");
83
+ // Simulate click to increment count
84
+ state.count++;
85
+ await new Promise((resolve) => setTimeout(resolve, 10));
86
+ // Child re-renders because count prop changed
87
+ expect(childRenderCount).toBe(2);
88
+ expect(container.textContent).toBe("Props count: 1");
89
+ // Another click
90
+ state.count++;
91
+ await new Promise((resolve) => setTimeout(resolve, 10));
92
+ expect(childRenderCount).toBe(3);
93
+ expect(container.textContent).toBe("Props count: 2");
94
+ });
95
+ });