@praxisjs/decorators 1.7.0 → 2.0.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.
Files changed (266) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/component/component.d.ts +1 -1
  3. package/dist/component/component.d.ts.map +1 -1
  4. package/dist/component/component.js +8 -15
  5. package/dist/component/component.js.map +1 -1
  6. package/dist/component/lazy.d.ts +1 -1
  7. package/dist/component/lazy.d.ts.map +1 -1
  8. package/dist/component/lazy.js +44 -36
  9. package/dist/component/lazy.js.map +1 -1
  10. package/dist/create-class-decorator.d.ts +27 -5
  11. package/dist/create-class-decorator.d.ts.map +1 -1
  12. package/dist/create-class-decorator.js +53 -18
  13. package/dist/create-class-decorator.js.map +1 -1
  14. package/dist/create-field-decorator.d.ts +42 -3
  15. package/dist/create-field-decorator.d.ts.map +1 -1
  16. package/dist/create-field-decorator.js +63 -27
  17. package/dist/create-field-decorator.js.map +1 -1
  18. package/dist/create-getter-decorator.d.ts +81 -16
  19. package/dist/create-getter-decorator.d.ts.map +1 -1
  20. package/dist/create-getter-decorator.js +127 -15
  21. package/dist/create-getter-decorator.js.map +1 -1
  22. package/dist/create-lifecycle-method-decorator.d.ts +21 -3
  23. package/dist/create-lifecycle-method-decorator.d.ts.map +1 -1
  24. package/dist/create-lifecycle-method-decorator.js +25 -18
  25. package/dist/create-lifecycle-method-decorator.js.map +1 -1
  26. package/dist/create-method-decorator.d.ts +43 -3
  27. package/dist/create-method-decorator.d.ts.map +1 -1
  28. package/dist/create-method-decorator.js +49 -8
  29. package/dist/create-method-decorator.js.map +1 -1
  30. package/dist/events/command.d.ts +2 -1
  31. package/dist/events/command.d.ts.map +1 -1
  32. package/dist/events/command.js +10 -8
  33. package/dist/events/command.js.map +1 -1
  34. package/dist/events/emit.d.ts +2 -2
  35. package/dist/events/emit.d.ts.map +1 -1
  36. package/dist/events/emit.js +18 -24
  37. package/dist/events/emit.js.map +1 -1
  38. package/dist/events/on-command.d.ts +2 -1
  39. package/dist/events/on-command.d.ts.map +1 -1
  40. package/dist/events/on-command.js +12 -14
  41. package/dist/events/on-command.js.map +1 -1
  42. package/dist/field.d.ts +51 -0
  43. package/dist/field.d.ts.map +1 -0
  44. package/dist/field.js +82 -0
  45. package/dist/field.js.map +1 -0
  46. package/dist/functions/bind.d.ts +2 -1
  47. package/dist/functions/bind.d.ts.map +1 -1
  48. package/dist/functions/bind.js +4 -4
  49. package/dist/functions/bind.js.map +1 -1
  50. package/dist/functions/debounce.d.ts +2 -2
  51. package/dist/functions/debounce.d.ts.map +1 -1
  52. package/dist/functions/debounce.js +25 -23
  53. package/dist/functions/debounce.js.map +1 -1
  54. package/dist/functions/log.d.ts +2 -1
  55. package/dist/functions/log.d.ts.map +1 -1
  56. package/dist/functions/log.js +38 -40
  57. package/dist/functions/log.js.map +1 -1
  58. package/dist/functions/memo.d.ts +2 -1
  59. package/dist/functions/memo.d.ts.map +1 -1
  60. package/dist/functions/memo.js +41 -40
  61. package/dist/functions/memo.js.map +1 -1
  62. package/dist/functions/once.d.ts +2 -1
  63. package/dist/functions/once.d.ts.map +1 -1
  64. package/dist/functions/once.js +10 -14
  65. package/dist/functions/once.js.map +1 -1
  66. package/dist/functions/retry.d.ts +2 -1
  67. package/dist/functions/retry.d.ts.map +1 -1
  68. package/dist/functions/retry.js +21 -23
  69. package/dist/functions/retry.js.map +1 -1
  70. package/dist/functions/throttle.d.ts +2 -1
  71. package/dist/functions/throttle.d.ts.map +1 -1
  72. package/dist/functions/throttle.js +12 -14
  73. package/dist/functions/throttle.js.map +1 -1
  74. package/dist/functions/until.d.ts +2 -1
  75. package/dist/functions/until.d.ts.map +1 -1
  76. package/dist/functions/until.js +13 -14
  77. package/dist/functions/until.js.map +1 -1
  78. package/dist/functions/watch.d.ts +1 -1
  79. package/dist/functions/watch.d.ts.map +1 -1
  80. package/dist/functions/watch.js +36 -34
  81. package/dist/functions/watch.js.map +1 -1
  82. package/dist/functions/when.d.ts +1 -1
  83. package/dist/functions/when.d.ts.map +1 -1
  84. package/dist/functions/when.js +19 -21
  85. package/dist/functions/when.js.map +1 -1
  86. package/dist/index.d.ts +5 -5
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +2 -2
  89. package/dist/index.js.map +1 -1
  90. package/dist/lifecycle.d.ts +24 -0
  91. package/dist/lifecycle.d.ts.map +1 -0
  92. package/dist/lifecycle.js +55 -0
  93. package/dist/lifecycle.js.map +1 -0
  94. package/dist/properties/compose.d.ts +3 -2
  95. package/dist/properties/compose.d.ts.map +1 -1
  96. package/dist/properties/compose.js +33 -19
  97. package/dist/properties/compose.js.map +1 -1
  98. package/dist/properties/computed.d.ts +12 -10
  99. package/dist/properties/computed.d.ts.map +1 -1
  100. package/dist/properties/computed.js +26 -21
  101. package/dist/properties/computed.js.map +1 -1
  102. package/dist/properties/deep-state.d.ts +2 -1
  103. package/dist/properties/deep-state.d.ts.map +1 -1
  104. package/dist/properties/deep-state.js +66 -60
  105. package/dist/properties/deep-state.js.map +1 -1
  106. package/dist/properties/function-prop.d.ts +2 -1
  107. package/dist/properties/function-prop.d.ts.map +1 -1
  108. package/dist/properties/function-prop.js +20 -22
  109. package/dist/properties/function-prop.js.map +1 -1
  110. package/dist/properties/history.d.ts +2 -2
  111. package/dist/properties/history.d.ts.map +1 -1
  112. package/dist/properties/history.js +34 -33
  113. package/dist/properties/history.js.map +1 -1
  114. package/dist/properties/index.d.ts +1 -0
  115. package/dist/properties/index.d.ts.map +1 -1
  116. package/dist/properties/index.js +1 -0
  117. package/dist/properties/index.js.map +1 -1
  118. package/dist/properties/persisted.d.ts +2 -1
  119. package/dist/properties/persisted.d.ts.map +1 -1
  120. package/dist/properties/persisted.js +24 -18
  121. package/dist/properties/persisted.js.map +1 -1
  122. package/dist/properties/prop.d.ts +2 -1
  123. package/dist/properties/prop.d.ts.map +1 -1
  124. package/dist/properties/prop.js +26 -22
  125. package/dist/properties/prop.js.map +1 -1
  126. package/dist/properties/ref.d.ts +2 -1
  127. package/dist/properties/ref.d.ts.map +1 -1
  128. package/dist/properties/ref.js +11 -8
  129. package/dist/properties/ref.js.map +1 -1
  130. package/dist/properties/resource.d.ts.map +1 -1
  131. package/dist/properties/resource.js +21 -16
  132. package/dist/properties/resource.js.map +1 -1
  133. package/dist/properties/selector.d.ts +12 -0
  134. package/dist/properties/selector.d.ts.map +1 -0
  135. package/dist/properties/selector.js +25 -0
  136. package/dist/properties/selector.js.map +1 -0
  137. package/dist/properties/slot.d.ts +2 -1
  138. package/dist/properties/slot.d.ts.map +1 -1
  139. package/dist/properties/slot.js +22 -20
  140. package/dist/properties/slot.js.map +1 -1
  141. package/dist/properties/state.d.ts.map +1 -1
  142. package/dist/properties/state.js +27 -16
  143. package/dist/properties/state.js.map +1 -1
  144. package/dist/properties/synced.d.ts +2 -1
  145. package/dist/properties/synced.d.ts.map +1 -1
  146. package/dist/properties/synced.js +27 -19
  147. package/dist/properties/synced.js.map +1 -1
  148. package/dist/shared-method.d.ts +14 -0
  149. package/dist/shared-method.d.ts.map +1 -0
  150. package/dist/shared-method.js +87 -0
  151. package/dist/shared-method.js.map +1 -0
  152. package/package.json +14 -5
  153. package/src/component/component.ts +8 -27
  154. package/src/component/lazy.ts +51 -41
  155. package/src/create-class-decorator.ts +90 -31
  156. package/src/create-field-decorator.ts +120 -43
  157. package/src/create-getter-decorator.ts +263 -66
  158. package/src/create-lifecycle-method-decorator.ts +50 -28
  159. package/src/create-method-decorator.ts +119 -13
  160. package/src/events/command.ts +11 -9
  161. package/src/events/emit.ts +16 -22
  162. package/src/events/on-command.ts +17 -18
  163. package/src/field.ts +116 -0
  164. package/src/functions/bind.ts +4 -4
  165. package/src/functions/debounce.ts +25 -23
  166. package/src/functions/log.ts +44 -46
  167. package/src/functions/memo.ts +39 -37
  168. package/src/functions/once.ts +10 -13
  169. package/src/functions/retry.ts +19 -21
  170. package/src/functions/throttle.ts +12 -13
  171. package/src/functions/until.ts +14 -14
  172. package/src/functions/watch.ts +37 -36
  173. package/src/functions/when.ts +18 -20
  174. package/src/index.ts +12 -5
  175. package/src/lifecycle.ts +74 -0
  176. package/src/properties/compose.ts +33 -20
  177. package/src/properties/computed.ts +35 -45
  178. package/src/properties/deep-state.ts +66 -64
  179. package/src/properties/function-prop.ts +25 -27
  180. package/src/properties/history.ts +34 -35
  181. package/src/properties/index.ts +1 -0
  182. package/src/properties/persisted.ts +30 -20
  183. package/src/properties/prop.ts +30 -22
  184. package/src/properties/ref.ts +12 -10
  185. package/src/properties/resource.ts +22 -17
  186. package/src/properties/selector.ts +45 -0
  187. package/src/properties/slot.ts +28 -22
  188. package/src/properties/state.ts +32 -16
  189. package/src/properties/synced.ts +31 -19
  190. package/src/shared-method.ts +101 -0
  191. package/dist/__tests__/component.test.d.ts +0 -2
  192. package/dist/__tests__/component.test.d.ts.map +0 -1
  193. package/dist/__tests__/component.test.js +0 -147
  194. package/dist/__tests__/component.test.js.map +0 -1
  195. package/dist/__tests__/compose.test.d.ts +0 -2
  196. package/dist/__tests__/compose.test.d.ts.map +0 -1
  197. package/dist/__tests__/compose.test.js +0 -164
  198. package/dist/__tests__/compose.test.js.map +0 -1
  199. package/dist/__tests__/deep-state.test.d.ts +0 -2
  200. package/dist/__tests__/deep-state.test.d.ts.map +0 -1
  201. package/dist/__tests__/deep-state.test.js +0 -221
  202. package/dist/__tests__/deep-state.test.js.map +0 -1
  203. package/dist/__tests__/events.test.d.ts +0 -2
  204. package/dist/__tests__/events.test.d.ts.map +0 -1
  205. package/dist/__tests__/events.test.js +0 -295
  206. package/dist/__tests__/events.test.js.map +0 -1
  207. package/dist/__tests__/functions.test.d.ts +0 -2
  208. package/dist/__tests__/functions.test.d.ts.map +0 -1
  209. package/dist/__tests__/functions.test.js +0 -618
  210. package/dist/__tests__/functions.test.js.map +0 -1
  211. package/dist/__tests__/lazy.test.d.ts +0 -2
  212. package/dist/__tests__/lazy.test.d.ts.map +0 -1
  213. package/dist/__tests__/lazy.test.js +0 -227
  214. package/dist/__tests__/lazy.test.js.map +0 -1
  215. package/dist/__tests__/memo.test.d.ts +0 -2
  216. package/dist/__tests__/memo.test.d.ts.map +0 -1
  217. package/dist/__tests__/memo.test.js +0 -249
  218. package/dist/__tests__/memo.test.js.map +0 -1
  219. package/dist/__tests__/properties-browser.test.d.ts +0 -2
  220. package/dist/__tests__/properties-browser.test.d.ts.map +0 -1
  221. package/dist/__tests__/properties-browser.test.js +0 -227
  222. package/dist/__tests__/properties-browser.test.js.map +0 -1
  223. package/dist/__tests__/properties.test.d.ts +0 -2
  224. package/dist/__tests__/properties.test.d.ts.map +0 -1
  225. package/dist/__tests__/properties.test.js +0 -1253
  226. package/dist/__tests__/properties.test.js.map +0 -1
  227. package/dist/__tests__/ref.test.d.ts +0 -2
  228. package/dist/__tests__/ref.test.d.ts.map +0 -1
  229. package/dist/__tests__/ref.test.js +0 -106
  230. package/dist/__tests__/ref.test.js.map +0 -1
  231. package/dist/__tests__/resource.test.d.ts +0 -2
  232. package/dist/__tests__/resource.test.d.ts.map +0 -1
  233. package/dist/__tests__/resource.test.js +0 -217
  234. package/dist/__tests__/resource.test.js.map +0 -1
  235. package/dist/__tests__/slot.test.d.ts +0 -2
  236. package/dist/__tests__/slot.test.d.ts.map +0 -1
  237. package/dist/__tests__/slot.test.js +0 -147
  238. package/dist/__tests__/slot.test.js.map +0 -1
  239. package/dist/__tests__/synced.test.d.ts +0 -2
  240. package/dist/__tests__/synced.test.d.ts.map +0 -1
  241. package/dist/__tests__/synced.test.js +0 -161
  242. package/dist/__tests__/synced.test.js.map +0 -1
  243. package/dist/__tests__/until.test.d.ts +0 -2
  244. package/dist/__tests__/until.test.d.ts.map +0 -1
  245. package/dist/__tests__/until.test.js +0 -88
  246. package/dist/__tests__/until.test.js.map +0 -1
  247. package/dist/create-accessor-decorator.d.ts +0 -14
  248. package/dist/create-accessor-decorator.d.ts.map +0 -1
  249. package/dist/create-accessor-decorator.js +0 -27
  250. package/dist/create-accessor-decorator.js.map +0 -1
  251. package/src/__tests__/component.test.ts +0 -167
  252. package/src/__tests__/compose.test.ts +0 -202
  253. package/src/__tests__/deep-state.test.ts +0 -265
  254. package/src/__tests__/events.test.ts +0 -366
  255. package/src/__tests__/functions.test.ts +0 -803
  256. package/src/__tests__/lazy.test.ts +0 -296
  257. package/src/__tests__/memo.test.ts +0 -293
  258. package/src/__tests__/properties-browser.test.ts +0 -288
  259. package/src/__tests__/properties.test.ts +0 -1556
  260. package/src/__tests__/ref.test.ts +0 -131
  261. package/src/__tests__/resource.test.ts +0 -246
  262. package/src/__tests__/slot.test.ts +0 -179
  263. package/src/__tests__/synced.test.ts +0 -193
  264. package/src/__tests__/until.test.ts +0 -113
  265. package/src/create-accessor-decorator.ts +0 -45
  266. package/tsconfig.json +0 -9
