@praxisjs/decorators 0.4.3 → 0.6.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 (192) hide show
  1. package/CHANGELOG.md +39 -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 +205 -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 +114 -25
  15. package/dist/__tests__/memo.test.js.map +1 -1
  16. package/dist/__tests__/properties.test.js +193 -0
  17. package/dist/__tests__/properties.test.js.map +1 -1
  18. package/dist/__tests__/resource.test.d.ts +2 -0
  19. package/dist/__tests__/resource.test.d.ts.map +1 -0
  20. package/dist/__tests__/resource.test.js +127 -0
  21. package/dist/__tests__/resource.test.js.map +1 -0
  22. package/dist/__tests__/slot.test.js +32 -0
  23. package/dist/__tests__/slot.test.js.map +1 -1
  24. package/dist/__tests__/until.test.d.ts +2 -0
  25. package/dist/__tests__/until.test.d.ts.map +1 -0
  26. package/dist/__tests__/until.test.js +88 -0
  27. package/dist/__tests__/until.test.js.map +1 -0
  28. package/dist/__tests__/virtual.test.js +164 -15
  29. package/dist/__tests__/virtual.test.js.map +1 -1
  30. package/dist/component/component.d.ts +1 -1
  31. package/dist/component/component.d.ts.map +1 -1
  32. package/dist/component/component.js +15 -10
  33. package/dist/component/component.js.map +1 -1
  34. package/dist/component/lazy.d.ts.map +1 -1
  35. package/dist/component/lazy.js +29 -30
  36. package/dist/component/lazy.js.map +1 -1
  37. package/dist/component/virtual.d.ts.map +1 -1
  38. package/dist/component/virtual.js +49 -50
  39. package/dist/component/virtual.js.map +1 -1
  40. package/dist/create-class-decorator.d.ts +16 -0
  41. package/dist/create-class-decorator.d.ts.map +1 -0
  42. package/dist/create-class-decorator.js +35 -0
  43. package/dist/create-class-decorator.js.map +1 -0
  44. package/dist/create-field-decorator.d.ts +14 -0
  45. package/dist/create-field-decorator.d.ts.map +1 -0
  46. package/dist/create-field-decorator.js +37 -0
  47. package/dist/create-field-decorator.js.map +1 -0
  48. package/dist/create-getter-decorator.d.ts +18 -0
  49. package/dist/create-getter-decorator.d.ts.map +1 -0
  50. package/dist/create-getter-decorator.js +15 -0
  51. package/dist/create-getter-decorator.js.map +1 -0
  52. package/dist/create-lifecycle-method-decorator.d.ts +6 -0
  53. package/dist/create-lifecycle-method-decorator.d.ts.map +1 -0
  54. package/dist/create-lifecycle-method-decorator.js +22 -0
  55. package/dist/create-lifecycle-method-decorator.js.map +1 -0
  56. package/dist/create-method-decorator.d.ts +6 -0
  57. package/dist/create-method-decorator.d.ts.map +1 -0
  58. package/dist/create-method-decorator.js +14 -0
  59. package/dist/create-method-decorator.js.map +1 -0
  60. package/dist/events/emit.d.ts +1 -1
  61. package/dist/events/emit.d.ts.map +1 -1
  62. package/dist/events/emit.js +24 -29
  63. package/dist/events/emit.js.map +1 -1
  64. package/dist/events/on-command.d.ts +1 -2
  65. package/dist/events/on-command.d.ts.map +1 -1
  66. package/dist/events/on-command.js +15 -32
  67. package/dist/events/on-command.js.map +1 -1
  68. package/dist/functions/bind.d.ts +1 -2
  69. package/dist/functions/bind.d.ts.map +1 -1
  70. package/dist/functions/bind.js +4 -11
  71. package/dist/functions/bind.js.map +1 -1
  72. package/dist/functions/debounce.d.ts.map +1 -1
  73. package/dist/functions/debounce.js +21 -20
  74. package/dist/functions/debounce.js.map +1 -1
  75. package/dist/functions/index.d.ts +1 -0
  76. package/dist/functions/index.d.ts.map +1 -1
  77. package/dist/functions/index.js +1 -0
  78. package/dist/functions/index.js.map +1 -1
  79. package/dist/functions/log.d.ts +1 -2
  80. package/dist/functions/log.d.ts.map +1 -1
  81. package/dist/functions/log.js +40 -38
  82. package/dist/functions/log.js.map +1 -1
  83. package/dist/functions/memo.d.ts +1 -2
  84. package/dist/functions/memo.d.ts.map +1 -1
  85. package/dist/functions/memo.js +29 -27
  86. package/dist/functions/memo.js.map +1 -1
  87. package/dist/functions/once.d.ts +1 -2
  88. package/dist/functions/once.d.ts.map +1 -1
  89. package/dist/functions/once.js +14 -12
  90. package/dist/functions/once.js.map +1 -1
  91. package/dist/functions/retry.d.ts +1 -2
  92. package/dist/functions/retry.d.ts.map +1 -1
  93. package/dist/functions/retry.js +23 -20
  94. package/dist/functions/retry.js.map +1 -1
  95. package/dist/functions/throttle.d.ts +1 -2
  96. package/dist/functions/throttle.d.ts.map +1 -1
  97. package/dist/functions/throttle.js +11 -17
  98. package/dist/functions/throttle.js.map +1 -1
  99. package/dist/functions/until.d.ts +2 -0
  100. package/dist/functions/until.d.ts.map +1 -0
  101. package/dist/functions/until.js +17 -0
  102. package/dist/functions/until.js.map +1 -0
  103. package/dist/functions/watch.d.ts +1 -1
  104. package/dist/functions/watch.d.ts.map +1 -1
  105. package/dist/functions/watch.js +25 -39
  106. package/dist/functions/watch.js.map +1 -1
  107. package/dist/functions/when.d.ts +1 -2
  108. package/dist/functions/when.d.ts.map +1 -1
  109. package/dist/functions/when.js +10 -26
  110. package/dist/functions/when.js.map +1 -1
  111. package/dist/index.d.ts +7 -2
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +7 -2
  114. package/dist/index.js.map +1 -1
  115. package/dist/properties/compose.d.ts +3 -0
  116. package/dist/properties/compose.d.ts.map +1 -0
  117. package/dist/properties/compose.js +40 -0
  118. package/dist/properties/compose.js.map +1 -0
  119. package/dist/properties/computed.d.ts +1 -2
  120. package/dist/properties/computed.d.ts.map +1 -1
  121. package/dist/properties/computed.js +10 -9
  122. package/dist/properties/computed.js.map +1 -1
  123. package/dist/properties/history.d.ts.map +1 -1
  124. package/dist/properties/history.js +37 -32
  125. package/dist/properties/history.js.map +1 -1
  126. package/dist/properties/index.d.ts +2 -0
  127. package/dist/properties/index.d.ts.map +1 -1
  128. package/dist/properties/index.js +2 -0
  129. package/dist/properties/index.js.map +1 -1
  130. package/dist/properties/persisted.d.ts +1 -2
  131. package/dist/properties/persisted.d.ts.map +1 -1
  132. package/dist/properties/persisted.js +13 -29
  133. package/dist/properties/persisted.js.map +1 -1
  134. package/dist/properties/prop.d.ts +1 -2
  135. package/dist/properties/prop.d.ts.map +1 -1
  136. package/dist/properties/prop.js +21 -25
  137. package/dist/properties/prop.js.map +1 -1
  138. package/dist/properties/resource.d.ts +14 -0
  139. package/dist/properties/resource.d.ts.map +1 -0
  140. package/dist/properties/resource.js +27 -0
  141. package/dist/properties/resource.js.map +1 -0
  142. package/dist/properties/slot.d.ts +1 -2
  143. package/dist/properties/slot.d.ts.map +1 -1
  144. package/dist/properties/slot.js +15 -20
  145. package/dist/properties/slot.js.map +1 -1
  146. package/dist/properties/state.d.ts +1 -2
  147. package/dist/properties/state.d.ts.map +1 -1
  148. package/dist/properties/state.js +13 -18
  149. package/dist/properties/state.js.map +1 -1
  150. package/package.json +2 -2
  151. package/src/__tests__/component.test.ts +10 -0
  152. package/src/__tests__/compose.test.ts +202 -0
  153. package/src/__tests__/events.test.ts +32 -0
  154. package/src/__tests__/functions.test.ts +257 -46
  155. package/src/__tests__/lazy.test.ts +3 -5
  156. package/src/__tests__/memo.test.ts +143 -25
  157. package/src/__tests__/properties.test.ts +233 -0
  158. package/src/__tests__/resource.test.ts +141 -0
  159. package/src/__tests__/slot.test.ts +38 -0
  160. package/src/__tests__/until.test.ts +113 -0
  161. package/src/__tests__/virtual.test.ts +135 -7
  162. package/src/component/component.ts +27 -12
  163. package/src/component/lazy.ts +33 -37
  164. package/src/component/virtual.tsx +67 -75
  165. package/src/create-class-decorator.ts +61 -0
  166. package/src/create-field-decorator.ts +66 -0
  167. package/src/create-getter-decorator.ts +52 -0
  168. package/src/create-lifecycle-method-decorator.ts +38 -0
  169. package/src/create-method-decorator.ts +26 -0
  170. package/src/events/emit.ts +22 -33
  171. package/src/events/on-command.ts +22 -47
  172. package/src/functions/bind.ts +4 -15
  173. package/src/functions/debounce.ts +21 -23
  174. package/src/functions/index.ts +1 -0
  175. package/src/functions/log.ts +40 -43
  176. package/src/functions/memo.ts +28 -42
  177. package/src/functions/once.ts +13 -16
  178. package/src/functions/retry.ts +21 -22
  179. package/src/functions/throttle.ts +11 -24
  180. package/src/functions/until.ts +19 -0
  181. package/src/functions/watch.ts +27 -43
  182. package/src/functions/when.ts +11 -36
  183. package/src/index.ts +29 -0
  184. package/src/properties/compose.ts +53 -0
  185. package/src/properties/computed.ts +11 -13
  186. package/src/properties/history.ts +42 -41
  187. package/src/properties/index.ts +2 -0
  188. package/src/properties/persisted.ts +17 -58
  189. package/src/properties/prop.ts +21 -31
  190. package/src/properties/resource.ts +34 -0
  191. package/src/properties/slot.ts +17 -30
  192. package/src/properties/state.ts +14 -24
