@ryupold/vode 1.8.7 → 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/README.md +34 -56
- package/dist/vode.cjs.min.js +2 -2
- package/dist/vode.d.ts +10 -10
- package/dist/vode.es5.min.js +7 -7
- package/dist/vode.js +97 -113
- package/dist/vode.min.js +1 -1
- package/dist/vode.min.mjs +1 -1
- package/dist/vode.mjs +97 -113
- package/dist/vode.tests.mjs +5303 -0
- package/package.json +5 -5
- package/src/state-context.ts +6 -4
- package/src/vode.ts +114 -126
- package/test/helper.ts +304 -113
- package/test/index.ts +10 -47
- package/test/mocks.ts +199 -43
- package/test/run-tests.ts +61 -0
- package/test/tests-app.ts +154 -38
- package/test/tests-catch.ts +160 -0
- 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 +35 -14
- package/test/tests-examples.ts +991 -0
- package/test/tests-hydrate.ts +59 -25
- package/test/tests-memo.ts +106 -64
- 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 +86 -0
- package/test/tests-patch-merge.ts +66 -0
- package/test/tests-props.ts +15 -15
- package/test/tests-state-context.ts +56 -25
- package/test/tests-tag.ts +14 -14
- package/test/tests-vode.ts +6 -6
package/test/mocks.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { globals } from "../src/vode";
|
|
2
|
+
|
|
1
3
|
const NodeConstants = {
|
|
2
4
|
ELEMENT_NODE: 1,
|
|
3
5
|
ATTRIBUTE_NODE: 2,
|
|
@@ -13,88 +15,208 @@ const NodeConstants = {
|
|
|
13
15
|
NOTATION_NODE: 12,
|
|
14
16
|
};
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
class FakeNodeList implements NodeListOf<ChildNode> {
|
|
19
|
+
[index: number]: ChildNode;
|
|
20
|
+
public readonly data: ChildNode[] = [];
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
let self = this;
|
|
24
|
+
|
|
25
|
+
return new Proxy(this, {
|
|
26
|
+
get(target, prop) {
|
|
27
|
+
const key: string = typeof prop === "symbol" ? String(prop) : prop;
|
|
28
|
+
|
|
29
|
+
if (<any>Number(key) == key && !(prop in target)) {
|
|
30
|
+
return self.data[parseInt(key)];
|
|
31
|
+
}
|
|
32
|
+
return target[prop as any];
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
item(index: number): ChildNode {
|
|
39
|
+
return this.data[index] ?? null;
|
|
40
|
+
}
|
|
41
|
+
forEach(callbackfn: (value: ChildNode, key: number, parent: NodeListOf<ChildNode>) => void, thisArg?: any): void {
|
|
42
|
+
for (let i = 0; i < this.length; i++) {
|
|
43
|
+
callbackfn.bind(thisArg)(this.data[i], i, this);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
entries(): ArrayIterator<[number, ChildNode]> {
|
|
47
|
+
return new Array(this.length).fill(0).map((_, i) => [i, this[i]] as [number, ChildNode])[Symbol.iterator]();
|
|
48
|
+
}
|
|
49
|
+
keys(): ArrayIterator<number> {
|
|
50
|
+
return new Array(this.data.length).fill(0).map((_, i) => i)[Symbol.iterator]();
|
|
51
|
+
}
|
|
52
|
+
values(): ArrayIterator<ChildNode> {
|
|
53
|
+
return new Array(this.data.length).fill(0).map((_, i) => this[i])[Symbol.iterator]();
|
|
54
|
+
}
|
|
55
|
+
[Symbol.iterator](): ArrayIterator<ChildNode> {
|
|
56
|
+
return new Array(this.data.length).fill(0).map((_, i) => this[i])[Symbol.iterator]();
|
|
57
|
+
}
|
|
58
|
+
get length() {
|
|
59
|
+
return this.data.length;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class FakeElement {
|
|
64
|
+
public fakeAttributes: Record<string, string> = {};
|
|
65
|
+
|
|
17
66
|
nodeType = NodeConstants.ELEMENT_NODE;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return Object.entries(this._attrs).map(([name, value]) => ({ name, value }));
|
|
67
|
+
parentElement: HTMLElement | null = null;
|
|
68
|
+
childNodes: NodeListOf<ChildNode> = new FakeNodeList();
|
|
69
|
+
get children(): HTMLCollection {
|
|
70
|
+
return this.childNodes as unknown as HTMLCollection;
|
|
23
71
|
}
|
|
24
72
|
style: { cssText: string } = { cssText: "" };
|
|
25
|
-
|
|
26
|
-
|
|
73
|
+
|
|
74
|
+
readonly tagName: string;
|
|
27
75
|
|
|
28
76
|
constructor(public tag?: string) {
|
|
29
|
-
|
|
77
|
+
this.tagName = tag?.toUpperCase() || "???";
|
|
30
78
|
}
|
|
31
79
|
|
|
32
80
|
get firstChild() { return this.childNodes[0] ?? null; }
|
|
33
81
|
get lastChild() { return this.childNodes[this.childNodes.length - 1] ?? null; }
|
|
34
82
|
get nextSibling() { return null; }
|
|
83
|
+
get attributes() {
|
|
84
|
+
return Object.entries(this.fakeAttributes).map(([name, value]) => ({ name, value })) as any;
|
|
85
|
+
}
|
|
35
86
|
|
|
36
|
-
hasAttributes() { return Object.keys(this.
|
|
87
|
+
hasAttributes() { return Object.keys(this.fakeAttributes).length > 0; }
|
|
37
88
|
hasChildNodes() { return this.childNodes.length > 0; }
|
|
38
|
-
setAttribute(name: string, value: string) { this.
|
|
39
|
-
removeAttribute(name: string) { delete this.
|
|
40
|
-
|
|
41
|
-
|
|
89
|
+
setAttribute(name: string, value: string) { this.fakeAttributes[name] = value; }
|
|
90
|
+
removeAttribute(name: string) { delete this.fakeAttributes[name]; }
|
|
91
|
+
|
|
92
|
+
appendChild(child: FakeElement | FakeTextNode): FakeElement | FakeTextNode {
|
|
93
|
+
(this.childNodes as FakeNodeList).data.push(child as any);
|
|
94
|
+
(child as any).parentElement = this;
|
|
95
|
+
return child;
|
|
42
96
|
}
|
|
43
97
|
remove() {
|
|
44
|
-
if (this.parentElement) {
|
|
98
|
+
if (this.parentElement) {
|
|
99
|
+
const i = (this.parentElement.childNodes as FakeNodeList).data.indexOf(this as any);
|
|
100
|
+
if (i >= 0)
|
|
101
|
+
(this.parentElement.childNodes as FakeNodeList).data.splice(i, 1);
|
|
102
|
+
}
|
|
45
103
|
}
|
|
46
|
-
replaceWith(...nodes: (
|
|
104
|
+
replaceWith(...nodes: (FakeElement | FakeTextNode)[]) {
|
|
47
105
|
const parent = this.parentElement;
|
|
48
106
|
if (parent) {
|
|
49
|
-
const i = parent.childNodes.indexOf(this);
|
|
50
|
-
if (i >= 0) {
|
|
51
|
-
|
|
107
|
+
const i = (<FakeNodeList>parent.childNodes).data.indexOf(this as any);
|
|
108
|
+
if (i >= 0) {
|
|
109
|
+
(<FakeNodeList>parent.childNodes).data.splice(i, 1, ...nodes as any);
|
|
110
|
+
}
|
|
111
|
+
for (const n of nodes) {
|
|
112
|
+
n.parentElement = parent;
|
|
113
|
+
}
|
|
52
114
|
}
|
|
53
115
|
}
|
|
54
|
-
before(...nodes: (
|
|
116
|
+
before(...nodes: (FakeElement | FakeTextNode)[]) {
|
|
55
117
|
const parent = this.parentElement;
|
|
56
118
|
if (parent) {
|
|
57
|
-
const i = parent.childNodes.indexOf(this);
|
|
58
|
-
if (i >= 0) {
|
|
59
|
-
|
|
119
|
+
const i = (<FakeNodeList>parent.childNodes).data.indexOf(this as any);
|
|
120
|
+
if (i >= 0) {
|
|
121
|
+
for (const n of nodes) {
|
|
122
|
+
if (n === this) continue;
|
|
123
|
+
if (n.parentElement) {
|
|
124
|
+
const ni = (<FakeNodeList>n.parentElement.childNodes).data.indexOf(n as any);
|
|
125
|
+
if (ni >= 0) (<FakeNodeList>n.parentElement.childNodes).data.splice(ni, 1);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const filtered = nodes.filter(n => n !== this);
|
|
129
|
+
(<FakeNodeList>parent.childNodes).data.splice(i, 0, ...filtered as any);
|
|
130
|
+
for (const n of filtered) {
|
|
131
|
+
n.parentElement = parent;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
60
134
|
}
|
|
61
135
|
}
|
|
62
|
-
get [Symbol.iterator]() {
|
|
136
|
+
get [Symbol.iterator]() {
|
|
137
|
+
return Array.prototype[Symbol.iterator].bind(this.children);
|
|
138
|
+
}
|
|
63
139
|
}
|
|
64
140
|
|
|
65
|
-
export class
|
|
141
|
+
export class FakeTextNode {
|
|
66
142
|
nodeType = NodeConstants.TEXT_NODE;
|
|
67
|
-
parentElement:
|
|
143
|
+
parentElement: HTMLElement | null = null;
|
|
68
144
|
constructor(public nodeValue: string) { }
|
|
69
145
|
get wholeText() { return this.nodeValue; }
|
|
70
|
-
remove() {
|
|
71
|
-
|
|
146
|
+
remove() {
|
|
147
|
+
if (this.parentElement) {
|
|
148
|
+
const i = (<FakeNodeList>this.parentElement.childNodes).data.indexOf(this as any);
|
|
149
|
+
if (i >= 0)
|
|
150
|
+
(<FakeNodeList>this.parentElement.childNodes).data.splice(i, 1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
replaceWith(...nodes: (FakeElement | FakeTextNode)[]) {
|
|
72
154
|
const parent = this.parentElement;
|
|
73
155
|
if (parent) {
|
|
74
|
-
const i = parent.childNodes.indexOf(this);
|
|
75
|
-
if (i >= 0) { parent.childNodes.splice(i, 1, ...nodes); }
|
|
76
|
-
for (const n of nodes)
|
|
156
|
+
const i = (<FakeNodeList>parent.childNodes).data.indexOf(this as any);
|
|
157
|
+
if (i >= 0) { (<FakeNodeList>parent.childNodes).data.splice(i, 1, ...nodes as any); }
|
|
158
|
+
for (const n of nodes) {
|
|
159
|
+
n.parentElement = parent;
|
|
160
|
+
}
|
|
77
161
|
}
|
|
78
162
|
}
|
|
79
|
-
before(...nodes: (
|
|
163
|
+
before(...nodes: (FakeElement | FakeTextNode)[]) {
|
|
80
164
|
const parent = this.parentElement;
|
|
81
165
|
if (parent) {
|
|
82
|
-
const i = parent.childNodes.indexOf(this);
|
|
83
|
-
if (i >= 0) {
|
|
84
|
-
|
|
166
|
+
const i = (<FakeNodeList>parent.childNodes).data.indexOf(this as any);
|
|
167
|
+
if (i >= 0) {
|
|
168
|
+
for (const n of nodes) {
|
|
169
|
+
if (n === this) continue;
|
|
170
|
+
if (n.parentElement) {
|
|
171
|
+
const ni = (<FakeNodeList>n.parentElement.childNodes).data.indexOf(n as any);
|
|
172
|
+
if (ni >= 0) (<FakeNodeList>n.parentElement.childNodes).data.splice(ni, 1);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const filtered = nodes.filter(n => n !== this);
|
|
176
|
+
(<FakeNodeList>parent.childNodes).data.splice(i, 0, ...filtered as any);
|
|
177
|
+
for (const n of filtered) {
|
|
178
|
+
n.parentElement = parent;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
85
181
|
}
|
|
86
182
|
}
|
|
87
183
|
}
|
|
88
184
|
|
|
89
185
|
export function resetMocks() {
|
|
186
|
+
let hidden = false;
|
|
187
|
+
let rafHandle = 0;
|
|
188
|
+
const rafQueue = new Map<number, FrameRequestCallback>();
|
|
189
|
+
let rafTimer: ReturnType<typeof setTimeout> | null = null;
|
|
190
|
+
|
|
191
|
+
function scheduleNextFrame() {
|
|
192
|
+
if (rafTimer !== null || hidden || rafQueue.size === 0) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
rafTimer = setTimeout(() => {
|
|
197
|
+
rafTimer = null;
|
|
198
|
+
|
|
199
|
+
if (hidden || rafQueue.size === 0) {
|
|
200
|
+
scheduleNextFrame();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const now = performance.now();
|
|
205
|
+
const callbacks = Array.from(rafQueue.values());
|
|
206
|
+
rafQueue.clear();
|
|
207
|
+
|
|
208
|
+
for (const cb of callbacks) {
|
|
209
|
+
cb(now);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
scheduleNextFrame();
|
|
213
|
+
}, 16);
|
|
214
|
+
}
|
|
215
|
+
|
|
90
216
|
const mockDoc: any = {
|
|
91
|
-
createElement: (tag: string) => new
|
|
92
|
-
createTextNode: (text: string) => new
|
|
93
|
-
createElementNS: (ns: string, tag: string) => new
|
|
94
|
-
hidden: false,
|
|
95
|
-
};
|
|
96
|
-
const mockWin: any = {
|
|
97
|
-
requestAnimationFrame: (cb: any) => cb(Date.now()),
|
|
217
|
+
createElement: (tag: string) => new FakeElement(tag),
|
|
218
|
+
createTextNode: (text: string) => new FakeTextNode(text),
|
|
219
|
+
createElementNS: (ns: string, tag: string) => new FakeElement(tag),
|
|
98
220
|
startViewTransition: (callbackOptions: any) => {
|
|
99
221
|
return {
|
|
100
222
|
finished: Promise.resolve(),
|
|
@@ -105,7 +227,41 @@ export function resetMocks() {
|
|
|
105
227
|
}
|
|
106
228
|
};
|
|
107
229
|
|
|
230
|
+
Object.defineProperty(mockDoc, "hidden", {
|
|
231
|
+
enumerable: true,
|
|
232
|
+
configurable: true,
|
|
233
|
+
get: () => hidden,
|
|
234
|
+
set: (value: boolean) => {
|
|
235
|
+
hidden = !!value;
|
|
236
|
+
if (!hidden) {
|
|
237
|
+
scheduleNextFrame();
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const mockWin: any = {
|
|
243
|
+
requestAnimationFrame: (cb: FrameRequestCallback) => {
|
|
244
|
+
const id = ++rafHandle;
|
|
245
|
+
rafQueue.set(id, cb);
|
|
246
|
+
scheduleNextFrame();
|
|
247
|
+
return id;
|
|
248
|
+
},
|
|
249
|
+
cancelAnimationFrame: (id: number) => {
|
|
250
|
+
rafQueue.delete(id);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
108
254
|
globalThis.document ??= mockDoc as Document;
|
|
109
255
|
globalThis.window ??= mockWin as (Window & typeof globalThis);
|
|
110
256
|
globalThis.Node ??= NodeConstants as any;
|
|
111
|
-
|
|
257
|
+
|
|
258
|
+
const raf = globalThis.window?.requestAnimationFrame;
|
|
259
|
+
if (typeof raf === "function") {
|
|
260
|
+
globals.requestAnimationFrame = raf.bind(globalThis.window);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const startViewTransition = (globalThis.document as any)?.startViewTransition;
|
|
264
|
+
globals.startViewTransition = typeof startViewTransition === "function"
|
|
265
|
+
? startViewTransition.bind(globalThis.document)
|
|
266
|
+
: null;
|
|
267
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { tests } from ".";
|
|
2
|
+
import { expect, ExpectationError } from "./helper";
|
|
3
|
+
import { resetMocks } from "./mocks";
|
|
4
|
+
|
|
5
|
+
const count = {
|
|
6
|
+
total: 0,
|
|
7
|
+
passed: 0,
|
|
8
|
+
failed: <string[]>[],
|
|
9
|
+
}
|
|
10
|
+
const line = "----------------------------------";
|
|
11
|
+
|
|
12
|
+
async function runTest(test: [string, () => any]) {
|
|
13
|
+
count.total++;
|
|
14
|
+
resetMocks();
|
|
15
|
+
const start = performance.now();
|
|
16
|
+
try {
|
|
17
|
+
expect(document).toBeNotHidden();
|
|
18
|
+
const result = test[1]();
|
|
19
|
+
if (result && typeof (result as any)?.then === "function") {
|
|
20
|
+
await result;
|
|
21
|
+
}
|
|
22
|
+
count.passed++;
|
|
23
|
+
const time = (performance.now() - start).toFixed(3) + " ms";
|
|
24
|
+
console.log(`#${count.total} ${test[0]}\n-> 🟢 passed ${time}\n${line}`);
|
|
25
|
+
} catch (err: any) {
|
|
26
|
+
const time = (performance.now() - start).toFixed(3) + " ms";
|
|
27
|
+
console.error(`#${count.total} ${test[0]}\n-> 🔴 failed ${time}`);
|
|
28
|
+
if (err instanceof ExpectationError) {
|
|
29
|
+
count.failed.push(`#${count.total} ${test[0]}\n-> 🔴 failed:\n${err.message}\n${line}`);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
count.failed.push(`#${count.total} ${test[0]}\n-> 🔴 failed:\n${err.message}\n${err.stack}\n${line}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const sw = performance.now();
|
|
38
|
+
(async () => {
|
|
39
|
+
for (const test of Object.entries(tests)) {
|
|
40
|
+
await runTest(test);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const time = (performance.now() - sw).toFixed(3) + " ms";
|
|
44
|
+
|
|
45
|
+
console.log(`
|
|
46
|
+
total: ${count.total}
|
|
47
|
+
passed: ${count.passed}
|
|
48
|
+
failed: ${count.failed.length}
|
|
49
|
+
|
|
50
|
+
time: ${time}
|
|
51
|
+
`);
|
|
52
|
+
|
|
53
|
+
if (count.passed === count.total) {
|
|
54
|
+
console.log("\n\nall tests passed\n");
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
console.error(`${line.replaceAll("-", "=")}\nError summary:\n\n${count.failed.join(`\n${line}\n`)}`);
|
|
58
|
+
|
|
59
|
+
throw "\n\nsome tests failed (see output)\n";
|
|
60
|
+
}
|
|
61
|
+
})();
|