@praxisjs/decorators 0.4.1 → 0.4.3

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 (63) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/__tests__/component.test.d.ts +2 -0
  3. package/dist/__tests__/component.test.d.ts.map +1 -0
  4. package/dist/__tests__/component.test.js +137 -0
  5. package/dist/__tests__/component.test.js.map +1 -0
  6. package/dist/__tests__/events.test.d.ts +2 -0
  7. package/dist/__tests__/events.test.d.ts.map +1 -0
  8. package/dist/__tests__/events.test.js +190 -0
  9. package/dist/__tests__/events.test.js.map +1 -0
  10. package/dist/__tests__/functions.test.d.ts +2 -0
  11. package/dist/__tests__/functions.test.d.ts.map +1 -0
  12. package/dist/__tests__/functions.test.js +427 -0
  13. package/dist/__tests__/functions.test.js.map +1 -0
  14. package/dist/__tests__/lazy.test.d.ts +2 -0
  15. package/dist/__tests__/lazy.test.d.ts.map +1 -0
  16. package/dist/__tests__/lazy.test.js +165 -0
  17. package/dist/__tests__/lazy.test.js.map +1 -0
  18. package/dist/__tests__/memo.test.d.ts +2 -0
  19. package/dist/__tests__/memo.test.d.ts.map +1 -0
  20. package/dist/__tests__/memo.test.js +79 -0
  21. package/dist/__tests__/memo.test.js.map +1 -0
  22. package/dist/__tests__/properties-browser.test.d.ts +2 -0
  23. package/dist/__tests__/properties-browser.test.d.ts.map +1 -0
  24. package/dist/__tests__/properties-browser.test.js +178 -0
  25. package/dist/__tests__/properties-browser.test.js.map +1 -0
  26. package/dist/__tests__/properties.test.d.ts +2 -0
  27. package/dist/__tests__/properties.test.d.ts.map +1 -0
  28. package/dist/__tests__/properties.test.js +571 -0
  29. package/dist/__tests__/properties.test.js.map +1 -0
  30. package/dist/__tests__/slot.test.d.ts +2 -0
  31. package/dist/__tests__/slot.test.d.ts.map +1 -0
  32. package/dist/__tests__/slot.test.js +115 -0
  33. package/dist/__tests__/slot.test.js.map +1 -0
  34. package/dist/__tests__/virtual.test.d.ts +2 -0
  35. package/dist/__tests__/virtual.test.d.ts.map +1 -0
  36. package/dist/__tests__/virtual.test.js +201 -0
  37. package/dist/__tests__/virtual.test.js.map +1 -0
  38. package/dist/component/lazy.d.ts +1 -1
  39. package/dist/component/lazy.d.ts.map +1 -1
  40. package/dist/component/lazy.js +4 -1
  41. package/dist/component/lazy.js.map +1 -1
  42. package/dist/component/virtual.d.ts +1 -1
  43. package/dist/component/virtual.d.ts.map +1 -1
  44. package/dist/functions/watch.d.ts.map +1 -1
  45. package/dist/functions/watch.js +10 -2
  46. package/dist/functions/watch.js.map +1 -1
  47. package/dist/properties/history.d.ts.map +1 -1
  48. package/dist/properties/history.js +4 -8
  49. package/dist/properties/history.js.map +1 -1
  50. package/package.json +2 -2
  51. package/src/__tests__/component.test.ts +156 -0
  52. package/src/__tests__/events.test.ts +236 -0
  53. package/src/__tests__/functions.test.ts +549 -0
  54. package/src/__tests__/lazy.test.ts +220 -0
  55. package/src/__tests__/memo.test.ts +90 -0
  56. package/src/__tests__/properties-browser.test.ts +234 -0
  57. package/src/__tests__/properties.test.ts +725 -0
  58. package/src/__tests__/slot.test.ts +141 -0
  59. package/src/__tests__/virtual.test.ts +241 -0
  60. package/src/component/lazy.ts +4 -2
  61. package/src/component/virtual.tsx +1 -1
  62. package/src/functions/watch.ts +11 -2
  63. package/src/properties/history.ts +4 -8
