@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.
@@ -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
- r = await this.what();
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 (!attributeValue) {
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, 0: "bar", 1: "baz" });
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({ 0: "foo", 1: "bar", baz: true, qux: false });
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()/onUnmount(): with catched component, full lifecycle symmetry of catch replacement": async () => {
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(): with catched component, original element's onMount does NOT fire when error caused replacement": async () => {
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 broken = () => {
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
- catch: [
3975
- ARTICLE,
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
- SECTION,
3986
- {
3987
- onMount: (s, ele) => {
3988
- logs.push("mount original section");
3989
- },
3990
- onUnmount: (s, ele) => {
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 fallback"]);
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