@praxisjs/decorators 1.7.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/dist/component/component.d.ts +1 -1
- package/dist/component/component.d.ts.map +1 -1
- package/dist/component/component.js +8 -15
- package/dist/component/component.js.map +1 -1
- package/dist/component/lazy.d.ts +1 -1
- package/dist/component/lazy.d.ts.map +1 -1
- package/dist/component/lazy.js +44 -36
- package/dist/component/lazy.js.map +1 -1
- package/dist/create-class-decorator.d.ts +27 -5
- package/dist/create-class-decorator.d.ts.map +1 -1
- package/dist/create-class-decorator.js +53 -18
- package/dist/create-class-decorator.js.map +1 -1
- package/dist/create-field-decorator.d.ts +42 -3
- package/dist/create-field-decorator.d.ts.map +1 -1
- package/dist/create-field-decorator.js +63 -27
- package/dist/create-field-decorator.js.map +1 -1
- package/dist/create-getter-decorator.d.ts +81 -16
- package/dist/create-getter-decorator.d.ts.map +1 -1
- package/dist/create-getter-decorator.js +127 -15
- package/dist/create-getter-decorator.js.map +1 -1
- package/dist/create-lifecycle-method-decorator.d.ts +21 -3
- package/dist/create-lifecycle-method-decorator.d.ts.map +1 -1
- package/dist/create-lifecycle-method-decorator.js +25 -18
- package/dist/create-lifecycle-method-decorator.js.map +1 -1
- package/dist/create-method-decorator.d.ts +43 -3
- package/dist/create-method-decorator.d.ts.map +1 -1
- package/dist/create-method-decorator.js +49 -8
- package/dist/create-method-decorator.js.map +1 -1
- package/dist/events/command.d.ts +2 -1
- package/dist/events/command.d.ts.map +1 -1
- package/dist/events/command.js +10 -8
- package/dist/events/command.js.map +1 -1
- package/dist/events/emit.d.ts +2 -2
- package/dist/events/emit.d.ts.map +1 -1
- package/dist/events/emit.js +18 -24
- package/dist/events/emit.js.map +1 -1
- package/dist/events/on-command.d.ts +2 -1
- package/dist/events/on-command.d.ts.map +1 -1
- package/dist/events/on-command.js +12 -14
- package/dist/events/on-command.js.map +1 -1
- package/dist/field.d.ts +51 -0
- package/dist/field.d.ts.map +1 -0
- package/dist/field.js +82 -0
- package/dist/field.js.map +1 -0
- package/dist/functions/bind.d.ts +2 -1
- package/dist/functions/bind.d.ts.map +1 -1
- package/dist/functions/bind.js +4 -4
- package/dist/functions/bind.js.map +1 -1
- package/dist/functions/debounce.d.ts +2 -2
- package/dist/functions/debounce.d.ts.map +1 -1
- package/dist/functions/debounce.js +25 -23
- package/dist/functions/debounce.js.map +1 -1
- package/dist/functions/log.d.ts +2 -1
- package/dist/functions/log.d.ts.map +1 -1
- package/dist/functions/log.js +38 -40
- package/dist/functions/log.js.map +1 -1
- package/dist/functions/memo.d.ts +2 -1
- package/dist/functions/memo.d.ts.map +1 -1
- package/dist/functions/memo.js +41 -40
- package/dist/functions/memo.js.map +1 -1
- package/dist/functions/once.d.ts +2 -1
- package/dist/functions/once.d.ts.map +1 -1
- package/dist/functions/once.js +10 -14
- package/dist/functions/once.js.map +1 -1
- package/dist/functions/retry.d.ts +2 -1
- package/dist/functions/retry.d.ts.map +1 -1
- package/dist/functions/retry.js +21 -23
- package/dist/functions/retry.js.map +1 -1
- package/dist/functions/throttle.d.ts +2 -1
- package/dist/functions/throttle.d.ts.map +1 -1
- package/dist/functions/throttle.js +12 -14
- package/dist/functions/throttle.js.map +1 -1
- package/dist/functions/until.d.ts +2 -1
- package/dist/functions/until.d.ts.map +1 -1
- package/dist/functions/until.js +13 -14
- package/dist/functions/until.js.map +1 -1
- package/dist/functions/watch.d.ts +1 -1
- package/dist/functions/watch.d.ts.map +1 -1
- package/dist/functions/watch.js +36 -34
- package/dist/functions/watch.js.map +1 -1
- package/dist/functions/when.d.ts +1 -1
- package/dist/functions/when.d.ts.map +1 -1
- package/dist/functions/when.js +19 -21
- package/dist/functions/when.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/lifecycle.d.ts +24 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +55 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/properties/compose.d.ts +3 -2
- package/dist/properties/compose.d.ts.map +1 -1
- package/dist/properties/compose.js +33 -19
- package/dist/properties/compose.js.map +1 -1
- package/dist/properties/computed.d.ts +12 -10
- package/dist/properties/computed.d.ts.map +1 -1
- package/dist/properties/computed.js +26 -21
- package/dist/properties/computed.js.map +1 -1
- package/dist/properties/deep-state.d.ts +2 -1
- package/dist/properties/deep-state.d.ts.map +1 -1
- package/dist/properties/deep-state.js +66 -60
- package/dist/properties/deep-state.js.map +1 -1
- package/dist/properties/function-prop.d.ts +2 -1
- package/dist/properties/function-prop.d.ts.map +1 -1
- package/dist/properties/function-prop.js +20 -22
- package/dist/properties/function-prop.js.map +1 -1
- package/dist/properties/history.d.ts +2 -2
- package/dist/properties/history.d.ts.map +1 -1
- package/dist/properties/history.js +34 -33
- package/dist/properties/history.js.map +1 -1
- package/dist/properties/index.d.ts +1 -0
- package/dist/properties/index.d.ts.map +1 -1
- package/dist/properties/index.js +1 -0
- package/dist/properties/index.js.map +1 -1
- package/dist/properties/persisted.d.ts +2 -1
- package/dist/properties/persisted.d.ts.map +1 -1
- package/dist/properties/persisted.js +24 -18
- package/dist/properties/persisted.js.map +1 -1
- package/dist/properties/prop.d.ts +2 -1
- package/dist/properties/prop.d.ts.map +1 -1
- package/dist/properties/prop.js +26 -22
- package/dist/properties/prop.js.map +1 -1
- package/dist/properties/ref.d.ts +2 -1
- package/dist/properties/ref.d.ts.map +1 -1
- package/dist/properties/ref.js +11 -8
- package/dist/properties/ref.js.map +1 -1
- package/dist/properties/resource.d.ts.map +1 -1
- package/dist/properties/resource.js +21 -16
- package/dist/properties/resource.js.map +1 -1
- package/dist/properties/selector.d.ts +12 -0
- package/dist/properties/selector.d.ts.map +1 -0
- package/dist/properties/selector.js +25 -0
- package/dist/properties/selector.js.map +1 -0
- package/dist/properties/slot.d.ts +2 -1
- package/dist/properties/slot.d.ts.map +1 -1
- package/dist/properties/slot.js +22 -20
- package/dist/properties/slot.js.map +1 -1
- package/dist/properties/state.d.ts.map +1 -1
- package/dist/properties/state.js +27 -16
- package/dist/properties/state.js.map +1 -1
- package/dist/properties/synced.d.ts +2 -1
- package/dist/properties/synced.d.ts.map +1 -1
- package/dist/properties/synced.js +27 -19
- package/dist/properties/synced.js.map +1 -1
- package/dist/shared-method.d.ts +14 -0
- package/dist/shared-method.d.ts.map +1 -0
- package/dist/shared-method.js +87 -0
- package/dist/shared-method.js.map +1 -0
- package/package.json +14 -5
- package/src/component/component.ts +8 -27
- package/src/component/lazy.ts +51 -41
- package/src/create-class-decorator.ts +90 -31
- package/src/create-field-decorator.ts +120 -43
- package/src/create-getter-decorator.ts +263 -66
- package/src/create-lifecycle-method-decorator.ts +50 -28
- package/src/create-method-decorator.ts +119 -13
- package/src/events/command.ts +11 -9
- package/src/events/emit.ts +16 -22
- package/src/events/on-command.ts +17 -18
- package/src/field.ts +116 -0
- package/src/functions/bind.ts +4 -4
- package/src/functions/debounce.ts +25 -23
- package/src/functions/log.ts +44 -46
- package/src/functions/memo.ts +39 -37
- package/src/functions/once.ts +10 -13
- package/src/functions/retry.ts +19 -21
- package/src/functions/throttle.ts +12 -13
- package/src/functions/until.ts +14 -14
- package/src/functions/watch.ts +37 -36
- package/src/functions/when.ts +18 -20
- package/src/index.ts +12 -5
- package/src/lifecycle.ts +74 -0
- package/src/properties/compose.ts +33 -20
- package/src/properties/computed.ts +35 -45
- package/src/properties/deep-state.ts +66 -64
- package/src/properties/function-prop.ts +25 -27
- package/src/properties/history.ts +34 -35
- package/src/properties/index.ts +1 -0
- package/src/properties/persisted.ts +30 -20
- package/src/properties/prop.ts +30 -22
- package/src/properties/ref.ts +12 -10
- package/src/properties/resource.ts +22 -17
- package/src/properties/selector.ts +45 -0
- package/src/properties/slot.ts +28 -22
- package/src/properties/state.ts +32 -16
- package/src/properties/synced.ts +31 -19
- package/src/shared-method.ts +101 -0
- package/dist/__tests__/component.test.d.ts +0 -2
- package/dist/__tests__/component.test.d.ts.map +0 -1
- package/dist/__tests__/component.test.js +0 -147
- package/dist/__tests__/component.test.js.map +0 -1
- package/dist/__tests__/compose.test.d.ts +0 -2
- package/dist/__tests__/compose.test.d.ts.map +0 -1
- package/dist/__tests__/compose.test.js +0 -164
- package/dist/__tests__/compose.test.js.map +0 -1
- package/dist/__tests__/deep-state.test.d.ts +0 -2
- package/dist/__tests__/deep-state.test.d.ts.map +0 -1
- package/dist/__tests__/deep-state.test.js +0 -221
- package/dist/__tests__/deep-state.test.js.map +0 -1
- package/dist/__tests__/events.test.d.ts +0 -2
- package/dist/__tests__/events.test.d.ts.map +0 -1
- package/dist/__tests__/events.test.js +0 -295
- package/dist/__tests__/events.test.js.map +0 -1
- package/dist/__tests__/functions.test.d.ts +0 -2
- package/dist/__tests__/functions.test.d.ts.map +0 -1
- package/dist/__tests__/functions.test.js +0 -618
- package/dist/__tests__/functions.test.js.map +0 -1
- package/dist/__tests__/lazy.test.d.ts +0 -2
- package/dist/__tests__/lazy.test.d.ts.map +0 -1
- package/dist/__tests__/lazy.test.js +0 -227
- package/dist/__tests__/lazy.test.js.map +0 -1
- package/dist/__tests__/memo.test.d.ts +0 -2
- package/dist/__tests__/memo.test.d.ts.map +0 -1
- package/dist/__tests__/memo.test.js +0 -249
- package/dist/__tests__/memo.test.js.map +0 -1
- package/dist/__tests__/properties-browser.test.d.ts +0 -2
- package/dist/__tests__/properties-browser.test.d.ts.map +0 -1
- package/dist/__tests__/properties-browser.test.js +0 -227
- package/dist/__tests__/properties-browser.test.js.map +0 -1
- package/dist/__tests__/properties.test.d.ts +0 -2
- package/dist/__tests__/properties.test.d.ts.map +0 -1
- package/dist/__tests__/properties.test.js +0 -1253
- package/dist/__tests__/properties.test.js.map +0 -1
- package/dist/__tests__/ref.test.d.ts +0 -2
- package/dist/__tests__/ref.test.d.ts.map +0 -1
- package/dist/__tests__/ref.test.js +0 -106
- package/dist/__tests__/ref.test.js.map +0 -1
- package/dist/__tests__/resource.test.d.ts +0 -2
- package/dist/__tests__/resource.test.d.ts.map +0 -1
- package/dist/__tests__/resource.test.js +0 -217
- package/dist/__tests__/resource.test.js.map +0 -1
- package/dist/__tests__/slot.test.d.ts +0 -2
- package/dist/__tests__/slot.test.d.ts.map +0 -1
- package/dist/__tests__/slot.test.js +0 -147
- package/dist/__tests__/slot.test.js.map +0 -1
- package/dist/__tests__/synced.test.d.ts +0 -2
- package/dist/__tests__/synced.test.d.ts.map +0 -1
- package/dist/__tests__/synced.test.js +0 -161
- package/dist/__tests__/synced.test.js.map +0 -1
- package/dist/__tests__/until.test.d.ts +0 -2
- package/dist/__tests__/until.test.d.ts.map +0 -1
- package/dist/__tests__/until.test.js +0 -88
- package/dist/__tests__/until.test.js.map +0 -1
- package/dist/create-accessor-decorator.d.ts +0 -14
- package/dist/create-accessor-decorator.d.ts.map +0 -1
- package/dist/create-accessor-decorator.js +0 -27
- package/dist/create-accessor-decorator.js.map +0 -1
- package/src/__tests__/component.test.ts +0 -167
- package/src/__tests__/compose.test.ts +0 -202
- package/src/__tests__/deep-state.test.ts +0 -265
- package/src/__tests__/events.test.ts +0 -366
- package/src/__tests__/functions.test.ts +0 -803
- package/src/__tests__/lazy.test.ts +0 -296
- package/src/__tests__/memo.test.ts +0 -293
- package/src/__tests__/properties-browser.test.ts +0 -288
- package/src/__tests__/properties.test.ts +0 -1556
- package/src/__tests__/ref.test.ts +0 -131
- package/src/__tests__/resource.test.ts +0 -246
- package/src/__tests__/slot.test.ts +0 -179
- package/src/__tests__/synced.test.ts +0 -193
- package/src/__tests__/until.test.ts +0 -113
- package/src/create-accessor-decorator.ts +0 -45
- package/tsconfig.json +0 -9
|
@@ -1,803 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { StatefulComponent } from "@praxisjs/core";
|
|
4
|
-
|
|
5
|
-
import { Bind } from "../functions/bind";
|
|
6
|
-
import { Debounce } from "../functions/debounce";
|
|
7
|
-
import { Log } from "../functions/log";
|
|
8
|
-
import { Once } from "../functions/once";
|
|
9
|
-
import { Retry } from "../functions/retry";
|
|
10
|
-
import { Throttle } from "../functions/throttle";
|
|
11
|
-
|
|
12
|
-
// Helpers to simulate TC39 decorator context
|
|
13
|
-
function mockMethodContext(name: string) {
|
|
14
|
-
const initializers: Array<(this: object) => void> = [];
|
|
15
|
-
const ctx = {
|
|
16
|
-
name,
|
|
17
|
-
kind: "method" as const,
|
|
18
|
-
addInitializer(fn: (this: object) => void) {
|
|
19
|
-
initializers.push(fn);
|
|
20
|
-
},
|
|
21
|
-
runInitializers(instance: object) {
|
|
22
|
-
initializers.forEach((fn) => { fn.call(instance); });
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
return ctx;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// ── Once ─────────────────────────────────────────────────────────────────────
|
|
29
|
-
|
|
30
|
-
describe("Once", () => {
|
|
31
|
-
it("only calls the original function once", () => {
|
|
32
|
-
const original = vi.fn(() => 42);
|
|
33
|
-
const ctx = mockMethodContext("doSomething");
|
|
34
|
-
Once()(original, ctx as unknown as ClassMethodDecoratorContext);
|
|
35
|
-
|
|
36
|
-
const obj = {};
|
|
37
|
-
ctx.runInitializers(obj);
|
|
38
|
-
const method = (obj as Record<string, () => unknown>).doSomething;
|
|
39
|
-
|
|
40
|
-
expect(method()).toBe(42);
|
|
41
|
-
expect(method()).toBe(42);
|
|
42
|
-
expect(original).toHaveBeenCalledTimes(1);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("returns the same result on subsequent calls", () => {
|
|
46
|
-
let counter = 0;
|
|
47
|
-
const original = () => ++counter;
|
|
48
|
-
const ctx = mockMethodContext("inc");
|
|
49
|
-
Once()(original, ctx as unknown as ClassMethodDecoratorContext);
|
|
50
|
-
|
|
51
|
-
const obj = {};
|
|
52
|
-
ctx.runInitializers(obj);
|
|
53
|
-
const method = (obj as Record<string, () => unknown>).inc;
|
|
54
|
-
|
|
55
|
-
method();
|
|
56
|
-
method();
|
|
57
|
-
method();
|
|
58
|
-
expect(counter).toBe(1);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("is per-instance — different instances call independently", () => {
|
|
62
|
-
const original = vi.fn(() => "result");
|
|
63
|
-
const ctx = mockMethodContext("fn");
|
|
64
|
-
Once()(original, ctx as unknown as ClassMethodDecoratorContext);
|
|
65
|
-
|
|
66
|
-
const a = {};
|
|
67
|
-
const 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();
|
|
75
|
-
expect(original).toHaveBeenCalledTimes(2);
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// ── Retry ─────────────────────────────────────────────────────────────────────
|
|
80
|
-
|
|
81
|
-
describe("Retry", () => {
|
|
82
|
-
it("returns result if the first attempt succeeds", async () => {
|
|
83
|
-
const original = vi.fn(async () => "ok");
|
|
84
|
-
const ctx = mockMethodContext("fetch");
|
|
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;
|
|
90
|
-
|
|
91
|
-
const result = await method();
|
|
92
|
-
expect(result).toBe("ok");
|
|
93
|
-
expect(original).toHaveBeenCalledTimes(1);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("retries and succeeds on a later attempt", async () => {
|
|
97
|
-
let attempt = 0;
|
|
98
|
-
const original = vi.fn(async () => {
|
|
99
|
-
attempt++;
|
|
100
|
-
if (attempt < 3) throw new Error("fail");
|
|
101
|
-
return "success";
|
|
102
|
-
});
|
|
103
|
-
const ctx = mockMethodContext("call");
|
|
104
|
-
Retry(3)(original, ctx as unknown as ClassMethodDecoratorContext);
|
|
105
|
-
|
|
106
|
-
const obj = {};
|
|
107
|
-
ctx.runInitializers(obj);
|
|
108
|
-
const method = (obj as Record<string, () => Promise<unknown>>).call;
|
|
109
|
-
|
|
110
|
-
const result = await method();
|
|
111
|
-
expect(result).toBe("success");
|
|
112
|
-
expect(original).toHaveBeenCalledTimes(3);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("throws after exhausting all attempts", async () => {
|
|
116
|
-
const original = vi.fn(async () => {
|
|
117
|
-
throw new Error("always fails");
|
|
118
|
-
});
|
|
119
|
-
const ctx = mockMethodContext("bad");
|
|
120
|
-
Retry(3)(original, ctx as unknown as ClassMethodDecoratorContext);
|
|
121
|
-
|
|
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");
|
|
127
|
-
expect(original).toHaveBeenCalledTimes(3);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it("applies delay+backoff between retries", async () => {
|
|
131
|
-
vi.useFakeTimers();
|
|
132
|
-
const original = vi.fn(async () => { throw new Error("fail"); });
|
|
133
|
-
const ctx = mockMethodContext("delayed");
|
|
134
|
-
Retry(3, { delay: 100, backoff: 2 })(
|
|
135
|
-
original,
|
|
136
|
-
ctx as unknown as ClassMethodDecoratorContext,
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
const obj = {};
|
|
140
|
-
ctx.runInitializers(obj);
|
|
141
|
-
const method = (obj as Record<string, () => Promise<unknown>>).delayed;
|
|
142
|
-
|
|
143
|
-
const p = method();
|
|
144
|
-
const rejection = expect(p).rejects.toThrow("fail");
|
|
145
|
-
await vi.runAllTimersAsync();
|
|
146
|
-
await rejection;
|
|
147
|
-
expect(original).toHaveBeenCalledTimes(3);
|
|
148
|
-
vi.useRealTimers();
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it("calls onRetry callback for each failed attempt (except last)", async () => {
|
|
152
|
-
const onRetry = vi.fn();
|
|
153
|
-
const original = vi.fn(async () => {
|
|
154
|
-
throw new Error("err");
|
|
155
|
-
});
|
|
156
|
-
const ctx = mockMethodContext("op");
|
|
157
|
-
Retry(3, { onRetry })(original, ctx as unknown as ClassMethodDecoratorContext);
|
|
158
|
-
|
|
159
|
-
const obj = {};
|
|
160
|
-
ctx.runInitializers(obj);
|
|
161
|
-
const method = (obj as Record<string, () => Promise<unknown>>).op;
|
|
162
|
-
|
|
163
|
-
await expect(method()).rejects.toThrow();
|
|
164
|
-
expect(onRetry).toHaveBeenCalledTimes(2); // attempts 1 and 2, not the last
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("maxAttempts=1 fails immediately without retrying", async () => {
|
|
168
|
-
const original = vi.fn(async () => { throw new Error("instant fail"); });
|
|
169
|
-
const ctx = mockMethodContext("once");
|
|
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;
|
|
175
|
-
|
|
176
|
-
await expect(method()).rejects.toThrow("instant fail");
|
|
177
|
-
expect(original).toHaveBeenCalledTimes(1);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("non-Error throw is wrapped in an Error", async () => {
|
|
181
|
-
const original = vi.fn(async () => {
|
|
182
|
-
// eslint-disable-next-line @typescript-eslint/only-throw-error
|
|
183
|
-
throw "string error";
|
|
184
|
-
});
|
|
185
|
-
const ctx = mockMethodContext("strThrow");
|
|
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;
|
|
191
|
-
|
|
192
|
-
await expect(method()).rejects.toThrow("string error");
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it("maxAttempts=0 throws immediately without calling the function", async () => {
|
|
196
|
-
const original = vi.fn(async () => "ok");
|
|
197
|
-
const ctx = mockMethodContext("never");
|
|
198
|
-
Retry(0)(original, ctx as unknown as ClassMethodDecoratorContext);
|
|
199
|
-
|
|
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");
|
|
205
|
-
expect(original).not.toHaveBeenCalled();
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it("onRetry receives the correct attempt number", async () => {
|
|
209
|
-
const attempts: number[] = [];
|
|
210
|
-
const original = vi.fn(async () => { throw new Error("fail"); });
|
|
211
|
-
const ctx = mockMethodContext("numbered");
|
|
212
|
-
Retry(3, {
|
|
213
|
-
onRetry: (_err, attempt) => { attempts.push(attempt); },
|
|
214
|
-
})(original, ctx as unknown as ClassMethodDecoratorContext);
|
|
215
|
-
|
|
216
|
-
const obj = {};
|
|
217
|
-
ctx.runInitializers(obj);
|
|
218
|
-
const method = (obj as Record<string, () => Promise<unknown>>).numbered;
|
|
219
|
-
|
|
220
|
-
await expect(method()).rejects.toThrow();
|
|
221
|
-
expect(attempts).toEqual([1, 2]); // attempt 1 failed → onRetry(1), attempt 2 failed → onRetry(2)
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it("backoff multiplies delay correctly", async () => {
|
|
225
|
-
vi.useFakeTimers();
|
|
226
|
-
const callTimes: number[] = [];
|
|
227
|
-
const original = vi.fn(async () => {
|
|
228
|
-
callTimes.push(Date.now());
|
|
229
|
-
throw new Error("fail");
|
|
230
|
-
});
|
|
231
|
-
const ctx = mockMethodContext("backoffTest");
|
|
232
|
-
Retry(3, { delay: 100, backoff: 2 })(
|
|
233
|
-
original,
|
|
234
|
-
ctx as unknown as ClassMethodDecoratorContext,
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
const obj = {};
|
|
238
|
-
ctx.runInitializers(obj);
|
|
239
|
-
const method = (obj as Record<string, () => Promise<unknown>>).backoffTest;
|
|
240
|
-
|
|
241
|
-
const p = method();
|
|
242
|
-
const rejection = expect(p).rejects.toThrow("fail");
|
|
243
|
-
await vi.runAllTimersAsync();
|
|
244
|
-
await rejection;
|
|
245
|
-
|
|
246
|
-
expect(callTimes.length).toBe(3);
|
|
247
|
-
// Attempt 2 runs 100ms after attempt 1
|
|
248
|
-
expect(callTimes[1] - callTimes[0]).toBeGreaterThanOrEqual(100);
|
|
249
|
-
// Attempt 3 runs 200ms after attempt 2 (backoff × 2)
|
|
250
|
-
expect(callTimes[2] - callTimes[1]).toBeGreaterThanOrEqual(200);
|
|
251
|
-
vi.useRealTimers();
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
// ── Log ──────────────────────────────────────────────────────────────────────
|
|
256
|
-
|
|
257
|
-
describe("Log", () => {
|
|
258
|
-
it("calls the original function and returns its value", () => {
|
|
259
|
-
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
260
|
-
const original = vi.fn(() => 99);
|
|
261
|
-
const ctx = mockMethodContext("myMethod");
|
|
262
|
-
Log({ args: false, result: false })(
|
|
263
|
-
original,
|
|
264
|
-
ctx as unknown as ClassMethodDecoratorContext,
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
class Dummy {}
|
|
268
|
-
const dummy = new Dummy();
|
|
269
|
-
ctx.runInitializers(dummy);
|
|
270
|
-
const method = (dummy as Record<string, () => unknown>).myMethod;
|
|
271
|
-
|
|
272
|
-
const result = method();
|
|
273
|
-
expect(result).toBe(99);
|
|
274
|
-
spy.mockRestore();
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it("logs args when args=true", () => {
|
|
278
|
-
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
279
|
-
const original = vi.fn((x: unknown) => x);
|
|
280
|
-
const ctx = mockMethodContext("greet");
|
|
281
|
-
Log({ args: true, result: false })(
|
|
282
|
-
original,
|
|
283
|
-
ctx as unknown as ClassMethodDecoratorContext,
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
class Foo {}
|
|
287
|
-
const foo = new Foo();
|
|
288
|
-
ctx.runInitializers(foo);
|
|
289
|
-
const method = (foo as Record<string, (...a: unknown[]) => unknown>).greet;
|
|
290
|
-
|
|
291
|
-
method("hello");
|
|
292
|
-
expect(spy).toHaveBeenCalledWith(expect.stringContaining("args:"), ["hello"]);
|
|
293
|
-
spy.mockRestore();
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
it("does not log in production when devOnly=true", () => {
|
|
297
|
-
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
298
|
-
const original = vi.fn(() => "value");
|
|
299
|
-
const ctx = mockMethodContext("prod");
|
|
300
|
-
Log({ devOnly: true })(
|
|
301
|
-
original,
|
|
302
|
-
ctx as unknown as ClassMethodDecoratorContext,
|
|
303
|
-
);
|
|
304
|
-
|
|
305
|
-
const prevEnv = process.env.NODE_ENV;
|
|
306
|
-
process.env.NODE_ENV = "production";
|
|
307
|
-
|
|
308
|
-
class App {}
|
|
309
|
-
const app = new App();
|
|
310
|
-
ctx.runInitializers(app);
|
|
311
|
-
const method = (app as Record<string, () => unknown>).prod;
|
|
312
|
-
|
|
313
|
-
method();
|
|
314
|
-
expect(spy).not.toHaveBeenCalled();
|
|
315
|
-
|
|
316
|
-
process.env.NODE_ENV = prevEnv;
|
|
317
|
-
spy.mockRestore();
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
it("handles async functions — logs resolved value", async () => {
|
|
321
|
-
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
322
|
-
const original = vi.fn(async () => "async-result");
|
|
323
|
-
const ctx = mockMethodContext("load");
|
|
324
|
-
Log({ args: false, result: true })(
|
|
325
|
-
original,
|
|
326
|
-
ctx as unknown as ClassMethodDecoratorContext,
|
|
327
|
-
);
|
|
328
|
-
|
|
329
|
-
class Svc {}
|
|
330
|
-
const svc = new Svc();
|
|
331
|
-
ctx.runInitializers(svc);
|
|
332
|
-
const method = (svc as Record<string, () => Promise<unknown>>).load;
|
|
333
|
-
|
|
334
|
-
await method();
|
|
335
|
-
expect(spy).toHaveBeenCalledWith(
|
|
336
|
-
expect.stringContaining("resolved:"),
|
|
337
|
-
"async-result",
|
|
338
|
-
expect.any(String),
|
|
339
|
-
);
|
|
340
|
-
spy.mockRestore();
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
it("handles async rejection — logs rejected value and re-throws", async () => {
|
|
344
|
-
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
345
|
-
const original = vi.fn(async () => { throw new Error("async-error"); });
|
|
346
|
-
const ctx = mockMethodContext("failing");
|
|
347
|
-
Log({ args: false, result: true })(
|
|
348
|
-
original,
|
|
349
|
-
ctx as unknown as ClassMethodDecoratorContext,
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
class Svc {}
|
|
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");
|
|
358
|
-
expect(spy).toHaveBeenCalledWith(
|
|
359
|
-
expect.stringContaining("rejected:"),
|
|
360
|
-
expect.any(Error),
|
|
361
|
-
);
|
|
362
|
-
spy.mockRestore();
|
|
363
|
-
});
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
// ── Log (additional branches) ─────────────────────────────────────────────────
|
|
367
|
-
|
|
368
|
-
describe("Log — time option", () => {
|
|
369
|
-
it("includes elapsed time in sync result log when time=true", () => {
|
|
370
|
-
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
371
|
-
const original = vi.fn(() => "value");
|
|
372
|
-
const ctx = mockMethodContext("timed");
|
|
373
|
-
Log({ args: false, result: true, time: true })(
|
|
374
|
-
original,
|
|
375
|
-
ctx as unknown as ClassMethodDecoratorContext,
|
|
376
|
-
);
|
|
377
|
-
|
|
378
|
-
class Svc {}
|
|
379
|
-
const svc = new Svc();
|
|
380
|
-
ctx.runInitializers(svc);
|
|
381
|
-
const method = (svc as Record<string, () => unknown>).timed;
|
|
382
|
-
|
|
383
|
-
method();
|
|
384
|
-
// Third argument should be a string containing "ms"
|
|
385
|
-
expect(spy).toHaveBeenCalledWith(
|
|
386
|
-
expect.stringContaining("returned:"),
|
|
387
|
-
"value",
|
|
388
|
-
expect.stringContaining("ms"),
|
|
389
|
-
);
|
|
390
|
-
spy.mockRestore();
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
it("includes elapsed time in async result log when time=true", async () => {
|
|
394
|
-
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
395
|
-
const original = vi.fn(async () => "async-val");
|
|
396
|
-
const ctx = mockMethodContext("asyncTimed");
|
|
397
|
-
Log({ args: false, result: true, time: true })(
|
|
398
|
-
original,
|
|
399
|
-
ctx as unknown as ClassMethodDecoratorContext,
|
|
400
|
-
);
|
|
401
|
-
|
|
402
|
-
class Svc {}
|
|
403
|
-
const svc = new Svc();
|
|
404
|
-
ctx.runInitializers(svc);
|
|
405
|
-
const method = (svc as Record<string, () => Promise<unknown>>).asyncTimed;
|
|
406
|
-
|
|
407
|
-
await method();
|
|
408
|
-
expect(spy).toHaveBeenCalledWith(
|
|
409
|
-
expect.stringContaining("resolved:"),
|
|
410
|
-
"async-val",
|
|
411
|
-
expect.stringContaining("ms"),
|
|
412
|
-
);
|
|
413
|
-
spy.mockRestore();
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
it("skips result log when result=false", () => {
|
|
417
|
-
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
418
|
-
const original = vi.fn(() => 42);
|
|
419
|
-
const ctx = mockMethodContext("noResult");
|
|
420
|
-
Log({ args: false, result: false })(
|
|
421
|
-
original,
|
|
422
|
-
ctx as unknown as ClassMethodDecoratorContext,
|
|
423
|
-
);
|
|
424
|
-
|
|
425
|
-
class Svc {}
|
|
426
|
-
const svc = new Svc();
|
|
427
|
-
ctx.runInitializers(svc);
|
|
428
|
-
const method = (svc as Record<string, () => unknown>).noResult;
|
|
429
|
-
|
|
430
|
-
method();
|
|
431
|
-
// Should not log a "returned:" line
|
|
432
|
-
const calls = spy.mock.calls.flat().join(" ");
|
|
433
|
-
expect(calls).not.toContain("returned:");
|
|
434
|
-
spy.mockRestore();
|
|
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
|
-
});
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
// ── Bind ─────────────────────────────────────────────────────────────────────
|
|
485
|
-
|
|
486
|
-
describe("Bind", () => {
|
|
487
|
-
it("binds method to the instance so 'this' is preserved", () => {
|
|
488
|
-
class Counter {
|
|
489
|
-
value = 10;
|
|
490
|
-
getValue() {
|
|
491
|
-
return this.value;
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
const ctx = mockMethodContext("getValue");
|
|
495
|
-
Bind()(Counter.prototype.getValue, ctx as unknown as ClassMethodDecoratorContext);
|
|
496
|
-
|
|
497
|
-
const c = new Counter();
|
|
498
|
-
ctx.runInitializers(c);
|
|
499
|
-
|
|
500
|
-
const detached = (c as unknown as Record<string, () => number>).getValue;
|
|
501
|
-
expect(detached()).toBe(10);
|
|
502
|
-
});
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
// ── Debounce ─────────────────────────────────────────────────────────────────
|
|
506
|
-
|
|
507
|
-
describe("Debounce", () => {
|
|
508
|
-
it("defers execution until after the wait period", () => {
|
|
509
|
-
vi.useFakeTimers();
|
|
510
|
-
const fn = vi.fn();
|
|
511
|
-
const ctx = mockMethodContext("search");
|
|
512
|
-
Debounce(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
|
|
513
|
-
|
|
514
|
-
const obj = {};
|
|
515
|
-
ctx.runInitializers(obj);
|
|
516
|
-
const method = (obj as Record<string, (...a: unknown[]) => void>).search;
|
|
517
|
-
|
|
518
|
-
method("a");
|
|
519
|
-
method("b");
|
|
520
|
-
method("c");
|
|
521
|
-
expect(fn).not.toHaveBeenCalled();
|
|
522
|
-
|
|
523
|
-
vi.advanceTimersByTime(100);
|
|
524
|
-
expect(fn).toHaveBeenCalledTimes(1);
|
|
525
|
-
expect(fn).toHaveBeenCalledWith("c");
|
|
526
|
-
vi.useRealTimers();
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
it("resets the timer on each call", () => {
|
|
530
|
-
vi.useFakeTimers();
|
|
531
|
-
const fn = vi.fn();
|
|
532
|
-
const ctx = mockMethodContext("onInput");
|
|
533
|
-
Debounce(50)(fn, ctx as unknown as ClassMethodDecoratorContext);
|
|
534
|
-
|
|
535
|
-
const obj = {};
|
|
536
|
-
ctx.runInitializers(obj);
|
|
537
|
-
const method = (obj as Record<string, (...a: unknown[]) => void>).onInput;
|
|
538
|
-
|
|
539
|
-
method("x");
|
|
540
|
-
vi.advanceTimersByTime(30);
|
|
541
|
-
method("y");
|
|
542
|
-
vi.advanceTimersByTime(30); // 30ms since last call — not enough
|
|
543
|
-
expect(fn).not.toHaveBeenCalled();
|
|
544
|
-
|
|
545
|
-
vi.advanceTimersByTime(20); // now 50ms since "y"
|
|
546
|
-
expect(fn).toHaveBeenCalledOnce();
|
|
547
|
-
vi.useRealTimers();
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
it("multiple instances have independent timers", () => {
|
|
551
|
-
vi.useFakeTimers();
|
|
552
|
-
const fn = vi.fn();
|
|
553
|
-
const ctx = mockMethodContext("act");
|
|
554
|
-
Debounce(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
|
|
555
|
-
|
|
556
|
-
const obj1 = {};
|
|
557
|
-
const obj2 = {};
|
|
558
|
-
ctx.runInitializers(obj1);
|
|
559
|
-
ctx.runInitializers(obj2);
|
|
560
|
-
|
|
561
|
-
const m1 = (obj1 as Record<string, (...a: unknown[]) => void>).act;
|
|
562
|
-
const m2 = (obj2 as Record<string, (...a: unknown[]) => void>).act;
|
|
563
|
-
|
|
564
|
-
m1("from-1");
|
|
565
|
-
m2("from-2");
|
|
566
|
-
|
|
567
|
-
vi.advanceTimersByTime(100);
|
|
568
|
-
// Both timers fire independently
|
|
569
|
-
expect(fn).toHaveBeenCalledTimes(2);
|
|
570
|
-
expect(fn).toHaveBeenCalledWith("from-1");
|
|
571
|
-
expect(fn).toHaveBeenCalledWith("from-2");
|
|
572
|
-
vi.useRealTimers();
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
it("delay=0 fires after the next macrotask", () => {
|
|
576
|
-
vi.useFakeTimers();
|
|
577
|
-
const fn = vi.fn();
|
|
578
|
-
const ctx = mockMethodContext("instant");
|
|
579
|
-
Debounce(0)(fn, ctx as unknown as ClassMethodDecoratorContext);
|
|
580
|
-
|
|
581
|
-
const obj = {};
|
|
582
|
-
ctx.runInitializers(obj);
|
|
583
|
-
const method = (obj as Record<string, (...a: unknown[]) => void>).instant;
|
|
584
|
-
|
|
585
|
-
method("a");
|
|
586
|
-
expect(fn).not.toHaveBeenCalled();
|
|
587
|
-
vi.advanceTimersByTime(0);
|
|
588
|
-
expect(fn).toHaveBeenCalledTimes(1);
|
|
589
|
-
expect(fn).toHaveBeenCalledWith("a");
|
|
590
|
-
vi.useRealTimers();
|
|
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("onUnmount with no pending timer is a no-op (if timer !== undefined false branch)", () => {
|
|
617
|
-
vi.useFakeTimers();
|
|
618
|
-
const fn = vi.fn();
|
|
619
|
-
const ctx = mockMethodContext("noTimerOp");
|
|
620
|
-
Debounce(50)(fn, ctx as unknown as ClassMethodDecoratorContext);
|
|
621
|
-
|
|
622
|
-
class BareComp extends StatefulComponent {
|
|
623
|
-
render() { return null; }
|
|
624
|
-
}
|
|
625
|
-
const instance = new BareComp();
|
|
626
|
-
ctx.runInitializers(instance);
|
|
627
|
-
|
|
628
|
-
// Never call the decorated method, so timer is undefined
|
|
629
|
-
// onUnmount with timer === undefined → false branch of if (timer !== undefined)
|
|
630
|
-
expect(() => instance.onUnmount?.()).not.toThrow();
|
|
631
|
-
vi.useRealTimers();
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
it("calls the pre-existing onUnmount when the component already had one", () => {
|
|
635
|
-
vi.useFakeTimers();
|
|
636
|
-
const existingUnmount = vi.fn();
|
|
637
|
-
const fn = vi.fn();
|
|
638
|
-
const ctx = mockMethodContext("debouncedWithPrior");
|
|
639
|
-
|
|
640
|
-
class CompWithUnmount extends StatefulComponent {
|
|
641
|
-
onUnmount() { existingUnmount(); }
|
|
642
|
-
render() { return null; }
|
|
643
|
-
}
|
|
644
|
-
Debounce(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
|
|
645
|
-
|
|
646
|
-
const instance = new CompWithUnmount();
|
|
647
|
-
ctx.runInitializers(instance);
|
|
648
|
-
const method = (instance as unknown as Record<string, (...a: unknown[]) => void>).debouncedWithPrior;
|
|
649
|
-
|
|
650
|
-
method("x");
|
|
651
|
-
instance.onUnmount?.();
|
|
652
|
-
|
|
653
|
-
expect(existingUnmount).toHaveBeenCalledOnce();
|
|
654
|
-
vi.advanceTimersByTime(200);
|
|
655
|
-
expect(fn).not.toHaveBeenCalled(); // timer was cancelled
|
|
656
|
-
vi.useRealTimers();
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
it("original method is NOT called after unmount if timer was pending", () => {
|
|
660
|
-
vi.useFakeTimers();
|
|
661
|
-
const fn = vi.fn();
|
|
662
|
-
const ctx = mockMethodContext("debouncedOp");
|
|
663
|
-
|
|
664
|
-
class TestComp2 extends StatefulComponent {
|
|
665
|
-
render() { return null; }
|
|
666
|
-
}
|
|
667
|
-
Debounce(50)(fn, ctx as unknown as ClassMethodDecoratorContext);
|
|
668
|
-
|
|
669
|
-
const instance = new TestComp2();
|
|
670
|
-
ctx.runInitializers(instance);
|
|
671
|
-
const method = (instance as unknown as Record<string, (...a: unknown[]) => void>).debouncedOp;
|
|
672
|
-
|
|
673
|
-
method("arg1");
|
|
674
|
-
method("arg2");
|
|
675
|
-
instance.onUnmount?.();
|
|
676
|
-
|
|
677
|
-
vi.runAllTimers();
|
|
678
|
-
expect(fn).not.toHaveBeenCalled();
|
|
679
|
-
vi.useRealTimers();
|
|
680
|
-
});
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
// ── Throttle ─────────────────────────────────────────────────────────────────
|
|
684
|
-
|
|
685
|
-
describe("Throttle", () => {
|
|
686
|
-
it("executes the first call immediately", () => {
|
|
687
|
-
vi.useFakeTimers();
|
|
688
|
-
const fn = vi.fn(() => "first");
|
|
689
|
-
const ctx = mockMethodContext("save");
|
|
690
|
-
Throttle(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
|
|
691
|
-
|
|
692
|
-
const obj = {};
|
|
693
|
-
ctx.runInitializers(obj);
|
|
694
|
-
const method = (obj as Record<string, (...a: unknown[]) => void>).save;
|
|
695
|
-
|
|
696
|
-
method();
|
|
697
|
-
expect(fn).toHaveBeenCalledTimes(1);
|
|
698
|
-
vi.useRealTimers();
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
it("ignores calls within the throttle window", () => {
|
|
702
|
-
vi.useFakeTimers();
|
|
703
|
-
const fn = vi.fn();
|
|
704
|
-
const ctx = mockMethodContext("scroll");
|
|
705
|
-
Throttle(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
|
|
706
|
-
|
|
707
|
-
const obj = {};
|
|
708
|
-
ctx.runInitializers(obj);
|
|
709
|
-
const method = (obj as Record<string, (...a: unknown[]) => void>).scroll;
|
|
710
|
-
|
|
711
|
-
method();
|
|
712
|
-
method();
|
|
713
|
-
method();
|
|
714
|
-
expect(fn).toHaveBeenCalledTimes(1);
|
|
715
|
-
vi.useRealTimers();
|
|
716
|
-
});
|
|
717
|
-
|
|
718
|
-
it("allows a new call after the window expires", () => {
|
|
719
|
-
vi.useFakeTimers();
|
|
720
|
-
const fn = vi.fn();
|
|
721
|
-
const ctx = mockMethodContext("resize");
|
|
722
|
-
Throttle(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
|
|
723
|
-
|
|
724
|
-
const obj = {};
|
|
725
|
-
ctx.runInitializers(obj);
|
|
726
|
-
const method = (obj as Record<string, (...a: unknown[]) => void>).resize;
|
|
727
|
-
|
|
728
|
-
method();
|
|
729
|
-
vi.advanceTimersByTime(100);
|
|
730
|
-
method();
|
|
731
|
-
expect(fn).toHaveBeenCalledTimes(2);
|
|
732
|
-
vi.useRealTimers();
|
|
733
|
-
});
|
|
734
|
-
|
|
735
|
-
it("passes the first call's arguments to the underlying function", () => {
|
|
736
|
-
vi.useFakeTimers();
|
|
737
|
-
const fn = vi.fn();
|
|
738
|
-
const ctx = mockMethodContext("click");
|
|
739
|
-
Throttle(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
|
|
740
|
-
|
|
741
|
-
const obj = {};
|
|
742
|
-
ctx.runInitializers(obj);
|
|
743
|
-
const method = (obj as Record<string, (...a: unknown[]) => void>).click;
|
|
744
|
-
|
|
745
|
-
method("first-arg");
|
|
746
|
-
method("second-arg");
|
|
747
|
-
expect(fn).toHaveBeenCalledWith("first-arg");
|
|
748
|
-
vi.useRealTimers();
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
it("multiple instances throttle independently", () => {
|
|
752
|
-
vi.useFakeTimers();
|
|
753
|
-
const fn = vi.fn();
|
|
754
|
-
const ctx = mockMethodContext("ping");
|
|
755
|
-
Throttle(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
|
|
756
|
-
|
|
757
|
-
const obj1 = {};
|
|
758
|
-
const obj2 = {};
|
|
759
|
-
ctx.runInitializers(obj1);
|
|
760
|
-
ctx.runInitializers(obj2);
|
|
761
|
-
|
|
762
|
-
const m1 = (obj1 as Record<string, (...a: unknown[]) => void>).ping;
|
|
763
|
-
const m2 = (obj2 as Record<string, (...a: unknown[]) => void>).ping;
|
|
764
|
-
|
|
765
|
-
m1("a");
|
|
766
|
-
m2("b");
|
|
767
|
-
// Both first calls go through
|
|
768
|
-
expect(fn).toHaveBeenCalledTimes(2);
|
|
769
|
-
vi.useRealTimers();
|
|
770
|
-
});
|
|
771
|
-
|
|
772
|
-
it("negative delay is treated as 0 — first call always executes", () => {
|
|
773
|
-
vi.useFakeTimers();
|
|
774
|
-
const fn = vi.fn();
|
|
775
|
-
const ctx = mockMethodContext("negDelay");
|
|
776
|
-
Throttle(-100)(fn, ctx as unknown as ClassMethodDecoratorContext);
|
|
777
|
-
|
|
778
|
-
const obj = {};
|
|
779
|
-
ctx.runInitializers(obj);
|
|
780
|
-
const method = (obj as Record<string, (...a: unknown[]) => void>).negDelay;
|
|
781
|
-
|
|
782
|
-
method();
|
|
783
|
-
expect(fn).toHaveBeenCalledTimes(1);
|
|
784
|
-
vi.useRealTimers();
|
|
785
|
-
});
|
|
786
|
-
|
|
787
|
-
it("call at exactly the ms boundary executes again", () => {
|
|
788
|
-
vi.useFakeTimers();
|
|
789
|
-
const fn = vi.fn();
|
|
790
|
-
const ctx = mockMethodContext("boundary");
|
|
791
|
-
Throttle(100)(fn, ctx as unknown as ClassMethodDecoratorContext);
|
|
792
|
-
|
|
793
|
-
const obj = {};
|
|
794
|
-
ctx.runInitializers(obj);
|
|
795
|
-
const method = (obj as Record<string, (...a: unknown[]) => void>).boundary;
|
|
796
|
-
|
|
797
|
-
method(); // first call at t=0
|
|
798
|
-
vi.advanceTimersByTime(100); // advance to exactly 100ms
|
|
799
|
-
method(); // second call at t=100 — should pass (now - last === ms, not less than ms)
|
|
800
|
-
expect(fn).toHaveBeenCalledTimes(2);
|
|
801
|
-
vi.useRealTimers();
|
|
802
|
-
});
|
|
803
|
-
});
|