@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.
- package/README.md +34 -56
- package/dist/vode.cjs.min.js +2 -2
- package/dist/vode.d.ts +6 -5
- package/dist/vode.es5.min.js +7 -7
- package/dist/vode.js +43 -69
- package/dist/vode.min.js +1 -1
- package/dist/vode.min.mjs +1 -1
- package/dist/vode.mjs +43 -69
- package/package.json +1 -1
- package/src/state-context.ts +6 -4
- package/src/vode.ts +53 -76
- package/test/helper.ts +78 -32
- package/test/index.ts +41 -16
- package/test/mocks.ts +132 -38
- package/test/tests-app.ts +117 -1
- package/test/tests-catch.ts +160 -0
- package/test/tests-defuse.ts +22 -1
- package/test/tests-examples.ts +992 -0
- package/test/tests-hydrate.ts +43 -9
- package/test/tests-memo.ts +91 -50
- package/test/tests-patch-advanced.ts +84 -0
- package/test/tests-patch-merge.ts +66 -0
- package/test/tests-state-context.ts +32 -1
package/src/state-context.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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>
|
|
393
|
-
|
|
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
|
|
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
|
|
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
|
|
619
|
-
if (
|
|
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 <
|
|
623
|
-
const child =
|
|
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)
|
|
652
|
+
if (properties?.xmlns !== undefined)
|
|
653
|
+
xmlns = properties.xmlns;
|
|
649
654
|
|
|
650
|
-
|
|
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
|
|
667
|
-
const
|
|
668
|
-
if (
|
|
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 <
|
|
672
|
-
const child =
|
|
673
|
-
const oldChild =
|
|
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
|
-
(<
|
|
671
|
+
(<any>newVode)[i + newStart] = attached;
|
|
677
672
|
if (attached) indexP++;
|
|
678
673
|
}
|
|
679
674
|
}
|
|
680
675
|
|
|
681
|
-
if (
|
|
682
|
-
const newKidsCount =
|
|
683
|
-
for (let i =
|
|
684
|
-
render(state, oldNode as Element, i, i,
|
|
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
|
-
|
|
755
|
-
|
|
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
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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
|
-
|
|
790
|
-
if (typeof
|
|
791
|
-
(<any>
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
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)?.
|
|
124
|
-
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\
|
|
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,
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
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
|
-
|
|
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
|
+
})();
|