rask-ui 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/component.d.ts +17 -0
- package/dist/component.d.ts.map +1 -1
- package/dist/createAsync.d.ts +23 -0
- package/dist/createAsync.d.ts.map +1 -1
- package/dist/createAsync.js +23 -0
- package/dist/createContext.d.ts +26 -2
- package/dist/createContext.d.ts.map +1 -1
- package/dist/createContext.js +31 -5
- package/dist/createContext.test.d.ts +2 -0
- package/dist/createContext.test.d.ts.map +1 -0
- package/dist/createContext.test.js +136 -0
- package/dist/createMutation.d.ts +23 -0
- package/dist/createMutation.d.ts.map +1 -1
- package/dist/createMutation.js +23 -0
- package/dist/createQuery.d.ts +23 -0
- package/dist/createQuery.d.ts.map +1 -1
- package/dist/createQuery.js +23 -0
- package/dist/createRef.test.d.ts +2 -0
- package/dist/createRef.test.d.ts.map +1 -0
- package/dist/createRef.test.js +80 -0
- package/dist/createState.d.ts +24 -0
- package/dist/createState.d.ts.map +1 -1
- package/dist/createState.js +24 -0
- package/dist/createView.d.ts +54 -0
- package/dist/createView.d.ts.map +1 -0
- package/dist/createView.js +68 -0
- package/dist/createView.test.d.ts +2 -0
- package/dist/createView.test.d.ts.map +1 -0
- package/dist/createView.test.js +203 -0
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +4 -5
- package/dist/error.test.d.ts +2 -0
- package/dist/error.test.d.ts.map +1 -0
- package/dist/error.test.js +144 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/integration.test.d.ts +2 -0
- package/dist/integration.test.d.ts.map +1 -0
- package/dist/integration.test.js +155 -0
- package/dist/jsx-dev-runtime.d.ts +3 -3
- package/dist/jsx-dev-runtime.d.ts.map +1 -1
- package/dist/jsx-dev-runtime.js +2 -2
- package/dist/jsx-runtime.d.ts +1 -4
- package/dist/jsx-runtime.d.ts.map +1 -1
- package/dist/jsx-runtime.js +3 -14
- package/dist/observation.d.ts +1 -0
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +5 -0
- package/dist/test-setup.d.ts +3 -3
- package/dist/test-setup.d.ts.map +1 -1
- package/dist/test-setup.js +7 -8
- package/dist/tests/class.test.d.ts +2 -0
- package/dist/tests/class.test.d.ts.map +1 -0
- package/dist/tests/class.test.js +143 -0
- package/dist/tests/complex-rendering.test.d.ts +2 -0
- package/dist/tests/complex-rendering.test.d.ts.map +1 -0
- package/dist/tests/complex-rendering.test.js +400 -0
- package/dist/tests/component.cleanup.test.d.ts +2 -0
- package/dist/tests/component.cleanup.test.d.ts.map +1 -0
- package/dist/tests/component.cleanup.test.js +325 -0
- package/dist/tests/component.counter.test.d.ts +2 -0
- package/dist/tests/component.counter.test.d.ts.map +1 -0
- package/dist/tests/component.counter.test.js +124 -0
- package/dist/tests/component.interaction.test.d.ts +2 -0
- package/dist/tests/component.interaction.test.d.ts.map +1 -0
- package/dist/tests/component.interaction.test.js +73 -0
- package/dist/tests/component.props.test.d.ts +2 -0
- package/dist/tests/component.props.test.d.ts.map +1 -0
- package/dist/tests/component.props.test.js +88 -0
- package/dist/tests/component.return-types.test.d.ts +2 -0
- package/dist/tests/component.return-types.test.d.ts.map +1 -0
- package/dist/tests/component.return-types.test.js +357 -0
- package/dist/tests/component.state.test.d.ts +2 -0
- package/dist/tests/component.state.test.d.ts.map +1 -0
- package/dist/tests/component.state.test.js +129 -0
- package/dist/tests/component.test.d.ts +2 -0
- package/dist/tests/component.test.d.ts.map +1 -0
- package/dist/tests/component.test.js +63 -0
- package/dist/tests/createAsync.test.d.ts +2 -0
- package/dist/tests/createAsync.test.d.ts.map +1 -0
- package/dist/tests/createAsync.test.js +110 -0
- package/dist/tests/createContext.test.d.ts +2 -0
- package/dist/tests/createContext.test.d.ts.map +1 -0
- package/dist/tests/createContext.test.js +141 -0
- package/dist/tests/createMutation.test.d.ts +2 -0
- package/dist/tests/createMutation.test.d.ts.map +1 -0
- package/dist/tests/createMutation.test.js +168 -0
- package/dist/tests/createQuery.test.d.ts +2 -0
- package/dist/tests/createQuery.test.d.ts.map +1 -0
- package/dist/tests/createQuery.test.js +156 -0
- package/dist/tests/createRef.test.d.ts +2 -0
- package/dist/tests/createRef.test.d.ts.map +1 -0
- package/dist/tests/createRef.test.js +84 -0
- package/dist/tests/createState.test.d.ts +2 -0
- package/dist/tests/createState.test.d.ts.map +1 -0
- package/dist/tests/createState.test.js +111 -0
- package/dist/tests/createView.test.d.ts +2 -0
- package/dist/tests/createView.test.d.ts.map +1 -0
- package/dist/tests/createView.test.js +203 -0
- package/dist/tests/edge-cases.test.d.ts +2 -0
- package/dist/tests/edge-cases.test.d.ts.map +1 -0
- package/dist/tests/edge-cases.test.js +637 -0
- package/dist/tests/error-no-boundary.test.d.ts +2 -0
- package/dist/tests/error-no-boundary.test.d.ts.map +1 -0
- package/dist/tests/error-no-boundary.test.js +174 -0
- package/dist/tests/error.test.d.ts +2 -0
- package/dist/tests/error.test.d.ts.map +1 -0
- package/dist/tests/error.test.js +199 -0
- package/dist/tests/fragment.test.d.ts +2 -0
- package/dist/tests/fragment.test.d.ts.map +1 -0
- package/dist/tests/fragment.test.js +618 -0
- package/dist/tests/integration.test.d.ts +2 -0
- package/dist/tests/integration.test.d.ts.map +1 -0
- package/dist/tests/integration.test.js +192 -0
- package/dist/tests/keys.test.d.ts +2 -0
- package/dist/tests/keys.test.d.ts.map +1 -0
- package/dist/tests/keys.test.js +293 -0
- package/dist/tests/mount.test.d.ts +2 -0
- package/dist/tests/mount.test.d.ts.map +1 -0
- package/dist/tests/mount.test.js +91 -0
- package/dist/tests/observation.test.d.ts +2 -0
- package/dist/tests/observation.test.d.ts.map +1 -0
- package/dist/tests/observation.test.js +150 -0
- package/dist/tests/patch.test.d.ts +2 -0
- package/dist/tests/patch.test.d.ts.map +1 -0
- package/dist/tests/patch.test.js +498 -0
- package/dist/tests/patchChildren.test.d.ts +2 -0
- package/dist/tests/patchChildren.test.d.ts.map +1 -0
- package/dist/tests/patchChildren.test.js +387 -0
- package/dist/tests/primitives.test.d.ts +2 -0
- package/dist/tests/primitives.test.d.ts.map +1 -0
- package/dist/tests/primitives.test.js +132 -0
- package/dist/vdom/AbstractVNode.d.ts +22 -0
- package/dist/vdom/AbstractVNode.d.ts.map +1 -0
- package/dist/vdom/AbstractVNode.js +106 -0
- package/dist/vdom/ComponentVNode.d.ts +48 -0
- package/dist/vdom/ComponentVNode.d.ts.map +1 -0
- package/dist/vdom/ComponentVNode.js +209 -0
- package/dist/vdom/ElementVNode.d.ts +24 -0
- package/dist/vdom/ElementVNode.d.ts.map +1 -0
- package/dist/vdom/ElementVNode.js +126 -0
- package/dist/vdom/FragmentVNode.d.ts +13 -0
- package/dist/vdom/FragmentVNode.d.ts.map +1 -0
- package/dist/vdom/FragmentVNode.js +34 -0
- package/dist/vdom/RootVNode.d.ts +22 -0
- package/dist/vdom/RootVNode.d.ts.map +1 -0
- package/dist/vdom/RootVNode.js +55 -0
- package/dist/vdom/TextVNode.d.ts +11 -0
- package/dist/vdom/TextVNode.d.ts.map +1 -0
- package/dist/vdom/TextVNode.js +32 -0
- package/dist/vdom/class.test.d.ts +2 -0
- package/dist/vdom/class.test.d.ts.map +1 -0
- package/dist/vdom/class.test.js +143 -0
- package/dist/vdom/complex-rendering.test.d.ts +2 -0
- package/dist/vdom/complex-rendering.test.d.ts.map +1 -0
- package/dist/vdom/complex-rendering.test.js +400 -0
- package/dist/vdom/component.cleanup.test.d.ts +2 -0
- package/dist/vdom/component.cleanup.test.d.ts.map +1 -0
- package/dist/vdom/component.cleanup.test.js +323 -0
- package/dist/vdom/component.counter.test.d.ts +2 -0
- package/dist/vdom/component.counter.test.d.ts.map +1 -0
- package/dist/vdom/component.counter.test.js +124 -0
- package/dist/vdom/component.interaction.test.d.ts +2 -0
- package/dist/vdom/component.interaction.test.d.ts.map +1 -0
- package/dist/vdom/component.interaction.test.js +73 -0
- package/dist/vdom/component.props.test.d.ts +2 -0
- package/dist/vdom/component.props.test.d.ts.map +1 -0
- package/dist/vdom/component.props.test.js +88 -0
- package/dist/vdom/component.return-types.test.d.ts +2 -0
- package/dist/vdom/component.return-types.test.d.ts.map +1 -0
- package/dist/vdom/component.return-types.test.js +357 -0
- package/dist/vdom/component.state.test.d.ts +2 -0
- package/dist/vdom/component.state.test.d.ts.map +1 -0
- package/dist/vdom/component.state.test.js +129 -0
- package/dist/vdom/component.test.d.ts +2 -0
- package/dist/vdom/component.test.d.ts.map +1 -0
- package/dist/vdom/component.test.js +63 -0
- package/dist/vdom/dom-utils.d.ts +9 -0
- package/dist/vdom/dom-utils.d.ts.map +1 -0
- package/dist/vdom/dom-utils.js +74 -0
- package/dist/vdom/edge-cases.test.d.ts +2 -0
- package/dist/vdom/edge-cases.test.d.ts.map +1 -0
- package/dist/vdom/edge-cases.test.js +637 -0
- package/dist/vdom/fragment.test.d.ts +2 -0
- package/dist/vdom/fragment.test.d.ts.map +1 -0
- package/dist/vdom/fragment.test.js +618 -0
- package/dist/vdom/index.d.ts +10 -0
- package/dist/vdom/index.d.ts.map +1 -0
- package/dist/vdom/index.js +26 -0
- package/dist/vdom/keys.test.d.ts +2 -0
- package/dist/vdom/keys.test.d.ts.map +1 -0
- package/dist/vdom/keys.test.js +293 -0
- package/dist/vdom/mount.test.d.ts +2 -0
- package/dist/vdom/mount.test.d.ts.map +1 -0
- package/dist/vdom/mount.test.js +91 -0
- package/dist/vdom/patch.test.d.ts +2 -0
- package/dist/vdom/patch.test.d.ts.map +1 -0
- package/dist/vdom/patch.test.js +498 -0
- package/dist/vdom/patchChildren.test.d.ts +2 -0
- package/dist/vdom/patchChildren.test.d.ts.map +1 -0
- package/dist/vdom/patchChildren.test.js +392 -0
- package/dist/vdom/primitives.test.d.ts +2 -0
- package/dist/vdom/primitives.test.d.ts.map +1 -0
- package/dist/vdom/primitives.test.js +132 -0
- package/dist/vdom/types.d.ts +8 -0
- package/dist/vdom/types.d.ts.map +1 -0
- package/dist/vdom/types.js +1 -0
- package/dist/vdom/utils.d.ts +6 -0
- package/dist/vdom/utils.d.ts.map +1 -0
- package/dist/vdom/utils.js +63 -0
- package/package.json +1 -4
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { onCleanup } from "./ComponentVNode";
|
|
3
|
+
import { jsx, render } from "./index";
|
|
4
|
+
import { createState } from "../createState";
|
|
5
|
+
describe("Component Cleanup", () => {
|
|
6
|
+
it("should call onCleanup callback when component unmounts", async () => {
|
|
7
|
+
const container = document.createElement("div");
|
|
8
|
+
const cleanupFn = vi.fn();
|
|
9
|
+
let stateFn;
|
|
10
|
+
const MyComponent = () => {
|
|
11
|
+
onCleanup(cleanupFn);
|
|
12
|
+
return () => jsx("div", { children: "Component" });
|
|
13
|
+
};
|
|
14
|
+
const App = () => {
|
|
15
|
+
const state = createState({ show: true });
|
|
16
|
+
stateFn = state;
|
|
17
|
+
return () => state.show ? jsx(MyComponent, {}) : jsx("div", { children: "Empty" });
|
|
18
|
+
};
|
|
19
|
+
render(jsx(App, {}), container);
|
|
20
|
+
expect(cleanupFn).not.toHaveBeenCalled();
|
|
21
|
+
// Hide component to trigger unmount
|
|
22
|
+
stateFn.show = false;
|
|
23
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
24
|
+
expect(cleanupFn).toHaveBeenCalledTimes(1);
|
|
25
|
+
});
|
|
26
|
+
it("should call multiple onCleanup callbacks in order", async () => {
|
|
27
|
+
const container = document.createElement("div");
|
|
28
|
+
const calls = [];
|
|
29
|
+
const cleanup1 = vi.fn(() => calls.push(1));
|
|
30
|
+
const cleanup2 = vi.fn(() => calls.push(2));
|
|
31
|
+
const cleanup3 = vi.fn(() => calls.push(3));
|
|
32
|
+
let stateFn;
|
|
33
|
+
const MyComponent = () => {
|
|
34
|
+
onCleanup(cleanup1);
|
|
35
|
+
onCleanup(cleanup2);
|
|
36
|
+
onCleanup(cleanup3);
|
|
37
|
+
return () => jsx("div", { children: "Component" });
|
|
38
|
+
};
|
|
39
|
+
const App = () => {
|
|
40
|
+
const state = createState({ show: true });
|
|
41
|
+
stateFn = state;
|
|
42
|
+
return () => (state.show ? jsx(MyComponent, {}) : null);
|
|
43
|
+
};
|
|
44
|
+
render(jsx(App, {}), container);
|
|
45
|
+
stateFn.show = false;
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
47
|
+
expect(cleanup1).toHaveBeenCalledTimes(1);
|
|
48
|
+
expect(cleanup2).toHaveBeenCalledTimes(1);
|
|
49
|
+
expect(cleanup3).toHaveBeenCalledTimes(1);
|
|
50
|
+
expect(calls).toEqual([1, 2, 3]);
|
|
51
|
+
});
|
|
52
|
+
it("should cleanup event listeners", async () => {
|
|
53
|
+
const container = document.createElement("div");
|
|
54
|
+
const cleanupFn = vi.fn();
|
|
55
|
+
let stateFn;
|
|
56
|
+
const MyComponent = () => {
|
|
57
|
+
const button = document.createElement("button");
|
|
58
|
+
button.addEventListener("click", cleanupFn);
|
|
59
|
+
onCleanup(() => {
|
|
60
|
+
button.removeEventListener("click", cleanupFn);
|
|
61
|
+
cleanupFn();
|
|
62
|
+
});
|
|
63
|
+
return () => jsx("div", { children: "Component" });
|
|
64
|
+
};
|
|
65
|
+
const App = () => {
|
|
66
|
+
const state = createState({ show: true });
|
|
67
|
+
stateFn = state;
|
|
68
|
+
return () => (state.show ? jsx(MyComponent, {}) : null);
|
|
69
|
+
};
|
|
70
|
+
render(jsx(App, {}), container);
|
|
71
|
+
expect(cleanupFn).not.toHaveBeenCalled();
|
|
72
|
+
stateFn.show = false;
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
74
|
+
// Cleanup function should be called
|
|
75
|
+
expect(cleanupFn).toHaveBeenCalledTimes(1);
|
|
76
|
+
});
|
|
77
|
+
it("should cleanup intervals", async () => {
|
|
78
|
+
vi.useFakeTimers();
|
|
79
|
+
const container = document.createElement("div");
|
|
80
|
+
const callback = vi.fn();
|
|
81
|
+
let stateFn;
|
|
82
|
+
const MyComponent = () => {
|
|
83
|
+
const intervalId = setInterval(callback, 1000);
|
|
84
|
+
onCleanup(() => {
|
|
85
|
+
clearInterval(intervalId);
|
|
86
|
+
});
|
|
87
|
+
return () => jsx("div", { children: "Component" });
|
|
88
|
+
};
|
|
89
|
+
const App = () => {
|
|
90
|
+
const state = createState({ show: true });
|
|
91
|
+
stateFn = state;
|
|
92
|
+
return () => (state.show ? jsx(MyComponent, {}) : null);
|
|
93
|
+
};
|
|
94
|
+
render(jsx(App, {}), container);
|
|
95
|
+
vi.advanceTimersByTime(2000);
|
|
96
|
+
expect(callback).toHaveBeenCalledTimes(2);
|
|
97
|
+
stateFn.show = false;
|
|
98
|
+
await vi.runAllTimersAsync();
|
|
99
|
+
// After cleanup, interval should be cleared
|
|
100
|
+
vi.advanceTimersByTime(2000);
|
|
101
|
+
expect(callback).toHaveBeenCalledTimes(2); // Still 2, not 4
|
|
102
|
+
vi.useRealTimers();
|
|
103
|
+
});
|
|
104
|
+
it("should cleanup timeouts", async () => {
|
|
105
|
+
vi.useFakeTimers();
|
|
106
|
+
const container = document.createElement("div");
|
|
107
|
+
const callback = vi.fn();
|
|
108
|
+
let stateFn;
|
|
109
|
+
const MyComponent = () => {
|
|
110
|
+
const timeoutId = setTimeout(callback, 1000);
|
|
111
|
+
onCleanup(() => {
|
|
112
|
+
clearTimeout(timeoutId);
|
|
113
|
+
});
|
|
114
|
+
return () => jsx("div", { children: "Component" });
|
|
115
|
+
};
|
|
116
|
+
const App = () => {
|
|
117
|
+
const state = createState({ show: true });
|
|
118
|
+
stateFn = state;
|
|
119
|
+
return () => (state.show ? jsx(MyComponent, {}) : null);
|
|
120
|
+
};
|
|
121
|
+
render(jsx(App, {}), container);
|
|
122
|
+
stateFn.show = false;
|
|
123
|
+
await vi.runAllTimersAsync();
|
|
124
|
+
// After cleanup, timeout should be cleared
|
|
125
|
+
vi.advanceTimersByTime(2000);
|
|
126
|
+
expect(callback).not.toHaveBeenCalled();
|
|
127
|
+
vi.useRealTimers();
|
|
128
|
+
});
|
|
129
|
+
it("should cleanup subscriptions", async () => {
|
|
130
|
+
const container = document.createElement("div");
|
|
131
|
+
const unsubscribe = vi.fn();
|
|
132
|
+
const subscribe = vi.fn(() => unsubscribe);
|
|
133
|
+
let stateFn;
|
|
134
|
+
const MyComponent = () => {
|
|
135
|
+
const subscription = subscribe();
|
|
136
|
+
onCleanup(() => {
|
|
137
|
+
subscription();
|
|
138
|
+
});
|
|
139
|
+
return () => jsx("div", { children: "Component" });
|
|
140
|
+
};
|
|
141
|
+
const App = () => {
|
|
142
|
+
const state = createState({ show: true });
|
|
143
|
+
stateFn = state;
|
|
144
|
+
return () => (state.show ? jsx(MyComponent, {}) : null);
|
|
145
|
+
};
|
|
146
|
+
render(jsx(App, {}), container);
|
|
147
|
+
expect(subscribe).toHaveBeenCalledTimes(1);
|
|
148
|
+
expect(unsubscribe).not.toHaveBeenCalled();
|
|
149
|
+
stateFn.show = false;
|
|
150
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
151
|
+
expect(unsubscribe).toHaveBeenCalledTimes(1);
|
|
152
|
+
});
|
|
153
|
+
it("should not call cleanup when component is still mounted", () => {
|
|
154
|
+
const container = document.createElement("div");
|
|
155
|
+
const cleanupFn = vi.fn();
|
|
156
|
+
const MyComponent = () => {
|
|
157
|
+
onCleanup(cleanupFn);
|
|
158
|
+
return () => jsx("div", { children: "Component" });
|
|
159
|
+
};
|
|
160
|
+
render(jsx(MyComponent, {}), container);
|
|
161
|
+
expect(cleanupFn).not.toHaveBeenCalled();
|
|
162
|
+
});
|
|
163
|
+
it("should call cleanup when component is replaced with different component", async () => {
|
|
164
|
+
const container = document.createElement("div");
|
|
165
|
+
const cleanup1 = vi.fn();
|
|
166
|
+
const cleanup2 = vi.fn();
|
|
167
|
+
let stateFn;
|
|
168
|
+
const Component1 = () => {
|
|
169
|
+
onCleanup(cleanup1);
|
|
170
|
+
return () => jsx("div", { children: "Component 1" });
|
|
171
|
+
};
|
|
172
|
+
const Component2 = () => {
|
|
173
|
+
onCleanup(cleanup2);
|
|
174
|
+
return () => jsx("div", { children: "Component 2" });
|
|
175
|
+
};
|
|
176
|
+
const App = () => {
|
|
177
|
+
const state = createState({ showFirst: true });
|
|
178
|
+
stateFn = state;
|
|
179
|
+
return () => state.showFirst ? jsx(Component1, {}) : jsx(Component2, {});
|
|
180
|
+
};
|
|
181
|
+
render(jsx(App, {}), container);
|
|
182
|
+
expect(cleanup1).not.toHaveBeenCalled();
|
|
183
|
+
stateFn.showFirst = false;
|
|
184
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
185
|
+
// Component1 should be cleaned up when replaced with Component2
|
|
186
|
+
expect(cleanup1).toHaveBeenCalledTimes(1);
|
|
187
|
+
expect(cleanup2).not.toHaveBeenCalled();
|
|
188
|
+
});
|
|
189
|
+
it("should throw error when onCleanup is called outside component setup", () => {
|
|
190
|
+
expect(() => {
|
|
191
|
+
onCleanup(() => { });
|
|
192
|
+
}).toThrow("Only use onCleanup in component setup");
|
|
193
|
+
});
|
|
194
|
+
it("should cleanup nested resources", async () => {
|
|
195
|
+
const container = document.createElement("div");
|
|
196
|
+
const outerCleanup = vi.fn();
|
|
197
|
+
const innerCleanup = vi.fn();
|
|
198
|
+
let stateFn;
|
|
199
|
+
const MyComponent = () => {
|
|
200
|
+
const resource1 = { close: outerCleanup };
|
|
201
|
+
onCleanup(() => resource1.close());
|
|
202
|
+
const resource2 = { dispose: innerCleanup };
|
|
203
|
+
onCleanup(() => resource2.dispose());
|
|
204
|
+
return () => jsx("div", { children: "Component" });
|
|
205
|
+
};
|
|
206
|
+
const App = () => {
|
|
207
|
+
const state = createState({ show: true });
|
|
208
|
+
stateFn = state;
|
|
209
|
+
return () => (state.show ? jsx(MyComponent, {}) : null);
|
|
210
|
+
};
|
|
211
|
+
render(jsx(App, {}), container);
|
|
212
|
+
stateFn.show = false;
|
|
213
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
214
|
+
expect(outerCleanup).toHaveBeenCalledTimes(1);
|
|
215
|
+
expect(innerCleanup).toHaveBeenCalledTimes(1);
|
|
216
|
+
});
|
|
217
|
+
it("should handle cleanup errors gracefully", async () => {
|
|
218
|
+
const container = document.createElement("div");
|
|
219
|
+
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
220
|
+
const cleanup1 = vi.fn(() => {
|
|
221
|
+
throw new Error("Cleanup error");
|
|
222
|
+
});
|
|
223
|
+
const cleanup2 = vi.fn();
|
|
224
|
+
let stateFn;
|
|
225
|
+
const MyComponent = () => {
|
|
226
|
+
onCleanup(cleanup1);
|
|
227
|
+
onCleanup(cleanup2);
|
|
228
|
+
return () => jsx("div", { children: "Component" });
|
|
229
|
+
};
|
|
230
|
+
const App = () => {
|
|
231
|
+
const state = createState({ show: true });
|
|
232
|
+
stateFn = state;
|
|
233
|
+
return () => (state.show ? jsx(MyComponent, {}) : null);
|
|
234
|
+
};
|
|
235
|
+
render(jsx(App, {}), container);
|
|
236
|
+
// Trigger unmount - errors should be logged but not thrown
|
|
237
|
+
stateFn.show = false;
|
|
238
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
239
|
+
// Both cleanups should have been called
|
|
240
|
+
expect(cleanup1).toHaveBeenCalledTimes(1);
|
|
241
|
+
expect(cleanup2).toHaveBeenCalledTimes(1);
|
|
242
|
+
// Error should have been logged
|
|
243
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith("Error during cleanup:", expect.objectContaining({ message: "Cleanup error" }));
|
|
244
|
+
consoleErrorSpy.mockRestore();
|
|
245
|
+
});
|
|
246
|
+
it("should cleanup component with state and effects", async () => {
|
|
247
|
+
vi.useFakeTimers();
|
|
248
|
+
const container = document.createElement("div");
|
|
249
|
+
const updateCallback = vi.fn();
|
|
250
|
+
const cleanupFn = vi.fn();
|
|
251
|
+
let stateFn;
|
|
252
|
+
const MyComponent = () => {
|
|
253
|
+
const intervalId = setInterval(updateCallback, 100);
|
|
254
|
+
onCleanup(() => {
|
|
255
|
+
clearInterval(intervalId);
|
|
256
|
+
cleanupFn();
|
|
257
|
+
});
|
|
258
|
+
return () => jsx("div", { children: "Component" });
|
|
259
|
+
};
|
|
260
|
+
const App = () => {
|
|
261
|
+
const state = createState({ show: true });
|
|
262
|
+
stateFn = state;
|
|
263
|
+
return () => (state.show ? jsx(MyComponent, {}) : null);
|
|
264
|
+
};
|
|
265
|
+
render(jsx(App, {}), container);
|
|
266
|
+
vi.advanceTimersByTime(500);
|
|
267
|
+
expect(updateCallback).toHaveBeenCalledTimes(5);
|
|
268
|
+
stateFn.show = false;
|
|
269
|
+
await vi.runAllTimersAsync();
|
|
270
|
+
expect(cleanupFn).toHaveBeenCalledTimes(1);
|
|
271
|
+
// After cleanup, no more updates
|
|
272
|
+
vi.advanceTimersByTime(500);
|
|
273
|
+
expect(updateCallback).toHaveBeenCalledTimes(5);
|
|
274
|
+
vi.useRealTimers();
|
|
275
|
+
});
|
|
276
|
+
it("should allow cleanup to access component closure variables", async () => {
|
|
277
|
+
const container = document.createElement("div");
|
|
278
|
+
let capturedValue;
|
|
279
|
+
let stateFn;
|
|
280
|
+
const MyComponent = () => {
|
|
281
|
+
const value = 42;
|
|
282
|
+
onCleanup(() => {
|
|
283
|
+
capturedValue = value;
|
|
284
|
+
});
|
|
285
|
+
return () => jsx("div", { children: "Component" });
|
|
286
|
+
};
|
|
287
|
+
const App = () => {
|
|
288
|
+
const state = createState({ show: true });
|
|
289
|
+
stateFn = state;
|
|
290
|
+
return () => (state.show ? jsx(MyComponent, {}) : null);
|
|
291
|
+
};
|
|
292
|
+
render(jsx(App, {}), container);
|
|
293
|
+
stateFn.show = false;
|
|
294
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
295
|
+
expect(capturedValue).toBe(42);
|
|
296
|
+
});
|
|
297
|
+
it("should cleanup parent and child components", async () => {
|
|
298
|
+
const container = document.createElement("div");
|
|
299
|
+
const parentCleanup = vi.fn();
|
|
300
|
+
const childCleanup = vi.fn();
|
|
301
|
+
let stateFn;
|
|
302
|
+
const ChildComponent = () => {
|
|
303
|
+
onCleanup(childCleanup);
|
|
304
|
+
return () => jsx("span", { children: "Child" });
|
|
305
|
+
};
|
|
306
|
+
const ParentComponent = () => {
|
|
307
|
+
onCleanup(parentCleanup);
|
|
308
|
+
return () => jsx(ChildComponent, {});
|
|
309
|
+
};
|
|
310
|
+
const App = () => {
|
|
311
|
+
const state = createState({ show: true });
|
|
312
|
+
stateFn = state;
|
|
313
|
+
return () => (state.show ? jsx(ParentComponent, {}) : null);
|
|
314
|
+
};
|
|
315
|
+
render(jsx(App, {}), container);
|
|
316
|
+
expect(parentCleanup).not.toHaveBeenCalled();
|
|
317
|
+
expect(childCleanup).not.toHaveBeenCalled();
|
|
318
|
+
stateFn.show = false;
|
|
319
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
320
|
+
expect(parentCleanup).toHaveBeenCalledTimes(1);
|
|
321
|
+
expect(childCleanup).toHaveBeenCalledTimes(1);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component.counter.test.d.ts","sourceRoot":"","sources":["../../src/vdom/component.counter.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { jsx, render } from "./index";
|
|
3
|
+
import { createState } from "../createState";
|
|
4
|
+
describe("Component Counter", () => {
|
|
5
|
+
it("should render a simple counter", () => {
|
|
6
|
+
const container = document.createElement("div");
|
|
7
|
+
const MyComponent = () => {
|
|
8
|
+
const state = createState({ count: 0 });
|
|
9
|
+
return () => jsx("h1", { children: `Counter ${state.count}` });
|
|
10
|
+
};
|
|
11
|
+
render(jsx(MyComponent, {}), container);
|
|
12
|
+
expect(container.children).toHaveLength(1);
|
|
13
|
+
expect(container.children[0].textContent).toBe("Counter 0");
|
|
14
|
+
});
|
|
15
|
+
it("should update counter text when state changes", async () => {
|
|
16
|
+
const container = document.createElement("div");
|
|
17
|
+
let stateFn;
|
|
18
|
+
const MyComponent = () => {
|
|
19
|
+
const state = createState({ count: 0 });
|
|
20
|
+
stateFn = state;
|
|
21
|
+
return () => jsx("h1", { children: `Counter ${state.count}` });
|
|
22
|
+
};
|
|
23
|
+
render(jsx(MyComponent, {}), container);
|
|
24
|
+
expect(container.children[0].textContent).toBe("Counter 0");
|
|
25
|
+
// Update the counter
|
|
26
|
+
stateFn.count++;
|
|
27
|
+
// Wait for reactive update
|
|
28
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
29
|
+
// Validate the container
|
|
30
|
+
expect(container.children[0].textContent).toBe("Counter 1");
|
|
31
|
+
});
|
|
32
|
+
it("should handle multiple counter increments", async () => {
|
|
33
|
+
const container = document.createElement("div");
|
|
34
|
+
let stateFn;
|
|
35
|
+
const MyComponent = () => {
|
|
36
|
+
const state = createState({ count: 0 });
|
|
37
|
+
stateFn = state;
|
|
38
|
+
return () => jsx("h1", { children: `Counter ${state.count}` });
|
|
39
|
+
};
|
|
40
|
+
render(jsx(MyComponent, {}), container);
|
|
41
|
+
// Multiple increments
|
|
42
|
+
stateFn.count++;
|
|
43
|
+
stateFn.count++;
|
|
44
|
+
stateFn.count++;
|
|
45
|
+
// Wait for reactive update
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
47
|
+
// Validate the container
|
|
48
|
+
expect(container.children[0].textContent).toBe("Counter 3");
|
|
49
|
+
});
|
|
50
|
+
it("should handle counter with click handler", async () => {
|
|
51
|
+
const container = document.createElement("div");
|
|
52
|
+
let stateFn;
|
|
53
|
+
const MyComponent = () => {
|
|
54
|
+
const state = createState({ count: 0 });
|
|
55
|
+
stateFn = state;
|
|
56
|
+
return () => jsx("h1", {
|
|
57
|
+
children: `Counter ${state.count}`,
|
|
58
|
+
onClick: () => state.count++,
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
render(jsx(MyComponent, {}), container);
|
|
62
|
+
expect(container.children[0].textContent).toBe("Counter 0");
|
|
63
|
+
// Simulate state change (like a click would do)
|
|
64
|
+
stateFn.count++;
|
|
65
|
+
// Wait for reactive update
|
|
66
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
67
|
+
// Validate the container
|
|
68
|
+
expect(container.children[0].textContent).toBe("Counter 1");
|
|
69
|
+
});
|
|
70
|
+
it("should handle counter in a div wrapper", async () => {
|
|
71
|
+
const container = document.createElement("div");
|
|
72
|
+
let stateFn;
|
|
73
|
+
const MyComponent = () => {
|
|
74
|
+
const state = createState({ count: 0 });
|
|
75
|
+
stateFn = state;
|
|
76
|
+
return () => jsx("div", {
|
|
77
|
+
children: jsx("h1", { children: `Counter ${state.count}` }),
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
render(jsx(MyComponent, {}), container);
|
|
81
|
+
stateFn.count++;
|
|
82
|
+
// Wait for reactive update
|
|
83
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
84
|
+
// Validate the container
|
|
85
|
+
expect(container.children[0].querySelector("h1")?.textContent).toBe("Counter 1");
|
|
86
|
+
});
|
|
87
|
+
it("should handle template literal with interpolated count", async () => {
|
|
88
|
+
const container = document.createElement("div");
|
|
89
|
+
let stateFn;
|
|
90
|
+
const MyComponent = () => {
|
|
91
|
+
const state = createState({ count: 0 });
|
|
92
|
+
stateFn = state;
|
|
93
|
+
return () => jsx("h1", {
|
|
94
|
+
children: `Hello World (${state.count})`,
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
render(jsx(MyComponent, {}), container);
|
|
98
|
+
stateFn.count = 5;
|
|
99
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
100
|
+
// Validate the container
|
|
101
|
+
expect(container.children[0].textContent).toBe("Hello World (5)");
|
|
102
|
+
});
|
|
103
|
+
it("should handle counter with nested elements", async () => {
|
|
104
|
+
const container = document.createElement("div");
|
|
105
|
+
let stateFn;
|
|
106
|
+
const MyComponent = () => {
|
|
107
|
+
const state = createState({ count: 0 });
|
|
108
|
+
stateFn = state;
|
|
109
|
+
return () => jsx("div", {
|
|
110
|
+
children: [
|
|
111
|
+
jsx("h1", { children: "Title" }),
|
|
112
|
+
jsx("p", { children: `Count: ${state.count}` }),
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
render(jsx(MyComponent, {}), container);
|
|
117
|
+
stateFn.count = 42;
|
|
118
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
119
|
+
// Validate the container
|
|
120
|
+
const div = container.children[0];
|
|
121
|
+
expect(div.querySelector("h1")?.textContent).toBe("Title");
|
|
122
|
+
expect(div.querySelector("p")?.textContent).toBe("Count: 42");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component.interaction.test.d.ts","sourceRoot":"","sources":["../../src/vdom/component.interaction.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { jsx, render } from "./index";
|
|
3
|
+
import { createState } from "../createState";
|
|
4
|
+
describe("Component State and Props Interaction", () => {
|
|
5
|
+
it("should use both state and props together", () => {
|
|
6
|
+
const container = document.createElement("div");
|
|
7
|
+
const MyComponent = (props) => {
|
|
8
|
+
const state = createState({ count: props.initialCount });
|
|
9
|
+
return () => jsx("div", { children: String(state.count) });
|
|
10
|
+
};
|
|
11
|
+
render(jsx(MyComponent, { initialCount: 10 }), container);
|
|
12
|
+
expect(container.children[0].textContent).toBe("10");
|
|
13
|
+
});
|
|
14
|
+
it("should derive state from props on mount", () => {
|
|
15
|
+
const container = document.createElement("div");
|
|
16
|
+
const MyComponent = (props) => {
|
|
17
|
+
const state = createState({ count: 0 });
|
|
18
|
+
return () => jsx("div", {
|
|
19
|
+
children: String(state.count * props.multiplier),
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
render(jsx(MyComponent, { multiplier: 5 }), container);
|
|
23
|
+
expect(container.children[0].textContent).toBe("0");
|
|
24
|
+
});
|
|
25
|
+
it("should handle state changes with prop-based rendering", () => {
|
|
26
|
+
const container = document.createElement("div");
|
|
27
|
+
let stateFn;
|
|
28
|
+
const MyComponent = (props) => {
|
|
29
|
+
const state = createState({ count: 0 });
|
|
30
|
+
stateFn = state;
|
|
31
|
+
return () => jsx("div", {
|
|
32
|
+
children: `${props.prefix}: ${state.count}`,
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
render(jsx(MyComponent, { prefix: "Count" }), container);
|
|
36
|
+
expect(container.children[0].textContent).toBe("Count: 0");
|
|
37
|
+
stateFn.count = 5;
|
|
38
|
+
// After state update, should show "Count: 5"
|
|
39
|
+
// The implementation will handle this
|
|
40
|
+
});
|
|
41
|
+
it("should support multiple state values with props", () => {
|
|
42
|
+
const container = document.createElement("div");
|
|
43
|
+
const MyComponent = (props) => {
|
|
44
|
+
const state = createState({
|
|
45
|
+
count: props.initialCount,
|
|
46
|
+
isVisible: true,
|
|
47
|
+
});
|
|
48
|
+
return () => jsx("div", {
|
|
49
|
+
children: state.isVisible
|
|
50
|
+
? `${props.title}: ${state.count}`
|
|
51
|
+
: "Hidden",
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
render(jsx(MyComponent, {
|
|
55
|
+
title: "Counter",
|
|
56
|
+
initialCount: 5,
|
|
57
|
+
}), container);
|
|
58
|
+
expect(container.children[0].textContent).toBe("Counter: 5");
|
|
59
|
+
});
|
|
60
|
+
it("should handle complex state derived from props", () => {
|
|
61
|
+
const container = document.createElement("div");
|
|
62
|
+
const MyComponent = (props) => {
|
|
63
|
+
const state = createState({ selectedIndex: 0 });
|
|
64
|
+
return () => jsx("div", {
|
|
65
|
+
children: props.items[state.selectedIndex] || "None",
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
render(jsx(MyComponent, {
|
|
69
|
+
items: ["First", "Second", "Third"],
|
|
70
|
+
}), container);
|
|
71
|
+
expect(container.children[0].textContent).toBe("First");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component.props.test.d.ts","sourceRoot":"","sources":["../../src/vdom/component.props.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { jsx, render } from "./index";
|
|
3
|
+
describe("Component Props", () => {
|
|
4
|
+
it("should pass string props to component", () => {
|
|
5
|
+
const container = document.createElement("div");
|
|
6
|
+
const MyComponent = (props) => {
|
|
7
|
+
return () => jsx("div", { children: props.message });
|
|
8
|
+
};
|
|
9
|
+
render(jsx(MyComponent, { message: "Hello Props" }), container);
|
|
10
|
+
expect(container.children[0].textContent).toBe("Hello Props");
|
|
11
|
+
});
|
|
12
|
+
it("should pass number props to component", () => {
|
|
13
|
+
const container = document.createElement("div");
|
|
14
|
+
const MyComponent = (props) => {
|
|
15
|
+
return () => jsx("div", { children: String(props.count) });
|
|
16
|
+
};
|
|
17
|
+
render(jsx(MyComponent, { count: 42 }), container);
|
|
18
|
+
expect(container.children[0].textContent).toBe("42");
|
|
19
|
+
});
|
|
20
|
+
it("should pass object props to component", () => {
|
|
21
|
+
const container = document.createElement("div");
|
|
22
|
+
const MyComponent = (props) => {
|
|
23
|
+
return () => jsx("div", {
|
|
24
|
+
children: `${props.user.name} is ${props.user.age}`,
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
render(jsx(MyComponent, { user: { name: "Alice", age: 25 } }), container);
|
|
28
|
+
expect(container.children[0].textContent).toBe("Alice is 25");
|
|
29
|
+
});
|
|
30
|
+
it("should pass array props to component", () => {
|
|
31
|
+
const container = document.createElement("div");
|
|
32
|
+
const MyComponent = (props) => {
|
|
33
|
+
return () => jsx("ul", {
|
|
34
|
+
children: props.items.map((item) => jsx("li", { children: item })),
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
render(jsx(MyComponent, { items: ["apple", "banana", "cherry"] }), container);
|
|
38
|
+
const ul = container.children[0];
|
|
39
|
+
expect(ul.children).toHaveLength(3);
|
|
40
|
+
expect(ul.children[0].textContent).toBe("apple");
|
|
41
|
+
expect(ul.children[1].textContent).toBe("banana");
|
|
42
|
+
expect(ul.children[2].textContent).toBe("cherry");
|
|
43
|
+
});
|
|
44
|
+
it("should pass function props to component", () => {
|
|
45
|
+
const container = document.createElement("div");
|
|
46
|
+
const handleClick = () => "clicked";
|
|
47
|
+
const MyComponent = (props) => {
|
|
48
|
+
return () => jsx("button", {
|
|
49
|
+
children: props.onClick(),
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
render(jsx(MyComponent, { onClick: handleClick }), container);
|
|
53
|
+
expect(container.children[0].textContent).toBe("clicked");
|
|
54
|
+
});
|
|
55
|
+
it("should pass multiple props to component", () => {
|
|
56
|
+
const container = document.createElement("div");
|
|
57
|
+
const MyComponent = (props) => {
|
|
58
|
+
return () => jsx("div", {
|
|
59
|
+
children: `${props.title}: ${props.count} (${props.isActive ? "active" : "inactive"})`,
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
render(jsx(MyComponent, { title: "Counter", count: 5, isActive: true }), container);
|
|
63
|
+
expect(container.children[0].textContent).toBe("Counter: 5 (active)");
|
|
64
|
+
});
|
|
65
|
+
it("should handle optional props", () => {
|
|
66
|
+
const container1 = document.createElement("div");
|
|
67
|
+
const container2 = document.createElement("div");
|
|
68
|
+
const MyComponent = (props) => {
|
|
69
|
+
return () => jsx("div", {
|
|
70
|
+
children: props.message || "Default message",
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
render(jsx(MyComponent, {}), container1);
|
|
74
|
+
expect(container1.children[0].textContent).toBe("Default message");
|
|
75
|
+
render(jsx(MyComponent, { message: "Custom message" }), container2);
|
|
76
|
+
expect(container2.children[0].textContent).toBe("Custom message");
|
|
77
|
+
});
|
|
78
|
+
it("should update when props change", () => {
|
|
79
|
+
const container = document.createElement("div");
|
|
80
|
+
const MyComponent = (props) => {
|
|
81
|
+
return () => jsx("div", { children: props.message });
|
|
82
|
+
};
|
|
83
|
+
render(jsx(MyComponent, { message: "Initial" }), container);
|
|
84
|
+
expect(container.children[0].textContent).toBe("Initial");
|
|
85
|
+
// Simulate props update
|
|
86
|
+
// The implementation will handle prop updates through patch
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component.return-types.test.d.ts","sourceRoot":"","sources":["../../src/vdom/component.return-types.test.tsx"],"names":[],"mappings":""}
|