@praxisjs/decorators 1.6.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/component/component.d.ts +1 -1
  3. package/dist/component/component.d.ts.map +1 -1
  4. package/dist/component/component.js +8 -15
  5. package/dist/component/component.js.map +1 -1
  6. package/dist/component/lazy.d.ts +1 -1
  7. package/dist/component/lazy.d.ts.map +1 -1
  8. package/dist/component/lazy.js +44 -36
  9. package/dist/component/lazy.js.map +1 -1
  10. package/dist/create-class-decorator.d.ts +27 -5
  11. package/dist/create-class-decorator.d.ts.map +1 -1
  12. package/dist/create-class-decorator.js +53 -18
  13. package/dist/create-class-decorator.js.map +1 -1
  14. package/dist/create-field-decorator.d.ts +42 -3
  15. package/dist/create-field-decorator.d.ts.map +1 -1
  16. package/dist/create-field-decorator.js +63 -27
  17. package/dist/create-field-decorator.js.map +1 -1
  18. package/dist/create-getter-decorator.d.ts +81 -16
  19. package/dist/create-getter-decorator.d.ts.map +1 -1
  20. package/dist/create-getter-decorator.js +127 -15
  21. package/dist/create-getter-decorator.js.map +1 -1
  22. package/dist/create-lifecycle-method-decorator.d.ts +21 -3
  23. package/dist/create-lifecycle-method-decorator.d.ts.map +1 -1
  24. package/dist/create-lifecycle-method-decorator.js +25 -18
  25. package/dist/create-lifecycle-method-decorator.js.map +1 -1
  26. package/dist/create-method-decorator.d.ts +43 -3
  27. package/dist/create-method-decorator.d.ts.map +1 -1
  28. package/dist/create-method-decorator.js +49 -8
  29. package/dist/create-method-decorator.js.map +1 -1
  30. package/dist/events/command.d.ts +2 -1
  31. package/dist/events/command.d.ts.map +1 -1
  32. package/dist/events/command.js +10 -8
  33. package/dist/events/command.js.map +1 -1
  34. package/dist/events/emit.d.ts +2 -2
  35. package/dist/events/emit.d.ts.map +1 -1
  36. package/dist/events/emit.js +18 -24
  37. package/dist/events/emit.js.map +1 -1
  38. package/dist/events/on-command.d.ts +2 -1
  39. package/dist/events/on-command.d.ts.map +1 -1
  40. package/dist/events/on-command.js +12 -14
  41. package/dist/events/on-command.js.map +1 -1
  42. package/dist/field.d.ts +51 -0
  43. package/dist/field.d.ts.map +1 -0
  44. package/dist/field.js +82 -0
  45. package/dist/field.js.map +1 -0
  46. package/dist/functions/bind.d.ts +2 -1
  47. package/dist/functions/bind.d.ts.map +1 -1
  48. package/dist/functions/bind.js +4 -4
  49. package/dist/functions/bind.js.map +1 -1
  50. package/dist/functions/debounce.d.ts +2 -2
  51. package/dist/functions/debounce.d.ts.map +1 -1
  52. package/dist/functions/debounce.js +25 -23
  53. package/dist/functions/debounce.js.map +1 -1
  54. package/dist/functions/index.d.ts +1 -1
  55. package/dist/functions/index.d.ts.map +1 -1
  56. package/dist/functions/index.js.map +1 -1
  57. package/dist/functions/log.d.ts +2 -1
  58. package/dist/functions/log.d.ts.map +1 -1
  59. package/dist/functions/log.js +38 -40
  60. package/dist/functions/log.js.map +1 -1
  61. package/dist/functions/memo.d.ts +9 -1
  62. package/dist/functions/memo.d.ts.map +1 -1
  63. package/dist/functions/memo.js +44 -30
  64. package/dist/functions/memo.js.map +1 -1
  65. package/dist/functions/once.d.ts +2 -1
  66. package/dist/functions/once.d.ts.map +1 -1
  67. package/dist/functions/once.js +10 -14
  68. package/dist/functions/once.js.map +1 -1
  69. package/dist/functions/retry.d.ts +2 -1
  70. package/dist/functions/retry.d.ts.map +1 -1
  71. package/dist/functions/retry.js +21 -23
  72. package/dist/functions/retry.js.map +1 -1
  73. package/dist/functions/throttle.d.ts +2 -1
  74. package/dist/functions/throttle.d.ts.map +1 -1
  75. package/dist/functions/throttle.js +12 -14
  76. package/dist/functions/throttle.js.map +1 -1
  77. package/dist/functions/until.d.ts +2 -1
  78. package/dist/functions/until.d.ts.map +1 -1
  79. package/dist/functions/until.js +13 -14
  80. package/dist/functions/until.js.map +1 -1
  81. package/dist/functions/watch.d.ts +1 -1
  82. package/dist/functions/watch.d.ts.map +1 -1
  83. package/dist/functions/watch.js +36 -34
  84. package/dist/functions/watch.js.map +1 -1
  85. package/dist/functions/when.d.ts +1 -1
  86. package/dist/functions/when.d.ts.map +1 -1
  87. package/dist/functions/when.js +19 -21
  88. package/dist/functions/when.js.map +1 -1
  89. package/dist/index.d.ts +6 -6
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +2 -2
  92. package/dist/index.js.map +1 -1
  93. package/dist/lifecycle.d.ts +24 -0
  94. package/dist/lifecycle.d.ts.map +1 -0
  95. package/dist/lifecycle.js +55 -0
  96. package/dist/lifecycle.js.map +1 -0
  97. package/dist/properties/compose.d.ts +3 -2
  98. package/dist/properties/compose.d.ts.map +1 -1
  99. package/dist/properties/compose.js +33 -19
  100. package/dist/properties/compose.js.map +1 -1
  101. package/dist/properties/computed.d.ts +12 -10
  102. package/dist/properties/computed.d.ts.map +1 -1
  103. package/dist/properties/computed.js +26 -21
  104. package/dist/properties/computed.js.map +1 -1
  105. package/dist/properties/deep-state.d.ts +2 -1
  106. package/dist/properties/deep-state.d.ts.map +1 -1
  107. package/dist/properties/deep-state.js +66 -49
  108. package/dist/properties/deep-state.js.map +1 -1
  109. package/dist/properties/function-prop.d.ts +2 -1
  110. package/dist/properties/function-prop.d.ts.map +1 -1
  111. package/dist/properties/function-prop.js +20 -22
  112. package/dist/properties/function-prop.js.map +1 -1
  113. package/dist/properties/history.d.ts +2 -2
  114. package/dist/properties/history.d.ts.map +1 -1
  115. package/dist/properties/history.js +34 -31
  116. package/dist/properties/history.js.map +1 -1
  117. package/dist/properties/index.d.ts +1 -0
  118. package/dist/properties/index.d.ts.map +1 -1
  119. package/dist/properties/index.js +1 -0
  120. package/dist/properties/index.js.map +1 -1
  121. package/dist/properties/persisted.d.ts +2 -1
  122. package/dist/properties/persisted.d.ts.map +1 -1
  123. package/dist/properties/persisted.js +30 -19
  124. package/dist/properties/persisted.js.map +1 -1
  125. package/dist/properties/prop.d.ts +2 -1
  126. package/dist/properties/prop.d.ts.map +1 -1
  127. package/dist/properties/prop.js +26 -22
  128. package/dist/properties/prop.js.map +1 -1
  129. package/dist/properties/ref.d.ts +2 -1
  130. package/dist/properties/ref.d.ts.map +1 -1
  131. package/dist/properties/ref.js +11 -8
  132. package/dist/properties/ref.js.map +1 -1
  133. package/dist/properties/resource.d.ts.map +1 -1
  134. package/dist/properties/resource.js +21 -16
  135. package/dist/properties/resource.js.map +1 -1
  136. package/dist/properties/selector.d.ts +12 -0
  137. package/dist/properties/selector.d.ts.map +1 -0
  138. package/dist/properties/selector.js +25 -0
  139. package/dist/properties/selector.js.map +1 -0
  140. package/dist/properties/slot.d.ts +2 -1
  141. package/dist/properties/slot.d.ts.map +1 -1
  142. package/dist/properties/slot.js +22 -20
  143. package/dist/properties/slot.js.map +1 -1
  144. package/dist/properties/state.d.ts.map +1 -1
  145. package/dist/properties/state.js +27 -16
  146. package/dist/properties/state.js.map +1 -1
  147. package/dist/properties/synced.d.ts +2 -1
  148. package/dist/properties/synced.d.ts.map +1 -1
  149. package/dist/properties/synced.js +27 -19
  150. package/dist/properties/synced.js.map +1 -1
  151. package/dist/shared-method.d.ts +14 -0
  152. package/dist/shared-method.d.ts.map +1 -0
  153. package/dist/shared-method.js +87 -0
  154. package/dist/shared-method.js.map +1 -0
  155. package/package.json +14 -5
  156. package/src/component/component.ts +8 -27
  157. package/src/component/lazy.ts +51 -41
  158. package/src/create-class-decorator.ts +90 -31
  159. package/src/create-field-decorator.ts +120 -43
  160. package/src/create-getter-decorator.ts +263 -66
  161. package/src/create-lifecycle-method-decorator.ts +50 -28
  162. package/src/create-method-decorator.ts +119 -13
  163. package/src/events/command.ts +11 -9
  164. package/src/events/emit.ts +16 -22
  165. package/src/events/on-command.ts +17 -18
  166. package/src/field.ts +116 -0
  167. package/src/functions/bind.ts +4 -4
  168. package/src/functions/debounce.ts +25 -23
  169. package/src/functions/index.ts +1 -1
  170. package/src/functions/log.ts +44 -46
  171. package/src/functions/memo.ts +54 -28
  172. package/src/functions/once.ts +10 -13
  173. package/src/functions/retry.ts +19 -21
  174. package/src/functions/throttle.ts +12 -13
  175. package/src/functions/until.ts +14 -14
  176. package/src/functions/watch.ts +37 -36
  177. package/src/functions/when.ts +18 -20
  178. package/src/index.ts +13 -5
  179. package/src/lifecycle.ts +74 -0
  180. package/src/properties/compose.ts +33 -20
  181. package/src/properties/computed.ts +35 -45
  182. package/src/properties/deep-state.ts +68 -53
  183. package/src/properties/function-prop.ts +25 -27
  184. package/src/properties/history.ts +34 -33
  185. package/src/properties/index.ts +1 -0
  186. package/src/properties/persisted.ts +41 -27
  187. package/src/properties/prop.ts +30 -22
  188. package/src/properties/ref.ts +12 -10
  189. package/src/properties/resource.ts +22 -17
  190. package/src/properties/selector.ts +45 -0
  191. package/src/properties/slot.ts +28 -22
  192. package/src/properties/state.ts +32 -16
  193. package/src/properties/synced.ts +31 -19
  194. package/src/shared-method.ts +101 -0
  195. package/dist/__tests__/component.test.d.ts +0 -2
  196. package/dist/__tests__/component.test.d.ts.map +0 -1
  197. package/dist/__tests__/component.test.js +0 -147
  198. package/dist/__tests__/component.test.js.map +0 -1
  199. package/dist/__tests__/compose.test.d.ts +0 -2
  200. package/dist/__tests__/compose.test.d.ts.map +0 -1
  201. package/dist/__tests__/compose.test.js +0 -164
  202. package/dist/__tests__/compose.test.js.map +0 -1
  203. package/dist/__tests__/deep-state.test.d.ts +0 -2
  204. package/dist/__tests__/deep-state.test.d.ts.map +0 -1
  205. package/dist/__tests__/deep-state.test.js +0 -150
  206. package/dist/__tests__/deep-state.test.js.map +0 -1
  207. package/dist/__tests__/events.test.d.ts +0 -2
  208. package/dist/__tests__/events.test.d.ts.map +0 -1
  209. package/dist/__tests__/events.test.js +0 -295
  210. package/dist/__tests__/events.test.js.map +0 -1
  211. package/dist/__tests__/functions.test.d.ts +0 -2
  212. package/dist/__tests__/functions.test.d.ts.map +0 -1
  213. package/dist/__tests__/functions.test.js +0 -618
  214. package/dist/__tests__/functions.test.js.map +0 -1
  215. package/dist/__tests__/lazy.test.d.ts +0 -2
  216. package/dist/__tests__/lazy.test.d.ts.map +0 -1
  217. package/dist/__tests__/lazy.test.js +0 -227
  218. package/dist/__tests__/lazy.test.js.map +0 -1
  219. package/dist/__tests__/memo.test.d.ts +0 -2
  220. package/dist/__tests__/memo.test.d.ts.map +0 -1
  221. package/dist/__tests__/memo.test.js +0 -168
  222. package/dist/__tests__/memo.test.js.map +0 -1
  223. package/dist/__tests__/properties-browser.test.d.ts +0 -2
  224. package/dist/__tests__/properties-browser.test.d.ts.map +0 -1
  225. package/dist/__tests__/properties-browser.test.js +0 -178
  226. package/dist/__tests__/properties-browser.test.js.map +0 -1
  227. package/dist/__tests__/properties.test.d.ts +0 -2
  228. package/dist/__tests__/properties.test.d.ts.map +0 -1
  229. package/dist/__tests__/properties.test.js +0 -1226
  230. package/dist/__tests__/properties.test.js.map +0 -1
  231. package/dist/__tests__/ref.test.d.ts +0 -2
  232. package/dist/__tests__/ref.test.d.ts.map +0 -1
  233. package/dist/__tests__/ref.test.js +0 -106
  234. package/dist/__tests__/ref.test.js.map +0 -1
  235. package/dist/__tests__/resource.test.d.ts +0 -2
  236. package/dist/__tests__/resource.test.d.ts.map +0 -1
  237. package/dist/__tests__/resource.test.js +0 -217
  238. package/dist/__tests__/resource.test.js.map +0 -1
  239. package/dist/__tests__/slot.test.d.ts +0 -2
  240. package/dist/__tests__/slot.test.d.ts.map +0 -1
  241. package/dist/__tests__/slot.test.js +0 -147
  242. package/dist/__tests__/slot.test.js.map +0 -1
  243. package/dist/__tests__/synced.test.d.ts +0 -2
  244. package/dist/__tests__/synced.test.d.ts.map +0 -1
  245. package/dist/__tests__/synced.test.js +0 -161
  246. package/dist/__tests__/synced.test.js.map +0 -1
  247. package/dist/__tests__/until.test.d.ts +0 -2
  248. package/dist/__tests__/until.test.d.ts.map +0 -1
  249. package/dist/__tests__/until.test.js +0 -88
  250. package/dist/__tests__/until.test.js.map +0 -1
  251. package/dist/create-accessor-decorator.d.ts +0 -14
  252. package/dist/create-accessor-decorator.d.ts.map +0 -1
  253. package/dist/create-accessor-decorator.js +0 -27
  254. package/dist/create-accessor-decorator.js.map +0 -1
  255. package/src/__tests__/component.test.ts +0 -167
  256. package/src/__tests__/compose.test.ts +0 -202
  257. package/src/__tests__/deep-state.test.ts +0 -185
  258. package/src/__tests__/events.test.ts +0 -366
  259. package/src/__tests__/functions.test.ts +0 -803
  260. package/src/__tests__/lazy.test.ts +0 -296
  261. package/src/__tests__/memo.test.ts +0 -208
  262. package/src/__tests__/properties-browser.test.ts +0 -234
  263. package/src/__tests__/properties.test.ts +0 -1526
  264. package/src/__tests__/ref.test.ts +0 -131
  265. package/src/__tests__/resource.test.ts +0 -246
  266. package/src/__tests__/slot.test.ts +0 -179
  267. package/src/__tests__/synced.test.ts +0 -193
  268. package/src/__tests__/until.test.ts +0 -113
  269. package/src/create-accessor-decorator.ts +0 -45
  270. package/tsconfig.json +0 -9
@@ -1,1526 +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
- });