@praxisjs/decorators 0.4.3 → 0.5.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 (175) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/__tests__/component.test.js +10 -1
  3. package/dist/__tests__/component.test.js.map +1 -1
  4. package/dist/__tests__/compose.test.d.ts +2 -0
  5. package/dist/__tests__/compose.test.d.ts.map +1 -0
  6. package/dist/__tests__/compose.test.js +171 -0
  7. package/dist/__tests__/compose.test.js.map +1 -0
  8. package/dist/__tests__/events.test.js +26 -0
  9. package/dist/__tests__/events.test.js.map +1 -1
  10. package/dist/__tests__/functions.test.js +142 -47
  11. package/dist/__tests__/functions.test.js.map +1 -1
  12. package/dist/__tests__/lazy.test.js +5 -7
  13. package/dist/__tests__/lazy.test.js.map +1 -1
  14. package/dist/__tests__/memo.test.js +61 -25
  15. package/dist/__tests__/memo.test.js.map +1 -1
  16. package/dist/__tests__/resource.test.d.ts +2 -0
  17. package/dist/__tests__/resource.test.d.ts.map +1 -0
  18. package/dist/__tests__/resource.test.js +103 -0
  19. package/dist/__tests__/resource.test.js.map +1 -0
  20. package/dist/__tests__/slot.test.js +32 -0
  21. package/dist/__tests__/slot.test.js.map +1 -1
  22. package/dist/__tests__/virtual.test.js +56 -15
  23. package/dist/__tests__/virtual.test.js.map +1 -1
  24. package/dist/component/component.d.ts +1 -1
  25. package/dist/component/component.d.ts.map +1 -1
  26. package/dist/component/component.js +15 -10
  27. package/dist/component/component.js.map +1 -1
  28. package/dist/component/lazy.d.ts.map +1 -1
  29. package/dist/component/lazy.js +29 -30
  30. package/dist/component/lazy.js.map +1 -1
  31. package/dist/component/virtual.d.ts.map +1 -1
  32. package/dist/component/virtual.js +46 -50
  33. package/dist/component/virtual.js.map +1 -1
  34. package/dist/create-class-decorator.d.ts +16 -0
  35. package/dist/create-class-decorator.d.ts.map +1 -0
  36. package/dist/create-class-decorator.js +35 -0
  37. package/dist/create-class-decorator.js.map +1 -0
  38. package/dist/create-field-decorator.d.ts +14 -0
  39. package/dist/create-field-decorator.d.ts.map +1 -0
  40. package/dist/create-field-decorator.js +37 -0
  41. package/dist/create-field-decorator.js.map +1 -0
  42. package/dist/create-getter-decorator.d.ts +18 -0
  43. package/dist/create-getter-decorator.d.ts.map +1 -0
  44. package/dist/create-getter-decorator.js +15 -0
  45. package/dist/create-getter-decorator.js.map +1 -0
  46. package/dist/create-lifecycle-method-decorator.d.ts +6 -0
  47. package/dist/create-lifecycle-method-decorator.d.ts.map +1 -0
  48. package/dist/create-lifecycle-method-decorator.js +22 -0
  49. package/dist/create-lifecycle-method-decorator.js.map +1 -0
  50. package/dist/create-method-decorator.d.ts +6 -0
  51. package/dist/create-method-decorator.d.ts.map +1 -0
  52. package/dist/create-method-decorator.js +14 -0
  53. package/dist/create-method-decorator.js.map +1 -0
  54. package/dist/events/emit.d.ts +1 -1
  55. package/dist/events/emit.d.ts.map +1 -1
  56. package/dist/events/emit.js +24 -29
  57. package/dist/events/emit.js.map +1 -1
  58. package/dist/events/on-command.d.ts +1 -2
  59. package/dist/events/on-command.d.ts.map +1 -1
  60. package/dist/events/on-command.js +15 -32
  61. package/dist/events/on-command.js.map +1 -1
  62. package/dist/functions/bind.d.ts +1 -2
  63. package/dist/functions/bind.d.ts.map +1 -1
  64. package/dist/functions/bind.js +4 -11
  65. package/dist/functions/bind.js.map +1 -1
  66. package/dist/functions/debounce.d.ts +1 -2
  67. package/dist/functions/debounce.d.ts.map +1 -1
  68. package/dist/functions/debounce.js +12 -20
  69. package/dist/functions/debounce.js.map +1 -1
  70. package/dist/functions/log.d.ts +1 -2
  71. package/dist/functions/log.d.ts.map +1 -1
  72. package/dist/functions/log.js +40 -38
  73. package/dist/functions/log.js.map +1 -1
  74. package/dist/functions/memo.d.ts +1 -2
  75. package/dist/functions/memo.d.ts.map +1 -1
  76. package/dist/functions/memo.js +15 -27
  77. package/dist/functions/memo.js.map +1 -1
  78. package/dist/functions/once.d.ts +1 -2
  79. package/dist/functions/once.d.ts.map +1 -1
  80. package/dist/functions/once.js +14 -12
  81. package/dist/functions/once.js.map +1 -1
  82. package/dist/functions/retry.d.ts +1 -2
  83. package/dist/functions/retry.d.ts.map +1 -1
  84. package/dist/functions/retry.js +23 -20
  85. package/dist/functions/retry.js.map +1 -1
  86. package/dist/functions/throttle.d.ts +1 -2
  87. package/dist/functions/throttle.d.ts.map +1 -1
  88. package/dist/functions/throttle.js +9 -16
  89. package/dist/functions/throttle.js.map +1 -1
  90. package/dist/functions/watch.d.ts +1 -1
  91. package/dist/functions/watch.d.ts.map +1 -1
  92. package/dist/functions/watch.js +25 -39
  93. package/dist/functions/watch.js.map +1 -1
  94. package/dist/functions/when.d.ts +1 -2
  95. package/dist/functions/when.d.ts.map +1 -1
  96. package/dist/functions/when.js +10 -26
  97. package/dist/functions/when.js.map +1 -1
  98. package/dist/index.d.ts +6 -1
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +6 -1
  101. package/dist/index.js.map +1 -1
  102. package/dist/properties/compose.d.ts +3 -0
  103. package/dist/properties/compose.d.ts.map +1 -0
  104. package/dist/properties/compose.js +40 -0
  105. package/dist/properties/compose.js.map +1 -0
  106. package/dist/properties/computed.d.ts +1 -2
  107. package/dist/properties/computed.d.ts.map +1 -1
  108. package/dist/properties/computed.js +10 -9
  109. package/dist/properties/computed.js.map +1 -1
  110. package/dist/properties/history.d.ts.map +1 -1
  111. package/dist/properties/history.js +37 -32
  112. package/dist/properties/history.js.map +1 -1
  113. package/dist/properties/index.d.ts +2 -0
  114. package/dist/properties/index.d.ts.map +1 -1
  115. package/dist/properties/index.js +2 -0
  116. package/dist/properties/index.js.map +1 -1
  117. package/dist/properties/persisted.d.ts +1 -2
  118. package/dist/properties/persisted.d.ts.map +1 -1
  119. package/dist/properties/persisted.js +13 -29
  120. package/dist/properties/persisted.js.map +1 -1
  121. package/dist/properties/prop.d.ts +1 -2
  122. package/dist/properties/prop.d.ts.map +1 -1
  123. package/dist/properties/prop.js +21 -25
  124. package/dist/properties/prop.js.map +1 -1
  125. package/dist/properties/resource.d.ts +14 -0
  126. package/dist/properties/resource.d.ts.map +1 -0
  127. package/dist/properties/resource.js +27 -0
  128. package/dist/properties/resource.js.map +1 -0
  129. package/dist/properties/slot.d.ts +1 -2
  130. package/dist/properties/slot.d.ts.map +1 -1
  131. package/dist/properties/slot.js +15 -20
  132. package/dist/properties/slot.js.map +1 -1
  133. package/dist/properties/state.d.ts +1 -2
  134. package/dist/properties/state.d.ts.map +1 -1
  135. package/dist/properties/state.js +13 -18
  136. package/dist/properties/state.js.map +1 -1
  137. package/package.json +2 -2
  138. package/src/__tests__/component.test.ts +10 -0
  139. package/src/__tests__/compose.test.ts +202 -0
  140. package/src/__tests__/events.test.ts +32 -0
  141. package/src/__tests__/functions.test.ts +177 -46
  142. package/src/__tests__/lazy.test.ts +3 -5
  143. package/src/__tests__/memo.test.ts +76 -25
  144. package/src/__tests__/resource.test.ts +113 -0
  145. package/src/__tests__/slot.test.ts +38 -0
  146. package/src/__tests__/virtual.test.ts +26 -7
  147. package/src/component/component.ts +27 -12
  148. package/src/component/lazy.ts +33 -37
  149. package/src/component/virtual.tsx +62 -75
  150. package/src/create-class-decorator.ts +61 -0
  151. package/src/create-field-decorator.ts +66 -0
  152. package/src/create-getter-decorator.ts +52 -0
  153. package/src/create-lifecycle-method-decorator.ts +38 -0
  154. package/src/create-method-decorator.ts +26 -0
  155. package/src/events/emit.ts +22 -33
  156. package/src/events/on-command.ts +22 -47
  157. package/src/functions/bind.ts +4 -15
  158. package/src/functions/debounce.ts +11 -26
  159. package/src/functions/log.ts +40 -43
  160. package/src/functions/memo.ts +15 -45
  161. package/src/functions/once.ts +13 -16
  162. package/src/functions/retry.ts +21 -22
  163. package/src/functions/throttle.ts +9 -23
  164. package/src/functions/watch.ts +27 -43
  165. package/src/functions/when.ts +11 -36
  166. package/src/index.ts +28 -0
  167. package/src/properties/compose.ts +53 -0
  168. package/src/properties/computed.ts +11 -13
  169. package/src/properties/history.ts +42 -41
  170. package/src/properties/index.ts +2 -0
  171. package/src/properties/persisted.ts +17 -58
  172. package/src/properties/prop.ts +21 -31
  173. package/src/properties/resource.ts +34 -0
  174. package/src/properties/slot.ts +17 -30
  175. package/src/properties/state.ts +14 -24
