rask-ui 0.28.2 → 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 (43) hide show
  1. package/dist/component.d.ts +1 -0
  2. package/dist/component.d.ts.map +1 -1
  3. package/dist/component.js +7 -2
  4. package/dist/tests/batch.test.js +202 -12
  5. package/dist/tests/createContext.test.js +50 -37
  6. package/dist/tests/error.test.js +25 -12
  7. package/dist/tests/renderCount.test.d.ts +2 -0
  8. package/dist/tests/renderCount.test.d.ts.map +1 -0
  9. package/dist/tests/renderCount.test.js +95 -0
  10. package/dist/tests/scopeEnforcement.test.d.ts +2 -0
  11. package/dist/tests/scopeEnforcement.test.d.ts.map +1 -0
  12. package/dist/tests/scopeEnforcement.test.js +157 -0
  13. package/dist/tests/useAction.test.d.ts +2 -0
  14. package/dist/tests/useAction.test.d.ts.map +1 -0
  15. package/dist/tests/useAction.test.js +132 -0
  16. package/dist/tests/useAsync.test.d.ts +2 -0
  17. package/dist/tests/useAsync.test.d.ts.map +1 -0
  18. package/dist/tests/useAsync.test.js +499 -0
  19. package/dist/tests/useDerived.test.d.ts +2 -0
  20. package/dist/tests/useDerived.test.d.ts.map +1 -0
  21. package/dist/tests/useDerived.test.js +407 -0
  22. package/dist/tests/useEffect.test.d.ts +2 -0
  23. package/dist/tests/useEffect.test.d.ts.map +1 -0
  24. package/dist/tests/useEffect.test.js +600 -0
  25. package/dist/tests/useLookup.test.d.ts +2 -0
  26. package/dist/tests/useLookup.test.d.ts.map +1 -0
  27. package/dist/tests/useLookup.test.js +299 -0
  28. package/dist/tests/useRef.test.d.ts +2 -0
  29. package/dist/tests/useRef.test.d.ts.map +1 -0
  30. package/dist/tests/useRef.test.js +189 -0
  31. package/dist/tests/useState.test.d.ts +2 -0
  32. package/dist/tests/useState.test.d.ts.map +1 -0
  33. package/dist/tests/useState.test.js +178 -0
  34. package/dist/tests/useSuspend.test.d.ts +2 -0
  35. package/dist/tests/useSuspend.test.d.ts.map +1 -0
  36. package/dist/tests/useSuspend.test.js +752 -0
  37. package/dist/tests/useView.test.d.ts +2 -0
  38. package/dist/tests/useView.test.d.ts.map +1 -0
  39. package/dist/tests/useView.test.js +305 -0
  40. package/dist/transformer.d.ts.map +1 -1
  41. package/dist/transformer.js +1 -5
  42. package/dist/useState.js +4 -2
  43. package/package.json +1 -1
