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