@ryupold/vode 1.8.7 → 1.8.8

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.
@@ -41,13 +41,13 @@ export interface SubContext<SubState> {
41
41
  }
42
42
 
43
43
  export type ProxyStateContext<S extends PatchableState, SubState> = StateContext<S, SubState> & {
44
- [K in keyof SubState]-?: SubState[K] extends object
44
+ [K in keyof SubState]-?: SubState[K] extends object | null
45
45
  ? ProxyStateContext<S, SubState[K]>
46
46
  : StateContext<S, SubState[K]>
47
47
  };
48
48
 
49
49
  export type ProxySubContext<SubState> = SubContext<SubState> & {
50
- [K in keyof SubState]-?: SubState[K] extends object
50
+ [K in keyof SubState]-?: SubState[K] extends object | null
51
51
  ? ProxySubContext<SubState[K]>
52
52
  : SubContext<SubState[K]>
53
53
  };
@@ -107,10 +107,12 @@ class ProxyStateContextImpl<S extends PatchableState, SubState>
107
107
  }
108
108
  raw[keys[i]] = value;
109
109
  } else if (keys.length === 1) {
110
- if (typeof (<any>target)[keys[0]] === "object" && typeof value === "object")
110
+ if (typeof (<any>target)[keys[0]] === "object" && typeof value === "object" && value !== null) {
111
111
  Object.assign((<any>target)[keys[0]], value);
112
- else
112
+ }
113
+ else {
113
114
  (<any>target)[keys[0]] = value;
115
+ }
114
116
  } else {
115
117
  Object.assign(target, value as DeepPartial<S>);
116
118
  }
package/src/vode.ts CHANGED
@@ -7,7 +7,7 @@ export type TextVode = string & {};
7
7
  export type NoVode = undefined | null | number | boolean | bigint | void;
8
8
  export type AttachedVode<S> = Vode<S> & { node: ChildNode } | Text & { node?: never };
9
9
  export type Tag = keyof (HTMLElementTagNameMap & SVGElementTagNameMap & MathMLElementTagNameMap) | (string & {});
10
- export type Component<S> = (s: S) => ChildVode<S>;
10
+ export type Component<S = PatchableState> = (s: S) => ChildVode<S>;
11
11
 
12
12
  export type Patch<S> =
13
13
  | IgnoredPatch // ignored
@@ -217,7 +217,7 @@ export function app<S extends PatchableState = PatchableState>(
217
217
  });
218
218
 
