rask-ui 0.1.1 → 0.2.1

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 (213) hide show
  1. package/README.md +923 -185
  2. package/dist/component.d.ts +17 -0
  3. package/dist/component.d.ts.map +1 -1
  4. package/dist/createAsync.d.ts +23 -0
  5. package/dist/createAsync.d.ts.map +1 -1
  6. package/dist/createAsync.js +23 -0
  7. package/dist/createContext.d.ts +26 -2
  8. package/dist/createContext.d.ts.map +1 -1
  9. package/dist/createContext.js +31 -5
  10. package/dist/createContext.test.d.ts +2 -0
  11. package/dist/createContext.test.d.ts.map +1 -0
  12. package/dist/createContext.test.js +136 -0
  13. package/dist/createMutation.d.ts +23 -0
  14. package/dist/createMutation.d.ts.map +1 -1
  15. package/dist/createMutation.js +23 -0
  16. package/dist/createQuery.d.ts +23 -0
  17. package/dist/createQuery.d.ts.map +1 -1
  18. package/dist/createQuery.js +23 -0
  19. package/dist/createRef.test.d.ts +2 -0
  20. package/dist/createRef.test.d.ts.map +1 -0
  21. package/dist/createRef.test.js +80 -0
  22. package/dist/createState.d.ts +24 -0
  23. package/dist/createState.d.ts.map +1 -1
  24. package/dist/createState.js +24 -0
  25. package/dist/createView.d.ts +54 -0
  26. package/dist/createView.d.ts.map +1 -0
  27. package/dist/createView.js +68 -0
  28. package/dist/createView.test.d.ts +2 -0
  29. package/dist/createView.test.d.ts.map +1 -0
  30. package/dist/createView.test.js +203 -0
  31. package/dist/error.d.ts.map +1 -1
  32. package/dist/error.js +4 -5
  33. package/dist/error.test.d.ts +2 -0
  34. package/dist/error.test.d.ts.map +1 -0
  35. package/dist/error.test.js +144 -0
  36. package/dist/index.d.ts +3 -2
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +3 -2
  39. package/dist/integration.test.d.ts +2 -0
  40. package/dist/integration.test.d.ts.map +1 -0
  41. package/dist/integration.test.js +155 -0
  42. package/dist/jsx-dev-runtime.d.ts +3 -3
  43. package/dist/jsx-dev-runtime.d.ts.map +1 -1
  44. package/dist/jsx-dev-runtime.js +2 -2
  45. package/dist/jsx-runtime.d.ts +1 -4
  46. package/dist/jsx-runtime.d.ts.map +1 -1
  47. package/dist/jsx-runtime.js +3 -14
  48. package/dist/observation.d.ts +1 -0
  49. package/dist/observation.d.ts.map +1 -1
  50. package/dist/observation.js +5 -0
  51. package/dist/test-setup.d.ts +3 -3
  52. package/dist/test-setup.d.ts.map +1 -1
  53. package/dist/test-setup.js +7 -8
  54. package/dist/tests/class.test.d.ts +2 -0
  55. package/dist/tests/class.test.d.ts.map +1 -0
  56. package/dist/tests/class.test.js +185 -0
  57. package/dist/tests/complex-rendering.test.d.ts +2 -0
  58. package/dist/tests/complex-rendering.test.d.ts.map +1 -0
  59. package/dist/tests/complex-rendering.test.js +400 -0
  60. package/dist/tests/component.cleanup.test.d.ts +2 -0
  61. package/dist/tests/component.cleanup.test.d.ts.map +1 -0
  62. package/dist/tests/component.cleanup.test.js +325 -0
  63. package/dist/tests/component.counter.test.d.ts +2 -0
  64. package/dist/tests/component.counter.test.d.ts.map +1 -0
  65. package/dist/tests/component.counter.test.js +124 -0
  66. package/dist/tests/component.interaction.test.d.ts +2 -0
  67. package/dist/tests/component.interaction.test.d.ts.map +1 -0
  68. package/dist/tests/component.interaction.test.js +73 -0
  69. package/dist/tests/component.props.test.d.ts +2 -0
  70. package/dist/tests/component.props.test.d.ts.map +1 -0
  71. package/dist/tests/component.props.test.js +334 -0
  72. package/dist/tests/component.return-types.test.d.ts +2 -0
  73. package/dist/tests/component.return-types.test.d.ts.map +1 -0
  74. package/dist/tests/component.return-types.test.js +357 -0
  75. package/dist/tests/component.state.test.d.ts +2 -0
  76. package/dist/tests/component.state.test.d.ts.map +1 -0
  77. package/dist/tests/component.state.test.js +135 -0
  78. package/dist/tests/component.test.d.ts +2 -0
  79. package/dist/tests/component.test.d.ts.map +1 -0
  80. package/dist/tests/component.test.js +63 -0
  81. package/dist/tests/createAsync.test.d.ts +2 -0
  82. package/dist/tests/createAsync.test.d.ts.map +1 -0
  83. package/dist/tests/createAsync.test.js +110 -0
  84. package/dist/tests/createContext.test.d.ts +2 -0
  85. package/dist/tests/createContext.test.d.ts.map +1 -0
  86. package/dist/tests/createContext.test.js +141 -0
  87. package/dist/tests/createMutation.test.d.ts +2 -0
  88. package/dist/tests/createMutation.test.d.ts.map +1 -0
  89. package/dist/tests/createMutation.test.js +168 -0
  90. package/dist/tests/createQuery.test.d.ts +2 -0
  91. package/dist/tests/createQuery.test.d.ts.map +1 -0
  92. package/dist/tests/createQuery.test.js +156 -0
  93. package/dist/tests/createRef.test.d.ts +2 -0
  94. package/dist/tests/createRef.test.d.ts.map +1 -0
  95. package/dist/tests/createRef.test.js +84 -0
  96. package/dist/tests/createState.test.d.ts +2 -0
  97. package/dist/tests/createState.test.d.ts.map +1 -0
  98. package/dist/tests/createState.test.js +103 -0
  99. package/dist/tests/createView.test.d.ts +2 -0
  100. package/dist/tests/createView.test.d.ts.map +1 -0
  101. package/dist/tests/createView.test.js +203 -0
  102. package/dist/tests/edge-cases.test.d.ts +2 -0
  103. package/dist/tests/edge-cases.test.d.ts.map +1 -0
  104. package/dist/tests/edge-cases.test.js +637 -0
  105. package/dist/tests/error-no-boundary.test.d.ts +2 -0
  106. package/dist/tests/error-no-boundary.test.d.ts.map +1 -0
  107. package/dist/tests/error-no-boundary.test.js +174 -0
  108. package/dist/tests/error.test.d.ts +2 -0
  109. package/dist/tests/error.test.d.ts.map +1 -0
  110. package/dist/tests/error.test.js +199 -0
  111. package/dist/tests/fragment.test.d.ts +2 -0
  112. package/dist/tests/fragment.test.d.ts.map +1 -0
  113. package/dist/tests/fragment.test.js +618 -0
  114. package/dist/tests/integration.test.d.ts +2 -0
  115. package/dist/tests/integration.test.d.ts.map +1 -0
  116. package/dist/tests/integration.test.js +192 -0
  117. package/dist/tests/keys.test.d.ts +2 -0
  118. package/dist/tests/keys.test.d.ts.map +1 -0
  119. package/dist/tests/keys.test.js +293 -0
  120. package/dist/tests/mount.test.d.ts +2 -0
  121. package/dist/tests/mount.test.d.ts.map +1 -0
  122. package/dist/tests/mount.test.js +91 -0
  123. package/dist/tests/observation.test.d.ts +2 -0
  124. package/dist/tests/observation.test.d.ts.map +1 -0
  125. package/dist/tests/observation.test.js +150 -0
  126. package/dist/tests/patch.test.d.ts +2 -0
  127. package/dist/tests/patch.test.d.ts.map +1 -0
  128. package/dist/tests/patch.test.js +498 -0
  129. package/dist/tests/patchChildren.test.d.ts +2 -0
  130. package/dist/tests/patchChildren.test.d.ts.map +1 -0
  131. package/dist/tests/patchChildren.test.js +387 -0
  132. package/dist/tests/primitives.test.d.ts +2 -0
  133. package/dist/tests/primitives.test.d.ts.map +1 -0
  134. package/dist/tests/primitives.test.js +132 -0
  135. package/dist/vdom/AbstractVNode.d.ts +22 -0
  136. package/dist/vdom/AbstractVNode.d.ts.map +1 -0
  137. package/dist/vdom/AbstractVNode.js +106 -0
  138. package/dist/vdom/ComponentVNode.d.ts +48 -0
  139. package/dist/vdom/ComponentVNode.d.ts.map +1 -0
  140. package/dist/vdom/ComponentVNode.js +209 -0
  141. package/dist/vdom/ElementVNode.d.ts +24 -0
  142. package/dist/vdom/ElementVNode.d.ts.map +1 -0
  143. package/dist/vdom/ElementVNode.js +126 -0
  144. package/dist/vdom/FragmentVNode.d.ts +13 -0
  145. package/dist/vdom/FragmentVNode.d.ts.map +1 -0
  146. package/dist/vdom/FragmentVNode.js +34 -0
  147. package/dist/vdom/RootVNode.d.ts +22 -0
  148. package/dist/vdom/RootVNode.d.ts.map +1 -0
  149. package/dist/vdom/RootVNode.js +55 -0
  150. package/dist/vdom/TextVNode.d.ts +11 -0
  151. package/dist/vdom/TextVNode.d.ts.map +1 -0
  152. package/dist/vdom/TextVNode.js +32 -0
  153. package/dist/vdom/class.test.d.ts +2 -0
  154. package/dist/vdom/class.test.d.ts.map +1 -0
  155. package/dist/vdom/class.test.js +143 -0
  156. package/dist/vdom/complex-rendering.test.d.ts +2 -0
  157. package/dist/vdom/complex-rendering.test.d.ts.map +1 -0
  158. package/dist/vdom/complex-rendering.test.js +400 -0
  159. package/dist/vdom/component.cleanup.test.d.ts +2 -0
  160. package/dist/vdom/component.cleanup.test.d.ts.map +1 -0
  161. package/dist/vdom/component.cleanup.test.js +323 -0
  162. package/dist/vdom/component.counter.test.d.ts +2 -0
  163. package/dist/vdom/component.counter.test.d.ts.map +1 -0
  164. package/dist/vdom/component.counter.test.js +124 -0
  165. package/dist/vdom/component.interaction.test.d.ts +2 -0
  166. package/dist/vdom/component.interaction.test.d.ts.map +1 -0
  167. package/dist/vdom/component.interaction.test.js +73 -0
  168. package/dist/vdom/component.props.test.d.ts +2 -0
  169. package/dist/vdom/component.props.test.d.ts.map +1 -0
  170. package/dist/vdom/component.props.test.js +88 -0
  171. package/dist/vdom/component.return-types.test.d.ts +2 -0
  172. package/dist/vdom/component.return-types.test.d.ts.map +1 -0
  173. package/dist/vdom/component.return-types.test.js +357 -0
  174. package/dist/vdom/component.state.test.d.ts +2 -0
  175. package/dist/vdom/component.state.test.d.ts.map +1 -0
  176. package/dist/vdom/component.state.test.js +129 -0
  177. package/dist/vdom/component.test.d.ts +2 -0
  178. package/dist/vdom/component.test.d.ts.map +1 -0
  179. package/dist/vdom/component.test.js +63 -0
  180. package/dist/vdom/dom-utils.d.ts +9 -0
  181. package/dist/vdom/dom-utils.d.ts.map +1 -0
  182. package/dist/vdom/dom-utils.js +84 -0
  183. package/dist/vdom/edge-cases.test.d.ts +2 -0
  184. package/dist/vdom/edge-cases.test.d.ts.map +1 -0
  185. package/dist/vdom/edge-cases.test.js +637 -0
  186. package/dist/vdom/fragment.test.d.ts +2 -0
  187. package/dist/vdom/fragment.test.d.ts.map +1 -0
  188. package/dist/vdom/fragment.test.js +618 -0
  189. package/dist/vdom/index.d.ts +10 -0
  190. package/dist/vdom/index.d.ts.map +1 -0
  191. package/dist/vdom/index.js +26 -0
  192. package/dist/vdom/keys.test.d.ts +2 -0
  193. package/dist/vdom/keys.test.d.ts.map +1 -0
  194. package/dist/vdom/keys.test.js +293 -0
  195. package/dist/vdom/mount.test.d.ts +2 -0
  196. package/dist/vdom/mount.test.d.ts.map +1 -0
  197. package/dist/vdom/mount.test.js +91 -0
  198. package/dist/vdom/patch.test.d.ts +2 -0
  199. package/dist/vdom/patch.test.d.ts.map +1 -0
  200. package/dist/vdom/patch.test.js +498 -0
  201. package/dist/vdom/patchChildren.test.d.ts +2 -0
  202. package/dist/vdom/patchChildren.test.d.ts.map +1 -0
  203. package/dist/vdom/patchChildren.test.js +392 -0
  204. package/dist/vdom/primitives.test.d.ts +2 -0
  205. package/dist/vdom/primitives.test.d.ts.map +1 -0
  206. package/dist/vdom/primitives.test.js +132 -0
  207. package/dist/vdom/types.d.ts +8 -0
  208. package/dist/vdom/types.d.ts.map +1 -0
  209. package/dist/vdom/types.js +1 -0
  210. package/dist/vdom/utils.d.ts +6 -0
  211. package/dist/vdom/utils.d.ts.map +1 -0
  212. package/dist/vdom/utils.js +63 -0
  213. package/package.json +4 -6