@@ -0,0 +1,299 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "rask-ui/jsx-runtime";
2
+ import { describe, it, expect } from "vitest";
3
+ import { useLookup } from "../useLookup";
4
+ import { useState } from "../useState";
5
+ import { render } from "../render";
6
+ describe("useLookup", () => {
7
+ it("should create a lookup function from array", () => {
8
+ let lookupResult1;
9
+ let lookupResult2;
10
+ let lookupResult3;
11
+ function Component() {
12
+ const state = useState({
13
+ items: [
14
+ { id: 1, name: "Alice" },
15
+ { id: 2, name: "Bob" },
16
+ ],
17
+ });
18
+ const lookup = useLookup(() => state.items, "id");
19
+ return () => {
20
+ lookupResult1 = lookup(1);
21
+ lookupResult2 = lookup(2);
22
+ lookupResult3 = lookup(3);
23
+ return _jsx("div", { children: "test" });
24
+ };
25
+ }
26
+ const container = document.createElement("div");
27
+ render(_jsx(Component, {}), container);
28
+ expect(lookupResult1).toEqual({ id: 1, name: "Alice" });
29
+ expect(lookupResult2).toEqual({ id: 2, name: "Bob" });
30
+ expect(lookupResult3).toBeUndefined();
31
+ });
32
+ it("should update lookup when useState array is mutated via push", async () => {
33
+ let lookupResult;
34
+ let state;
35
+ function Component() {
36
+ state = useState({ items: [{ id: 1, name: "Alice" }] });
37
+ const lookup = useLookup(() => state.items, "id");
38
+ return () => {
39
+ lookupResult = lookup(2);
40
+ return (_jsxs("div", { children: [lookup(1)?.name, " - ", lookup(2)?.name] }));
41
+ };
42
+ }
43
+ const container = document.createElement("div");
44
+ render(_jsx(Component, {}), container);
45
+ expect(lookupResult).toBeUndefined();
46
+ expect(container.textContent).toBe("Alice - ");
47
+ // Mutate the array by pushing
48
+ state.items.push({ id: 2, name: "Bob" });
49
+ await new Promise((resolve) => setTimeout(resolve, 0));
50
+ expect(lookupResult).toEqual({ id: 2, name: "Bob" });
51
+ expect(container.textContent).toBe("Alice - Bob");
52
+ });
53
+ it("should update lookup when useState array is mutated via pop", async () => {
54
+ let state;
55
+ function Component() {
56
+ state = useState({
57
+ items: [
58
+ { id: 1, name: "Alice" },
59
+ { id: 2, name: "Bob" },
60
+ ],
61
+ });
62
+ const lookup = useLookup(() => state.items, "id");
63
+ return () => {
64
+ return (_jsxs("div", { children: [lookup(1)?.name, " - ", lookup(2)?.name] }));
65
+ };
66
+ }
67
+ const container = document.createElement("div");
68
+ render(_jsx(Component, {}), container);
69
+ expect(container.textContent).toBe("Alice - Bob");
70
+ // Mutate the array by popping
71
+ state.items.pop();
72
+ await new Promise((resolve) => setTimeout(resolve, 0));
73
+ expect(container.textContent).toBe("Alice - ");
74
+ });
75
+ it("should update lookup when useState array is mutated via splice", async () => {
76
+ let state;
77
+ function Component() {
78
+ state = useState({
79
+ items: [
80
+ { id: 1, name: "Alice" },
81
+ { id: 2, name: "Bob" },
82
+ { id: 3, name: "Charlie" },
83
+ ],
84
+ });
85
+ const lookup = useLookup(() => state.items, "id");
86
+ return () => {
87
+ return (_jsxs("div", { children: [lookup(1)?.name, " - ", lookup(2)?.name, " - ", lookup(3)?.name] }));
88
+ };
89
+ }
90
+ const container = document.createElement("div");
91
+ render(_jsx(Component, {}), container);
92
+ expect(container.textContent).toBe("Alice - Bob - Charlie");
93
+ // Remove Bob via splice
94
+ state.items.splice(1, 1);
95
+ await new Promise((resolve) => setTimeout(resolve, 0));
96
+ expect(container.textContent).toBe("Alice - - Charlie");
97
+ });
98
+ it("should update lookup when useState array is mutated via unshift", async () => {
99
+ let state;
100
+ function Component() {
101
+ state = useState({ items: [{ id: 2, name: "Bob" }] });
102
+ const lookup = useLookup(() => state.items, "id");
103
+ return () => {
104
+ return (_jsxs("div", { children: [lookup(1)?.name, " - ", lookup(2)?.name] }));
105
+ };
106
+ }
107
+ const container = document.createElement("div");
108
+ render(_jsx(Component, {}), container);
109
+ expect(container.textContent).toBe(" - Bob");
110
+ // Add item at beginning
111
+ state.items.unshift({ id: 1, name: "Alice" });
112
+ await new Promise((resolve) => setTimeout(resolve, 0));
113
+ expect(container.textContent).toBe("Alice - Bob");
114
+ });
115
+ it("should update lookup when array reference is replaced", async () => {
116
+ let state;
117
+ function Component() {
118
+ state = useState({
119
+ items: [
120
+ { id: 1, name: "Alice" },
121
+ { id: 2, name: "Bob" },
122
+ ],
123
+ });
124
+ const lookup = useLookup(() => state.items, "id");
125
+ return () => {
126
+ return (_jsxs("div", { children: [lookup(1)?.name, " - ", lookup(3)?.name, " - ", lookup(4)?.name] }));
127
+ };
128
+ }
129
+ const container = document.createElement("div");
130
+ render(_jsx(Component, {}), container);
131
+ expect(container.textContent).toBe("Alice - - ");
132
+ // Replace the entire array
133
+ state.items = [
134
+ { id: 3, name: "Charlie" },
135
+ { id: 4, name: "Diana" },
136
+ ];
137
+ await new Promise((resolve) => setTimeout(resolve, 0));
138
+ expect(container.textContent).toBe(" - Charlie - Diana");
139
+ });
140
+ it("should handle mutations when items are modified", async () => {
141
+ let state;
142
+ function Component() {
143
+ state = useState({
144
+ items: [
145
+ { id: 1, name: "Alice" },
146
+ { id: 2, name: "Bob" },
147
+ ],
148
+ });
149
+ const lookup = useLookup(() => state.items, "id");
150
+ return () => {
151
+ return _jsx("div", { children: lookup(1)?.name });
152
+ };
153
+ }
154
+ const container = document.createElement("div");
155
+ render(_jsx(Component, {}), container);
156
+ expect(container.textContent).toBe("Alice");
157
+ // Modify Alice's name (this doesn't change the lookup key)
158
+ state.items[0].name = "Alicia";
159
+ await new Promise((resolve) => setTimeout(resolve, 0));
160
+ // The lookup should still find the same object, now with modified name
161
+ expect(container.textContent).toBe("Alicia");
162
+ });
163
+ it("should update lookup when item key is changed", async () => {
164
+ let state;
165
+ function Component() {
166
+ state = useState({
167
+ items: [
168
+ { id: 1, name: "Alice" },
169
+ { id: 2, name: "Bob" },
170
+ ],
171
+ });
172
+ const lookup = useLookup(() => state.items, "id");
173
+ return () => {
174
+ return (_jsxs("div", { children: [lookup(1)?.name, " - ", lookup(5)?.name] }));
175
+ };
176
+ }
177
+ const container = document.createElement("div");
178
+ render(_jsx(Component, {}), container);
179
+ expect(container.textContent).toBe("Alice - ");
180
+ // Change Alice's id
181
+ state.items[0].id = 5;
182
+ await new Promise((resolve) => setTimeout(resolve, 0));
183
+ // Old id should not be found, new id should be found
184
+ expect(container.textContent).toBe(" - Alice");
185
+ });
186
+ it("should work with string keys", async () => {
187
+ let state;
188
+ function Component() {
189
+ state = useState({
190
+ users: [
191
+ { username: "alice", email: "alice@example.com" },
192
+ { username: "bob", email: "bob@example.com" },
193
+ ],
194
+ });
195
+ const lookup = useLookup(() => state.users, "username");
196
+ return () => {
197
+ return (_jsxs("div", { children: [lookup("alice")?.email, " - ", lookup("charlie")?.email] }));
198
+ };
199
+ }
200
+ const container = document.createElement("div");
201
+ render(_jsx(Component, {}), container);
202
+ expect(container.textContent).toBe("alice@example.com - ");
203
+ // Add new user
204
+ state.users.push({ username: "charlie", email: "charlie@example.com" });
205
+ await new Promise((resolve) => setTimeout(resolve, 0));
206
+ expect(container.textContent).toBe("alice@example.com - charlie@example.com");
207
+ });
208
+ it("should handle array with duplicate keys (last one wins)", () => {
209
+ let lookupResult;
210
+ function Component() {
211
+ const state = useState({
212
+ items: [
213
+ { id: 1, name: "Alice" },
214
+ { id: 2, name: "Bob" },
215
+ { id: 1, name: "Alice2" }, // Duplicate id
216
+ ],
217
+ });
218
+ const lookup = useLookup(() => state.items, "id");
219
+ return () => {
220
+ lookupResult = lookup(1);
221
+ return _jsx("div", { children: lookup(1)?.name });
222
+ };
223
+ }
224
+ const container = document.createElement("div");
225
+ render(_jsx(Component, {}), container);
226
+ // Last item with id=1 should win
227
+ expect(lookupResult).toEqual({ id: 1, name: "Alice2" });
228
+ expect(container.textContent).toBe("Alice2");
229
+ });
230
+ it("should handle empty array", async () => {
231
+ let state;
232
+ function Component() {
233
+ state = useState({ items: [] });
234
+ const lookup = useLookup(() => state.items, "id");
235
+ return () => {
236
+ return _jsx("div", { children: lookup(1)?.name || "empty" });
237
+ };
238
+ }
239
+ const container = document.createElement("div");
240
+ render(_jsx(Component, {}), container);
241
+ expect(container.textContent).toBe("empty");
242
+ // Add item to empty array
243
+ state.items.push({ id: 1, name: "Alice" });
244
+ await new Promise((resolve) => setTimeout(resolve, 0));
245
+ expect(container.textContent).toBe("Alice");
246
+ });
247
+ it("should handle multiple array mutations in sequence", async () => {
248
+ let state;
249
+ function Component() {
250
+ state = useState({
251
+ items: [{ id: 1, name: "Alice" }],
252
+ });
253
+ const lookup = useLookup(() => state.items, "id");
254
+ return () => {
255
+ return (_jsx("div", { children: state.items.map((item) => lookup(item.id)?.name).join(", ") }));
256
+ };
257
+ }
258
+ const container = document.createElement("div");
259
+ render(_jsx(Component, {}), container);
260
+ expect(container.textContent).toBe("Alice");
261
+ // Multiple mutations
262
+ state.items.push({ id: 2, name: "Bob" });
263
+ await new Promise((resolve) => setTimeout(resolve, 0));
264
+ expect(container.textContent).toBe("Alice, Bob");
265
+ state.items.push({ id: 3, name: "Charlie" });
266
+ await new Promise((resolve) => setTimeout(resolve, 0));
267
+ expect(container.textContent).toBe("Alice, Bob, Charlie");
268
+ state.items.shift();
269
+ await new Promise((resolve) => setTimeout(resolve, 0));
270
+ expect(container.textContent).toBe("Bob, Charlie");
271
+ state.items[0].name = "Robert";
272
+ await new Promise((resolve) => setTimeout(resolve, 0));
273
+ expect(container.textContent).toBe("Robert, Charlie");
274
+ });
275
+ it("should handle lookup in nested component", async () => {
276
+ let parentState;
277
+ function Child(props) {
278
+ const lookup = useLookup(() => props.items, "id");
279
+ return () => {
280
+ console.log("Render Child");
281
+ return _jsx("div", { children: lookup(2)?.name || "not found" });
282
+ };
283
+ }
284
+ function Parent() {
285
+ parentState = useState({
286
+ items: [{ id: 1, name: "Alice" }],
287
+ });
288
+ return () => {
289
+ return _jsx(Child, { items: parentState.items });
290
+ };
291
+ }
292
+ const container = document.createElement("div");
293
+ render(_jsx(Parent, {}), container);
294
+ expect(container.textContent).toBe("not found");
295
+ parentState.items.push({ id: 2, name: "Bob" });
296
+ await new Promise((resolve) => setTimeout(resolve, 0));
297
+ expect(container.textContent).toBe("Bob");
298
+ });
299
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=useRef.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useRef.test.d.ts","sourceRoot":"","sources":["../../src/tests/useRef.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,189 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "rask-ui/jsx-runtime";
2
+ import { describe, it, expect, vi } from "vitest";
3
+ import { useRef } from "../useRef";
4
+ import { useEffect } from "../useEffect";
5
+ import { render } from "../";
6
+ describe("useRef", () => {
7
+ it("should create a ref object with current property", () => {
8
+ function Component() {
9
+ const ref = useRef();
10
+ expect(ref).toHaveProperty("current");
11
+ return () => _jsx("div", {});
12
+ }
13
+ const container = document.createElement("div");
14
+ document.body.appendChild(container);
15
+ render(_jsx(Component, {}), container);
16
+ document.body.removeChild(container);
17
+ });
18
+ it("should allow setting and getting ref values", () => {
19
+ function Component() {
20
+ const ref = useRef();
21
+ ref.current = document.createElement("div");
22
+ expect(ref.current).toBeInstanceOf(HTMLDivElement);
23
+ return () => _jsx("div", {});
24
+ }
25
+ const container = document.createElement("div");
26
+ document.body.appendChild(container);
27
+ render(_jsx(Component, {}), container);
28
+ document.body.removeChild(container);
29
+ });
30
+ it("should have initial value of null", () => {
31
+ function Component() {
32
+ const ref = useRef();
33
+ expect(ref.current).toBe(null);
34
+ return () => _jsx("div", {});
35
+ }
36
+ const container = document.createElement("div");
37
+ document.body.appendChild(container);
38
+ render(_jsx(Component, {}), container);
39
+ document.body.removeChild(container);
40
+ });
41
+ it("should allow ref to be assigned to DOM elements", () => {
42
+ let capturedRef;
43
+ function Component() {
44
+ const ref = useRef();
45
+ capturedRef = ref;
46
+ return () => _jsx("div", { ref: ref, children: "Content" });
47
+ }
48
+ const container = document.createElement("div");
49
+ document.body.appendChild(container);
50
+ render(_jsx(Component, {}), container);
51
+ // After render, the ref should be assigned to the div element
52
+ expect(capturedRef.current).toBeInstanceOf(HTMLDivElement);
53
+ expect(capturedRef.current.textContent).toBe("Content");
54
+ document.body.removeChild(container);
55
+ });
56
+ it("should trigger useEffect when ref is assigned to an element", async () => {
57
+ const effectCallback = vi.fn();
58
+ function Component() {
59
+ const ref = useRef();
60
+ useEffect(() => {
61
+ // Reading ref.current subscribes the effect to the signal
62
+ const element = ref.current;
63
+ effectCallback(element);
64
+ });
65
+ return () => _jsx("div", { ref: ref, children: "Content" });
66
+ }
67
+ const container = document.createElement("div");
68
+ document.body.appendChild(container);
69
+ render(_jsx(Component, {}), container);
70
+ // Wait for microtask queue to process signal notifications
71
+ await new Promise((resolve) => queueMicrotask(resolve));
72
+ // Effect should run twice:
73
+ // 1. Initially when effect is created (ref.current is null)
74
+ // 2. After the element is mounted and assigned to ref.current
75
+ expect(effectCallback).toHaveBeenCalledTimes(2);
76
+ expect(effectCallback).toHaveBeenNthCalledWith(1, null);
77
+ expect(effectCallback).toHaveBeenNthCalledWith(2, expect.any(HTMLDivElement));
78
+ document.body.removeChild(container);
79
+ });
80
+ it("should re-run effect when ref value changes", async () => {
81
+ const effectCallback = vi.fn();
82
+ let updateRef;
83
+ function Component() {
84
+ const ref = useRef();
85
+ useEffect(() => {
86
+ // Reading ref.current subscribes the effect to changes
87
+ const element = ref.current;
88
+ effectCallback(element);
89
+ });
90
+ updateRef = (el) => {
91
+ ref.current = el;
92
+ };
93
+ return () => _jsx("div", { children: "Content" });
94
+ }
95
+ const container = document.createElement("div");
96
+ document.body.appendChild(container);
97
+ render(_jsx(Component, {}), container);
98
+ await new Promise((resolve) => queueMicrotask(resolve));
99
+ // Initial effect run
100
+ expect(effectCallback).toHaveBeenCalledTimes(1);
101
+ expect(effectCallback).toHaveBeenNthCalledWith(1, null);
102
+ effectCallback.mockClear();
103
+ // Manually set the ref to an element
104
+ const testEl = document.createElement("div");
105
+ updateRef?.(testEl);
106
+ await new Promise((resolve) => queueMicrotask(resolve));
107
+ // Effect should run again with the new element
108
+ expect(effectCallback).toHaveBeenCalledTimes(1);
109
+ expect(effectCallback).toHaveBeenCalledWith(testEl);
110
+ effectCallback.mockClear();
111
+ // Change ref to null
112
+ updateRef?.(null);
113
+ await new Promise((resolve) => queueMicrotask(resolve));
114
+ // Effect should run again with null
115
+ expect(effectCallback).toHaveBeenCalledTimes(1);
116
+ expect(effectCallback).toHaveBeenCalledWith(null);
117
+ document.body.removeChild(container);
118
+ });
119
+ it("should work with multiple refs in the same effect", async () => {
120
+ const effectCallback = vi.fn();
121
+ function Component() {
122
+ const ref1 = useRef();
123
+ const ref2 = useRef();
124
+ useEffect(() => {
125
+ // Reading both refs subscribes to both signals
126
+ effectCallback({
127
+ ref1: ref1.current,
128
+ ref2: ref2.current,
129
+ });
130
+ });
131
+ return () => (_jsxs("div", { children: [_jsx("div", { ref: ref1, children: "Div" }), _jsx("span", { ref: ref2, children: "Span" })] }));
132
+ }
133
+ const container = document.createElement("div");
134
+ document.body.appendChild(container);
135
+ render(_jsx(Component, {}), container);
136
+ await new Promise((resolve) => queueMicrotask(resolve));
137
+ // Effect should be called:
138
+ // 1. Initially (both refs null)
139
+ // 2. When ref1 is assigned
140
+ // 3. When ref2 is assigned
141
+ expect(effectCallback.mock.calls.length).toBeGreaterThanOrEqual(2);
142
+ // Last call should have both refs assigned
143
+ const lastCall = effectCallback.mock.calls[effectCallback.mock.calls.length - 1][0];
144
+ expect(lastCall.ref1).toBeInstanceOf(HTMLDivElement);
145
+ expect(lastCall.ref2).toBeInstanceOf(HTMLSpanElement);
146
+ document.body.removeChild(container);
147
+ });
148
+ it("should only notify when value is set, not when read", async () => {
149
+ const effectCallback = vi.fn();
150
+ let readRef;
151
+ function Component() {
152
+ const ref = useRef();
153
+ useEffect(() => {
154
+ const element = ref.current;
155
+ effectCallback(element);
156
+ });
157
+ readRef = () => {
158
+ // Just reading should not trigger a notification
159
+ const _ = ref.current;
160
+ };
161
+ return () => _jsx("div", { children: "Content" });
162
+ }
163
+ const container = document.createElement("div");
164
+ document.body.appendChild(container);
165
+ render(_jsx(Component, {}), container);
166
+ await new Promise((resolve) => queueMicrotask(resolve));
167
+ const initialCallCount = effectCallback.mock.calls.length;
168
+ // Reading the ref should not cause effect to re-run
169
+ readRef?.();
170
+ readRef?.();
171
+ readRef?.();
172
+ await new Promise((resolve) => queueMicrotask(resolve));
173
+ expect(effectCallback).toHaveBeenCalledTimes(initialCallCount);
174
+ document.body.removeChild(container);
175
+ });
176
+ it("should work with callback refs", () => {
177
+ const refCallback = vi.fn();
178
+ function Component() {
179
+ return () => _jsx("div", { ref: refCallback, children: "Content" });
180
+ }
181
+ const container = document.createElement("div");
182
+ document.body.appendChild(container);
183
+ render(_jsx(Component, {}), container);
184
+ // Callback ref should be called with the element
185
+ expect(refCallback).toHaveBeenCalledWith(expect.any(HTMLDivElement));
186
+ expect(refCallback.mock.calls[0][0].textContent).toBe("Content");
187
+ document.body.removeChild(container);
188
+ });
189
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=useState.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useState.test.d.ts","sourceRoot":"","sources":["../../src/tests/useState.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,178 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "rask-ui/jsx-runtime";
2
+ import { describe, it, expect } from "vitest";
3
+ import { useState } from "../useState";
4
+ import { Observer } from "../observation";
5
+ import { render } from "../render";
6
+ describe("useState", () => {
7
+ it("should create a reactive proxy from an object", () => {
8
+ const state = useState({ count: 0 });
9
+ expect(state.count).toBe(0);
10
+ });
11
+ it("should allow mutations", () => {
12
+ const state = useState({ count: 0 });
13
+ state.count = 5;
14
+ expect(state.count).toBe(5);
15
+ });
16
+ it("should return the same proxy for the same object", () => {
17
+ const obj = { count: 0 };
18
+ const proxy1 = useState(obj);
19
+ const proxy2 = useState(obj);
20
+ expect(proxy1).toBe(proxy2);
21
+ });
22
+ it("should create nested proxies for nested objects", () => {
23
+ const state = useState({ user: { name: "Alice", age: 30 } });
24
+ state.user.name = "Bob";
25
+ expect(state.user.name).toBe("Bob");
26
+ });
27
+ it("should handle arrays reactively", () => {
28
+ const state = useState({ items: [1, 2, 3] });
29
+ state.items.push(4);
30
+ expect(state.items).toEqual([1, 2, 3, 4]);
31
+ });
32
+ it("should track property access in observers", async () => {
33
+ const state = useState({ count: 0 });
34
+ let renderCount = 0;
35
+ const observer = new Observer(() => {
36
+ renderCount++;
37
+ });
38
+ const dispose = observer.observe();
39
+ state.count; // Access property to track it
40
+ dispose();
41
+ expect(renderCount).toBe(0);
42
+ // Mutate after observation setup
43
+ const dispose2 = observer.observe();
44
+ const value = state.count; // Track
45
+ dispose2(); // Stop observing, subscriptions are now active
46
+ state.count = 1;
47
+ // Wait for microtasks to complete
48
+ await new Promise((resolve) => setTimeout(resolve, 0));
49
+ expect(renderCount).toBeGreaterThan(0);
50
+ });
51
+ it("should handle property deletion", () => {
52
+ const state = useState({ count: 0, temp: "value" });
53
+ delete state.temp;
54
+ expect(state.temp).toBeUndefined();
55
+ expect("temp" in state).toBe(false);
56
+ });
57
+ it("should not create proxies for functions", () => {
58
+ const fn = () => "hello";
59
+ const state = useState({ method: fn });
60
+ expect(state.method).toBe(fn);
61
+ expect(state.method()).toBe("hello");
62
+ });
63
+ it("should handle symbol properties", () => {
64
+ const sym = Symbol("test");
65
+ const state = useState({ [sym]: "value" });
66
+ expect(state[sym]).toBe("value");
67
+ });
68
+ it("should notify observers only on actual changes", async () => {
69
+ const state = useState({ count: 0 });
70
+ let notifyCount = 0;
71
+ const observer = new Observer(() => {
72
+ notifyCount++;
73
+ });
74
+ const dispose = observer.observe();
75
+ state.count; // Track
76
+ dispose();
77
+ state.count = 0; // Same value - should still notify per current implementation
78
+ state.count = 0;
79
+ await new Promise((resolve) => setTimeout(resolve, 0));
80
+ // The implementation notifies even for same value, except for optimization cases
81
+ observer.dispose();
82
+ });
83
+ it("should handle deeply nested objects", () => {
84
+ const state = useState({
85
+ level1: {
86
+ level2: {
87
+ level3: {
88
+ value: "deep",
89
+ },
90
+ },
91
+ },
92
+ });
93
+ state.level1.level2.level3.value = "modified";
94
+ expect(state.level1.level2.level3.value).toBe("modified");
95
+ });
96
+ it("should handle array mutations correctly", () => {
97
+ const state = useState({ items: [1, 2, 3] });
98
+ state.items.pop();
99
+ expect(state.items).toEqual([1, 2]);
100
+ state.items.unshift(0);
101
+ expect(state.items).toEqual([0, 1, 2]);
102
+ state.items.splice(1, 1, 99);
103
+ expect(state.items).toEqual([0, 99, 2]);
104
+ });
105
+ it("should cache proxies for array elements to prevent double-wrapping", () => {
106
+ const state = useState({
107
+ data: [
108
+ { id: 1, label: "Item 1" },
109
+ { id: 2, label: "Item 2" },
110
+ { id: 3, label: "Item 3" },
111
+ ],
112
+ });
113
+ // Access the same array element multiple times
114
+ const firstAccess = state.data[0];
115
+ const secondAccess = state.data[0];
116
+ // Should return the exact same proxy reference
117
+ expect(firstAccess).toBe(secondAccess);
118
+ // Test with array iteration methods
119
+ const mapped = state.data.map((item) => item);
120
+ const firstItem = mapped[0];
121
+ const directAccess = state.data[0];
122
+ // The proxy returned from iteration should be the same as direct access
123
+ expect(firstItem).toBe(directAccess);
124
+ // Test that we don't double-wrap when iterating multiple times
125
+ const mapped2 = state.data.map((item) => item);
126
+ expect(mapped[0]).toBe(mapped2[0]);
127
+ });
128
+ it("should maintain proxy identity after filter operations", () => {
129
+ const state = useState({
130
+ data: [
131
+ { id: 1, label: "Item 1" },
132
+ { id: 2, label: "Item 2" },
133
+ { id: 3, label: "Item 3" },
134
+ ],
135
+ });
136
+ // Get reference to an item before filtering
137
+ const originalItem = state.data[0];
138
+ // Simulate the remove operation: filter creates a new array but reuses proxies
139
+ state.data = state.data.filter((row) => row.id !== 2);
140
+ // After filter, the first item should still be the same proxy reference
141
+ const afterFilter = state.data[0];
142
+ expect(afterFilter).toBe(originalItem);
143
+ // And accessing it multiple times should return the same reference
144
+ expect(state.data[0]).toBe(state.data[0]);
145
+ });
146
+ it("should rerender child when array prop changes", async () => {
147
+ let childRenderCount = 0;
148
+ let parentState;
149
+ function Child(props) {
150
+ return () => {
151
+ childRenderCount++;
152
+ return (_jsxs("div", { children: [_jsx("div", { class: "count", children: props.todos.length }), _jsx("ul", { children: props.todos.map((todo) => (_jsx("li", { children: todo.text }, todo.id))) })] }));
153
+ };
154
+ }
155
+ function Parent() {
156
+ parentState = useState({
157
+ todos: [
158
+ { id: "1", text: "Task 1", completed: false },
159
+ { id: "2", text: "Task 2", completed: true },
160
+ { id: "3", text: "Task 3", completed: false },
161
+ ],
162
+ filter: "all",
163
+ });
164
+ return () => {
165
+ const filtered = parentState.filter === "active"
166
+ ? parentState.todos.filter((t) => !t.completed)
167
+ : parentState.todos;
168
+ return _jsx(Child, { todos: filtered });
169
+ };
170
+ }
171
+ const container = document.createElement("div");
172
+ render(_jsx(Parent, {}), container);
173
+ expect(childRenderCount).toBe(1);
174
+ parentState.filter = "active";
175
+ await new Promise((resolve) => setTimeout(resolve, 10));
176
+ expect(childRenderCount).toBe(2);
177
+ });
178
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=useSuspend.test.d.ts.map