@@ -29,11 +29,14 @@ describe("Once", () => {
29
29
  it("only calls the original function once", () => {
30
30
  const original = vi.fn(() => 42);
31
31
  const ctx = mockMethodContext("doSomething");
32
- const wrapped = Once()(original, ctx as unknown as ClassMethodDecoratorContext);
32
+ Once()(original, ctx as unknown as ClassMethodDecoratorContext);
33
33
 
34
34
  const obj = {};
35
- expect(wrapped.call(obj)).toBe(42);
36
- expect(wrapped.call(obj)).toBe(42);
35
+ ctx.runInitializers(obj);
36
+ const method = (obj as Record<string, () => unknown>).doSomething;
37
+
38
+ expect(method()).toBe(42);
39
+ expect(method()).toBe(42);
37
40
  expect(original).toHaveBeenCalledTimes(1);
38
41
  });
39
42
 
@@ -41,24 +44,32 @@ describe("Once", () => {
41
44
  let counter = 0;
42
45
  const original = () => ++counter;
43
46
  const ctx = mockMethodContext("inc");
44
- const wrapped = Once()(original, ctx as unknown as ClassMethodDecoratorContext);
47
+ Once()(original, ctx as unknown as ClassMethodDecoratorContext);
45
48
 
46
49
  const obj = {};
47
- wrapped.call(obj);
48
- wrapped.call(obj);
49
- wrapped.call(obj);
50
+ ctx.runInitializers(obj);
51
+ const method = (obj as Record<string, () => unknown>).inc;
52
+
53
+ method();
54
+ method();
55
+ method();
50
56
  expect(counter).toBe(1);
51
57
  });
52
58
 
53
59
  it("is per-instance — different instances call independently", () => {
54
60
  const original = vi.fn(() => "result");
55
61
  const ctx = mockMethodContext("fn");
56
- const wrapped = Once()(original, ctx as unknown as ClassMethodDecoratorContext);
62
+ Once()(original, ctx as unknown as ClassMethodDecoratorContext);
57
63
 
58
64
  const a = {};
59
65
  const b = {};
60
- wrapped.call(a);
61
- wrapped.call(b);
66
+ ctx.runInitializers(a);
67
+ ctx.runInitializers(b);
68
+
69
+ const methodA = (a as Record<string, () => unknown>).fn;
70
+ const methodB = (b as Record<string, () => unknown>).fn;
71
+ methodA();
72
+ methodB();
62
73
  expect(original).toHaveBeenCalledTimes(2);
63
74
  });
64
75
  });