@@ -1,296 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { beforeEach, afterEach, describe, it, expect, vi } from "vitest";
3
-
4
- import { StatefulComponent } from "@praxisjs/core";
5
- import { setComponentAnchor } from "@praxisjs/core/internal";
6
- import { Lazy } from "../component/lazy";
7
- import type { LazyOptions } from "../component/lazy";
8
-
9
- type AnyConstructor = new (...args: unknown[]) => StatefulComponent;
10
-
11
- function applyLazy<T extends AnyConstructor>(cls: T, placeholderOrOptions?: number | LazyOptions): T {
12
- if (placeholderOrOptions === undefined) return Lazy()(cls, {} as ClassDecoratorContext);
13
- return Lazy(placeholderOrOptions)(cls, {} as ClassDecoratorContext);
14
- }
15
-
16
- /** render() now returns a reactive thunk — resolve it to get the actual DOM output. */
17
- function resolveRender(instance: StatefulComponent): Node | Node[] | null {
18
- const result = instance.render();
19
- if (typeof result === "function") return (result as () => Node | Node[] | null)();
20
- return result;
21
- }
22
-
23
- class BaseComp extends StatefulComponent {
24
- render() {
25
- return document.createTextNode("content");
26
- }
27
- }
28
-
29
- describe("Lazy decorator", () => {
30
- let intersectionCallback: IntersectionObserverCallback = () => {};
31
- let mockObserve: ReturnType<typeof vi.fn>;
32
- let mockDisconnect: ReturnType<typeof vi.fn>;
33
-
34
- beforeEach(() => {
35
- intersectionCallback = () => {};
36
- mockObserve = vi.fn();
37
- mockDisconnect = vi.fn();
38
-
39
- globalThis.IntersectionObserver = class {
40
- constructor(cb: IntersectionObserverCallback) {
41
- intersectionCallback = cb;
42
- }
43
- observe = mockObserve;
44
- disconnect = mockDisconnect;
45
- } as unknown as typeof IntersectionObserver;
46
- });
47
-
48
- afterEach(() => {
49
- // @ts-expect-error – removing test stub
50
- delete globalThis.IntersectionObserver;
51
- });
52
-
53
- it("returns null from render before visibility", () => {
54
- const Wrapped = applyLazy(BaseComp as AnyConstructor);
55
- const instance = new Wrapped();
56
- expect(resolveRender(instance)).toBeNull();
57
- });
58
-
59
- it("sets lazy visibility to true when IntersectionObserver is not supported", () => {
60
- // Remove IntersectionObserver to simulate unsupported environment
61
- // @ts-expect-error – removing test stub
62
- delete globalThis.IntersectionObserver;
63
-
64
- const Wrapped = applyLazy(BaseComp as AnyConstructor);
65
- const instance = new Wrapped();
66
-
67
- // Simulate anchor with parent element
68
- const parent = document.createElement("div");
69
- document.body.appendChild(parent);
70
- const anchor = document.createComment("end");
71
- parent.appendChild(anchor);
72
- setComponentAnchor(instance, anchor);
73
-
74
- instance.onMount?.();
75
- // Without IntersectionObserver, component should immediately become visible
76
- expect(instance.render()).not.toBeNull();
77
- document.body.removeChild(parent);
78
- });
79
-
80
- it("sets minHeight placeholder on mount", () => {
81
- const Wrapped = applyLazy(BaseComp as AnyConstructor, 300);
82
- const instance = new Wrapped();
83
-
84
- const parent = document.createElement("div");
85
- document.body.appendChild(parent);
86
- const anchor = document.createComment("end");
87
- parent.appendChild(anchor);
88
- setComponentAnchor(instance, anchor);
89
-
90
- instance.onMount?.();
91
- expect(parent.style.minHeight).toBe("300px");
92
- document.body.removeChild(parent);
93
- });
94
-
95
- it("sets _lazyVisible and clears minHeight when intersecting", () => {
96
- const Wrapped = applyLazy(BaseComp as AnyConstructor, 200);
97
- const instance = new Wrapped();
98
-
99
- const parent = document.createElement("div");
100
- document.body.appendChild(parent);
101
- const anchor = document.createComment("end");
102
- parent.appendChild(anchor);
103
- setComponentAnchor(instance, anchor);
104
-
105
- instance.onMount?.();
106
- expect(resolveRender(instance)).toBeNull();
107
-
108
- intersectionCallback(
109
- [{ isIntersecting: true } as IntersectionObserverEntry],
110
- null as unknown as IntersectionObserver,
111
- );
112
-
113
- expect(parent.style.minHeight).toBe("");
114
- expect(resolveRender(instance)).not.toBeNull(); // component became visible after intersection
115
- expect(mockDisconnect).toHaveBeenCalled();
116
- document.body.removeChild(parent);
117
- });
118
-
119
- it("does nothing when the anchor has no parent", () => {
120
- const Wrapped = applyLazy(BaseComp as AnyConstructor);
121
- const instance = new Wrapped();
122
-
123
- const anchor = document.createComment("end");
124
- setComponentAnchor(instance, anchor);
125
-
126
- // Should not throw even though anchor has no parent
127
- expect(() => instance.onMount?.()).not.toThrow();
128
- });
129
-
130
- it("disconnects observer on unmount", () => {
131
- const Wrapped = applyLazy(BaseComp as AnyConstructor);
132
- const instance = new Wrapped();
133
-
134
- const parent = document.createElement("div");
135
- document.body.appendChild(parent);
136
- const anchor = document.createComment("end");
137
- parent.appendChild(anchor);
138
- setComponentAnchor(instance, anchor);
139
-
140
- instance.onMount?.();
141
- instance.onUnmount?.();
142
- expect(mockDisconnect).toHaveBeenCalled();
143
- document.body.removeChild(parent);
144
- });
145
-
146
- it("onMount does nothing when anchor is undefined", () => {
147
- const Wrapped = applyLazy(BaseComp as AnyConstructor);
148
- const instance = new Wrapped();
149
- expect(() => instance.onMount?.()).not.toThrow();
150
- });
151
-
152
- it("render() returns original content after becoming visible", () => {
153
- const Wrapped = applyLazy(BaseComp as AnyConstructor, 200);
154
- const instance = new Wrapped();
155
-
156
- const parent = document.createElement("div");
157
- document.body.appendChild(parent);
158
- const anchor = document.createComment("end");
159
- parent.appendChild(anchor);
160
- setComponentAnchor(instance, anchor);
161
-
162
- instance.onMount?.();
163
- expect(resolveRender(instance)).toBeNull(); // not yet visible
164
-
165
- intersectionCallback(
166
- [{ isIntersecting: true } as IntersectionObserverEntry],
167
- null as unknown as IntersectionObserver,
168
- );
169
-
170
- // Now visible: render() must delegate to the original render()
171
- const result = resolveRender(instance);
172
- expect(result).not.toBeNull();
173
- document.body.removeChild(parent);
174
- });
175
-
176
- it("observer stays active and minHeight is preserved when entry is not intersecting", () => {
177
- const Wrapped = applyLazy(BaseComp as AnyConstructor, 200);
178
- const instance = new Wrapped();
179
-
180
- const parent = document.createElement("div");
181
- document.body.appendChild(parent);
182
- const anchor = document.createComment("end");
183
- parent.appendChild(anchor);
184
- setComponentAnchor(instance, anchor);
185
-
186
- instance.onMount?.();
187
-
188
- // Fire callback with isIntersecting=false — should be a no-op
189
- intersectionCallback(
190
- [{ isIntersecting: false } as IntersectionObserverEntry],
191
- null as unknown as IntersectionObserver,
192
- );
193
-
194
- expect(resolveRender(instance)).toBeNull();
195
- expect(mockDisconnect).not.toHaveBeenCalled();
196
- expect(parent.style.minHeight).toBe("200px");
197
- document.body.removeChild(parent);
198
- });
199
-
200
- it("accepts a LazyOptions object instead of a number (line 74 false branch)", () => {
201
- const Wrapped = applyLazy(BaseComp as AnyConstructor, { placeholder: 150, rootMargin: "50px" });
202
- const instance = new Wrapped();
203
-
204
- const parent = document.createElement("div");
205
- document.body.appendChild(parent);
206
- const anchor = document.createComment("end");
207
- parent.appendChild(anchor);
208
- setComponentAnchor(instance, anchor);
209
-
210
- instance.onMount?.();
211
- expect(parent.style.minHeight).toBe("150px");
212
- document.body.removeChild(parent);
213
- });
214
-
215
- it("uses root.current as the IntersectionObserver root when provided (line 42 false branch)", () => {
216
- const rootEl = document.createElement("div");
217
- document.body.appendChild(rootEl);
218
-
219
- let capturedInit: IntersectionObserverInit | undefined;
220
- globalThis.IntersectionObserver = class {
221
- constructor(cb: IntersectionObserverCallback, init?: IntersectionObserverInit) {
222
- intersectionCallback = cb;
223
- capturedInit = init;
224
- }
225
- observe = mockObserve;
226
- disconnect = mockDisconnect;
227
- } as unknown as typeof IntersectionObserver;
228
-
229
- const Wrapped = applyLazy(BaseComp as AnyConstructor, { root: { current: rootEl } });
230
- const instance = new Wrapped();
231
-
232
- const parent = document.createElement("div");
233
- document.body.appendChild(parent);
234
- const anchor = document.createComment("end");
235
- parent.appendChild(anchor);
236
- setComponentAnchor(instance, anchor);
237
-
238
- instance.onMount?.();
239
- expect(capturedInit?.root).toBe(rootEl);
240
-
241
- document.body.removeChild(parent);
242
- document.body.removeChild(rootEl);
243
- });
244
-
245
- it("falls back to null root when root.current is null (line 42 ?? null branch)", () => {
246
- let capturedInit: IntersectionObserverInit | undefined;
247
- globalThis.IntersectionObserver = class {
248
- constructor(cb: IntersectionObserverCallback, init?: IntersectionObserverInit) {
249
- intersectionCallback = cb;
250
- capturedInit = init;
251
- }
252
- observe = mockObserve;
253
- disconnect = mockDisconnect;
254
- } as unknown as typeof IntersectionObserver;
255
-
256
- const Wrapped = applyLazy(BaseComp as AnyConstructor, { root: { current: null } });
257
- const instance = new Wrapped();
258
-
259
- const parent = document.createElement("div");
260
- document.body.appendChild(parent);
261
- const anchor = document.createComment("end");
262
- parent.appendChild(anchor);
263
- setComponentAnchor(instance, anchor);
264
-
265
- instance.onMount?.();
266
- expect(capturedInit?.root).toBeNull();
267
-
268
- document.body.removeChild(parent);
269
- });
270
-
271
- it("does not re-apply minHeight on remount when already visible", () => {
272
- const Wrapped = applyLazy(BaseComp as AnyConstructor, 300);
273
- const instance = new Wrapped();
274
-
275
- const parent = document.createElement("div");
276
- document.body.appendChild(parent);
277
- const anchor = document.createComment("end");
278
- parent.appendChild(anchor);
279
- setComponentAnchor(instance, anchor);
280
-
281
- // First mount — become visible
282
- instance.onMount?.();
283
- intersectionCallback(
284
- [{ isIntersecting: true } as IntersectionObserverEntry],
285
- null as unknown as IntersectionObserver,
286
- );
287
- instance.onUnmount?.();
288
-
289
- // Second mount — lazy visibility is already true
290
- instance.onMount?.();
291
-
292
- // minHeight must NOT be re-applied because lazy visibility is true
293
- expect(parent.style.minHeight).toBe("");
294
- document.body.removeChild(parent);
295
- });
296
- });
@@ -1,293 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
-
3
- import { effect, signal } from "@praxisjs/core/internal";
4
- import { Memo } from "../functions/memo";
5
-
6
- function mockCtx(name: string) {
7
- const initializers: Array<(this: object) => void> = [];
8
- return {
9
- name,
10
- kind: "method" as const,
11
- addInitializer(fn: (this: object) => void) {
12
- initializers.push(fn);
13
- },
14
- runInitializers(instance: object) {
15
- initializers.forEach((fn) => { fn.call(instance); });
16
- },
17
- } as unknown as ClassMethodDecoratorContext & { runInitializers(instance: object): void };
18
- }
19
-
20
- describe("Memo", () => {
21
- it("returns the computed value", () => {
22
- const fn = vi.fn((x: unknown) => (x as number) * 2);
23
- const ctx = mockCtx("double");
24
- Memo()(fn, ctx);
25
-
26
- const obj = {};
27
- ctx.runInitializers(obj);
28
- const method = (obj as Record<string, (...a: unknown[]) => unknown>).double;
29
-
30
- expect(method(5)).toBe(10);
31
- });
32
-
33
- it("caches result for same args — calls original only once", () => {
34
- const fn = vi.fn((x: unknown) => (x as number) + 1);
35
- const ctx = mockCtx("inc");
36
- Memo()(fn, ctx);
37
-
38
- const obj = {};
39
- ctx.runInitializers(obj);
40
- const method = (obj as Record<string, (...a: unknown[]) => unknown>).inc;
41
-
42
- method(3);
43
- method(3);
44
- method(3);
45
- expect(fn).toHaveBeenCalledTimes(1);
46
- });
47
-
48
- it("computes separately for different args", () => {
49
- const fn = vi.fn((x: unknown) => (x as number) * 10);
50
- const ctx = mockCtx("mul");
51
- Memo()(fn, ctx);
52
-
53
- const obj = {};
54
- ctx.runInitializers(obj);
55
- const method = (obj as Record<string, (...a: unknown[]) => unknown>).mul;
56
-
57
- expect(method(2)).toBe(20);
58
- expect(method(3)).toBe(30);
59
- expect(fn).toHaveBeenCalledTimes(2);
60
- });
61
-
62
- it("is per-instance — different instances have separate caches", () => {
63
- const fn = vi.fn(() => Math.random());
64
- const ctx = mockCtx("rand");
65
- Memo()(fn, ctx);
66
-
67
- const a = {};
68
- const b = {};
69
- ctx.runInitializers(a);
70
- ctx.runInitializers(b);
71
-
72
- const methodA = (a as Record<string, () => unknown>).rand;
73
- const methodB = (b as Record<string, () => unknown>).rand;
74
- const v1 = methodA();
75
- const v2 = methodB();
76
- expect(fn).toHaveBeenCalledTimes(2);
77
- expect(v1).not.toBe(v2); // different random values per instance
78
- });
79
-
80
- it("serializes no-args calls with __no_args__ key", () => {
81
- const fn = vi.fn(() => 42);
82
- const ctx = mockCtx("noop");
83
- Memo()(fn, ctx);
84
-
85
- const obj = {};
86
- ctx.runInitializers(obj);
87
- const method = (obj as Record<string, () => unknown>).noop;
88
-
89
- method();
90
- method();
91
- expect(fn).toHaveBeenCalledTimes(1);
92
- });
93
-
94
- it("serializes object args via JSON.stringify", () => {
95
- const fn = vi.fn((o: unknown) => JSON.stringify(o));
96
- const ctx = mockCtx("obj");
97
- Memo()(fn, ctx);
98
-
99
- const obj = {};
100
- ctx.runInitializers(obj);
101
- const method = (obj as Record<string, (...a: unknown[]) => unknown>).obj;
102
-
103
- method({ a: 1 });
104
- method({ a: 1 }); // same key
105
- expect(fn).toHaveBeenCalledTimes(1);
106
- });
107
-
108
- it("re-evaluates when a reactive signal read inside the method changes", () => {
109
- const s = signal(1);
110
- const fn = vi.fn(() => s() * 10);
111
- const ctx = mockCtx("reactive");
112
- Memo()(fn, ctx);
113
-
114
- const obj = {};
115
- ctx.runInitializers(obj);
116
- const method = (obj as Record<string, () => unknown>).reactive;
117
-
118
- expect(method()).toBe(10);
119
- expect(fn).toHaveBeenCalledTimes(1);
120
-
121
- s.set(5);
122
- // The computed tracks s() — next read triggers recomputation
123
- expect(method()).toBe(50);
124
- expect(fn).toHaveBeenCalledTimes(2);
125
- });
126
-
127
- it("serializes symbol args via toString()", () => {
128
- const fn = vi.fn((s: unknown) => String(s));
129
- const ctx = mockCtx("sym");
130
- Memo()(fn, ctx);
131
-
132
- const obj = {};
133
- ctx.runInitializers(obj);
134
- const method = (obj as Record<string, (...a: unknown[]) => unknown>).sym;
135
-
136
- const sym = Symbol("test");
137
- method(sym);
138
- method(sym);
139
- expect(fn).toHaveBeenCalledTimes(1);
140
- });
141
-
142
- it("circular object argument does not crash — falls back to stable identity key", () => {
143
- const fn = vi.fn((o: unknown) => o);
144
- const ctx = mockCtx("circular");
145
- Memo()(fn, ctx);
146
-
147
- const obj = {};
148
- ctx.runInitializers(obj);
149
- const method = (obj as Record<string, (...a: unknown[]) => unknown>).circular;
150
-
151
- // Create a circular reference
152
- const circ: Record<string, unknown> = {};
153
- circ.self = circ;
154
-
155
- expect(() => method(circ)).not.toThrow();
156
- // Calling again with the same object should hit cache (not call fn again)
157
- method(circ);
158
- expect(fn).toHaveBeenCalledTimes(1);
159
- });
160
-
161
- it("two circular objects produce different cache keys", () => {
162
- const fn = vi.fn((o: unknown) => o);
163
- const ctx = mockCtx("circular2");
164
- Memo()(fn, ctx);
165
-
166
- const obj = {};
167
- ctx.runInitializers(obj);
168
- const method = (obj as Record<string, (...a: unknown[]) => unknown>).circular2;
169
-
170
- const a: Record<string, unknown> = {};
171
- a.self = a;
172
- const b: Record<string, unknown> = {};
173
- b.self = b;
174
-
175
- method(a);
176
- method(b);
177
- expect(fn).toHaveBeenCalledTimes(2);
178
- });
179
-
180
- it("undefined vs null as arguments produce different cache keys", () => {
181
- const fn = vi.fn((x: unknown) => x);
182
- const ctx = mockCtx("nullUndef");
183
- Memo()(fn, ctx);
184
-
185
- const obj = {};
186
- ctx.runInitializers(obj);
187
- const method = (obj as Record<string, (...a: unknown[]) => unknown>).nullUndef;
188
-
189
- method(undefined);
190
- method(null);
191
- expect(fn).toHaveBeenCalledTimes(2);
192
- });
193
-
194
- it("BigInt argument does not crash JSON.stringify path", () => {
195
- const fn = vi.fn((x: unknown) => x);
196
- const ctx = mockCtx("bigint");
197
- Memo()(fn, ctx);
198
-
199
- const obj = {};
200
- ctx.runInitializers(obj);
201
- const method = (obj as Record<string, (...a: unknown[]) => unknown>).bigint;
202
-
203
- // BigInt is a non-object, non-symbol primitive — goes through String() path
204
- expect(() => method(BigInt(42))).not.toThrow();
205
- method(BigInt(42));
206
- expect(fn).toHaveBeenCalledTimes(1);
207
- });
208
- });
209
-
210
- describe("Memo({ max })", () => {
211
- function setup(max: number) {
212
- const fn = vi.fn((x: unknown) => (x as number) * 2);
213
- const ctx = mockCtx("dbl");
214
- Memo({ max })(fn, ctx);
215
- const obj = {};
216
- ctx.runInitializers(obj);
217
- const method = (obj as Record<string, (...a: unknown[]) => unknown>).dbl;
218
- return { fn, method };
219
- }
220
-
221
- it("keeps at most max entries, evicting the oldest first", () => {
222
- const { fn, method } = setup(2);
223
- method(1);
224
- method(2);
225
- method(3); // evicts 1
226
- expect(fn).toHaveBeenCalledTimes(3);
227
- method(2);
228
- method(3);
229
- expect(fn).toHaveBeenCalledTimes(3);
230
- method(1); // recomputed
231
- expect(fn).toHaveBeenCalledTimes(4);
232
- });
233
-
234
- it("a cache hit refreshes recency so the used entry survives", () => {
235
- const { fn, method } = setup(2);
236
- method(1);
237
- method(2);
238
- method(1); // 1 is now the most recent
239
- method(3); // evicts 2
240
- fn.mockClear();
241
- method(1);
242
- expect(fn).not.toHaveBeenCalled();
243
- method(2);
244
- expect(fn).toHaveBeenCalledTimes(1);
245
- });
246
-
247
- it("keeps 100 combinations by default and drops the oldest beyond that", () => {
248
- const fn = vi.fn((x: unknown) => x);
249
- const ctx = mockCtx("id");
250
- Memo()(fn, ctx);
251
- const obj = {};
252
- ctx.runInitializers(obj);
253
- const method = (obj as Record<string, (...a: unknown[]) => unknown>).id;
254
- for (let i = 0; i < 101; i++) method(i);
255
- fn.mockClear();
256
- method(100);
257
- expect(fn).not.toHaveBeenCalled();
258
- method(0);
259
- expect(fn).toHaveBeenCalledTimes(1);
260
- });
261
-
262
- it("max: Infinity keeps every combination", () => {
263
- const fn = vi.fn((x: unknown) => x);
264
- const ctx = mockCtx("id");
265
- Memo({ max: Infinity })(fn, ctx);
266
- const obj = {};
267
- ctx.runInitializers(obj);
268
- const method = (obj as Record<string, (...a: unknown[]) => unknown>).id;
269
- for (let i = 0; i < 200; i++) method(i);
270
- fn.mockClear();
271
- for (let i = 0; i < 200; i++) method(i);
272
- expect(fn).not.toHaveBeenCalled();
273
- });
274
-
275
- it("an evicted entry that a binding still uses keeps updating", async () => {
276
- const s = signal(1);
277
- const fn = vi.fn((x: unknown) => (x as number) * s());
278
- const ctx = mockCtx("scaled");
279
- Memo({ max: 1 })(fn, ctx);
280
- const obj = {};
281
- ctx.runInitializers(obj);
282
- const method = (obj as Record<string, (...a: unknown[]) => unknown>).scaled;
283
-
284
- let seen: unknown;
285
- effect(() => {
286
- seen = method(10);
287
- });
288
- method(20); // evicts the entry the effect reads
289
- s.set(2);
290
- await Promise.resolve();
291
- expect(seen).toBe(20);
292
- });
293
- });