@ryupold/vode 1.8.4 → 1.8.6
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/.github/workflows/publish.yml +13 -0
- package/.github/workflows/tests.yml +17 -0
- package/README.md +55 -5
- package/dist/vode.cjs.min.js +2 -2
- package/dist/vode.d.ts +5 -0
- package/dist/vode.es5.min.js +4 -4
- package/dist/vode.js +100 -20
- package/dist/vode.min.js +1 -1
- package/dist/vode.min.mjs +1 -1
- package/dist/vode.mjs +100 -20
- package/package.json +4 -3
- package/src/merge-style.ts +4 -1
- package/src/vode.ts +110 -26
- package/test/helper.ts +168 -0
- package/test/index.ts +82 -0
- package/test/mocks.ts +111 -0
- package/test/tests-app.ts +226 -0
- package/test/tests-children.ts +69 -0
- package/test/tests-createPatch.ts +28 -0
- package/test/tests-createState.ts +43 -0
- package/test/tests-defuse.ts +74 -0
- package/test/tests-hydrate.ts +68 -0
- package/test/tests-memo.ts +119 -0
- package/test/tests-mergeClass.ts +63 -0
- package/test/tests-mergeProps.ts +43 -0
- package/test/tests-mergeStyle.ts +39 -0
- package/test/tests-mount-unmount.ts +1140 -0
- package/test/tests-props.ts +34 -0
- package/test/tests-state-context.ts +106 -0
- package/test/tests-tag.ts +33 -0
- package/test/tests-vode.ts +27 -0
- package/tsconfig.test.json +18 -0
package/test/helper.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { children, ChildVode, PatchableState, tag, Vode } from "../src/vode";
|
|
2
|
+
import { MockElement, MockText } from "./mocks";
|
|
3
|
+
|
|
4
|
+
export class Expectation {
|
|
5
|
+
constructor(public readonly what: any) { }
|
|
6
|
+
|
|
7
|
+
toBeA(type: "undefined" | "object" | "function" | "bigint" | "boolean" | "number" | "string" | "symbol") {
|
|
8
|
+
if (typeof this.what !== type) {
|
|
9
|
+
throw new ExpectationError(this, `expected \n\ntypeof ${this.what}\n\nto be \n\n${type}`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
toEqual(other: any) {
|
|
14
|
+
function deepCompare(a: any, b: any, path: string[]): string[] | null {
|
|
15
|
+
if (typeof a !== typeof b) {
|
|
16
|
+
if (path.length === 0) path.push(``);
|
|
17
|
+
path[path.length - 1] += ` (type: ${typeof a} != ${typeof b})`;
|
|
18
|
+
return path;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (typeof a !== "object" || a === null) {
|
|
22
|
+
if (path.length === 0) path.push(``);
|
|
23
|
+
path[path.length - 1] += ` (value: ${a} != ${b})`;
|
|
24
|
+
return a !== b ? path : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const prop of Object.entries(a)) {
|
|
28
|
+
const [k, v] = prop;
|
|
29
|
+
const result = deepCompare(v, b[k], [...path, k]);
|
|
30
|
+
if (result) {
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const prop of Object.entries(b)) {
|
|
36
|
+
const [k, v] = prop;
|
|
37
|
+
const result = deepCompare(a[k], v, [...path, k]);
|
|
38
|
+
if (result) {
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (typeof this.what === "object" && typeof other === "object" && this.what !== null && other !== null) {
|
|
47
|
+
const unequal = deepCompare(this.what, other, []);
|
|
48
|
+
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(".")}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
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}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
toSucceed<Result>(...args: any): Result {
|
|
60
|
+
if (typeof this.what !== "function") {
|
|
61
|
+
throw new ExpectationError(this, `expected a function\n\nbut it is a ${typeof this.what}`);
|
|
62
|
+
}
|
|
63
|
+
return this.what(...args);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
toFail(...args: any): Error {
|
|
67
|
+
if (typeof this.what !== "function") {
|
|
68
|
+
throw new ExpectationError(this, `expected a function\n\nbut it is a ${typeof this.what}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let r: any;
|
|
72
|
+
try {
|
|
73
|
+
r = this.what(...args);
|
|
74
|
+
} catch (err: any) {
|
|
75
|
+
return err;
|
|
76
|
+
}
|
|
77
|
+
throw new ExpectationError(this, `expected function to fail\n\nbut it succeeded with a result of type ${typeof r}\n\n${r}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
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
|
+
const that = this;
|
|
83
|
+
function deepCompare(e: MockElement | MockText | ChildVode, cv: ChildVode, path: string[]): string[] | null {
|
|
84
|
+
|
|
85
|
+
// unwrap component
|
|
86
|
+
while (typeof cv === "function") {
|
|
87
|
+
if (!state) {
|
|
88
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na Component\n\nbut got no state passed in [toMatch]`);
|
|
89
|
+
}
|
|
90
|
+
cv = cv(state);
|
|
91
|
+
}
|
|
92
|
+
while (typeof e === "function") {
|
|
93
|
+
if (!state) {
|
|
94
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na Component\n\nbut got no state passed in [toMatch]`);
|
|
95
|
+
}
|
|
96
|
+
e = e(state);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (typeof cv === "string" && e instanceof MockText) {
|
|
100
|
+
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}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else if (typeof cv === "string" && typeof e === "string") {
|
|
105
|
+
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}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
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}>`);
|
|
113
|
+
}
|
|
114
|
+
const kids = children(cv) || [];
|
|
115
|
+
for (let i = 0; i < kids.length; i++) {
|
|
116
|
+
deepCompare(e.children[i], kids[i], [...path, `${tag(kids[i] as Vode) || "#text"}`]);
|
|
117
|
+
}
|
|
118
|
+
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`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
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)}]`);
|
|
125
|
+
}
|
|
126
|
+
const kids = children(cv) || [];
|
|
127
|
+
const otherKids = children(e) || [];
|
|
128
|
+
for (let i = 0; i < kids.length; i++) {
|
|
129
|
+
deepCompare(otherKids[i], kids[i], [...path, `${tag(kids[i] as Vode) || "#text"}`]);
|
|
130
|
+
}
|
|
131
|
+
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`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
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}>`);
|
|
138
|
+
}
|
|
139
|
+
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)}]`);
|
|
141
|
+
}
|
|
142
|
+
|
|
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})`);
|
|
145
|
+
}
|
|
146
|
+
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})`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
deepCompare(this.what, v, [tag(v as Vode) || "#text"]);
|
|
154
|
+
} else {
|
|
155
|
+
throw new ExpectationError(this, `expected an element or text node\n\nbut it is a ${typeof this.what}\n${this.what}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export class ExpectationError extends Error {
|
|
161
|
+
constructor(public readonly expectation: Expectation, message?: string) {
|
|
162
|
+
super(message)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function expect(what: any) {
|
|
167
|
+
return new Expectation(what);
|
|
168
|
+
}
|
package/test/index.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { resetMocks } from "./mocks";
|
|
2
|
+
import { ExpectationError } from "./helper";
|
|
3
|
+
|
|
4
|
+
//=== REGISTERED TESTS =========================================
|
|
5
|
+
import vodeTests from "./tests-vode";
|
|
6
|
+
import appTests from "./tests-app";
|
|
7
|
+
import defuseTests from "./tests-defuse";
|
|
8
|
+
import hydrateTests from "./tests-hydrate";
|
|
9
|
+
import memoTests from "./tests-memo";
|
|
10
|
+
import createStateTests from "./tests-createState";
|
|
11
|
+
import createPatchTests from "./tests-createPatch";
|
|
12
|
+
import tagTests from "./tests-tag";
|
|
13
|
+
import childrenTests from "./tests-children";
|
|
14
|
+
import propsTests from "./tests-props";
|
|
15
|
+
import mergeClassTests from "./tests-mergeClass";
|
|
16
|
+
import mergeStyleTests from "./tests-mergeStyle";
|
|
17
|
+
import mergePropsTests from "./tests-mergeProps";
|
|
18
|
+
import stateContextTests from "./tests-state-context";
|
|
19
|
+
import mountUnmountTests from "./tests-mount-unmount";
|
|
20
|
+
|
|
21
|
+
const tests = {
|
|
22
|
+
...vodeTests,
|
|
23
|
+
...appTests,
|
|
24
|
+
...defuseTests,
|
|
25
|
+
...hydrateTests,
|
|
26
|
+
...memoTests,
|
|
27
|
+
...mountUnmountTests,
|
|
28
|
+
|
|
29
|
+
...createStateTests,
|
|
30
|
+
...createPatchTests,
|
|
31
|
+
|
|
32
|
+
...tagTests,
|
|
33
|
+
...propsTests,
|
|
34
|
+
...childrenTests,
|
|
35
|
+
|
|
36
|
+
...mergeClassTests,
|
|
37
|
+
...mergeStyleTests,
|
|
38
|
+
...mergePropsTests,
|
|
39
|
+
|
|
40
|
+
...stateContextTests,
|
|
41
|
+
};
|
|
42
|
+
//===================================================
|
|
43
|
+
|
|
44
|
+
const count = {
|
|
45
|
+
total: 0,
|
|
46
|
+
passed: 0,
|
|
47
|
+
failed: <string[]>[],
|
|
48
|
+
}
|
|
49
|
+
const line = "----------------------------------";
|
|
50
|
+
|
|
51
|
+
for (const test of Object.entries(tests)) {
|
|
52
|
+
count.total++;
|
|
53
|
+
resetMocks();
|
|
54
|
+
try {
|
|
55
|
+
test[1]()
|
|
56
|
+
count.passed++;
|
|
57
|
+
console.log(`#${count.total} ${test[0]}\n-> 🟢 passed\n${line}`);
|
|
58
|
+
} catch (err: any) {
|
|
59
|
+
console.error(`#${count.total} ${test[0]}\n-> 🔴 failed`);
|
|
60
|
+
if (err instanceof ExpectationError) {
|
|
61
|
+
count.failed.push(`#${count.total} ${test[0]}\n-> 🔴 failed:\n${err.message}\n${line}`);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
count.failed.push(`#${count.total} ${test[0]}\n-> 🔴 failed:\n${err.message}\n${err.stack}\n${line}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log(`
|
|
70
|
+
total: ${count.total}
|
|
71
|
+
passed: ${count.passed}
|
|
72
|
+
failed: ${count.failed.length}
|
|
73
|
+
`);
|
|
74
|
+
|
|
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`)}`);
|
|
80
|
+
|
|
81
|
+
throw "\n\nsome tests failed (see output)\n";
|
|
82
|
+
}
|
package/test/mocks.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const NodeConstants = {
|
|
2
|
+
ELEMENT_NODE: 1,
|
|
3
|
+
ATTRIBUTE_NODE: 2,
|
|
4
|
+
TEXT_NODE: 3,
|
|
5
|
+
CDATA_SECTION_NODE: 4,
|
|
6
|
+
ENTITY_REFERENCE_NODE: 5,
|
|
7
|
+
ENTITY_NODE: 6,
|
|
8
|
+
PROCESSING_INSTRUCTION_NODE: 7,
|
|
9
|
+
COMMENT_NODE: 8,
|
|
10
|
+
DOCUMENT_NODE: 9,
|
|
11
|
+
DOCUMENT_TYPE_NODE: 10,
|
|
12
|
+
DOCUMENT_FRAGMENT_NODE: 11,
|
|
13
|
+
NOTATION_NODE: 12,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export class MockElement {
|
|
17
|
+
nodeType = NodeConstants.ELEMENT_NODE;
|
|
18
|
+
childNodes: (MockElement | MockText)[] = [];
|
|
19
|
+
children: (MockElement | MockText)[] = [];
|
|
20
|
+
parentElement: MockElement | null = null;
|
|
21
|
+
get attributes() {
|
|
22
|
+
return Object.entries(this._attrs).map(([name, value]) => ({ name, value }));
|
|
23
|
+
}
|
|
24
|
+
style: { cssText: string } = { cssText: "" };
|
|
25
|
+
tagName = "UNKNOWN";
|
|
26
|
+
private _attrs: Record<string, string> = {};
|
|
27
|
+
|
|
28
|
+
constructor(public tag?: string) {
|
|
29
|
+
if (tag) this.tagName = tag.toUpperCase();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get firstChild() { return this.childNodes[0] ?? null; }
|
|
33
|
+
get lastChild() { return this.childNodes[this.childNodes.length - 1] ?? null; }
|
|
34
|
+
get nextSibling() { return null; }
|
|
35
|
+
|
|
36
|
+
hasAttributes() { return Object.keys(this._attrs).length > 0; }
|
|
37
|
+
hasChildNodes() { return this.childNodes.length > 0; }
|
|
38
|
+
setAttribute(name: string, value: string) { this._attrs[name] = value; }
|
|
39
|
+
removeAttribute(name: string) { delete this._attrs[name]; }
|
|
40
|
+
appendChild(child: MockElement | MockText) {
|
|
41
|
+
this.childNodes.push(child); this.children.push(child); if (child.parentElement !== undefined) child.parentElement = this; return child;
|
|
42
|
+
}
|
|
43
|
+
remove() {
|
|
44
|
+
if (this.parentElement) { const i = this.parentElement.childNodes.indexOf(this); if (i >= 0) this.parentElement.childNodes.splice(i, 1); }
|
|
45
|
+
}
|
|
46
|
+
replaceWith(...nodes: (MockElement | MockText)[]) {
|
|
47
|
+
const parent = this.parentElement;
|
|
48
|
+
if (parent) {
|
|
49
|
+
const i = parent.childNodes.indexOf(this);
|
|
50
|
+
if (i >= 0) { parent.childNodes.splice(i, 1, ...nodes); }
|
|
51
|
+
for (const n of nodes) n.parentElement = parent;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
before(...nodes: (MockElement | MockText)[]) {
|
|
55
|
+
const parent = this.parentElement;
|
|
56
|
+
if (parent) {
|
|
57
|
+
const i = parent.childNodes.indexOf(this);
|
|
58
|
+
if (i >= 0) { parent.childNodes.splice(i, 0, ...nodes); }
|
|
59
|
+
for (const n of nodes) n.parentElement = parent;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
get [Symbol.iterator]() { return Array.prototype[Symbol.iterator].bind(this.children); }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class MockText {
|
|
66
|
+
nodeType = NodeConstants.TEXT_NODE;
|
|
67
|
+
parentElement: MockElement | null = null;
|
|
68
|
+
constructor(public nodeValue: string) { }
|
|
69
|
+
get wholeText() { return this.nodeValue; }
|
|
70
|
+
remove() { if (this.parentElement) { const i = this.parentElement.childNodes.indexOf(this); if (i >= 0) this.parentElement.childNodes.splice(i, 1); } }
|
|
71
|
+
replaceWith(...nodes: (MockElement | MockText)[]) {
|
|
72
|
+
const parent = this.parentElement;
|
|
73
|
+
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) n.parentElement = parent;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
before(...nodes: (MockElement | MockText)[]) {
|
|
80
|
+
const parent = this.parentElement;
|
|
81
|
+
if (parent) {
|
|
82
|
+
const i = parent.childNodes.indexOf(this);
|
|
83
|
+
if (i >= 0) { parent.childNodes.splice(i, 0, ...nodes); }
|
|
84
|
+
for (const n of nodes) n.parentElement = parent;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function resetMocks() {
|
|
90
|
+
const mockDoc: any = {
|
|
91
|
+
createElement: (tag: string) => new MockElement(tag),
|
|
92
|
+
createTextNode: (text: string) => new MockText(text),
|
|
93
|
+
createElementNS: (ns: string, tag: string) => new MockElement(tag),
|
|
94
|
+
hidden: false,
|
|
95
|
+
};
|
|
96
|
+
const mockWin: any = {
|
|
97
|
+
requestAnimationFrame: (cb: any) => cb(Date.now()),
|
|
98
|
+
startViewTransition: (callbackOptions: any) => {
|
|
99
|
+
return {
|
|
100
|
+
finished: Promise.resolve(),
|
|
101
|
+
ready: Promise.resolve(),
|
|
102
|
+
updateCallbackDone: Promise.resolve(),
|
|
103
|
+
skipTransition() { },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
globalThis.document ??= mockDoc as Document;
|
|
109
|
+
globalThis.window ??= mockWin as (Window & typeof globalThis);
|
|
110
|
+
globalThis.Node ??= NodeConstants as any;
|
|
111
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { expect } from "./helper";
|
|
2
|
+
import { app, ARTICLE, DIV, P, SPAN } from "../index";
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
"app(): successful initialization": () => {
|
|
6
|
+
const root = document.createElement("div");
|
|
7
|
+
const container = document.createElement("div");
|
|
8
|
+
root.appendChild(container);
|
|
9
|
+
|
|
10
|
+
const patch = expect(
|
|
11
|
+
() => app(container, {}, () =>
|
|
12
|
+
[DIV,
|
|
13
|
+
[ARTICLE,
|
|
14
|
+
[P, "foo", [SPAN, "bar"]]
|
|
15
|
+
]
|
|
16
|
+
]
|
|
17
|
+
)
|
|
18
|
+
).toSucceed();
|
|
19
|
+
|
|
20
|
+
expect(patch).toBeA("function");
|
|
21
|
+
|
|
22
|
+
expect(container).toMatch(
|
|
23
|
+
[DIV,
|
|
24
|
+
[ARTICLE,
|
|
25
|
+
[P, "foo", [SPAN, "bar"]]
|
|
26
|
+
]
|
|
27
|
+
]
|
|
28
|
+
);
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
//=== FAILURE CASES ===
|
|
32
|
+
|
|
33
|
+
"app(): fails when the container has no parent": () => {
|
|
34
|
+
const container = document.createElement("div");
|
|
35
|
+
const err = expect(() => app(container, {}, () => [DIV]))
|
|
36
|
+
.toFail();
|
|
37
|
+
|
|
38
|
+
expect(err.message).toEqual("first argument to app() must be a valid HTMLElement inside the <html></html> document");
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
"app(): fails when the state is not an object": () => {
|
|
42
|
+
const root = document.createElement("div");
|
|
43
|
+
const container = document.createElement("div");
|
|
44
|
+
root.appendChild(container);
|
|
45
|
+
|
|
46
|
+
const err = expect(() => app(container, "oops", () => [DIV]))
|
|
47
|
+
.toFail();
|
|
48
|
+
|
|
49
|
+
expect(err.message).toEqual("second argument to app() must be a state object");
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
"app(): fails when the dom factory is not a function": () => {
|
|
53
|
+
const root = document.createElement("div");
|
|
54
|
+
const container = document.createElement("div");
|
|
55
|
+
root.appendChild(container);
|
|
56
|
+
|
|
57
|
+
const err = expect(() => app(container, {}, [DIV] as any))
|
|
58
|
+
.toFail();
|
|
59
|
+
|
|
60
|
+
expect(err.message).toEqual("third argument to app() must be a function that returns a vode");
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
//=== INITIAL PATCHES ===
|
|
64
|
+
|
|
65
|
+
"app(): executes initial patches after first render": () => {
|
|
66
|
+
const root = document.createElement("div");
|
|
67
|
+
const container = document.createElement("div");
|
|
68
|
+
root.appendChild(container);
|
|
69
|
+
|
|
70
|
+
const state = { count: 6, start: 1 };
|
|
71
|
+
|
|
72
|
+
app(container, state, () => [DIV],
|
|
73
|
+
{ count: 7 },
|
|
74
|
+
() => ({ start: 2 })
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(state).toEqual({ count: 7, start: 2 });
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
//=== STATE PATCHING ===
|
|
81
|
+
|
|
82
|
+
"app(): patch with object updates state and re-renders DOM": () => {
|
|
83
|
+
const root = document.createElement("div");
|
|
84
|
+
const container = document.createElement("div");
|
|
85
|
+
root.appendChild(container);
|
|
86
|
+
|
|
87
|
+
const state: any = { msg: "hello" };
|
|
88
|
+
app(container, state, (s: any) => [DIV, s.msg]);
|
|
89
|
+
|
|
90
|
+
expect(state.msg).toEqual("hello");
|
|
91
|
+
expect(container).toMatch([DIV, "hello"]);
|
|
92
|
+
|
|
93
|
+
state.patch({ msg: "world" });
|
|
94
|
+
|
|
95
|
+
expect(state.msg).toEqual("world");
|
|
96
|
+
expect(container).toMatch([DIV, "world"]);
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
"app(): patch with effect function executes and applies result": () => {
|
|
100
|
+
const root = document.createElement("div");
|
|
101
|
+
const container = document.createElement("div");
|
|
102
|
+
root.appendChild(container);
|
|
103
|
+
|
|
104
|
+
const state: any = { count: 0 };
|
|
105
|
+
app(container, state, (s: any) => [DIV, String(s.count)]);
|
|
106
|
+
|
|
107
|
+
state.patch(() => ({ count: 5 }));
|
|
108
|
+
|
|
109
|
+
expect(state.count).toEqual(5);
|
|
110
|
+
expect(container).toMatch([DIV, "5"]);
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
"app(): patch with array applies multiple patches in sequence": () => {
|
|
114
|
+
const root = document.createElement("div");
|
|
115
|
+
const container = document.createElement("div");
|
|
116
|
+
root.appendChild(container);
|
|
117
|
+
|
|
118
|
+
const state: any = { a: 1, b: 2 };
|
|
119
|
+
app(container, state, () => [DIV]);
|
|
120
|
+
|
|
121
|
+
state.patch([{ a: 10 }, { b: 20 }]);
|
|
122
|
+
|
|
123
|
+
expect(state.a).toEqual(10);
|
|
124
|
+
expect(state.b).toEqual(20);
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
"app(): multiple sequential patches both apply": () => {
|
|
128
|
+
const root = document.createElement("div");
|
|
129
|
+
const container = document.createElement("div");
|
|
130
|
+
root.appendChild(container);
|
|
131
|
+
|
|
132
|
+
const state: any = { x: 0, y: 0 };
|
|
133
|
+
app(container, state, () => [DIV]);
|
|
134
|
+
|
|
135
|
+
state.patch({ x: 1 });
|
|
136
|
+
state.patch({ y: 2 });
|
|
137
|
+
|
|
138
|
+
expect(state).toEqual({ x: 1, y: 2 });
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
//=== LIFECYCLE ===
|
|
142
|
+
|
|
143
|
+
"app(): onMount callback is called on newly created child elements": () => {
|
|
144
|
+
const root = document.createElement("div");
|
|
145
|
+
const container = document.createElement("div");
|
|
146
|
+
root.appendChild(container);
|
|
147
|
+
|
|
148
|
+
let mountCalled = false;
|
|
149
|
+
app(container, {}, () =>
|
|
150
|
+
[DIV,
|
|
151
|
+
[SPAN, { onMount: () => { mountCalled = true; return {}; } }, "text"]
|
|
152
|
+
] as any
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
expect(mountCalled).toEqual(true);
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
//=== COMPONENTS ===
|
|
159
|
+
|
|
160
|
+
"app(): component function as child renders correctly": () => {
|
|
161
|
+
const root = document.createElement("div");
|
|
162
|
+
const container = document.createElement("div");
|
|
163
|
+
root.appendChild(container);
|
|
164
|
+
|
|
165
|
+
app(container, {}, () =>
|
|
166
|
+
[DIV,
|
|
167
|
+
((s: any) => [SPAN, "component rendered"]) as any
|
|
168
|
+
]
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
expect(container).toMatch(
|
|
172
|
+
[DIV, [SPAN, "component rendered"]]
|
|
173
|
+
);
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
"app(): component accesses state and renders dynamic content": () => {
|
|
177
|
+
const root = document.createElement("div");
|
|
178
|
+
const container = document.createElement("div");
|
|
179
|
+
root.appendChild(container);
|
|
180
|
+
|
|
181
|
+
const state: any = { label: "dynamic" };
|
|
182
|
+
app(container, state, (s) =>
|
|
183
|
+
[DIV,
|
|
184
|
+
((st: any) => [SPAN, st.label]) as any
|
|
185
|
+
]
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
expect(container).toMatch([DIV, [SPAN, "dynamic"]]);
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
//=== DEEP STATE ===
|
|
192
|
+
|
|
193
|
+
"app(): deep nested state merges correctly via patch": () => {
|
|
194
|
+
const root = document.createElement("div");
|
|
195
|
+
const container = document.createElement("div");
|
|
196
|
+
root.appendChild(container);
|
|
197
|
+
|
|
198
|
+
const state: any = { nested: { value: 1, other: "keep" } };
|
|
199
|
+
app(container, state, (s: any) => [DIV, String(s.nested.value)]);
|
|
200
|
+
|
|
201
|
+
state.patch({ nested: { value: 2 } });
|
|
202
|
+
|
|
203
|
+
expect(state.nested.value).toEqual(2);
|
|
204
|
+
expect(state.nested.other).toEqual("keep");
|
|
205
|
+
expect(container).toMatch([DIV, "2"]);
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
//=== IGNORED PATCHES ===
|
|
209
|
+
|
|
210
|
+
"app(): patching with ignored types is a no-op": () => {
|
|
211
|
+
const root = document.createElement("div");
|
|
212
|
+
const container = document.createElement("div");
|
|
213
|
+
root.appendChild(container);
|
|
214
|
+
|
|
215
|
+
const state: any = { x: 1 };
|
|
216
|
+
app(container, state, () => [DIV]);
|
|
217
|
+
|
|
218
|
+
state.patch(null);
|
|
219
|
+
state.patch(undefined);
|
|
220
|
+
state.patch(42);
|
|
221
|
+
state.patch("ignored");
|
|
222
|
+
state.patch(true);
|
|
223
|
+
|
|
224
|
+
expect(state.x).toEqual(1);
|
|
225
|
+
},
|
|
226
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { children, child, childCount, childrenStart, DIV, SPAN, P, Vode } from "../index";
|
|
2
|
+
import { expect } from "./helper";
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
"children(): tag+props+children returns children array": () => {
|
|
6
|
+
const v: Vode = [DIV, { class: "x" }, [SPAN, "a"], [P, "b"]];
|
|
7
|
+
const c = children(v);
|
|
8
|
+
expect(Array.isArray(c)).toEqual(true);
|
|
9
|
+
expect(c!.length).toEqual(2);
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
"children(): tag+children (no props) returns children array": () => {
|
|
13
|
+
const v: Vode = [DIV, [SPAN, "a"], [P, "b"]];
|
|
14
|
+
const c = children(v);
|
|
15
|
+
expect(Array.isArray(c)).toEqual(true);
|
|
16
|
+
expect(c!.length).toEqual(2);
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
"children(): just-tag vode returns null": () => {
|
|
20
|
+
expect(children([DIV])).toEqual(null);
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
"children(): text vode returns null": () => {
|
|
24
|
+
expect(children("hello")).toEqual(null);
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
"childrenStart(): with props+children returns 2": () => {
|
|
28
|
+
expect(childrenStart([DIV, { class: "x" }, [SPAN]])).toEqual(2);
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
"childrenStart(): without props but with children returns 1": () => {
|
|
32
|
+
expect(childrenStart([DIV, [SPAN]])).toEqual(1);
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
"childrenStart(): just-tag returns -1": () => {
|
|
36
|
+
expect(childrenStart([DIV])).toEqual(-1);
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
"childrenStart(): text vode returns -1": () => {
|
|
40
|
+
expect(childrenStart("hello")).toEqual(-1);
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
"childCount(): matches actual child count": () => {
|
|
44
|
+
expect(childCount([DIV, { class: "x" }, [SPAN, "a"], [P, "b"]])).toEqual(2);
|
|
45
|
+
expect(childCount([DIV, [SPAN]])).toEqual(1);
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
"childCount(): returns 0 for no-children vode": () => {
|
|
49
|
+
expect(childCount([DIV])).toEqual(0);
|
|
50
|
+
expect(childCount("hello" as any)).toEqual(0);
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
"child(): returns correct child at index": () => {
|
|
54
|
+
const v: Vode = [DIV, { class: "x" }, [SPAN, "a"], [P, "b"]];
|
|
55
|
+
|
|
56
|
+
expect(child(v, 0)).toEqual([SPAN, "a"]);
|
|
57
|
+
expect(child(v, 1)).toEqual([P, "b"]);
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
"child(): returns undefined for out-of-bounds": () => {
|
|
61
|
+
expect(child([DIV, { class: "x" }, [SPAN]], 5))
|
|
62
|
+
.toEqual(undefined);
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
"child(): returns undefined for text vode": () => {
|
|
66
|
+
expect(child("hello" as any, 0))
|
|
67
|
+
.toEqual(undefined);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createPatch } from "../src/vode";
|
|
2
|
+
import { expect } from "./helper";
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
"createPatch(): just returns the input": () => {
|
|
6
|
+
const p = { a: 123 };
|
|
7
|
+
expect(createPatch(p) === p).toEqual(true);
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
"createPatch(): returns undefined as-is": () => {
|
|
11
|
+
expect(createPatch(undefined)).toEqual(undefined);
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
"createPatch(): returns null as-is": () => {
|
|
15
|
+
expect(createPatch(null)).toEqual(null);
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
"createPatch(): returns function as-is": () => {
|
|
19
|
+
const fn = () => ({});
|
|
20
|
+
expect(createPatch(fn) === fn).toEqual(true);
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
"createPatch(): returns primitive as-is": () => {
|
|
24
|
+
expect(createPatch(42)).toEqual(42);
|
|
25
|
+
expect(createPatch("ignored")).toEqual("ignored");
|
|
26
|
+
expect(createPatch(false)).toEqual(false);
|
|
27
|
+
}
|
|
28
|
+
};
|