@@ -0,0 +1,549 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+
3
+ import { Bind } from "../functions/bind";
4
+ import { Debounce } from "../functions/debounce";
5
+ import { Log } from "../functions/log";
6
+ import { Once } from "../functions/once";
7
+ import { Retry } from "../functions/retry";
8
+ import { Throttle } from "../functions/throttle";
9
+
10
+ // Helpers to simulate TC39 decorator context
11
+ function mockMethodContext(name: string) {
12
+ const initializers: Array<(this: object) => void> = [];
13
+ const ctx = {
14
+ name,
15
+ kind: "method" as const,
16
+ addInitializer(fn: (this: object) => void) {
17
+ initializers.push(fn);
18
+ },
19
+ runInitializers(instance: object) {
20
+ initializers.forEach((fn) => { fn.call(instance); });
21
+ },
22
+ };
23
+ return ctx;
24
+ }
25
+
26
+ // ── Once ─────────────────────────────────────────────────────────────────────
27
+
28
+ describe("Once", () => {
29
+ it("only calls the original function once", () => {
30
+ const original = vi.fn(() => 42);
31
+ const ctx = mockMethodContext("doSomething");
32
+ const wrapped = Once()(original, ctx as unknown as ClassMethodDecoratorContext);
33
+
34
+ const obj = {};
35
+ expect(wrapped.call(obj)).toBe(42);
36
+ expect(wrapped.call(obj)).toBe(42);
37
+ expect(original).toHaveBeenCalledTimes(1);
38
+ });
39
+
40
+ it("returns the same result on subsequent calls", () => {
41
+ let counter = 0;
42
+ const original = () => ++counter;
43
+ const ctx = mockMethodContext("inc");
44
+ const wrapped = Once()(original, ctx as unknown as ClassMethodDecoratorContext);
45
+
46
+ const obj = {};
47
+ wrapped.call(obj);
48
+ wrapped.call(obj);
49
+ wrapped.call(obj);
50
+ expect(counter).toBe(1);
51
+ });
52
+
53
+ it("is per-instance — different instances call independently", () => {
54
+ const original = vi.fn(() => "result");
55
+ const ctx = mockMethodContext("fn");
56
+ const wrapped = Once()(original, ctx as unknown as ClassMethodDecoratorContext);
57
+
58
+ const a = {};
59
+ const b = {};
60
+ wrapped.call(a);
61
+ wrapped.call(b);
62
+ expect(original).toHaveBeenCalledTimes(2);
63
+ });
64
+ });
65
+
66
+ // ── Retry ─────────────────────────────────────────────────────────────────────
67
+
68
+ describe("Retry", () => {
69
+ it("returns result if the first attempt succeeds", async () => {
70
+ const original = vi.fn(async () => "ok");
71
+ const ctx = mockMethodContext("fetch");
72
+ const wrapped = Retry(3)(original, ctx as unknown as ClassMethodDecoratorContext);
73
+
74
+ const result = await wrapped.call({});
75
+ expect(result).toBe("ok");
76
+ expect(original).toHaveBeenCalledTimes(1);
77
+ });
78
+
79
+ it("retries and succeeds on a later attempt", async () => {
80
+ let attempt = 0;
81
+ const original = vi.fn(async () => {
82
+ attempt++;
83
+ if (attempt < 3) throw new Error("fail");
84
+ return "success";
85
+ });
86
+ const ctx = mockMethodContext("call");
87
+ const wrapped = Retry(3)(original, ctx as unknown as ClassMethodDecoratorContext);
88
+
89
+ const result = await wrapped.call({});
90
+ expect(result).toBe("success");
91
+ expect(original).toHaveBeenCalledTimes(3);
92
+ });
93
+
94
+ it("throws after exhausting all attempts", async () => {
95
+ const original = vi.fn(async () => {
96
+ throw new Error("always fails");
97
+ });
98
+ const ctx = mockMethodContext("bad");
99
+ const wrapped = Retry(3)(original, ctx as unknown as ClassMethodDecoratorContext);
100
+
101
+ await expect(wrapped.call({})).rejects.toThrow("always fails");
102
+ expect(original).toHaveBeenCalledTimes(3);
103
+ });
104
+
105
+ it("applies delay+backoff between retries", async () => {
106
+ vi.useFakeTimers();
107
+ const original = vi.fn(async () => { throw new Error("fail"); });
108
+ const ctx = mockMethodContext("delayed");
109
+ const wrapped = Retry(3, { delay: 100, backoff: 2 })(
110
+ original,
111
+ ctx as unknown as ClassMethodDecoratorContext,
112
+ );
113
+ const p = wrapped.call({});
114
+ const rejection = expect(p).rejects.toThrow("fail");
115
+ await vi.runAllTimersAsync();
116
+ await rejection;
117
+ expect(original).toHaveBeenCalledTimes(3);
118
+ vi.useRealTimers();
119
+ });
120
+
121
+ it("calls onRetry callback for each failed attempt (except last)", async () => {
122
+ const onRetry = vi.fn();
123
+ const original = vi.fn(async () => {
124
+ throw new Error("err");
125
+ });
126
+ const ctx = mockMethodContext("op");
127
+ const wrapped = Retry(3, { onRetry })(original, ctx as unknown as ClassMethodDecoratorContext);
128
+
129
+ await expect(wrapped.call({})).rejects.toThrow();
130
+ expect(onRetry).toHaveBeenCalledTimes(2); // attempts 1 and 2, not the last
131
+ });
132
+
133
+ it("maxAttempts=1 fails immediately without retrying", async () => {
134
+ const original = vi.fn(async () => { throw new Error("instant fail"); });
135
+ const ctx = mockMethodContext("once");
136
+ const wrapped = Retry(1)(original, ctx as unknown as ClassMethodDecoratorContext);
137
+
138
+ await expect(wrapped.call({})).rejects.toThrow("instant fail");
139
+ expect(original).toHaveBeenCalledTimes(1);
140
+ });
141
+
142
+ it("non-Error throw is wrapped in an Error", async () => {
143
+ const original = vi.fn(async () => {
144
+ // eslint-disable-next-line @typescript-eslint/only-throw-error
145
+ throw "string error";
146
+ });
147
+ const ctx = mockMethodContext("strThrow");
148
+ const wrapped = Retry(1)(original, ctx as unknown as ClassMethodDecoratorContext);
149
+
150
+ await expect(wrapped.call({})).rejects.toThrow("string error");
151
+ });
152
+
153
+ it("maxAttempts=0 throws immediately without calling the function", async () => {
154
+ const original = vi.fn(async () => "ok");
155
+ const ctx = mockMethodContext("never");
156
+ const wrapped = Retry(0)(original, ctx as unknown as ClassMethodDecoratorContext);
157
+
158
+ await expect(wrapped.call({})).rejects.toThrow("Unknown error");
159
+ expect(original).not.toHaveBeenCalled();
160
+ });
161
+
162
+ it("onRetry receives the correct attempt number", async () => {
163
+ const attempts: number[] = [];
164
+ const original = vi.fn(async () => { throw new Error("fail"); });
165
+ const ctx = mockMethodContext("numbered");
166
+ const wrapped = Retry(3, {
167
+ onRetry: (_err, attempt) => { attempts.push(attempt); },
168
+ })(original, ctx as unknown as ClassMethodDecoratorContext);
169
+
170
+ await expect(wrapped.call({})).rejects.toThrow();
171
+ expect(attempts).toEqual([1, 2]); // attempt 1 failed → onRetry(1), attempt 2 failed → onRetry(2)
172
+ });
173
+
174
+ it("backoff multiplies delay correctly", async () => {
175
+ vi.useFakeTimers();
176
+ const callTimes: number[] = [];
177
+ const original = vi.fn(async () => {
178
+ callTimes.push(Date.now());
179
+ throw new Error("fail");
180
+ });
181
+ const ctx = mockMethodContext("backoffTest");
182
+ const wrapped = Retry(3, { delay: 100, backoff: 2 })(
183
+ original,
184
+ ctx as unknown as ClassMethodDecoratorContext,
185
+ );
186
+
187
+ const p = wrapped.call({});
188
+ const rejection = expect(p).rejects.toThrow("fail");
189
+ await vi.runAllTimersAsync();
190
+ await rejection;
191
+
192
+ expect(callTimes.length).toBe(3);
193
+ // Attempt 2 runs 100ms after attempt 1
194
+ expect(callTimes[1] - callTimes[0]).toBeGreaterThanOrEqual(100);
195
+ // Attempt 3 runs 200ms after attempt 2 (backoff × 2)
196
+ expect(callTimes[2] - callTimes[1]).toBeGreaterThanOrEqual(200);
197
+ vi.useRealTimers();
198
+ });
199
+ });
200
+
201
+ // ── Log ──────────────────────────────────────────────────────────────────────
202
+
203
+ describe("Log", () => {
204
+ it("calls the original function and returns its value", () => {
205
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
206
+ const original = vi.fn(() => 99);
207
+ const ctx = mockMethodContext("myMethod");
208
+ const wrapped = Log({ args: false, result: false })(
209
+ original,
210
+ ctx as unknown as ClassMethodDecoratorContext,
211
+ );
212
+
213
+ class Dummy {}
214
+ const result = wrapped.call(new Dummy());
215
+ expect(result).toBe(99);
216
+ spy.mockRestore();
217
+ });
218
+
219
+ it("logs args when args=true", () => {
220
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
221
+ const original = vi.fn((x: unknown) => x);
222
+ const ctx = mockMethodContext("greet");
223
+ const wrapped = Log({ args: true, result: false })(
224
+ original,
225
+ ctx as unknown as ClassMethodDecoratorContext,
226
+ );
227
+
228
+ class Foo {}
229
+ wrapped.call(new Foo(), "hello");
230
+ expect(spy).toHaveBeenCalledWith(expect.stringContaining("args:"), ["hello"]);
231
+ spy.mockRestore();
232
+ });
233
+
234
+ it("does not log in production when devOnly=true", () => {
235
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
236
+ const original = vi.fn(() => "value");
237
+ const ctx = mockMethodContext("prod");
238
+ const wrapped = Log({ devOnly: true })(
239
+ original,
240
+ ctx as unknown as ClassMethodDecoratorContext,
241
+ );
242
+
243
+ const prevEnv = process.env.NODE_ENV;
244
+ process.env.NODE_ENV = "production";
245
+
246
+ class App {}
247
+ wrapped.call(new App());
248
+ expect(spy).not.toHaveBeenCalled();
249
+
250
+ process.env.NODE_ENV = prevEnv;
251
+ spy.mockRestore();
252
+ });
253
+
254
+ it("handles async functions — logs resolved value", async () => {
255
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
256
+ const original = vi.fn(async () => "async-result");
257
+ const ctx = mockMethodContext("load");
258
+ const wrapped = Log({ args: false, result: true })(
259
+ original,
260
+ ctx as unknown as ClassMethodDecoratorContext,
261
+ );
262
+
263
+ class Svc {}
264
+ await wrapped.call(new Svc());
265
+ expect(spy).toHaveBeenCalledWith(
266
+ expect.stringContaining("resolved:"),
267
+ "async-result",
268
+ expect.any(String),
269
+ );
270
+ spy.mockRestore();
271
+ });
272
+
273
+ it("handles async rejection — logs rejected value and re-throws", async () => {
274
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
275
+ const original = vi.fn(async () => { throw new Error("async-error"); });
276
+ const ctx = mockMethodContext("failing");
277
+ const wrapped = Log({ args: false, result: true })(
278
+ original,
279
+ ctx as unknown as ClassMethodDecoratorContext,
280
+ );
281
+ class Svc {}
282
+ await expect(wrapped.call(new Svc())).rejects.toThrow("async-error");
283
+ expect(spy).toHaveBeenCalledWith(
284
+ expect.stringContaining("rejected:"),
285
+ expect.any(Error),
286
+ );
287
+ spy.mockRestore();
288
+ });
289
+ });
290
+
291
+ // ── Log (additional branches) ─────────────────────────────────────────────────
292
+
293
+ describe("Log — time option", () => {
294
+ it("includes elapsed time in sync result log when time=true", () => {
295
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
296
+ const original = vi.fn(() => "value");
297
+ const ctx = mockMethodContext("timed");
298
+ const wrapped = Log({ args: false, result: true, time: true })(
299
+ original,
300
+ ctx as unknown as ClassMethodDecoratorContext,
301
+ );
302
+
303
+ class Svc {}
304
+ wrapped.call(new Svc());
305
+ // Third argument should be a string containing "ms"
306
+ expect(spy).toHaveBeenCalledWith(
307
+ expect.stringContaining("returned:"),
308
+ "value",
309
+ expect.stringContaining("ms"),
310
+ );
311
+ spy.mockRestore();
312
+ });
313
+
314
+ it("includes elapsed time in async result log when time=true", async () => {
315
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
316
+ const original = vi.fn(async () => "async-val");
317
+ const ctx = mockMethodContext("asyncTimed");
318
+ const wrapped = Log({ args: false, result: true, time: true })(
319
+ original,
320
+ ctx as unknown as ClassMethodDecoratorContext,
321
+ );
322
+
323
+ class Svc {}
324
+ await wrapped.call(new Svc());
325
+ expect(spy).toHaveBeenCalledWith(
326
+ expect.stringContaining("resolved:"),
327
+ "async-val",
328
+ expect.stringContaining("ms"),
329
+ );
330
+ spy.mockRestore();
331
+ });
332
+
333
+ it("skips result log when result=false", () => {
334
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
335
+ const original = vi.fn(() => 42);
336
+ const ctx = mockMethodContext("noResult");
337
+ const wrapped = Log({ args: false, result: false })(
338
+ original,
339
+ ctx as unknown as ClassMethodDecoratorContext,
340
+ );
341
+
342
+ class Svc {}
343
+ wrapped.call(new Svc());
344
+ // Should not log a "returned:" line
345
+ const calls = spy.mock.calls.flat().join(" ");
346
+ expect(calls).not.toContain("returned:");
347
+ spy.mockRestore();
348
+ });
349
+ });
350
+
351
+ // ── Bind ─────────────────────────────────────────────────────────────────────
352
+
353
+ describe("Bind", () => {
354
+ it("binds method to the instance so 'this' is preserved", () => {
355
+ class Counter {
356
+ value = 10;
357
+ getValue() {
358
+ return this.value;
359
+ }
360
+ }
361
+ const ctx = mockMethodContext("getValue");
362
+ Bind()(Counter.prototype.getValue, ctx as unknown as ClassMethodDecoratorContext);
363
+
364
+ const c = new Counter();
365
+ ctx.runInitializers(c);
366
+
367
+ const detached = (c as unknown as Record<string, () => number>).getValue;
368
+ expect(detached()).toBe(10);
369
+ });
370
+ });
371
+
372
+ // ── Debounce ─────────────────────────────────────────────────────────────────
373
+
374
+ describe("Debounce", () => {
375
+ it("defers execution until after the wait period", () => {
376
+ vi.useFakeTimers();
377
+ const fn = vi.fn();
378
+ const ctx = mockMethodContext("search");
379
+ Debounce(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
380
+
381
+ const obj = {};
382
+ ctx.runInitializers(obj);
383
+ const method = (obj as Record<string, (...a: unknown[]) => void>).search;
384
+
385
+ method("a");
386
+ method("b");
387
+ method("c");
388
+ expect(fn).not.toHaveBeenCalled();
389
+
390
+ vi.advanceTimersByTime(100);
391
+ expect(fn).toHaveBeenCalledTimes(1);
392
+ expect(fn).toHaveBeenCalledWith("c");
393
+ vi.useRealTimers();
394
+ });
395
+
396
+ it("resets the timer on each call", () => {
397
+ vi.useFakeTimers();
398
+ const fn = vi.fn();
399
+ const ctx = mockMethodContext("onInput");
400
+ Debounce(50)(fn, ctx as unknown as ClassMethodDecoratorContext);
401
+
402
+ const obj = {};
403
+ ctx.runInitializers(obj);
404
+ const method = (obj as Record<string, (...a: unknown[]) => void>).onInput;
405
+
406
+ method("x");
407
+ vi.advanceTimersByTime(30);
408
+ method("y");
409
+ vi.advanceTimersByTime(30); // 30ms since last call — not enough
410
+ expect(fn).not.toHaveBeenCalled();
411
+
412
+ vi.advanceTimersByTime(20); // now 50ms since "y"
413
+ expect(fn).toHaveBeenCalledOnce();
414
+ vi.useRealTimers();
415
+ });
416
+
417
+ it("multiple instances have independent timers", () => {
418
+ vi.useFakeTimers();
419
+ const fn = vi.fn();
420
+ const ctx = mockMethodContext("act");
421
+ Debounce(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
422
+
423
+ const obj1 = {};
424
+ const obj2 = {};
425
+ ctx.runInitializers(obj1);
426
+ ctx.runInitializers(obj2);
427
+
428
+ const m1 = (obj1 as Record<string, (...a: unknown[]) => void>).act;
429
+ const m2 = (obj2 as Record<string, (...a: unknown[]) => void>).act;
430
+
431
+ m1("from-1");
432
+ m2("from-2");
433
+
434
+ vi.advanceTimersByTime(100);
435
+ // Both timers fire independently
436
+ expect(fn).toHaveBeenCalledTimes(2);
437
+ expect(fn).toHaveBeenCalledWith("from-1");
438
+ expect(fn).toHaveBeenCalledWith("from-2");
439
+ vi.useRealTimers();
440
+ });
441
+
442
+ it("delay=0 fires after the next macrotask", () => {
443
+ vi.useFakeTimers();
444
+ const fn = vi.fn();
445
+ const ctx = mockMethodContext("instant");
446
+ Debounce(0)(fn, ctx as unknown as ClassMethodDecoratorContext);
447
+
448
+ const obj = {};
449
+ ctx.runInitializers(obj);
450
+ const method = (obj as Record<string, (...a: unknown[]) => void>).instant;
451
+
452
+ method("a");
453
+ expect(fn).not.toHaveBeenCalled();
454
+ vi.advanceTimersByTime(0);
455
+ expect(fn).toHaveBeenCalledTimes(1);
456
+ expect(fn).toHaveBeenCalledWith("a");
457
+ vi.useRealTimers();
458
+ });
459
+ });
460
+
461
+ // ── Throttle ─────────────────────────────────────────────────────────────────
462
+
463
+ describe("Throttle", () => {
464
+ it("executes the first call immediately", () => {
465
+ vi.useFakeTimers();
466
+ const fn = vi.fn(() => "first");
467
+ const ctx = mockMethodContext("save");
468
+ Throttle(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
469
+
470
+ const obj = {};
471
+ ctx.runInitializers(obj);
472
+ const method = (obj as Record<string, (...a: unknown[]) => void>).save;
473
+
474
+ method();
475
+ expect(fn).toHaveBeenCalledTimes(1);
476
+ vi.useRealTimers();
477
+ });
478
+
479
+ it("ignores calls within the throttle window", () => {
480
+ vi.useFakeTimers();
481
+ const fn = vi.fn();
482
+ const ctx = mockMethodContext("scroll");
483
+ Throttle(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
484
+
485
+ const obj = {};
486
+ ctx.runInitializers(obj);
487
+ const method = (obj as Record<string, (...a: unknown[]) => void>).scroll;
488
+
489
+ method();
490
+ method();
491
+ method();
492
+ expect(fn).toHaveBeenCalledTimes(1);
493
+ vi.useRealTimers();
494
+ });
495
+
496
+ it("allows a new call after the window expires", () => {
497
+ vi.useFakeTimers();
498
+ const fn = vi.fn();
499
+ const ctx = mockMethodContext("resize");
500
+ Throttle(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
501
+
502
+ const obj = {};
503
+ ctx.runInitializers(obj);
504
+ const method = (obj as Record<string, (...a: unknown[]) => void>).resize;
505
+
506
+ method();
507
+ vi.advanceTimersByTime(100);
508
+ method();
509
+ expect(fn).toHaveBeenCalledTimes(2);
510
+ vi.useRealTimers();
511
+ });
512
+
513
+ it("passes the first call's arguments to the underlying function", () => {
514
+ vi.useFakeTimers();
515
+ const fn = vi.fn();
516
+ const ctx = mockMethodContext("click");
517
+ Throttle(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
518
+
519
+ const obj = {};
520
+ ctx.runInitializers(obj);
521
+ const method = (obj as Record<string, (...a: unknown[]) => void>).click;
522
+
523
+ method("first-arg");
524
+ method("second-arg");
525
+ expect(fn).toHaveBeenCalledWith("first-arg");
526
+ vi.useRealTimers();
527
+ });
528
+
529
+ it("multiple instances throttle independently", () => {
530
+ vi.useFakeTimers();
531
+ const fn = vi.fn();
532
+ const ctx = mockMethodContext("ping");
533
+ Throttle(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
534
+
535
+ const obj1 = {};
536
+ const obj2 = {};
537
+ ctx.runInitializers(obj1);
538
+ ctx.runInitializers(obj2);
539
+
540
+ const m1 = (obj1 as Record<string, (...a: unknown[]) => void>).ping;
541
+ const m2 = (obj2 as Record<string, (...a: unknown[]) => void>).ping;
542
+
543
+ m1("a");
544
+ m2("b");
545
+ // Both first calls go through
546
+ expect(fn).toHaveBeenCalledTimes(2);
547
+ vi.useRealTimers();
548
+ });
549
+ });