@praxisjs/decorators 0.4.3 → 0.6.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 +39 -0
- package/dist/__tests__/component.test.js +10 -1
- package/dist/__tests__/component.test.js.map +1 -1
- 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.js +26 -0
- package/dist/__tests__/events.test.js.map +1 -1
- package/dist/__tests__/functions.test.js +205 -47
- package/dist/__tests__/functions.test.js.map +1 -1
- package/dist/__tests__/lazy.test.js +5 -7
- package/dist/__tests__/lazy.test.js.map +1 -1
- package/dist/__tests__/memo.test.js +114 -25
- package/dist/__tests__/memo.test.js.map +1 -1
- package/dist/__tests__/properties.test.js +193 -0
- 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 +127 -0
- package/dist/__tests__/resource.test.js.map +1 -0
- package/dist/__tests__/slot.test.js +32 -0
- package/dist/__tests__/slot.test.js.map +1 -1
- package/dist/__tests__/until.test.d.ts +2 -0
- package/dist/__tests__/until.test.d.ts.map +1 -0
- package/dist/__tests__/until.test.js +88 -0
- package/dist/__tests__/until.test.js.map +1 -0
- package/dist/__tests__/virtual.test.js +164 -15
- package/dist/__tests__/virtual.test.js.map +1 -1
- 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.map +1 -1
- package/dist/component/lazy.js +29 -30
- package/dist/component/lazy.js.map +1 -1
- package/dist/component/virtual.d.ts.map +1 -1
- package/dist/component/virtual.js +49 -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.map +1 -1
- package/dist/functions/debounce.js +21 -20
- package/dist/functions/debounce.js.map +1 -1
- package/dist/functions/index.d.ts +1 -0
- package/dist/functions/index.d.ts.map +1 -1
- package/dist/functions/index.js +1 -0
- package/dist/functions/index.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 +29 -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 +11 -17
- package/dist/functions/throttle.js.map +1 -1
- package/dist/functions/until.d.ts +2 -0
- package/dist/functions/until.d.ts.map +1 -0
- package/dist/functions/until.js +17 -0
- package/dist/functions/until.js.map +1 -0
- package/dist/functions/watch.d.ts +1 -1
- package/dist/functions/watch.d.ts.map +1 -1
- package/dist/functions/watch.js +25 -39
- 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 +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- 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 +10 -0
- package/src/__tests__/compose.test.ts +202 -0
- package/src/__tests__/events.test.ts +32 -0
- package/src/__tests__/functions.test.ts +257 -46
- package/src/__tests__/lazy.test.ts +3 -5
- package/src/__tests__/memo.test.ts +143 -25
- package/src/__tests__/properties.test.ts +233 -0
- package/src/__tests__/resource.test.ts +141 -0
- package/src/__tests__/slot.test.ts +38 -0
- package/src/__tests__/until.test.ts +113 -0
- package/src/__tests__/virtual.test.ts +135 -7
- package/src/component/component.ts +27 -12
- package/src/component/lazy.ts +33 -37
- package/src/component/virtual.tsx +67 -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 +21 -23
- package/src/functions/index.ts +1 -0
- package/src/functions/log.ts +40 -43
- package/src/functions/memo.ts +28 -42
- package/src/functions/once.ts +13 -16
- package/src/functions/retry.ts +21 -22
- package/src/functions/throttle.ts +11 -24
- package/src/functions/until.ts +19 -0
- package/src/functions/watch.ts +27 -43
- package/src/functions/when.ts +11 -36
- package/src/index.ts +29 -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
|
@@ -4,87 +4,205 @@ import { signal } from "@praxisjs/core/internal";
|
|
|
4
4
|
import { Memo } from "../functions/memo";
|
|
5
5
|
|
|
6
6
|
function mockCtx(name: string) {
|
|
7
|
-
|
|
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 };
|
|
8
18
|
}
|
|
9
19
|
|
|
10
20
|
describe("Memo", () => {
|
|
11
21
|
it("returns the computed value", () => {
|
|
12
22
|
const fn = vi.fn((x: unknown) => (x as number) * 2);
|
|
13
|
-
const
|
|
23
|
+
const ctx = mockCtx("double");
|
|
24
|
+
Memo()(fn, ctx);
|
|
25
|
+
|
|
14
26
|
const obj = {};
|
|
15
|
-
|
|
27
|
+
ctx.runInitializers(obj);
|
|
28
|
+
const method = (obj as Record<string, (...a: unknown[]) => unknown>).double;
|
|
29
|
+
|
|
30
|
+
expect(method(5)).toBe(10);
|
|
16
31
|
});
|
|
17
32
|
|
|
18
33
|
it("caches result for same args — calls original only once", () => {
|
|
19
34
|
const fn = vi.fn((x: unknown) => (x as number) + 1);
|
|
20
|
-
const
|
|
35
|
+
const ctx = mockCtx("inc");
|
|
36
|
+
Memo()(fn, ctx);
|
|
37
|
+
|
|
21
38
|
const obj = {};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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);
|
|
25
45
|
expect(fn).toHaveBeenCalledTimes(1);
|
|
26
46
|
});
|
|
27
47
|
|
|
28
48
|
it("computes separately for different args", () => {
|
|
29
49
|
const fn = vi.fn((x: unknown) => (x as number) * 10);
|
|
30
|
-
const
|
|
50
|
+
const ctx = mockCtx("mul");
|
|
51
|
+
Memo()(fn, ctx);
|
|
52
|
+
|
|
31
53
|
const obj = {};
|
|
32
|
-
|
|
33
|
-
|
|
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);
|
|
34
59
|
expect(fn).toHaveBeenCalledTimes(2);
|
|
35
60
|
});
|
|
36
61
|
|
|
37
62
|
it("is per-instance — different instances have separate caches", () => {
|
|
38
63
|
const fn = vi.fn(() => Math.random());
|
|
39
|
-
const
|
|
64
|
+
const ctx = mockCtx("rand");
|
|
65
|
+
Memo()(fn, ctx);
|
|
66
|
+
|
|
40
67
|
const a = {};
|
|
41
68
|
const b = {};
|
|
42
|
-
|
|
43
|
-
|
|
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();
|
|
44
76
|
expect(fn).toHaveBeenCalledTimes(2);
|
|
45
77
|
expect(v1).not.toBe(v2); // different random values per instance
|
|
46
78
|
});
|
|
47
79
|
|
|
48
80
|
it("serializes no-args calls with __no_args__ key", () => {
|
|
49
81
|
const fn = vi.fn(() => 42);
|
|
50
|
-
const
|
|
82
|
+
const ctx = mockCtx("noop");
|
|
83
|
+
Memo()(fn, ctx);
|
|
84
|
+
|
|
51
85
|
const obj = {};
|
|
52
|
-
|
|
53
|
-
|
|
86
|
+
ctx.runInitializers(obj);
|
|
87
|
+
const method = (obj as Record<string, () => unknown>).noop;
|
|
88
|
+
|
|
89
|
+
method();
|
|
90
|
+
method();
|
|
54
91
|
expect(fn).toHaveBeenCalledTimes(1);
|
|
55
92
|
});
|
|
56
93
|
|
|
57
94
|
it("serializes object args via JSON.stringify", () => {
|
|
58
95
|
const fn = vi.fn((o: unknown) => JSON.stringify(o));
|
|
59
|
-
const
|
|
96
|
+
const ctx = mockCtx("obj");
|
|
97
|
+
Memo()(fn, ctx);
|
|
98
|
+
|
|
60
99
|
const obj = {};
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
63
105
|
expect(fn).toHaveBeenCalledTimes(1);
|
|
64
106
|
});
|
|
65
107
|
|
|
66
108
|
it("re-evaluates when a reactive signal read inside the method changes", () => {
|
|
67
109
|
const s = signal(1);
|
|
68
110
|
const fn = vi.fn(() => s() * 10);
|
|
69
|
-
const
|
|
111
|
+
const ctx = mockCtx("reactive");
|
|
112
|
+
Memo()(fn, ctx);
|
|
113
|
+
|
|
70
114
|
const obj = {};
|
|
115
|
+
ctx.runInitializers(obj);
|
|
116
|
+
const method = (obj as Record<string, () => unknown>).reactive;
|
|
71
117
|
|
|
72
|
-
expect(
|
|
118
|
+
expect(method()).toBe(10);
|
|
73
119
|
expect(fn).toHaveBeenCalledTimes(1);
|
|
74
120
|
|
|
75
121
|
s.set(5);
|
|
76
122
|
// The computed tracks s() — next read triggers recomputation
|
|
77
|
-
expect(
|
|
123
|
+
expect(method()).toBe(50);
|
|
78
124
|
expect(fn).toHaveBeenCalledTimes(2);
|
|
79
125
|
});
|
|
80
126
|
|
|
81
127
|
it("serializes symbol args via toString()", () => {
|
|
82
128
|
const fn = vi.fn((s: unknown) => String(s));
|
|
83
|
-
const
|
|
129
|
+
const ctx = mockCtx("sym");
|
|
130
|
+
Memo()(fn, ctx);
|
|
131
|
+
|
|
84
132
|
const obj = {};
|
|
133
|
+
ctx.runInitializers(obj);
|
|
134
|
+
const method = (obj as Record<string, (...a: unknown[]) => unknown>).sym;
|
|
135
|
+
|
|
85
136
|
const sym = Symbol("test");
|
|
86
|
-
|
|
87
|
-
|
|
137
|
+
method(sym);
|
|
138
|
+
method(sym);
|
|
139
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("circular object argument does not crash — falls back to stable identity key", () => {
|
|
143
|
+
const fn = vi.fn((o: unknown) => o);
|
|
144
|
+
const ctx = mockCtx("circular");
|
|
145
|
+
Memo()(fn, ctx);
|
|
146
|
+
|
|
147
|
+
const obj = {};
|
|
148
|
+
ctx.runInitializers(obj);
|
|
149
|
+
const method = (obj as Record<string, (...a: unknown[]) => unknown>).circular;
|
|
150
|
+
|
|
151
|
+
// Create a circular reference
|
|
152
|
+
const circ: Record<string, unknown> = {};
|
|
153
|
+
circ.self = circ;
|
|
154
|
+
|
|
155
|
+
expect(() => method(circ)).not.toThrow();
|
|
156
|
+
// Calling again with the same object should hit cache (not call fn again)
|
|
157
|
+
method(circ);
|
|
158
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("two circular objects produce different cache keys", () => {
|
|
162
|
+
const fn = vi.fn((o: unknown) => o);
|
|
163
|
+
const ctx = mockCtx("circular2");
|
|
164
|
+
Memo()(fn, ctx);
|
|
165
|
+
|
|
166
|
+
const obj = {};
|
|
167
|
+
ctx.runInitializers(obj);
|
|
168
|
+
const method = (obj as Record<string, (...a: unknown[]) => unknown>).circular2;
|
|
169
|
+
|
|
170
|
+
const a: Record<string, unknown> = {};
|
|
171
|
+
a.self = a;
|
|
172
|
+
const b: Record<string, unknown> = {};
|
|
173
|
+
b.self = b;
|
|
174
|
+
|
|
175
|
+
method(a);
|
|
176
|
+
method(b);
|
|
177
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("undefined vs null as arguments produce different cache keys", () => {
|
|
181
|
+
const fn = vi.fn((x: unknown) => x);
|
|
182
|
+
const ctx = mockCtx("nullUndef");
|
|
183
|
+
Memo()(fn, ctx);
|
|
184
|
+
|
|
185
|
+
const obj = {};
|
|
186
|
+
ctx.runInitializers(obj);
|
|
187
|
+
const method = (obj as Record<string, (...a: unknown[]) => unknown>).nullUndef;
|
|
188
|
+
|
|
189
|
+
method(undefined);
|
|
190
|
+
method(null);
|
|
191
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("BigInt argument does not crash JSON.stringify path", () => {
|
|
195
|
+
const fn = vi.fn((x: unknown) => x);
|
|
196
|
+
const ctx = mockCtx("bigint");
|
|
197
|
+
Memo()(fn, ctx);
|
|
198
|
+
|
|
199
|
+
const obj = {};
|
|
200
|
+
ctx.runInitializers(obj);
|
|
201
|
+
const method = (obj as Record<string, (...a: unknown[]) => unknown>).bigint;
|
|
202
|
+
|
|
203
|
+
// BigInt is a non-object, non-symbol primitive — goes through String() path
|
|
204
|
+
expect(() => method(BigInt(42))).not.toThrow();
|
|
205
|
+
method(BigInt(42));
|
|
88
206
|
expect(fn).toHaveBeenCalledTimes(1);
|
|
89
207
|
});
|
|
90
208
|
});
|
|
@@ -5,6 +5,7 @@ import { signal, computed } from "@praxisjs/core/internal";
|
|
|
5
5
|
|
|
6
6
|
import { Watch } from "../functions/watch";
|
|
7
7
|
import { When } from "../functions/when";
|
|
8
|
+
import { Compose } from "../properties/compose";
|
|
8
9
|
import { Computed } from "../properties/computed";
|
|
9
10
|
import { History } from "../properties/history";
|
|
10
11
|
import { Prop } from "../properties/prop";
|
|
@@ -723,3 +724,235 @@ describe("Watch decorator — reading computed values", () => {
|
|
|
723
724
|
expect(handler).toHaveBeenCalledTimes(1);
|
|
724
725
|
});
|
|
725
726
|
});
|
|
727
|
+
|
|
728
|
+
// ── Watch — additional new tests ──────────────────────────────────────────────
|
|
729
|
+
|
|
730
|
+
describe("Watch decorator — new cases", () => {
|
|
731
|
+
it("NaN → NaN does NOT fire (Object.is semantics)", () => {
|
|
732
|
+
const { ctx, run } = methodCtx("onNaN");
|
|
733
|
+
const handler = vi.fn();
|
|
734
|
+
Watch("nanProp" as never)(handler, ctx as unknown as ClassMethodDecoratorContext);
|
|
735
|
+
|
|
736
|
+
const s = signal<number>(NaN);
|
|
737
|
+
const instance = new TestComponent();
|
|
738
|
+
Object.defineProperty(instance, "nanProp", { get: () => s(), configurable: true });
|
|
739
|
+
run(instance);
|
|
740
|
+
(instance as unknown as { onMount: () => void }).onMount?.();
|
|
741
|
+
|
|
742
|
+
s.set(NaN); // Object.is(NaN, NaN) === true → no change
|
|
743
|
+
expect(handler).not.toHaveBeenCalled();
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
it("watch callback that throws does not crash the effect system", () => {
|
|
747
|
+
const { ctx, run } = methodCtx("onThrow");
|
|
748
|
+
const throwingHandler = vi.fn(() => { throw new Error("handler error"); });
|
|
749
|
+
Watch("tProp" as never)(throwingHandler, ctx as unknown as ClassMethodDecoratorContext);
|
|
750
|
+
|
|
751
|
+
const s = signal(0);
|
|
752
|
+
const instance = new TestComponent();
|
|
753
|
+
Object.defineProperty(instance, "tProp", { get: () => s(), configurable: true });
|
|
754
|
+
run(instance);
|
|
755
|
+
(instance as unknown as { onMount: () => void }).onMount?.();
|
|
756
|
+
|
|
757
|
+
// Even if the handler throws, the signal change shouldn't crash the effect framework
|
|
758
|
+
// The throw propagates from the effect, so we catch it here
|
|
759
|
+
expect(() => { s.set(1); }).toThrow("handler error");
|
|
760
|
+
// The system should still be intact — further calls are not tested for recovery
|
|
761
|
+
expect(throwingHandler).toHaveBeenCalledTimes(1);
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it("multiple @Watch decorators on different methods both fire independently", () => {
|
|
765
|
+
const { ctx: ctxA, run: runA } = methodCtx("onA");
|
|
766
|
+
const { ctx: ctxB, run: runB } = methodCtx("onB");
|
|
767
|
+
const handlerA = vi.fn();
|
|
768
|
+
const handlerB = vi.fn();
|
|
769
|
+
Watch("propA" as never)(handlerA, ctxA as unknown as ClassMethodDecoratorContext);
|
|
770
|
+
Watch("propB" as never)(handlerB, ctxB as unknown as ClassMethodDecoratorContext);
|
|
771
|
+
|
|
772
|
+
const sa = signal(0);
|
|
773
|
+
const sb = signal(0);
|
|
774
|
+
const instance = new TestComponent();
|
|
775
|
+
Object.defineProperty(instance, "propA", { get: () => sa(), configurable: true });
|
|
776
|
+
Object.defineProperty(instance, "propB", { get: () => sb(), configurable: true });
|
|
777
|
+
runA(instance);
|
|
778
|
+
runB(instance);
|
|
779
|
+
(instance as unknown as { onMount: () => void }).onMount?.();
|
|
780
|
+
|
|
781
|
+
sa.set(1);
|
|
782
|
+
expect(handlerA).toHaveBeenCalledTimes(1);
|
|
783
|
+
expect(handlerB).not.toHaveBeenCalled();
|
|
784
|
+
|
|
785
|
+
sb.set(1);
|
|
786
|
+
expect(handlerA).toHaveBeenCalledTimes(1);
|
|
787
|
+
expect(handlerB).toHaveBeenCalledTimes(1);
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
// ── When — additional new tests ───────────────────────────────────────────────
|
|
792
|
+
|
|
793
|
+
describe("When decorator — new cases", () => {
|
|
794
|
+
function methodCtxFor(name: string) {
|
|
795
|
+
const initializers: Array<(this: unknown) => void> = [];
|
|
796
|
+
return {
|
|
797
|
+
ctx: {
|
|
798
|
+
name,
|
|
799
|
+
kind: "method" as const,
|
|
800
|
+
addInitializer(fn: (this: unknown) => void) {
|
|
801
|
+
initializers.push(fn);
|
|
802
|
+
},
|
|
803
|
+
} as ClassMethodDecoratorContext,
|
|
804
|
+
run(instance: unknown) {
|
|
805
|
+
initializers.forEach((fn) => { fn.call(instance); });
|
|
806
|
+
},
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
it("@When fires only once — first truthy value; subsequent truthy values are ignored", () => {
|
|
811
|
+
const { ctx, run } = methodCtxFor("onToggle");
|
|
812
|
+
const handler = vi.fn();
|
|
813
|
+
When("toggle")(handler, ctx as unknown as ClassMethodDecoratorContext);
|
|
814
|
+
|
|
815
|
+
const s = signal<number | null>(null);
|
|
816
|
+
const instance = new TestComponent();
|
|
817
|
+
(instance as unknown as Record<string, unknown>).toggle = s;
|
|
818
|
+
run(instance);
|
|
819
|
+
(instance as unknown as { onMount: () => void }).onMount?.();
|
|
820
|
+
|
|
821
|
+
s.set(1); // truthy → fires (first and only time)
|
|
822
|
+
s.set(null); // falsy
|
|
823
|
+
s.set(2); // truthy again — but when() has already disposed itself
|
|
824
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
825
|
+
expect(handler).toHaveBeenCalledWith(1);
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
it("@When callback that throws — onUnmount cleanup still works", () => {
|
|
829
|
+
const { ctx, run } = methodCtxFor("onThrowWhen");
|
|
830
|
+
const throwingHandler = vi.fn(() => { throw new Error("when error"); });
|
|
831
|
+
When("whenProp")(throwingHandler, ctx as unknown as ClassMethodDecoratorContext);
|
|
832
|
+
|
|
833
|
+
const s = signal<number | null>(null);
|
|
834
|
+
const instance = new TestComponent();
|
|
835
|
+
(instance as unknown as Record<string, unknown>).whenProp = s;
|
|
836
|
+
run(instance);
|
|
837
|
+
(instance as unknown as { onMount: () => void }).onMount?.();
|
|
838
|
+
|
|
839
|
+
// Triggering truthy fires handler which throws
|
|
840
|
+
expect(() => { s.set(1); }).toThrow("when error");
|
|
841
|
+
|
|
842
|
+
// onUnmount should still work without crashing
|
|
843
|
+
expect(() => {
|
|
844
|
+
(instance as unknown as { onUnmount: () => void }).onUnmount?.();
|
|
845
|
+
}).not.toThrow();
|
|
846
|
+
});
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
// ── State — additional new tests ──────────────────────────────────────────────
|
|
850
|
+
|
|
851
|
+
describe("State decorator — new cases", () => {
|
|
852
|
+
it("two @State fields can be set independently without cross-contamination", () => {
|
|
853
|
+
const { ctx: ctxX, run: runX } = fieldCtx("x");
|
|
854
|
+
const { ctx: ctxY, run: runY } = fieldCtx("y");
|
|
855
|
+
State()(undefined, ctxX);
|
|
856
|
+
State()(undefined, ctxY);
|
|
857
|
+
|
|
858
|
+
const instance = new TestComponent();
|
|
859
|
+
(instance as unknown as Record<string, unknown>).x = 10;
|
|
860
|
+
(instance as unknown as Record<string, unknown>).y = 20;
|
|
861
|
+
runX(instance);
|
|
862
|
+
runY(instance);
|
|
863
|
+
|
|
864
|
+
(instance as unknown as Record<string, unknown>).x = 99;
|
|
865
|
+
expect((instance as unknown as Record<string, unknown>).x).toBe(99);
|
|
866
|
+
expect((instance as unknown as Record<string, unknown>).y).toBe(20);
|
|
867
|
+
|
|
868
|
+
(instance as unknown as Record<string, unknown>).y = 77;
|
|
869
|
+
expect((instance as unknown as Record<string, unknown>).x).toBe(99);
|
|
870
|
+
expect((instance as unknown as Record<string, unknown>).y).toBe(77);
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
it("setting @State to undefined works (signal stores undefined)", () => {
|
|
874
|
+
const { ctx, run } = fieldCtx("maybeVal");
|
|
875
|
+
State()(undefined, ctx);
|
|
876
|
+
|
|
877
|
+
const instance = new TestComponent();
|
|
878
|
+
(instance as unknown as Record<string, unknown>).maybeVal = "initial";
|
|
879
|
+
run(instance);
|
|
880
|
+
|
|
881
|
+
(instance as unknown as Record<string, unknown>).maybeVal = undefined;
|
|
882
|
+
expect((instance as unknown as Record<string, unknown>).maybeVal).toBeUndefined();
|
|
883
|
+
});
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
// ── Prop — additional new tests ───────────────────────────────────────────────
|
|
887
|
+
|
|
888
|
+
describe("Prop decorator — new cases", () => {
|
|
889
|
+
it("function prop that throws — getter propagates the error", () => {
|
|
890
|
+
const { ctx, run } = fieldCtx("dynProp");
|
|
891
|
+
Prop()(undefined, ctx);
|
|
892
|
+
|
|
893
|
+
const throwingFn = () => { throw new Error("prop error"); };
|
|
894
|
+
const instance = new TestComponent({ dynProp: throwingFn });
|
|
895
|
+
run(instance);
|
|
896
|
+
|
|
897
|
+
expect(() => (instance as unknown as Record<string, unknown>).dynProp).toThrow("prop error");
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
it("undefined passed explicitly vs key not present — both fall back to default", () => {
|
|
901
|
+
const { ctx: ctx1, run: run1 } = fieldCtx("optA");
|
|
902
|
+
const { ctx: ctx2, run: run2 } = fieldCtx("optB");
|
|
903
|
+
Prop()(undefined, ctx1);
|
|
904
|
+
Prop()(undefined, ctx2);
|
|
905
|
+
|
|
906
|
+
// Case 1: key explicitly set to undefined in rawProps
|
|
907
|
+
const instance1 = new TestComponent({ optA: undefined });
|
|
908
|
+
(instance1 as unknown as Record<string, unknown>).optA = "default-a";
|
|
909
|
+
run1(instance1);
|
|
910
|
+
// undefined from rawProps — Prop checks `!== undefined`, so falls back to default
|
|
911
|
+
expect((instance1 as unknown as Record<string, unknown>).optA).toBe("default-a");
|
|
912
|
+
|
|
913
|
+
// Case 2: key not present in rawProps at all
|
|
914
|
+
const instance2 = new TestComponent({});
|
|
915
|
+
(instance2 as unknown as Record<string, unknown>).optB = "default-b";
|
|
916
|
+
run2(instance2);
|
|
917
|
+
expect((instance2 as unknown as Record<string, unknown>).optB).toBe("default-b");
|
|
918
|
+
});
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
// ── Compose — additional new tests ────────────────────────────────────────────
|
|
922
|
+
|
|
923
|
+
describe("@Compose decorator — new cases", () => {
|
|
924
|
+
it("setup() that throws — field access on the instance throws", () => {
|
|
925
|
+
class ThrowingComposable {
|
|
926
|
+
setup(): Record<string, unknown> {
|
|
927
|
+
throw new Error("setup failed");
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const { ctx, run } = fieldCtx("throwing");
|
|
932
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
933
|
+
expect(() => {
|
|
934
|
+
const instance = new TestComponent();
|
|
935
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
936
|
+
Compose(ThrowingComposable as any)(undefined, ctx);
|
|
937
|
+
run(instance);
|
|
938
|
+
}).toThrow("setup failed");
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
it("string argument resolution where the instance property is undefined", () => {
|
|
942
|
+
class ArgComposable {
|
|
943
|
+
constructor(private val: unknown) {}
|
|
944
|
+
setup() { return { resolved: this.val }; }
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const { ctx, run } = fieldCtx("strArgUndef");
|
|
948
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
949
|
+
Compose(ArgComposable as any, "missingProp")(undefined, ctx);
|
|
950
|
+
|
|
951
|
+
const instance = new TestComponent();
|
|
952
|
+
// missingProp is not set on instance — resolves to undefined
|
|
953
|
+
run(instance);
|
|
954
|
+
|
|
955
|
+
const view = (instance as unknown as Record<string, unknown>).strArgUndef as Record<string, unknown>;
|
|
956
|
+
expect(view.resolved).toBeUndefined();
|
|
957
|
+
});
|
|
958
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { Resource } from "../properties/resource";
|
|
4
|
+
|
|
5
|
+
function makeFieldCtx(name: string) {
|
|
6
|
+
const initializers: Array<(this: unknown) => void> = [];
|
|
7
|
+
return {
|
|
8
|
+
ctx: {
|
|
9
|
+
name,
|
|
10
|
+
kind: "field" as const,
|
|
11
|
+
addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
|
|
12
|
+
} as ClassFieldDecoratorContext,
|
|
13
|
+
run(instance: unknown) { initializers.forEach((fn) => { fn.call(instance); }); },
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("@Resource", () => {
|
|
18
|
+
it("binds a Resource object to the field", () => {
|
|
19
|
+
const { ctx, run } = makeFieldCtx("users");
|
|
20
|
+
Resource(() => Promise.resolve([]))(undefined, ctx);
|
|
21
|
+
const instance: Record<string, unknown> = {};
|
|
22
|
+
run(instance);
|
|
23
|
+
const r = instance.users as Record<string, unknown>;
|
|
24
|
+
expect(typeof r.data).toBe("function");
|
|
25
|
+
expect(typeof r.pending).toBe("function");
|
|
26
|
+
expect(typeof r.error).toBe("function");
|
|
27
|
+
expect(typeof r.status).toBe("function");
|
|
28
|
+
expect(typeof r.refetch).toBe("function");
|
|
29
|
+
expect(typeof r.cancel).toBe("function");
|
|
30
|
+
expect(typeof r.mutate).toBe("function");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("starts in pending state when immediate=true (default)", () => {
|
|
34
|
+
const { ctx, run } = makeFieldCtx("data");
|
|
35
|
+
Resource(() => new Promise(() => {}))(undefined, ctx);
|
|
36
|
+
const instance: Record<string, unknown> = {};
|
|
37
|
+
run(instance);
|
|
38
|
+
const r = instance.data as { pending: () => boolean; status: () => string };
|
|
39
|
+
expect(r.pending()).toBe(true);
|
|
40
|
+
expect(r.status()).toBe("pending");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("starts idle when immediate=false", () => {
|
|
44
|
+
const { ctx, run } = makeFieldCtx("data");
|
|
45
|
+
Resource(() => Promise.resolve(42), { immediate: false })(undefined, ctx);
|
|
46
|
+
const instance: Record<string, unknown> = {};
|
|
47
|
+
run(instance);
|
|
48
|
+
const r = instance.data as { status: () => string; pending: () => boolean };
|
|
49
|
+
expect(r.status()).toBe("idle");
|
|
50
|
+
expect(r.pending()).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("resolves data asynchronously", async () => {
|
|
54
|
+
const { ctx, run } = makeFieldCtx("items");
|
|
55
|
+
Resource(() => Promise.resolve([1, 2, 3]))(undefined, ctx);
|
|
56
|
+
const instance: Record<string, unknown> = {};
|
|
57
|
+
run(instance);
|
|
58
|
+
const r = instance.items as { data: () => unknown; status: () => string };
|
|
59
|
+
await vi.waitFor(() => r.status() === "success");
|
|
60
|
+
expect(r.data()).toEqual([1, 2, 3]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("sets error state on rejection", async () => {
|
|
64
|
+
const { ctx, run } = makeFieldCtx("items");
|
|
65
|
+
Resource(() => Promise.reject(new Error("fetch failed")))(undefined, ctx);
|
|
66
|
+
const instance: Record<string, unknown> = {};
|
|
67
|
+
run(instance);
|
|
68
|
+
const r = instance.items as { error: () => Error | null; status: () => string };
|
|
69
|
+
await new Promise((res) => setTimeout(res, 0));
|
|
70
|
+
expect(r.status()).toBe("error");
|
|
71
|
+
expect((r.error() as Error).message).toBe("fetch failed");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("each instance gets its own resource", () => {
|
|
75
|
+
const { ctx, run } = makeFieldCtx("r");
|
|
76
|
+
Resource(() => Promise.resolve(0))(undefined, ctx);
|
|
77
|
+
const a: Record<string, unknown> = {};
|
|
78
|
+
const b: Record<string, unknown> = {};
|
|
79
|
+
run(a);
|
|
80
|
+
run(b);
|
|
81
|
+
expect(a.r).not.toBe(b.r);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("mutate() updates data immediately", async () => {
|
|
85
|
+
const { ctx, run } = makeFieldCtx("r");
|
|
86
|
+
Resource(() => Promise.resolve(1))(undefined, ctx);
|
|
87
|
+
const instance: Record<string, unknown> = {};
|
|
88
|
+
run(instance);
|
|
89
|
+
const r = instance.r as { mutate: (v: number) => void; data: () => number; status: () => string };
|
|
90
|
+
r.mutate(99);
|
|
91
|
+
expect(r.data()).toBe(99);
|
|
92
|
+
expect(r.status()).toBe("success");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("cancel() stops a pending fetch", async () => {
|
|
96
|
+
const { ctx, run } = makeFieldCtx("r");
|
|
97
|
+
Resource(() => new Promise(() => {}))(undefined, ctx);
|
|
98
|
+
const instance: Record<string, unknown> = {};
|
|
99
|
+
run(instance);
|
|
100
|
+
const r = instance.r as { cancel: () => void; status: () => string };
|
|
101
|
+
r.cancel();
|
|
102
|
+
expect(r.status()).toBe("idle");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("uses initialData option", () => {
|
|
106
|
+
const { ctx, run } = makeFieldCtx("r");
|
|
107
|
+
Resource(() => Promise.resolve("new"), { initialData: "default", immediate: false })(undefined, ctx);
|
|
108
|
+
const instance: Record<string, unknown> = {};
|
|
109
|
+
run(instance);
|
|
110
|
+
const r = instance.r as { data: () => string };
|
|
111
|
+
expect(r.data()).toBe("default");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("cancel() called when not pending — no-op, no crash", () => {
|
|
115
|
+
const { ctx, run } = makeFieldCtx("r");
|
|
116
|
+
Resource(() => Promise.resolve(42), { immediate: false })(undefined, ctx);
|
|
117
|
+
const instance: Record<string, unknown> = {};
|
|
118
|
+
run(instance);
|
|
119
|
+
const r = instance.r as { cancel: () => void; status: () => string };
|
|
120
|
+
// Not pending (immediate=false → idle)
|
|
121
|
+
expect(r.status()).toBe("idle");
|
|
122
|
+
expect(() => r.cancel()).not.toThrow();
|
|
123
|
+
expect(r.status()).toBe("idle");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("refetch() called after successful load — re-fetches", async () => {
|
|
127
|
+
let callCount = 0;
|
|
128
|
+
const { ctx, run } = makeFieldCtx("r");
|
|
129
|
+
Resource(() => Promise.resolve(++callCount))(undefined, ctx);
|
|
130
|
+
const instance: Record<string, unknown> = {};
|
|
131
|
+
run(instance);
|
|
132
|
+
const r = instance.r as { refetch: () => void; data: () => number; status: () => string };
|
|
133
|
+
|
|
134
|
+
await vi.waitFor(() => r.status() === "success");
|
|
135
|
+
expect(r.data()).toBe(1);
|
|
136
|
+
|
|
137
|
+
r.refetch();
|
|
138
|
+
await vi.waitFor(() => r.status() === "success" && r.data() === 2);
|
|
139
|
+
expect(callCount).toBe(2);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -61,6 +61,19 @@ describe("initSlots / getSlot", () => {
|
|
|
61
61
|
initSlots(instance, [null, el, undefined]);
|
|
62
62
|
expect(getSlot(instance, "default")).toEqual([el]);
|
|
63
63
|
});
|
|
64
|
+
|
|
65
|
+
it("appends to existing named slot when same slot name appears twice", () => {
|
|
66
|
+
const instance = {};
|
|
67
|
+
const a = document.createElement("div");
|
|
68
|
+
a.setAttribute("slot", "header");
|
|
69
|
+
const b = document.createElement("div");
|
|
70
|
+
b.setAttribute("slot", "header");
|
|
71
|
+
initSlots(instance, [a, b]);
|
|
72
|
+
const headerSlot = getSlot(instance, "header");
|
|
73
|
+
expect(headerSlot).toHaveLength(2);
|
|
74
|
+
expect(headerSlot[0]).toBe(a);
|
|
75
|
+
expect(headerSlot[1]).toBe(b);
|
|
76
|
+
});
|
|
64
77
|
});
|
|
65
78
|
|
|
66
79
|
// ── Slot decorator ───────────────────────────────────────────────────────────
|
|
@@ -116,6 +129,31 @@ describe("Slot decorator", () => {
|
|
|
116
129
|
expect(slot[0]).toBe(el);
|
|
117
130
|
});
|
|
118
131
|
|
|
132
|
+
it("skips warn on direct assignment in production mode", () => {
|
|
133
|
+
class MyComp extends StatefulComponent {
|
|
134
|
+
render() { return null; }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const initializers: Array<(this: unknown) => void> = [];
|
|
138
|
+
const ctx = {
|
|
139
|
+
name: "default",
|
|
140
|
+
kind: "field" as const,
|
|
141
|
+
addInitializer(fn: (this: unknown) => void) { initializers.push(fn); },
|
|
142
|
+
} as ClassFieldDecoratorContext<StatefulComponent>;
|
|
143
|
+
|
|
144
|
+
Slot()(undefined, ctx);
|
|
145
|
+
|
|
146
|
+
const instance = new MyComp() as unknown as Record<string, unknown>;
|
|
147
|
+
initializers.forEach((fn) => { fn.call(instance); });
|
|
148
|
+
|
|
149
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
150
|
+
vi.stubEnv("NODE_ENV", "production");
|
|
151
|
+
(instance as Record<string, unknown>).default = "bad";
|
|
152
|
+
expect(warn).not.toHaveBeenCalled();
|
|
153
|
+
vi.unstubAllEnvs();
|
|
154
|
+
warn.mockRestore();
|
|
155
|
+
});
|
|
156
|
+
|
|
119
157
|
it("warns on direct assignment in non-production", () => {
|
|
120
158
|
class MyComp extends StatefulComponent {
|
|
121
159
|
render() { return null; }
|