@praxisjs/decorators 0.4.2 → 0.5.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.
- package/CHANGELOG.md +43 -0
- package/dist/__tests__/component.test.d.ts +2 -0
- package/dist/__tests__/component.test.d.ts.map +1 -0
- package/dist/__tests__/component.test.js +146 -0
- package/dist/__tests__/component.test.js.map +1 -0
- package/dist/__tests__/compose.test.d.ts +2 -0
- package/dist/__tests__/compose.test.d.ts.map +1 -0
- package/dist/__tests__/compose.test.js +171 -0
- package/dist/__tests__/compose.test.js.map +1 -0
- package/dist/__tests__/events.test.d.ts +2 -0
- package/dist/__tests__/events.test.d.ts.map +1 -0
- package/dist/__tests__/events.test.js +216 -0
- package/dist/__tests__/events.test.js.map +1 -0
- package/dist/__tests__/functions.test.js +283 -31
- package/dist/__tests__/functions.test.js.map +1 -1
- package/dist/__tests__/lazy.test.d.ts +2 -0
- package/dist/__tests__/lazy.test.d.ts.map +1 -0
- package/dist/__tests__/lazy.test.js +163 -0
- package/dist/__tests__/lazy.test.js.map +1 -0
- package/dist/__tests__/memo.test.js +71 -22
- package/dist/__tests__/memo.test.js.map +1 -1
- package/dist/__tests__/properties.test.js +191 -1
- package/dist/__tests__/properties.test.js.map +1 -1
- package/dist/__tests__/resource.test.d.ts +2 -0
- package/dist/__tests__/resource.test.d.ts.map +1 -0
- package/dist/__tests__/resource.test.js +103 -0
- package/dist/__tests__/resource.test.js.map +1 -0
- package/dist/__tests__/slot.test.d.ts +2 -0
- package/dist/__tests__/slot.test.d.ts.map +1 -0
- package/dist/__tests__/slot.test.js +147 -0
- package/dist/__tests__/slot.test.js.map +1 -0
- package/dist/__tests__/virtual.test.d.ts +2 -0
- package/dist/__tests__/virtual.test.d.ts.map +1 -0
- package/dist/__tests__/virtual.test.js +242 -0
- package/dist/__tests__/virtual.test.js.map +1 -0
- package/dist/component/component.d.ts +1 -1
- package/dist/component/component.d.ts.map +1 -1
- package/dist/component/component.js +15 -10
- package/dist/component/component.js.map +1 -1
- package/dist/component/lazy.d.ts +1 -1
- package/dist/component/lazy.d.ts.map +1 -1
- package/dist/component/lazy.js +29 -27
- package/dist/component/lazy.js.map +1 -1
- package/dist/component/virtual.d.ts +1 -1
- package/dist/component/virtual.d.ts.map +1 -1
- package/dist/component/virtual.js +46 -50
- package/dist/component/virtual.js.map +1 -1
- package/dist/create-class-decorator.d.ts +16 -0
- package/dist/create-class-decorator.d.ts.map +1 -0
- package/dist/create-class-decorator.js +35 -0
- package/dist/create-class-decorator.js.map +1 -0
- package/dist/create-field-decorator.d.ts +14 -0
- package/dist/create-field-decorator.d.ts.map +1 -0
- package/dist/create-field-decorator.js +37 -0
- package/dist/create-field-decorator.js.map +1 -0
- package/dist/create-getter-decorator.d.ts +18 -0
- package/dist/create-getter-decorator.d.ts.map +1 -0
- package/dist/create-getter-decorator.js +15 -0
- package/dist/create-getter-decorator.js.map +1 -0
- package/dist/create-lifecycle-method-decorator.d.ts +6 -0
- package/dist/create-lifecycle-method-decorator.d.ts.map +1 -0
- package/dist/create-lifecycle-method-decorator.js +22 -0
- package/dist/create-lifecycle-method-decorator.js.map +1 -0
- package/dist/create-method-decorator.d.ts +6 -0
- package/dist/create-method-decorator.d.ts.map +1 -0
- package/dist/create-method-decorator.js +14 -0
- package/dist/create-method-decorator.js.map +1 -0
- package/dist/events/emit.d.ts +1 -1
- package/dist/events/emit.d.ts.map +1 -1
- package/dist/events/emit.js +24 -29
- package/dist/events/emit.js.map +1 -1
- package/dist/events/on-command.d.ts +1 -2
- package/dist/events/on-command.d.ts.map +1 -1
- package/dist/events/on-command.js +15 -32
- package/dist/events/on-command.js.map +1 -1
- package/dist/functions/bind.d.ts +1 -2
- package/dist/functions/bind.d.ts.map +1 -1
- package/dist/functions/bind.js +4 -11
- package/dist/functions/bind.js.map +1 -1
- package/dist/functions/debounce.d.ts +1 -2
- package/dist/functions/debounce.d.ts.map +1 -1
- package/dist/functions/debounce.js +12 -20
- package/dist/functions/debounce.js.map +1 -1
- package/dist/functions/log.d.ts +1 -2
- package/dist/functions/log.d.ts.map +1 -1
- package/dist/functions/log.js +40 -38
- package/dist/functions/log.js.map +1 -1
- package/dist/functions/memo.d.ts +1 -2
- package/dist/functions/memo.d.ts.map +1 -1
- package/dist/functions/memo.js +15 -27
- package/dist/functions/memo.js.map +1 -1
- package/dist/functions/once.d.ts +1 -2
- package/dist/functions/once.d.ts.map +1 -1
- package/dist/functions/once.js +14 -12
- package/dist/functions/once.js.map +1 -1
- package/dist/functions/retry.d.ts +1 -2
- package/dist/functions/retry.d.ts.map +1 -1
- package/dist/functions/retry.js +23 -20
- package/dist/functions/retry.js.map +1 -1
- package/dist/functions/throttle.d.ts +1 -2
- package/dist/functions/throttle.d.ts.map +1 -1
- package/dist/functions/throttle.js +9 -16
- package/dist/functions/throttle.js.map +1 -1
- package/dist/functions/watch.d.ts +1 -1
- package/dist/functions/watch.d.ts.map +1 -1
- package/dist/functions/watch.js +25 -31
- package/dist/functions/watch.js.map +1 -1
- package/dist/functions/when.d.ts +1 -2
- package/dist/functions/when.d.ts.map +1 -1
- package/dist/functions/when.js +10 -26
- package/dist/functions/when.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/properties/compose.d.ts +3 -0
- package/dist/properties/compose.d.ts.map +1 -0
- package/dist/properties/compose.js +40 -0
- package/dist/properties/compose.js.map +1 -0
- package/dist/properties/computed.d.ts +1 -2
- package/dist/properties/computed.d.ts.map +1 -1
- package/dist/properties/computed.js +10 -9
- package/dist/properties/computed.js.map +1 -1
- package/dist/properties/history.d.ts.map +1 -1
- package/dist/properties/history.js +37 -32
- package/dist/properties/history.js.map +1 -1
- package/dist/properties/index.d.ts +2 -0
- package/dist/properties/index.d.ts.map +1 -1
- package/dist/properties/index.js +2 -0
- package/dist/properties/index.js.map +1 -1
- package/dist/properties/persisted.d.ts +1 -2
- package/dist/properties/persisted.d.ts.map +1 -1
- package/dist/properties/persisted.js +13 -29
- package/dist/properties/persisted.js.map +1 -1
- package/dist/properties/prop.d.ts +1 -2
- package/dist/properties/prop.d.ts.map +1 -1
- package/dist/properties/prop.js +21 -25
- package/dist/properties/prop.js.map +1 -1
- package/dist/properties/resource.d.ts +14 -0
- package/dist/properties/resource.d.ts.map +1 -0
- package/dist/properties/resource.js +27 -0
- package/dist/properties/resource.js.map +1 -0
- package/dist/properties/slot.d.ts +1 -2
- package/dist/properties/slot.d.ts.map +1 -1
- package/dist/properties/slot.js +15 -20
- package/dist/properties/slot.js.map +1 -1
- package/dist/properties/state.d.ts +1 -2
- package/dist/properties/state.d.ts.map +1 -1
- package/dist/properties/state.js +13 -18
- package/dist/properties/state.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/component.test.ts +166 -0
- package/src/__tests__/compose.test.ts +202 -0
- package/src/__tests__/events.test.ts +268 -0
- package/src/__tests__/functions.test.ts +368 -30
- package/src/__tests__/lazy.test.ts +218 -0
- package/src/__tests__/memo.test.ts +89 -22
- package/src/__tests__/properties.test.ts +249 -1
- package/src/__tests__/resource.test.ts +113 -0
- package/src/__tests__/slot.test.ts +179 -0
- package/src/__tests__/virtual.test.ts +260 -0
- package/src/component/component.ts +27 -12
- package/src/component/lazy.ts +33 -35
- package/src/component/virtual.tsx +62 -75
- package/src/create-class-decorator.ts +61 -0
- package/src/create-field-decorator.ts +66 -0
- package/src/create-getter-decorator.ts +52 -0
- package/src/create-lifecycle-method-decorator.ts +38 -0
- package/src/create-method-decorator.ts +26 -0
- package/src/events/emit.ts +22 -33
- package/src/events/on-command.ts +22 -47
- package/src/functions/bind.ts +4 -15
- package/src/functions/debounce.ts +11 -26
- package/src/functions/log.ts +40 -43
- package/src/functions/memo.ts +15 -45
- package/src/functions/once.ts +13 -16
- package/src/functions/retry.ts +21 -22
- package/src/functions/throttle.ts +9 -23
- package/src/functions/watch.ts +27 -34
- package/src/functions/when.ts +11 -36
- package/src/index.ts +28 -0
- package/src/properties/compose.ts +53 -0
- package/src/properties/computed.ts +11 -13
- package/src/properties/history.ts +42 -41
- package/src/properties/index.ts +2 -0
- package/src/properties/persisted.ts +17 -58
- package/src/properties/prop.ts +21 -31
- package/src/properties/resource.ts +34 -0
- package/src/properties/slot.ts +17 -30
- package/src/properties/state.ts +14 -24
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { beforeEach, afterEach, describe, it, expect, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { StatefulComponent } from "@praxisjs/core";
|
|
5
|
+
import { Lazy } from "../component/lazy";
|
|
6
|
+
|
|
7
|
+
type AnyConstructor = new (...args: unknown[]) => StatefulComponent;
|
|
8
|
+
|
|
9
|
+
function applyLazy<T extends AnyConstructor>(cls: T, placeholder?: number): T {
|
|
10
|
+
return placeholder !== undefined
|
|
11
|
+
? Lazy(placeholder)(cls, {} as ClassDecoratorContext)
|
|
12
|
+
: Lazy()(cls, {} as ClassDecoratorContext);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class BaseComp extends StatefulComponent {
|
|
16
|
+
render() {
|
|
17
|
+
return document.createTextNode("content");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("Lazy decorator", () => {
|
|
22
|
+
let intersectionCallback: IntersectionObserverCallback = () => {};
|
|
23
|
+
let mockObserve: ReturnType<typeof vi.fn>;
|
|
24
|
+
let mockDisconnect: ReturnType<typeof vi.fn>;
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
intersectionCallback = () => {};
|
|
28
|
+
mockObserve = vi.fn();
|
|
29
|
+
mockDisconnect = vi.fn();
|
|
30
|
+
|
|
31
|
+
globalThis.IntersectionObserver = class {
|
|
32
|
+
constructor(cb: IntersectionObserverCallback) {
|
|
33
|
+
intersectionCallback = cb;
|
|
34
|
+
}
|
|
35
|
+
observe = mockObserve;
|
|
36
|
+
disconnect = mockDisconnect;
|
|
37
|
+
} as unknown as typeof IntersectionObserver;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
// @ts-expect-error – removing test stub
|
|
42
|
+
delete globalThis.IntersectionObserver;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns null from render before visibility", () => {
|
|
46
|
+
const Wrapped = applyLazy(BaseComp as AnyConstructor);
|
|
47
|
+
const instance = new Wrapped();
|
|
48
|
+
expect(instance.render()).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("sets _lazyVisible to true when IntersectionObserver is not supported", () => {
|
|
52
|
+
// Remove IntersectionObserver to simulate unsupported environment
|
|
53
|
+
// @ts-expect-error – removing test stub
|
|
54
|
+
delete globalThis.IntersectionObserver;
|
|
55
|
+
|
|
56
|
+
const Wrapped = applyLazy(BaseComp as AnyConstructor);
|
|
57
|
+
const instance = new Wrapped();
|
|
58
|
+
|
|
59
|
+
// Simulate anchor with parent element
|
|
60
|
+
const parent = document.createElement("div");
|
|
61
|
+
document.body.appendChild(parent);
|
|
62
|
+
const anchor = document.createComment("end");
|
|
63
|
+
parent.appendChild(anchor);
|
|
64
|
+
(instance as unknown as { _anchor: Comment })._anchor = anchor;
|
|
65
|
+
|
|
66
|
+
instance.onMount?.();
|
|
67
|
+
// Without IntersectionObserver, component should immediately become visible
|
|
68
|
+
expect(instance.render()).not.toBeNull();
|
|
69
|
+
document.body.removeChild(parent);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("sets minHeight placeholder on mount", () => {
|
|
73
|
+
const Wrapped = applyLazy(BaseComp as AnyConstructor, 300);
|
|
74
|
+
const instance = new Wrapped();
|
|
75
|
+
|
|
76
|
+
const parent = document.createElement("div");
|
|
77
|
+
document.body.appendChild(parent);
|
|
78
|
+
const anchor = document.createComment("end");
|
|
79
|
+
parent.appendChild(anchor);
|
|
80
|
+
(instance as unknown as { _anchor: Comment })._anchor = anchor;
|
|
81
|
+
|
|
82
|
+
instance.onMount?.();
|
|
83
|
+
expect(parent.style.minHeight).toBe("300px");
|
|
84
|
+
document.body.removeChild(parent);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("sets _lazyVisible and clears minHeight when intersecting", () => {
|
|
88
|
+
const Wrapped = applyLazy(BaseComp as AnyConstructor, 200);
|
|
89
|
+
const instance = new Wrapped();
|
|
90
|
+
|
|
91
|
+
const parent = document.createElement("div");
|
|
92
|
+
document.body.appendChild(parent);
|
|
93
|
+
const anchor = document.createComment("end");
|
|
94
|
+
parent.appendChild(anchor);
|
|
95
|
+
(instance as unknown as { _anchor: Comment })._anchor = anchor;
|
|
96
|
+
|
|
97
|
+
instance.onMount?.();
|
|
98
|
+
expect(instance.render()).toBeNull();
|
|
99
|
+
|
|
100
|
+
intersectionCallback(
|
|
101
|
+
[{ isIntersecting: true } as IntersectionObserverEntry],
|
|
102
|
+
null as unknown as IntersectionObserver,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
expect(parent.style.minHeight).toBe("");
|
|
106
|
+
expect(instance.render()).not.toBeNull(); // component became visible after intersection
|
|
107
|
+
expect(mockDisconnect).toHaveBeenCalled();
|
|
108
|
+
document.body.removeChild(parent);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("does nothing when _anchor has no parent", () => {
|
|
112
|
+
const Wrapped = applyLazy(BaseComp as AnyConstructor);
|
|
113
|
+
const instance = new Wrapped();
|
|
114
|
+
|
|
115
|
+
const anchor = document.createComment("end");
|
|
116
|
+
(instance as unknown as { _anchor: Comment })._anchor = anchor;
|
|
117
|
+
|
|
118
|
+
// Should not throw even though anchor has no parent
|
|
119
|
+
expect(() => instance.onMount?.()).not.toThrow();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("disconnects observer on unmount", () => {
|
|
123
|
+
const Wrapped = applyLazy(BaseComp as AnyConstructor);
|
|
124
|
+
const instance = new Wrapped();
|
|
125
|
+
|
|
126
|
+
const parent = document.createElement("div");
|
|
127
|
+
document.body.appendChild(parent);
|
|
128
|
+
const anchor = document.createComment("end");
|
|
129
|
+
parent.appendChild(anchor);
|
|
130
|
+
(instance as unknown as { _anchor: Comment })._anchor = anchor;
|
|
131
|
+
|
|
132
|
+
instance.onMount?.();
|
|
133
|
+
instance.onUnmount?.();
|
|
134
|
+
expect(mockDisconnect).toHaveBeenCalled();
|
|
135
|
+
document.body.removeChild(parent);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("onMount does nothing when _anchor is undefined", () => {
|
|
139
|
+
const Wrapped = applyLazy(BaseComp as AnyConstructor);
|
|
140
|
+
const instance = new Wrapped();
|
|
141
|
+
// _anchor is undefined by default
|
|
142
|
+
expect(() => instance.onMount?.()).not.toThrow();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("render() returns original content after becoming visible", () => {
|
|
146
|
+
const Wrapped = applyLazy(BaseComp as AnyConstructor, 200);
|
|
147
|
+
const instance = new Wrapped();
|
|
148
|
+
|
|
149
|
+
const parent = document.createElement("div");
|
|
150
|
+
document.body.appendChild(parent);
|
|
151
|
+
const anchor = document.createComment("end");
|
|
152
|
+
parent.appendChild(anchor);
|
|
153
|
+
(instance as unknown as { _anchor: Comment })._anchor = anchor;
|
|
154
|
+
|
|
155
|
+
instance.onMount?.();
|
|
156
|
+
expect(instance.render()).toBeNull(); // not yet visible
|
|
157
|
+
|
|
158
|
+
intersectionCallback(
|
|
159
|
+
[{ isIntersecting: true } as IntersectionObserverEntry],
|
|
160
|
+
null as unknown as IntersectionObserver,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// Now visible: render() must delegate to the original render()
|
|
164
|
+
const result = instance.render();
|
|
165
|
+
expect(result).not.toBeNull();
|
|
166
|
+
document.body.removeChild(parent);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("observer stays active and minHeight is preserved when entry is not intersecting", () => {
|
|
170
|
+
const Wrapped = applyLazy(BaseComp as AnyConstructor, 200);
|
|
171
|
+
const instance = new Wrapped();
|
|
172
|
+
|
|
173
|
+
const parent = document.createElement("div");
|
|
174
|
+
document.body.appendChild(parent);
|
|
175
|
+
const anchor = document.createComment("end");
|
|
176
|
+
parent.appendChild(anchor);
|
|
177
|
+
(instance as unknown as { _anchor: Comment })._anchor = anchor;
|
|
178
|
+
|
|
179
|
+
instance.onMount?.();
|
|
180
|
+
|
|
181
|
+
// Fire callback with isIntersecting=false — should be a no-op
|
|
182
|
+
intersectionCallback(
|
|
183
|
+
[{ isIntersecting: false } as IntersectionObserverEntry],
|
|
184
|
+
null as unknown as IntersectionObserver,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
expect(instance.render()).toBeNull();
|
|
188
|
+
expect(mockDisconnect).not.toHaveBeenCalled();
|
|
189
|
+
expect(parent.style.minHeight).toBe("200px");
|
|
190
|
+
document.body.removeChild(parent);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("does not re-apply minHeight on remount when already visible", () => {
|
|
194
|
+
const Wrapped = applyLazy(BaseComp as AnyConstructor, 300);
|
|
195
|
+
const instance = new Wrapped();
|
|
196
|
+
|
|
197
|
+
const parent = document.createElement("div");
|
|
198
|
+
document.body.appendChild(parent);
|
|
199
|
+
const anchor = document.createComment("end");
|
|
200
|
+
parent.appendChild(anchor);
|
|
201
|
+
(instance as unknown as { _anchor: Comment })._anchor = anchor;
|
|
202
|
+
|
|
203
|
+
// First mount — become visible
|
|
204
|
+
instance.onMount?.();
|
|
205
|
+
intersectionCallback(
|
|
206
|
+
[{ isIntersecting: true } as IntersectionObserverEntry],
|
|
207
|
+
null as unknown as IntersectionObserver,
|
|
208
|
+
);
|
|
209
|
+
instance.onUnmount?.();
|
|
210
|
+
|
|
211
|
+
// Second mount — _lazyVisible is already true
|
|
212
|
+
instance.onMount?.();
|
|
213
|
+
|
|
214
|
+
// minHeight must NOT be re-applied because _lazyVisible() is true
|
|
215
|
+
expect(parent.style.minHeight).toBe("");
|
|
216
|
+
document.body.removeChild(parent);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
@@ -1,74 +1,141 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from "vitest";
|
|
2
2
|
|
|
3
|
+
import { signal } from "@praxisjs/core/internal";
|
|
3
4
|
import { Memo } from "../functions/memo";
|
|
4
5
|
|
|
5
6
|
function mockCtx(name: string) {
|
|
6
|
-
|
|
7
|
+
const initializers: Array<(this: object) => void> = [];
|
|
8
|
+
return {
|
|
9
|
+
name,
|
|
10
|
+
kind: "method" as const,
|
|
11
|
+
addInitializer(fn: (this: object) => void) {
|
|
12
|
+
initializers.push(fn);
|
|
13
|
+
},
|
|
14
|
+
runInitializers(instance: object) {
|
|
15
|
+
initializers.forEach((fn) => { fn.call(instance); });
|
|
16
|
+
},
|
|
17
|
+
} as unknown as ClassMethodDecoratorContext & { runInitializers(instance: object): void };
|
|
7
18
|
}
|
|
8
19
|
|
|
9
20
|
describe("Memo", () => {
|
|
10
21
|
it("returns the computed value", () => {
|
|
11
22
|
const fn = vi.fn((x: unknown) => (x as number) * 2);
|
|
12
|
-
const
|
|
23
|
+
const ctx = mockCtx("double");
|
|
24
|
+
Memo()(fn, ctx);
|
|
25
|
+
|
|
13
26
|
const obj = {};
|
|
14
|
-
|
|
27
|
+
ctx.runInitializers(obj);
|
|
28
|
+
const method = (obj as Record<string, (...a: unknown[]) => unknown>).double;
|
|
29
|
+
|
|
30
|
+
expect(method(5)).toBe(10);
|
|
15
31
|
});
|
|
16
32
|
|
|
17
33
|
it("caches result for same args — calls original only once", () => {
|
|
18
34
|
const fn = vi.fn((x: unknown) => (x as number) + 1);
|
|
19
|
-
const
|
|
35
|
+
const ctx = mockCtx("inc");
|
|
36
|
+
Memo()(fn, ctx);
|
|
37
|
+
|
|
20
38
|
const obj = {};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
39
|
+
ctx.runInitializers(obj);
|
|
40
|
+
const method = (obj as Record<string, (...a: unknown[]) => unknown>).inc;
|
|
41
|
+
|
|
42
|
+
method(3);
|
|
43
|
+
method(3);
|
|
44
|
+
method(3);
|
|
24
45
|
expect(fn).toHaveBeenCalledTimes(1);
|
|
25
46
|
});
|
|
26
47
|
|
|
27
48
|
it("computes separately for different args", () => {
|
|
28
49
|
const fn = vi.fn((x: unknown) => (x as number) * 10);
|
|
29
|
-
const
|
|
50
|
+
const ctx = mockCtx("mul");
|
|
51
|
+
Memo()(fn, ctx);
|
|
52
|
+
|
|
30
53
|
const obj = {};
|
|
31
|
-
|
|
32
|
-
|
|
54
|
+
ctx.runInitializers(obj);
|
|
55
|
+
const method = (obj as Record<string, (...a: unknown[]) => unknown>).mul;
|
|
56
|
+
|
|
57
|
+
expect(method(2)).toBe(20);
|
|
58
|
+
expect(method(3)).toBe(30);
|
|
33
59
|
expect(fn).toHaveBeenCalledTimes(2);
|
|
34
60
|
});
|
|
35
61
|
|
|
36
62
|
it("is per-instance — different instances have separate caches", () => {
|
|
37
63
|
const fn = vi.fn(() => Math.random());
|
|
38
|
-
const
|
|
64
|
+
const ctx = mockCtx("rand");
|
|
65
|
+
Memo()(fn, ctx);
|
|
66
|
+
|
|
39
67
|
const a = {};
|
|
40
68
|
const b = {};
|
|
41
|
-
|
|
42
|
-
|
|
69
|
+
ctx.runInitializers(a);
|
|
70
|
+
ctx.runInitializers(b);
|
|
71
|
+
|
|
72
|
+
const methodA = (a as Record<string, () => unknown>).rand;
|
|
73
|
+
const methodB = (b as Record<string, () => unknown>).rand;
|
|
74
|
+
const v1 = methodA();
|
|
75
|
+
const v2 = methodB();
|
|
43
76
|
expect(fn).toHaveBeenCalledTimes(2);
|
|
44
77
|
expect(v1).not.toBe(v2); // different random values per instance
|
|
45
78
|
});
|
|
46
79
|
|
|
47
80
|
it("serializes no-args calls with __no_args__ key", () => {
|
|
48
81
|
const fn = vi.fn(() => 42);
|
|
49
|
-
const
|
|
82
|
+
const ctx = mockCtx("noop");
|
|
83
|
+
Memo()(fn, ctx);
|
|
84
|
+
|
|
50
85
|
const obj = {};
|
|
51
|
-
|
|
52
|
-
|
|
86
|
+
ctx.runInitializers(obj);
|
|
87
|
+
const method = (obj as Record<string, () => unknown>).noop;
|
|
88
|
+
|
|
89
|
+
method();
|
|
90
|
+
method();
|
|
53
91
|
expect(fn).toHaveBeenCalledTimes(1);
|
|
54
92
|
});
|
|
55
93
|
|
|
56
94
|
it("serializes object args via JSON.stringify", () => {
|
|
57
95
|
const fn = vi.fn((o: unknown) => JSON.stringify(o));
|
|
58
|
-
const
|
|
96
|
+
const ctx = mockCtx("obj");
|
|
97
|
+
Memo()(fn, ctx);
|
|
98
|
+
|
|
59
99
|
const obj = {};
|
|
60
|
-
|
|
61
|
-
|
|
100
|
+
ctx.runInitializers(obj);
|
|
101
|
+
const method = (obj as Record<string, (...a: unknown[]) => unknown>).obj;
|
|
102
|
+
|
|
103
|
+
method({ a: 1 });
|
|
104
|
+
method({ a: 1 }); // same key
|
|
62
105
|
expect(fn).toHaveBeenCalledTimes(1);
|
|
63
106
|
});
|
|
64
107
|
|
|
108
|
+
it("re-evaluates when a reactive signal read inside the method changes", () => {
|
|
109
|
+
const s = signal(1);
|
|
110
|
+
const fn = vi.fn(() => s() * 10);
|
|
111
|
+
const ctx = mockCtx("reactive");
|
|
112
|
+
Memo()(fn, ctx);
|
|
113
|
+
|
|
114
|
+
const obj = {};
|
|
115
|
+
ctx.runInitializers(obj);
|
|
116
|
+
const method = (obj as Record<string, () => unknown>).reactive;
|
|
117
|
+
|
|
118
|
+
expect(method()).toBe(10);
|
|
119
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
120
|
+
|
|
121
|
+
s.set(5);
|
|
122
|
+
// The computed tracks s() — next read triggers recomputation
|
|
123
|
+
expect(method()).toBe(50);
|
|
124
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
125
|
+
});
|
|
126
|
+
|
|
65
127
|
it("serializes symbol args via toString()", () => {
|
|
66
128
|
const fn = vi.fn((s: unknown) => String(s));
|
|
67
|
-
const
|
|
129
|
+
const ctx = mockCtx("sym");
|
|
130
|
+
Memo()(fn, ctx);
|
|
131
|
+
|
|
68
132
|
const obj = {};
|
|
133
|
+
ctx.runInitializers(obj);
|
|
134
|
+
const method = (obj as Record<string, (...a: unknown[]) => unknown>).sym;
|
|
135
|
+
|
|
69
136
|
const sym = Symbol("test");
|
|
70
|
-
|
|
71
|
-
|
|
137
|
+
method(sym);
|
|
138
|
+
method(sym);
|
|
72
139
|
expect(fn).toHaveBeenCalledTimes(1);
|
|
73
140
|
});
|
|
74
141
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from "vitest";
|
|
2
2
|
|
|
3
3
|
import { StatefulComponent } from "@praxisjs/core";
|
|
4
|
-
import { signal } from "@praxisjs/core/internal";
|
|
4
|
+
import { signal, computed } from "@praxisjs/core/internal";
|
|
5
5
|
|
|
6
6
|
import { Watch } from "../functions/watch";
|
|
7
7
|
import { When } from "../functions/when";
|
|
@@ -76,6 +76,36 @@ describe("State decorator", () => {
|
|
|
76
76
|
(instance as unknown as Record<string, unknown>).value = "world";
|
|
77
77
|
expect(instance._stateDirty).toBe(true);
|
|
78
78
|
});
|
|
79
|
+
|
|
80
|
+
it("accepts null as initial value", () => {
|
|
81
|
+
const { ctx, run } = fieldCtx("nullable");
|
|
82
|
+
State()(undefined, ctx);
|
|
83
|
+
|
|
84
|
+
const instance = new TestComponent();
|
|
85
|
+
(instance as unknown as Record<string, unknown>).nullable = null;
|
|
86
|
+
run(instance);
|
|
87
|
+
|
|
88
|
+
expect((instance as unknown as Record<string, unknown>).nullable).toBeNull();
|
|
89
|
+
(instance as unknown as Record<string, unknown>).nullable = "set";
|
|
90
|
+
expect((instance as unknown as Record<string, unknown>).nullable).toBe("set");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("multiple State fields on same class are independent", () => {
|
|
94
|
+
const { ctx: ctxA, run: runA } = fieldCtx("a");
|
|
95
|
+
const { ctx: ctxB, run: runB } = fieldCtx("b");
|
|
96
|
+
State()(undefined, ctxA);
|
|
97
|
+
State()(undefined, ctxB);
|
|
98
|
+
|
|
99
|
+
const instance = new TestComponent();
|
|
100
|
+
(instance as unknown as Record<string, unknown>).a = 1;
|
|
101
|
+
(instance as unknown as Record<string, unknown>).b = 2;
|
|
102
|
+
runA(instance);
|
|
103
|
+
runB(instance);
|
|
104
|
+
|
|
105
|
+
(instance as unknown as Record<string, unknown>).a = 10;
|
|
106
|
+
expect((instance as unknown as Record<string, unknown>).a).toBe(10);
|
|
107
|
+
expect((instance as unknown as Record<string, unknown>).b).toBe(2);
|
|
108
|
+
});
|
|
79
109
|
});
|
|
80
110
|
|
|
81
111
|
// ── Prop ──────────────────────────────────────────────────────────────────────
|
|
@@ -474,4 +504,222 @@ describe("When decorator", () => {
|
|
|
474
504
|
expect(mountSpy).toHaveBeenCalled();
|
|
475
505
|
expect(unmountSpy).toHaveBeenCalled();
|
|
476
506
|
});
|
|
507
|
+
|
|
508
|
+
it("works when the watched prop is a computed value", () => {
|
|
509
|
+
const { ctx, run } = methodCtxFor("onComputed");
|
|
510
|
+
const handler = vi.fn();
|
|
511
|
+
When("doubled")(handler, ctx as unknown as ClassMethodDecoratorContext);
|
|
512
|
+
|
|
513
|
+
const s = signal(0);
|
|
514
|
+
const c = computed(() => s() * 2);
|
|
515
|
+
const instance = new TestComponent();
|
|
516
|
+
// Store the computed itself (not the result) so When can call isComputed on it
|
|
517
|
+
(instance as unknown as Record<string, unknown>).doubled = c;
|
|
518
|
+
run(instance);
|
|
519
|
+
|
|
520
|
+
(instance as unknown as { onMount: () => void }).onMount?.();
|
|
521
|
+
|
|
522
|
+
s.set(5); // doubled becomes 10 (truthy)
|
|
523
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// ── Prop (additional branches) ────────────────────────────────────────────────
|
|
528
|
+
|
|
529
|
+
describe("Prop decorator — additional branches", () => {
|
|
530
|
+
it("set() updates the default value (used when no _rawProps entry)", () => {
|
|
531
|
+
const { ctx, run } = fieldCtx("size");
|
|
532
|
+
Prop()(undefined, ctx);
|
|
533
|
+
|
|
534
|
+
const instance = new TestComponent();
|
|
535
|
+
(instance as unknown as Record<string, unknown>).size = "sm";
|
|
536
|
+
run(instance);
|
|
537
|
+
|
|
538
|
+
// Setter path: updates _defaults
|
|
539
|
+
(instance as unknown as Record<string, unknown>).size = "lg";
|
|
540
|
+
expect((instance as unknown as Record<string, unknown>).size).toBe("lg");
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// ── History (additional branches) ────────────────────────────────────────────
|
|
545
|
+
|
|
546
|
+
describe("History decorator — undo with no previous value", () => {
|
|
547
|
+
it("undo() does nothing when there is no previous value", () => {
|
|
548
|
+
const { ctx, run } = fieldCtx("pts");
|
|
549
|
+
History()(undefined, ctx);
|
|
550
|
+
|
|
551
|
+
const s = signal(0);
|
|
552
|
+
const instance = new TestComponent();
|
|
553
|
+
Object.defineProperty(instance, "pts", {
|
|
554
|
+
get: () => s(),
|
|
555
|
+
set: (v: number) => { s.set(v); },
|
|
556
|
+
configurable: true,
|
|
557
|
+
});
|
|
558
|
+
run(instance);
|
|
559
|
+
|
|
560
|
+
const h = (instance as unknown as Record<string, unknown>).ptsHistory as {
|
|
561
|
+
undo: () => void;
|
|
562
|
+
canUndo: () => boolean;
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
// No changes yet — undo should be a no-op
|
|
566
|
+
expect(h.canUndo()).toBe(false);
|
|
567
|
+
expect(() => h.undo()).not.toThrow();
|
|
568
|
+
expect(s()).toBe(0);
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// ── History decorator — multiple fields ───────────────────────────────────────
|
|
573
|
+
|
|
574
|
+
describe("History decorator — multiple History properties on same instance", () => {
|
|
575
|
+
it("two History fields are independent", () => {
|
|
576
|
+
const { ctx: ctxX, run: runX } = fieldCtx("x");
|
|
577
|
+
const { ctx: ctxY, run: runY } = fieldCtx("y");
|
|
578
|
+
History()(undefined, ctxX);
|
|
579
|
+
History()(undefined, ctxY);
|
|
580
|
+
|
|
581
|
+
const sx = signal(0);
|
|
582
|
+
const sy = signal(0);
|
|
583
|
+
const instance = new TestComponent();
|
|
584
|
+
Object.defineProperty(instance, "x", {
|
|
585
|
+
get: () => sx(), set: (v: number) => { sx.set(v); }, configurable: true,
|
|
586
|
+
});
|
|
587
|
+
Object.defineProperty(instance, "y", {
|
|
588
|
+
get: () => sy(), set: (v: number) => { sy.set(v); }, configurable: true,
|
|
589
|
+
});
|
|
590
|
+
runX(instance);
|
|
591
|
+
runY(instance);
|
|
592
|
+
|
|
593
|
+
// Access history first to trigger lazy creation and subscribe to signals
|
|
594
|
+
const hx = (instance as unknown as Record<string, unknown>).xHistory as {
|
|
595
|
+
canUndo: () => boolean; current: () => number;
|
|
596
|
+
};
|
|
597
|
+
const hy = (instance as unknown as Record<string, unknown>).yHistory as {
|
|
598
|
+
canUndo: () => boolean; current: () => number;
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
sx.set(5);
|
|
602
|
+
sy.set(10);
|
|
603
|
+
|
|
604
|
+
expect(hx.canUndo()).toBe(true);
|
|
605
|
+
expect(hy.canUndo()).toBe(true);
|
|
606
|
+
expect(hx.current()).toBe(5);
|
|
607
|
+
expect(hy.current()).toBe(10);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it("clear() after undo removes redo entries", () => {
|
|
611
|
+
const { ctx, run } = fieldCtx("val");
|
|
612
|
+
History()(undefined, ctx);
|
|
613
|
+
|
|
614
|
+
const s = signal(0);
|
|
615
|
+
const instance = new TestComponent();
|
|
616
|
+
Object.defineProperty(instance, "val", {
|
|
617
|
+
get: () => s(), set: (v: number) => { s.set(v); }, configurable: true,
|
|
618
|
+
});
|
|
619
|
+
run(instance);
|
|
620
|
+
|
|
621
|
+
const h = (instance as unknown as Record<string, unknown>).valHistory as {
|
|
622
|
+
undo: () => void; clear: () => void;
|
|
623
|
+
canUndo: () => boolean; canRedo: () => boolean;
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
s.set(1);
|
|
627
|
+
s.set(2);
|
|
628
|
+
h.undo();
|
|
629
|
+
expect(h.canRedo()).toBe(true);
|
|
630
|
+
h.clear();
|
|
631
|
+
expect(h.canRedo()).toBe(false);
|
|
632
|
+
expect(h.canUndo()).toBe(false);
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// ── Watch (cleanup + multi-prop non-watched) ──────────────────────────────────
|
|
637
|
+
|
|
638
|
+
describe("Watch decorator — onUnmount cleanup", () => {
|
|
639
|
+
it("stops firing after onUnmount is called", () => {
|
|
640
|
+
const { ctx, run } = methodCtx("onCount");
|
|
641
|
+
const handler = vi.fn();
|
|
642
|
+
Watch("count" as never)(handler, ctx as unknown as ClassMethodDecoratorContext);
|
|
643
|
+
|
|
644
|
+
const s = signal(0);
|
|
645
|
+
const instance = new TestComponent();
|
|
646
|
+
Object.defineProperty(instance, "count", { get: () => s(), configurable: true });
|
|
647
|
+
run(instance);
|
|
648
|
+
(instance as unknown as { onMount: () => void }).onMount?.();
|
|
649
|
+
|
|
650
|
+
s.set(1);
|
|
651
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
652
|
+
|
|
653
|
+
(instance as unknown as { onUnmount: () => void }).onUnmount?.();
|
|
654
|
+
s.set(2);
|
|
655
|
+
|
|
656
|
+
// After unmount the effect is stopped — handler must NOT fire again
|
|
657
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it("preserves existing onUnmount when adding cleanup", () => {
|
|
661
|
+
const { ctx, run } = methodCtx("onVal");
|
|
662
|
+
const handler = vi.fn();
|
|
663
|
+
Watch("val" as never)(handler, ctx as unknown as ClassMethodDecoratorContext);
|
|
664
|
+
|
|
665
|
+
const s = signal(0);
|
|
666
|
+
const instance = new TestComponent();
|
|
667
|
+
Object.defineProperty(instance, "val", { get: () => s(), configurable: true });
|
|
668
|
+
run(instance);
|
|
669
|
+
|
|
670
|
+
const unmountSpy = vi.fn();
|
|
671
|
+
const prevUnmount = instance.onUnmount?.bind(instance);
|
|
672
|
+
instance.onUnmount = function () { unmountSpy(); prevUnmount?.(); };
|
|
673
|
+
|
|
674
|
+
(instance as unknown as { onMount: () => void }).onMount?.();
|
|
675
|
+
instance.onUnmount?.();
|
|
676
|
+
expect(unmountSpy).toHaveBeenCalled();
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it("multi-prop watch does NOT fire when an unwatched prop changes", () => {
|
|
680
|
+
const { ctx, run } = methodCtx("onAB");
|
|
681
|
+
const handler = vi.fn();
|
|
682
|
+
Watch("a" as never, "b" as never)(handler, ctx as unknown as ClassMethodDecoratorContext);
|
|
683
|
+
|
|
684
|
+
const sa = signal(0);
|
|
685
|
+
const sb = signal(0);
|
|
686
|
+
const sc = signal(0); // not watched
|
|
687
|
+
|
|
688
|
+
const instance = new TestComponent();
|
|
689
|
+
Object.defineProperty(instance, "a", { get: () => sa(), configurable: true });
|
|
690
|
+
Object.defineProperty(instance, "b", { get: () => sb(), configurable: true });
|
|
691
|
+
Object.defineProperty(instance, "c", { get: () => sc(), configurable: true });
|
|
692
|
+
run(instance);
|
|
693
|
+
(instance as unknown as { onMount: () => void }).onMount?.();
|
|
694
|
+
|
|
695
|
+
sc.set(99); // unwatched prop — handler must NOT fire
|
|
696
|
+
expect(handler).not.toHaveBeenCalled();
|
|
697
|
+
|
|
698
|
+
sa.set(1); // watched — handler fires
|
|
699
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
// ── Watch (additional branches — computed readValue) ──────────────────────────
|
|
704
|
+
|
|
705
|
+
describe("Watch decorator — reading computed values", () => {
|
|
706
|
+
it("reads the value of a computed field (not the computed object)", () => {
|
|
707
|
+
const { ctx, run } = methodCtx("onDoubled");
|
|
708
|
+
const handler = vi.fn();
|
|
709
|
+
Watch("doubled" as never)(handler, ctx as unknown as ClassMethodDecoratorContext);
|
|
710
|
+
|
|
711
|
+
const s = signal(1);
|
|
712
|
+
const c = computed(() => s() * 2);
|
|
713
|
+
const instance = new TestComponent();
|
|
714
|
+
// Watch reads instance["doubled"] — if it's a computed, it calls it
|
|
715
|
+
Object.defineProperty(instance, "doubled", {
|
|
716
|
+
get: () => c,
|
|
717
|
+
configurable: true,
|
|
718
|
+
});
|
|
719
|
+
run(instance);
|
|
720
|
+
(instance as unknown as { onMount: () => void }).onMount?.();
|
|
721
|
+
|
|
722
|
+
s.set(5); // c() goes from 2 to 10 → triggers handler
|
|
723
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
724
|
+
});
|
|
477
725
|
});
|