@ryupold/vode 1.8.11 → 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 +3 -3
- package/dist/vode.d.ts +23 -25
- package/dist/vode.es5.min.js +6 -6
- package/dist/vode.js +32 -9
- package/dist/vode.min.js +1 -1
- package/dist/vode.min.mjs +1 -1
- package/dist/vode.mjs +32 -9
- package/dist/vode.tests.mjs +281 -45
- package/package.json +4 -4
- package/src/merge-class.ts +3 -3
- package/src/state-context.ts +65 -38
- package/src/vode.ts +10 -3
- package/test/helper.ts +1 -1
- package/test/tests-app.ts +42 -2
- package/test/tests-catch.ts +61 -1
- package/test/tests-examples.ts +3 -3
- package/test/tests-mergeClass.ts +11 -4
- package/test/tests-mergeProps.ts +5 -0
- package/test/tests-patch-advanced.ts +20 -21
- package/test/tests-state-context.ts +181 -21
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/helper.ts
CHANGED
|
@@ -255,7 +255,7 @@ export class Expectation {
|
|
|
255
255
|
} else {
|
|
256
256
|
attributeValue = (e as HTMLElement).getAttribute(k);
|
|
257
257
|
}
|
|
258
|
-
if (
|
|
258
|
+
if (attributeValue === null) {
|
|
259
259
|
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\nan element <${tag(cv)?.toUpperCase()}>\n\nwith attribute [${k}="${val}"]\n\nbut it was not found${failSuffix}`);
|
|
260
260
|
}
|
|
261
261
|
if (attributeValue !== val) {
|
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 () => {
|
|
@@ -339,4 +338,45 @@ export default {
|
|
|
339
338
|
const el = (container as any)._vode.vode.node;
|
|
340
339
|
expect(el.onclick).toBeA("function");
|
|
341
340
|
},
|
|
341
|
+
|
|
342
|
+
"app(): class as array renders correctly": async () => {
|
|
343
|
+
const root = document.createElement("div");
|
|
344
|
+
const container = document.createElement("div");
|
|
345
|
+
root.appendChild(container);
|
|
346
|
+
|
|
347
|
+
app(container, {}, () =>
|
|
348
|
+
[DIV, { class: ["foo", "bar", "baz"] }, "text"]
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
await expect(container).toMatch([DIV, { class: "foo bar baz" }, "text"]);
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
"app(): class as number becomes empty string": async () => {
|
|
355
|
+
const root = document.createElement("div");
|
|
356
|
+
const container = document.createElement("div");
|
|
357
|
+
root.appendChild(container);
|
|
358
|
+
|
|
359
|
+
app(container, {}, () =>
|
|
360
|
+
[DIV, { class: 123 as any }, "text"]
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
await expect(container).toMatch([DIV, { class: "" }, "text"]);
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
"app(): style object to string transition": async () => {
|
|
367
|
+
const root = document.createElement("div");
|
|
368
|
+
const container = document.createElement("div");
|
|
369
|
+
root.appendChild(container);
|
|
370
|
+
const state: any = { useObject: true };
|
|
371
|
+
|
|
372
|
+
app(container, state, (s: any) =>
|
|
373
|
+
[DIV, { style: s.useObject ? { color: "red" } : "color: blue" }, "text"]
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
await expect(container).toMatch([DIV, "text"]);
|
|
377
|
+
|
|
378
|
+
state.patch({ useObject: false });
|
|
379
|
+
|
|
380
|
+
await expect(container).toMatch([DIV, "text"]);
|
|
381
|
+
},
|
|
342
382
|
};
|
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
|
};
|
package/test/tests-examples.ts
CHANGED
|
@@ -91,7 +91,7 @@ export default {
|
|
|
91
91
|
[INPUT],
|
|
92
92
|
[BUTTON, "Add"],
|
|
93
93
|
[NAV,
|
|
94
|
-
[BUTTON, "All"],
|
|
94
|
+
[BUTTON, { class: 'active' }, "All"],
|
|
95
95
|
[BUTTON, "Active"],
|
|
96
96
|
[BUTTON, "Done"],
|
|
97
97
|
],
|
|
@@ -115,7 +115,7 @@ export default {
|
|
|
115
115
|
[BUTTON, "Add"],
|
|
116
116
|
[NAV,
|
|
117
117
|
[BUTTON, "All"],
|
|
118
|
-
[BUTTON, "Active"],
|
|
118
|
+
[BUTTON, { class: 'active' }, "Active"],
|
|
119
119
|
[BUTTON, "Done"],
|
|
120
120
|
],
|
|
121
121
|
[UL,
|
|
@@ -135,7 +135,7 @@ export default {
|
|
|
135
135
|
[NAV,
|
|
136
136
|
[BUTTON, "All"],
|
|
137
137
|
[BUTTON, "Active"],
|
|
138
|
-
[BUTTON, "Done"],
|
|
138
|
+
[BUTTON, { class: 'active' }, "Done"],
|
|
139
139
|
],
|
|
140
140
|
[UL,
|
|
141
141
|
[LI, "[X] Walk dog"],
|
package/test/tests-mergeClass.ts
CHANGED
|
@@ -43,12 +43,14 @@ export default {
|
|
|
43
43
|
await expect(mergeClass({ foo: true, bar: true }, { bar: false, baz: true })).toEqual({ foo: true, bar: false, baz: true });
|
|
44
44
|
},
|
|
45
45
|
|
|
46
|
-
"mergeClass(): object and array": async () => {
|
|
47
|
-
await expect(mergeClass({ foo: true }, ["bar", "baz"])).toEqual({ foo: true,
|
|
46
|
+
"mergeClass(): object and array (array items become class names with true)": async () => {
|
|
47
|
+
await expect(mergeClass({ foo: true }, ["bar", "baz"])).toEqual({ foo: true, bar: true, baz: true });
|
|
48
|
+
await expect(mergeClass({ active: true }, ["btn", "primary"])).toEqual({ active: true, btn: true, primary: true });
|
|
48
49
|
},
|
|
49
50
|
|
|
50
|
-
"mergeClass(): array and object": async () => {
|
|
51
|
-
await expect(mergeClass(["foo", "bar"], { baz: true, qux: false })).toEqual({
|
|
51
|
+
"mergeClass(): array and object (object keys become class names)": async () => {
|
|
52
|
+
await expect(mergeClass(["foo", "bar"], { baz: true, qux: false })).toEqual({ foo: true, bar: true, baz: true, qux: false });
|
|
53
|
+
await expect(mergeClass(["a", "b"], { c: true, d: false })).toEqual({ a: true, b: true, c: true, d: false });
|
|
52
54
|
},
|
|
53
55
|
|
|
54
56
|
"mergeClass(): falsy entries are skipped": async () => {
|
|
@@ -59,5 +61,10 @@ export default {
|
|
|
59
61
|
"mergeClass(): multiple args (3+)": async () => {
|
|
60
62
|
await expect(mergeClass("a", "b", "c")).toEqual("a b c");
|
|
61
63
|
await expect(mergeClass("x", null, ["y", "z"], "w")).toEqual("y z x w");
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
"mergeClass(): incompatible types throw": async () => {
|
|
67
|
+
await expect(() => (mergeClass as any)(123 as any, "foo")).toFail();
|
|
68
|
+
await expect(() => (mergeClass as any)("foo", 456 as any)).toFail();
|
|
62
69
|
}
|
|
63
70
|
};
|
package/test/tests-mergeProps.ts
CHANGED
|
@@ -11,6 +11,11 @@ export default {
|
|
|
11
11
|
await expect(mergeProps(p) === p).toEqual(true);
|
|
12
12
|
},
|
|
13
13
|
|
|
14
|
+
"mergeProps(): single falsy arg returns undefined": async () => {
|
|
15
|
+
await expect(mergeProps(null)).toEqual(undefined);
|
|
16
|
+
await expect(mergeProps(undefined)).toEqual(undefined);
|
|
17
|
+
},
|
|
18
|
+
|
|
14
19
|
"mergeProps(): two plain objects merged": async () => {
|
|
15
20
|
await expect(mergeProps({ a: 1 }, { b: 2 })).toEqual({ a: 1, b: 2 });
|
|
16
21
|
},
|
|
@@ -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 () => {
|