@ryupold/vode 1.8.8 → 1.8.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/vode.cjs.min.js +1 -1
- package/dist/vode.d.ts +4 -5
- package/dist/vode.es5.min.js +1 -1
- package/dist/vode.js +54 -44
- package/dist/vode.min.js +1 -1
- package/dist/vode.min.mjs +1 -1
- package/dist/vode.mjs +54 -44
- package/dist/vode.tests.mjs +5303 -0
- package/package.json +5 -5
- package/src/vode.ts +61 -50
- package/test/helper.ts +288 -143
- package/test/index.ts +2 -64
- package/test/mocks.ts +67 -5
- package/test/run-tests.ts +61 -0
- package/test/tests-app.ts +48 -48
- package/test/tests-catch.ts +15 -15
- package/test/tests-children.ts +31 -31
- package/test/tests-createPatch.ts +12 -12
- package/test/tests-createState.ts +11 -11
- package/test/tests-defuse.ts +18 -18
- package/test/tests-examples.ts +87 -88
- package/test/tests-hydrate.ts +28 -28
- package/test/tests-memo.ts +29 -28
- package/test/tests-mergeClass.ts +31 -31
- package/test/tests-mergeProps.ts +19 -19
- package/test/tests-mergeStyle.ts +28 -14
- package/test/tests-mount-unmount.ts +177 -154
- package/test/tests-patch-advanced.ts +19 -17
- package/test/tests-patch-merge.ts +15 -15
- package/test/tests-props.ts +15 -15
- package/test/tests-state-context.ts +33 -33
- package/test/tests-tag.ts +14 -14
- package/test/tests-vode.ts +6 -6
package/test/helper.ts
CHANGED
|
@@ -1,205 +1,350 @@
|
|
|
1
1
|
import { children, ChildVode, PatchableState, props, tag, Vode } from "../src/vode";
|
|
2
2
|
import { FakeElement, FakeTextNode } from "./mocks";
|
|
3
3
|
|
|
4
|
+
// Helper to detect if we're in a browser environment with real DOM
|
|
5
|
+
const isBrowser = typeof window !== 'undefined' && typeof HTMLElement !== 'undefined';
|
|
6
|
+
|
|
7
|
+
// Type guards for real DOM elements
|
|
8
|
+
function isRealElement(node: any): node is HTMLElement {
|
|
9
|
+
return isBrowser && node instanceof HTMLElement;
|
|
10
|
+
}
|
|
11
|
+
function isRealTextNode(node: any): node is Text {
|
|
12
|
+
return isBrowser && node instanceof Text;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
|
16
|
+
|
|
17
|
+
function retry<T = void>(fn: () => Promise<T>, waitTime: number): Promise<T> {
|
|
18
|
+
const { promise, resolve, reject } = Promise.withResolvers<T>();
|
|
19
|
+
|
|
20
|
+
function retryInternal(timeLeft: number) {
|
|
21
|
+
const start = performance.now();
|
|
22
|
+
try {
|
|
23
|
+
const prom = fn();
|
|
24
|
+
if (typeof prom?.then === "function") {
|
|
25
|
+
prom.then(resolve).catch((err) => {
|
|
26
|
+
if (timeLeft >= 0) {
|
|
27
|
+
setTimeout(
|
|
28
|
+
() => retryInternal(timeLeft - (performance.now() - start)),
|
|
29
|
+
10
|
|
30
|
+
);
|
|
31
|
+
} else {
|
|
32
|
+
reject(err);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
} else {
|
|
36
|
+
resolve(prom);
|
|
37
|
+
}
|
|
38
|
+
} catch (err) {
|
|
39
|
+
if (timeLeft >= 0) {
|
|
40
|
+
setTimeout(() => {
|
|
41
|
+
retryInternal(timeLeft - (performance.now() - start))
|
|
42
|
+
}, 10);
|
|
43
|
+
} else {
|
|
44
|
+
reject(err);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
retryInternal(waitTime);
|
|
50
|
+
|
|
51
|
+
return promise;
|
|
52
|
+
}
|
|
53
|
+
|
|
4
54
|
export class Expectation {
|
|
5
55
|
constructor(public readonly what: any) { }
|
|
6
56
|
|
|
57
|
+
toBeNotHidden() {
|
|
58
|
+
if (document.hidden) {
|
|
59
|
+
throw new ExpectationError(this, `expect the document to be not hidden. if you run this in a real browser this means the window must be in focus in order for the tests to work.`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
7
63
|
toBeA(type: "undefined" | "object" | "function" | "bigint" | "boolean" | "number" | "string" | "symbol", failMessage?: string) {
|
|
8
64
|
if (typeof this.what !== type) {
|
|
9
65
|
throw new ExpectationError(this, `expected \n\ntypeof ${this.what}\n\nto be \n\n${type}${failMessage ? `\n\n${failMessage}` : ""}`);
|
|
10
66
|
}
|
|
11
67
|
}
|
|
12
68
|
|
|
13
|
-
|
|
14
|
-
|
|
69
|
+
toBeGreaterThan(other: number, failMessage?: string) {
|
|
70
|
+
if (!(this.what > other)) {
|
|
71
|
+
throw new ExpectationError(this, `expected \n\n${this.what}\n\nto be >\n\n${other}${failMessage ? `\n\n${failMessage}` : ""}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
15
74
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
75
|
+
toBeGreaterOrEqualThan(other: number, failMessage?: string) {
|
|
76
|
+
if (!(this.what >= other)) {
|
|
77
|
+
throw new ExpectationError(this, `expected \n\n${this.what}\n\nto be >= \n\n${other}${failMessage ? `\n\n${failMessage}` : ""}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
22
80
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
81
|
+
toBeSmallerThan(other: number, failMessage?: string) {
|
|
82
|
+
if (!(this.what < other)) {
|
|
83
|
+
throw new ExpectationError(this, `expected \n\n${this.what}\n\nto be <\n\n${other}${failMessage ? `\n\n${failMessage}` : ""}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
28
86
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
87
|
+
toBeSmallerOrEqual(other: number, failMessage?: string) {
|
|
88
|
+
if (!(this.what <= other)) {
|
|
89
|
+
throw new ExpectationError(this, `expected \n\n${this.what}\n\nto be <= \n\n${other}${failMessage ? `\n\n${failMessage}` : ""}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async toEqual(other: any, failMessage?: string, waitTimeMs: number = 100) {
|
|
94
|
+
return await retry(
|
|
95
|
+
async () => {
|
|
96
|
+
const failSuffix = failMessage ? `\n\n${failMessage}` : "";
|
|
97
|
+
|
|
98
|
+
function deepCompare(a: any, b: any, path: string[]): string[] | null {
|
|
99
|
+
if (typeof a !== typeof b) {
|
|
100
|
+
if (path.length === 0) path.push(``);
|
|
101
|
+
path[path.length - 1] += ` (type: ${typeof a} != ${typeof b})`;
|
|
102
|
+
return path;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (typeof a !== "object" || a === null) {
|
|
106
|
+
if (path.length === 0) path.push(``);
|
|
107
|
+
path[path.length - 1] += ` (value: ${a} != ${b})`;
|
|
108
|
+
return a !== b ? path : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const prop of Object.entries(a)) {
|
|
112
|
+
const [k, v] = prop;
|
|
113
|
+
const result = deepCompare(v, b[k], [...path, k]);
|
|
114
|
+
if (result) {
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for (const prop of Object.entries(b)) {
|
|
120
|
+
const [k, v] = prop;
|
|
121
|
+
const result = deepCompare(a[k], v, [...path, k]);
|
|
122
|
+
if (result) {
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return null;
|
|
34
128
|
}
|
|
35
|
-
}
|
|
36
129
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
130
|
+
if (typeof this.what === "object" && typeof other === "object" && this.what !== null && other !== null) {
|
|
131
|
+
const unequal = deepCompare(this.what, other, []);
|
|
132
|
+
if (unequal) {
|
|
133
|
+
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}`);
|
|
134
|
+
}
|
|
42
135
|
}
|
|
43
|
-
|
|
136
|
+
else {
|
|
137
|
+
if (this.what !== other) {
|
|
138
|
+
throw new ExpectationError(this, `expected (${typeof this.what})\n\n${this.what}\n\nto equal (${typeof other})\n\n${other}${failSuffix}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
waitTimeMs
|
|
143
|
+
);
|
|
144
|
+
}
|
|
44
145
|
|
|
45
|
-
|
|
146
|
+
toSucceed<Result>(): Result {
|
|
147
|
+
if (typeof this.what !== "function") {
|
|
148
|
+
throw new ExpectationError(this, `expected a function\n\nbut it is a ${typeof this.what}`);
|
|
46
149
|
}
|
|
150
|
+
return this.what();
|
|
151
|
+
}
|
|
47
152
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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}`);
|
|
52
|
-
}
|
|
153
|
+
toFail(): Error {
|
|
154
|
+
if (typeof this.what !== "function") {
|
|
155
|
+
throw new ExpectationError(this, `expected a function\n\nbut it is a ${typeof this.what}`);
|
|
53
156
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
157
|
+
|
|
158
|
+
let r: any;
|
|
159
|
+
try {
|
|
160
|
+
r = this.what();
|
|
161
|
+
} catch (err: any) {
|
|
162
|
+
return err;
|
|
58
163
|
}
|
|
164
|
+
throw new ExpectationError(this, `expected function to fail\n\nbut it succeeded with a result of type ${typeof r}\n\n${r}`);
|
|
59
165
|
}
|
|
60
166
|
|
|
61
|
-
|
|
167
|
+
toSucceedAsync<Result>(waitTime: number = 100): Promise<Result> {
|
|
62
168
|
if (typeof this.what !== "function") {
|
|
63
169
|
throw new ExpectationError(this, `expected a function\n\nbut it is a ${typeof this.what}`);
|
|
64
170
|
}
|
|
65
|
-
return this.what(
|
|
171
|
+
return retry<Result>(() => this.what(), waitTime);
|
|
66
172
|
}
|
|
67
173
|
|
|
68
|
-
|
|
174
|
+
async toFailAsync(): Promise<Error> {
|
|
69
175
|
if (typeof this.what !== "function") {
|
|
70
176
|
throw new ExpectationError(this, `expected a function\n\nbut it is a ${typeof this.what}`);
|
|
71
177
|
}
|
|
72
178
|
|
|
73
179
|
let r: any;
|
|
74
180
|
try {
|
|
75
|
-
r = this.what(
|
|
181
|
+
r = await this.what();
|
|
76
182
|
} catch (err: any) {
|
|
77
183
|
return err;
|
|
78
184
|
}
|
|
79
185
|
throw new ExpectationError(this, `expected function to fail\n\nbut it succeeded with a result of type ${typeof r}\n\n${r}`);
|
|
80
186
|
}
|
|
81
187
|
|
|
82
|
-
toMatch(v: ChildVode,
|
|
83
|
-
|
|
188
|
+
async toMatch(v: ChildVode,
|
|
189
|
+
state?: PatchableState | null,
|
|
190
|
+
failMessage?: string,
|
|
191
|
+
waitTimeMs: number = 100
|
|
192
|
+
) {
|
|
193
|
+
return await retry(
|
|
194
|
+
async () => {
|
|
195
|
+
const failSuffix = failMessage ? `\n\n${failMessage}` : "";
|
|
84
196
|
|
|
85
|
-
|
|
86
|
-
|
|
197
|
+
// Support FakeElement, FakeTextNode, real HTMLElement, real Text nodes, strings, arrays, functions
|
|
198
|
+
if (this.what instanceof FakeElement || this.what instanceof FakeTextNode ||
|
|
199
|
+
isRealElement(this.what) || isRealTextNode(this.what) ||
|
|
200
|
+
typeof this.what === "string" || Array.isArray(this.what) || typeof this.what === "function") {
|
|
201
|
+
const that = this;
|
|
87
202
|
|
|
88
|
-
|
|
203
|
+
function deepCompare(e: FakeElement | FakeTextNode | HTMLElement | Text | ChildVode, cv: ChildVode, path: string[]): string[] | null {
|
|
89
204
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
205
|
+
// unwrap component
|
|
206
|
+
while (typeof cv === "function") {
|
|
207
|
+
if (!state) {
|
|
208
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na Component\n\nbut got no state passed in [toMatch]${failSuffix}`);
|
|
209
|
+
}
|
|
210
|
+
cv = cv(state);
|
|
211
|
+
}
|
|
212
|
+
while (typeof e === "function") {
|
|
213
|
+
if (!state) {
|
|
214
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na Component\n\nbut got no state passed in [toMatch]${failSuffix}`);
|
|
215
|
+
}
|
|
216
|
+
e = e(state);
|
|
217
|
+
}
|
|
103
218
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
219
|
+
// string matches TextNode (fake or real)
|
|
220
|
+
if (typeof cv === "string" && (e instanceof FakeTextNode || isRealTextNode(e))) {
|
|
221
|
+
const text = e instanceof FakeTextNode ? e.wholeText : (e as Text).wholeText;
|
|
222
|
+
if (cv !== text) {
|
|
223
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na text node with\n${cv}\n\nbut text was\n${text}${failSuffix}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// string matches string
|
|
227
|
+
else if (typeof cv === "string" && typeof e === "string") {
|
|
228
|
+
if (cv !== e) {
|
|
229
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na text node with\n${cv}\n\nbut text was\n${e}${failSuffix}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
116
232
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
233
|
+
// vode matches element (fake or real)
|
|
234
|
+
else if (Array.isArray(cv) && (e instanceof FakeElement || isRealElement(e))) {
|
|
235
|
+
const tagName = e instanceof FakeElement ? e.tagName : (e as HTMLElement).tagName;
|
|
236
|
+
if (tag(cv)?.toUpperCase() !== tagName.toUpperCase()) {
|
|
237
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\nan element <${tag(cv)?.toUpperCase()}>\n\nbut got <${tagName.toUpperCase()}>${failSuffix}`);
|
|
238
|
+
}
|
|
122
239
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
240
|
+
// compare attributes/props
|
|
241
|
+
const properties = props(cv);
|
|
242
|
+
if (properties) {
|
|
243
|
+
for (const [k, val] of Object.entries(properties)) {
|
|
244
|
+
let attributeValue: string | null;
|
|
245
|
+
if (e instanceof FakeElement) {
|
|
246
|
+
attributeValue = e.fakeAttributes[k] ?? null;
|
|
247
|
+
} else {
|
|
248
|
+
attributeValue = (e as HTMLElement).getAttribute(k);
|
|
249
|
+
}
|
|
250
|
+
if (!attributeValue) {
|
|
251
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\nan element <${tag(cv)?.toUpperCase()}>\n\nwith attribute [${k}="${val}"]\n\nbut it was not found${failSuffix}`);
|
|
252
|
+
}
|
|
253
|
+
if (attributeValue !== val) {
|
|
254
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\nan element <${tag(cv)?.toUpperCase()}>\n\nwith attribute [${k}="${val}"]\n\nbut it was [${k}="${attributeValue}"]${failSuffix}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
130
257
|
}
|
|
131
|
-
|
|
132
|
-
|
|
258
|
+
|
|
259
|
+
// compare children - handle browser text node normalization
|
|
260
|
+
const kids = children(cv) || [];
|
|
261
|
+
const childNodes = e instanceof FakeElement ? e.children : (e as HTMLElement).childNodes;
|
|
262
|
+
|
|
263
|
+
// Check if all kids are text (strings) - browsers may merge these
|
|
264
|
+
const allKidsAreText = kids.every(k => typeof k === "string");
|
|
265
|
+
if (allKidsAreText && isBrowser && kids.length > 1) {
|
|
266
|
+
// Browser likely merged text nodes - compare concatenated text
|
|
267
|
+
const expectedText = kids.join("");
|
|
268
|
+
const actualText = (e as HTMLElement).textContent || "";
|
|
269
|
+
if (expectedText !== actualText) {
|
|
270
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\ntext content "${expectedText}"\n\nbut got "${actualText}"${failSuffix}`);
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
// Normal child-by-child comparison
|
|
274
|
+
for (let i = 0; i < kids.length; i++) {
|
|
275
|
+
const childNode = e instanceof FakeElement
|
|
276
|
+
? childNodes.item(i) as any
|
|
277
|
+
: childNodes.item(i) as any;
|
|
278
|
+
deepCompare(childNode, kids[i], [...path, `[${i}]${tag(kids[i] as Vode)?.toUpperCase() || "#text"}`]);
|
|
279
|
+
}
|
|
280
|
+
const childCount = e instanceof FakeElement ? e.children.length : (e as HTMLElement).childNodes.length;
|
|
281
|
+
if (kids.length !== childCount) {
|
|
282
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\n${kids.length} children\n\nbut <${tagName.toUpperCase()}> has ${childCount} children${failSuffix}`);
|
|
283
|
+
}
|
|
133
284
|
}
|
|
134
285
|
}
|
|
135
|
-
|
|
286
|
+
// vode matches vode
|
|
287
|
+
else if (Array.isArray(cv) && Array.isArray(e)) {
|
|
288
|
+
if (tag(cv)?.toUpperCase() !== tag(e)?.toUpperCase()) {
|
|
289
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na vode [${tag(cv)?.toUpperCase()}]\n\nbut got [${tag(e)?.toUpperCase()}]${failSuffix}`);
|
|
290
|
+
}
|
|
136
291
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
292
|
+
// compare attributes/props
|
|
293
|
+
const properties = props(cv);
|
|
294
|
+
const otherProperties = props(e) || {};
|
|
295
|
+
if (properties) {
|
|
296
|
+
for (const [k, val] of Object.entries(properties)) {
|
|
297
|
+
const attributeValue = otherProperties[k];
|
|
298
|
+
if (!attributeValue) {
|
|
299
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na vode [${tag(cv)?.toUpperCase()}]\n\nwith attribute [${k}="${val}"]\n\nbut it was not found${failSuffix}`);
|
|
300
|
+
}
|
|
301
|
+
if (attributeValue !== val) {
|
|
302
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na vode [${tag(cv)?.toUpperCase()}]\n\nwith attribute [${k}="${val}"]\n\nbut its value was [${k}="${attributeValue}"]${failSuffix}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
151
306
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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}`);
|
|
307
|
+
// compare children
|
|
308
|
+
const kids = children(cv) || [];
|
|
309
|
+
const otherKids = children(e) || [];
|
|
310
|
+
for (let i = 0; i < kids.length; i++) {
|
|
311
|
+
deepCompare(otherKids[i], kids[i], [...path, `[${i}]${tag(kids[i] as Vode)?.toUpperCase() || "#text"}`]);
|
|
160
312
|
}
|
|
161
|
-
if (
|
|
162
|
-
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\
|
|
313
|
+
if (kids.length !== otherKids.length) {
|
|
314
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\n${kids.length} children\n\nbut [${tag(e)?.toUpperCase()}] has ${otherKids.length} children${failSuffix}`);
|
|
163
315
|
}
|
|
164
316
|
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// compare children
|
|
168
|
-
const kids = children(cv) || [];
|
|
169
|
-
const otherKids = children(e) || [];
|
|
170
|
-
for (let i = 0; i < kids.length; i++) {
|
|
171
|
-
deepCompare(otherKids[i], kids[i], [...path, `[${i}]${tag(kids[i] as Vode)?.toUpperCase() || "#text"}`]);
|
|
172
|
-
}
|
|
173
|
-
if (kids.length !== otherKids.length) {
|
|
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}`);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
317
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
318
|
+
// mismatch between text and element (fake or real)
|
|
319
|
+
else if (typeof cv === "string" && (e instanceof FakeElement || isRealElement(e))) {
|
|
320
|
+
const tagName = e instanceof FakeElement ? e.tagName : (e as HTMLElement).tagName;
|
|
321
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na text node\n\nbut got <${tagName.toUpperCase()}>${failSuffix}`);
|
|
322
|
+
}
|
|
323
|
+
// mismatch between text and vode
|
|
324
|
+
else if (typeof cv === "string" && Array.isArray(e)) {
|
|
325
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na text node\n\nbut got [${tag(e)?.toUpperCase()}]${failSuffix}`);
|
|
326
|
+
}
|
|
186
327
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
328
|
+
// mismatch between vode and text node (fake or real)
|
|
329
|
+
else if (Array.isArray(cv) && (e instanceof FakeTextNode || isRealTextNode(e))) {
|
|
330
|
+
const text = e instanceof FakeTextNode ? e.wholeText : (e as Text).wholeText;
|
|
331
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\nan element <${tag(cv)?.toUpperCase()}>\n\nbut got #text (${text})${failSuffix}`);
|
|
332
|
+
}
|
|
333
|
+
// mismatch between vode and text
|
|
334
|
+
else if (Array.isArray(cv) && typeof e === "string") {
|
|
335
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\nan element <${tag(cv)?.toUpperCase()}>\n\nbut got #text (${e})${failSuffix}`);
|
|
336
|
+
}
|
|
195
337
|
|
|
196
|
-
|
|
197
|
-
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
198
340
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
341
|
+
deepCompare(this.what, v, [`${tag(v as Vode)?.toUpperCase() || "#text"}`]);
|
|
342
|
+
} else {
|
|
343
|
+
throw new ExpectationError(this, `expected an element or text node\n\nbut it is a ${typeof this.what}\n${this.what}${failSuffix}`);
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
waitTimeMs
|
|
347
|
+
);
|
|
203
348
|
}
|
|
204
349
|
};
|
|
205
350
|
|
|
@@ -211,4 +356,4 @@ export class ExpectationError extends Error {
|
|
|
211
356
|
|
|
212
357
|
export function expect(what: any) {
|
|
213
358
|
return new Expectation(what);
|
|
214
|
-
}
|
|
359
|
+
}
|
package/test/index.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import { resetMocks } from "./mocks";
|
|
2
|
-
import { ExpectationError } from "./helper";
|
|
3
|
-
|
|
4
|
-
//=== REGISTERED TESTS =========================================
|
|
5
1
|
import vodeTests from "./tests-vode";
|
|
6
2
|
import appTests from "./tests-app";
|
|
7
3
|
import defuseTests from "./tests-defuse";
|
|
@@ -22,7 +18,7 @@ import catchTests from "./tests-catch";
|
|
|
22
18
|
import patchAdvancedTests from "./tests-patch-advanced";
|
|
23
19
|
import patchMergeTests from "./tests-patch-merge";
|
|
24
20
|
|
|
25
|
-
const tests = {
|
|
21
|
+
export const tests = {
|
|
26
22
|
...vodeTests,
|
|
27
23
|
...appTests,
|
|
28
24
|
...defuseTests,
|
|
@@ -46,62 +42,4 @@ const tests = {
|
|
|
46
42
|
...catchTests,
|
|
47
43
|
...patchAdvancedTests,
|
|
48
44
|
...patchMergeTests,
|
|
49
|
-
};
|
|
50
|
-
//===================================================
|
|
51
|
-
|
|
52
|
-
const count = {
|
|
53
|
-
total: 0,
|
|
54
|
-
passed: 0,
|
|
55
|
-
failed: <string[]>[],
|
|
56
|
-
}
|
|
57
|
-
const line = "----------------------------------";
|
|
58
|
-
|
|
59
|
-
async function runTest(test: [string, () => any]) {
|
|
60
|
-
count.total++;
|
|
61
|
-
resetMocks();
|
|
62
|
-
const start = performance.now();
|
|
63
|
-
try {
|
|
64
|
-
const result = test[1]();
|
|
65
|
-
if (result && typeof (result as any)?.then === "function") {
|
|
66
|
-
await result;
|
|
67
|
-
}
|
|
68
|
-
count.passed++;
|
|
69
|
-
const time = (performance.now() - start).toFixed(3) + " ms";
|
|
70
|
-
console.log(`#${count.total} ${test[0]}\n-> 🟢 passed ${time}\n${line}`);
|
|
71
|
-
} catch (err: any) {
|
|
72
|
-
const time = (performance.now() - start).toFixed(3) + " ms";
|
|
73
|
-
console.error(`#${count.total} ${test[0]}\n-> 🔴 failed ${time}`);
|
|
74
|
-
if (err instanceof ExpectationError) {
|
|
75
|
-
count.failed.push(`#${count.total} ${test[0]}\n-> 🔴 failed:\n${err.message}\n${line}`);
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
count.failed.push(`#${count.total} ${test[0]}\n-> 🔴 failed:\n${err.message}\n${err.stack}\n${line}`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
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";
|
|
90
|
-
|
|
91
|
-
console.log(`
|
|
92
|
-
total: ${count.total}
|
|
93
|
-
passed: ${count.passed}
|
|
94
|
-
failed: ${count.failed.length}
|
|
95
|
-
|
|
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
|
-
})();
|
|
45
|
+
};
|