@@ -1,5 +1,7 @@
1
1
  import { describe, it, expect, vi } from "vitest";
2
2
 
3
+ import { StatefulComponent } from "@praxisjs/core";
4
+
3
5
  import { Bind } from "../functions/bind";
4
6
  import { Debounce } from "../functions/debounce";
5
7
  import { Log } from "../functions/log";
@@ -29,11 +31,14 @@ describe("Once", () => {
29
31
  it("only calls the original function once", () => {
30
32
  const original = vi.fn(() => 42);
31
33
  const ctx = mockMethodContext("doSomething");
32
- const wrapped = Once()(original, ctx as unknown as ClassMethodDecoratorContext);
34
+ Once()(original, ctx as unknown as ClassMethodDecoratorContext);
33
35
 
34
36
  const obj = {};
35
- expect(wrapped.call(obj)).toBe(42);
36
- expect(wrapped.call(obj)).toBe(42);
37
+ ctx.runInitializers(obj);
38
+ const method = (obj as Record<string, () => unknown>).doSomething;
39
+
40
+ expect(method()).toBe(42);
41
+ expect(method()).toBe(42);
37
42
  expect(original).toHaveBeenCalledTimes(1);
38
43
  });
39
44
 
@@ -41,24 +46,32 @@ describe("Once", () => {
41
46
  let counter = 0;
42
47
  const original = () => ++counter;
43
48
  const ctx = mockMethodContext("inc");
44
- const wrapped = Once()(original, ctx as unknown as ClassMethodDecoratorContext);
49
+ Once()(original, ctx as unknown as ClassMethodDecoratorContext);
45
50
 
46
51
  const obj = {};
47
- wrapped.call(obj);
48
- wrapped.call(obj);
49
- wrapped.call(obj);
52
+ ctx.runInitializers(obj);
53
+ const method = (obj as Record<string, () => unknown>).inc;
54
+
55
+ method();
56
+ method();
57
+ method();
50
58
  expect(counter).toBe(1);
51
59
  });
52
60
 
53
61
  it("is per-instance — different instances call independently", () => {
54
62
  const original = vi.fn(() => "result");
55
63
  const ctx = mockMethodContext("fn");
56
- const wrapped = Once()(original, ctx as unknown as ClassMethodDecoratorContext);
64
+ Once()(original, ctx as unknown as ClassMethodDecoratorContext);
57
65
 
58
66
  const a = {};
59
67
  const b = {};
60
- wrapped.call(a);
61
- wrapped.call(b);
68
+ ctx.runInitializers(a);
69
+ ctx.runInitializers(b);
70
+
71
+ const methodA = (a as Record<string, () => unknown>).fn;
72
+ const methodB = (b as Record<string, () => unknown>).fn;
73
+ methodA();
74
+ methodB();
62
75
  expect(original).toHaveBeenCalledTimes(2);
63
76
  });
64
77
  });