@@ -0,0 +1,387 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { AbstractVNode } from "../vdom/AbstractVNode";
3
+ // Helper to convert MockVNode arrays to VNode arrays
4
+ const toVNodes = (nodes) => nodes;
5
+ // Mock VNode for testing
6
+ class MockVNode extends AbstractVNode {
7
+ mountCalls = 0;
8
+ patchCalls = 0;
9
+ unmountCalls = 0;
10
+ patchedWith;
11
+ constructor(key) {
12
+ super();
13
+ this.key = key;
14
+ }
15
+ mount(parent) {
16
+ this.mountCalls++;
17
+ this.parent = parent;
18
+ this.elm = document.createTextNode(`mock-${this.key || "no-key"}`);
19
+ return this.elm;
20
+ }
21
+ patch(newNode) {
22
+ this.patchCalls++;
23
+ this.patchedWith = newNode;
24
+ // In real implementation, old node updates itself from new node
25
+ // but keeps the same object reference
26
+ }
27
+ unmount() {
28
+ this.unmountCalls++;
29
+ delete this.elm;
30
+ delete this.parent;
31
+ }
32
+ rerender() {
33
+ // No-op for mock
34
+ }
35
+ }
36
+ // Parent mock that has children
37
+ class MockParentVNode extends MockVNode {
38
+ constructor(initialChildren, key) {
39
+ super(key);
40
+ this.children = (initialChildren || []);
41
+ }
42
+ }
43
+ describe("patchChildren (new approach: keep old, patch in new)", () => {
44
+ describe("Edge cases", () => {
45
+ it("should mount all new children when starting with empty", () => {
46
+ const newChild1 = new MockVNode("a");
47
+ const newChild2 = new MockVNode("b");
48
+ const parent = new MockParentVNode([]);
49
+ const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
50
+ // New children should be mounted
51
+ expect(newChild1.mountCalls).toBe(1);
52
+ expect(newChild2.mountCalls).toBe(1);
53
+ expect(newChild1.parent).toBe(parent);
54
+ expect(newChild2.parent).toBe(parent);
55
+ // Result should be the new children (since old was empty)
56
+ expect(result).toEqual([newChild1, newChild2]);
57
+ });
58
+ it("should unmount all old children when new is empty", () => {
59
+ const oldChild1 = new MockVNode("a");
60
+ const oldChild2 = new MockVNode("b");
61
+ const parent = new MockParentVNode([oldChild1, oldChild2]);
62
+ const result = parent.patchChildren(toVNodes([]));
63
+ // Old children should be unmounted
64
+ expect(oldChild1.unmountCalls).toBe(1);
65
+ expect(oldChild2.unmountCalls).toBe(1);
66
+ // Result should be empty
67
+ expect(result).toEqual([]);
68
+ });
69
+ });
70
+ describe("Patching with keys", () => {
71
+ it("should reuse old VNodes when keys match", () => {
72
+ const oldChild1 = new MockVNode("a");
73
+ const oldChild2 = new MockVNode("b");
74
+ const oldChild3 = new MockVNode("c");
75
+ const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
76
+ const newChild1 = new MockVNode("a");
77
+ const newChild2 = new MockVNode("b");
78
+ const newChild3 = new MockVNode("c");
79
+ const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
80
+ // OLD children should be patched with new children
81
+ expect(oldChild1.patchCalls).toBe(1);
82
+ expect(oldChild2.patchCalls).toBe(1);
83
+ expect(oldChild3.patchCalls).toBe(1);
84
+ expect(oldChild1.patchedWith).toBe(newChild1);
85
+ expect(oldChild2.patchedWith).toBe(newChild2);
86
+ expect(oldChild3.patchedWith).toBe(newChild3);
87
+ // NEW children should NOT be mounted
88
+ expect(newChild1.mountCalls).toBe(0);
89
+ expect(newChild2.mountCalls).toBe(0);
90
+ expect(newChild3.mountCalls).toBe(0);
91
+ // OLD children should NOT be unmounted
92
+ expect(oldChild1.unmountCalls).toBe(0);
93
+ expect(oldChild2.unmountCalls).toBe(0);
94
+ expect(oldChild3.unmountCalls).toBe(0);
95
+ // Result should still be the OLD children (reused)
96
+ expect(result).toEqual([oldChild1, oldChild2, oldChild3]);
97
+ });
98
+ it("should handle reordered children with keys", () => {
99
+ const oldChild1 = new MockVNode("a");
100
+ const oldChild2 = new MockVNode("b");
101
+ const oldChild3 = new MockVNode("c");
102
+ const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
103
+ // Reordered: c, a, b
104
+ const newChild1 = new MockVNode("c");
105
+ const newChild2 = new MockVNode("a");
106
+ const newChild3 = new MockVNode("b");
107
+ const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
108
+ // Old nodes should be patched with corresponding new nodes by key
109
+ expect(oldChild1.patchedWith).toBe(newChild2); // a->a
110
+ expect(oldChild2.patchedWith).toBe(newChild3); // b->b
111
+ expect(oldChild3.patchedWith).toBe(newChild1); // c->c
112
+ // No unmounts
113
+ expect(oldChild1.unmountCalls).toBe(0);
114
+ expect(oldChild2.unmountCalls).toBe(0);
115
+ expect(oldChild3.unmountCalls).toBe(0);
116
+ // Result should be old children in NEW order (c, a, b)
117
+ expect(result).toEqual([oldChild3, oldChild1, oldChild2]);
118
+ // Verify correct keys
119
+ expect(result[0].key).toBe("c");
120
+ expect(result[1].key).toBe("a");
121
+ expect(result[2].key).toBe("b");
122
+ });
123
+ it("should mount new children and unmount removed children", () => {
124
+ const oldChild1 = new MockVNode("a");
125
+ const oldChild2 = new MockVNode("b");
126
+ const oldChild3 = new MockVNode("c");
127
+ const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
128
+ // Remove 'b', add 'd'
129
+ const newChild1 = new MockVNode("a");
130
+ const newChild2 = new MockVNode("c");
131
+ const newChild3 = new MockVNode("d");
132
+ const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
133
+ // a and c should be patched (reused)
134
+ expect(oldChild1.patchedWith).toBe(newChild1);
135
+ expect(oldChild3.patchedWith).toBe(newChild2);
136
+ // d should be mounted
137
+ expect(newChild3.mountCalls).toBe(1);
138
+ // b should be unmounted
139
+ expect(oldChild2.unmountCalls).toBe(1);
140
+ // Result should contain old a, old c, and new d
141
+ expect(result).toContain(oldChild1);
142
+ expect(result).toContain(oldChild3);
143
+ expect(result).toContain(newChild3);
144
+ expect(result).not.toContain(oldChild2);
145
+ expect(result.length).toBe(3);
146
+ });
147
+ it("should replace all children when all keys change", () => {
148
+ const oldChild1 = new MockVNode("a");
149
+ const oldChild2 = new MockVNode("b");
150
+ const parent = new MockParentVNode([oldChild1, oldChild2]);
151
+ const newChild1 = new MockVNode("x");
152
+ const newChild2 = new MockVNode("y");
153
+ const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
154
+ // All new children should be mounted
155
+ expect(newChild1.mountCalls).toBe(1);
156
+ expect(newChild2.mountCalls).toBe(1);
157
+ // All old children should be unmounted
158
+ expect(oldChild1.unmountCalls).toBe(1);
159
+ expect(oldChild2.unmountCalls).toBe(1);
160
+ // Result should be the new children
161
+ expect(result).toEqual([newChild1, newChild2]);
162
+ });
163
+ });
164
+ describe("Patching without keys (index-based)", () => {
165
+ it("should patch children by index when no keys", () => {
166
+ const oldChild1 = new MockVNode();
167
+ const oldChild2 = new MockVNode();
168
+ const oldChild3 = new MockVNode();
169
+ const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
170
+ const newChild1 = new MockVNode();
171
+ const newChild2 = new MockVNode();
172
+ const newChild3 = new MockVNode();
173
+ const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
174
+ // Should patch by index: 0->0, 1->1, 2->2
175
+ expect(oldChild1.patchedWith).toBe(newChild1);
176
+ expect(oldChild2.patchedWith).toBe(newChild2);
177
+ expect(oldChild3.patchedWith).toBe(newChild3);
178
+ // No unmounts
179
+ expect(oldChild1.unmountCalls).toBe(0);
180
+ expect(oldChild2.unmountCalls).toBe(0);
181
+ expect(oldChild3.unmountCalls).toBe(0);
182
+ // Result should be old children (reused)
183
+ expect(result).toEqual([oldChild1, oldChild2, oldChild3]);
184
+ });
185
+ it("should mount new children when growing without keys", () => {
186
+ const oldChild1 = new MockVNode();
187
+ const oldChild2 = new MockVNode();
188
+ const parent = new MockParentVNode([oldChild1, oldChild2]);
189
+ const newChild1 = new MockVNode();
190
+ const newChild2 = new MockVNode();
191
+ const newChild3 = new MockVNode();
192
+ const newChild4 = new MockVNode();
193
+ const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3, newChild4]));
194
+ // First two should be patched (reused)
195
+ expect(oldChild1.patchedWith).toBe(newChild1);
196
+ expect(oldChild2.patchedWith).toBe(newChild2);
197
+ // Last two should be mounted
198
+ expect(newChild3.mountCalls).toBe(1);
199
+ expect(newChild4.mountCalls).toBe(1);
200
+ // No unmounts
201
+ expect(oldChild1.unmountCalls).toBe(0);
202
+ expect(oldChild2.unmountCalls).toBe(0);
203
+ // Result should be [oldChild1, oldChild2, newChild3, newChild4]
204
+ expect(result).toEqual([oldChild1, oldChild2, newChild3, newChild4]);
205
+ });
206
+ it("should unmount old children when shrinking without keys", () => {
207
+ const oldChild1 = new MockVNode();
208
+ const oldChild2 = new MockVNode();
209
+ const oldChild3 = new MockVNode();
210
+ const oldChild4 = new MockVNode();
211
+ const parent = new MockParentVNode([
212
+ oldChild1,
213
+ oldChild2,
214
+ oldChild3,
215
+ oldChild4,
216
+ ]);
217
+ const newChild1 = new MockVNode();
218
+ const newChild2 = new MockVNode();
219
+ const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
220
+ // First two should be patched
221
+ expect(oldChild1.patchedWith).toBe(newChild1);
222
+ expect(oldChild2.patchedWith).toBe(newChild2);
223
+ // Last two old children should be unmounted
224
+ expect(oldChild3.unmountCalls).toBe(1);
225
+ expect(oldChild4.unmountCalls).toBe(1);
226
+ // First two should not be unmounted
227
+ expect(oldChild1.unmountCalls).toBe(0);
228
+ expect(oldChild2.unmountCalls).toBe(0);
229
+ // Result should be [oldChild1, oldChild2]
230
+ expect(result).toEqual([oldChild1, oldChild2]);
231
+ });
232
+ });
233
+ describe("Mixed keys and indices", () => {
234
+ it("should handle mix of keyed and non-keyed children", () => {
235
+ const oldChild1 = new MockVNode("a"); // key: "a"
236
+ const oldChild2 = new MockVNode(); // key: undefined -> index 1
237
+ const oldChild3 = new MockVNode("c"); // key: "c"
238
+ const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
239
+ const newChild1 = new MockVNode("a"); // key: "a"
240
+ const newChild2 = new MockVNode(); // key: undefined -> index 1
241
+ const newChild3 = new MockVNode("c"); // key: "c"
242
+ const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
243
+ // Keyed children should patch by key
244
+ expect(oldChild1.patchedWith).toBe(newChild1);
245
+ expect(oldChild3.patchedWith).toBe(newChild3);
246
+ // Non-keyed child should patch by index
247
+ expect(oldChild2.patchedWith).toBe(newChild2);
248
+ // No unmounts
249
+ expect(oldChild1.unmountCalls).toBe(0);
250
+ expect(oldChild2.unmountCalls).toBe(0);
251
+ expect(oldChild3.unmountCalls).toBe(0);
252
+ // Result should be old children (reused)
253
+ expect(result).toEqual([oldChild1, oldChild2, oldChild3]);
254
+ });
255
+ });
256
+ describe("Real-world scenarios", () => {
257
+ it("should handle conditional rendering (null -> component)", () => {
258
+ const oldChild1 = new MockVNode("title");
259
+ const parent = new MockParentVNode([oldChild1]);
260
+ const newChild1 = new MockVNode("title");
261
+ const newChild2 = new MockVNode("details");
262
+ const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
263
+ // Title should be patched (reused)
264
+ expect(oldChild1.patchedWith).toBe(newChild1);
265
+ // Details should be mounted
266
+ expect(newChild2.mountCalls).toBe(1);
267
+ // No unmounts
268
+ expect(oldChild1.unmountCalls).toBe(0);
269
+ // Result should be [oldChild1, newChild2]
270
+ expect(result).toEqual([oldChild1, newChild2]);
271
+ });
272
+ it("should handle conditional rendering (component -> null)", () => {
273
+ const oldChild1 = new MockVNode("title");
274
+ const oldChild2 = new MockVNode("details");
275
+ const parent = new MockParentVNode([oldChild1, oldChild2]);
276
+ const newChild1 = new MockVNode("title");
277
+ const result = parent.patchChildren(toVNodes([newChild1]));
278
+ // Title should be patched (reused)
279
+ expect(oldChild1.patchedWith).toBe(newChild1);
280
+ // Details should be unmounted
281
+ expect(oldChild2.unmountCalls).toBe(1);
282
+ // Title should not be unmounted
283
+ expect(oldChild1.unmountCalls).toBe(0);
284
+ // Result should be [oldChild1]
285
+ expect(result).toEqual([oldChild1]);
286
+ });
287
+ it("should handle list with items added at beginning", () => {
288
+ const oldChild1 = new MockVNode("item-1");
289
+ const oldChild2 = new MockVNode("item-2");
290
+ const parent = new MockParentVNode([oldChild1, oldChild2]);
291
+ const newChild1 = new MockVNode("item-0"); // New item at start
292
+ const newChild2 = new MockVNode("item-1");
293
+ const newChild3 = new MockVNode("item-2");
294
+ const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
295
+ // New item should be mounted
296
+ expect(newChild1.mountCalls).toBe(1);
297
+ // Existing items should be patched (reused)
298
+ expect(oldChild1.patchedWith).toBe(newChild2);
299
+ expect(oldChild2.patchedWith).toBe(newChild3);
300
+ // No unmounts
301
+ expect(oldChild1.unmountCalls).toBe(0);
302
+ expect(oldChild2.unmountCalls).toBe(0);
303
+ // Result should be [newChild1, oldChild1, oldChild2]
304
+ expect(result).toEqual([newChild1, oldChild1, oldChild2]);
305
+ });
306
+ it("should handle list with items added at end", () => {
307
+ const oldChild1 = new MockVNode("item-1");
308
+ const oldChild2 = new MockVNode("item-2");
309
+ const parent = new MockParentVNode([oldChild1, oldChild2]);
310
+ const newChild1 = new MockVNode("item-1");
311
+ const newChild2 = new MockVNode("item-2");
312
+ const newChild3 = new MockVNode("item-3"); // New item at end
313
+ const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
314
+ // Existing items should be patched (reused)
315
+ expect(oldChild1.patchedWith).toBe(newChild1);
316
+ expect(oldChild2.patchedWith).toBe(newChild2);
317
+ // New item should be mounted
318
+ expect(newChild3.mountCalls).toBe(1);
319
+ // No unmounts
320
+ expect(oldChild1.unmountCalls).toBe(0);
321
+ expect(oldChild2.unmountCalls).toBe(0);
322
+ // Result should be [oldChild1, oldChild2, newChild3]
323
+ expect(result).toEqual([oldChild1, oldChild2, newChild3]);
324
+ });
325
+ it("should handle list with item removed from middle", () => {
326
+ const oldChild1 = new MockVNode("item-1");
327
+ const oldChild2 = new MockVNode("item-2");
328
+ const oldChild3 = new MockVNode("item-3");
329
+ const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
330
+ const newChild1 = new MockVNode("item-1");
331
+ const newChild2 = new MockVNode("item-3"); // item-2 removed
332
+ const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
333
+ // item-1 and item-3 should be patched (reused)
334
+ expect(oldChild1.patchedWith).toBe(newChild1);
335
+ expect(oldChild3.patchedWith).toBe(newChild2);
336
+ // item-2 should be unmounted
337
+ expect(oldChild2.unmountCalls).toBe(1);
338
+ // Others should not be unmounted
339
+ expect(oldChild1.unmountCalls).toBe(0);
340
+ expect(oldChild3.unmountCalls).toBe(0);
341
+ // Result should be [oldChild1, oldChild3]
342
+ expect(result).toEqual([oldChild1, oldChild3]);
343
+ });
344
+ it("should handle empty -> multiple children", () => {
345
+ const parent = new MockParentVNode([]);
346
+ const newChild1 = new MockVNode("a");
347
+ const newChild2 = new MockVNode("b");
348
+ const newChild3 = new MockVNode("c");
349
+ const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
350
+ // All should be mounted
351
+ expect(newChild1.mountCalls).toBe(1);
352
+ expect(newChild2.mountCalls).toBe(1);
353
+ expect(newChild3.mountCalls).toBe(1);
354
+ // Result should be the new children
355
+ expect(result).toEqual([newChild1, newChild2, newChild3]);
356
+ });
357
+ it("should handle multiple children -> empty", () => {
358
+ const oldChild1 = new MockVNode("a");
359
+ const oldChild2 = new MockVNode("b");
360
+ const oldChild3 = new MockVNode("c");
361
+ const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
362
+ const result = parent.patchChildren(toVNodes([]));
363
+ // All should be unmounted
364
+ expect(oldChild1.unmountCalls).toBe(1);
365
+ expect(oldChild2.unmountCalls).toBe(1);
366
+ expect(oldChild3.unmountCalls).toBe(1);
367
+ // Result should be empty
368
+ expect(result).toEqual([]);
369
+ });
370
+ });
371
+ describe("Object reference preservation", () => {
372
+ it("should preserve old VNode object references when patching", () => {
373
+ const oldChild1 = new MockVNode("a");
374
+ const oldChild2 = new MockVNode("b");
375
+ const parent = new MockParentVNode([oldChild1, oldChild2]);
376
+ const newChild1 = new MockVNode("a");
377
+ const newChild2 = new MockVNode("b");
378
+ const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
379
+ // The result should contain the EXACT SAME object references as the old children
380
+ expect(result[0]).toBe(oldChild1); // Same object reference
381
+ expect(result[1]).toBe(oldChild2); // Same object reference
382
+ // NOT the new children
383
+ expect(result[0]).not.toBe(newChild1);
384
+ expect(result[1]).not.toBe(newChild2);
385
+ });
386
+ });
387
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=primitives.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"primitives.test.d.ts","sourceRoot":"","sources":["../../src/tests/primitives.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,132 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { jsx, render } from "../vdom/index";
3
+ describe("VDOM Primitives", () => {
4
+ it("should render element with string child", () => {
5
+ // Create a container element
6
+ const container = document.createElement("div");
7
+ // Create vnode with string child
8
+ const vnode = jsx("div", {
9
+ children: "Hello World",
10
+ });
11
+ // Render the vnode
12
+ render(vnode, container);
13
+ // Verify string was rendered as text content
14
+ const div = container.children[0];
15
+ expect(div.textContent).toBe("Hello World");
16
+ expect(div.childNodes.length).toBe(1);
17
+ expect(div.childNodes[0].nodeType).toBe(Node.TEXT_NODE);
18
+ });
19
+ it("should render element with number child", () => {
20
+ // Create a container element
21
+ const container = document.createElement("div");
22
+ // Create vnode with number child
23
+ const vnode = jsx("span", {
24
+ children: 42,
25
+ });
26
+ // Render the vnode
27
+ render(vnode, container);
28
+ // Verify number was rendered as text content
29
+ const span = container.children[0];
30
+ expect(span.textContent).toBe("42");
31
+ expect(span.childNodes.length).toBe(1);
32
+ expect(span.childNodes[0].nodeType).toBe(Node.TEXT_NODE);
33
+ });
34
+ it("should render element with null child", () => {
35
+ // Create a container element
36
+ const container = document.createElement("div");
37
+ // Create vnode with null child
38
+ const vnode = jsx("div", {
39
+ children: null,
40
+ });
41
+ // Render the vnode
42
+ render(vnode, container);
43
+ // Verify null renders as empty
44
+ const div = container.children[0];
45
+ expect(div.childNodes.length).toBe(0);
46
+ });
47
+ it("should render element with undefined child", () => {
48
+ // Create a container element
49
+ const container = document.createElement("div");
50
+ // Create vnode with undefined child
51
+ const vnode = jsx("div", {
52
+ children: undefined,
53
+ });
54
+ // Render the vnode
55
+ render(vnode, container);
56
+ // Verify undefined renders as empty
57
+ const div = container.children[0];
58
+ expect(div.childNodes.length).toBe(0);
59
+ });
60
+ it("should render element with mixed children including primitives", () => {
61
+ // Create a container element
62
+ const container = document.createElement("div");
63
+ // Create vnode with mixed children
64
+ const vnode = jsx("div", {
65
+ children: [
66
+ "Text before",
67
+ jsx("span", { textContent: "middle" }),
68
+ "Text after",
69
+ 42,
70
+ null,
71
+ jsx("div", { textContent: "end" }),
72
+ ],
73
+ });
74
+ // Render the vnode
75
+ render(vnode, container);
76
+ // Verify mixed children were rendered correctly
77
+ const div = container.children[0];
78
+ // Should have: text node, span, text node, text node (42), div
79
+ // null should be skipped
80
+ expect(div.childNodes.length).toBe(5);
81
+ expect(div.childNodes[0].nodeType).toBe(Node.TEXT_NODE);
82
+ expect(div.childNodes[0].textContent).toBe("Text before");
83
+ expect(div.childNodes[1].tagName).toBe("SPAN");
84
+ expect(div.childNodes[2].textContent).toBe("Text after");
85
+ expect(div.childNodes[3].textContent).toBe("42");
86
+ expect(div.childNodes[4].tagName).toBe("DIV");
87
+ });
88
+ it("should render element with boolean children", () => {
89
+ // Create a container element
90
+ const container = document.createElement("div");
91
+ // Create vnode with boolean children
92
+ const vnode = jsx("div", {
93
+ children: [true, false, "visible"],
94
+ });
95
+ // Render the vnode
96
+ render(vnode, container);
97
+ // Verify booleans render as empty (like React)
98
+ const div = container.children[0];
99
+ // Should only have the text node "visible", booleans should be skipped
100
+ expect(div.childNodes.length).toBe(1);
101
+ expect(div.childNodes[0].textContent).toBe("visible");
102
+ });
103
+ it("should render element with zero as child", () => {
104
+ // Create a container element
105
+ const container = document.createElement("div");
106
+ // Create vnode with zero as child (should render, not be treated as falsy)
107
+ const vnode = jsx("div", {
108
+ children: 0,
109
+ });
110
+ // Render the vnode
111
+ render(vnode, container);
112
+ // Verify zero is rendered
113
+ const div = container.children[0];
114
+ expect(div.textContent).toBe("0");
115
+ expect(div.childNodes.length).toBe(1);
116
+ });
117
+ it("should render element with empty string as child", () => {
118
+ // Create a container element
119
+ const container = document.createElement("div");
120
+ // Create vnode with empty string as child
121
+ const vnode = jsx("div", {
122
+ children: "",
123
+ });
124
+ // Render the vnode
125
+ render(vnode, container);
126
+ // Verify empty string renders as empty text node
127
+ const div = container.children[0];
128
+ expect(div.textContent).toBe("");
129
+ expect(div.childNodes.length).toBe(1);
130
+ expect(div.childNodes[0].nodeType).toBe(Node.TEXT_NODE);
131
+ });
132
+ });
@@ -0,0 +1,22 @@
1
+ import { RootVNode } from "./RootVNode";
2
+ import { VNode } from "./types";
3
+ export declare abstract class AbstractVNode {
4
+ key?: string;
5
+ parent?: VNode;
6
+ root?: RootVNode;
7
+ elm?: Node;
8
+ children?: VNode[];
9
+ abstract mount(parent?: VNode): Node | Node[];
10
+ abstract patch(oldNode: VNode): void;
11
+ abstract unmount(): void;
12
+ abstract rerender(): void;
13
+ protected getHTMLElement(): HTMLElement;
14
+ /**
15
+ * A VNode can represent multiple elements (fragment of component)
16
+ */
17
+ getElements(): Node[];
18
+ getParentElement(): HTMLElement;
19
+ protected canPatch(oldNode: VNode, newNode: VNode): boolean;
20
+ patchChildren(newChildren: VNode[]): VNode[];
21
+ }
22
+ //# sourceMappingURL=AbstractVNode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AbstractVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/AbstractVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,8BAAsB,aAAa;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC;IACnB,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI,GAAG,IAAI,EAAE;IAC7C,QAAQ,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,GAAG,IAAI;IACpC,QAAQ,CAAC,OAAO,IAAI,IAAI;IACxB,QAAQ,CAAC,QAAQ,IAAI,IAAI;IACzB,SAAS,CAAC,cAAc;IAOxB;;OAEG;IACH,WAAW,IAAI,IAAI,EAAE;IAWrB,gBAAgB,IAAI,WAAW;IAiB/B,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,GAAG,OAAO;IAoB3D,aAAa,CAAC,WAAW,EAAE,KAAK,EAAE,GAAG,KAAK,EAAE;CA2D7C"}
@@ -0,0 +1,106 @@
1
+ export class AbstractVNode {
2
+ key;
3
+ parent;
4
+ root;
5
+ elm;
6
+ children;
7
+ getHTMLElement() {
8
+ if (!this.elm || !(this.elm instanceof HTMLElement)) {
9
+ throw new Error("This VNode does not have an HTMLElement");
10
+ }
11
+ return this.elm;
12
+ }
13
+ /**
14
+ * A VNode can represent multiple elements (fragment of component)
15
+ */
16
+ getElements() {
17
+ if (this.elm) {
18
+ return [this.elm];
19
+ }
20
+ if (!this.children) {
21
+ throw new Error("This VNode has no element or children");
22
+ }
23
+ return this.children.map((child) => child.getElements()).flat();
24
+ }
25
+ getParentElement() {
26
+ let parent = this.parent;
27
+ // This VNode might not have an element, but relies
28
+ // on a parent for it. So we make sure that we get
29
+ // the actual parent of the element related to this VNode
30
+ while (parent) {
31
+ if (parent.elm instanceof HTMLElement) {
32
+ // This will always be an HTMLElement as text nodes has no children
33
+ return parent.elm;
34
+ }
35
+ parent = parent.parent;
36
+ }
37
+ throw new Error("There is no parent element for this VNode");
38
+ }
39
+ canPatch(oldNode, newNode) {
40
+ // Must be same constructor type
41
+ if (oldNode.constructor !== newNode.constructor) {
42
+ return false;
43
+ }
44
+ // For ElementVNodes, must have same tag
45
+ if ("tag" in oldNode && "tag" in newNode) {
46
+ return oldNode.tag === newNode.tag;
47
+ }
48
+ // For ComponentVNodes, must have same component function
49
+ if ("component" in oldNode && "component" in newNode) {
50
+ return oldNode.component === newNode.component;
51
+ }
52
+ // TextVNodes and FragmentVNodes can always patch
53
+ return true;
54
+ }
55
+ patchChildren(newChildren) {
56
+ const prevChildren = this.children;
57
+ // When there are only new children, we just mount them
58
+ if (newChildren && prevChildren.length === 0) {
59
+ newChildren.forEach((child) => child.mount(this));
60
+ return newChildren;
61
+ }
62
+ // If we want to remove all children, we just unmount the previous ones
63
+ if (!newChildren.length && prevChildren.length) {
64
+ prevChildren.forEach((child) => child.unmount());
65
+ return [];
66
+ }
67
+ const oldKeys = {};
68
+ prevChildren.forEach((prevChild, index) => {
69
+ oldKeys[prevChild.key || index] = prevChild;
70
+ });
71
+ // Build result array in the NEW order
72
+ const result = [];
73
+ newChildren.forEach((newChild, index) => {
74
+ const key = newChild.key || index;
75
+ const prevChild = oldKeys[key];
76
+ if (!prevChild) {
77
+ // New child - mount and add to result
78
+ newChild.mount(this);
79
+ result.push(newChild);
80
+ }
81
+ else if (prevChild === newChild) {
82
+ // Same instance - no patching needed, just reuse
83
+ result.push(prevChild);
84
+ delete oldKeys[key];
85
+ }
86
+ else if (this.canPatch(prevChild, newChild)) {
87
+ // Compatible types - patch and reuse old VNode
88
+ prevChild.patch(newChild);
89
+ result.push(prevChild);
90
+ delete oldKeys[key];
91
+ }
92
+ else {
93
+ // Incompatible types - replace completely
94
+ newChild.mount(this);
95
+ prevChild.unmount();
96
+ result.push(newChild);
97
+ delete oldKeys[key];
98
+ }
99
+ });
100
+ // Unmount any old children that weren't reused
101
+ for (const key in oldKeys) {
102
+ oldKeys[key].unmount();
103
+ }
104
+ return result;
105
+ }
106
+ }