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,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
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scopeEnforcement.test.d.ts","sourceRoot":"","sources":["../../src/tests/scopeEnforcement.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,157 @@
|
|
|
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 { useEffect } from "../useEffect";
|
|
5
|
+
import { useDerived } from "../useDerived";
|
|
6
|
+
import { render } from "../index";
|
|
7
|
+
describe("Scope Enforcement", () => {
|
|
8
|
+
describe("useState", () => {
|
|
9
|
+
it("should allow useState in global scope", () => {
|
|
10
|
+
expect(() => {
|
|
11
|
+
const state = useState({ count: 0 });
|
|
12
|
+
}).not.toThrow();
|
|
13
|
+
});
|
|
14
|
+
it("should allow useState in component setup", () => {
|
|
15
|
+
function Component() {
|
|
16
|
+
const state = useState({ count: 0 });
|
|
17
|
+
return () => _jsx("div", { children: state.count });
|
|
18
|
+
}
|
|
19
|
+
const container = document.createElement("div");
|
|
20
|
+
expect(() => {
|
|
21
|
+
render(_jsx(Component, {}), container);
|
|
22
|
+
}).not.toThrow();
|
|
23
|
+
});
|
|
24
|
+
it("should throw when useState is called during render", () => {
|
|
25
|
+
function Component() {
|
|
26
|
+
const state = useState({ count: 0 });
|
|
27
|
+
return () => {
|
|
28
|
+
// This should throw - useState in render scope
|
|
29
|
+
expect(() => {
|
|
30
|
+
useState({ nested: 1 });
|
|
31
|
+
}).toThrow("useState cannot be called during render");
|
|
32
|
+
return _jsx("div", { children: state.count });
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const container = document.createElement("div");
|
|
36
|
+
render(_jsx(Component, {}), container);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe("useEffect", () => {
|
|
40
|
+
it("should throw when useEffect is called outside component setup", () => {
|
|
41
|
+
expect(() => {
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
// Effect logic
|
|
44
|
+
});
|
|
45
|
+
}).toThrow("Only use useEffect in component setup");
|
|
46
|
+
});
|
|
47
|
+
it("should allow useEffect in component setup", () => {
|
|
48
|
+
function Component() {
|
|
49
|
+
const state = useState({ count: 0 });
|
|
50
|
+
let effectRan = false;
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
effectRan = true;
|
|
53
|
+
state.count;
|
|
54
|
+
});
|
|
55
|
+
expect(effectRan).toBe(true);
|
|
56
|
+
return () => _jsx("div", { children: state.count });
|
|
57
|
+
}
|
|
58
|
+
const container = document.createElement("div");
|
|
59
|
+
expect(() => {
|
|
60
|
+
render(_jsx(Component, {}), container);
|
|
61
|
+
}).not.toThrow();
|
|
62
|
+
});
|
|
63
|
+
it("should throw when useEffect is called during render", () => {
|
|
64
|
+
function Component() {
|
|
65
|
+
const state = useState({ count: 0 });
|
|
66
|
+
return () => {
|
|
67
|
+
// This should throw - useEffect in render scope
|
|
68
|
+
expect(() => {
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
state.count;
|
|
71
|
+
});
|
|
72
|
+
}).toThrow("Only use useEffect in component setup");
|
|
73
|
+
return _jsx("div", { children: state.count });
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const container = document.createElement("div");
|
|
77
|
+
render(_jsx(Component, {}), container);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe("useDerived", () => {
|
|
81
|
+
it("should throw when useDerived is called outside component setup", () => {
|
|
82
|
+
expect(() => {
|
|
83
|
+
useDerived({
|
|
84
|
+
doubled: () => 2 * 2,
|
|
85
|
+
});
|
|
86
|
+
}).toThrow("Only use useDerived in component setup");
|
|
87
|
+
});
|
|
88
|
+
it("should allow useDerived in component setup", () => {
|
|
89
|
+
function Component() {
|
|
90
|
+
const state = useState({ count: 5 });
|
|
91
|
+
const computed = useDerived({
|
|
92
|
+
doubled: () => state.count * 2,
|
|
93
|
+
});
|
|
94
|
+
return () => _jsx("div", { children: computed.doubled });
|
|
95
|
+
}
|
|
96
|
+
const container = document.createElement("div");
|
|
97
|
+
expect(() => {
|
|
98
|
+
render(_jsx(Component, {}), container);
|
|
99
|
+
}).not.toThrow();
|
|
100
|
+
});
|
|
101
|
+
it("should throw when useDerived is called during render", () => {
|
|
102
|
+
function Component() {
|
|
103
|
+
const state = useState({ count: 0 });
|
|
104
|
+
return () => {
|
|
105
|
+
// This should throw - useDerived in render scope
|
|
106
|
+
expect(() => {
|
|
107
|
+
useDerived({
|
|
108
|
+
doubled: () => state.count * 2,
|
|
109
|
+
});
|
|
110
|
+
}).toThrow("Only use useDerived in component setup");
|
|
111
|
+
return _jsx("div", { children: state.count });
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const container = document.createElement("div");
|
|
115
|
+
render(_jsx(Component, {}), container);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
describe("Mixed scenarios", () => {
|
|
119
|
+
it("should allow all reactive primitives in setup but not in render", () => {
|
|
120
|
+
function Component() {
|
|
121
|
+
// All of these should work in setup
|
|
122
|
+
const state = useState({ count: 0 });
|
|
123
|
+
const computed = useDerived({
|
|
124
|
+
doubled: () => state.count * 2,
|
|
125
|
+
});
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
state.count;
|
|
128
|
+
});
|
|
129
|
+
return () => {
|
|
130
|
+
// None of these should work in render
|
|
131
|
+
expect(() => useState({ bad: 1 })).toThrow();
|
|
132
|
+
expect(() => useEffect(() => { })).toThrow();
|
|
133
|
+
expect(() => useDerived({ bad: () => 1 })).toThrow();
|
|
134
|
+
return _jsx("div", { children: computed.doubled });
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const container = document.createElement("div");
|
|
138
|
+
render(_jsx(Component, {}), container);
|
|
139
|
+
});
|
|
140
|
+
it("should properly enforce scope in nested renders", () => {
|
|
141
|
+
function Child() {
|
|
142
|
+
const childState = useState({ value: "child" });
|
|
143
|
+
return () => _jsx("div", { children: childState.value });
|
|
144
|
+
}
|
|
145
|
+
function Parent() {
|
|
146
|
+
const parentState = useState({ value: "parent" });
|
|
147
|
+
return () => {
|
|
148
|
+
// This should fail in render
|
|
149
|
+
expect(() => useState({ bad: 1 })).toThrow();
|
|
150
|
+
return (_jsxs("div", { children: [parentState.value, _jsx(Child, {})] }));
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const container = document.createElement("div");
|
|
154
|
+
render(_jsx(Parent, {}), container);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAction.test.d.ts","sourceRoot":"","sources":["../../src/tests/useAction.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { useAction } from "../useAction";
|
|
3
|
+
describe("useAction", () => {
|
|
4
|
+
it("should initialize with idle state", () => {
|
|
5
|
+
const [state] = useAction(async () => "result");
|
|
6
|
+
expect(state.isPending).toBe(false);
|
|
7
|
+
expect(state.params).toBe(null);
|
|
8
|
+
expect(state.result).toBe(null);
|
|
9
|
+
expect(state.error).toBe(null);
|
|
10
|
+
});
|
|
11
|
+
it("should handle successful action without parameters", async () => {
|
|
12
|
+
const fn = vi.fn(async () => "success");
|
|
13
|
+
const [state, run] = useAction(fn);
|
|
14
|
+
run();
|
|
15
|
+
expect(state.isPending).toBe(true);
|
|
16
|
+
expect(state.params).toBe(null);
|
|
17
|
+
expect(state.result).toBe(null);
|
|
18
|
+
expect(state.error).toBe(null);
|
|
19
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
20
|
+
expect(state.isPending).toBe(false);
|
|
21
|
+
expect(state.result).toBe("success");
|
|
22
|
+
expect(state.error).toBe(null);
|
|
23
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
24
|
+
});
|
|
25
|
+
it("should handle successful action with parameters", async () => {
|
|
26
|
+
const fn = vi.fn(async (x) => x * 2);
|
|
27
|
+
const [state, run] = useAction(fn);
|
|
28
|
+
run(5);
|
|
29
|
+
expect(state.isPending).toBe(true);
|
|
30
|
+
expect(state.params).toBe(5);
|
|
31
|
+
expect(state.result).toBe(null);
|
|
32
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
33
|
+
expect(state.isPending).toBe(false);
|
|
34
|
+
expect(state.params).toBe(5);
|
|
35
|
+
expect(state.result).toBe(10);
|
|
36
|
+
expect(state.error).toBe(null);
|
|
37
|
+
});
|
|
38
|
+
it("should handle errors", async () => {
|
|
39
|
+
const fn = vi.fn(async () => {
|
|
40
|
+
throw new Error("Test error");
|
|
41
|
+
});
|
|
42
|
+
const [state, run] = useAction(fn);
|
|
43
|
+
run();
|
|
44
|
+
expect(state.isPending).toBe(true);
|
|
45
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
46
|
+
expect(state.isPending).toBe(false);
|
|
47
|
+
expect(state.result).toBe(null);
|
|
48
|
+
expect(state.error?.message).toBe("Test error");
|
|
49
|
+
});
|
|
50
|
+
it("should abort previous action when new action starts", async () => {
|
|
51
|
+
let resolveFirst = null;
|
|
52
|
+
let resolveSecond = null;
|
|
53
|
+
const fn = vi.fn(async (id) => {
|
|
54
|
+
if (id === 1) {
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
resolveFirst = resolve;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
resolveSecond = resolve;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
const [state, run] = useAction(fn);
|
|
66
|
+
// Start first action
|
|
67
|
+
run(1);
|
|
68
|
+
expect(state.isPending).toBe(true);
|
|
69
|
+
expect(state.params).toBe(1);
|
|
70
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
71
|
+
// Start second action (should abort first)
|
|
72
|
+
run(2);
|
|
73
|
+
expect(state.isPending).toBe(true);
|
|
74
|
+
expect(state.params).toBe(2);
|
|
75
|
+
// Resolve first action (should be ignored due to abort)
|
|
76
|
+
resolveFirst?.("first");
|
|
77
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
78
|
+
// State should not reflect first action
|
|
79
|
+
expect(state.result).toBe(null);
|
|
80
|
+
// Resolve second action
|
|
81
|
+
resolveSecond?.("second");
|
|
82
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
83
|
+
// State should reflect second action
|
|
84
|
+
expect(state.isPending).toBe(false);
|
|
85
|
+
expect(state.params).toBe(2);
|
|
86
|
+
expect(state.result).toBe("second");
|
|
87
|
+
});
|
|
88
|
+
it("should track params even on error", async () => {
|
|
89
|
+
const fn = vi.fn(async (x) => {
|
|
90
|
+
throw new Error("Failed");
|
|
91
|
+
});
|
|
92
|
+
const [state, run] = useAction(fn);
|
|
93
|
+
run(42);
|
|
94
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
95
|
+
expect(state.isPending).toBe(false);
|
|
96
|
+
expect(state.params).toBe(42);
|
|
97
|
+
expect(state.error?.message).toBe("Failed");
|
|
98
|
+
expect(state.result).toBe(null);
|
|
99
|
+
});
|
|
100
|
+
it("should handle rapid consecutive calls", async () => {
|
|
101
|
+
let callCount = 0;
|
|
102
|
+
const fn = vi.fn(async (x) => {
|
|
103
|
+
callCount++;
|
|
104
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
105
|
+
return x;
|
|
106
|
+
});
|
|
107
|
+
const [state, run] = useAction(fn);
|
|
108
|
+
// Fire off multiple calls
|
|
109
|
+
run(1);
|
|
110
|
+
run(2);
|
|
111
|
+
run(3);
|
|
112
|
+
// Wait for all to settle
|
|
113
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
114
|
+
// Should have called function 3 times (not aborted, just ignored results)
|
|
115
|
+
expect(callCount).toBe(3);
|
|
116
|
+
// State should reflect the last call
|
|
117
|
+
expect(state.isPending).toBe(false);
|
|
118
|
+
expect(state.params).toBe(3);
|
|
119
|
+
expect(state.result).toBe(3);
|
|
120
|
+
});
|
|
121
|
+
it("should handle null as valid param value", async () => {
|
|
122
|
+
const fn = vi.fn(async (x) => {
|
|
123
|
+
return x === null ? "was null" : x;
|
|
124
|
+
});
|
|
125
|
+
const [state, run] = useAction(fn);
|
|
126
|
+
run(null);
|
|
127
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
128
|
+
expect(state.isPending).toBe(false);
|
|
129
|
+
expect(state.params).toBe(null);
|
|
130
|
+
expect(state.result).toBe("was null");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAsync.test.d.ts","sourceRoot":"","sources":["../../src/tests/useAsync.test.tsx"],"names":[],"mappings":""}
|