@praxisjs/decorators 0.4.1 → 0.4.2
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 +13 -0
- package/dist/__tests__/functions.test.d.ts +2 -0
- package/dist/__tests__/functions.test.d.ts.map +1 -0
- package/dist/__tests__/functions.test.js +270 -0
- package/dist/__tests__/functions.test.js.map +1 -0
- package/dist/__tests__/memo.test.d.ts +2 -0
- package/dist/__tests__/memo.test.d.ts.map +1 -0
- package/dist/__tests__/memo.test.js +66 -0
- package/dist/__tests__/memo.test.js.map +1 -0
- package/dist/__tests__/properties-browser.test.d.ts +2 -0
- package/dist/__tests__/properties-browser.test.d.ts.map +1 -0
- package/dist/__tests__/properties-browser.test.js +178 -0
- package/dist/__tests__/properties-browser.test.js.map +1 -0
- package/dist/__tests__/properties.test.d.ts +2 -0
- package/dist/__tests__/properties.test.d.ts.map +1 -0
- package/dist/__tests__/properties.test.js +381 -0
- package/dist/__tests__/properties.test.js.map +1 -0
- package/dist/properties/history.d.ts.map +1 -1
- package/dist/properties/history.js +4 -8
- package/dist/properties/history.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/functions.test.ts +342 -0
- package/src/__tests__/memo.test.ts +74 -0
- package/src/__tests__/properties-browser.test.ts +234 -0
- package/src/__tests__/properties.test.ts +477 -0
- package/src/properties/history.ts +4 -8
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { StatefulComponent } from "@praxisjs/core";
|
|
4
|
+
import { signal } from "@praxisjs/core/internal";
|
|
5
|
+
|
|
6
|
+
import { Watch } from "../functions/watch";
|
|
7
|
+
import { When } from "../functions/when";
|
|
8
|
+
import { Computed } from "../properties/computed";
|
|
9
|
+
import { History } from "../properties/history";
|
|
10
|
+
import { Prop } from "../properties/prop";
|
|
11
|
+
import { State } from "../properties/state";
|
|
12
|
+
|
|
13
|
+
// Helper: creates a ClassFieldDecoratorContext mock
|
|
14
|
+
function fieldCtx(name: string) {
|
|
15
|
+
const initializers: Array<(this: unknown) => void> = [];
|
|
16
|
+
return {
|
|
17
|
+
ctx: {
|
|
18
|
+
name,
|
|
19
|
+
kind: "field" as const,
|
|
20
|
+
addInitializer(fn: (this: unknown) => void) {
|
|
21
|
+
initializers.push(fn);
|
|
22
|
+
},
|
|
23
|
+
} as ClassFieldDecoratorContext,
|
|
24
|
+
run(instance: unknown) {
|
|
25
|
+
initializers.forEach((fn) => { fn.call(instance); });
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function methodCtx(name: string) {
|
|
31
|
+
const initializers: Array<(this: unknown) => void> = [];
|
|
32
|
+
return {
|
|
33
|
+
ctx: {
|
|
34
|
+
name,
|
|
35
|
+
kind: "method" as const,
|
|
36
|
+
addInitializer(fn: (this: unknown) => void) {
|
|
37
|
+
initializers.push(fn);
|
|
38
|
+
},
|
|
39
|
+
} as ClassMethodDecoratorContext,
|
|
40
|
+
run(instance: unknown) {
|
|
41
|
+
initializers.forEach((fn) => { fn.call(instance); });
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Minimal concrete component
|
|
47
|
+
class TestComponent extends StatefulComponent {
|
|
48
|
+
render() { return null; }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── State ─────────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
describe("State decorator", () => {
|
|
54
|
+
it("converts a field to a reactive getter/setter", () => {
|
|
55
|
+
const { ctx, run } = fieldCtx("count");
|
|
56
|
+
State()(undefined, ctx);
|
|
57
|
+
|
|
58
|
+
const instance = new TestComponent();
|
|
59
|
+
(instance as unknown as Record<string, unknown>).count = 10;
|
|
60
|
+
run(instance);
|
|
61
|
+
|
|
62
|
+
expect((instance as unknown as Record<string, unknown>).count).toBe(10);
|
|
63
|
+
(instance as unknown as Record<string, unknown>).count = 20;
|
|
64
|
+
expect((instance as unknown as Record<string, unknown>).count).toBe(20);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("sets _stateDirty on write", () => {
|
|
68
|
+
const { ctx, run } = fieldCtx("value");
|
|
69
|
+
State()(undefined, ctx);
|
|
70
|
+
|
|
71
|
+
const instance = new TestComponent();
|
|
72
|
+
(instance as unknown as Record<string, unknown>).value = "hello";
|
|
73
|
+
run(instance);
|
|
74
|
+
|
|
75
|
+
instance._stateDirty = false;
|
|
76
|
+
(instance as unknown as Record<string, unknown>).value = "world";
|
|
77
|
+
expect(instance._stateDirty).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ── Prop ──────────────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
describe("Prop decorator", () => {
|
|
84
|
+
it("reads from _rawProps when set by parent", () => {
|
|
85
|
+
const { ctx, run } = fieldCtx("label");
|
|
86
|
+
Prop()(undefined, ctx);
|
|
87
|
+
|
|
88
|
+
const instance = new TestComponent({ label: "hello" });
|
|
89
|
+
run(instance);
|
|
90
|
+
|
|
91
|
+
expect((instance as unknown as Record<string, unknown>).label).toBe("hello");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("falls back to default value when not in _rawProps", () => {
|
|
95
|
+
const { ctx, run } = fieldCtx("size");
|
|
96
|
+
Prop()(undefined, ctx);
|
|
97
|
+
|
|
98
|
+
const instance = new TestComponent();
|
|
99
|
+
(instance as unknown as Record<string, unknown>).size = "md"; // set before initializer runs
|
|
100
|
+
run(instance);
|
|
101
|
+
|
|
102
|
+
expect((instance as unknown as Record<string, unknown>).size).toBe("md");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("unwraps a function prop (reactive prop)", () => {
|
|
106
|
+
const { ctx, run } = fieldCtx("text");
|
|
107
|
+
Prop()(undefined, ctx);
|
|
108
|
+
|
|
109
|
+
const s = signal("reactive");
|
|
110
|
+
const instance = new TestComponent({ text: () => s() });
|
|
111
|
+
run(instance);
|
|
112
|
+
|
|
113
|
+
expect((instance as unknown as Record<string, unknown>).text).toBe("reactive");
|
|
114
|
+
s.set("updated");
|
|
115
|
+
expect((instance as unknown as Record<string, unknown>).text).toBe("updated");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ── Computed ──────────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
describe("Computed decorator", () => {
|
|
122
|
+
it("wraps a getter in a computed — result is memoized", () => {
|
|
123
|
+
const fn = vi.fn(function (this: { base: number }) {
|
|
124
|
+
return this.base * 2;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const wrappedGetter = Computed()(fn, {} as ClassGetterDecoratorContext);
|
|
128
|
+
const instance = { base: 5 } as unknown as StatefulComponent;
|
|
129
|
+
|
|
130
|
+
const result1 = wrappedGetter.call(instance);
|
|
131
|
+
const result2 = wrappedGetter.call(instance);
|
|
132
|
+
expect(result1).toBe(10);
|
|
133
|
+
expect(result2).toBe(10);
|
|
134
|
+
expect(fn).toHaveBeenCalledTimes(1); // lazy caching
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("recomputes when underlying signal changes", () => {
|
|
138
|
+
const s = signal(3);
|
|
139
|
+
const getter = function (this: object) { return s() * 2; };
|
|
140
|
+
const wrapped = Computed()(getter, {} as ClassGetterDecoratorContext);
|
|
141
|
+
const obj = {} as unknown as StatefulComponent;
|
|
142
|
+
expect(wrapped.call(obj)).toBe(6);
|
|
143
|
+
s.set(5);
|
|
144
|
+
expect(wrapped.call(obj)).toBe(10);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("is per-instance — separate computed per object", () => {
|
|
148
|
+
const s = signal(1);
|
|
149
|
+
const getter = function (this: object) { return s(); };
|
|
150
|
+
const wrapped = Computed()(getter, {} as ClassGetterDecoratorContext);
|
|
151
|
+
const a = {} as unknown as StatefulComponent;
|
|
152
|
+
const b = {} as unknown as StatefulComponent;
|
|
153
|
+
expect(wrapped.call(a)).toBe(1);
|
|
154
|
+
expect(wrapped.call(b)).toBe(1);
|
|
155
|
+
// Both share the same signal but have independent computed instances
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ── Watch ─────────────────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
describe("Watch decorator", () => {
|
|
162
|
+
it("calls handler when a single watched property changes", () => {
|
|
163
|
+
const { ctx, run } = methodCtx("onChange");
|
|
164
|
+
Watch("count" as never)(
|
|
165
|
+
function (this: unknown, newVal: unknown) {
|
|
166
|
+
(this as Record<string, unknown>).__lastVal = newVal;
|
|
167
|
+
},
|
|
168
|
+
ctx as unknown as ClassMethodDecoratorContext,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const s = signal(0);
|
|
172
|
+
const instance = new TestComponent();
|
|
173
|
+
Object.defineProperty(instance, "count", {
|
|
174
|
+
get: () => s(),
|
|
175
|
+
configurable: true,
|
|
176
|
+
});
|
|
177
|
+
run(instance);
|
|
178
|
+
|
|
179
|
+
// Simulate onMount
|
|
180
|
+
(instance as unknown as { onMount: () => void }).onMount?.();
|
|
181
|
+
|
|
182
|
+
s.set(5);
|
|
183
|
+
expect((instance as unknown as Record<string, unknown>).__lastVal).toBe(5);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("does not fire if value hasn't changed", () => {
|
|
187
|
+
const { ctx, run } = methodCtx("onNotChange");
|
|
188
|
+
const handler = vi.fn();
|
|
189
|
+
Watch("x" as never)(handler, ctx as unknown as ClassMethodDecoratorContext);
|
|
190
|
+
|
|
191
|
+
const s = signal(1);
|
|
192
|
+
const instance = new TestComponent();
|
|
193
|
+
Object.defineProperty(instance, "x", {
|
|
194
|
+
get: () => s(),
|
|
195
|
+
configurable: true,
|
|
196
|
+
});
|
|
197
|
+
run(instance);
|
|
198
|
+
(instance as unknown as { onMount: () => void }).onMount?.();
|
|
199
|
+
|
|
200
|
+
s.set(1); // same value
|
|
201
|
+
expect(handler).not.toHaveBeenCalled();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("multi-prop watch fires when any watched prop changes", () => {
|
|
205
|
+
const { ctx, run } = methodCtx("onMulti");
|
|
206
|
+
const handler = vi.fn();
|
|
207
|
+
Watch("a" as never, "b" as never)(handler, ctx as unknown as ClassMethodDecoratorContext);
|
|
208
|
+
|
|
209
|
+
const sa = signal(0);
|
|
210
|
+
const sb = signal(0);
|
|
211
|
+
const instance = new TestComponent();
|
|
212
|
+
Object.defineProperty(instance, "a", { get: () => sa(), configurable: true });
|
|
213
|
+
Object.defineProperty(instance, "b", { get: () => sb(), configurable: true });
|
|
214
|
+
run(instance);
|
|
215
|
+
(instance as unknown as { onMount: () => void }).onMount?.();
|
|
216
|
+
|
|
217
|
+
sa.set(1);
|
|
218
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
219
|
+
sb.set(1);
|
|
220
|
+
expect(handler).toHaveBeenCalledTimes(2);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ── History ───────────────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
describe("History decorator", () => {
|
|
227
|
+
it("creates a {name}History property on the instance", () => {
|
|
228
|
+
const { ctx, run } = fieldCtx("score");
|
|
229
|
+
History()(undefined, ctx);
|
|
230
|
+
const instance = new TestComponent();
|
|
231
|
+
run(instance);
|
|
232
|
+
expect((instance as unknown as Record<string, unknown>).scoreHistory).toBeDefined();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("history tracks changes to the property", () => {
|
|
236
|
+
const { ctx, run } = fieldCtx("score");
|
|
237
|
+
History()(undefined, ctx);
|
|
238
|
+
|
|
239
|
+
const s = signal(0);
|
|
240
|
+
const instance = new TestComponent();
|
|
241
|
+
Object.defineProperty(instance, "score", {
|
|
242
|
+
get: () => s(),
|
|
243
|
+
set: (v: number) => { s.set(v); },
|
|
244
|
+
configurable: true,
|
|
245
|
+
});
|
|
246
|
+
run(instance);
|
|
247
|
+
|
|
248
|
+
const h = (instance as unknown as Record<string, unknown>).scoreHistory as {
|
|
249
|
+
values: () => number[];
|
|
250
|
+
canUndo: () => boolean;
|
|
251
|
+
};
|
|
252
|
+
s.set(10);
|
|
253
|
+
expect(h.canUndo()).toBe(true);
|
|
254
|
+
expect(h.values()).toContain(10);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("canUndo becomes true after a value change", () => {
|
|
258
|
+
const { ctx, run } = fieldCtx("num");
|
|
259
|
+
History()(undefined, ctx);
|
|
260
|
+
|
|
261
|
+
const s = signal(0);
|
|
262
|
+
const instance = new TestComponent();
|
|
263
|
+
Object.defineProperty(instance, "num", {
|
|
264
|
+
get: () => s(),
|
|
265
|
+
set: (v: number) => { s.set(v); },
|
|
266
|
+
configurable: true,
|
|
267
|
+
});
|
|
268
|
+
run(instance);
|
|
269
|
+
|
|
270
|
+
const h = (instance as unknown as Record<string, unknown>).numHistory as {
|
|
271
|
+
canUndo: () => boolean;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
expect(h.canUndo()).toBe(false);
|
|
275
|
+
s.set(5);
|
|
276
|
+
expect(h.canUndo()).toBe(true);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("values() contains the current value", () => {
|
|
280
|
+
const { ctx, run } = fieldCtx("x");
|
|
281
|
+
History()(undefined, ctx);
|
|
282
|
+
|
|
283
|
+
const s = signal(42);
|
|
284
|
+
const instance = new TestComponent();
|
|
285
|
+
Object.defineProperty(instance, "x", {
|
|
286
|
+
get: () => s(),
|
|
287
|
+
set: (v: number) => { s.set(v); },
|
|
288
|
+
configurable: true,
|
|
289
|
+
});
|
|
290
|
+
run(instance);
|
|
291
|
+
|
|
292
|
+
const h = (instance as unknown as Record<string, unknown>).xHistory as {
|
|
293
|
+
values: () => number[];
|
|
294
|
+
current: () => number;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
expect(h.current()).toBe(42);
|
|
298
|
+
expect(h.values()).toContain(42);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("undo() reverts the property to the previous value", () => {
|
|
302
|
+
const { ctx, run } = fieldCtx("score2");
|
|
303
|
+
History()(undefined, ctx);
|
|
304
|
+
|
|
305
|
+
const s = signal(0);
|
|
306
|
+
const instance = new TestComponent();
|
|
307
|
+
Object.defineProperty(instance, "score2", {
|
|
308
|
+
get: () => s(),
|
|
309
|
+
set: (v: number) => { s.set(v); },
|
|
310
|
+
configurable: true,
|
|
311
|
+
});
|
|
312
|
+
run(instance);
|
|
313
|
+
|
|
314
|
+
const h = (instance as unknown as Record<string, unknown>).score2History as {
|
|
315
|
+
undo: () => void;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
s.set(5);
|
|
319
|
+
s.set(10);
|
|
320
|
+
h.undo();
|
|
321
|
+
expect(s()).toBe(5);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("redo() moves forward after undo", () => {
|
|
325
|
+
const { ctx, run } = fieldCtx("score3");
|
|
326
|
+
History()(undefined, ctx);
|
|
327
|
+
|
|
328
|
+
const s = signal(0);
|
|
329
|
+
const instance = new TestComponent();
|
|
330
|
+
Object.defineProperty(instance, "score3", {
|
|
331
|
+
get: () => s(),
|
|
332
|
+
set: (v: number) => { s.set(v); },
|
|
333
|
+
configurable: true,
|
|
334
|
+
});
|
|
335
|
+
run(instance);
|
|
336
|
+
|
|
337
|
+
const h = (instance as unknown as Record<string, unknown>).score3History as {
|
|
338
|
+
undo: () => void;
|
|
339
|
+
redo: () => void;
|
|
340
|
+
canRedo: () => boolean;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
s.set(1);
|
|
344
|
+
s.set(2);
|
|
345
|
+
h.undo();
|
|
346
|
+
expect(h.canRedo()).toBe(true);
|
|
347
|
+
h.redo();
|
|
348
|
+
expect(s()).toBe(2);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("respects a custom history limit", () => {
|
|
352
|
+
const { ctx, run } = fieldCtx("v");
|
|
353
|
+
History(3)(undefined, ctx);
|
|
354
|
+
|
|
355
|
+
const s = signal(0);
|
|
356
|
+
const instance = new TestComponent();
|
|
357
|
+
Object.defineProperty(instance, "v", {
|
|
358
|
+
get: () => s(),
|
|
359
|
+
set: (v: number) => { s.set(v); },
|
|
360
|
+
configurable: true,
|
|
361
|
+
});
|
|
362
|
+
run(instance);
|
|
363
|
+
|
|
364
|
+
const h = (instance as unknown as Record<string, unknown>).vHistory as {
|
|
365
|
+
values: () => number[];
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
s.set(1); s.set(2); s.set(3); s.set(4); s.set(5);
|
|
369
|
+
// values includes current + up to 3 past items → max 4 items
|
|
370
|
+
expect(h.values().length).toBeLessThanOrEqual(4);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("returns the same history instance on repeated access", () => {
|
|
374
|
+
const { ctx, run } = fieldCtx("z");
|
|
375
|
+
History()(undefined, ctx);
|
|
376
|
+
const instance = new TestComponent();
|
|
377
|
+
run(instance);
|
|
378
|
+
const h1 = (instance as unknown as Record<string, unknown>).zHistory;
|
|
379
|
+
const h2 = (instance as unknown as Record<string, unknown>).zHistory;
|
|
380
|
+
expect(h1).toBe(h2);
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// ── When ──────────────────────────────────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
describe("When decorator", () => {
|
|
387
|
+
function methodCtxFor(name: string) {
|
|
388
|
+
const initializers: Array<(this: unknown) => void> = [];
|
|
389
|
+
return {
|
|
390
|
+
ctx: {
|
|
391
|
+
name,
|
|
392
|
+
kind: "method" as const,
|
|
393
|
+
addInitializer(fn: (this: unknown) => void) {
|
|
394
|
+
initializers.push(fn);
|
|
395
|
+
},
|
|
396
|
+
} as ClassMethodDecoratorContext,
|
|
397
|
+
run(instance: unknown) {
|
|
398
|
+
initializers.forEach((fn) => { fn.call(instance); });
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
it("calls the method when a truthy value is emitted from the watched prop", () => {
|
|
404
|
+
const { ctx, run } = methodCtxFor("onReady");
|
|
405
|
+
const handler = vi.fn();
|
|
406
|
+
When("flag")(handler, ctx as unknown as ClassMethodDecoratorContext);
|
|
407
|
+
|
|
408
|
+
// @When accesses instance[propName] and expects a Signal/Computed object
|
|
409
|
+
const s = signal<string | null>(null);
|
|
410
|
+
const instance = new TestComponent();
|
|
411
|
+
(instance as unknown as Record<string, unknown>).flag = s;
|
|
412
|
+
run(instance);
|
|
413
|
+
|
|
414
|
+
(instance as unknown as { onMount: () => void }).onMount?.();
|
|
415
|
+
|
|
416
|
+
s.set("go");
|
|
417
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("does not call the method for falsy values", () => {
|
|
421
|
+
const { ctx, run } = methodCtxFor("onData");
|
|
422
|
+
const handler = vi.fn();
|
|
423
|
+
When("item")(handler, ctx as unknown as ClassMethodDecoratorContext);
|
|
424
|
+
|
|
425
|
+
const s = signal<number>(0);
|
|
426
|
+
const instance = new TestComponent();
|
|
427
|
+
(instance as unknown as Record<string, unknown>).item = s;
|
|
428
|
+
run(instance);
|
|
429
|
+
(instance as unknown as { onMount: () => void }).onMount?.();
|
|
430
|
+
|
|
431
|
+
// 0 is falsy — handler should NOT be called
|
|
432
|
+
expect(handler).not.toHaveBeenCalled();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("calls onUnmount cleanup without throwing", () => {
|
|
436
|
+
const { ctx, run } = methodCtxFor("onVal");
|
|
437
|
+
When("val")(vi.fn(), ctx as unknown as ClassMethodDecoratorContext);
|
|
438
|
+
|
|
439
|
+
const s = signal<number>(0);
|
|
440
|
+
const instance = new TestComponent();
|
|
441
|
+
(instance as unknown as Record<string, unknown>).val = s;
|
|
442
|
+
run(instance);
|
|
443
|
+
(instance as unknown as { onMount: () => void }).onMount?.();
|
|
444
|
+
expect(() =>
|
|
445
|
+
{ (instance as unknown as { onUnmount: () => void }).onUnmount?.(); }
|
|
446
|
+
).not.toThrow();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("preserves existing onMount/onUnmount implementations", () => {
|
|
450
|
+
const { ctx, run } = methodCtxFor("onV");
|
|
451
|
+
When("v")(vi.fn(), ctx as unknown as ClassMethodDecoratorContext);
|
|
452
|
+
|
|
453
|
+
const mountSpy = vi.fn();
|
|
454
|
+
const unmountSpy = vi.fn();
|
|
455
|
+
const s = signal<number>(0);
|
|
456
|
+
const instance = new TestComponent();
|
|
457
|
+
(instance as unknown as Record<string, unknown>).v = s;
|
|
458
|
+
run(instance);
|
|
459
|
+
|
|
460
|
+
// Manually set callbacks after initializer ran
|
|
461
|
+
const prevMount = instance.onMount?.bind(instance);
|
|
462
|
+
instance.onMount = function () {
|
|
463
|
+
mountSpy();
|
|
464
|
+
prevMount?.();
|
|
465
|
+
};
|
|
466
|
+
const prevUnmount = instance.onUnmount?.bind(instance);
|
|
467
|
+
instance.onUnmount = function () {
|
|
468
|
+
unmountSpy();
|
|
469
|
+
prevUnmount?.();
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
instance.onMount?.();
|
|
473
|
+
instance.onUnmount?.();
|
|
474
|
+
expect(mountSpy).toHaveBeenCalled();
|
|
475
|
+
expect(unmountSpy).toHaveBeenCalled();
|
|
476
|
+
});
|
|
477
|
+
});
|
|
@@ -26,22 +26,18 @@ export function History(limit = 50) {
|
|
|
26
26
|
|
|
27
27
|
const h = history(source as Signal<unknown>, limit);
|
|
28
28
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
};
|
|
32
|
-
const originalRedo = () => {
|
|
33
|
-
h.redo();
|
|
34
|
-
};
|
|
29
|
+
const _undo = h.undo.bind(h);
|
|
30
|
+
const _redo = h.redo.bind(h);
|
|
35
31
|
|
|
36
32
|
h.undo = () => {
|
|
37
33
|
const prev = h.values()[h.values().length - 2];
|
|
38
34
|
if (prev === undefined) return;
|
|
39
|
-
|
|
35
|
+
_undo();
|
|
40
36
|
this[propertyKey] = prev;
|
|
41
37
|
};
|
|
42
38
|
|
|
43
39
|
h.redo = () => {
|
|
44
|
-
|
|
40
|
+
_redo();
|
|
45
41
|
this[propertyKey] = h.current();
|
|
46
42
|
};
|
|
47
43
|
|