@@ -69,9 +82,13 @@ describe("Retry", () => {
69
82
  it("returns result if the first attempt succeeds", async () => {
70
83
  const original = vi.fn(async () => "ok");
71
84
  const ctx = mockMethodContext("fetch");
72
- const wrapped = Retry(3)(original, ctx as unknown as ClassMethodDecoratorContext);
85
+ Retry(3)(original, ctx as unknown as ClassMethodDecoratorContext);
86
+
87
+ const obj = {};
88
+ ctx.runInitializers(obj);
89
+ const method = (obj as Record<string, () => Promise<unknown>>).fetch;
73
90
 
74
- const result = await wrapped.call({});
91
+ const result = await method();
75
92
  expect(result).toBe("ok");
76
93
  expect(original).toHaveBeenCalledTimes(1);
77
94
  });
@@ -84,9 +101,13 @@ describe("Retry", () => {
84
101
  return "success";
85
102
  });
86
103
  const ctx = mockMethodContext("call");
87
- const wrapped = Retry(3)(original, ctx as unknown as ClassMethodDecoratorContext);
104
+ Retry(3)(original, ctx as unknown as ClassMethodDecoratorContext);
88
105
 
89
- const result = await wrapped.call({});
106
+ const obj = {};
107
+ ctx.runInitializers(obj);
108
+ const method = (obj as Record<string, () => Promise<unknown>>).call;
109
+
110
+ const result = await method();
90
111
  expect(result).toBe("success");
91
112
  expect(original).toHaveBeenCalledTimes(3);
92
113
  });
