@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,1253 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { StatefulComponent } from "@praxisjs/core";
|
|
3
|
-
import { computed, isStateDirty, setStateDirty, signal, } from "@praxisjs/core/internal";
|
|
4
|
-
import { createFieldDecorator } from "../create-field-decorator";
|
|
5
|
-
import { createGetterDecorator } from "../create-getter-decorator";
|
|
6
|
-
import { createAccessorDecorator } from "../create-accessor-decorator";
|
|
7
|
-
import { Watch } from "../functions/watch";
|
|
8
|
-
import { When } from "../functions/when";
|
|
9
|
-
import { Compose } from "../properties/compose";
|
|
10
|
-
import { Computed } from "../properties/computed";
|
|
11
|
-
import { History } from "../properties/history";
|
|
12
|
-
import { FunctionProp } from "../properties/function-prop";
|
|
13
|
-
import { Prop } from "../properties/prop";
|
|
14
|
-
import { State } from "../properties/state";
|
|
15
|
-
// Helper: creates a ClassFieldDecoratorContext mock
|
|
16
|
-
function fieldCtx(name) {
|
|
17
|
-
const initializers = [];
|
|
18
|
-
return {
|
|
19
|
-
ctx: {
|
|
20
|
-
name,
|
|
21
|
-
kind: "field",
|
|
22
|
-
addInitializer(fn) {
|
|
23
|
-
initializers.push(fn);
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
run(instance) {
|
|
27
|
-
initializers.forEach((fn) => { fn.call(instance); });
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
function methodCtx(name) {
|
|
32
|
-
const initializers = [];
|
|
33
|
-
return {
|
|
34
|
-
ctx: {
|
|
35
|
-
name,
|
|
36
|
-
kind: "method",
|
|
37
|
-
addInitializer(fn) {
|
|
38
|
-
initializers.push(fn);
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
run(instance) {
|
|
42
|
-
initializers.forEach((fn) => { fn.call(instance); });
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
// Minimal concrete component
|
|
47
|
-
class TestComponent extends StatefulComponent {
|
|
48
|
-
render() { return null; }
|
|
49
|
-
}
|
|
50
|
-
// ── State ─────────────────────────────────────────────────────────────────────
|
|
51
|
-
describe("State decorator", () => {
|
|
52
|
-
it("converts a field to a reactive getter/setter", () => {
|
|
53
|
-
const { ctx, run } = fieldCtx("count");
|
|
54
|
-
State()(undefined, ctx);
|
|
55
|
-
const instance = new TestComponent();
|
|
56
|
-
instance.count = 10;
|
|
57
|
-
run(instance);
|
|
58
|
-
expect(instance.count).toBe(10);
|
|
59
|
-
instance.count = 20;
|
|
60
|
-
expect(instance.count).toBe(20);
|
|
61
|
-
});
|
|
62
|
-
it("sets state dirty on write", () => {
|
|
63
|
-
const { ctx, run } = fieldCtx("value");
|
|
64
|
-
State()(undefined, ctx);
|
|
65
|
-
const instance = new TestComponent();
|
|
66
|
-
instance.value = "hello";
|
|
67
|
-
run(instance);
|
|
68
|
-
setStateDirty(instance, false);
|
|
69
|
-
instance.value = "world";
|
|
70
|
-
expect(isStateDirty(instance)).toBe(true);
|
|
71
|
-
});
|
|
72
|
-
it("accepts null as initial value", () => {
|
|
73
|
-
const { ctx, run } = fieldCtx("nullable");
|
|
74
|
-
State()(undefined, ctx);
|
|
75
|
-
const instance = new TestComponent();
|
|
76
|
-
instance.nullable = null;
|
|
77
|
-
run(instance);
|
|
78
|
-
expect(instance.nullable).toBeNull();
|
|
79
|
-
instance.nullable = "set";
|
|
80
|
-
expect(instance.nullable).toBe("set");
|
|
81
|
-
});
|
|
82
|
-
it("multiple State fields on same class are independent", () => {
|
|
83
|
-
const { ctx: ctxA, run: runA } = fieldCtx("a");
|
|
84
|
-
const { ctx: ctxB, run: runB } = fieldCtx("b");
|
|
85
|
-
State()(undefined, ctxA);
|
|
86
|
-
State()(undefined, ctxB);
|
|
87
|
-
const instance = new TestComponent();
|
|
88
|
-
instance.a = 1;
|
|
89
|
-
instance.b = 2;
|
|
90
|
-
runA(instance);
|
|
91
|
-
runB(instance);
|
|
92
|
-
instance.a = 10;
|
|
93
|
-
expect(instance.a).toBe(10);
|
|
94
|
-
expect(instance.b).toBe(2);
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
// ── Prop ──────────────────────────────────────────────────────────────────────
|
|
98
|
-
describe("Prop decorator", () => {
|
|
99
|
-
it("reads from raw props when set by parent", () => {
|
|
100
|
-
const { ctx, run } = fieldCtx("label");
|
|
101
|
-
Prop()(undefined, ctx);
|
|
102
|
-
const instance = new TestComponent({ label: "hello" });
|
|
103
|
-
run(instance);
|
|
104
|
-
expect(instance.label).toBe("hello");
|
|
105
|
-
});
|
|
106
|
-
it("falls back to default value when not in raw props", () => {
|
|
107
|
-
const { ctx, run } = fieldCtx("size");
|
|
108
|
-
Prop()(undefined, ctx);
|
|
109
|
-
const instance = new TestComponent();
|
|
110
|
-
instance.size = "md"; // set before initializer runs
|
|
111
|
-
run(instance);
|
|
112
|
-
expect(instance.size).toBe("md");
|
|
113
|
-
});
|
|
114
|
-
it("unwraps a function prop (reactive prop)", () => {
|
|
115
|
-
const { ctx, run } = fieldCtx("text");
|
|
116
|
-
Prop()(undefined, ctx);
|
|
117
|
-
const s = signal("reactive");
|
|
118
|
-
const instance = new TestComponent({ text: () => s() });
|
|
119
|
-
run(instance);
|
|
120
|
-
expect(instance.text).toBe("reactive");
|
|
121
|
-
s.set("updated");
|
|
122
|
-
expect(instance.text).toBe("updated");
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
// ── FunctionProp ──────────────────────────────────────────────────────────────
|
|
126
|
-
describe("FunctionProp decorator", () => {
|
|
127
|
-
it("reads function values from raw props without invoking them", () => {
|
|
128
|
-
const { ctx, run } = fieldCtx("filter");
|
|
129
|
-
FunctionProp()(undefined, ctx);
|
|
130
|
-
const filter = vi.fn((value) => value > 1);
|
|
131
|
-
const instance = new TestComponent({ filter });
|
|
132
|
-
run(instance);
|
|
133
|
-
const received = instance.filter;
|
|
134
|
-
expect(received).toBe(filter);
|
|
135
|
-
expect(filter).not.toHaveBeenCalled();
|
|
136
|
-
expect(received(2)).toBe(true);
|
|
137
|
-
expect(filter).toHaveBeenCalledWith(2);
|
|
138
|
-
});
|
|
139
|
-
it("falls back to the default function when not in raw props", () => {
|
|
140
|
-
const { ctx, run } = fieldCtx("filter");
|
|
141
|
-
FunctionProp()(undefined, ctx);
|
|
142
|
-
const defaultFilter = vi.fn((value) => value % 2 === 0);
|
|
143
|
-
const instance = new TestComponent();
|
|
144
|
-
instance.filter = defaultFilter;
|
|
145
|
-
run(instance);
|
|
146
|
-
const received = instance.filter;
|
|
147
|
-
expect(received).toBe(defaultFilter);
|
|
148
|
-
expect(received(4)).toBe(true);
|
|
149
|
-
expect(defaultFilter).toHaveBeenCalledWith(4);
|
|
150
|
-
});
|
|
151
|
-
it("set() updates the default function used without a raw prop", () => {
|
|
152
|
-
const { ctx, run } = fieldCtx("filter");
|
|
153
|
-
FunctionProp()(undefined, ctx);
|
|
154
|
-
const instance = new TestComponent();
|
|
155
|
-
run(instance);
|
|
156
|
-
const nextFilter = (value) => value < 10;
|
|
157
|
-
instance.filter = nextFilter;
|
|
158
|
-
expect(instance.filter).toBe(nextFilter);
|
|
159
|
-
});
|
|
160
|
-
it("warns on a non-function raw prop in non-production", () => {
|
|
161
|
-
const { ctx, run } = fieldCtx("filter");
|
|
162
|
-
FunctionProp()(undefined, ctx);
|
|
163
|
-
const instance = new TestComponent({ filter: "not-a-function" });
|
|
164
|
-
run(instance);
|
|
165
|
-
const warn = vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
166
|
-
expect(instance.filter).toBe("not-a-function");
|
|
167
|
-
expect(warn).toHaveBeenCalledWith(expect.stringContaining("[FunctionProp]"));
|
|
168
|
-
warn.mockRestore();
|
|
169
|
-
});
|
|
170
|
-
it("does not warn in production", () => {
|
|
171
|
-
const { ctx, run } = fieldCtx("filter");
|
|
172
|
-
FunctionProp()(undefined, ctx);
|
|
173
|
-
const instance = new TestComponent({ filter: "not-a-function" });
|
|
174
|
-
run(instance);
|
|
175
|
-
const warn = vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
176
|
-
vi.stubEnv("NODE_ENV", "production");
|
|
177
|
-
expect(instance.filter).toBe("not-a-function");
|
|
178
|
-
expect(warn).not.toHaveBeenCalled();
|
|
179
|
-
vi.unstubAllEnvs();
|
|
180
|
-
warn.mockRestore();
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
// ── Computed ──────────────────────────────────────────────────────────────────
|
|
184
|
-
describe("Computed decorator", () => {
|
|
185
|
-
it("wraps a getter in a computed — result is memoized", () => {
|
|
186
|
-
const s = signal(5);
|
|
187
|
-
const fn = vi.fn(function () { return s() * 2; });
|
|
188
|
-
const { ctx, run } = getterCtx("doubled");
|
|
189
|
-
Computed()(fn, ctx);
|
|
190
|
-
const instance = {};
|
|
191
|
-
run(instance);
|
|
192
|
-
const result1 = instance.doubled;
|
|
193
|
-
const result2 = instance.doubled;
|
|
194
|
-
expect(result1).toBe(10);
|
|
195
|
-
expect(result2).toBe(10);
|
|
196
|
-
expect(fn).toHaveBeenCalledTimes(1);
|
|
197
|
-
});
|
|
198
|
-
it("recomputes when underlying signal changes", () => {
|
|
199
|
-
const s = signal(3);
|
|
200
|
-
const getter = function () { return s() * 2; };
|
|
201
|
-
const { ctx, run } = getterCtx("val");
|
|
202
|
-
Computed()(getter, ctx);
|
|
203
|
-
const obj = {};
|
|
204
|
-
run(obj);
|
|
205
|
-
expect(obj.val).toBe(6);
|
|
206
|
-
s.set(5);
|
|
207
|
-
expect(obj.val).toBe(10);
|
|
208
|
-
});
|
|
209
|
-
it("is per-instance — separate computed per object", () => {
|
|
210
|
-
const s = signal(1);
|
|
211
|
-
const getter = function () { return s(); };
|
|
212
|
-
const { ctx, run } = getterCtx("val");
|
|
213
|
-
Computed()(getter, ctx);
|
|
214
|
-
const a = {};
|
|
215
|
-
const b = {};
|
|
216
|
-
run(a);
|
|
217
|
-
run(b);
|
|
218
|
-
expect(a.val).toBe(1);
|
|
219
|
-
expect(b.val).toBe(1);
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
// ── Computed (writable) ───────────────────────────────────────────────────────
|
|
223
|
-
function getterCtx(name) {
|
|
224
|
-
const initializers = [];
|
|
225
|
-
return {
|
|
226
|
-
ctx: {
|
|
227
|
-
name,
|
|
228
|
-
kind: "getter",
|
|
229
|
-
addInitializer(fn) {
|
|
230
|
-
initializers.push(fn);
|
|
231
|
-
},
|
|
232
|
-
},
|
|
233
|
-
run(instance) {
|
|
234
|
-
initializers.forEach((fn) => { fn.call(instance); });
|
|
235
|
-
},
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
describe("Computed decorator — writable (set option)", () => {
|
|
239
|
-
it("setter is called when property is assigned", () => {
|
|
240
|
-
const written = [];
|
|
241
|
-
const { ctx, run } = getterCtx("double");
|
|
242
|
-
const s = signal(3);
|
|
243
|
-
const getter = function () { return s() * 2; };
|
|
244
|
-
Computed({ set: (v) => { written.push(v); } })(getter, ctx);
|
|
245
|
-
const instance = {};
|
|
246
|
-
run(instance);
|
|
247
|
-
instance.double = 42;
|
|
248
|
-
expect(written).toEqual([42]);
|
|
249
|
-
});
|
|
250
|
-
it("setter receives the component instance as this", () => {
|
|
251
|
-
let capturedThis;
|
|
252
|
-
const { ctx, run } = getterCtx("val");
|
|
253
|
-
const getter = function () { return 0; };
|
|
254
|
-
Computed({ set(_v) { capturedThis = this; } })(getter, ctx);
|
|
255
|
-
const instance = { id: "target" };
|
|
256
|
-
run(instance);
|
|
257
|
-
instance.val = 99;
|
|
258
|
-
expect(capturedThis).toBe(instance);
|
|
259
|
-
});
|
|
260
|
-
it("getter still returns the computed value after set option is added", () => {
|
|
261
|
-
const s = signal(5);
|
|
262
|
-
const { ctx, run } = getterCtx("doubled");
|
|
263
|
-
const getter = function () { return s() * 2; };
|
|
264
|
-
Computed({ set: (_v) => { } })(getter, ctx);
|
|
265
|
-
const instance = {};
|
|
266
|
-
run(instance);
|
|
267
|
-
expect(instance.doubled).toBe(10);
|
|
268
|
-
s.set(7);
|
|
269
|
-
expect(instance.doubled).toBe(14);
|
|
270
|
-
});
|
|
271
|
-
it("setter writes through to underlying signals, updating the getter", () => {
|
|
272
|
-
const s = signal(0);
|
|
273
|
-
const { ctx, run } = getterCtx("celsius");
|
|
274
|
-
const getter = function () { return s(); };
|
|
275
|
-
Computed({ set: (v) => { s.set(v); } })(getter, ctx);
|
|
276
|
-
const instance = {};
|
|
277
|
-
run(instance);
|
|
278
|
-
instance.celsius = 100;
|
|
279
|
-
expect(instance.celsius).toBe(100);
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
// ── Computed (accessor form) ──────────────────────────────────────────────────
|
|
283
|
-
describe("Computed decorator — accessor form ({ get, set })", () => {
|
|
284
|
-
function applyAccessorDecorator(options, instance) {
|
|
285
|
-
const target = {
|
|
286
|
-
get() { return undefined; },
|
|
287
|
-
set(_v) { },
|
|
288
|
-
};
|
|
289
|
-
const ctx = { kind: "accessor", name: "value" };
|
|
290
|
-
const result = Computed(options)(target, ctx);
|
|
291
|
-
result.init.call(instance, undefined);
|
|
292
|
-
return {
|
|
293
|
-
get: () => result.get.call(instance),
|
|
294
|
-
set: (v) => result.set.call(instance, v),
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
it("get function is wrapped in a computed — result is memoized", () => {
|
|
298
|
-
const s = signal(3);
|
|
299
|
-
const getFn = vi.fn(function () { return s() * 2; });
|
|
300
|
-
const instance = {};
|
|
301
|
-
const { get } = applyAccessorDecorator({ get: getFn, set: () => { } }, instance);
|
|
302
|
-
expect(get()).toBe(6);
|
|
303
|
-
expect(get()).toBe(6);
|
|
304
|
-
expect(getFn).toHaveBeenCalledTimes(1);
|
|
305
|
-
});
|
|
306
|
-
it("recomputes when underlying signal changes", () => {
|
|
307
|
-
const s = signal(10);
|
|
308
|
-
const instance = {};
|
|
309
|
-
const { get } = applyAccessorDecorator({
|
|
310
|
-
get() { return s() + 1; },
|
|
311
|
-
set() { },
|
|
312
|
-
}, instance);
|
|
313
|
-
expect(get()).toBe(11);
|
|
314
|
-
s.set(20);
|
|
315
|
-
expect(get()).toBe(21);
|
|
316
|
-
});
|
|
317
|
-
it("set function is called when the setter is invoked", () => {
|
|
318
|
-
const written = [];
|
|
319
|
-
const instance = {};
|
|
320
|
-
const { set } = applyAccessorDecorator({
|
|
321
|
-
get() { return 0; },
|
|
322
|
-
set(_v) { written.push(_v); },
|
|
323
|
-
}, instance);
|
|
324
|
-
set(42);
|
|
325
|
-
expect(written).toEqual([42]);
|
|
326
|
-
});
|
|
327
|
-
it("set function receives the component instance as this", () => {
|
|
328
|
-
let capturedThis;
|
|
329
|
-
const instance = { id: "target" };
|
|
330
|
-
const { set } = applyAccessorDecorator({
|
|
331
|
-
get() { return 0; },
|
|
332
|
-
set() { capturedThis = this; },
|
|
333
|
-
}, instance);
|
|
334
|
-
set(1);
|
|
335
|
-
expect(capturedThis).toBe(instance);
|
|
336
|
-
});
|
|
337
|
-
it("set writes through to underlying signals, updating the getter", () => {
|
|
338
|
-
const s = signal(0);
|
|
339
|
-
const instance = {};
|
|
340
|
-
const { get, set } = applyAccessorDecorator({
|
|
341
|
-
get() { return s(); },
|
|
342
|
-
set(v) { s.set(v); },
|
|
343
|
-
}, instance);
|
|
344
|
-
set(100);
|
|
345
|
-
expect(get()).toBe(100);
|
|
346
|
-
});
|
|
347
|
-
it("is per-instance — separate computed per object", () => {
|
|
348
|
-
const s = signal(5);
|
|
349
|
-
const options = {
|
|
350
|
-
get() { return s() * 2; },
|
|
351
|
-
set() { },
|
|
352
|
-
};
|
|
353
|
-
const a = {};
|
|
354
|
-
const b = {};
|
|
355
|
-
const decA = applyAccessorDecorator(options, a);
|
|
356
|
-
const decB = applyAccessorDecorator(options, b);
|
|
357
|
-
expect(decA.get()).toBe(10);
|
|
358
|
-
expect(decB.get()).toBe(10);
|
|
359
|
-
s.set(7);
|
|
360
|
-
expect(decA.get()).toBe(14);
|
|
361
|
-
expect(decB.get()).toBe(14);
|
|
362
|
-
});
|
|
363
|
-
it("accessor form without set — setter is a no-op", () => {
|
|
364
|
-
const s = signal(5);
|
|
365
|
-
const instance = {};
|
|
366
|
-
const target = { get() { return undefined; }, set() { } };
|
|
367
|
-
const ctx = { kind: "accessor", name: "value" };
|
|
368
|
-
const result = Computed({ get() { return s() * 2; } })(target, ctx);
|
|
369
|
-
result.init.call(instance, 0);
|
|
370
|
-
expect(result.get.call(instance)).toBe(10);
|
|
371
|
-
expect(() => { result.set.call(instance, 99); }).not.toThrow();
|
|
372
|
-
expect(result.get.call(instance)).toBe(10); // unchanged — no setter
|
|
373
|
-
});
|
|
374
|
-
});
|
|
375
|
-
// ── Watch ─────────────────────────────────────────────────────────────────────
|
|
376
|
-
describe("Watch decorator", () => {
|
|
377
|
-
it("calls handler when a single watched property changes", () => {
|
|
378
|
-
const { ctx, run } = methodCtx("onChange");
|
|
379
|
-
Watch("count")(function (newVal) {
|
|
380
|
-
this.__lastVal = newVal;
|
|
381
|
-
}, ctx);
|
|
382
|
-
const s = signal(0);
|
|
383
|
-
const instance = new TestComponent();
|
|
384
|
-
Object.defineProperty(instance, "count", {
|
|
385
|
-
get: () => s(),
|
|
386
|
-
configurable: true,
|
|
387
|
-
});
|
|
388
|
-
run(instance);
|
|
389
|
-
// Simulate onMount
|
|
390
|
-
instance.onMount?.();
|
|
391
|
-
s.set(5);
|
|
392
|
-
expect(instance.__lastVal).toBe(5);
|
|
393
|
-
});
|
|
394
|
-
it("does not fire if value hasn't changed", () => {
|
|
395
|
-
const { ctx, run } = methodCtx("onNotChange");
|
|
396
|
-
const handler = vi.fn();
|
|
397
|
-
Watch("x")(handler, ctx);
|
|
398
|
-
const s = signal(1);
|
|
399
|
-
const instance = new TestComponent();
|
|
400
|
-
Object.defineProperty(instance, "x", {
|
|
401
|
-
get: () => s(),
|
|
402
|
-
configurable: true,
|
|
403
|
-
});
|
|
404
|
-
run(instance);
|
|
405
|
-
instance.onMount?.();
|
|
406
|
-
s.set(1); // same value
|
|
407
|
-
expect(handler).not.toHaveBeenCalled();
|
|
408
|
-
});
|
|
409
|
-
it("multi-prop watch fires when any watched prop changes", async () => {
|
|
410
|
-
const { ctx, run } = methodCtx("onMulti");
|
|
411
|
-
const handler = vi.fn();
|
|
412
|
-
Watch("a", "b")(handler, ctx);
|
|
413
|
-
const sa = signal(0);
|
|
414
|
-
const sb = signal(0);
|
|
415
|
-
const instance = new TestComponent();
|
|
416
|
-
Object.defineProperty(instance, "a", { get: () => sa(), configurable: true });
|
|
417
|
-
Object.defineProperty(instance, "b", { get: () => sb(), configurable: true });
|
|
418
|
-
run(instance);
|
|
419
|
-
instance.onMount?.();
|
|
420
|
-
sa.set(1);
|
|
421
|
-
await Promise.resolve();
|
|
422
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
423
|
-
sb.set(1);
|
|
424
|
-
await Promise.resolve();
|
|
425
|
-
expect(handler).toHaveBeenCalledTimes(2);
|
|
426
|
-
});
|
|
427
|
-
it("multi-prop watch coalesces simultaneous changes into a single callback", async () => {
|
|
428
|
-
const { ctx, run } = methodCtx("onMulti");
|
|
429
|
-
const handler = vi.fn();
|
|
430
|
-
Watch("a", "b")(handler, ctx);
|
|
431
|
-
const sa = signal(0);
|
|
432
|
-
const sb = signal(0);
|
|
433
|
-
const instance = new TestComponent();
|
|
434
|
-
Object.defineProperty(instance, "a", { get: () => sa(), configurable: true });
|
|
435
|
-
Object.defineProperty(instance, "b", { get: () => sb(), configurable: true });
|
|
436
|
-
run(instance);
|
|
437
|
-
instance.onMount?.();
|
|
438
|
-
// both change in the same synchronous block
|
|
439
|
-
sa.set(1);
|
|
440
|
-
sb.set(1);
|
|
441
|
-
await Promise.resolve();
|
|
442
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
443
|
-
expect(handler).toHaveBeenCalledWith({ a: 1, b: 1 }, { a: 0, b: 0 });
|
|
444
|
-
});
|
|
445
|
-
});
|
|
446
|
-
// ── History ───────────────────────────────────────────────────────────────────
|
|
447
|
-
describe("History decorator", () => {
|
|
448
|
-
it("creates the history field on the instance", () => {
|
|
449
|
-
const { ctx, run } = fieldCtx("scoreHistory");
|
|
450
|
-
History("score")(undefined, ctx);
|
|
451
|
-
const instance = new TestComponent();
|
|
452
|
-
run(instance);
|
|
453
|
-
expect(instance.scoreHistory).toBeDefined();
|
|
454
|
-
});
|
|
455
|
-
it("history tracks changes to the property", () => {
|
|
456
|
-
const { ctx, run } = fieldCtx("scoreHistory");
|
|
457
|
-
History("score")(undefined, ctx);
|
|
458
|
-
const s = signal(0);
|
|
459
|
-
const instance = new TestComponent();
|
|
460
|
-
Object.defineProperty(instance, "score", {
|
|
461
|
-
get: () => s(),
|
|
462
|
-
set: (v) => { s.set(v); },
|
|
463
|
-
configurable: true,
|
|
464
|
-
});
|
|
465
|
-
run(instance);
|
|
466
|
-
const h = instance.scoreHistory;
|
|
467
|
-
s.set(10);
|
|
468
|
-
expect(h.canUndo()).toBe(true);
|
|
469
|
-
expect(h.values()).toContain(10);
|
|
470
|
-
});
|
|
471
|
-
it("canUndo becomes true after a value change", () => {
|
|
472
|
-
const { ctx, run } = fieldCtx("numHistory");
|
|
473
|
-
History("num")(undefined, ctx);
|
|
474
|
-
const s = signal(0);
|
|
475
|
-
const instance = new TestComponent();
|
|
476
|
-
Object.defineProperty(instance, "num", {
|
|
477
|
-
get: () => s(),
|
|
478
|
-
set: (v) => { s.set(v); },
|
|
479
|
-
configurable: true,
|
|
480
|
-
});
|
|
481
|
-
run(instance);
|
|
482
|
-
const h = instance.numHistory;
|
|
483
|
-
expect(h.canUndo()).toBe(false);
|
|
484
|
-
s.set(5);
|
|
485
|
-
expect(h.canUndo()).toBe(true);
|
|
486
|
-
});
|
|
487
|
-
it("values() contains the current value", () => {
|
|
488
|
-
const { ctx, run } = fieldCtx("xHistory");
|
|
489
|
-
History("x")(undefined, ctx);
|
|
490
|
-
const s = signal(42);
|
|
491
|
-
const instance = new TestComponent();
|
|
492
|
-
Object.defineProperty(instance, "x", {
|
|
493
|
-
get: () => s(),
|
|
494
|
-
set: (v) => { s.set(v); },
|
|
495
|
-
configurable: true,
|
|
496
|
-
});
|
|
497
|
-
run(instance);
|
|
498
|
-
const h = instance.xHistory;
|
|
499
|
-
expect(h.current()).toBe(42);
|
|
500
|
-
expect(h.values()).toContain(42);
|
|
501
|
-
});
|
|
502
|
-
it("undo() reverts the property to the previous value", () => {
|
|
503
|
-
const { ctx, run } = fieldCtx("score2History");
|
|
504
|
-
History("score2")(undefined, ctx);
|
|
505
|
-
const s = signal(0);
|
|
506
|
-
const instance = new TestComponent();
|
|
507
|
-
Object.defineProperty(instance, "score2", {
|
|
508
|
-
get: () => s(),
|
|
509
|
-
set: (v) => { s.set(v); },
|
|
510
|
-
configurable: true,
|
|
511
|
-
});
|
|
512
|
-
run(instance);
|
|
513
|
-
const h = instance.score2History;
|
|
514
|
-
s.set(5);
|
|
515
|
-
s.set(10);
|
|
516
|
-
h.undo();
|
|
517
|
-
expect(s()).toBe(5);
|
|
518
|
-
});
|
|
519
|
-
it("redo() moves forward after undo", () => {
|
|
520
|
-
const { ctx, run } = fieldCtx("score3History");
|
|
521
|
-
History("score3")(undefined, ctx);
|
|
522
|
-
const s = signal(0);
|
|
523
|
-
const instance = new TestComponent();
|
|
524
|
-
Object.defineProperty(instance, "score3", {
|
|
525
|
-
get: () => s(),
|
|
526
|
-
set: (v) => { s.set(v); },
|
|
527
|
-
configurable: true,
|
|
528
|
-
});
|
|
529
|
-
run(instance);
|
|
530
|
-
const h = instance.score3History;
|
|
531
|
-
s.set(1);
|
|
532
|
-
s.set(2);
|
|
533
|
-
h.undo();
|
|
534
|
-
expect(h.canRedo()).toBe(true);
|
|
535
|
-
h.redo();
|
|
536
|
-
expect(s()).toBe(2);
|
|
537
|
-
});
|
|
538
|
-
it("respects a custom history limit", () => {
|
|
539
|
-
const { ctx, run } = fieldCtx("vHistory");
|
|
540
|
-
History("v", 3)(undefined, ctx);
|
|
541
|
-
const s = signal(0);
|
|
542
|
-
const instance = new TestComponent();
|
|
543
|
-
Object.defineProperty(instance, "v", {
|
|
544
|
-
get: () => s(),
|
|
545
|
-
set: (v) => { s.set(v); },
|
|
546
|
-
configurable: true,
|
|
547
|
-
});
|
|
548
|
-
run(instance);
|
|
549
|
-
const h = instance.vHistory;
|
|
550
|
-
s.set(1);
|
|
551
|
-
s.set(2);
|
|
552
|
-
s.set(3);
|
|
553
|
-
s.set(4);
|
|
554
|
-
s.set(5);
|
|
555
|
-
// values includes current + up to 3 past items → max 4 items
|
|
556
|
-
expect(h.values().length).toBeLessThanOrEqual(4);
|
|
557
|
-
});
|
|
558
|
-
it("returns the same history instance on repeated access", () => {
|
|
559
|
-
const { ctx, run } = fieldCtx("zHistory");
|
|
560
|
-
History("z")(undefined, ctx);
|
|
561
|
-
const instance = new TestComponent();
|
|
562
|
-
run(instance);
|
|
563
|
-
const h1 = instance.zHistory;
|
|
564
|
-
const h2 = instance.zHistory;
|
|
565
|
-
expect(h1).toBe(h2);
|
|
566
|
-
});
|
|
567
|
-
});
|
|
568
|
-
// ── When ──────────────────────────────────────────────────────────────────────
|
|
569
|
-
describe("When decorator", () => {
|
|
570
|
-
function methodCtxFor(name) {
|
|
571
|
-
const initializers = [];
|
|
572
|
-
return {
|
|
573
|
-
ctx: {
|
|
574
|
-
name,
|
|
575
|
-
kind: "method",
|
|
576
|
-
addInitializer(fn) {
|
|
577
|
-
initializers.push(fn);
|
|
578
|
-
},
|
|
579
|
-
},
|
|
580
|
-
run(instance) {
|
|
581
|
-
initializers.forEach((fn) => { fn.call(instance); });
|
|
582
|
-
},
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
it("calls the method when a truthy value is emitted from the watched prop", () => {
|
|
586
|
-
const { ctx, run } = methodCtxFor("onReady");
|
|
587
|
-
const handler = vi.fn();
|
|
588
|
-
When("flag")(handler, ctx);
|
|
589
|
-
// @When accesses instance[propName] and expects a Signal/Computed object
|
|
590
|
-
const s = signal(null);
|
|
591
|
-
const instance = new TestComponent();
|
|
592
|
-
instance.flag = s;
|
|
593
|
-
run(instance);
|
|
594
|
-
instance.onMount?.();
|
|
595
|
-
s.set("go");
|
|
596
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
597
|
-
});
|
|
598
|
-
it("does not call the method for falsy values", () => {
|
|
599
|
-
const { ctx, run } = methodCtxFor("onData");
|
|
600
|
-
const handler = vi.fn();
|
|
601
|
-
When("item")(handler, ctx);
|
|
602
|
-
const s = signal(0);
|
|
603
|
-
const instance = new TestComponent();
|
|
604
|
-
instance.item = s;
|
|
605
|
-
run(instance);
|
|
606
|
-
instance.onMount?.();
|
|
607
|
-
// 0 is falsy — handler should NOT be called
|
|
608
|
-
expect(handler).not.toHaveBeenCalled();
|
|
609
|
-
});
|
|
610
|
-
it("calls onUnmount cleanup without throwing", () => {
|
|
611
|
-
const { ctx, run } = methodCtxFor("onVal");
|
|
612
|
-
When("val")(vi.fn(), ctx);
|
|
613
|
-
const s = signal(0);
|
|
614
|
-
const instance = new TestComponent();
|
|
615
|
-
instance.val = s;
|
|
616
|
-
run(instance);
|
|
617
|
-
instance.onMount?.();
|
|
618
|
-
expect(() => { instance.onUnmount?.(); }).not.toThrow();
|
|
619
|
-
});
|
|
620
|
-
it("preserves existing onMount/onUnmount implementations", () => {
|
|
621
|
-
const { ctx, run } = methodCtxFor("onV");
|
|
622
|
-
When("v")(vi.fn(), ctx);
|
|
623
|
-
const mountSpy = vi.fn();
|
|
624
|
-
const unmountSpy = vi.fn();
|
|
625
|
-
const s = signal(0);
|
|
626
|
-
const instance = new TestComponent();
|
|
627
|
-
instance.v = s;
|
|
628
|
-
run(instance);
|
|
629
|
-
// Manually set callbacks after initializer ran
|
|
630
|
-
const prevMount = instance.onMount?.bind(instance);
|
|
631
|
-
instance.onMount = function () {
|
|
632
|
-
mountSpy();
|
|
633
|
-
prevMount?.();
|
|
634
|
-
};
|
|
635
|
-
const prevUnmount = instance.onUnmount?.bind(instance);
|
|
636
|
-
instance.onUnmount = function () {
|
|
637
|
-
unmountSpy();
|
|
638
|
-
prevUnmount?.();
|
|
639
|
-
};
|
|
640
|
-
instance.onMount?.();
|
|
641
|
-
instance.onUnmount?.();
|
|
642
|
-
expect(mountSpy).toHaveBeenCalled();
|
|
643
|
-
expect(unmountSpy).toHaveBeenCalled();
|
|
644
|
-
});
|
|
645
|
-
it("works when the watched prop is a computed value", async () => {
|
|
646
|
-
const { ctx, run } = methodCtxFor("onComputed");
|
|
647
|
-
const handler = vi.fn();
|
|
648
|
-
When("doubled")(handler, ctx);
|
|
649
|
-
const s = signal(0);
|
|
650
|
-
const c = computed(() => s() * 2);
|
|
651
|
-
const instance = new TestComponent();
|
|
652
|
-
// Store the computed itself (not the result) so When can call isComputed on it
|
|
653
|
-
instance.doubled = c;
|
|
654
|
-
run(instance);
|
|
655
|
-
instance.onMount?.();
|
|
656
|
-
s.set(5); // doubled becomes 10 (truthy)
|
|
657
|
-
await Promise.resolve();
|
|
658
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
659
|
-
});
|
|
660
|
-
it("fires immediately when the watched prop is a plain truthy value (non-signal)", () => {
|
|
661
|
-
const { ctx, run } = methodCtxFor("onPlain");
|
|
662
|
-
const handler = vi.fn();
|
|
663
|
-
When("plainProp")(handler, ctx);
|
|
664
|
-
const instance = new TestComponent();
|
|
665
|
-
instance.plainProp = "hello";
|
|
666
|
-
run(instance);
|
|
667
|
-
instance.onMount?.();
|
|
668
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
669
|
-
expect(handler).toHaveBeenCalledWith("hello");
|
|
670
|
-
});
|
|
671
|
-
it("condition: fires when condition returns true", async () => {
|
|
672
|
-
const { ctx, run } = methodCtxFor("onHigh");
|
|
673
|
-
const handler = vi.fn();
|
|
674
|
-
When("count", (v) => v > 5)(handler, ctx);
|
|
675
|
-
const s = signal(0);
|
|
676
|
-
const instance = new TestComponent();
|
|
677
|
-
instance.count = s;
|
|
678
|
-
run(instance);
|
|
679
|
-
instance.onMount?.();
|
|
680
|
-
s.set(3); // condition false — should not fire
|
|
681
|
-
await Promise.resolve();
|
|
682
|
-
expect(handler).not.toHaveBeenCalled();
|
|
683
|
-
s.set(7); // condition true — should fire
|
|
684
|
-
await Promise.resolve();
|
|
685
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
686
|
-
expect(handler).toHaveBeenCalledWith(7);
|
|
687
|
-
});
|
|
688
|
-
it("condition: does not fire when value is truthy but condition returns false", async () => {
|
|
689
|
-
const { ctx, run } = methodCtxFor("onPositive");
|
|
690
|
-
const handler = vi.fn();
|
|
691
|
-
When("n", (v) => v > 100)(handler, ctx);
|
|
692
|
-
const s = signal(0);
|
|
693
|
-
const instance = new TestComponent();
|
|
694
|
-
instance.n = s;
|
|
695
|
-
run(instance);
|
|
696
|
-
instance.onMount?.();
|
|
697
|
-
s.set(1); // truthy but condition fails
|
|
698
|
-
s.set(50); // truthy but condition fails
|
|
699
|
-
await Promise.resolve();
|
|
700
|
-
expect(handler).not.toHaveBeenCalled();
|
|
701
|
-
});
|
|
702
|
-
it("condition: fires on the first value satisfying the condition, then stops", async () => {
|
|
703
|
-
const { ctx, run } = methodCtxFor("onFirst");
|
|
704
|
-
const handler = vi.fn();
|
|
705
|
-
When("v", (v) => v >= 10)(handler, ctx);
|
|
706
|
-
const s = signal(0);
|
|
707
|
-
const instance = new TestComponent();
|
|
708
|
-
instance.v = s;
|
|
709
|
-
run(instance);
|
|
710
|
-
instance.onMount?.();
|
|
711
|
-
s.set(10); // first match — fires
|
|
712
|
-
await Promise.resolve();
|
|
713
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
714
|
-
s.set(20); // when() already disposed — should not fire again
|
|
715
|
-
await Promise.resolve();
|
|
716
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
717
|
-
});
|
|
718
|
-
});
|
|
719
|
-
// ── Prop (additional branches) ────────────────────────────────────────────────
|
|
720
|
-
describe("Prop decorator — additional branches", () => {
|
|
721
|
-
it("set() updates the default value (used when no raw props entry)", () => {
|
|
722
|
-
const { ctx, run } = fieldCtx("size");
|
|
723
|
-
Prop()(undefined, ctx);
|
|
724
|
-
const instance = new TestComponent();
|
|
725
|
-
instance.size = "sm";
|
|
726
|
-
run(instance);
|
|
727
|
-
// Setter path: updates defaults.
|
|
728
|
-
instance.size = "lg";
|
|
729
|
-
expect(instance.size).toBe("lg");
|
|
730
|
-
});
|
|
731
|
-
});
|
|
732
|
-
// ── History (additional branches) ────────────────────────────────────────────
|
|
733
|
-
describe("History decorator — undo with no previous value", () => {
|
|
734
|
-
it("undo() does nothing when there is no previous value", () => {
|
|
735
|
-
const { ctx, run } = fieldCtx("ptsHistory");
|
|
736
|
-
History("pts")(undefined, ctx);
|
|
737
|
-
const s = signal(0);
|
|
738
|
-
const instance = new TestComponent();
|
|
739
|
-
Object.defineProperty(instance, "pts", {
|
|
740
|
-
get: () => s(),
|
|
741
|
-
set: (v) => { s.set(v); },
|
|
742
|
-
configurable: true,
|
|
743
|
-
});
|
|
744
|
-
run(instance);
|
|
745
|
-
const h = instance.ptsHistory;
|
|
746
|
-
// No changes yet — undo should be a no-op
|
|
747
|
-
expect(h.canUndo()).toBe(false);
|
|
748
|
-
expect(() => h.undo()).not.toThrow();
|
|
749
|
-
expect(s()).toBe(0);
|
|
750
|
-
});
|
|
751
|
-
});
|
|
752
|
-
// ── History decorator — multiple fields ───────────────────────────────────────
|
|
753
|
-
describe("History decorator — multiple History properties on same instance", () => {
|
|
754
|
-
it("two History fields are independent", () => {
|
|
755
|
-
const { ctx: ctxX, run: runX } = fieldCtx("xHistory");
|
|
756
|
-
const { ctx: ctxY, run: runY } = fieldCtx("yHistory");
|
|
757
|
-
History("x")(undefined, ctxX);
|
|
758
|
-
History("y")(undefined, ctxY);
|
|
759
|
-
const sx = signal(0);
|
|
760
|
-
const sy = signal(0);
|
|
761
|
-
const instance = new TestComponent();
|
|
762
|
-
Object.defineProperty(instance, "x", {
|
|
763
|
-
get: () => sx(), set: (v) => { sx.set(v); }, configurable: true,
|
|
764
|
-
});
|
|
765
|
-
Object.defineProperty(instance, "y", {
|
|
766
|
-
get: () => sy(), set: (v) => { sy.set(v); }, configurable: true,
|
|
767
|
-
});
|
|
768
|
-
runX(instance);
|
|
769
|
-
runY(instance);
|
|
770
|
-
// Access history first to trigger lazy creation and subscribe to signals
|
|
771
|
-
const hx = instance.xHistory;
|
|
772
|
-
const hy = instance.yHistory;
|
|
773
|
-
sx.set(5);
|
|
774
|
-
sy.set(10);
|
|
775
|
-
expect(hx.canUndo()).toBe(true);
|
|
776
|
-
expect(hy.canUndo()).toBe(true);
|
|
777
|
-
expect(hx.current()).toBe(5);
|
|
778
|
-
expect(hy.current()).toBe(10);
|
|
779
|
-
});
|
|
780
|
-
it("clear() after undo removes redo entries", () => {
|
|
781
|
-
const { ctx, run } = fieldCtx("valHistory");
|
|
782
|
-
History("val")(undefined, ctx);
|
|
783
|
-
const s = signal(0);
|
|
784
|
-
const instance = new TestComponent();
|
|
785
|
-
Object.defineProperty(instance, "val", {
|
|
786
|
-
get: () => s(), set: (v) => { s.set(v); }, configurable: true,
|
|
787
|
-
});
|
|
788
|
-
run(instance);
|
|
789
|
-
const h = instance.valHistory;
|
|
790
|
-
s.set(1);
|
|
791
|
-
s.set(2);
|
|
792
|
-
h.undo();
|
|
793
|
-
expect(h.canRedo()).toBe(true);
|
|
794
|
-
h.clear();
|
|
795
|
-
expect(h.canRedo()).toBe(false);
|
|
796
|
-
expect(h.canUndo()).toBe(false);
|
|
797
|
-
});
|
|
798
|
-
});
|
|
799
|
-
// ── Watch (cleanup + multi-prop non-watched) ──────────────────────────────────
|
|
800
|
-
describe("Watch decorator — onUnmount cleanup", () => {
|
|
801
|
-
it("stops firing after onUnmount is called", () => {
|
|
802
|
-
const { ctx, run } = methodCtx("onCount");
|
|
803
|
-
const handler = vi.fn();
|
|
804
|
-
Watch("count")(handler, ctx);
|
|
805
|
-
const s = signal(0);
|
|
806
|
-
const instance = new TestComponent();
|
|
807
|
-
Object.defineProperty(instance, "count", { get: () => s(), configurable: true });
|
|
808
|
-
run(instance);
|
|
809
|
-
instance.onMount?.();
|
|
810
|
-
s.set(1);
|
|
811
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
812
|
-
instance.onUnmount?.();
|
|
813
|
-
s.set(2);
|
|
814
|
-
// After unmount the effect is stopped — handler must NOT fire again
|
|
815
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
816
|
-
});
|
|
817
|
-
it("preserves existing onUnmount when adding cleanup", () => {
|
|
818
|
-
const { ctx, run } = methodCtx("onVal");
|
|
819
|
-
const handler = vi.fn();
|
|
820
|
-
Watch("val")(handler, ctx);
|
|
821
|
-
const s = signal(0);
|
|
822
|
-
const instance = new TestComponent();
|
|
823
|
-
Object.defineProperty(instance, "val", { get: () => s(), configurable: true });
|
|
824
|
-
run(instance);
|
|
825
|
-
const unmountSpy = vi.fn();
|
|
826
|
-
const prevUnmount = instance.onUnmount?.bind(instance);
|
|
827
|
-
instance.onUnmount = function () { unmountSpy(); prevUnmount?.(); };
|
|
828
|
-
instance.onMount?.();
|
|
829
|
-
instance.onUnmount?.();
|
|
830
|
-
expect(unmountSpy).toHaveBeenCalled();
|
|
831
|
-
});
|
|
832
|
-
it("multi-prop watch does NOT fire when an unwatched prop changes", async () => {
|
|
833
|
-
const { ctx, run } = methodCtx("onAB");
|
|
834
|
-
const handler = vi.fn();
|
|
835
|
-
Watch("a", "b")(handler, ctx);
|
|
836
|
-
const sa = signal(0);
|
|
837
|
-
const sb = signal(0);
|
|
838
|
-
const sc = signal(0); // not watched
|
|
839
|
-
const instance = new TestComponent();
|
|
840
|
-
Object.defineProperty(instance, "a", { get: () => sa(), configurable: true });
|
|
841
|
-
Object.defineProperty(instance, "b", { get: () => sb(), configurable: true });
|
|
842
|
-
Object.defineProperty(instance, "c", { get: () => sc(), configurable: true });
|
|
843
|
-
run(instance);
|
|
844
|
-
instance.onMount?.();
|
|
845
|
-
sc.set(99); // unwatched prop — handler must NOT fire
|
|
846
|
-
await Promise.resolve();
|
|
847
|
-
expect(handler).not.toHaveBeenCalled();
|
|
848
|
-
sa.set(1); // watched — handler fires
|
|
849
|
-
await Promise.resolve();
|
|
850
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
851
|
-
});
|
|
852
|
-
});
|
|
853
|
-
// ── Watch (additional branches — computed readValue) ──────────────────────────
|
|
854
|
-
describe("Watch decorator — reading computed values", () => {
|
|
855
|
-
it("reads the value of a computed field (not the computed object)", async () => {
|
|
856
|
-
const { ctx, run } = methodCtx("onDoubled");
|
|
857
|
-
const handler = vi.fn();
|
|
858
|
-
Watch("doubled")(handler, ctx);
|
|
859
|
-
const s = signal(1);
|
|
860
|
-
const c = computed(() => s() * 2);
|
|
861
|
-
const instance = new TestComponent();
|
|
862
|
-
// Watch reads instance["doubled"] — if it's a computed, it calls it
|
|
863
|
-
Object.defineProperty(instance, "doubled", {
|
|
864
|
-
get: () => c,
|
|
865
|
-
configurable: true,
|
|
866
|
-
});
|
|
867
|
-
run(instance);
|
|
868
|
-
instance.onMount?.();
|
|
869
|
-
s.set(5); // c() goes from 2 to 10 → triggers handler
|
|
870
|
-
await Promise.resolve();
|
|
871
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
872
|
-
});
|
|
873
|
-
});
|
|
874
|
-
// ── Watch — additional new tests ──────────────────────────────────────────────
|
|
875
|
-
describe("Watch decorator — new cases", () => {
|
|
876
|
-
it("NaN → NaN does NOT fire (Object.is semantics)", () => {
|
|
877
|
-
const { ctx, run } = methodCtx("onNaN");
|
|
878
|
-
const handler = vi.fn();
|
|
879
|
-
Watch("nanProp")(handler, ctx);
|
|
880
|
-
const s = signal(NaN);
|
|
881
|
-
const instance = new TestComponent();
|
|
882
|
-
Object.defineProperty(instance, "nanProp", { get: () => s(), configurable: true });
|
|
883
|
-
run(instance);
|
|
884
|
-
instance.onMount?.();
|
|
885
|
-
s.set(NaN); // Object.is(NaN, NaN) === true → no change
|
|
886
|
-
expect(handler).not.toHaveBeenCalled();
|
|
887
|
-
});
|
|
888
|
-
it("watch callback that throws does not crash the effect system", () => {
|
|
889
|
-
const { ctx, run } = methodCtx("onThrow");
|
|
890
|
-
const throwingHandler = vi.fn(() => { throw new Error("handler error"); });
|
|
891
|
-
Watch("tProp")(throwingHandler, ctx);
|
|
892
|
-
const s = signal(0);
|
|
893
|
-
const instance = new TestComponent();
|
|
894
|
-
Object.defineProperty(instance, "tProp", { get: () => s(), configurable: true });
|
|
895
|
-
run(instance);
|
|
896
|
-
instance.onMount?.();
|
|
897
|
-
// Even if the handler throws, the signal change shouldn't crash the effect framework
|
|
898
|
-
// The throw propagates from the effect, so we catch it here
|
|
899
|
-
expect(() => { s.set(1); }).toThrow("handler error");
|
|
900
|
-
// The system should still be intact — further calls are not tested for recovery
|
|
901
|
-
expect(throwingHandler).toHaveBeenCalledTimes(1);
|
|
902
|
-
});
|
|
903
|
-
it("multiple @Watch decorators on different methods both fire independently", () => {
|
|
904
|
-
const { ctx: ctxA, run: runA } = methodCtx("onA");
|
|
905
|
-
const { ctx: ctxB, run: runB } = methodCtx("onB");
|
|
906
|
-
const handlerA = vi.fn();
|
|
907
|
-
const handlerB = vi.fn();
|
|
908
|
-
Watch("propA")(handlerA, ctxA);
|
|
909
|
-
Watch("propB")(handlerB, ctxB);
|
|
910
|
-
const sa = signal(0);
|
|
911
|
-
const sb = signal(0);
|
|
912
|
-
const instance = new TestComponent();
|
|
913
|
-
Object.defineProperty(instance, "propA", { get: () => sa(), configurable: true });
|
|
914
|
-
Object.defineProperty(instance, "propB", { get: () => sb(), configurable: true });
|
|
915
|
-
runA(instance);
|
|
916
|
-
runB(instance);
|
|
917
|
-
instance.onMount?.();
|
|
918
|
-
sa.set(1);
|
|
919
|
-
expect(handlerA).toHaveBeenCalledTimes(1);
|
|
920
|
-
expect(handlerB).not.toHaveBeenCalled();
|
|
921
|
-
sb.set(1);
|
|
922
|
-
expect(handlerA).toHaveBeenCalledTimes(1);
|
|
923
|
-
expect(handlerB).toHaveBeenCalledTimes(1);
|
|
924
|
-
});
|
|
925
|
-
});
|
|
926
|
-
// ── When — additional new tests ───────────────────────────────────────────────
|
|
927
|
-
describe("When decorator — new cases", () => {
|
|
928
|
-
function methodCtxFor(name) {
|
|
929
|
-
const initializers = [];
|
|
930
|
-
return {
|
|
931
|
-
ctx: {
|
|
932
|
-
name,
|
|
933
|
-
kind: "method",
|
|
934
|
-
addInitializer(fn) {
|
|
935
|
-
initializers.push(fn);
|
|
936
|
-
},
|
|
937
|
-
},
|
|
938
|
-
run(instance) {
|
|
939
|
-
initializers.forEach((fn) => { fn.call(instance); });
|
|
940
|
-
},
|
|
941
|
-
};
|
|
942
|
-
}
|
|
943
|
-
it("@When fires only once — first truthy value; subsequent truthy values are ignored", () => {
|
|
944
|
-
const { ctx, run } = methodCtxFor("onToggle");
|
|
945
|
-
const handler = vi.fn();
|
|
946
|
-
When("toggle")(handler, ctx);
|
|
947
|
-
const s = signal(null);
|
|
948
|
-
const instance = new TestComponent();
|
|
949
|
-
instance.toggle = s;
|
|
950
|
-
run(instance);
|
|
951
|
-
instance.onMount?.();
|
|
952
|
-
s.set(1); // truthy → fires (first and only time)
|
|
953
|
-
s.set(null); // falsy
|
|
954
|
-
s.set(2); // truthy again — but when() has already disposed itself
|
|
955
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
956
|
-
expect(handler).toHaveBeenCalledWith(1);
|
|
957
|
-
});
|
|
958
|
-
it("@When callback that throws — onUnmount cleanup still works", () => {
|
|
959
|
-
const { ctx, run } = methodCtxFor("onThrowWhen");
|
|
960
|
-
const throwingHandler = vi.fn(() => { throw new Error("when error"); });
|
|
961
|
-
When("whenProp")(throwingHandler, ctx);
|
|
962
|
-
const s = signal(null);
|
|
963
|
-
const instance = new TestComponent();
|
|
964
|
-
instance.whenProp = s;
|
|
965
|
-
run(instance);
|
|
966
|
-
instance.onMount?.();
|
|
967
|
-
// Triggering truthy fires handler which throws
|
|
968
|
-
expect(() => { s.set(1); }).toThrow("when error");
|
|
969
|
-
// onUnmount should still work without crashing
|
|
970
|
-
expect(() => {
|
|
971
|
-
instance.onUnmount?.();
|
|
972
|
-
}).not.toThrow();
|
|
973
|
-
});
|
|
974
|
-
});
|
|
975
|
-
// ── State — additional new tests ──────────────────────────────────────────────
|
|
976
|
-
describe("State decorator — new cases", () => {
|
|
977
|
-
it("two @State fields can be set independently without cross-contamination", () => {
|
|
978
|
-
const { ctx: ctxX, run: runX } = fieldCtx("x");
|
|
979
|
-
const { ctx: ctxY, run: runY } = fieldCtx("y");
|
|
980
|
-
State()(undefined, ctxX);
|
|
981
|
-
State()(undefined, ctxY);
|
|
982
|
-
const instance = new TestComponent();
|
|
983
|
-
instance.x = 10;
|
|
984
|
-
instance.y = 20;
|
|
985
|
-
runX(instance);
|
|
986
|
-
runY(instance);
|
|
987
|
-
instance.x = 99;
|
|
988
|
-
expect(instance.x).toBe(99);
|
|
989
|
-
expect(instance.y).toBe(20);
|
|
990
|
-
instance.y = 77;
|
|
991
|
-
expect(instance.x).toBe(99);
|
|
992
|
-
expect(instance.y).toBe(77);
|
|
993
|
-
});
|
|
994
|
-
it("setting @State to undefined works (signal stores undefined)", () => {
|
|
995
|
-
const { ctx, run } = fieldCtx("maybeVal");
|
|
996
|
-
State()(undefined, ctx);
|
|
997
|
-
const instance = new TestComponent();
|
|
998
|
-
instance.maybeVal = "initial";
|
|
999
|
-
run(instance);
|
|
1000
|
-
instance.maybeVal = undefined;
|
|
1001
|
-
expect(instance.maybeVal).toBeUndefined();
|
|
1002
|
-
});
|
|
1003
|
-
});
|
|
1004
|
-
// ── Prop — additional new tests ───────────────────────────────────────────────
|
|
1005
|
-
describe("Prop decorator — new cases", () => {
|
|
1006
|
-
it("function prop that throws — getter propagates the error", () => {
|
|
1007
|
-
const { ctx, run } = fieldCtx("dynProp");
|
|
1008
|
-
Prop()(undefined, ctx);
|
|
1009
|
-
const throwingFn = () => { throw new Error("prop error"); };
|
|
1010
|
-
const instance = new TestComponent({ dynProp: throwingFn });
|
|
1011
|
-
run(instance);
|
|
1012
|
-
expect(() => instance.dynProp).toThrow("prop error");
|
|
1013
|
-
});
|
|
1014
|
-
it("undefined passed explicitly vs key not present — both fall back to default", () => {
|
|
1015
|
-
const { ctx: ctx1, run: run1 } = fieldCtx("optA");
|
|
1016
|
-
const { ctx: ctx2, run: run2 } = fieldCtx("optB");
|
|
1017
|
-
Prop()(undefined, ctx1);
|
|
1018
|
-
Prop()(undefined, ctx2);
|
|
1019
|
-
// Case 1: key explicitly set to undefined in rawProps
|
|
1020
|
-
const instance1 = new TestComponent({ optA: undefined });
|
|
1021
|
-
instance1.optA = "default-a";
|
|
1022
|
-
run1(instance1);
|
|
1023
|
-
// undefined from rawProps — Prop checks `!== undefined`, so falls back to default
|
|
1024
|
-
expect(instance1.optA).toBe("default-a");
|
|
1025
|
-
// Case 2: key not present in rawProps at all
|
|
1026
|
-
const instance2 = new TestComponent({});
|
|
1027
|
-
instance2.optB = "default-b";
|
|
1028
|
-
run2(instance2);
|
|
1029
|
-
expect(instance2.optB).toBe("default-b");
|
|
1030
|
-
});
|
|
1031
|
-
});
|
|
1032
|
-
// ── Compose — additional new tests ────────────────────────────────────────────
|
|
1033
|
-
describe("@Compose decorator — new cases", () => {
|
|
1034
|
-
it("setup() that throws — field access on the instance throws", () => {
|
|
1035
|
-
class ThrowingComposable {
|
|
1036
|
-
setup() {
|
|
1037
|
-
throw new Error("setup failed");
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
const { ctx, run } = fieldCtx("throwing");
|
|
1041
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1042
|
-
expect(() => {
|
|
1043
|
-
const instance = new TestComponent();
|
|
1044
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1045
|
-
Compose(ThrowingComposable)(undefined, ctx);
|
|
1046
|
-
run(instance);
|
|
1047
|
-
}).toThrow("setup failed");
|
|
1048
|
-
});
|
|
1049
|
-
it("string argument that matches no instance property falls back to the literal string", () => {
|
|
1050
|
-
class ArgComposable {
|
|
1051
|
-
val;
|
|
1052
|
-
constructor(val) {
|
|
1053
|
-
this.val = val;
|
|
1054
|
-
}
|
|
1055
|
-
setup() { return { resolved: this.val }; }
|
|
1056
|
-
}
|
|
1057
|
-
const { ctx, run } = fieldCtx("strArgUndef");
|
|
1058
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1059
|
-
Compose(ArgComposable, "missingProp")(undefined, ctx);
|
|
1060
|
-
const instance = new TestComponent();
|
|
1061
|
-
// missingProp is not set on instance — falls back to the literal "missingProp"
|
|
1062
|
-
run(instance);
|
|
1063
|
-
const view = instance.strArgUndef;
|
|
1064
|
-
expect(view.resolved).toBe("missingProp");
|
|
1065
|
-
});
|
|
1066
|
-
it("string argument that matches an instance property resolves to its value", () => {
|
|
1067
|
-
class ArgComposable {
|
|
1068
|
-
val;
|
|
1069
|
-
constructor(val) {
|
|
1070
|
-
this.val = val;
|
|
1071
|
-
}
|
|
1072
|
-
setup() { return { resolved: this.val }; }
|
|
1073
|
-
}
|
|
1074
|
-
const { ctx, run } = fieldCtx("strArgFound");
|
|
1075
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1076
|
-
Compose(ArgComposable, "myProp")(undefined, ctx);
|
|
1077
|
-
const instance = new TestComponent();
|
|
1078
|
-
instance.myProp = 42;
|
|
1079
|
-
run(instance);
|
|
1080
|
-
const view = instance.strArgFound;
|
|
1081
|
-
expect(view.resolved).toBe(42);
|
|
1082
|
-
});
|
|
1083
|
-
});
|
|
1084
|
-
// ── getter() helper ───────────────────────────────────────────────────────────
|
|
1085
|
-
import { getter } from "../properties/compose";
|
|
1086
|
-
describe("getter() factory", () => {
|
|
1087
|
-
it("resolves to a function that reads the named property from the instance", () => {
|
|
1088
|
-
class ArgComposable {
|
|
1089
|
-
getVal;
|
|
1090
|
-
constructor(getVal) {
|
|
1091
|
-
this.getVal = getVal;
|
|
1092
|
-
}
|
|
1093
|
-
setup() { return { current: this.getVal() }; }
|
|
1094
|
-
}
|
|
1095
|
-
const { ctx, run } = fieldCtx("withGetter");
|
|
1096
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1097
|
-
Compose(ArgComposable, getter("dynProp"))(undefined, ctx);
|
|
1098
|
-
const instance = new TestComponent();
|
|
1099
|
-
instance.dynProp = 99;
|
|
1100
|
-
run(instance);
|
|
1101
|
-
const view = instance.withGetter;
|
|
1102
|
-
expect(view.current).toBe(99);
|
|
1103
|
-
});
|
|
1104
|
-
it("getter() produces a live reader — reads the property at call time", () => {
|
|
1105
|
-
class ArgComposable {
|
|
1106
|
-
getVal;
|
|
1107
|
-
constructor(getVal) {
|
|
1108
|
-
this.getVal = getVal;
|
|
1109
|
-
}
|
|
1110
|
-
setup() { return { read: this.getVal }; }
|
|
1111
|
-
}
|
|
1112
|
-
const { ctx, run } = fieldCtx("liveGetter");
|
|
1113
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1114
|
-
Compose(ArgComposable, getter("mutableProp"))(undefined, ctx);
|
|
1115
|
-
const instance = new TestComponent();
|
|
1116
|
-
instance.mutableProp = "first";
|
|
1117
|
-
run(instance);
|
|
1118
|
-
const view = instance.liveGetter;
|
|
1119
|
-
expect(view.read()).toBe("first");
|
|
1120
|
-
instance.mutableProp = "second";
|
|
1121
|
-
expect(view.read()).toBe("second");
|
|
1122
|
-
});
|
|
1123
|
-
it("getter() is marked as a ComposeFactory (has __isComposeFactory)", () => {
|
|
1124
|
-
const g = getter("foo");
|
|
1125
|
-
expect(g.__isComposeFactory).toBe(true);
|
|
1126
|
-
});
|
|
1127
|
-
});
|
|
1128
|
-
// ── createFieldDecorator — additional property ────────────────────────────────
|
|
1129
|
-
describe("createFieldDecorator — additional property", () => {
|
|
1130
|
-
it("defines extra properties on the instance when additional is returned", () => {
|
|
1131
|
-
const decorator = createFieldDecorator({
|
|
1132
|
-
bind(_instance, _name, initialValue) {
|
|
1133
|
-
return {
|
|
1134
|
-
descriptor: {
|
|
1135
|
-
get() { return initialValue; },
|
|
1136
|
-
set() { },
|
|
1137
|
-
},
|
|
1138
|
-
additional: {
|
|
1139
|
-
extraProp: {
|
|
1140
|
-
configurable: true,
|
|
1141
|
-
get() { return "extra"; },
|
|
1142
|
-
},
|
|
1143
|
-
},
|
|
1144
|
-
};
|
|
1145
|
-
},
|
|
1146
|
-
});
|
|
1147
|
-
const { ctx, run } = fieldCtx("myField");
|
|
1148
|
-
decorator(undefined, ctx);
|
|
1149
|
-
const instance = new TestComponent();
|
|
1150
|
-
instance.myField = 42;
|
|
1151
|
-
run(instance);
|
|
1152
|
-
expect(instance.myField).toBe(42);
|
|
1153
|
-
expect(instance.extraProp).toBe("extra");
|
|
1154
|
-
});
|
|
1155
|
-
});
|
|
1156
|
-
// ── createGetterDecorator ─────────────────────────────────────────────────────
|
|
1157
|
-
describe("createGetterDecorator", () => {
|
|
1158
|
-
it("calls behavior.wrap on access and returns the wrapped value", () => {
|
|
1159
|
-
const wrap = vi.fn((original, instance) => {
|
|
1160
|
-
return () => original.call(instance) * 2;
|
|
1161
|
-
});
|
|
1162
|
-
const decorator = createGetterDecorator({ wrap });
|
|
1163
|
-
const originalGetter = function () { return 5; };
|
|
1164
|
-
const ctx = { kind: "getter" };
|
|
1165
|
-
const wrapped = decorator(originalGetter, ctx);
|
|
1166
|
-
const instance = {};
|
|
1167
|
-
expect(wrapped.call(instance)).toBe(10);
|
|
1168
|
-
expect(wrap).toHaveBeenCalledWith(originalGetter, instance);
|
|
1169
|
-
});
|
|
1170
|
-
it("passes the correct instance as this to behavior.wrap", () => {
|
|
1171
|
-
let capturedInstance;
|
|
1172
|
-
const decorator = createGetterDecorator({
|
|
1173
|
-
wrap(_original, instance) {
|
|
1174
|
-
capturedInstance = instance;
|
|
1175
|
-
return () => 0;
|
|
1176
|
-
},
|
|
1177
|
-
});
|
|
1178
|
-
const getter = function () { return 0; };
|
|
1179
|
-
const ctx = { kind: "getter" };
|
|
1180
|
-
const wrapped = decorator(getter, ctx);
|
|
1181
|
-
const instance = { id: "test-instance" };
|
|
1182
|
-
wrapped.call(instance);
|
|
1183
|
-
expect(capturedInstance).toBe(instance);
|
|
1184
|
-
});
|
|
1185
|
-
});
|
|
1186
|
-
// ── createAccessorDecorator — init ────────────────────────────────────────────
|
|
1187
|
-
describe("createAccessorDecorator — init", () => {
|
|
1188
|
-
it("init stores the initial value that bind receives on first get", () => {
|
|
1189
|
-
let capturedInitialValue = "NOT_SET";
|
|
1190
|
-
const behavior = {
|
|
1191
|
-
bind(_inst, _name, initialValue) {
|
|
1192
|
-
capturedInitialValue = initialValue;
|
|
1193
|
-
return { get: () => initialValue };
|
|
1194
|
-
},
|
|
1195
|
-
};
|
|
1196
|
-
const decorator = createAccessorDecorator(behavior);
|
|
1197
|
-
const target = {
|
|
1198
|
-
get() { return 0; },
|
|
1199
|
-
set() { },
|
|
1200
|
-
};
|
|
1201
|
-
const ctx = { kind: "accessor", name: "val" };
|
|
1202
|
-
const result = decorator(target, ctx);
|
|
1203
|
-
const instance = {};
|
|
1204
|
-
const returned = result.init.call(instance, 42);
|
|
1205
|
-
result.get.call(instance); // triggers bind
|
|
1206
|
-
expect(returned).toBe(42);
|
|
1207
|
-
expect(capturedInitialValue).toBe(42);
|
|
1208
|
-
});
|
|
1209
|
-
it("init returns the initial value unchanged", () => {
|
|
1210
|
-
const behavior = {
|
|
1211
|
-
bind(_inst, _name, initialValue) {
|
|
1212
|
-
return { get: () => initialValue };
|
|
1213
|
-
},
|
|
1214
|
-
};
|
|
1215
|
-
const decorator = createAccessorDecorator(behavior);
|
|
1216
|
-
const target = {
|
|
1217
|
-
get() { return ""; },
|
|
1218
|
-
set() { },
|
|
1219
|
-
};
|
|
1220
|
-
const ctx = { kind: "accessor", name: "s" };
|
|
1221
|
-
const result = decorator(target, ctx);
|
|
1222
|
-
const instance = {};
|
|
1223
|
-
expect(result.init.call(instance, "hello")).toBe("hello");
|
|
1224
|
-
});
|
|
1225
|
-
});
|
|
1226
|
-
describe("History decorator — unmount cleanup", () => {
|
|
1227
|
-
function mountHistory() {
|
|
1228
|
-
const { ctx, run } = fieldCtx("scoreHistory");
|
|
1229
|
-
History("score")(undefined, ctx);
|
|
1230
|
-
const s = signal(0);
|
|
1231
|
-
const instance = new TestComponent();
|
|
1232
|
-
Object.defineProperty(instance, "score", {
|
|
1233
|
-
get: () => s(),
|
|
1234
|
-
set: (v) => { s.set(v); },
|
|
1235
|
-
configurable: true,
|
|
1236
|
-
});
|
|
1237
|
-
run(instance);
|
|
1238
|
-
return { s, instance: instance };
|
|
1239
|
-
}
|
|
1240
|
-
it("stops recording once the component unmounts", () => {
|
|
1241
|
-
const { s, instance } = mountHistory();
|
|
1242
|
-
expect(instance.scoreHistory.values()).toEqual([0]);
|
|
1243
|
-
s.set(1);
|
|
1244
|
-
instance.onUnmount?.();
|
|
1245
|
-
s.set(2);
|
|
1246
|
-
expect(instance.scoreHistory.values()).toEqual([0, 1]);
|
|
1247
|
-
});
|
|
1248
|
-
it("unmounting before the history was ever read is a no-op", () => {
|
|
1249
|
-
const { instance } = mountHistory();
|
|
1250
|
-
expect(() => { instance.onUnmount?.(); }).not.toThrow();
|
|
1251
|
-
});
|
|
1252
|
-
});
|
|
1253
|
-
//# sourceMappingURL=properties.test.js.map
|