@praxisjs/decorators 0.4.2 → 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.
- package/CHANGELOG.md +18 -0
- package/dist/__tests__/component.test.d.ts +2 -0
- package/dist/__tests__/component.test.d.ts.map +1 -0
- package/dist/__tests__/component.test.js +137 -0
- package/dist/__tests__/component.test.js.map +1 -0
- package/dist/__tests__/events.test.d.ts +2 -0
- package/dist/__tests__/events.test.d.ts.map +1 -0
- package/dist/__tests__/events.test.js +190 -0
- package/dist/__tests__/events.test.js.map +1 -0
- package/dist/__tests__/functions.test.js +157 -0
- package/dist/__tests__/functions.test.js.map +1 -1
- package/dist/__tests__/lazy.test.d.ts +2 -0
- package/dist/__tests__/lazy.test.d.ts.map +1 -0
- package/dist/__tests__/lazy.test.js +165 -0
- package/dist/__tests__/lazy.test.js.map +1 -0
- package/dist/__tests__/memo.test.js +13 -0
- package/dist/__tests__/memo.test.js.map +1 -1
- package/dist/__tests__/properties.test.js +191 -1
- package/dist/__tests__/properties.test.js.map +1 -1
- package/dist/__tests__/slot.test.d.ts +2 -0
- package/dist/__tests__/slot.test.d.ts.map +1 -0
- package/dist/__tests__/slot.test.js +115 -0
- package/dist/__tests__/slot.test.js.map +1 -0
- package/dist/__tests__/virtual.test.d.ts +2 -0
- package/dist/__tests__/virtual.test.d.ts.map +1 -0
- package/dist/__tests__/virtual.test.js +201 -0
- package/dist/__tests__/virtual.test.js.map +1 -0
- package/dist/component/lazy.d.ts +1 -1
- package/dist/component/lazy.d.ts.map +1 -1
- package/dist/component/lazy.js +4 -1
- package/dist/component/lazy.js.map +1 -1
- package/dist/component/virtual.d.ts +1 -1
- package/dist/component/virtual.d.ts.map +1 -1
- package/dist/functions/watch.d.ts.map +1 -1
- package/dist/functions/watch.js +10 -2
- package/dist/functions/watch.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/component.test.ts +156 -0
- package/src/__tests__/events.test.ts +236 -0
- package/src/__tests__/functions.test.ts +207 -0
- package/src/__tests__/lazy.test.ts +220 -0
- package/src/__tests__/memo.test.ts +16 -0
- package/src/__tests__/properties.test.ts +249 -1
- package/src/__tests__/slot.test.ts +141 -0
- package/src/__tests__/virtual.test.ts +241 -0
- package/src/component/lazy.ts +4 -2
- package/src/component/virtual.tsx +1 -1
- package/src/functions/watch.ts +11 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @praxisjs/decorators
|
|
2
2
|
|
|
3
|
+
## 0.4.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ea59035: Fix three bugs in `Lazy`, `Virtual`, and `Watch` decorators
|
|
8
|
+
|
|
9
|
+
**`Lazy` / `Virtual` — incompatible type constraint**
|
|
10
|
+
Both decorators constrained their generic to `new (...args: any[]) => RootComponent`, where the bare `RootComponent` defaults to `RootComponent<Record<string, never>>`. This made the constraint incompatible with `StatefulComponent`, whose `_rawProps` is typed as `Record<string, unknown>`, causing a TypeScript error at the call site. Changed the constraint to `RootComponent<Record<string, any>>` so any component subclass is accepted.
|
|
11
|
+
|
|
12
|
+
**`Lazy` — infinite recursion on render after becoming visible**
|
|
13
|
+
`_originalRender` was initialized to `this.render.bind(this)`, which at instance-creation time resolves to `LazyWrapper.render` (the override itself). Calling `render()` after the component became visible would then recurse infinitely. Fixed by capturing `constructor.prototype.render` — the parent class's render method — instead.
|
|
14
|
+
|
|
15
|
+
**`Watch` — reactive effect leaked after component unmount**
|
|
16
|
+
The decorator created a reactive `effect()` on `onMount` but never called the returned stop function on unmount. This caused the effect to keep running and the handler to keep firing even after the component was unmounted, resulting in a memory leak and stale callbacks. The decorator now hooks into `onUnmount` to stop the effect and preserves any existing `onUnmount` implementation on the instance.
|
|
17
|
+
|
|
18
|
+
- Updated dependencies [d11a10a]
|
|
19
|
+
- @praxisjs/core@0.4.2
|
|
20
|
+
|
|
3
21
|
## 0.4.2
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/component.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { StatefulComponent, StatelessComponent } from "@praxisjs/core";
|
|
3
|
+
import { Component } from "../component/component";
|
|
4
|
+
// ── Component decorator ───────────────────────────────────────────────────────
|
|
5
|
+
describe("Component decorator", () => {
|
|
6
|
+
it("sets __isComponent = true on the enhanced class", () => {
|
|
7
|
+
class MyComp extends StatefulComponent {
|
|
8
|
+
render() { return null; }
|
|
9
|
+
}
|
|
10
|
+
const Enhanced = Component()(MyComp, {});
|
|
11
|
+
expect(Enhanced.__isComponent).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
it("preserves the original class name", () => {
|
|
14
|
+
class MyButton extends StatefulComponent {
|
|
15
|
+
render() { return null; }
|
|
16
|
+
}
|
|
17
|
+
const Enhanced = Component()(MyButton, {});
|
|
18
|
+
expect(Enhanced.name).toBe("MyButton");
|
|
19
|
+
});
|
|
20
|
+
it("marks __isStateless = false for StatefulComponent subclasses", () => {
|
|
21
|
+
class MyStateful extends StatefulComponent {
|
|
22
|
+
render() { return null; }
|
|
23
|
+
}
|
|
24
|
+
const Enhanced = Component()(MyStateful, {});
|
|
25
|
+
expect(Enhanced.__isStateless).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
it("marks __isStateless = true for StatelessComponent subclasses", () => {
|
|
28
|
+
class MyStateless extends StatelessComponent {
|
|
29
|
+
render() { return null; }
|
|
30
|
+
}
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
const Enhanced = Component()(MyStateless, {});
|
|
33
|
+
expect(Enhanced.__isStateless).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
it("enhanced class can still be instantiated", () => {
|
|
36
|
+
class Counter extends StatefulComponent {
|
|
37
|
+
render() { return null; }
|
|
38
|
+
}
|
|
39
|
+
const Enhanced = Component()(Counter, {});
|
|
40
|
+
const instance = new Enhanced();
|
|
41
|
+
expect(instance).toBeInstanceOf(Counter);
|
|
42
|
+
});
|
|
43
|
+
it("returns a subclass of the original constructor", () => {
|
|
44
|
+
class Base extends StatefulComponent {
|
|
45
|
+
render() { return null; }
|
|
46
|
+
}
|
|
47
|
+
const Enhanced = Component()(Base, {});
|
|
48
|
+
expect(Enhanced.prototype).toBeInstanceOf(Base);
|
|
49
|
+
});
|
|
50
|
+
// ── Happy paths ──────────────────────────────────────────────────────────────
|
|
51
|
+
it("forwards constructor args — props reach the base constructor", () => {
|
|
52
|
+
class WithProps extends StatefulComponent {
|
|
53
|
+
render() { return null; }
|
|
54
|
+
}
|
|
55
|
+
const Enhanced = Component()(WithProps, {});
|
|
56
|
+
const instance = new Enhanced({ greeting: "hello" });
|
|
57
|
+
expect(instance.props.greeting).toBe("hello");
|
|
58
|
+
});
|
|
59
|
+
it("instance methods from the original class remain accessible", () => {
|
|
60
|
+
class Greeter extends StatefulComponent {
|
|
61
|
+
greet() { return "hello"; }
|
|
62
|
+
render() { return null; }
|
|
63
|
+
}
|
|
64
|
+
const Enhanced = Component()(Greeter, {});
|
|
65
|
+
const instance = new Enhanced();
|
|
66
|
+
expect(instance.greet()).toBe("hello");
|
|
67
|
+
});
|
|
68
|
+
it("static properties of the original class are preserved on Enhanced", () => {
|
|
69
|
+
class WithStatic extends StatefulComponent {
|
|
70
|
+
static version = "1.0";
|
|
71
|
+
render() { return null; }
|
|
72
|
+
}
|
|
73
|
+
const Enhanced = Component()(WithStatic, {});
|
|
74
|
+
expect(Enhanced.version).toBe("1.0");
|
|
75
|
+
});
|
|
76
|
+
// ── Edge cases ───────────────────────────────────────────────────────────────
|
|
77
|
+
it("applying Component() twice preserves name and flags", () => {
|
|
78
|
+
class MyComp extends StatefulComponent {
|
|
79
|
+
render() { return null; }
|
|
80
|
+
}
|
|
81
|
+
const E1 = Component()(MyComp, {});
|
|
82
|
+
const E2 = Component()(E1, {});
|
|
83
|
+
expect(E2.__isComponent).toBe(true);
|
|
84
|
+
expect(E2.__isStateless).toBe(false);
|
|
85
|
+
expect(E2.name).toBe("MyComp"); // name survives double-wrap
|
|
86
|
+
});
|
|
87
|
+
it("deep StatelessComponent inheritance — __isStateless is still true", () => {
|
|
88
|
+
class BaseStateless extends StatelessComponent {
|
|
89
|
+
render() { return null; }
|
|
90
|
+
}
|
|
91
|
+
class DeepChild extends BaseStateless {
|
|
92
|
+
render() { return null; }
|
|
93
|
+
}
|
|
94
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
95
|
+
const Enhanced = Component()(DeepChild, {});
|
|
96
|
+
expect(Enhanced.__isStateless).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
it("anonymous class gets an empty name", () => {
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
100
|
+
const Enhanced = Component()(
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
102
|
+
class extends StatefulComponent {
|
|
103
|
+
render() { return null; }
|
|
104
|
+
}, {});
|
|
105
|
+
expect(Enhanced.name).toBe("");
|
|
106
|
+
});
|
|
107
|
+
it("__isComponent and __isStateless are static — not own properties of instances", () => {
|
|
108
|
+
class MyComp extends StatefulComponent {
|
|
109
|
+
render() { return null; }
|
|
110
|
+
}
|
|
111
|
+
const Enhanced = Component()(MyComp, {});
|
|
112
|
+
const instance = new Enhanced();
|
|
113
|
+
// Static fields live on the class, not on instances
|
|
114
|
+
expect(Object.prototype.hasOwnProperty.call(instance, "__isComponent")).toBe(false);
|
|
115
|
+
expect(Object.prototype.hasOwnProperty.call(instance, "__isStateless")).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
it("subclassing Enhanced inherits __isComponent and __isStateless", () => {
|
|
118
|
+
class MyComp extends StatefulComponent {
|
|
119
|
+
render() { return null; }
|
|
120
|
+
}
|
|
121
|
+
const Enhanced = Component()(MyComp, {});
|
|
122
|
+
// `class Sub extends Enhanced` sets Object.getPrototypeOf(Sub) === Enhanced,
|
|
123
|
+
// making static fields visible through the chain. Simulate that here to
|
|
124
|
+
// avoid TS2510 (conflicting constructor return types in the intersection type).
|
|
125
|
+
const Sub = Object.create(Enhanced);
|
|
126
|
+
expect(Sub.__isComponent).toBe(true);
|
|
127
|
+
expect(Sub.__isStateless).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
it("Enhanced is a different reference from the original constructor", () => {
|
|
130
|
+
class MyComp extends StatefulComponent {
|
|
131
|
+
render() { return null; }
|
|
132
|
+
}
|
|
133
|
+
const Enhanced = Component()(MyComp, {});
|
|
134
|
+
expect(Enhanced).not.toBe(MyComp);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
//# sourceMappingURL=component.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component.test.js","sourceRoot":"","sources":["../../src/__tests__/component.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/events.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { StatefulComponent } from "@praxisjs/core";
|
|
3
|
+
import { createCommand } from "../events/command";
|
|
4
|
+
import { Emit } from "../events/emit";
|
|
5
|
+
import { readProp } from "../events/helper";
|
|
6
|
+
import { OnCommand } from "../events/on-command";
|
|
7
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
8
|
+
class TestComponent extends StatefulComponent {
|
|
9
|
+
render() { return null; }
|
|
10
|
+
}
|
|
11
|
+
function methodCtx(name) {
|
|
12
|
+
const initializers = [];
|
|
13
|
+
return {
|
|
14
|
+
ctx: {
|
|
15
|
+
name,
|
|
16
|
+
kind: "method",
|
|
17
|
+
addInitializer(fn) { initializers.push(fn); },
|
|
18
|
+
},
|
|
19
|
+
run(instance) { initializers.forEach((fn) => { fn.call(instance); }); },
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
// ── createCommand ─────────────────────────────────────────────────────────────
|
|
23
|
+
describe("createCommand", () => {
|
|
24
|
+
it("trigger calls all subscribed handlers with the argument", () => {
|
|
25
|
+
const cmd = createCommand();
|
|
26
|
+
const h1 = vi.fn();
|
|
27
|
+
const h2 = vi.fn();
|
|
28
|
+
cmd.subscribe(h1);
|
|
29
|
+
cmd.subscribe(h2);
|
|
30
|
+
cmd.trigger(42);
|
|
31
|
+
expect(h1).toHaveBeenCalledWith(42);
|
|
32
|
+
expect(h2).toHaveBeenCalledWith(42);
|
|
33
|
+
});
|
|
34
|
+
it("trigger does nothing when no handlers are subscribed", () => {
|
|
35
|
+
const cmd = createCommand();
|
|
36
|
+
expect(() => cmd.trigger("hello")).not.toThrow();
|
|
37
|
+
});
|
|
38
|
+
it("subscribe returns an unsubscribe function", () => {
|
|
39
|
+
const cmd = createCommand();
|
|
40
|
+
const handler = vi.fn();
|
|
41
|
+
const unsub = cmd.subscribe(handler);
|
|
42
|
+
unsub();
|
|
43
|
+
cmd.trigger();
|
|
44
|
+
expect(handler).not.toHaveBeenCalled();
|
|
45
|
+
});
|
|
46
|
+
it("void command can be triggered without args", () => {
|
|
47
|
+
const cmd = createCommand();
|
|
48
|
+
const handler = vi.fn();
|
|
49
|
+
cmd.subscribe(handler);
|
|
50
|
+
cmd.trigger(undefined);
|
|
51
|
+
expect(handler).toHaveBeenCalledOnce();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
// ── readProp ──────────────────────────────────────────────────────────────────
|
|
55
|
+
describe("readProp", () => {
|
|
56
|
+
it("returns value from _rawProps when defined", () => {
|
|
57
|
+
const instance = new TestComponent({ myProp: "fromParent" });
|
|
58
|
+
expect(readProp(instance, "myProp")).toBe("fromParent");
|
|
59
|
+
});
|
|
60
|
+
it("falls back to _defaults when _rawProps does not define the prop", () => {
|
|
61
|
+
const instance = new TestComponent();
|
|
62
|
+
instance._defaults.myProp = "default";
|
|
63
|
+
expect(readProp(instance, "myProp")).toBe("default");
|
|
64
|
+
});
|
|
65
|
+
it("returns undefined when prop is not in either", () => {
|
|
66
|
+
const instance = new TestComponent();
|
|
67
|
+
expect(readProp(instance, "missing")).toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
// ── Emit ─────────────────────────────────────────────────────────────────────
|
|
71
|
+
describe("Emit decorator", () => {
|
|
72
|
+
it("calls the prop callback with the return value of the method", () => {
|
|
73
|
+
const { ctx, run } = methodCtx("handleInput");
|
|
74
|
+
const original = function (value) { return value.toUpperCase(); };
|
|
75
|
+
Emit("onChange")(original, ctx);
|
|
76
|
+
const callback = vi.fn();
|
|
77
|
+
const instance = new TestComponent({ onChange: callback });
|
|
78
|
+
run(instance);
|
|
79
|
+
const method = instance.handleInput;
|
|
80
|
+
method.call(instance, "hello");
|
|
81
|
+
expect(callback).toHaveBeenCalledWith("HELLO");
|
|
82
|
+
});
|
|
83
|
+
it("calls the prop callback with the method args when return value is undefined", () => {
|
|
84
|
+
const { ctx, run } = methodCtx("doSomething");
|
|
85
|
+
const original = function (a, b) { return undefined; };
|
|
86
|
+
Emit("onDo")(original, ctx);
|
|
87
|
+
const callback = vi.fn();
|
|
88
|
+
const instance = new TestComponent({ onDo: callback });
|
|
89
|
+
run(instance);
|
|
90
|
+
const method = instance.doSomething;
|
|
91
|
+
method.call(instance, 1, 2);
|
|
92
|
+
expect(callback).toHaveBeenCalledWith(1, 2);
|
|
93
|
+
});
|
|
94
|
+
it("calls the prop callback with no args when return is undefined and no args", () => {
|
|
95
|
+
const { ctx, run } = methodCtx("fire");
|
|
96
|
+
const original = function () { return undefined; };
|
|
97
|
+
Emit("onFire")(original, ctx);
|
|
98
|
+
const callback = vi.fn();
|
|
99
|
+
const instance = new TestComponent({ onFire: callback });
|
|
100
|
+
run(instance);
|
|
101
|
+
const method = instance.fire;
|
|
102
|
+
method.call(instance);
|
|
103
|
+
expect(callback).toHaveBeenCalledOnce();
|
|
104
|
+
expect(callback).toHaveBeenCalledWith();
|
|
105
|
+
});
|
|
106
|
+
it("does not throw when the prop callback is not a function", () => {
|
|
107
|
+
const { ctx, run } = methodCtx("submit");
|
|
108
|
+
const original = function () { return "result"; };
|
|
109
|
+
Emit("onSubmit")(original, ctx);
|
|
110
|
+
const instance = new TestComponent(); // no onSubmit prop
|
|
111
|
+
run(instance);
|
|
112
|
+
const method = instance.submit;
|
|
113
|
+
expect(() => method.call(instance)).not.toThrow();
|
|
114
|
+
});
|
|
115
|
+
it("still returns the original method's return value", () => {
|
|
116
|
+
const { ctx, run } = methodCtx("compute");
|
|
117
|
+
const original = function () { return 99; };
|
|
118
|
+
Emit("onResult")(original, ctx);
|
|
119
|
+
const callback = vi.fn();
|
|
120
|
+
const instance = new TestComponent({ onResult: callback });
|
|
121
|
+
run(instance);
|
|
122
|
+
const method = instance.compute;
|
|
123
|
+
const result = method.call(instance);
|
|
124
|
+
expect(result).toBe(99);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
// ── OnCommand ─────────────────────────────────────────────────────────────────
|
|
128
|
+
describe("OnCommand decorator", () => {
|
|
129
|
+
it("subscribes to the command on mount and calls the method when triggered", () => {
|
|
130
|
+
const { ctx, run } = methodCtx("handleCmd");
|
|
131
|
+
const handler = vi.fn();
|
|
132
|
+
OnCommand("myCommand")(handler, ctx);
|
|
133
|
+
const cmd = createCommand();
|
|
134
|
+
const instance = new TestComponent({ myCommand: cmd });
|
|
135
|
+
run(instance);
|
|
136
|
+
instance.onMount?.();
|
|
137
|
+
cmd.trigger("payload");
|
|
138
|
+
expect(handler).toHaveBeenCalledWith("payload");
|
|
139
|
+
});
|
|
140
|
+
it("unsubscribes from the command on unmount", () => {
|
|
141
|
+
const { ctx, run } = methodCtx("onMsg");
|
|
142
|
+
const handler = vi.fn();
|
|
143
|
+
OnCommand("msg")(handler, ctx);
|
|
144
|
+
const cmd = createCommand();
|
|
145
|
+
const instance = new TestComponent({ msg: cmd });
|
|
146
|
+
run(instance);
|
|
147
|
+
instance.onMount?.();
|
|
148
|
+
instance.onUnmount?.();
|
|
149
|
+
cmd.trigger("after-unmount");
|
|
150
|
+
expect(handler).not.toHaveBeenCalled();
|
|
151
|
+
});
|
|
152
|
+
it("warns when the command prop is not provided", () => {
|
|
153
|
+
const { ctx, run } = methodCtx("onMissing");
|
|
154
|
+
OnCommand("missingProp")(vi.fn(), ctx);
|
|
155
|
+
const instance = new TestComponent(); // no missingProp
|
|
156
|
+
run(instance);
|
|
157
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
158
|
+
instance.onMount?.();
|
|
159
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("[OnCommand]"));
|
|
160
|
+
warn.mockRestore();
|
|
161
|
+
});
|
|
162
|
+
it("warns when the command prop is not a valid Command", () => {
|
|
163
|
+
const { ctx, run } = methodCtx("onBadCmd");
|
|
164
|
+
OnCommand("badProp")(vi.fn(), ctx);
|
|
165
|
+
const instance = new TestComponent({ badProp: "not-a-command" });
|
|
166
|
+
run(instance);
|
|
167
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
168
|
+
instance.onMount?.();
|
|
169
|
+
expect(warn).toHaveBeenCalledWith(expect.stringContaining("[OnCommand]"));
|
|
170
|
+
warn.mockRestore();
|
|
171
|
+
});
|
|
172
|
+
it("preserves the original onMount and onUnmount implementations", () => {
|
|
173
|
+
const { ctx, run } = methodCtx("onEvt");
|
|
174
|
+
OnCommand("evt")(vi.fn(), ctx);
|
|
175
|
+
const cmd = createCommand();
|
|
176
|
+
const instance = new TestComponent({ evt: cmd });
|
|
177
|
+
run(instance);
|
|
178
|
+
const mountSpy = vi.fn();
|
|
179
|
+
const unmountSpy = vi.fn();
|
|
180
|
+
const prevMount = instance.onMount?.bind(instance);
|
|
181
|
+
const prevUnmount = instance.onUnmount?.bind(instance);
|
|
182
|
+
instance.onMount = function () { mountSpy(); prevMount?.(); };
|
|
183
|
+
instance.onUnmount = function () { unmountSpy(); prevUnmount?.(); };
|
|
184
|
+
instance.onMount();
|
|
185
|
+
instance.onUnmount();
|
|
186
|
+
expect(mountSpy).toHaveBeenCalled();
|
|
187
|
+
expect(unmountSpy).toHaveBeenCalled();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
//# sourceMappingURL=events.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.test.js","sourceRoot":"","sources":["../../src/__tests__/events.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -108,6 +108,59 @@ describe("Retry", () => {
|
|
|
108
108
|
await expect(wrapped.call({})).rejects.toThrow();
|
|
109
109
|
expect(onRetry).toHaveBeenCalledTimes(2); // attempts 1 and 2, not the last
|
|
110
110
|
});
|
|
111
|
+
it("maxAttempts=1 fails immediately without retrying", async () => {
|
|
112
|
+
const original = vi.fn(async () => { throw new Error("instant fail"); });
|
|
113
|
+
const ctx = mockMethodContext("once");
|
|
114
|
+
const wrapped = Retry(1)(original, ctx);
|
|
115
|
+
await expect(wrapped.call({})).rejects.toThrow("instant fail");
|
|
116
|
+
expect(original).toHaveBeenCalledTimes(1);
|
|
117
|
+
});
|
|
118
|
+
it("non-Error throw is wrapped in an Error", async () => {
|
|
119
|
+
const original = vi.fn(async () => {
|
|
120
|
+
// eslint-disable-next-line @typescript-eslint/only-throw-error
|
|
121
|
+
throw "string error";
|
|
122
|
+
});
|
|
123
|
+
const ctx = mockMethodContext("strThrow");
|
|
124
|
+
const wrapped = Retry(1)(original, ctx);
|
|
125
|
+
await expect(wrapped.call({})).rejects.toThrow("string error");
|
|
126
|
+
});
|
|
127
|
+
it("maxAttempts=0 throws immediately without calling the function", async () => {
|
|
128
|
+
const original = vi.fn(async () => "ok");
|
|
129
|
+
const ctx = mockMethodContext("never");
|
|
130
|
+
const wrapped = Retry(0)(original, ctx);
|
|
131
|
+
await expect(wrapped.call({})).rejects.toThrow("Unknown error");
|
|
132
|
+
expect(original).not.toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
it("onRetry receives the correct attempt number", async () => {
|
|
135
|
+
const attempts = [];
|
|
136
|
+
const original = vi.fn(async () => { throw new Error("fail"); });
|
|
137
|
+
const ctx = mockMethodContext("numbered");
|
|
138
|
+
const wrapped = Retry(3, {
|
|
139
|
+
onRetry: (_err, attempt) => { attempts.push(attempt); },
|
|
140
|
+
})(original, ctx);
|
|
141
|
+
await expect(wrapped.call({})).rejects.toThrow();
|
|
142
|
+
expect(attempts).toEqual([1, 2]); // attempt 1 failed → onRetry(1), attempt 2 failed → onRetry(2)
|
|
143
|
+
});
|
|
144
|
+
it("backoff multiplies delay correctly", async () => {
|
|
145
|
+
vi.useFakeTimers();
|
|
146
|
+
const callTimes = [];
|
|
147
|
+
const original = vi.fn(async () => {
|
|
148
|
+
callTimes.push(Date.now());
|
|
149
|
+
throw new Error("fail");
|
|
150
|
+
});
|
|
151
|
+
const ctx = mockMethodContext("backoffTest");
|
|
152
|
+
const wrapped = Retry(3, { delay: 100, backoff: 2 })(original, ctx);
|
|
153
|
+
const p = wrapped.call({});
|
|
154
|
+
const rejection = expect(p).rejects.toThrow("fail");
|
|
155
|
+
await vi.runAllTimersAsync();
|
|
156
|
+
await rejection;
|
|
157
|
+
expect(callTimes.length).toBe(3);
|
|
158
|
+
// Attempt 2 runs 100ms after attempt 1
|
|
159
|
+
expect(callTimes[1] - callTimes[0]).toBeGreaterThanOrEqual(100);
|
|
160
|
+
// Attempt 3 runs 200ms after attempt 2 (backoff × 2)
|
|
161
|
+
expect(callTimes[2] - callTimes[1]).toBeGreaterThanOrEqual(200);
|
|
162
|
+
vi.useRealTimers();
|
|
163
|
+
});
|
|
111
164
|
});
|
|
112
165
|
// ── Log ──────────────────────────────────────────────────────────────────────
|
|
113
166
|
describe("Log", () => {
|
|
@@ -170,6 +223,45 @@ describe("Log", () => {
|
|
|
170
223
|
spy.mockRestore();
|
|
171
224
|
});
|
|
172
225
|
});
|
|
226
|
+
// ── Log (additional branches) ─────────────────────────────────────────────────
|
|
227
|
+
describe("Log — time option", () => {
|
|
228
|
+
it("includes elapsed time in sync result log when time=true", () => {
|
|
229
|
+
const spy = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
230
|
+
const original = vi.fn(() => "value");
|
|
231
|
+
const ctx = mockMethodContext("timed");
|
|
232
|
+
const wrapped = Log({ args: false, result: true, time: true })(original, ctx);
|
|
233
|
+
class Svc {
|
|
234
|
+
}
|
|
235
|
+
wrapped.call(new Svc());
|
|
236
|
+
// Third argument should be a string containing "ms"
|
|
237
|
+
expect(spy).toHaveBeenCalledWith(expect.stringContaining("returned:"), "value", expect.stringContaining("ms"));
|
|
238
|
+
spy.mockRestore();
|
|
239
|
+
});
|
|
240
|
+
it("includes elapsed time in async result log when time=true", async () => {
|
|
241
|
+
const spy = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
242
|
+
const original = vi.fn(async () => "async-val");
|
|
243
|
+
const ctx = mockMethodContext("asyncTimed");
|
|
244
|
+
const wrapped = Log({ args: false, result: true, time: true })(original, ctx);
|
|
245
|
+
class Svc {
|
|
246
|
+
}
|
|
247
|
+
await wrapped.call(new Svc());
|
|
248
|
+
expect(spy).toHaveBeenCalledWith(expect.stringContaining("resolved:"), "async-val", expect.stringContaining("ms"));
|
|
249
|
+
spy.mockRestore();
|
|
250
|
+
});
|
|
251
|
+
it("skips result log when result=false", () => {
|
|
252
|
+
const spy = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
253
|
+
const original = vi.fn(() => 42);
|
|
254
|
+
const ctx = mockMethodContext("noResult");
|
|
255
|
+
const wrapped = Log({ args: false, result: false })(original, ctx);
|
|
256
|
+
class Svc {
|
|
257
|
+
}
|
|
258
|
+
wrapped.call(new Svc());
|
|
259
|
+
// Should not log a "returned:" line
|
|
260
|
+
const calls = spy.mock.calls.flat().join(" ");
|
|
261
|
+
expect(calls).not.toContain("returned:");
|
|
262
|
+
spy.mockRestore();
|
|
263
|
+
});
|
|
264
|
+
});
|
|
173
265
|
// ── Bind ─────────────────────────────────────────────────────────────────────
|
|
174
266
|
describe("Bind", () => {
|
|
175
267
|
it("binds method to the instance so 'this' is preserved", () => {
|
|
@@ -223,6 +315,41 @@ describe("Debounce", () => {
|
|
|
223
315
|
expect(fn).toHaveBeenCalledOnce();
|
|
224
316
|
vi.useRealTimers();
|
|
225
317
|
});
|
|
318
|
+
it("multiple instances have independent timers", () => {
|
|
319
|
+
vi.useFakeTimers();
|
|
320
|
+
const fn = vi.fn();
|
|
321
|
+
const ctx = mockMethodContext("act");
|
|
322
|
+
Debounce(100)(fn, ctx);
|
|
323
|
+
const obj1 = {};
|
|
324
|
+
const obj2 = {};
|
|
325
|
+
ctx.runInitializers(obj1);
|
|
326
|
+
ctx.runInitializers(obj2);
|
|
327
|
+
const m1 = obj1.act;
|
|
328
|
+
const m2 = obj2.act;
|
|
329
|
+
m1("from-1");
|
|
330
|
+
m2("from-2");
|
|
331
|
+
vi.advanceTimersByTime(100);
|
|
332
|
+
// Both timers fire independently
|
|
333
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
334
|
+
expect(fn).toHaveBeenCalledWith("from-1");
|
|
335
|
+
expect(fn).toHaveBeenCalledWith("from-2");
|
|
336
|
+
vi.useRealTimers();
|
|
337
|
+
});
|
|
338
|
+
it("delay=0 fires after the next macrotask", () => {
|
|
339
|
+
vi.useFakeTimers();
|
|
340
|
+
const fn = vi.fn();
|
|
341
|
+
const ctx = mockMethodContext("instant");
|
|
342
|
+
Debounce(0)(fn, ctx);
|
|
343
|
+
const obj = {};
|
|
344
|
+
ctx.runInitializers(obj);
|
|
345
|
+
const method = obj.instant;
|
|
346
|
+
method("a");
|
|
347
|
+
expect(fn).not.toHaveBeenCalled();
|
|
348
|
+
vi.advanceTimersByTime(0);
|
|
349
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
350
|
+
expect(fn).toHaveBeenCalledWith("a");
|
|
351
|
+
vi.useRealTimers();
|
|
352
|
+
});
|
|
226
353
|
});
|
|
227
354
|
// ── Throttle ─────────────────────────────────────────────────────────────────
|
|
228
355
|
describe("Throttle", () => {
|
|
@@ -266,5 +393,35 @@ describe("Throttle", () => {
|
|
|
266
393
|
expect(fn).toHaveBeenCalledTimes(2);
|
|
267
394
|
vi.useRealTimers();
|
|
268
395
|
});
|
|
396
|
+
it("passes the first call's arguments to the underlying function", () => {
|
|
397
|
+
vi.useFakeTimers();
|
|
398
|
+
const fn = vi.fn();
|
|
399
|
+
const ctx = mockMethodContext("click");
|
|
400
|
+
Throttle(100)(fn, ctx);
|
|
401
|
+
const obj = {};
|
|
402
|
+
ctx.runInitializers(obj);
|
|
403
|
+
const method = obj.click;
|
|
404
|
+
method("first-arg");
|
|
405
|
+
method("second-arg");
|
|
406
|
+
expect(fn).toHaveBeenCalledWith("first-arg");
|
|
407
|
+
vi.useRealTimers();
|
|
408
|
+
});
|
|
409
|
+
it("multiple instances throttle independently", () => {
|
|
410
|
+
vi.useFakeTimers();
|
|
411
|
+
const fn = vi.fn();
|
|
412
|
+
const ctx = mockMethodContext("ping");
|
|
413
|
+
Throttle(100)(fn, ctx);
|
|
414
|
+
const obj1 = {};
|
|
415
|
+
const obj2 = {};
|
|
416
|
+
ctx.runInitializers(obj1);
|
|
417
|
+
ctx.runInitializers(obj2);
|
|
418
|
+
const m1 = obj1.ping;
|
|
419
|
+
const m2 = obj2.ping;
|
|
420
|
+
m1("a");
|
|
421
|
+
m2("b");
|
|
422
|
+
// Both first calls go through
|
|
423
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
424
|
+
vi.useRealTimers();
|
|
425
|
+
});
|
|
269
426
|
});
|
|
270
427
|
//# sourceMappingURL=functions.test.js.map
|