@@ -96,9 +117,13 @@ describe("Retry", () => {
96
117
  throw new Error("always fails");
97
118
  });
98
119
  const ctx = mockMethodContext("bad");
99
- const wrapped = Retry(3)(original, ctx as unknown as ClassMethodDecoratorContext);
120
+ Retry(3)(original, ctx as unknown as ClassMethodDecoratorContext);
100
121
 
101
- await expect(wrapped.call({})).rejects.toThrow("always fails");
122
+ const obj = {};
123
+ ctx.runInitializers(obj);
124
+ const method = (obj as Record<string, () => Promise<unknown>>).bad;
125
+
126
+ await expect(method()).rejects.toThrow("always fails");
102
127
  expect(original).toHaveBeenCalledTimes(3);
103
128
  });
104
129
 
@@ -106,11 +131,16 @@ describe("Retry", () => {
106
131
  vi.useFakeTimers();
107
132
  const original = vi.fn(async () => { throw new Error("fail"); });
108
133
  const ctx = mockMethodContext("delayed");
109
- const wrapped = Retry(3, { delay: 100, backoff: 2 })(
134
+ Retry(3, { delay: 100, backoff: 2 })(
110
135
  original,
111
136
  ctx as unknown as ClassMethodDecoratorContext,
112
137
  );
113
- const p = wrapped.call({});
138
+
139
+ const obj = {};
140
+ ctx.runInitializers(obj);
141
+ const method = (obj as Record<string, () => Promise<unknown>>).delayed;
142
+
143
+ const p = method();
114
144
  const rejection = expect(p).rejects.toThrow("fail");
115
145
  await vi.runAllTimersAsync();
116
146
  await rejection;
@@ -124,18 +154,26 @@ describe("Retry", () => {
124
154
  throw new Error("err");
125
155
  });
126
156
  const ctx = mockMethodContext("op");
127
- const wrapped = Retry(3, { onRetry })(original, ctx as unknown as ClassMethodDecoratorContext);
157
+ Retry(3, { onRetry })(original, ctx as unknown as ClassMethodDecoratorContext);
128
158
 
129
- await expect(wrapped.call({})).rejects.toThrow();
159
+ const obj = {};
160
+ ctx.runInitializers(obj);
161
+ const method = (obj as Record<string, () => Promise<unknown>>).op;
162
+
163
+ await expect(method()).rejects.toThrow();
130
164
  expect(onRetry).toHaveBeenCalledTimes(2); // attempts 1 and 2, not the last
131
165
  });
132
166
 
133
167
  it("maxAttempts=1 fails immediately without retrying", async () => {
134
168
  const original = vi.fn(async () => { throw new Error("instant fail"); });
135
169
  const ctx = mockMethodContext("once");
136
- const wrapped = Retry(1)(original, ctx as unknown as ClassMethodDecoratorContext);
170
+ Retry(1)(original, ctx as unknown as ClassMethodDecoratorContext);
171
+
172
+ const obj = {};
173
+ ctx.runInitializers(obj);
174
+ const method = (obj as Record<string, () => Promise<unknown>>).once;
137
175
 
138
- await expect(wrapped.call({})).rejects.toThrow("instant fail");
176
+ await expect(method()).rejects.toThrow("instant fail");
139
177
  expect(original).toHaveBeenCalledTimes(1);
140
178
  });
141
179
 
@@ -145,17 +183,25 @@ describe("Retry", () => {
145
183
  throw "string error";
146
184
  });
147
185
  const ctx = mockMethodContext("strThrow");
148
- const wrapped = Retry(1)(original, ctx as unknown as ClassMethodDecoratorContext);
186
+ Retry(1)(original, ctx as unknown as ClassMethodDecoratorContext);
187
+
188
+ const obj = {};
189
+ ctx.runInitializers(obj);
190
+ const method = (obj as Record<string, () => Promise<unknown>>).strThrow;
149
191
 
150
- await expect(wrapped.call({})).rejects.toThrow("string error");
192
+ await expect(method()).rejects.toThrow("string error");
151
193
  });
