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.
- package/dist/component.d.ts +1 -0
- package/dist/component.d.ts.map +1 -1
- package/dist/component.js +7 -2
- package/dist/tests/batch.test.js +202 -12
- package/dist/tests/createContext.test.js +50 -37
- package/dist/tests/error.test.js +25 -12
- package/dist/tests/renderCount.test.d.ts +2 -0
- package/dist/tests/renderCount.test.d.ts.map +1 -0
- package/dist/tests/renderCount.test.js +95 -0
- package/dist/tests/scopeEnforcement.test.d.ts +2 -0
- package/dist/tests/scopeEnforcement.test.d.ts.map +1 -0
- package/dist/tests/scopeEnforcement.test.js +157 -0
- package/dist/tests/useAction.test.d.ts +2 -0
- package/dist/tests/useAction.test.d.ts.map +1 -0
- package/dist/tests/useAction.test.js +132 -0
- package/dist/tests/useAsync.test.d.ts +2 -0
- package/dist/tests/useAsync.test.d.ts.map +1 -0
- package/dist/tests/useAsync.test.js +499 -0
- package/dist/tests/useDerived.test.d.ts +2 -0
- package/dist/tests/useDerived.test.d.ts.map +1 -0
- package/dist/tests/useDerived.test.js +407 -0
- package/dist/tests/useEffect.test.d.ts +2 -0
- package/dist/tests/useEffect.test.d.ts.map +1 -0
- package/dist/tests/useEffect.test.js +600 -0
- package/dist/tests/useLookup.test.d.ts +2 -0
- package/dist/tests/useLookup.test.d.ts.map +1 -0
- package/dist/tests/useLookup.test.js +299 -0
- package/dist/tests/useRef.test.d.ts +2 -0
- package/dist/tests/useRef.test.d.ts.map +1 -0
- package/dist/tests/useRef.test.js +189 -0
- package/dist/tests/useState.test.d.ts +2 -0
- package/dist/tests/useState.test.d.ts.map +1 -0
- package/dist/tests/useState.test.js +178 -0
- package/dist/tests/useSuspend.test.d.ts +2 -0
- package/dist/tests/useSuspend.test.d.ts.map +1 -0
- package/dist/tests/useSuspend.test.js +752 -0
- package/dist/tests/useView.test.d.ts +2 -0
- package/dist/tests/useView.test.d.ts.map +1 -0
- package/dist/tests/useView.test.js +305 -0
- package/dist/transformer.d.ts.map +1 -1
- package/dist/transformer.js +1 -5
- package/dist/useState.js +4 -2
- 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 @@
|
|
|
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 @@
|
|
|
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
|
+
});
|