@@ -69,9 +80,13 @@ describe("Retry", () => {
69
80
  it("returns result if the first attempt succeeds", async () => {
70
81
  const original = vi.fn(async () => "ok");
71
82
  const ctx = mockMethodContext("fetch");
72
- const wrapped = Retry(3)(original, ctx as unknown as ClassMethodDecoratorContext);
83
+ Retry(3)(original, ctx as unknown as ClassMethodDecoratorContext);
73
84
 
74
- const result = await wrapped.call({});
85
+ const obj = {};
86
+ ctx.runInitializers(obj);
87
+ const method = (obj as Record<string, () => Promise<unknown>>).fetch;
88
+
89
+ const result = await method();
75
90
  expect(result).toBe("ok");
76
91
  expect(original).toHaveBeenCalledTimes(1);
77
92
  });
@@ -84,9 +99,13 @@ describe("Retry", () => {
84
99
  return "success";
85
100
  });
86
101
  const ctx = mockMethodContext("call");
87
- const wrapped = Retry(3)(original, ctx as unknown as ClassMethodDecoratorContext);
102
+ Retry(3)(original, ctx as unknown as ClassMethodDecoratorContext);
88
103
 
89
- const result = await wrapped.call({});
104
+ const obj = {};
105
+ ctx.runInitializers(obj);
106
+ const method = (obj as Record<string, () => Promise<unknown>>).call;
107
+
108
+ const result = await method();
90
109
  expect(result).toBe("success");
91
110
  expect(original).toHaveBeenCalledTimes(3);
92
111
  });