152
194
 
153
195
  it("maxAttempts=0 throws immediately without calling the function", async () => {
154
196
  const original = vi.fn(async () => "ok");
155
197
  const ctx = mockMethodContext("never");
156
- const wrapped = Retry(0)(original, ctx as unknown as ClassMethodDecoratorContext);
198
+ Retry(0)(original, ctx as unknown as ClassMethodDecoratorContext);
157
199
 
158
- await expect(wrapped.call({})).rejects.toThrow("Unknown error");
200
+ const obj = {};
201
+ ctx.runInitializers(obj);
202
+ const method = (obj as Record<string, () => Promise<unknown>>).never;
203
+
204
+ await expect(method()).rejects.toThrow("Unknown error");
159
205
  expect(original).not.toHaveBeenCalled();
160
206
  });
161
207
 
@@ -163,11 +209,15 @@ describe("Retry", () => {
163
209
  const attempts: number[] = [];
164
210
  const original = vi.fn(async () => { throw new Error("fail"); });
165
211
  const ctx = mockMethodContext("numbered");
166
- const wrapped = Retry(3, {
212
+ Retry(3, {
167
213
  onRetry: (_err, attempt) => { attempts.push(attempt); },
168
214
  })(original, ctx as unknown as ClassMethodDecoratorContext);
169
215
 
170
- await expect(wrapped.call({})).rejects.toThrow();
216
+ const obj = {};
217
+ ctx.runInitializers(obj);
218
+ const method = (obj as Record<string, () => Promise<unknown>>).numbered;
219
+
220
+ await expect(method()).rejects.toThrow();
171
221
  expect(attempts).toEqual([1, 2]); // attempt 1 failed → onRetry(1), attempt 2 failed → onRetry(2)
172
222
  });
173
223
 
@@ -179,12 +229,16 @@ describe("Retry", () => {
179
229
  throw new Error("fail");
180
230
  });
181
231
  const ctx = mockMethodContext("backoffTest");
182
- const wrapped = Retry(3, { delay: 100, backoff: 2 })(
232
+ Retry(3, { delay: 100, backoff: 2 })(
183
233
  original,
184
234
  ctx as unknown as ClassMethodDecoratorContext,
185
235
  );
186
236
 
187
- const p = wrapped.call({});
237
+ const obj = {};
238
+ ctx.runInitializers(obj);
239
+ const method = (obj as Record<string, () => Promise<unknown>>).backoffTest;
240
+
241
+ const p = method();
188
242
  const rejection = expect(p).rejects.toThrow("fail");
189
243
  await vi.runAllTimersAsync();
190
244
  await rejection;
@@ -205,13 +259,17 @@ describe("Log", () => {
205
259
  const spy = vi.spyOn(console, "log").mockImplementation(() => {});
206
260
  const original = vi.fn(() => 99);
207
261
  const ctx = mockMethodContext("myMethod");
208
- const wrapped = Log({ args: false, result: false })(
262
+ Log({ args: false, result: false })(
209
263
  original,
210
264
  ctx as unknown as ClassMethodDecoratorContext,
211
265
  );
212
266
 
213
267
  class Dummy {}
214
- const result = wrapped.call(new Dummy());
268
+ const dummy = new Dummy();
269
+ ctx.runInitializers(dummy);
270
+ const method = (dummy as Record<string, () => unknown>).myMethod;
271
+
272
+ const result = method();
215
273
  expect(result).toBe(99);
216
274
  spy.mockRestore();
217
275
  });
@@ -220,13 +278,17 @@ describe("Log", () => {
220
278
  const spy = vi.spyOn(console, "log").mockImplementation(() => {});
221
279
  const original = vi.fn((x: unknown) => x);
222
280
  const ctx = mockMethodContext("greet");
223
- const wrapped = Log({ args: true, result: false })(
281
+ Log({ args: true, result: false })(
224
282
  original,
225
283
  ctx as unknown as ClassMethodDecoratorContext,
226
284
  );
227
285
 
228
286
  class Foo {}
229
- wrapped.call(new Foo(), "hello");
287
+ const foo = new Foo();
288
+ ctx.runInitializers(foo);
289
+ const method = (foo as Record<string, (...a: unknown[]) => unknown>).greet;
290
+
291
+ method("hello");
230
292
  expect(spy).toHaveBeenCalledWith(expect.stringContaining("args:"), ["hello"]);
231
293
  spy.mockRestore();
232
294
  });
@@ -235,7 +297,7 @@ describe("Log", () => {
235
297
  const spy = vi.spyOn(console, "log").mockImplementation(() => {});
236
298
  const original = vi.fn(() => "value");
237
299
  const ctx = mockMethodContext("prod");
238
- const wrapped = Log({ devOnly: true })(
300
+ Log({ devOnly: true })(
239
301
  original,
240
302
  ctx as unknown as ClassMethodDecoratorContext,
241
303
  );
@@ -244,7 +306,11 @@ describe("Log", () => {
244
306
  process.env.NODE_ENV = "production";
245
307
 
246
308
  class App {}
247
- wrapped.call(new App());
309
+ const app = new App();
310
+ ctx.runInitializers(app);
311
+ const method = (app as Record<string, () => unknown>).prod;
312
+
313
+ method();
248
314
  expect(spy).not.toHaveBeenCalled();
249
315
 
250
316
  process.env.NODE_ENV = prevEnv;
@@ -255,13 +321,17 @@ describe("Log", () => {
255
321
  const spy = vi.spyOn(console, "log").mockImplementation(() => {});
256
322
  const original = vi.fn(async () => "async-result");
257
323
  const ctx = mockMethodContext("load");
258
- const wrapped = Log({ args: false, result: true })(
324
+ Log({ args: false, result: true })(
259
325
  original,
260
326
  ctx as unknown as ClassMethodDecoratorContext,
261
327
  );
262
328
 
263
329
  class Svc {}
264
- await wrapped.call(new Svc());
330
+ const svc = new Svc();
331
+ ctx.runInitializers(svc);
332
+ const method = (svc as Record<string, () => Promise<unknown>>).load;
333
+
334
+ await method();
265
335
  expect(spy).toHaveBeenCalledWith(
266
336
  expect.stringContaining("resolved:"),
267
337
  "async-result",
@@ -274,12 +344,17 @@ describe("Log", () => {
274
344
  const spy = vi.spyOn(console, "log").mockImplementation(() => {});
275
345
  const original = vi.fn(async () => { throw new Error("async-error"); });
276
346
  const ctx = mockMethodContext("failing");
277
- const wrapped = Log({ args: false, result: true })(
347
+ Log({ args: false, result: true })(
278
348
  original,
279
349
  ctx as unknown as ClassMethodDecoratorContext,
280
350
  );
351
+
281
352
  class Svc {}
282
- await expect(wrapped.call(new Svc())).rejects.toThrow("async-error");
353
+ const svc = new Svc();
354
+ ctx.runInitializers(svc);
355
+ const method = (svc as Record<string, () => Promise<unknown>>).failing;
356
+
357
+ await expect(method()).rejects.toThrow("async-error");
283
358
  expect(spy).toHaveBeenCalledWith(
284
359
  expect.stringContaining("rejected:"),
285
360
  expect.any(Error),
@@ -295,13 +370,17 @@ describe("Log — time option", () => {
295
370
  const spy = vi.spyOn(console, "log").mockImplementation(() => {});
296
371
  const original = vi.fn(() => "value");
297
372
  const ctx = mockMethodContext("timed");
298
- const wrapped = Log({ args: false, result: true, time: true })(
373
+ Log({ args: false, result: true, time: true })(
299
374
  original,
300
375
  ctx as unknown as ClassMethodDecoratorContext,
301
376
  );
302
377
 
303
378
  class Svc {}
304
- wrapped.call(new Svc());
379
+ const svc = new Svc();
380
+ ctx.runInitializers(svc);
381
+ const method = (svc as Record<string, () => unknown>).timed;
382
+
383
+ method();
305
384
  // Third argument should be a string containing "ms"
306
385
  expect(spy).toHaveBeenCalledWith(
307
386
  expect.stringContaining("returned:"),
@@ -315,13 +394,17 @@ describe("Log — time option", () => {
315
394
  const spy = vi.spyOn(console, "log").mockImplementation(() => {});
316
395
  const original = vi.fn(async () => "async-val");
317
396
  const ctx = mockMethodContext("asyncTimed");
318
- const wrapped = Log({ args: false, result: true, time: true })(
397
+ Log({ args: false, result: true, time: true })(
319
398
  original,
320
399
  ctx as unknown as ClassMethodDecoratorContext,
321
400
  );
322
401
 
323
402
  class Svc {}
324
- await wrapped.call(new Svc());
403
+ const svc = new Svc();
404
+ ctx.runInitializers(svc);
405
+ const method = (svc as Record<string, () => Promise<unknown>>).asyncTimed;
406
+
407
+ await method();
325
408
  expect(spy).toHaveBeenCalledWith(
326
409
  expect.stringContaining("resolved:"),
327
410
  "async-val",
@@ -334,18 +417,68 @@ describe("Log — time option", () => {
334
417
  const spy = vi.spyOn(console, "log").mockImplementation(() => {});
335
418
  const original = vi.fn(() => 42);
336
419
  const ctx = mockMethodContext("noResult");
337
- const wrapped = Log({ args: false, result: false })(
420
+ Log({ args: false, result: false })(
338
421
  original,
339
422
  ctx as unknown as ClassMethodDecoratorContext,
340
423
  );
341
424
 
342
425
  class Svc {}
343
- wrapped.call(new Svc());
426
+ const svc = new Svc();
427
+ ctx.runInitializers(svc);
428
+ const method = (svc as Record<string, () => unknown>).noResult;
429
+
430
+ method();
344
431
  // Should not log a "returned:" line
345
432
  const calls = spy.mock.calls.flat().join(" ");
346
433
  expect(calls).not.toContain("returned:");
347
434
  spy.mockRestore();
348
435
  });
436
+
437
+ it("skips resolved log when result=false for async functions", async () => {
438
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
439
+ const original = vi.fn(async () => "async-value");
440
+ const ctx = mockMethodContext("asyncNoResult");
441
+ Log({ args: false, result: false })(
442
+ original,
443
+ ctx as unknown as ClassMethodDecoratorContext,
444
+ );
445
+
446
+ class Svc {}
447
+ const svc = new Svc();
448
+ ctx.runInitializers(svc);
449
+ const method = (svc as Record<string, () => Promise<unknown>>).asyncNoResult;
450
+
451
+ const result = await method();
452
+ expect(result).toBe("async-value");
453
+ const calls = spy.mock.calls.flat().join(" ");
454
+ expect(calls).not.toContain("resolved:");
455
+ spy.mockRestore();
456
+ });
457
+
458
+ it("uses 'Unknown' as class name when constructor.name is undefined", () => {
459
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
460
+ const original = vi.fn(() => "val");
461
+ const ctx = mockMethodContext("m");
462
+ Log({ args: false, result: true })(
463
+ original,
464
+ ctx as unknown as ClassMethodDecoratorContext,
465
+ );
466
+
467
+ // Construct an object whose constructor.name is undefined to trigger ?? "Unknown"
468
+ const fakeInstance = Object.assign(Object.create(null) as object, {
469
+ constructor: { name: undefined },
470
+ });
471
+ ctx.runInitializers(fakeInstance);
472
+ const method = (fakeInstance as Record<string, () => unknown>).m;
473
+
474
+ method();
475
+ expect(spy).toHaveBeenCalledWith(
476
+ expect.stringContaining("Unknown"),
477
+ expect.anything(),
478
+ expect.anything(),
479
+ );
480
+ spy.mockRestore();
481
+ });
349
482
  });
350
483
 
351
484
  // ── Bind ─────────────────────────────────────────────────────────────────────
@@ -456,6 +589,52 @@ describe("Debounce", () => {
456
589
  expect(fn).toHaveBeenCalledWith("a");
457
590
  vi.useRealTimers();
458
591
  });
592
+
593
+ it("timer is cleared if onUnmount fires before debounce completes", () => {
594
+ vi.useFakeTimers();
595
+ const fn = vi.fn();
596
+ const ctx = mockMethodContext("debounced");
597
+
598
+ class TestComp extends StatefulComponent {
599
+ render() { return null; }
600
+ }
601
+ Debounce(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
602
+
603
+ const instance = new TestComp();
604
+ ctx.runInitializers(instance);
605
+ const method = (instance as unknown as Record<string, (...a: unknown[]) => void>).debounced;
606
+
607
+ method("hello");
608
+ // Unmount before the 100ms debounce fires
609
+ instance.onUnmount?.();
610
+
611
+ vi.advanceTimersByTime(200);
612
+ expect(fn).not.toHaveBeenCalled();
613
+ vi.useRealTimers();
614
+ });
615
+
616
+ it("original method is NOT called after unmount if timer was pending", () => {
617
+ vi.useFakeTimers();
618
+ const fn = vi.fn();
619
+ const ctx = mockMethodContext("debouncedOp");
620
+
621
+ class TestComp2 extends StatefulComponent {
622
+ render() { return null; }
623
+ }
624
+ Debounce(50)(fn, ctx as unknown as ClassMethodDecoratorContext);
625
+
626
+ const instance = new TestComp2();
627
+ ctx.runInitializers(instance);
628
+ const method = (instance as unknown as Record<string, (...a: unknown[]) => void>).debouncedOp;
629
+
630
+ method("arg1");
631
+ method("arg2");
632
+ instance.onUnmount?.();
633
+
634
+ vi.runAllTimers();
635
+ expect(fn).not.toHaveBeenCalled();
636
+ vi.useRealTimers();
637
+ });
459
638
  });
460
639
 
461
640
  // ── Throttle ─────────────────────────────────────────────────────────────────
@@ -546,4 +725,36 @@ describe("Throttle", () => {
546
725
  expect(fn).toHaveBeenCalledTimes(2);
547
726
  vi.useRealTimers();
548
727
  });
728
+
729
+ it("negative delay is treated as 0 — first call always executes", () => {
730
+ vi.useFakeTimers();
731
+ const fn = vi.fn();
732
+ const ctx = mockMethodContext("negDelay");
733
+ Throttle(-100)(fn, ctx as unknown as ClassMethodDecoratorContext);
734
+
735
+ const obj = {};
736
+ ctx.runInitializers(obj);
737
+ const method = (obj as Record<string, (...a: unknown[]) => void>).negDelay;
738
+
739
+ method();
740
+ expect(fn).toHaveBeenCalledTimes(1);
741
+ vi.useRealTimers();
742
+ });
743
+
744
+ it("call at exactly the ms boundary executes again", () => {
745
+ vi.useFakeTimers();
746
+ const fn = vi.fn();
747
+ const ctx = mockMethodContext("boundary");
748
+ Throttle(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
749
+
750
+ const obj = {};
751
+ ctx.runInitializers(obj);
752
+ const method = (obj as Record<string, (...a: unknown[]) => void>).boundary;
753
+
754
+ method(); // first call at t=0
755
+ vi.advanceTimersByTime(100); // advance to exactly 100ms
756
+ method(); // second call at t=100 — should pass (now - last === ms, not less than ms)
757
+ expect(fn).toHaveBeenCalledTimes(2);
758
+ vi.useRealTimers();
759
+ });
549
760
  });
@@ -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
  });