@ryupold/vode 1.8.12 → 1.9.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/dist/vode.cjs.min.js +2 -2
- package/dist/vode.d.ts +23 -25
- package/dist/vode.es5.min.js +5 -5
- package/dist/vode.js +30 -7
- package/dist/vode.min.js +1 -1
- package/dist/vode.min.mjs +1 -1
- package/dist/vode.mjs +30 -7
- package/dist/vode.tests.mjs +220 -36
- package/package.json +4 -4
- package/src/state-context.ts +65 -38
- package/src/vode.ts +10 -3
- package/test/tests-app.ts +1 -2
- package/test/tests-catch.ts +61 -1
- package/test/tests-patch-advanced.ts +20 -21
- package/test/tests-state-context.ts +175 -23
package/src/state-context.ts
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DeepPartial, Patchable, PatchableState, RenderPatch } from "./vode";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* State context for type-safe access and manipulation of nested state paths
|
|
5
5
|
* while still be able to access the parent state.
|
|
6
6
|
*/
|
|
7
|
-
export interface StateContext<S extends Patchable<S>, SubState> extends SubContext<SubState> {
|
|
8
|
-
/**
|
|
9
|
-
* parent state
|
|
10
|
-
* @see PatchableState<S>
|
|
11
|
-
*/
|
|
12
|
-
get state(): S;
|
|
13
|
-
}
|
|
7
|
+
export interface StateContext<S extends Patchable<S>, SubState> extends SubContext<SubState> { }
|
|
14
8
|
|
|
15
9
|
/**
|
|
16
10
|
* State context for type-safe access and manipulation of nested sub-state values without knowledge of the parent state.
|
|
@@ -52,43 +46,56 @@ export type ProxySubContext<SubState> = SubContext<SubState> & {
|
|
|
52
46
|
: SubContext<SubState[K]>
|
|
53
47
|
};
|
|
54
48
|
|
|
49
|
+
type ProxyState<SubState> = SubState & {
|
|
50
|
+
[K in keyof SubState]-?: NonNullable<SubState[K]> extends object | null
|
|
51
|
+
? ProxyState<NonNullable<SubState[K]>>
|
|
52
|
+
: SubState[K]
|
|
53
|
+
};
|
|
54
|
+
|
|
55
55
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
56
|
+
* Creates a `ProxyStateContext` for type-safe access and manipulation of nested state.
|
|
57
|
+
*
|
|
58
|
+
* There are two ways to reach a subcontext:
|
|
59
|
+
*
|
|
60
|
+
* **1. Property chaining** — traverse the proxy directly via property access:
|
|
59
61
|
* ```typescript
|
|
60
|
-
* const state = createState({
|
|
61
|
-
* user: {
|
|
62
|
-
* profile: {
|
|
63
|
-
* settings: { theme: 'dark', lang: 'en' }
|
|
64
|
-
* }
|
|
65
|
-
* });
|
|
66
|
-
*
|
|
67
|
-
* // Create a proxy context for the state
|
|
68
62
|
* const ctx = context(state).user.profile.settings;
|
|
69
|
-
*
|
|
70
|
-
* // Access nested state dynamically
|
|
71
|
-
* const settings = ctx.get(); // { theme: 'dark', lang: 'en' }
|
|
72
|
-
*
|
|
73
|
-
* // Update and trigger render
|
|
74
|
-
* ctx.patch({ theme: 'light' });
|
|
75
|
-
*
|
|
76
|
-
* // Update without render (silent mutation)
|
|
77
|
-
* ctx.put({ lang: 'de' });
|
|
78
63
|
* ```
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
64
|
+
*
|
|
65
|
+
* **2. Path producer function** — pass a callback that navigates
|
|
66
|
+
* the state tree; needed if your intermediate path contains 'get', 'put' or 'patch' properties that would conflict with the context API:
|
|
67
|
+
* ```typescript
|
|
68
|
+
* const ctx = context(state, s => s.user.profile.settings);
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* Both forms return a `ProxyStateContext` that supports the same operations:
|
|
72
|
+
* ```typescript
|
|
73
|
+
* ctx.get(); // read current value
|
|
74
|
+
* ctx.patch({ theme: 'light' }); // update and trigger render
|
|
75
|
+
* ctx.put({ lang: 'de' }); // update without render (silent mutation)
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* @param state - The root `PatchableState` to create a context for
|
|
79
|
+
* @param producePath - Optional path producer; receives a proxy of the state and should return the desired sub-node
|
|
80
|
+
* @returns A `ProxyStateContext` rooted at the given path, with further property-chain access available
|
|
82
81
|
*/
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
|
|
83
|
+
export function context<S extends PatchableState, SS = S>(state: S): ProxyStateContext<S, SS>;
|
|
84
|
+
export function context<S extends PatchableState, SS>(state: S, producePath: (ctx: ProxyState<S>) => ProxyState<SS>): ProxyStateContext<S, SS>;
|
|
85
|
+
export function context<S extends PatchableState, SS = S>(state: S, producePath?: (ctx: ProxyState<S>) => ProxyState<SS>): ProxyStateContext<S, SS> {
|
|
86
|
+
if (producePath) {
|
|
87
|
+
const proxy = producePath(proxyState<S>(state, [] as string[]));
|
|
88
|
+
const keys = (proxy as any)["___KeYs___"] as string[];
|
|
89
|
+
return new ProxyStateContextImpl<S, SS>(state, keys) as unknown as ProxyStateContext<S, SS>;
|
|
90
|
+
}
|
|
91
|
+
return new ProxyStateContextImpl<S, S>(state, []) as unknown as ProxyStateContext<S, SS>;
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
class ProxyStateContextImpl<S extends PatchableState, SubState>
|
|
88
95
|
implements StateContext<S, SubState> {
|
|
89
96
|
|
|
90
97
|
constructor(
|
|
91
|
-
|
|
98
|
+
private readonly state: S,
|
|
92
99
|
private readonly keys: string[]
|
|
93
100
|
) {
|
|
94
101
|
function putDeep(value: SubState | DeepPartial<SubState> | undefined | null, target: S | DeepPartial<S>) {
|
|
@@ -150,8 +157,6 @@ class ProxyStateContextImpl<S extends PatchableState, SubState>
|
|
|
150
157
|
|
|
151
158
|
return new Proxy(this, {
|
|
152
159
|
get: (target, prop, receiver) => {
|
|
153
|
-
if (prop === 'state')
|
|
154
|
-
return state;
|
|
155
160
|
|
|
156
161
|
if (prop === 'get')
|
|
157
162
|
return get;
|
|
@@ -162,10 +167,12 @@ class ProxyStateContextImpl<S extends PatchableState, SubState>
|
|
|
162
167
|
if (prop === 'patch')
|
|
163
168
|
return patch;
|
|
164
169
|
|
|
165
|
-
|
|
166
170
|
// otherwise return a new ProxyStateContext for nested access
|
|
167
|
-
|
|
171
|
+
const newKeys = [...keys, String(prop)];
|
|
168
172
|
return new ProxyStateContextImpl<S, any>(target.state, newKeys);
|
|
173
|
+
},
|
|
174
|
+
set: (target: this, p: string | symbol, newValue: any, receiver: any) => {
|
|
175
|
+
throw new Error("ProxyStateContext is not meant to be directly mutated. Use put() or patch() methods on the StateContext instead");
|
|
169
176
|
}
|
|
170
177
|
});
|
|
171
178
|
}
|
|
@@ -174,3 +181,23 @@ class ProxyStateContextImpl<S extends PatchableState, SubState>
|
|
|
174
181
|
put(value: SubState | DeepPartial<SubState>): void { }
|
|
175
182
|
patch(value: SubState | DeepPartial<SubState> | DeepPartial<SubState>[]): void { }
|
|
176
183
|
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
function proxyState<S extends PatchableState>(
|
|
187
|
+
state: S,
|
|
188
|
+
keys: string[]
|
|
189
|
+
) {
|
|
190
|
+
return new Proxy(state, {
|
|
191
|
+
get: (target, prop, receiver) => {
|
|
192
|
+
if (prop === "___KeYs___") {
|
|
193
|
+
return keys;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const newKeys = [...keys, String(prop)];
|
|
197
|
+
return proxyState<S>(state, newKeys);
|
|
198
|
+
},
|
|
199
|
+
set: (target: any, p: string | symbol, newValue: any, receiver: any) => {
|
|
200
|
+
throw new Error("ProxyState is not meant to be directly mutated");
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
package/src/vode.ts
CHANGED
|
@@ -228,10 +228,10 @@ export function app<S extends PatchableState = PatchableState>(
|
|
|
228
228
|
|
|
229
229
|
function renderDom(isAnimated: boolean) {
|
|
230
230
|
const sw = performance.now();
|
|
231
|
-
|
|
232
|
-
_vode.vode = render<S>(_vode.state, container.parentElement as Element, 0, 0, _vode.vode, vom)!;
|
|
231
|
+
_vode.vode = render<S>(_vode.state, container.parentElement as Element, 0, 0, _vode.vode, dom(_vode.state))!;
|
|
233
232
|
|
|
234
|
-
if ((<ContainerNode<S>>container).tagName.
|
|
233
|
+
if ((<ContainerNode<S>>container).tagName.toLowerCase() !== (<Vode<S>>_vode.vode)[0].toLowerCase()) {
|
|
234
|
+
//the tag name was changed during render -> update reference to vode-app-root
|
|
235
235
|
container = _vode.vode.node as Element;
|
|
236
236
|
(<ContainerNode<S>>container)._vode = _vode
|
|
237
237
|
}
|
|
@@ -299,6 +299,13 @@ export function app<S extends PatchableState = PatchableState>(
|
|
|
299
299
|
hydrate<S>(container, true) as AttachedVode<S>,
|
|
300
300
|
dom(<S>state)
|
|
301
301
|
)!;
|
|
302
|
+
|
|
303
|
+
// if during initial render the tag of the root vode was changed (catch or different Tag)
|
|
304
|
+
if (container.tagName.toLowerCase() !== (<Vode<S>>_vode.vode)[0].toLowerCase()) {
|
|
305
|
+
container = _vode.vode.node as Element;
|
|
306
|
+
(container as ContainerNode<S>)._vode = _vode;
|
|
307
|
+
}
|
|
308
|
+
|
|
302
309
|
const continueRendering = _vode.stats.syncRenderPatchCount !== patchCountBefore;
|
|
303
310
|
_vode.isRendering = 0;
|
|
304
311
|
_vode.stats.syncRenderCount++;
|
package/test/tests-app.ts
CHANGED
|
@@ -120,8 +120,7 @@ export default {
|
|
|
120
120
|
|
|
121
121
|
await state.patch([{ a: 10 }, { b: 20 }]);
|
|
122
122
|
|
|
123
|
-
await expect(state
|
|
124
|
-
await expect(state.b).toEqual(20);
|
|
123
|
+
await expect(state).toEqual({ a: 10, b: 20 });
|
|
125
124
|
},
|
|
126
125
|
|
|
127
126
|
"app(): multiple sequential patches both apply": async () => {
|
package/test/tests-catch.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { expect } from "./helper";
|
|
2
|
-
import { app, createState, DIV, ARTICLE, SECTION, P } from "../index";
|
|
2
|
+
import { app, createState, DIV, ARTICLE, SECTION, P, MAIN } from "../index";
|
|
3
3
|
|
|
4
4
|
function setup() {
|
|
5
5
|
const root = document.createElement("div");
|
|
@@ -157,4 +157,64 @@ export default {
|
|
|
157
157
|
]
|
|
158
158
|
);
|
|
159
159
|
},
|
|
160
|
+
|
|
161
|
+
"catch: bubbles up to the root component if deeply nested vodes dont catch it earlier": async () => {
|
|
162
|
+
const root = document.createElement("div");
|
|
163
|
+
const container = document.createElement("div");
|
|
164
|
+
root.appendChild(container);
|
|
165
|
+
|
|
166
|
+
app(container, {}, () =>
|
|
167
|
+
[DIV,
|
|
168
|
+
{
|
|
169
|
+
catch: (s: unknown, err: Error) => [DIV, `caught: ${err.message}`]
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
[MAIN,
|
|
173
|
+
[SECTION,
|
|
174
|
+
[ARTICLE, {
|
|
175
|
+
onMount: () => {
|
|
176
|
+
throw new Error("boom");
|
|
177
|
+
}
|
|
178
|
+
}],
|
|
179
|
+
],
|
|
180
|
+
]
|
|
181
|
+
]
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
await expect(container).toMatch(
|
|
185
|
+
[DIV, "caught: boom"],
|
|
186
|
+
);
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
"catch: if catching in root vode with different Tag -> container will be replaced": async () => {
|
|
190
|
+
const root = document.createElement("div");
|
|
191
|
+
const container = document.createElement("div");
|
|
192
|
+
root.appendChild(container);
|
|
193
|
+
|
|
194
|
+
await expect(root.firstChild === container).toEqual(true);
|
|
195
|
+
|
|
196
|
+
app(container, {}, () =>
|
|
197
|
+
[DIV,
|
|
198
|
+
{
|
|
199
|
+
catch: (s: unknown, err: Error) => [P, `caught: ${err.message}`]
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
[MAIN,
|
|
203
|
+
[SECTION,
|
|
204
|
+
[ARTICLE, {
|
|
205
|
+
onMount: () => {
|
|
206
|
+
throw new Error("boom");
|
|
207
|
+
}
|
|
208
|
+
}],
|
|
209
|
+
],
|
|
210
|
+
]
|
|
211
|
+
]
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
await expect(root.firstChild === container).toEqual(false);
|
|
215
|
+
|
|
216
|
+
await expect(root.firstChild).toMatch(
|
|
217
|
+
[P, "caught: boom"],
|
|
218
|
+
);
|
|
219
|
+
},
|
|
160
220
|
};
|
|
@@ -11,8 +11,8 @@ function setup() {
|
|
|
11
11
|
export default {
|
|
12
12
|
"patch(): generator function yields multiple state updates": async () => {
|
|
13
13
|
const container = setup();
|
|
14
|
-
const state
|
|
15
|
-
app(container, state, (s
|
|
14
|
+
const state = createState({ count: 0 });
|
|
15
|
+
app<typeof state>(container, state, (s) => [DIV, String(s.count)]);
|
|
16
16
|
|
|
17
17
|
await expect(state.count).toEqual(0);
|
|
18
18
|
|
|
@@ -30,8 +30,8 @@ export default {
|
|
|
30
30
|
|
|
31
31
|
"patch(): async generator yields over time": async () => {
|
|
32
32
|
const container = setup();
|
|
33
|
-
const state
|
|
34
|
-
app(container, state, (s
|
|
33
|
+
const state = createState({ phase: "start", value: 0 });
|
|
34
|
+
app<typeof state>(container, state, (s) => [DIV, s.phase, String(s.value)]);
|
|
35
35
|
|
|
36
36
|
await expect(state.phase).toEqual("start");
|
|
37
37
|
|
|
@@ -53,38 +53,37 @@ export default {
|
|
|
53
53
|
|
|
54
54
|
"patch(): Promise resolves and applies patch": async () => {
|
|
55
55
|
const container = setup();
|
|
56
|
-
const state
|
|
57
|
-
app(container, state, (s
|
|
58
|
-
|
|
59
|
-
state.patch(Promise.resolve({ msg: "after" }));
|
|
56
|
+
const state = createState({ msg: "before" });
|
|
57
|
+
app<typeof state>(container, state, (s) => [DIV, s.msg]);
|
|
60
58
|
|
|
61
|
-
await
|
|
59
|
+
await state.patch(Promise.resolve({ msg: "after" }));
|
|
62
60
|
|
|
63
|
-
await expect(state
|
|
61
|
+
await expect(state).toEqual({ msg: "after" });
|
|
64
62
|
await expect(container).toMatch([DIV, "after"]);
|
|
65
63
|
},
|
|
66
64
|
|
|
67
65
|
"patch(): array with empty patches applies nothing": async () => {
|
|
68
66
|
const container = setup();
|
|
69
|
-
const state
|
|
70
|
-
app(container, state, (s
|
|
67
|
+
const state = createState({ x: 1, y: 2 });
|
|
68
|
+
app(container, state, (s) => [DIV]);
|
|
69
|
+
|
|
70
|
+
await state.patch([{}, {}]);
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
await expect(state
|
|
74
|
-
await expect(state.y).toEqual(2);
|
|
72
|
+
await delay(10);
|
|
73
|
+
await expect(state).toEqual({ x: 1, y: 2 });
|
|
75
74
|
},
|
|
76
75
|
|
|
77
76
|
"patch(): array with null/undefined items skips them": async () => {
|
|
78
77
|
const container = setup();
|
|
79
|
-
const state
|
|
80
|
-
app(container, state, (s
|
|
78
|
+
const state = createState({ x: 0, y: 0 });
|
|
79
|
+
app<typeof state>(container, state, (s) => [DIV, String(s.x), String(s.y)]);
|
|
81
80
|
|
|
82
81
|
state.patch([null, { x: 10 }, undefined, { y: 20 }]);
|
|
83
82
|
|
|
84
|
-
await
|
|
85
|
-
|
|
86
|
-
await expect(state.
|
|
87
|
-
|
|
83
|
+
await expect(() => expect(state.x).toEqual(10))
|
|
84
|
+
.toSucceedAsync();
|
|
85
|
+
await expect(() => expect(state.y).toEqual(20))
|
|
86
|
+
.toSucceedAsync();
|
|
88
87
|
},
|
|
89
88
|
|
|
90
89
|
"patch(): returns Promise for generator functions, can be awaited": async () => {
|
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
import { expect } from "./helper";
|
|
2
|
-
import { context
|
|
2
|
+
import { context } from "../src/state-context";
|
|
3
3
|
import { createState } from "../src/vode";
|
|
4
4
|
|
|
5
5
|
export default {
|
|
6
|
-
"
|
|
7
|
-
const state = createState({ x: 10 });
|
|
8
|
-
const ctx = context(state);
|
|
9
|
-
|
|
10
|
-
await expect((ctx).state === state)
|
|
11
|
-
.toEqual(true);
|
|
12
|
-
},
|
|
13
|
-
|
|
14
|
-
"StateContext.get() returns whole state": async () => {
|
|
6
|
+
"context(s)...get(): returns whole state": async () => {
|
|
15
7
|
const state = createState({ a: 1, b: 2 });
|
|
16
8
|
const ctx = context(state);
|
|
17
9
|
|
|
@@ -20,7 +12,7 @@ export default {
|
|
|
20
12
|
},
|
|
21
13
|
|
|
22
14
|
|
|
23
|
-
"
|
|
15
|
+
"context(s)...get(): deep nested": async () => {
|
|
24
16
|
const state = createState({ a: { b: { c: 42 } } });
|
|
25
17
|
const ctx = context(state);
|
|
26
18
|
|
|
@@ -28,7 +20,7 @@ export default {
|
|
|
28
20
|
.toEqual(42);
|
|
29
21
|
},
|
|
30
22
|
|
|
31
|
-
"
|
|
23
|
+
"context(s)...get(): missing nested path returns undefined": async () => {
|
|
32
24
|
const state = createState({ a: {} });
|
|
33
25
|
const ctx = context(state);
|
|
34
26
|
|
|
@@ -36,7 +28,7 @@ export default {
|
|
|
36
28
|
.toEqual(undefined);
|
|
37
29
|
},
|
|
38
30
|
|
|
39
|
-
"
|
|
31
|
+
"context(s)...put(): silently mutates state": async () => {
|
|
40
32
|
const state = createState({ a: { b: 1 } });
|
|
41
33
|
const ctx = context(state);
|
|
42
34
|
ctx.a.b.put(2);
|
|
@@ -45,7 +37,7 @@ export default {
|
|
|
45
37
|
.toEqual(2);
|
|
46
38
|
},
|
|
47
39
|
|
|
48
|
-
"
|
|
40
|
+
"context(s)...put(): on nested object replaces the sub-object": async () => {
|
|
49
41
|
const state = createState({ a: { b: { x: 1, y: 2 } } });
|
|
50
42
|
const ctx = context(state);
|
|
51
43
|
ctx.a.b.put({ y: 99 });
|
|
@@ -54,7 +46,7 @@ export default {
|
|
|
54
46
|
.toEqual({ y: 99 });
|
|
55
47
|
},
|
|
56
48
|
|
|
57
|
-
"
|
|
49
|
+
"context(s)...put(): at root level with empty keys": async () => {
|
|
58
50
|
const state = createState({ a: 1, b: 2 });
|
|
59
51
|
const ctx = context(state);
|
|
60
52
|
ctx.put({ b: undefined });
|
|
@@ -63,7 +55,7 @@ export default {
|
|
|
63
55
|
.toEqual({ a: 1 });
|
|
64
56
|
},
|
|
65
57
|
|
|
66
|
-
"
|
|
58
|
+
"context(s)...patch(): calls state.patch with proper deep partial": async () => {
|
|
67
59
|
const state = createState({ a: { b: 1 } });
|
|
68
60
|
const ctx = context(state);
|
|
69
61
|
ctx.a.b.patch(2);
|
|
@@ -76,7 +68,7 @@ export default {
|
|
|
76
68
|
.toEqual({ a: { b: 2 } });
|
|
77
69
|
},
|
|
78
70
|
|
|
79
|
-
"
|
|
71
|
+
"context(s)...patch(): async wraps in array": async () => {
|
|
80
72
|
const state = createState({ a: { b: 1 } });
|
|
81
73
|
const ctx = context(state);
|
|
82
74
|
ctx.a.b.patch(2, true);
|
|
@@ -91,7 +83,7 @@ export default {
|
|
|
91
83
|
.toEqual({ a: { b: 2 } });
|
|
92
84
|
},
|
|
93
85
|
|
|
94
|
-
"
|
|
86
|
+
"context(s)...patch(): on nested deep path three levels": async () => {
|
|
95
87
|
const state = createState({ x: { y: { z: 0 } } });
|
|
96
88
|
const ctx = context(state);
|
|
97
89
|
ctx.x.y.z.patch(100);
|
|
@@ -104,7 +96,7 @@ export default {
|
|
|
104
96
|
.toEqual({ x: { y: { z: 100 } } });
|
|
105
97
|
},
|
|
106
98
|
|
|
107
|
-
"
|
|
99
|
+
"context(s)...put(): with intermediate null creates objects along the path": async () => {
|
|
108
100
|
const state = createState({ a: null as { b: number } | null });
|
|
109
101
|
const ctx = context(state);
|
|
110
102
|
|
|
@@ -116,7 +108,7 @@ export default {
|
|
|
116
108
|
await expect(state.a?.b).toEqual(undefined);
|
|
117
109
|
},
|
|
118
110
|
|
|
119
|
-
"
|
|
111
|
+
"context(s)...put(): with three-level intermediate null": async () => {
|
|
120
112
|
const state = createState({ a: null as { b: { c: number } } | null });
|
|
121
113
|
const ctx = context(state);
|
|
122
114
|
|
|
@@ -125,7 +117,7 @@ export default {
|
|
|
125
117
|
await expect(state.a?.b.c).toEqual(99);
|
|
126
118
|
},
|
|
127
119
|
|
|
128
|
-
"
|
|
120
|
+
"context(s)...put(): with multiple intermediate nulls": async () => {
|
|
129
121
|
const state = createState({ a: { x: null as { z: string } | null, y: 1 } });
|
|
130
122
|
const ctx = context(state);
|
|
131
123
|
|
|
@@ -135,11 +127,171 @@ export default {
|
|
|
135
127
|
await expect(state.a.y).toEqual(1);
|
|
136
128
|
},
|
|
137
129
|
|
|
138
|
-
"
|
|
130
|
+
"context(s)...put(): merges into existing object properties via Object.assign": async () => {
|
|
139
131
|
const state = createState({ items: { count: 0, name: "test", hidden: false } });
|
|
140
132
|
const ctx = context(state);
|
|
141
|
-
// Line 110-111: when existing value is object and new value is object, Object.assign merges
|
|
142
133
|
ctx.items.put({ count: 5 });
|
|
143
134
|
await expect(state.items).toEqual({ count: 5, name: "test", hidden: false });
|
|
144
135
|
},
|
|
136
|
+
|
|
137
|
+
"context(state, s => s).get(): returns whole state": async () => {
|
|
138
|
+
const state = createState({ a: 1, b: 2 });
|
|
139
|
+
const ctx = context(state, s => s);
|
|
140
|
+
|
|
141
|
+
await expect(ctx.get())
|
|
142
|
+
.toEqual({ a: 1, b: 2 });
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
"context(state, s => s.a.b.c).get(): deep nested": async () => {
|
|
146
|
+
const state = createState({ a: { b: { c: 42 } } });
|
|
147
|
+
const ctx = context(state, s => s.a.b.c);
|
|
148
|
+
|
|
149
|
+
await expect(ctx.get())
|
|
150
|
+
.toEqual(42);
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
"context(state, s => s.a.b).get(): missing nested path returns undefined": async () => {
|
|
154
|
+
const state = createState({ a: {} });
|
|
155
|
+
const ctx = context(state, s => (s.a as any).b);
|
|
156
|
+
|
|
157
|
+
await expect(ctx.get())
|
|
158
|
+
.toEqual(undefined);
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
"context(state, s => s.a.b).put(): silently mutates state": async () => {
|
|
162
|
+
const state = createState({ a: { b: 1 } });
|
|
163
|
+
const ctx = context(state, s => s.a.b);
|
|
164
|
+
ctx.put(2);
|
|
165
|
+
|
|
166
|
+
await expect(state.a.b)
|
|
167
|
+
.toEqual(2);
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
"context(state, s => s.a.b).put(): on nested object replaces the sub-object": async () => {
|
|
171
|
+
const state = createState({ a: { b: { x: 1, y: 2 } } });
|
|
172
|
+
const ctx = context(state, s => s.a.b);
|
|
173
|
+
ctx.put({ y: 99 });
|
|
174
|
+
|
|
175
|
+
await expect(state.a.b)
|
|
176
|
+
.toEqual({ y: 99 });
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
"context(state, s => s).put(): at root level with empty keys": async () => {
|
|
180
|
+
const state = createState({ a: 1, b: 2 });
|
|
181
|
+
const ctx = context(state, s => s);
|
|
182
|
+
ctx.put({ b: undefined });
|
|
183
|
+
|
|
184
|
+
await expect(state)
|
|
185
|
+
.toEqual({ a: 1 });
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
"context(state, s => s.a.b).patch(): calls state.patch with proper deep partial": async () => {
|
|
189
|
+
const state = createState({ a: { b: 1 } });
|
|
190
|
+
const ctx = context(state, s => s.a.b);
|
|
191
|
+
ctx.patch(2);
|
|
192
|
+
|
|
193
|
+
const patches = (state as any).patch.initialPatches;
|
|
194
|
+
|
|
195
|
+
await expect(patches.length)
|
|
196
|
+
.toEqual(1);
|
|
197
|
+
await expect(patches[0])
|
|
198
|
+
.toEqual({ a: { b: 2 } });
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
"context(state, s => s.a.b).patch(): async wraps in array": async () => {
|
|
202
|
+
const state = createState({ a: { b: 1 } });
|
|
203
|
+
const ctx = context(state, s => s.a.b);
|
|
204
|
+
ctx.patch(2, true);
|
|
205
|
+
|
|
206
|
+
const patches = (state as any).patch.initialPatches;
|
|
207
|
+
|
|
208
|
+
await expect(patches.length)
|
|
209
|
+
.toEqual(1);
|
|
210
|
+
await expect(Array.isArray(patches[0]))
|
|
211
|
+
.toEqual(true);
|
|
212
|
+
await expect(patches[0][0])
|
|
213
|
+
.toEqual({ a: { b: 2 } });
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
"context(state, s => s.x.y.z).patch(): on nested deep path three levels": async () => {
|
|
217
|
+
const state = createState({ x: { y: { z: 0 } } });
|
|
218
|
+
const ctx = context(state, s => s.x.y.z);
|
|
219
|
+
ctx.patch(100);
|
|
220
|
+
|
|
221
|
+
const patches = (state as any).patch.initialPatches;
|
|
222
|
+
|
|
223
|
+
await expect(patches.length)
|
|
224
|
+
.toEqual(1);
|
|
225
|
+
await expect(patches[0])
|
|
226
|
+
.toEqual({ x: { y: { z: 100 } } });
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
"context(state, s => s.x).y.z: continue proxy sub-state targeting": async () => {
|
|
230
|
+
const state = createState({ x: { y: { z: 0 } } });
|
|
231
|
+
const ctx = context(state, s => s.x).y.z;
|
|
232
|
+
ctx.patch(100);
|
|
233
|
+
|
|
234
|
+
const patches = (state as any).patch.initialPatches;
|
|
235
|
+
|
|
236
|
+
await expect(patches.length)
|
|
237
|
+
.toEqual(1);
|
|
238
|
+
await expect(patches[0])
|
|
239
|
+
.toEqual({ x: { y: { z: 100 } } });
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
"context(state, s => s.a.b).put(): with intermediate null creates objects": async () => {
|
|
243
|
+
const state = createState({ a: null as { b: number } | null });
|
|
244
|
+
const ctx = context(state, s => s.a.b);
|
|
245
|
+
|
|
246
|
+
ctx.put(42);
|
|
247
|
+
await expect(state.a?.b).toEqual(42);
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
"context(state, s => s.a.b.c).put(): with three-level intermediate null": async () => {
|
|
251
|
+
const state = createState({ a: null as { b: { c: number } } | null });
|
|
252
|
+
const ctx = context(state, s => s.a.b.c);
|
|
253
|
+
|
|
254
|
+
ctx.put(99);
|
|
255
|
+
|
|
256
|
+
await expect(state.a?.b.c).toEqual(99);
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
"context(state, s => s.a.x.z).put(): with multiple intermediate nulls": async () => {
|
|
260
|
+
const state = createState({ a: { x: null as { z: string } | null, y: 1 } });
|
|
261
|
+
const ctx = context(state, s => s.a.x.z);
|
|
262
|
+
|
|
263
|
+
ctx.put("deep");
|
|
264
|
+
|
|
265
|
+
await expect(state.a.x?.z).toEqual("deep");
|
|
266
|
+
await expect(state.a.y).toEqual(1);
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
"context(state, s => s.items).put(): merges into existing object properties via Object.assign": async () => {
|
|
270
|
+
const state = createState({ items: { count: 0, name: "test", hidden: false } });
|
|
271
|
+
const ctx = context(state, s => s.items);
|
|
272
|
+
ctx.put({ count: 5 });
|
|
273
|
+
await expect(state.items).toEqual({ count: 5, name: "test", hidden: false });
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
"context(state, s => s.get|put|patch...): 'get','put','patch' as intermediate properties without conflict": async () => {
|
|
277
|
+
const state = createState({
|
|
278
|
+
endpoints: {
|
|
279
|
+
get: { count: 1 },
|
|
280
|
+
put: { count: 2 },
|
|
281
|
+
patch: { count: 3 }
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const getCtx = context(state, s => s.endpoints.get);
|
|
286
|
+
await expect(getCtx.get()).toEqual({ count: 1 });
|
|
287
|
+
|
|
288
|
+
const putCtx = context(state, s => s.endpoints.put);
|
|
289
|
+
putCtx.put({ count: 99 });
|
|
290
|
+
await expect(state.endpoints.put).toEqual({ count: 99 });
|
|
291
|
+
|
|
292
|
+
const patchCtx = context(state, s => s.endpoints.patch);
|
|
293
|
+
patchCtx.patch({ count: 42 });
|
|
294
|
+
const patches = (state as any).patch.initialPatches;
|
|
295
|
+
await expect(patches[0]).toEqual({ endpoints: { patch: { count: 42 } } });
|
|
296
|
+
},
|
|
145
297
|
};
|