@@ -96,9 +115,13 @@ describe("Retry", () => {
96
115
  throw new Error("always fails");
97
116
  });
98
117
  const ctx = mockMethodContext("bad");
99
- const wrapped = Retry(3)(original, ctx as unknown as ClassMethodDecoratorContext);
118
+ Retry(3)(original, ctx as unknown as ClassMethodDecoratorContext);
100
119
 
101
- await expect(wrapped.call({})).rejects.toThrow("always fails");
120
+ const obj = {};
121
+ ctx.runInitializers(obj);
122
+ const method = (obj as Record<string, () => Promise<unknown>>).bad;
123
+
124
+ await expect(method()).rejects.toThrow("always fails");
102
125
  expect(original).toHaveBeenCalledTimes(3);
103
126
  });
104
127
 
@@ -106,11 +129,16 @@ describe("Retry", () => {
106
129
  vi.useFakeTimers();
107
130
  const original = vi.fn(async () => { throw new Error("fail"); });
108
131
  const ctx = mockMethodContext("delayed");
109
- const wrapped = Retry(3, { delay: 100, backoff: 2 })(
132
+ Retry(3, { delay: 100, backoff: 2 })(
110
133
  original,
111
134
  ctx as unknown as ClassMethodDecoratorContext,
112
135
  );
113
- const p = wrapped.call({});
136
+
137
+ const obj = {};
138
+ ctx.runInitializers(obj);
139
+ const method = (obj as Record<string, () => Promise<unknown>>).delayed;
140
+
141
+ const p = method();
114
142
  const rejection = expect(p).rejects.toThrow("fail");
115
143
  await vi.runAllTimersAsync();
116
144
  await rejection;
@@ -124,18 +152,26 @@ describe("Retry", () => {
124
152
  throw new Error("err");
125
153
  });
126
154
  const ctx = mockMethodContext("op");
127
- const wrapped = Retry(3, { onRetry })(original, ctx as unknown as ClassMethodDecoratorContext);
155
+ Retry(3, { onRetry })(original, ctx as unknown as ClassMethodDecoratorContext);
156
+
157
+ const obj = {};
158
+ ctx.runInitializers(obj);
159
+ const method = (obj as Record<string, () => Promise<unknown>>).op;
128
160
 
129
- await expect(wrapped.call({})).rejects.toThrow();
161
+ await expect(method()).rejects.toThrow();
130
162
  expect(onRetry).toHaveBeenCalledTimes(2); // attempts 1 and 2, not the last
131
163
  });
132
164
 
133
165
  it("maxAttempts=1 fails immediately without retrying", async () => {
134
166
  const original = vi.fn(async () => { throw new Error("instant fail"); });
135
167
  const ctx = mockMethodContext("once");
136
- const wrapped = Retry(1)(original, ctx as unknown as ClassMethodDecoratorContext);
168
+ Retry(1)(original, ctx as unknown as ClassMethodDecoratorContext);
137
169
 
138
- await expect(wrapped.call({})).rejects.toThrow("instant fail");
170
+ const obj = {};
171
+ ctx.runInitializers(obj);
172
+ const method = (obj as Record<string, () => Promise<unknown>>).once;
173
+
174
+ await expect(method()).rejects.toThrow("instant fail");
139
175
  expect(original).toHaveBeenCalledTimes(1);
140
176
  });
141
177
 
@@ -145,17 +181,25 @@ describe("Retry", () => {
145
181
  throw "string error";
146
182
  });
147
183
  const ctx = mockMethodContext("strThrow");
148
- const wrapped = Retry(1)(original, ctx as unknown as ClassMethodDecoratorContext);
184
+ Retry(1)(original, ctx as unknown as ClassMethodDecoratorContext);
149
185
 
150
- await expect(wrapped.call({})).rejects.toThrow("string error");
186
+ const obj = {};
187
+ ctx.runInitializers(obj);
188
+ const method = (obj as Record<string, () => Promise<unknown>>).strThrow;
189
+
190
+ await expect(method()).rejects.toThrow("string error");
151
191
  });
152
192
 