219
219
  function renderDom(isAsync: boolean) {
220
- const sw = Date.now();
220
+ const sw = performance.now();
221
221
  const vom = dom(_vode.state);
222
222
  _vode.vode = render<S>(_vode.state, container.parentElement as Element, 0, 0, _vode.vode, vom)!;
223
223
 
@@ -227,7 +227,7 @@ export function app<S extends PatchableState = PatchableState>(
227
227
  }
228
228
 
229
229
  if (!isAsync) {
230
- _vode.stats.lastSyncRenderTime = Date.now() - sw;
230
+ _vode.stats.lastSyncRenderTime = performance.now() - sw;
231
231
  _vode.stats.syncRenderCount++;
232
232
  _vode.isRendering = false;
233
233
  if (_vode.qSync) _vode.renderSync();
@@ -258,7 +258,7 @@ export function app<S extends PatchableState = PatchableState>(
258
258
  if (_vode.isAnimating || !_vode.qAsync || document.hidden) return;
259
259
 
260
260
  _vode.isAnimating = true;
261
- const sw = Date.now();
261
+ const sw = performance.now();
262
262
  try {
263
263
  _vode.state = mergeState(_vode.state, _vode.qAsync, true);
264
264
  _vode.qAsync = null;
@@ -267,7 +267,7 @@ export function app<S extends PatchableState = PatchableState>(
267
267
 
268
268
  await globals.currentViewTransition?.updateCallbackDone;
269
269
  } finally {
270
- _vode.stats.lastAsyncRenderTime = Date.now() - sw;
270
+ _vode.stats.lastAsyncRenderTime = performance.now() - sw;
271
271
  _vode.stats.asyncRenderCount++;
272
272
  _vode.isAnimating = false;
273
273
  }
@@ -384,13 +384,20 @@ export function hydrate<S = PatchableState>(element: Element | Text, prepareForR
384
384
  }
385
385
 
386
386
  /** memoizes the resulting component or props by comparing element by element (===) with the
387
- * `compare` of the previous render. otherwise skips the render step (not calling `componentOrProps`)*/
388
- export function memo<S = PatchableState>(compare: any[], componentOrProps: Component<S> | ((s: S) => Props<S>)): typeof componentOrProps extends ((s: S) => Props<S>) ? ((s: S) => Props<S>) : Component<S> {
387
+ * `compare` of the previous render. otherwise skips the render step (not calling `componentOrProps`)
388
+ */
389
+ export function memo<S = PatchableState>(compare: any[], component: Component<S>): Component<S> {
389
390
  if (!compare || !Array.isArray(compare)) throw new Error("first argument to memo() must be an array of values to compare");
390
- if (typeof componentOrProps !== "function") throw new Error("second argument to memo() must be a function that returns a vode or props object");
391
+ if (typeof component !== "function") throw new Error("second argument to memo() must be a function that returns a child vode");
392
+
393
+ if ((<any>component).__memo) { // wrap to prevent double memoization
394
+ const comp = component;
395
+ component = (s: S) => comp(s);
396
+ }
391
397
 
392
- (<any>componentOrProps).__memo = compare;
393
- return componentOrProps as typeof componentOrProps extends ((s: S) => Props<S>) ? ((s: S) => Props<S>) : Component<S>;
398
+ (<any>component).__memo = compare;
399
+
400
+ return component;
394
401
  }
395
402
 
396
403
  /**
@@ -559,7 +566,7 @@ function render<S extends PatchableState>(state: S, parent: Element, childIndex:
559
566
  for (let i = indexInParent; i < parent.childNodes.length; i++) {
560
567
  const nextSibling = parent.childNodes[i];
561
568
  if (nextSibling) {
562
- nextSibling.before(text, nextSibling);
569
+ nextSibling.before(text);
563
570
  inserted = true;
564
571
  break;
565
572
  }
@@ -605,7 +612,7 @@ function render<S extends PatchableState>(state: S, parent: Element, childIndex:
605
612
  for (let i = indexInParent; i < parent.childNodes.length; i++) {
606
613
  const nextSibling = parent.childNodes[i];
607
614
  if (nextSibling) {
608
- nextSibling.before(newNode, nextSibling);
615
+ nextSibling.before(newNode);
609
616
  inserted = true;
610
617
  break;
611
618
  }
@@ -615,12 +622,12 @@ function render<S extends PatchableState>(state: S, parent: Element, childIndex:
615
622
  }
616
623
  }
617
624
 
618
- const newKids = children(newVode);
619
- if (newKids) {
625
+ const newStart = childrenStart(newVode);
626
+ if (newStart > 0) {
620
627
  const childOffset = !!properties ? 2 : 1;
621
628
  let indexP = 0;
622
- for (let i = 0; i < newKids.length; i++) {
623
- const child = newKids[i];
629
+ for (let i = 0; i < (<Vode<S>>newVode).length - newStart; i++) {
630
+ const child = (<Vode<S>>newVode)[i + newStart] as ChildVode<S>;
624
631
  // render child in xml mode to prevent using the dom properties
625
632
  const attached = render(state, newNode as Element, i, indexP, undefined, child, xmlns ?? null);
626
633
  (<Vode<S>>newVode!)[i + childOffset] = <Vode<S>>attached;
@@ -639,49 +646,37 @@ function render<S extends PatchableState>(state: S, parent: Element, childIndex:
639
646
  if (!oldIsText && isNode && (<Vode<S>>oldVode)[0] === (<Vode<S>>newVode)[0]) {
640
647
  (<AttachedVode<S>>newVode).node = oldNode;
641
648
 
642
- const newvode = <Vode<S>>newVode;
643
- const oldvode = <Vode<S>>oldVode;
644
-
645
649
  const properties = props(newVode);
646
650
  const oldProps = props(oldVode);
647
651
 
648
- if (properties?.xmlns !== undefined) xmlns = properties.xmlns;
652
+ if (properties?.xmlns !== undefined)
653
+ xmlns = properties.xmlns;
649
654
 
650
- if ((<any>newvode[1])?.__memo) {
651
- const prev = newvode[1] as any;
652
- newvode[1] = remember(state, newvode[1], oldvode[1]) as Vode<S>;
653
- if (prev !== newvode[1]) {
654
- patchProperties(state, oldNode!, oldProps, properties, xmlns);
655
- }
656
- }
657
- else {
658
- patchProperties(state, oldNode!, oldProps, properties, xmlns);
659
- }
655
+ patchProperties(state, oldNode!, oldProps, properties, xmlns);
660
656
 
661
657
  if (!!properties?.catch && oldProps?.catch !== properties.catch) {
662
658
  (<any>newVode).node['catch'] = null;
663
659
  (<any>newVode).node.removeAttribute('catch');
664
660
  }
665
661
 
666
- const newKids = children(newVode);
667
- const oldKids = children(oldVode) as AttachedVode<S>[];
668
- if (newKids) {
669
- const childOffset = !!properties ? 2 : 1;
662
+ const newStart = childrenStart(newVode);
663
+ const oldStart = childrenStart(oldVode);
664
+ if (newStart > 0) {
670
665
  let indexP = 0;
671
- for (let i = 0; i < newKids.length; i++) {
672
- const child = newKids[i];
673
- const oldChild = oldKids && oldKids[i];
666
+ for (let i = 0; i < (<Vode<S>>newVode).length - newStart; i++) {
667
+ const child = (<Vode<S>>newVode)[i + newStart] as ChildVode<S>;
668
+ const oldChild = oldStart > 0 ? (<Vode<S>>oldVode)[i + oldStart] as AttachedVode<S> : undefined;
674
669
 
675
670
  const attached = render(state, oldNode as Element, i, indexP, oldChild, child, xmlns);
676
- (<Vode<S>>newVode)[i + childOffset] = <Vode<S>>attached;
671
+ (<any>newVode)[i + newStart] = attached;
677
672
  if (attached) indexP++;
678
673
  }
679
674
  }
680
675
 
681
- if (oldKids) {
682
- const newKidsCount = newKids ? newKids.length : 0;
683
- for (let i = oldKids.length - 1; i >= newKidsCount; i--) {
684
- render(state, oldNode as Element, i, i, oldKids[i], undefined, xmlns);
676
+ if (oldStart > 0) {
677
+ const newKidsCount = newStart > 0 ? (<Vode<S>>newVode).length - newStart : 0;
678
+ for (let i = (<Vode<S>>oldVode).length - 1 - oldStart; i >= newKidsCount; i--) {
679
+ render(state, oldNode as Element, i, i, (<any>oldVode)[i + oldStart], undefined, xmlns);
685
680
  }
686
681
  }
687
682
 
@@ -745,14 +740,19 @@ function isTextVode<S>(x: ChildVode<S>): x is TextVode {
745
740
  }
746
741
 
747
742
  function remember<S>(state: S, present: any, past: any): ChildVode<S> | AttachedVode<S> {
743
+ while (typeof present === "function" && !present.__memo) {
744
+ present = present(state);
745
+ }
746
+
748
747
  if (typeof present !== "function")
749
748
  return present;
750
749
 
751
- const presentMemo = present?.__memo;
752
- const pastMemo = past?.__memo;
753
750
 
754
- if (Array.isArray(presentMemo)
755
- && Array.isArray(pastMemo)
751
+ const presentMemo: unknown[] = present?.__memo;
752
+ const pastMemo: unknown[] = past?.__memo;
753
+
754
+ if (
755
+ Array.isArray(presentMemo) && Array.isArray(pastMemo)
756
756
  && presentMemo.length === pastMemo.length
757
757
  ) {
758
758
  let same = true;
@@ -765,40 +765,17 @@ function remember<S>(state: S, present: any, past: any): ChildVode<S> | Attached
765
765
  if (same) return past;
766
766
  }
767
767
 
768
- const result = present(state);
769
-
770
- if (typeof result === "function" && result?.__memo) {
771
- const resultMemo = result.__memo;
772
- if (Array.isArray(resultMemo) && Array.isArray(pastMemo) && resultMemo.length === pastMemo.length) {
773
- let same = true;
774
- for (let i = 0; i < resultMemo.length; i++) {
775
- if (resultMemo[i] !== pastMemo[i]) {
776
- same = false;
777
- break;
778
- }
779
- }
780
- if (same) return past;
781
- }
782
- const innerRender = result(state);
783
- if (typeof innerRender === "object") {
784
- innerRender.__memo = resultMemo;
785
- }
786
- return innerRender;
768
+ // memos are not equal so we unwrap the present
769
+ while (typeof present === "function") {
770
+ present = present(state);
787
771
  }
788
772
 
789
- const newRender = typeof result === "function" ? unwrap(result, state) : result;
790
- if (typeof newRender === "object") {
791
- (<any>newRender).__memo = result?.__memo || present?.__memo;
773
+ // attach memo to the unwrapped present for future comparisons
774
+ if (typeof present === "object") {
775
+ (<any>present).__memo = presentMemo;
792
776
  }
793
- return newRender;
794
- }
795
777
 
796
- function unwrap<S>(c: Component<S> | ChildVode<S>, s: S): ChildVode<S> {
797
- if (typeof c === "function") {
798
- return unwrap(c(s), s);
799
- } else {
800
- return c;
801
- }
778
+ return present;
802
779
  }
803
780
 
804
781
  function patchProperties<S extends PatchableState>(
package/test/helper.ts CHANGED
@@ -1,16 +1,18 @@
1
- import { children, ChildVode, PatchableState, tag, Vode } from "../src/vode";
2
- import { MockElement, MockText } from "./mocks";
1
+ import { children, ChildVode, PatchableState, props, tag, Vode } from "../src/vode";
2
+ import { FakeElement, FakeTextNode } from "./mocks";
3
3
 
4
4
  export class Expectation {
5
5
  constructor(public readonly what: any) { }
6
6
 
7
- toBeA(type: "undefined" | "object" | "function" | "bigint" | "boolean" | "number" | "string" | "symbol") {
7
+ toBeA(type: "undefined" | "object" | "function" | "bigint" | "boolean" | "number" | "string" | "symbol", failMessage?: string) {
8
8
  if (typeof this.what !== type) {
9
- throw new ExpectationError(this, `expected \n\ntypeof ${this.what}\n\nto be \n\n${type}`);
9
+ throw new ExpectationError(this, `expected \n\ntypeof ${this.what}\n\nto be \n\n${type}${failMessage ? `\n\n${failMessage}` : ""}`);
10
10
  }
11
11
  }
12
12
 
13
- toEqual(other: any) {
13
+ toEqual(other: any, failMessage?: string) {
14
+ const failSuffix = failMessage ? `\n\n${failMessage}` : "";
15
+
14
16
  function deepCompare(a: any, b: any, path: string[]): string[] | null {
15
17
  if (typeof a !== typeof b) {
16
18
  if (path.length === 0) path.push(``);
@@ -46,12 +48,12 @@ export class Expectation {
46
48
  if (typeof this.what === "object" && typeof other === "object" && this.what !== null && other !== null) {
47
49
  const unequal = deepCompare(this.what, other, []);
48
50
  if (unequal) {
49
- throw new ExpectationError(this, `expected \n\n${JSON.stringify(this.what, null, 2)}\n\n to equal \n\n${JSON.stringify(other, null, 2)}\n\nThey differ in: ${unequal.join(".")}`);
51
+ throw new ExpectationError(this, `expected \n\n${JSON.stringify(this.what, null, 2)}\n\n to equal \n\n${JSON.stringify(other, null, 2)}\n\nThey differ in: ${unequal.join(".")}${failSuffix}`);
50
52
  }
51
53
  }
52
54
  else {
53
55
  if (this.what !== other) {
54
- throw new ExpectationError(this, `expected (${typeof this.what})\n\n${this.what}\n\nto equal (${typeof other})\n\n${other}`);
56
+ throw new ExpectationError(this, `expected (${typeof this.what})\n\n${this.what}\n\nto equal (${typeof other})\n\n${other}${failSuffix}`);
55
57
  }
56
58
  }
57
59
  }
@@ -77,82 +79,126 @@ export class Expectation {
77
79
  throw new ExpectationError(this, `expected function to fail\n\nbut it succeeded with a result of type ${typeof r}\n\n${r}`);
78
80
  }
79
81
 
80
- toMatch(v: ChildVode, state?: PatchableState) {
81
- if (this.what instanceof MockElement || this.what instanceof MockText || typeof this.what === "string" || Array.isArray(this.what) || typeof this.what === "function") {
82
+ toMatch(v: ChildVode, state?: PatchableState | null, failMessage?: string) {
83
+ const failSuffix = failMessage ? `\n\n${failMessage}` : "";
84
+
85
+ if (this.what instanceof FakeElement || this.what instanceof FakeTextNode || typeof this.what === "string" || Array.isArray(this.what) || typeof this.what === "function") {
82
86
  const that = this;
83
- function deepCompare(e: MockElement | MockText | ChildVode, cv: ChildVode, path: string[]): string[] | null {
87
+
88
+ function deepCompare(e: FakeElement | FakeTextNode | ChildVode, cv: ChildVode, path: string[]): string[] | null {
84
89
 
85
90
  // unwrap component
86
91
  while (typeof cv === "function") {
87
92
  if (!state) {
88
- throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na Component\n\nbut got no state passed in [toMatch]`);
93
+ throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na Component\n\nbut got no state passed in [toMatch]${failSuffix}`);
89
94
  }
90
95
  cv = cv(state);
91
96
  }
92
97
  while (typeof e === "function") {
93
98
  if (!state) {
94
- throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na Component\n\nbut got no state passed in [toMatch]`);
99
+ throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na Component\n\nbut got no state passed in [toMatch]${failSuffix}`);
95
100
  }
96
101
  e = e(state);
97
102
  }
98
103
 
99
- if (typeof cv === "string" && e instanceof MockText) {
104
+ // string matches TextNode
105
+ if (typeof cv === "string" && e instanceof FakeTextNode) {
100
106
  if (cv !== e.wholeText) {
101
- throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na text node with\n${cv}\n\nbut text was\n${e.wholeText}`);
107
+ throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na text node with\n${cv}\n\nbut text was\n${e.wholeText}${failSuffix}`);
102
108
  }
103
109
  }
110
+ // string matches string
104
111
  else if (typeof cv === "string" && typeof e === "string") {
105
112
  if (cv !== e) {
106
- throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na text node with\n${cv}\n\nbut text was\n${e}`);
113
+ throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na text node with\n${cv}\n\nbut text was\n${e}${failSuffix}`);
107
114
  }
108
115
  }
109
116
 
110
- else if (Array.isArray(cv) && e instanceof MockElement) {
111
- if (tag(cv)?.toLocaleUpperCase() !== e.tagName.toUpperCase()) {
112
- throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\nan element <${tag(cv)}>\n\nbut got <${e.tagName}>`);
117
+ // vode matches element
118
+ else if (Array.isArray(cv) && e instanceof FakeElement) {
119
+ if (tag(cv)?.toUpperCase() !== e.tagName.toUpperCase()) {
120
+ throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\nan element <${tag(cv)?.toUpperCase()}>\n\nbut got <${e.tagName.toUpperCase()}>${failSuffix}`);
121
+ }
122
+
123
+ // compare attributes/props
124
+ const properties = props(cv);
125
+ if (properties) {
126
+ for (const [k, v] of Object.entries(properties)) {
127
+ const attributeValue = e.fakeAttributes[k];
128
+ if (!attributeValue) {
129
+ throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\nan element <${tag(cv)?.toUpperCase()}>\n\nwith attribute [${k}="${v}"]\n\nbut it was not found${failSuffix}`);
130
+ }
131
+ if (attributeValue !== v) {
132
+ throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\nan element <${tag(cv)?.toUpperCase()}>\n\nwith attribute [${k}="${v}"]\n\nbut it was [${k}="${attributeValue}"]${failSuffix}`);
133
+ }
134
+ }
113
135
  }
136
+
137
+ // compare children
114
138
  const kids = children(cv) || [];
115
139
  for (let i = 0; i < kids.length; i++) {
116
- deepCompare(e.children[i], kids[i], [...path, `${tag(kids[i] as Vode) || "#text"}`]);
140
+ deepCompare(e.children.item(i) as any, kids[i], [...path, `[${i}]${tag(kids[i] as Vode)?.toUpperCase() || "#text"}`]);
117
141
  }
118
142
  if (kids.length !== e.children.length) {
119
- throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\n${kids.length} children\n\nbut <${e.tagName}> has ${e.children.length} children`);
143
+ throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\n${kids.length} children\n\nbut <${e.tagName.toUpperCase()}> has ${e.children.length} children${failSuffix}`);
120
144
  }
121
145
  }
146
+ // vode matches vode
122
147
  else if (Array.isArray(cv) && Array.isArray(e)) {
123
- if (tag(cv)?.toLocaleUpperCase() !== tag(e)?.toUpperCase()) {
124
- throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\nan element [${tag(cv)}]\n\nbut got [${tag(e)}]`);
148
+ if (tag(cv)?.toUpperCase() !== tag(e)?.toUpperCase()) {
149
+ throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na vode [${tag(cv)?.toUpperCase()}]\n\nbut got [${tag(e)?.toUpperCase()}]${failSuffix}`);
150
+ }
151
+
152
+ // compare attributes/props
153
+ const properties = props(cv);
154
+ const otherProperties = props(e) || {};
155
+ if (properties) {
156
+ for (const [k, v] of Object.entries(properties)) {
157
+ const attributeValue = otherProperties[k];
158
+ if (!attributeValue) {
159
+ throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na vode [${tag(cv)?.toUpperCase()}]\n\nwith attribute [${k}="${v}"]\n\nbut it was not found${failSuffix}`);
160
+ }
161
+ if (attributeValue !== v) {
162
+ throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na vode [${tag(cv)?.toUpperCase()}]\n\nwith attribute [${k}="${v}"]\n\nbut its value was [${k}="${attributeValue}"]${failSuffix}`);
163
+ }
164
+ }
125
165
  }
166
+
167
+ // compare children
126
168
  const kids = children(cv) || [];
127
169
  const otherKids = children(e) || [];
128
170
  for (let i = 0; i < kids.length; i++) {
129
- deepCompare(otherKids[i], kids[i], [...path, `${tag(kids[i] as Vode) || "#text"}`]);
171
+ deepCompare(otherKids[i], kids[i], [...path, `[${i}]${tag(kids[i] as Vode)?.toUpperCase() || "#text"}`]);
130
172
  }
131
173
  if (kids.length !== otherKids.length) {
132
- throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\n${kids.length} children\n\nbut [${tag(e)}] has ${otherKids.length} children`);
174
+ throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\n${kids.length} children\n\nbut [${tag(e)?.toUpperCase()}] has ${otherKids.length} children${failSuffix}`);
133
175
  }
134
176
  }
135
177
 
136
- else if (typeof cv === "string" && e instanceof MockElement) {
137
- throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na text node\n\nbut got <${e.tagName}>`);
178
+ // mismatch between text and element
179
+ else if (typeof cv === "string" && e instanceof FakeElement) {
180
+ throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na text node\n\nbut got <${e.tagName.toUpperCase()}>${failSuffix}`);
138
181
  }
182
+ // mismatch between text and vode
139
183
  else if (typeof cv === "string" && Array.isArray(e)) {
140
- throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na text node\n\nbut got [${tag(e)}]`);
184
+ throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na text node\n\nbut got [${tag(e)?.toUpperCase()}]${failSuffix}`);
141
185
  }
142
186
 
143
- else if (Array.isArray(cv) && e instanceof MockText) {
144
- throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\nan element <${tag(cv)}>\n\nbut got #text (${e.wholeText})`);
187
+ // mismatch between vode and text node
188
+ else if (Array.isArray(cv) && e instanceof FakeTextNode) {
189
+ throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\nan element <${tag(cv)?.toUpperCase()}>\n\nbut got #text (${e.wholeText})${failSuffix}`);
145
190
  }
191
+ // mismatch between vode and text
146
192
  else if (Array.isArray(cv) && typeof e === "string") {
147
- throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\nan element <${tag(cv)}>\n\nbut got #text (${e})`);
193
+ throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\nan element <${tag(cv)?.toUpperCase()}>\n\nbut got #text (${e})${failSuffix}`);
148
194
  }
149
195
 
150
196
  return null;
151
197
  }
152
198
 
153
- deepCompare(this.what, v, [tag(v as Vode) || "#text"]);
199
+ deepCompare(this.what, v, [`${tag(v as Vode)?.toUpperCase() || "#text"}`]);
154
200
  } else {
155
- throw new ExpectationError(this, `expected an element or text node\n\nbut it is a ${typeof this.what}\n${this.what}`);
201
+ throw new ExpectationError(this, `expected an element or text node\n\nbut it is a ${typeof this.what}\n${this.what}${failSuffix}`);
156
202
  }
157
203
  }
158
204
  };
package/test/index.ts CHANGED
@@ -17,6 +17,10 @@ import mergeStyleTests from "./tests-mergeStyle";
17
17
  import mergePropsTests from "./tests-mergeProps";
18
18
  import stateContextTests from "./tests-state-context";
19
19
  import mountUnmountTests from "./tests-mount-unmount";
20
+ import exampleTests from "./tests-examples";
21
+ import catchTests from "./tests-catch";
22
+ import patchAdvancedTests from "./tests-patch-advanced";
23
+ import patchMergeTests from "./tests-patch-merge";
20
24
 
21
25
  const tests = {
22
26
  ...vodeTests,
@@ -38,6 +42,10 @@ const tests = {
38
42
  ...mergePropsTests,
39
43
 
40
44
  ...stateContextTests,
45
+ ...exampleTests,
46
+ ...catchTests,
47
+ ...patchAdvancedTests,
48
+ ...patchMergeTests,
41
49
  };
42
50
  //===================================================
43
51
 
@@ -48,15 +56,21 @@ const count = {
48
56
  }
49
57
  const line = "----------------------------------";
50
58
 
51
- for (const test of Object.entries(tests)) {
59
+ async function runTest(test: [string, () => any]) {
52
60
  count.total++;
53
61
  resetMocks();
62
+ const start = performance.now();
54
63
  try {
55
- test[1]()
64
+ const result = test[1]();
65
+ if (result && typeof (result as any)?.then === "function") {
66
+ await result;
67
+ }
56
68
  count.passed++;
57
- console.log(`#${count.total} ${test[0]}\n-> 🟢 passed\n${line}`);
69
+ const time = (performance.now() - start).toFixed(3) + " ms";
70
+ console.log(`#${count.total} ${test[0]}\n-> 🟢 passed ${time}\n${line}`);
58
71
  } catch (err: any) {
59
- console.error(`#${count.total} ${test[0]}\n-> 🔴 failed`);
72
+ const time = (performance.now() - start).toFixed(3) + " ms";
73
+ console.error(`#${count.total} ${test[0]}\n-> 🔴 failed ${time}`);
60
74
  if (err instanceof ExpectationError) {
61
75
  count.failed.push(`#${count.total} ${test[0]}\n-> 🔴 failed:\n${err.message}\n${line}`);
62
76
  }
@@ -66,17 +80,28 @@ for (const test of Object.entries(tests)) {
66
80
  }
67
81
  }
68
82
 
69
- console.log(`
70
- total: ${count.total}
71
- passed: ${count.passed}
72
- failed: ${count.failed.length}
73
- `);
83
+ const sw = performance.now();
84
+ (async () => {
85
+ for (const test of Object.entries(tests)) {
86
+ await runTest(test);
87
+ }
88
+
89
+ const time = (performance.now() - sw).toFixed(3) + " ms";
74
90
 
75
- if (count.passed === count.total) {
76
- console.log("\n\nall tests passed\n");
77
- }
78
- else {
79
- console.error(`${line.replaceAll("-", "=")}\nError summary:\n\n${count.failed.join(`\n${line}\n`)}`);
91
+ console.log(`
92
+ total: ${count.total}
93
+ passed: ${count.passed}
94
+ failed: ${count.failed.length}
80
95
 
81
- throw "\n\nsome tests failed (see output)\n";
82
- }
96
+ time: ${time}
97
+ `);
98
+
99
+ if (count.passed === count.total) {
100
+ console.log("\n\nall tests passed\n");
101
+ }
102
+ else {
103
+ console.error(`${line.replaceAll("-", "=")}\nError summary:\n\n${count.failed.join(`\n${line}\n`)}`);
104
+
105
+ throw "\n\nsome tests failed (see output)\n";
106
+ }
107
+ })();