@ryupold/vode 1.8.10 → 1.8.12
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/.github/workflows/publish.yml +5 -0
- package/.github/workflows/tests.yml +1 -0
- package/README.md +36 -2
- package/dist/vode.cjs.min.js +2 -2
- package/dist/vode.d.ts +1 -1
- package/dist/vode.es5.min.js +2 -2
- package/dist/vode.js +7 -7
- package/dist/vode.min.js +1 -1
- package/dist/vode.min.mjs +1 -1
- package/dist/vode.mjs +7 -7
- package/dist/vode.tests.mjs +393 -169
- package/log.txt +1 -0
- package/package.json +1 -1
- package/src/merge-class.ts +3 -3
- package/src/vode.ts +8 -8
- package/test/helper.ts +20 -12
- package/test/mocks.ts +26 -14
- package/test/tests-app.ts +41 -0
- package/test/tests-examples.ts +3 -3
- package/test/tests-mergeClass.ts +11 -4
- package/test/tests-mergeProps.ts +5 -0
- package/test/tests-mount-unmount.ts +219 -142
- package/test/tests-patch-advanced.ts +108 -2
- package/test/tests-state-context.ts +8 -0
package/dist/vode.tests.mjs
CHANGED
|
@@ -28,7 +28,7 @@ function app(container, state, dom, ...initialPatches) {
|
|
|
28
28
|
_vode.stats.liveEffectCount++;
|
|
29
29
|
try {
|
|
30
30
|
const resolvedPatch = await action;
|
|
31
|
-
patchableState.patch(resolvedPatch, isAnimated);
|
|
31
|
+
await patchableState.patch(resolvedPatch, isAnimated);
|
|
32
32
|
} finally {
|
|
33
33
|
_vode.stats.liveEffectCount--;
|
|
34
34
|
}
|
|
@@ -41,13 +41,13 @@ function app(container, state, dom, ...initialPatches) {
|
|
|
41
41
|
while (v.done === false) {
|
|
42
42
|
_vode.stats.liveEffectCount++;
|
|
43
43
|
try {
|
|
44
|
-
patchableState.patch(v.value, isAnimated);
|
|
44
|
+
await patchableState.patch(v.value, isAnimated);
|
|
45
45
|
v = await generator.next();
|
|
46
46
|
} finally {
|
|
47
47
|
_vode.stats.liveEffectCount--;
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
patchableState.patch(v.value, isAnimated);
|
|
50
|
+
await patchableState.patch(v.value, isAnimated);
|
|
51
51
|
} finally {
|
|
52
52
|
_vode.stats.liveEffectCount--;
|
|
53
53
|
}
|
|
@@ -63,9 +63,9 @@ function app(container, state, dom, ...initialPatches) {
|
|
|
63
63
|
if (!action || typeof action !== "object") return;
|
|
64
64
|
_vode.stats.patchCount++;
|
|
65
65
|
if (action?.next) {
|
|
66
|
-
generatorPatch(action, isAnimated);
|
|
66
|
+
return generatorPatch(action, isAnimated);
|
|
67
67
|
} else if (action.then) {
|
|
68
|
-
promisePatch(action, isAnimated);
|
|
68
|
+
return promisePatch(action, isAnimated);
|
|
69
69
|
} else if (Array.isArray(action)) {
|
|
70
70
|
if (action.length > 0) {
|
|
71
71
|
for (const p of action) {
|
|
@@ -688,8 +688,6 @@ function mergeClass(...classes) {
|
|
|
688
688
|
finalClass = { [a]: true, ...b };
|
|
689
689
|
} else if (typeof a === "object" && typeof b === "string") {
|
|
690
690
|
finalClass = { ...a, [b]: true };
|
|
691
|
-
} else if (typeof a === "object" && typeof b === "object") {
|
|
692
|
-
finalClass = { ...a, ...b };
|
|
693
691
|
} else if (typeof a === "object" && Array.isArray(b)) {
|
|
694
692
|
const aa = { ...a };
|
|
695
693
|
for (const item of b) {
|
|
@@ -705,6 +703,8 @@ function mergeClass(...classes) {
|
|
|
705
703
|
aa[bKey] = b[bKey];
|
|
706
704
|
}
|
|
707
705
|
finalClass = aa;
|
|
706
|
+
} else if (typeof a === "object" && typeof b === "object") {
|
|
707
|
+
finalClass = { ...a, ...b };
|
|
708
708
|
} else throw new Error(`cannot merge classes of ${a} (${typeof a}) and ${b} (${typeof b})`);
|
|
709
709
|
}
|
|
710
710
|
return finalClass;
|
|
@@ -1206,19 +1206,25 @@ ${other}${failSuffix}`);
|
|
|
1206
1206
|
waitTimeMs
|
|
1207
1207
|
);
|
|
1208
1208
|
}
|
|
1209
|
-
toSucceed() {
|
|
1209
|
+
toSucceed(failMessage) {
|
|
1210
|
+
const failSuffix = failMessage ? `
|
|
1211
|
+
|
|
1212
|
+
${failMessage}` : "";
|
|
1210
1213
|
if (typeof this.what !== "function") {
|
|
1211
1214
|
throw new ExpectationError(this, `expected a function
|
|
1212
1215
|
|
|
1213
|
-
but it is a ${typeof this.what}`);
|
|
1216
|
+
but it is a ${typeof this.what}${failSuffix}`);
|
|
1214
1217
|
}
|
|
1215
1218
|
return this.what();
|
|
1216
1219
|
}
|
|
1217
|
-
toFail() {
|
|
1220
|
+
toFail(failMessage) {
|
|
1221
|
+
const failSuffix = failMessage ? `
|
|
1222
|
+
|
|
1223
|
+
${failMessage}` : "";
|
|
1218
1224
|
if (typeof this.what !== "function") {
|
|
1219
1225
|
throw new ExpectationError(this, `expected a function
|
|
1220
1226
|
|
|
1221
|
-
but it is a ${typeof this.what}`);
|
|
1227
|
+
but it is a ${typeof this.what}${failSuffix}`);
|
|
1222
1228
|
}
|
|
1223
1229
|
let r;
|
|
1224
1230
|
try {
|
|
@@ -1230,25 +1236,34 @@ but it is a ${typeof this.what}`);
|
|
|
1230
1236
|
|
|
1231
1237
|
but it succeeded with a result of type ${typeof r}
|
|
1232
1238
|
|
|
1233
|
-
${r}`);
|
|
1239
|
+
${r}${failSuffix}`);
|
|
1234
1240
|
}
|
|
1235
|
-
toSucceedAsync(waitTime = 100) {
|
|
1241
|
+
toSucceedAsync(failMessage, waitTime = 100) {
|
|
1242
|
+
const failSuffix = failMessage ? `
|
|
1243
|
+
|
|
1244
|
+
${failMessage}` : "";
|
|
1236
1245
|
if (typeof this.what !== "function") {
|
|
1237
1246
|
throw new ExpectationError(this, `expected a function
|
|
1238
1247
|
|
|
1239
|
-
but it is a ${typeof this.what}`);
|
|
1248
|
+
but it is a ${typeof this.what}${failSuffix}`);
|
|
1240
1249
|
}
|
|
1241
1250
|
return retry(() => this.what(), waitTime);
|
|
1242
1251
|
}
|
|
1243
|
-
async toFailAsync() {
|
|
1252
|
+
async toFailAsync(failMessage) {
|
|
1253
|
+
const failSuffix = failMessage ? `
|
|
1254
|
+
|
|
1255
|
+
${failMessage}` : "";
|
|
1244
1256
|
if (typeof this.what !== "function") {
|
|
1245
1257
|
throw new ExpectationError(this, `expected a function
|
|
1246
1258
|
|
|
1247
|
-
but it is a ${typeof this.what}`);
|
|
1259
|
+
but it is a ${typeof this.what}${failSuffix}`);
|
|
1248
1260
|
}
|
|
1249
1261
|
let r;
|
|
1250
1262
|
try {
|
|
1251
|
-
|
|
1263
|
+
if (typeof this.what === "function")
|
|
1264
|
+
r = await this.what();
|
|
1265
|
+
else
|
|
1266
|
+
r = await this.what;
|
|
1252
1267
|
} catch (err) {
|
|
1253
1268
|
return err;
|
|
1254
1269
|
}
|
|
@@ -1256,7 +1271,7 @@ but it is a ${typeof this.what}`);
|
|
|
1256
1271
|
|
|
1257
1272
|
but it succeeded with a result of type ${typeof r}
|
|
1258
1273
|
|
|
1259
|
-
${r}`);
|
|
1274
|
+
${r}${failSuffix}`);
|
|
1260
1275
|
}
|
|
1261
1276
|
async toMatch(v, state, failMessage, waitTimeMs = 100) {
|
|
1262
1277
|
return await retry(
|
|
@@ -1330,7 +1345,7 @@ but got <${tagName.toUpperCase()}>${failSuffix}`);
|
|
|
1330
1345
|
} else {
|
|
1331
1346
|
attributeValue = e.getAttribute(k);
|
|
1332
1347
|
}
|
|
1333
|
-
if (
|
|
1348
|
+
if (attributeValue === null) {
|
|
1334
1349
|
throw new ExpectationError(that, `expected at
|
|
1335
1350
|
${path.join(" > ")}
|
|
1336
1351
|
|
|
@@ -1799,6 +1814,42 @@ var tests_app_default = {
|
|
|
1799
1814
|
);
|
|
1800
1815
|
const el = container._vode.vode.node;
|
|
1801
1816
|
expect(el.onclick).toBeA("function");
|
|
1817
|
+
},
|
|
1818
|
+
"app(): class as array renders correctly": async () => {
|
|
1819
|
+
const root = document.createElement("div");
|
|
1820
|
+
const container = document.createElement("div");
|
|
1821
|
+
root.appendChild(container);
|
|
1822
|
+
app(
|
|
1823
|
+
container,
|
|
1824
|
+
{},
|
|
1825
|
+
() => [DIV, { class: ["foo", "bar", "baz"] }, "text"]
|
|
1826
|
+
);
|
|
1827
|
+
await expect(container).toMatch([DIV, { class: "foo bar baz" }, "text"]);
|
|
1828
|
+
},
|
|
1829
|
+
"app(): class as number becomes empty string": async () => {
|
|
1830
|
+
const root = document.createElement("div");
|
|
1831
|
+
const container = document.createElement("div");
|
|
1832
|
+
root.appendChild(container);
|
|
1833
|
+
app(
|
|
1834
|
+
container,
|
|
1835
|
+
{},
|
|
1836
|
+
() => [DIV, { class: 123 }, "text"]
|
|
1837
|
+
);
|
|
1838
|
+
await expect(container).toMatch([DIV, { class: "" }, "text"]);
|
|
1839
|
+
},
|
|
1840
|
+
"app(): style object to string transition": async () => {
|
|
1841
|
+
const root = document.createElement("div");
|
|
1842
|
+
const container = document.createElement("div");
|
|
1843
|
+
root.appendChild(container);
|
|
1844
|
+
const state = { useObject: true };
|
|
1845
|
+
app(
|
|
1846
|
+
container,
|
|
1847
|
+
state,
|
|
1848
|
+
(s) => [DIV, { style: s.useObject ? { color: "red" } : "color: blue" }, "text"]
|
|
1849
|
+
);
|
|
1850
|
+
await expect(container).toMatch([DIV, "text"]);
|
|
1851
|
+
state.patch({ useObject: false });
|
|
1852
|
+
await expect(container).toMatch([DIV, "text"]);
|
|
1802
1853
|
}
|
|
1803
1854
|
};
|
|
1804
1855
|
|
|
@@ -2273,11 +2324,13 @@ var tests_mergeClass_default = {
|
|
|
2273
2324
|
"mergeClass(): two objects": async () => {
|
|
2274
2325
|
await expect(mergeClass({ foo: true, bar: true }, { bar: false, baz: true })).toEqual({ foo: true, bar: false, baz: true });
|
|
2275
2326
|
},
|
|
2276
|
-
"mergeClass(): object and array": async () => {
|
|
2277
|
-
await expect(mergeClass({ foo: true }, ["bar", "baz"])).toEqual({ foo: true,
|
|
2327
|
+
"mergeClass(): object and array (array items become class names with true)": async () => {
|
|
2328
|
+
await expect(mergeClass({ foo: true }, ["bar", "baz"])).toEqual({ foo: true, bar: true, baz: true });
|
|
2329
|
+
await expect(mergeClass({ active: true }, ["btn", "primary"])).toEqual({ active: true, btn: true, primary: true });
|
|
2278
2330
|
},
|
|
2279
|
-
"mergeClass(): array and object": async () => {
|
|
2280
|
-
await expect(mergeClass(["foo", "bar"], { baz: true, qux: false })).toEqual({
|
|
2331
|
+
"mergeClass(): array and object (object keys become class names)": async () => {
|
|
2332
|
+
await expect(mergeClass(["foo", "bar"], { baz: true, qux: false })).toEqual({ foo: true, bar: true, baz: true, qux: false });
|
|
2333
|
+
await expect(mergeClass(["a", "b"], { c: true, d: false })).toEqual({ a: true, b: true, c: true, d: false });
|
|
2281
2334
|
},
|
|
2282
2335
|
"mergeClass(): falsy entries are skipped": async () => {
|
|
2283
2336
|
await expect(mergeClass("foo", null, "bar")).toEqual("foo bar");
|
|
@@ -2286,6 +2339,10 @@ var tests_mergeClass_default = {
|
|
|
2286
2339
|
"mergeClass(): multiple args (3+)": async () => {
|
|
2287
2340
|
await expect(mergeClass("a", "b", "c")).toEqual("a b c");
|
|
2288
2341
|
await expect(mergeClass("x", null, ["y", "z"], "w")).toEqual("y z x w");
|
|
2342
|
+
},
|
|
2343
|
+
"mergeClass(): incompatible types throw": async () => {
|
|
2344
|
+
await expect(() => mergeClass(123, "foo")).toFail();
|
|
2345
|
+
await expect(() => mergeClass("foo", 456)).toFail();
|
|
2289
2346
|
}
|
|
2290
2347
|
};
|
|
2291
2348
|
|
|
@@ -2341,6 +2398,10 @@ var tests_mergeProps_default = {
|
|
|
2341
2398
|
const p = { class: "foo" };
|
|
2342
2399
|
await expect(mergeProps(p) === p).toEqual(true);
|
|
2343
2400
|
},
|
|
2401
|
+
"mergeProps(): single falsy arg returns undefined": async () => {
|
|
2402
|
+
await expect(mergeProps(null)).toEqual(void 0);
|
|
2403
|
+
await expect(mergeProps(void 0)).toEqual(void 0);
|
|
2404
|
+
},
|
|
2344
2405
|
"mergeProps(): two plain objects merged": async () => {
|
|
2345
2406
|
await expect(mergeProps({ a: 1 }, { b: 2 })).toEqual({ a: 1, b: 2 });
|
|
2346
2407
|
},
|
|
@@ -2452,6 +2513,12 @@ var tests_state_context_default = {
|
|
|
2452
2513
|
ctx.a.x.z.put("deep");
|
|
2453
2514
|
await expect(state.a.x?.z).toEqual("deep");
|
|
2454
2515
|
await expect(state.a.y).toEqual(1);
|
|
2516
|
+
},
|
|
2517
|
+
"StateContext.put() merges into existing object properties via Object.assign": async () => {
|
|
2518
|
+
const state = createState({ items: { count: 0, name: "test", hidden: false } });
|
|
2519
|
+
const ctx = context(state);
|
|
2520
|
+
ctx.items.put({ count: 5 });
|
|
2521
|
+
await expect(state.items).toEqual({ count: 5, name: "test", hidden: false });
|
|
2455
2522
|
}
|
|
2456
2523
|
};
|
|
2457
2524
|
|
|
@@ -2942,6 +3009,103 @@ var tests_mount_unmount_default = {
|
|
|
2942
3009
|
patch({ show: true });
|
|
2943
3010
|
await expect(mounts).toEqual(["mount span"]);
|
|
2944
3011
|
},
|
|
3012
|
+
"onMount(): with catched component, replacement vode's onMount fires when error occurs": async () => {
|
|
3013
|
+
const container = setup();
|
|
3014
|
+
const mounts = [];
|
|
3015
|
+
const broken = () => {
|
|
3016
|
+
throw new Error("boom");
|
|
3017
|
+
};
|
|
3018
|
+
app(
|
|
3019
|
+
container,
|
|
3020
|
+
{},
|
|
3021
|
+
() => [
|
|
3022
|
+
DIV,
|
|
3023
|
+
{
|
|
3024
|
+
catch: [
|
|
3025
|
+
SECTION,
|
|
3026
|
+
{
|
|
3027
|
+
onMount: (s, ele) => {
|
|
3028
|
+
mounts.push("mount fallback");
|
|
3029
|
+
}
|
|
3030
|
+
},
|
|
3031
|
+
"fallback"
|
|
3032
|
+
]
|
|
3033
|
+
},
|
|
3034
|
+
broken
|
|
3035
|
+
]
|
|
3036
|
+
);
|
|
3037
|
+
await expect(mounts).toEqual(["mount fallback"]);
|
|
3038
|
+
},
|
|
3039
|
+
"onMount(): with catched component, returned vode's onMount fires and receives error": async () => {
|
|
3040
|
+
const container = setup();
|
|
3041
|
+
const mounts = [];
|
|
3042
|
+
const caughtErrors = [];
|
|
3043
|
+
const broken = () => {
|
|
3044
|
+
throw new Error("boom");
|
|
3045
|
+
};
|
|
3046
|
+
app(
|
|
3047
|
+
container,
|
|
3048
|
+
{},
|
|
3049
|
+
() => [
|
|
3050
|
+
DIV,
|
|
3051
|
+
{
|
|
3052
|
+
catch: (s, err) => {
|
|
3053
|
+
caughtErrors.push(err.message);
|
|
3054
|
+
return [
|
|
3055
|
+
SECTION,
|
|
3056
|
+
{
|
|
3057
|
+
onMount: (s2, ele) => {
|
|
3058
|
+
mounts.push("mount fallback");
|
|
3059
|
+
}
|
|
3060
|
+
},
|
|
3061
|
+
"fallback"
|
|
3062
|
+
];
|
|
3063
|
+
}
|
|
3064
|
+
},
|
|
3065
|
+
broken
|
|
3066
|
+
]
|
|
3067
|
+
);
|
|
3068
|
+
await expect(mounts).toEqual(["mount fallback"]);
|
|
3069
|
+
await expect(caughtErrors).toEqual(["boom"]);
|
|
3070
|
+
},
|
|
3071
|
+
"onMount(): with catched component, original element's onMount does NOT fire when error caused replacement": async () => {
|
|
3072
|
+
const container = setup();
|
|
3073
|
+
const logs = [];
|
|
3074
|
+
const broken = () => {
|
|
3075
|
+
throw new Error("boom");
|
|
3076
|
+
};
|
|
3077
|
+
app(
|
|
3078
|
+
container,
|
|
3079
|
+
{},
|
|
3080
|
+
() => [
|
|
3081
|
+
DIV,
|
|
3082
|
+
{
|
|
3083
|
+
catch: [
|
|
3084
|
+
ARTICLE,
|
|
3085
|
+
{
|
|
3086
|
+
onMount: (s, ele) => {
|
|
3087
|
+
logs.push("mount fallback");
|
|
3088
|
+
}
|
|
3089
|
+
},
|
|
3090
|
+
"fallback"
|
|
3091
|
+
]
|
|
3092
|
+
},
|
|
3093
|
+
[
|
|
3094
|
+
SECTION,
|
|
3095
|
+
{
|
|
3096
|
+
onMount: (s, ele) => {
|
|
3097
|
+
logs.push("mount original section");
|
|
3098
|
+
},
|
|
3099
|
+
onUnmount: (s, ele) => {
|
|
3100
|
+
logs.push("unmount original section");
|
|
3101
|
+
}
|
|
3102
|
+
},
|
|
3103
|
+
broken
|
|
3104
|
+
]
|
|
3105
|
+
]
|
|
3106
|
+
);
|
|
3107
|
+
await expect(logs).toEqual(["mount fallback"]);
|
|
3108
|
+
},
|
|
2945
3109
|
"onUnmount(): called when node is removed from the DOM": async () => {
|
|
2946
3110
|
const container = setup();
|
|
2947
3111
|
const unmounts = [];
|
|
@@ -3725,121 +3889,6 @@ var tests_mount_unmount_default = {
|
|
|
3725
3889
|
patch({ expanded: true, showB: false });
|
|
3726
3890
|
await expect(fired).toEqual(["unmount B"]);
|
|
3727
3891
|
},
|
|
3728
|
-
"onMount() + onUnmount: symmetry of calls": async () => {
|
|
3729
|
-
const container = setup();
|
|
3730
|
-
const state = createState({
|
|
3731
|
-
startTime: 0,
|
|
3732
|
-
inputReady: false,
|
|
3733
|
-
showInput: true,
|
|
3734
|
-
showTimer: true
|
|
3735
|
-
});
|
|
3736
|
-
const logs = [];
|
|
3737
|
-
const patch = app(
|
|
3738
|
-
container,
|
|
3739
|
-
state,
|
|
3740
|
-
(s) => {
|
|
3741
|
-
return [
|
|
3742
|
-
DIV,
|
|
3743
|
-
s.showInput && [INPUT, {
|
|
3744
|
-
type: "text",
|
|
3745
|
-
placeholder: "Auto-focused on mount",
|
|
3746
|
-
onMount: (s2, ele) => {
|
|
3747
|
-
logs.push("Input mounted");
|
|
3748
|
-
return { inputReady: true };
|
|
3749
|
-
},
|
|
3750
|
-
onUnmount: (s2, ele) => {
|
|
3751
|
-
logs.push("Input removed");
|
|
3752
|
-
return { inputReady: false };
|
|
3753
|
-
}
|
|
3754
|
-
}],
|
|
3755
|
-
s.showTimer && [P, {
|
|
3756
|
-
onMount: (s2, ele) => {
|
|
3757
|
-
logs.push("Timer started");
|
|
3758
|
-
return { startTime: Date.now() };
|
|
3759
|
-
},
|
|
3760
|
-
onUnmount: (s2, ele) => {
|
|
3761
|
-
logs.push("Timer removed");
|
|
3762
|
-
}
|
|
3763
|
-
}, "Mount/unmount lifecycle demo"]
|
|
3764
|
-
];
|
|
3765
|
-
}
|
|
3766
|
-
);
|
|
3767
|
-
await expect(state.inputReady).toEqual(true);
|
|
3768
|
-
await expect(state.startTime != 0).toEqual(true);
|
|
3769
|
-
patch({ showInput: false });
|
|
3770
|
-
await expect(
|
|
3771
|
-
async () => await expect(state.inputReady).toEqual(false, "expected: inputReady == false")
|
|
3772
|
-
).toSucceedAsync();
|
|
3773
|
-
patch({ showTimer: false });
|
|
3774
|
-
await expect(
|
|
3775
|
-
async () => await expect(container._vode.stats.syncRenderCount >= 4).toEqual(true)
|
|
3776
|
-
).toSucceedAsync();
|
|
3777
|
-
await expect(logs).toEqual([
|
|
3778
|
-
"Input mounted",
|
|
3779
|
-
"Timer started",
|
|
3780
|
-
"Input removed",
|
|
3781
|
-
"Timer removed"
|
|
3782
|
-
]);
|
|
3783
|
-
},
|
|
3784
|
-
"onMount(): with catched component, replacement vode's onMount fires when error occurs": async () => {
|
|
3785
|
-
const container = setup();
|
|
3786
|
-
const mounts = [];
|
|
3787
|
-
const broken = () => {
|
|
3788
|
-
throw new Error("boom");
|
|
3789
|
-
};
|
|
3790
|
-
app(
|
|
3791
|
-
container,
|
|
3792
|
-
{},
|
|
3793
|
-
() => [
|
|
3794
|
-
DIV,
|
|
3795
|
-
{
|
|
3796
|
-
catch: [
|
|
3797
|
-
SECTION,
|
|
3798
|
-
{
|
|
3799
|
-
onMount: (s, ele) => {
|
|
3800
|
-
mounts.push("mount fallback");
|
|
3801
|
-
}
|
|
3802
|
-
},
|
|
3803
|
-
"fallback"
|
|
3804
|
-
]
|
|
3805
|
-
},
|
|
3806
|
-
broken
|
|
3807
|
-
]
|
|
3808
|
-
);
|
|
3809
|
-
await expect(mounts).toEqual(["mount fallback"]);
|
|
3810
|
-
},
|
|
3811
|
-
"onMount(): with catched component, returned vode's onMount fires and receives error": async () => {
|
|
3812
|
-
const container = setup();
|
|
3813
|
-
const mounts = [];
|
|
3814
|
-
const caughtErrors = [];
|
|
3815
|
-
const broken = () => {
|
|
3816
|
-
throw new Error("boom");
|
|
3817
|
-
};
|
|
3818
|
-
app(
|
|
3819
|
-
container,
|
|
3820
|
-
{},
|
|
3821
|
-
() => [
|
|
3822
|
-
DIV,
|
|
3823
|
-
{
|
|
3824
|
-
catch: (s, err) => {
|
|
3825
|
-
caughtErrors.push(err.message);
|
|
3826
|
-
return [
|
|
3827
|
-
SECTION,
|
|
3828
|
-
{
|
|
3829
|
-
onMount: (s2, ele) => {
|
|
3830
|
-
mounts.push("mount fallback");
|
|
3831
|
-
}
|
|
3832
|
-
},
|
|
3833
|
-
"fallback"
|
|
3834
|
-
];
|
|
3835
|
-
}
|
|
3836
|
-
},
|
|
3837
|
-
broken
|
|
3838
|
-
]
|
|
3839
|
-
);
|
|
3840
|
-
await expect(mounts).toEqual(["mount fallback"]);
|
|
3841
|
-
await expect(caughtErrors).toEqual(["boom"]);
|
|
3842
|
-
},
|
|
3843
3892
|
"onUnmount(): with catched component, replacement vode's onUnmount fires when removed": async () => {
|
|
3844
3893
|
const container = setup();
|
|
3845
3894
|
const unmounts = [];
|
|
@@ -3923,7 +3972,7 @@ var tests_mount_unmount_default = {
|
|
|
3923
3972
|
patch({ show: false });
|
|
3924
3973
|
await expect(unmounts).toEqual(["unmount span", "unmount p", "unmount article"]);
|
|
3925
3974
|
},
|
|
3926
|
-
"onMount()
|
|
3975
|
+
"onMount() + onUnmount(): with catched component, full lifecycle symmetry of catch replacement": async () => {
|
|
3927
3976
|
const container = setup();
|
|
3928
3977
|
const logs = [];
|
|
3929
3978
|
const state = createState({ show: true });
|
|
@@ -3959,43 +4008,134 @@ var tests_mount_unmount_default = {
|
|
|
3959
4008
|
patch({ show: false });
|
|
3960
4009
|
await expect(logs).toEqual(["mount article", "unmount article"]);
|
|
3961
4010
|
},
|
|
3962
|
-
"onMount()
|
|
4011
|
+
"onMount() + onUnmount: symmetry of calls": async () => {
|
|
3963
4012
|
const container = setup();
|
|
4013
|
+
const state = createState({
|
|
4014
|
+
startTime: 0,
|
|
4015
|
+
inputReady: false,
|
|
4016
|
+
showInput: true,
|
|
4017
|
+
showTimer: true
|
|
4018
|
+
});
|
|
3964
4019
|
const logs = [];
|
|
3965
|
-
const
|
|
3966
|
-
throw new Error("boom");
|
|
3967
|
-
};
|
|
3968
|
-
app(
|
|
4020
|
+
const patch = app(
|
|
3969
4021
|
container,
|
|
3970
|
-
|
|
3971
|
-
() =>
|
|
4022
|
+
state,
|
|
4023
|
+
(s) => {
|
|
4024
|
+
return [
|
|
4025
|
+
DIV,
|
|
4026
|
+
s.showInput && [INPUT, {
|
|
4027
|
+
type: "text",
|
|
4028
|
+
placeholder: "Auto-focused on mount",
|
|
4029
|
+
onMount: (s2, ele) => {
|
|
4030
|
+
logs.push("Input mounted");
|
|
4031
|
+
return { inputReady: true };
|
|
4032
|
+
},
|
|
4033
|
+
onUnmount: (s2, ele) => {
|
|
4034
|
+
logs.push("Input removed");
|
|
4035
|
+
return { inputReady: false };
|
|
4036
|
+
}
|
|
4037
|
+
}],
|
|
4038
|
+
s.showTimer && [P, {
|
|
4039
|
+
onMount: (s2, ele) => {
|
|
4040
|
+
logs.push("Timer started");
|
|
4041
|
+
return { startTime: Date.now() };
|
|
4042
|
+
},
|
|
4043
|
+
onUnmount: (s2, ele) => {
|
|
4044
|
+
logs.push("Timer removed");
|
|
4045
|
+
}
|
|
4046
|
+
}, "Mount/unmount lifecycle demo"]
|
|
4047
|
+
];
|
|
4048
|
+
}
|
|
4049
|
+
);
|
|
4050
|
+
await expect(state.inputReady).toEqual(true);
|
|
4051
|
+
await expect(state.startTime != 0).toEqual(true);
|
|
4052
|
+
patch({ showInput: false });
|
|
4053
|
+
await expect(
|
|
4054
|
+
async () => await expect(state.inputReady).toEqual(false, "expected: inputReady == false")
|
|
4055
|
+
).toSucceedAsync();
|
|
4056
|
+
patch({ showTimer: false });
|
|
4057
|
+
await expect(
|
|
4058
|
+
async () => await expect(container._vode.stats.syncRenderCount >= 4).toEqual(true)
|
|
4059
|
+
).toSucceedAsync();
|
|
4060
|
+
await expect(logs).toEqual([
|
|
4061
|
+
"Input mounted",
|
|
4062
|
+
"Timer started",
|
|
4063
|
+
"Input removed",
|
|
4064
|
+
"Timer removed"
|
|
4065
|
+
]);
|
|
4066
|
+
},
|
|
4067
|
+
"onMount() + onUnmount(): Not called when DOM does not require element creation or removal (same TAGs)": async () => {
|
|
4068
|
+
const container = setup();
|
|
4069
|
+
const logs = [];
|
|
4070
|
+
const Comp = (name) => () => [
|
|
4071
|
+
ARTICLE,
|
|
4072
|
+
[
|
|
3972
4073
|
DIV,
|
|
3973
4074
|
{
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
{
|
|
3977
|
-
onMount: (s, ele) => {
|
|
3978
|
-
logs.push("mount fallback");
|
|
3979
|
-
}
|
|
3980
|
-
},
|
|
3981
|
-
"fallback"
|
|
3982
|
-
]
|
|
4075
|
+
onMount: () => logs.push("mount " + name),
|
|
4076
|
+
onUnmount: () => logs.push("unmount " + name)
|
|
3983
4077
|
},
|
|
4078
|
+
"Component " + name
|
|
4079
|
+
]
|
|
4080
|
+
];
|
|
4081
|
+
const state = createState({ showB: false, showD: false });
|
|
4082
|
+
app(container, state, (s) => [
|
|
4083
|
+
DIV,
|
|
4084
|
+
// this way they both "share a slot"
|
|
4085
|
+
s.showB ? Comp("B") : Comp("A"),
|
|
4086
|
+
// this way each component occupies its own "slot"
|
|
4087
|
+
!s.showD && Comp("C"),
|
|
4088
|
+
s.showD && Comp("D")
|
|
4089
|
+
]);
|
|
4090
|
+
await expect(container).toMatch(
|
|
4091
|
+
[
|
|
4092
|
+
DIV,
|
|
3984
4093
|
[
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
logs.push("unmount original section");
|
|
3992
|
-
}
|
|
3993
|
-
},
|
|
3994
|
-
broken
|
|
4094
|
+
ARTICLE,
|
|
4095
|
+
[DIV, "Component A"]
|
|
4096
|
+
],
|
|
4097
|
+
[
|
|
4098
|
+
ARTICLE,
|
|
4099
|
+
[DIV, "Component C"]
|
|
3995
4100
|
]
|
|
3996
4101
|
]
|
|
3997
4102
|
);
|
|
3998
|
-
await expect(logs).toEqual(["mount
|
|
4103
|
+
await expect(logs).toEqual(["mount A", "mount C"]);
|
|
4104
|
+
state.patch({ showB: true });
|
|
4105
|
+
await expect(container).toMatch(
|
|
4106
|
+
[
|
|
4107
|
+
DIV,
|
|
4108
|
+
[
|
|
4109
|
+
ARTICLE,
|
|
4110
|
+
[DIV, "Component B"]
|
|
4111
|
+
],
|
|
4112
|
+
[
|
|
4113
|
+
ARTICLE,
|
|
4114
|
+
[DIV, "Component C"]
|
|
4115
|
+
]
|
|
4116
|
+
]
|
|
4117
|
+
);
|
|
4118
|
+
await expect(logs).toEqual(["mount A", "mount C"]);
|
|
4119
|
+
state.patch({ showD: true });
|
|
4120
|
+
await expect(container).toMatch(
|
|
4121
|
+
[
|
|
4122
|
+
DIV,
|
|
4123
|
+
[
|
|
4124
|
+
ARTICLE,
|
|
4125
|
+
[DIV, "Component B"]
|
|
4126
|
+
],
|
|
4127
|
+
[
|
|
4128
|
+
ARTICLE,
|
|
4129
|
+
[DIV, "Component D"]
|
|
4130
|
+
]
|
|
4131
|
+
]
|
|
4132
|
+
);
|
|
4133
|
+
await expect(logs).toEqual([
|
|
4134
|
+
"mount A",
|
|
4135
|
+
"mount C",
|
|
4136
|
+
"unmount C",
|
|
4137
|
+
"mount D"
|
|
4138
|
+
]);
|
|
3999
4139
|
}
|
|
4000
4140
|
};
|
|
4001
4141
|
|
|
@@ -4088,7 +4228,7 @@ var tests_examples_default = {
|
|
|
4088
4228
|
[BUTTON, "Add"],
|
|
4089
4229
|
[
|
|
4090
4230
|
NAV,
|
|
4091
|
-
[BUTTON, "All"],
|
|
4231
|
+
[BUTTON, { class: "active" }, "All"],
|
|
4092
4232
|
[BUTTON, "Active"],
|
|
4093
4233
|
[BUTTON, "Done"]
|
|
4094
4234
|
],
|
|
@@ -4112,7 +4252,7 @@ var tests_examples_default = {
|
|
|
4112
4252
|
[
|
|
4113
4253
|
NAV,
|
|
4114
4254
|
[BUTTON, "All"],
|
|
4115
|
-
[BUTTON, "Active"],
|
|
4255
|
+
[BUTTON, { class: "active" }, "Active"],
|
|
4116
4256
|
[BUTTON, "Done"]
|
|
4117
4257
|
],
|
|
4118
4258
|
[
|
|
@@ -4133,7 +4273,7 @@ var tests_examples_default = {
|
|
|
4133
4273
|
NAV,
|
|
4134
4274
|
[BUTTON, "All"],
|
|
4135
4275
|
[BUTTON, "Active"],
|
|
4136
|
-
[BUTTON, "Done"]
|
|
4276
|
+
[BUTTON, { class: "active" }, "Done"]
|
|
4137
4277
|
],
|
|
4138
4278
|
[
|
|
4139
4279
|
UL,
|
|
@@ -5189,8 +5329,11 @@ var tests_patch_advanced_default = {
|
|
|
5189
5329
|
app(container, state, (s) => [DIV, s.phase, String(s.value)]);
|
|
5190
5330
|
await expect(state.phase).toEqual("start");
|
|
5191
5331
|
state.patch((async function* () {
|
|
5332
|
+
await expect(container._vode.stats.syncRenderPatchCount).toEqual(0);
|
|
5192
5333
|
yield { phase: "working", value: 10 };
|
|
5334
|
+
await expect(container._vode.stats.syncRenderPatchCount).toEqual(1);
|
|
5193
5335
|
yield { phase: "almost", value: 20 };
|
|
5336
|
+
await expect(container._vode.stats.syncRenderPatchCount).toEqual(2);
|
|
5194
5337
|
return { phase: "done", value: 30 };
|
|
5195
5338
|
})());
|
|
5196
5339
|
await new Promise((r) => setTimeout(r, 0));
|
|
@@ -5223,6 +5366,87 @@ var tests_patch_advanced_default = {
|
|
|
5223
5366
|
await delay(10);
|
|
5224
5367
|
await expect(state.x).toEqual(10);
|
|
5225
5368
|
await expect(state.y).toEqual(20);
|
|
5369
|
+
},
|
|
5370
|
+
"patch(): returns Promise for generator functions, can be awaited": async () => {
|
|
5371
|
+
const container = setup4();
|
|
5372
|
+
const state = createState({ count: 0 });
|
|
5373
|
+
app(container, state, (s) => [DIV, String(s.count)]);
|
|
5374
|
+
await expect(container._vode.stats.patchCount).toEqual(0);
|
|
5375
|
+
const result = state.patch(function* () {
|
|
5376
|
+
yield { count: 1 };
|
|
5377
|
+
return { count: 2 };
|
|
5378
|
+
});
|
|
5379
|
+
await expect(container._vode.stats.patchCount).toEqual(1);
|
|
5380
|
+
expect(result).toBeA("object");
|
|
5381
|
+
await expect(result instanceof Promise).toEqual(true);
|
|
5382
|
+
await result;
|
|
5383
|
+
await expect(container._vode.stats.patchCount).toEqual(3);
|
|
5384
|
+
await expect(state.count).toEqual(2);
|
|
5385
|
+
await expect(container).toMatch([DIV, "2"]);
|
|
5386
|
+
},
|
|
5387
|
+
"patch(): returns Promise for Promise patches, can be awaited": async () => {
|
|
5388
|
+
const container = setup4();
|
|
5389
|
+
const state = createState({ msg: "before" });
|
|
5390
|
+
app(container, state, (s) => [DIV, s.msg]);
|
|
5391
|
+
const result = state.patch(Promise.resolve({ msg: "after" }));
|
|
5392
|
+
expect(result).toBeA("object");
|
|
5393
|
+
await expect(result instanceof Promise).toEqual(true);
|
|
5394
|
+
await result;
|
|
5395
|
+
await expect(state.msg).toEqual("after");
|
|
5396
|
+
await expect(container).toMatch([DIV, "after"]);
|
|
5397
|
+
},
|
|
5398
|
+
"patch(): returns void for object patches": async () => {
|
|
5399
|
+
const container = setup4();
|
|
5400
|
+
const state = createState({ x: 1 });
|
|
5401
|
+
app(container, state, (s) => [DIV, String(s.x)]);
|
|
5402
|
+
const result = state.patch({ x: 2 });
|
|
5403
|
+
expect(result).toBeA("undefined");
|
|
5404
|
+
await expect(state.x).toEqual(2);
|
|
5405
|
+
await expect(container).toMatch([DIV, "2"]);
|
|
5406
|
+
},
|
|
5407
|
+
"patch(): forward promise error when one happens during patch": async () => {
|
|
5408
|
+
const container = setup4();
|
|
5409
|
+
const state = createState({ msg: "before" });
|
|
5410
|
+
app(container, state, (s) => [DIV, s.msg]);
|
|
5411
|
+
const mockPromise = Promise.withResolvers();
|
|
5412
|
+
const promisePatchResult = state.patch(mockPromise.promise);
|
|
5413
|
+
mockPromise.reject(new Error("promise error"));
|
|
5414
|
+
let err = await expect(() => promisePatchResult).toFailAsync("promise (1) error expected");
|
|
5415
|
+
expect(err.message).toEqual("promise error");
|
|
5416
|
+
err = await expect(() => state.patch(async () => {
|
|
5417
|
+
await delay(1);
|
|
5418
|
+
throw new Error("promise error");
|
|
5419
|
+
})).toFailAsync("promise (2) error expected");
|
|
5420
|
+
expect(err.message).toEqual("promise error");
|
|
5421
|
+
},
|
|
5422
|
+
"patch(): forward generator error when one happens during patch": async () => {
|
|
5423
|
+
const container = setup4();
|
|
5424
|
+
const state = createState({ msg: "before" });
|
|
5425
|
+
app(container, state, (s) => [DIV, s.msg]);
|
|
5426
|
+
const err = await expect(
|
|
5427
|
+
() => state.patch(
|
|
5428
|
+
async function* () {
|
|
5429
|
+
yield {};
|
|
5430
|
+
await delay(1);
|
|
5431
|
+
yield {};
|
|
5432
|
+
throw new Error("generator error");
|
|
5433
|
+
}
|
|
5434
|
+
)
|
|
5435
|
+
).toFailAsync("generator error expected");
|
|
5436
|
+
expect(err.message).toEqual("generator error");
|
|
5437
|
+
},
|
|
5438
|
+
"patch(): forward error when one happens during patch": async () => {
|
|
5439
|
+
const container = setup4();
|
|
5440
|
+
const state = createState({ msg: "before" });
|
|
5441
|
+
app(container, state, (s) => [DIV, s.msg]);
|
|
5442
|
+
const err = await expect(
|
|
5443
|
+
() => state.patch(
|
|
5444
|
+
() => {
|
|
5445
|
+
throw new Error("void error");
|
|
5446
|
+
}
|
|
5447
|
+
)
|
|
5448
|
+
).toFailAsync("void error expected");
|
|
5449
|
+
expect(err.message).toEqual("void error");
|
|
5226
5450
|
}
|
|
5227
5451
|
};
|
|
5228
5452
|
|