153
193
  it("maxAttempts=0 throws immediately without calling the function", async () => {
154
194
  const original = vi.fn(async () => "ok");
155
195
  const ctx = mockMethodContext("never");
156
- const wrapped = Retry(0)(original, ctx as unknown as ClassMethodDecoratorContext);
196
+ Retry(0)(original, ctx as unknown as ClassMethodDecoratorContext);
157
197
 
158
- await expect(wrapped.call({})).rejects.toThrow("Unknown error");
198
+ const obj = {};
199
+ ctx.runInitializers(obj);
200
+ const method = (obj as Record<string, () => Promise<unknown>>).never;
201
+
202
+ await expect(method()).rejects.toThrow("Unknown error");
159
203
  expect(original).not.toHaveBeenCalled();
160
204
  });
161
205
 
@@ -163,11 +207,15 @@ describe("Retry", () => {
163
207
  const attempts: number[] = [];
164
208
  const original = vi.fn(async () => { throw new Error("fail"); });
165
209
  const ctx = mockMethodContext("numbered");
166
- const wrapped = Retry(3, {
210
+ Retry(3, {
167
211
  onRetry: (_err, attempt) => { attempts.push(attempt); },
168
212
  })(original, ctx as unknown as ClassMethodDecoratorContext);
169
213
 
170
- await expect(wrapped.call({})).rejects.toThrow();
214
+ const obj = {};
215
+ ctx.runInitializers(obj);
216
+ const method = (obj as Record<string, () => Promise<unknown>>).numbered;
217
+
218
+ await expect(method()).rejects.toThrow();
171
219
  expect(attempts).toEqual([1, 2]); // attempt 1 failed → onRetry(1), attempt 2 failed → onRetry(2)
172
220
  });
173
221
 
@@ -179,12 +227,16 @@ describe("Retry", () => {
179
227
  throw new Error("fail");
180
228
  });
181
229
  const ctx = mockMethodContext("backoffTest");
182
- const wrapped = Retry(3, { delay: 100, backoff: 2 })(
230
+ Retry(3, { delay: 100, backoff: 2 })(
183
231
  original,
184
232
  ctx as unknown as ClassMethodDecoratorContext,
185
233
  );
186
234
 
187
- const p = wrapped.call({});
235
+ const obj = {};
236
+ ctx.runInitializers(obj);
237
+ const method = (obj as Record<string, () => Promise<unknown>>).backoffTest;
238
+
239
+ const p = method();
188
240
  const rejection = expect(p).rejects.toThrow("fail");
189
241
  await vi.runAllTimersAsync();
190
242
  await rejection;
@@ -205,13 +257,17 @@ describe("Log", () => {
205
257
  const spy = vi.spyOn(console, "log").mockImplementation(() => {});
206
258
  const original = vi.fn(() => 99);
207
259
  const ctx = mockMethodContext("myMethod");
208
- const wrapped = Log({ args: false, result: false })(
260
+ Log({ args: false, result: false })(
209
261
  original,
210
262
  ctx as unknown as ClassMethodDecoratorContext,
211
263
  );
212
264
 
213
265
  class Dummy {}
214
- const result = wrapped.call(new Dummy());
266
+ const dummy = new Dummy();
267
+ ctx.runInitializers(dummy);
268
+ const method = (dummy as Record<string, () => unknown>).myMethod;
269
+
270
+ const result = method();
215
271
  expect(result).toBe(99);
216
272
  spy.mockRestore();
217
273
  });
@@ -220,13 +276,17 @@ describe("Log", () => {
220
276
  const spy = vi.spyOn(console, "log").mockImplementation(() => {});
221
277
  const original = vi.fn((x: unknown) => x);
222
278
  const ctx = mockMethodContext("greet");
223
- const wrapped = Log({ args: true, result: false })(
279
+ Log({ args: true, result: false })(
224
280
  original,
225
281
  ctx as unknown as ClassMethodDecoratorContext,
226
282
  );
227
283
 
228
284
  class Foo {}
229
- wrapped.call(new Foo(), "hello");
285
+ const foo = new Foo();
286
+ ctx.runInitializers(foo);
287
+ const method = (foo as Record<string, (...a: unknown[]) => unknown>).greet;
288
+
289
+ method("hello");
230
290
  expect(spy).toHaveBeenCalledWith(expect.stringContaining("args:"), ["hello"]);
231
291
  spy.mockRestore();
232
292
  });
@@ -235,7 +295,7 @@ describe("Log", () => {
235
295
  const spy = vi.spyOn(console, "log").mockImplementation(() => {});
236
296
  const original = vi.fn(() => "value");
237
297
  const ctx = mockMethodContext("prod");
238
- const wrapped = Log({ devOnly: true })(
298
+ Log({ devOnly: true })(
239
299
  original,
240
300
  ctx as unknown as ClassMethodDecoratorContext,
241
301
  );
@@ -244,7 +304,11 @@ describe("Log", () => {
244
304
  process.env.NODE_ENV = "production";
245
305
 
246
306
  class App {}
247
- wrapped.call(new App());
307
+ const app = new App();
308
+ ctx.runInitializers(app);
309
+ const method = (app as Record<string, () => unknown>).prod;
310
+
311
+ method();
248
312
  expect(spy).not.toHaveBeenCalled();
249
313
 
250
314
  process.env.NODE_ENV = prevEnv;
@@ -255,13 +319,17 @@ describe("Log", () => {
255
319
  const spy = vi.spyOn(console, "log").mockImplementation(() => {});
256
320
  const original = vi.fn(async () => "async-result");
257
321
  const ctx = mockMethodContext("load");
258
- const wrapped = Log({ args: false, result: true })(
322
+ Log({ args: false, result: true })(
259
323
  original,
260
324
  ctx as unknown as ClassMethodDecoratorContext,
261
325
  );
262
326
 
263
327
  class Svc {}
264
- await wrapped.call(new Svc());
328
+ const svc = new Svc();
329
+ ctx.runInitializers(svc);
330
+ const method = (svc as Record<string, () => Promise<unknown>>).load;
331
+
332
+ await method();
265
333
  expect(spy).toHaveBeenCalledWith(
266
334
  expect.stringContaining("resolved:"),
267
335
  "async-result",
@@ -274,12 +342,17 @@ describe("Log", () => {
274
342
  const spy = vi.spyOn(console, "log").mockImplementation(() => {});
275
343
  const original = vi.fn(async () => { throw new Error("async-error"); });
276
344
  const ctx = mockMethodContext("failing");
277
- const wrapped = Log({ args: false, result: true })(
345
+ Log({ args: false, result: true })(
278
346
  original,
279
347
  ctx as unknown as ClassMethodDecoratorContext,
280
348
  );
349
+
281
350
  class Svc {}
282
- await expect(wrapped.call(new Svc())).rejects.toThrow("async-error");
351
+ const svc = new Svc();
352
+ ctx.runInitializers(svc);
353
+ const method = (svc as Record<string, () => Promise<unknown>>).failing;
354
+
355
+ await expect(method()).rejects.toThrow("async-error");
283
356
  expect(spy).toHaveBeenCalledWith(
284
357
  expect.stringContaining("rejected:"),
285
358
  expect.any(Error),
@@ -295,13 +368,17 @@ describe("Log — time option", () => {
295
368
  const spy = vi.spyOn(console, "log").mockImplementation(() => {});
296
369
  const original = vi.fn(() => "value");
297
370
  const ctx = mockMethodContext("timed");
298
- const wrapped = Log({ args: false, result: true, time: true })(
371
+ Log({ args: false, result: true, time: true })(
299
372
  original,
300
373
  ctx as unknown as ClassMethodDecoratorContext,
301
374
  );
302
375
 
303
376
  class Svc {}
304
- wrapped.call(new Svc());
377
+ const svc = new Svc();
378
+ ctx.runInitializers(svc);
379
+ const method = (svc as Record<string, () => unknown>).timed;
380
+
381
+ method();
305
382
  // Third argument should be a string containing "ms"
306
383
  expect(spy).toHaveBeenCalledWith(
307
384
  expect.stringContaining("returned:"),
@@ -315,13 +392,17 @@ describe("Log — time option", () => {
315
392
  const spy = vi.spyOn(console, "log").mockImplementation(() => {});
316
393
  const original = vi.fn(async () => "async-val");
317
394
  const ctx = mockMethodContext("asyncTimed");
318
- const wrapped = Log({ args: false, result: true, time: true })(
395
+ Log({ args: false, result: true, time: true })(
319
396
  original,
320
397
  ctx as unknown as ClassMethodDecoratorContext,
321
398
  );
322
399
 
323
400
  class Svc {}
324
- await wrapped.call(new Svc());
401
+ const svc = new Svc();
402
+ ctx.runInitializers(svc);
403
+ const method = (svc as Record<string, () => Promise<unknown>>).asyncTimed;
404
+
405
+ await method();
325
406
  expect(spy).toHaveBeenCalledWith(
326
407
  expect.stringContaining("resolved:"),
327
408
  "async-val",
@@ -334,18 +415,68 @@ describe("Log — time option", () => {
334
415
  const spy = vi.spyOn(console, "log").mockImplementation(() => {});
335
416
  const original = vi.fn(() => 42);
336
417
  const ctx = mockMethodContext("noResult");
337
- const wrapped = Log({ args: false, result: false })(
418
+ Log({ args: false, result: false })(
338
419
  original,
339
420
  ctx as unknown as ClassMethodDecoratorContext,
340
421
  );
341
422
 
342
423
  class Svc {}
343
- wrapped.call(new Svc());
424
+ const svc = new Svc();
425
+ ctx.runInitializers(svc);
426
+ const method = (svc as Record<string, () => unknown>).noResult;
427
+
428
+ method();
344
429
  // Should not log a "returned:" line
345
430
  const calls = spy.mock.calls.flat().join(" ");
346
431
  expect(calls).not.toContain("returned:");
347
432
  spy.mockRestore();
348
433
  });
434
+
435
+ it("skips resolved log when result=false for async functions", async () => {
436
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
437
+ const original = vi.fn(async () => "async-value");
438
+ const ctx = mockMethodContext("asyncNoResult");
439
+ Log({ args: false, result: false })(
440
+ original,
441
+ ctx as unknown as ClassMethodDecoratorContext,
442
+ );
443
+
444
+ class Svc {}
445
+ const svc = new Svc();
446
+ ctx.runInitializers(svc);
447
+ const method = (svc as Record<string, () => Promise<unknown>>).asyncNoResult;
448
+
449
+ const result = await method();
450
+ expect(result).toBe("async-value");
451
+ const calls = spy.mock.calls.flat().join(" ");
452
+ expect(calls).not.toContain("resolved:");
453
+ spy.mockRestore();
454
+ });
455
+
456
+ it("uses 'Unknown' as class name when constructor.name is undefined", () => {
457
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
458
+ const original = vi.fn(() => "val");
459
+ const ctx = mockMethodContext("m");
460
+ Log({ args: false, result: true })(
461
+ original,
462
+ ctx as unknown as ClassMethodDecoratorContext,
463
+ );
464
+
465
+ // Construct an object whose constructor.name is undefined to trigger ?? "Unknown"
466
+ const fakeInstance = Object.assign(Object.create(null) as object, {
467
+ constructor: { name: undefined },
468
+ });
469
+ ctx.runInitializers(fakeInstance);
470
+ const method = (fakeInstance as Record<string, () => unknown>).m;
471
+
472
+ method();
473
+ expect(spy).toHaveBeenCalledWith(
474
+ expect.stringContaining("Unknown"),
475
+ expect.anything(),
476
+ expect.anything(),
477
+ );
478
+ spy.mockRestore();
479
+ });
349
480
  });
350
481
 
351
482
  // ── Bind ─────────────────────────────────────────────────────────────────────
@@ -64,9 +64,8 @@ describe("Lazy decorator", () => {
64
64
  (instance as unknown as { _anchor: Comment })._anchor = anchor;
65
65
 
66
66
  instance.onMount?.();
67
- // Without IntersectionObserver, _lazyVisible should be set to true
68
- const lazyVisible = (instance as unknown as { _lazyVisible: () => boolean })._lazyVisible;
69
- expect(lazyVisible()).toBe(true);
67
+ // Without IntersectionObserver, component should immediately become visible
68
+ expect(instance.render()).not.toBeNull();
70
69
  document.body.removeChild(parent);
71
70
  });
72
71
 
@@ -104,8 +103,7 @@ describe("Lazy decorator", () => {
104
103
  );
105
104
 
106
105
  expect(parent.style.minHeight).toBe("");
107
- const lazyVisible = (instance as unknown as { _lazyVisible: () => boolean })._lazyVisible;
108
- expect(lazyVisible()).toBe(true);
106
+ expect(instance.render()).not.toBeNull(); // component became visible after intersection
109
107
  expect(mockDisconnect).toHaveBeenCalled();
110
108
  document.body.removeChild(parent);
111
109
  });
@@ -4,87 +4,138 @@ import { signal } from "@praxisjs/core/internal";
4
4
  import { Memo } from "../functions/memo";
5
5
 
6
6
  function mockCtx(name: string) {
7
- return { name, kind: "method" as const } as ClassMethodDecoratorContext;
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 };
8
18
  }
9
19
 
10
20
  describe("Memo", () => {
11
21
  it("returns the computed value", () => {
12
22
  const fn = vi.fn((x: unknown) => (x as number) * 2);
13
- const wrapped = Memo()(fn, mockCtx("double"));
23
+ const ctx = mockCtx("double");
24
+ Memo()(fn, ctx);
25
+
14
26
  const obj = {};
15
- expect(wrapped.call(obj, 5)).toBe(10);
27
+ ctx.runInitializers(obj);
28
+ const method = (obj as Record<string, (...a: unknown[]) => unknown>).double;
29
+
30
+ expect(method(5)).toBe(10);
16
31
  });
17
32
 
18
33
  it("caches result for same args — calls original only once", () => {
19
34
  const fn = vi.fn((x: unknown) => (x as number) + 1);
20
- const wrapped = Memo()(fn, mockCtx("inc"));
35
+ const ctx = mockCtx("inc");
36
+ Memo()(fn, ctx);
37
+
21
38
  const obj = {};
22
- wrapped.call(obj, 3);
23
- wrapped.call(obj, 3);
24
- wrapped.call(obj, 3);
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);
25
45
  expect(fn).toHaveBeenCalledTimes(1);
26
46
  });
27
47
 
28
48
  it("computes separately for different args", () => {
29
49
  const fn = vi.fn((x: unknown) => (x as number) * 10);
30
- const wrapped = Memo()(fn, mockCtx("mul"));
50
+ const ctx = mockCtx("mul");
51
+ Memo()(fn, ctx);
52
+
31
53
  const obj = {};
32
- expect(wrapped.call(obj, 2)).toBe(20);
33
- expect(wrapped.call(obj, 3)).toBe(30);
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);
34
59
  expect(fn).toHaveBeenCalledTimes(2);
35
60
  });
36
61
 
37
62
  it("is per-instance — different instances have separate caches", () => {
38
63
  const fn = vi.fn(() => Math.random());
39
- const wrapped = Memo()(fn, mockCtx("rand"));
64
+ const ctx = mockCtx("rand");
65
+ Memo()(fn, ctx);
66
+
40
67
  const a = {};
41
68
  const b = {};
42
- const v1 = wrapped.call(a);
43
- const v2 = wrapped.call(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();
44
76
  expect(fn).toHaveBeenCalledTimes(2);
45
77
  expect(v1).not.toBe(v2); // different random values per instance
46
78
  });
47
79
 
48
80
  it("serializes no-args calls with __no_args__ key", () => {
49
81
  const fn = vi.fn(() => 42);
50
- const wrapped = Memo()(fn, mockCtx("noop"));
82
+ const ctx = mockCtx("noop");
83
+ Memo()(fn, ctx);
84
+
51
85
  const obj = {};
52
- wrapped.call(obj);
53
- wrapped.call(obj);
86
+ ctx.runInitializers(obj);
87
+ const method = (obj as Record<string, () => unknown>).noop;
88
+
89
+ method();
90
+ method();
54
91
  expect(fn).toHaveBeenCalledTimes(1);
55
92
  });
56
93
 
57
94
  it("serializes object args via JSON.stringify", () => {
58
95
  const fn = vi.fn((o: unknown) => JSON.stringify(o));
59
- const wrapped = Memo()(fn, mockCtx("obj"));
96
+ const ctx = mockCtx("obj");
97
+ Memo()(fn, ctx);
98
+
60
99
  const obj = {};
61
- wrapped.call(obj, { a: 1 });
62
- wrapped.call(obj, { a: 1 }); // same key
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
63
105
  expect(fn).toHaveBeenCalledTimes(1);
64
106
  });
65
107
 
66
108
  it("re-evaluates when a reactive signal read inside the method changes", () => {
67
109
  const s = signal(1);
68
110
  const fn = vi.fn(() => s() * 10);
69
- const wrapped = Memo()(fn, mockCtx("reactive"));
111
+ const ctx = mockCtx("reactive");
112
+ Memo()(fn, ctx);
113
+
70
114
  const obj = {};
115
+ ctx.runInitializers(obj);
116
+ const method = (obj as Record<string, () => unknown>).reactive;
71
117
 
72
- expect(wrapped.call(obj)).toBe(10);
118
+ expect(method()).toBe(10);
73
119
  expect(fn).toHaveBeenCalledTimes(1);
74
120
 
75
121
  s.set(5);
76
122
  // The computed tracks s() — next read triggers recomputation
77
- expect(wrapped.call(obj)).toBe(50);
123
+ expect(method()).toBe(50);
78
124
  expect(fn).toHaveBeenCalledTimes(2);
79
125
  });
80
126
 
81
127
  it("serializes symbol args via toString()", () => {
82
128
  const fn = vi.fn((s: unknown) => String(s));
83
- const wrapped = Memo()(fn, mockCtx("sym"));
129
+ const ctx = mockCtx("sym");
130
+ Memo()(fn, ctx);
131
+
84
132
  const obj = {};
133
+ ctx.runInitializers(obj);
134
+ const method = (obj as Record<string, (...a: unknown[]) => unknown>).sym;
135
+
85
136
  const sym = Symbol("test");
86
- wrapped.call(obj, sym);
87
- wrapped.call(obj, sym);
137
+ method(sym);
138
+ method(sym);
88
139
  expect(fn).toHaveBeenCalledTimes(1);
89
140
  });
90
141
  });