@ryupold/vode 1.8.6 → 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 -10
- package/dist/vode.es5.min.js +7 -7
- package/dist/vode.js +84 -138
- package/dist/vode.min.js +1 -1
- package/dist/vode.min.mjs +1 -1
- package/dist/vode.mjs +84 -138
- package/package.json +1 -1
- package/src/state-context.ts +6 -4
- package/src/vode.ts +105 -156
- 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-mount-unmount.ts +265 -1
- 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/test/tests-hydrate.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { expect } from "./helper";
|
|
2
2
|
import { hydrate, DIV, SPAN, P } from "../index";
|
|
3
|
-
import {
|
|
3
|
+
import { FakeElement, FakeTextNode } from "./mocks";
|
|
4
4
|
|
|
5
5
|
export default {
|
|
6
6
|
"hydrate(): text node returns its text content": () => {
|
|
7
|
-
const text = new
|
|
7
|
+
const text = new FakeTextNode("hello world");
|
|
8
8
|
|
|
9
9
|
expect(hydrate(text as any))
|
|
10
10
|
.toMatch("hello world");
|
|
11
11
|
},
|
|
12
12
|
|
|
13
13
|
"hydrate(): empty element returns a vode": () => {
|
|
14
|
-
const el = new
|
|
14
|
+
const el = new FakeElement("div");
|
|
15
15
|
const result = hydrate(el as any);
|
|
16
16
|
|
|
17
17
|
expect(result)
|
|
@@ -19,8 +19,8 @@ export default {
|
|
|
19
19
|
},
|
|
20
20
|
|
|
21
21
|
"hydrate(): element with children returns full vode tree": () => {
|
|
22
|
-
const parent = new
|
|
23
|
-
const child = new
|
|
22
|
+
const parent = new FakeElement("div");
|
|
23
|
+
const child = new FakeElement("span");
|
|
24
24
|
parent.appendChild(child);
|
|
25
25
|
|
|
26
26
|
expect(hydrate(parent as any))
|
|
@@ -28,8 +28,8 @@ export default {
|
|
|
28
28
|
},
|
|
29
29
|
|
|
30
30
|
"hydrate(): element with text child": () => {
|
|
31
|
-
const parent = new
|
|
32
|
-
const text = new
|
|
31
|
+
const parent = new FakeElement("p");
|
|
32
|
+
const text = new FakeTextNode("hello");
|
|
33
33
|
parent.appendChild(text);
|
|
34
34
|
|
|
35
35
|
expect(hydrate(parent as any))
|
|
@@ -37,7 +37,7 @@ export default {
|
|
|
37
37
|
},
|
|
38
38
|
|
|
39
39
|
"hydrate(): element with attributes reads them into props": () => {
|
|
40
|
-
const el = new
|
|
40
|
+
const el = new FakeElement("div");
|
|
41
41
|
el.setAttribute("class", "foo");
|
|
42
42
|
el.setAttribute("id", "bar");
|
|
43
43
|
|
|
@@ -53,7 +53,7 @@ export default {
|
|
|
53
53
|
},
|
|
54
54
|
|
|
55
55
|
"hydrate(): empty text node returns undefined": () => {
|
|
56
|
-
const text = new
|
|
56
|
+
const text = new FakeTextNode(" ");
|
|
57
57
|
|
|
58
58
|
expect(hydrate(text as any))
|
|
59
59
|
.toEqual(undefined);
|
|
@@ -65,4 +65,38 @@ export default {
|
|
|
65
65
|
expect(hydrate(comment))
|
|
66
66
|
.toEqual(undefined);
|
|
67
67
|
},
|
|
68
|
+
|
|
69
|
+
"hydrate(): prepareForRender returns text node for text input": () => {
|
|
70
|
+
const text = new FakeTextNode("hello");
|
|
71
|
+
|
|
72
|
+
const result = hydrate(text as any, true);
|
|
73
|
+
|
|
74
|
+
expect(result instanceof FakeTextNode).toEqual(true);
|
|
75
|
+
expect((result as any).nodeValue).toEqual("hello");
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
"hydrate(): prepareForRender attaches .node to element vode": () => {
|
|
79
|
+
const el = new FakeElement("div");
|
|
80
|
+
|
|
81
|
+
const result = hydrate(el as any, true) as any;
|
|
82
|
+
|
|
83
|
+
expect(Array.isArray(result)).toEqual(true);
|
|
84
|
+
expect(result[0]).toEqual("div");
|
|
85
|
+
expect(result.node instanceof FakeElement).toEqual(true);
|
|
86
|
+
expect(result.node.tagName).toEqual("DIV");
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
"hydrate(): prepareForRender removes whitespace text nodes": () => {
|
|
90
|
+
const el = new FakeElement("div");
|
|
91
|
+
el.appendChild(new FakeTextNode(" "));
|
|
92
|
+
el.appendChild(new FakeElement("span"));
|
|
93
|
+
el.appendChild(new FakeTextNode(" "));
|
|
94
|
+
|
|
95
|
+
expect(el.childNodes.length).toEqual(3);
|
|
96
|
+
|
|
97
|
+
const result = hydrate(el as any, true);
|
|
98
|
+
|
|
99
|
+
expect(el.childNodes.length).toEqual(1);
|
|
100
|
+
expect((el.childNodes[0] as any).tagName).toEqual("SPAN");
|
|
101
|
+
},
|
|
68
102
|
};
|
package/test/tests-memo.ts
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
import { expect } from "./helper";
|
|
2
|
-
import { memo, DIV, app, createState, SPAN } from "../index";
|
|
2
|
+
import { memo, DIV, app, createState, SPAN, H1, BR, P, UL, LI, Component } from "../index";
|
|
3
3
|
|
|
4
4
|
export default {
|
|
5
|
-
"memo(): returns the given function": () => {
|
|
6
|
-
const fn = (s: any) => [DIV];
|
|
7
|
-
const result = memo([1, 2], fn);
|
|
8
|
-
expect(result === fn).toEqual(true);
|
|
9
|
-
},
|
|
10
|
-
|
|
11
5
|
"memo(): throws when compare is not an array": () => {
|
|
12
6
|
const err = expect(() => memo(null as any, (s: any) => [DIV]))
|
|
13
7
|
.toFail();
|
|
@@ -19,7 +13,7 @@ export default {
|
|
|
19
13
|
const err = expect(() => memo([1], null as any))
|
|
20
14
|
.toFail();
|
|
21
15
|
expect(err.message)
|
|
22
|
-
.toEqual("second argument to memo() must be a function that returns a vode
|
|
16
|
+
.toEqual("second argument to memo() must be a function that returns a child vode");
|
|
23
17
|
},
|
|
24
18
|
|
|
25
19
|
"memo(): integration with app prevents re-render when deps match": () => {
|
|
@@ -53,67 +47,114 @@ export default {
|
|
|
53
47
|
);
|
|
54
48
|
},
|
|
55
49
|
|
|
56
|
-
"memo():
|
|
57
|
-
const state = createState({ count: 12
|
|
50
|
+
"memo(): can be used with a nested component function": () => {
|
|
51
|
+
const state = createState({ count: 12 });
|
|
58
52
|
const root = document.createElement("div");
|
|
59
53
|
const container = document.createElement("div");
|
|
60
54
|
root.appendChild(container);
|
|
61
55
|
|
|
62
56
|
let callCount = 0;
|
|
63
57
|
app<typeof state>(container, state, (s) => [DIV,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
low: s.count < 10,
|
|
72
|
-
high: s.count >= 10,
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
),
|
|
77
|
-
[SPAN, `${s.prefix}${s.count}`]
|
|
78
|
-
],
|
|
79
|
-
]);
|
|
58
|
+
() => memo(
|
|
59
|
+
[s.count],
|
|
60
|
+
(s) => {
|
|
61
|
+
callCount++;
|
|
62
|
+
return [DIV, [SPAN, `${s.count}`]];
|
|
63
|
+
}
|
|
64
|
+
)]);
|
|
80
65
|
|
|
81
66
|
|
|
82
67
|
expect(callCount).toEqual(1);
|
|
83
|
-
state.patch({ count: 12 });
|
|
84
|
-
expect(callCount).toEqual(1);
|
|
85
|
-
state.patch({ count: 13 });
|
|
86
|
-
expect(callCount).toEqual(2);
|
|
87
|
-
state.patch({ prefix: "count: " });
|
|
88
|
-
expect(callCount).toEqual(3);
|
|
89
|
-
expect(container).toMatch(
|
|
90
|
-
[DIV,
|
|
91
|
-
[DIV, { class: { low: false, high: true } },
|
|
92
|
-
[SPAN, "count: 13"]
|
|
93
|
-
]
|
|
94
|
-
]
|
|
95
|
-
);
|
|
68
|
+
state.patch({ count: 12 }); //same value, should not re-render
|
|
69
|
+
expect(callCount).toEqual(1);
|
|
96
70
|
},
|
|
97
71
|
|
|
98
|
-
"memo(): can be
|
|
99
|
-
const state = createState({
|
|
72
|
+
"memo(): can be used with the same component function": () => {
|
|
73
|
+
const state = createState({ test: "foo" });
|
|
100
74
|
const root = document.createElement("div");
|
|
101
75
|
const container = document.createElement("div");
|
|
102
76
|
root.appendChild(container);
|
|
103
77
|
|
|
104
78
|
let callCount = 0;
|
|
79
|
+
const Comp: Component<typeof state> = (s) => {
|
|
80
|
+
callCount++;
|
|
81
|
+
return [DIV, [SPAN, s.test]];
|
|
82
|
+
};
|
|
105
83
|
app<typeof state>(container, state, (s) => [DIV,
|
|
106
|
-
|
|
107
|
-
[s.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
84
|
+
memo(
|
|
85
|
+
[s.test],
|
|
86
|
+
Comp,
|
|
87
|
+
),
|
|
88
|
+
memo(
|
|
89
|
+
[s.test],
|
|
90
|
+
Comp,
|
|
91
|
+
),
|
|
92
|
+
]);
|
|
113
93
|
|
|
114
94
|
|
|
95
|
+
expect(callCount).toEqual(2);
|
|
96
|
+
state.patch({ test: "foo" });
|
|
97
|
+
expect(callCount).toEqual(2);
|
|
98
|
+
state.patch({ test: "bar" });
|
|
99
|
+
expect(callCount).toEqual(4);
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
"memo(): memo with many item list": () => {
|
|
103
|
+
const root = document.createElement("div");
|
|
104
|
+
const container = document.createElement("div");
|
|
105
|
+
root.appendChild(container);
|
|
106
|
+
|
|
107
|
+
const state = createState({ title: "hello", body: "world" });
|
|
108
|
+
type State = typeof state;
|
|
109
|
+
|
|
110
|
+
const CompMemoList: Component<State> = (s) =>
|
|
111
|
+
[DIV, { class: "container" },
|
|
112
|
+
[H1, "Hello World"],
|
|
113
|
+
[BR],
|
|
114
|
+
[P, "This is a paragraph."],
|
|
115
|
+
memo(
|
|
116
|
+
[s.title, s.body],
|
|
117
|
+
(s) => {
|
|
118
|
+
const list = [UL];
|
|
119
|
+
for (let i = 0; i < 10000; i++) {
|
|
120
|
+
list.push(LI, `Item ${i}`);
|
|
121
|
+
}
|
|
122
|
+
return list;
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
app<State>(container, state, (s) => [DIV,
|
|
128
|
+
CompMemoList,
|
|
129
|
+
]);
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
"memo(): double-wrapping ignores the inner memo dependencies, only the outer memo is checked": () => {
|
|
133
|
+
const state = createState({ outer: 1, inner: 1 });
|
|
134
|
+
const root = document.createElement("div");
|
|
135
|
+
const container = document.createElement("div");
|
|
136
|
+
root.appendChild(container);
|
|
137
|
+
|
|
138
|
+
let callCount = 0;
|
|
139
|
+
const comp = (s: typeof state) => {
|
|
140
|
+
callCount++;
|
|
141
|
+
return [DIV, `${s.outer}`];
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const memoed = (s: typeof state) => memo([s.inner], comp);
|
|
145
|
+
const doubleMemoed = (s: typeof state) => memo([s.outer], memoed);
|
|
146
|
+
|
|
147
|
+
expect(() => app(container, state, () => [DIV, doubleMemoed]))
|
|
148
|
+
.toSucceed();
|
|
149
|
+
|
|
115
150
|
expect(callCount).toEqual(1);
|
|
116
|
-
|
|
117
|
-
|
|
151
|
+
expect(container).toMatch([DIV, [DIV, "1"]]);
|
|
152
|
+
|
|
153
|
+
state.patch({ outer: 2 });
|
|
154
|
+
expect(callCount).toEqual(2);
|
|
155
|
+
state.patch({ inner: 2 });
|
|
156
|
+
expect(callCount).toEqual(2);
|
|
157
|
+
state.patch({ outer: 3 });
|
|
158
|
+
expect(callCount).toEqual(3);
|
|
118
159
|
},
|
|
119
160
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { app, createState } from "../src/vode"
|
|
1
|
+
import { app, createState, memo } from "../src/vode"
|
|
2
2
|
import { ARTICLE, ASIDE, DIV, INPUT, MAIN, NAV, P, SECTION, SPAN } from "../src/vode-tags";
|
|
3
3
|
import { expect } from "./helper";
|
|
4
4
|
|
|
@@ -1081,6 +1081,82 @@ export default {
|
|
|
1081
1081
|
expect(unmounts).toEqual(["unmount p-inner"]);
|
|
1082
1082
|
},
|
|
1083
1083
|
|
|
1084
|
+
"onUnmount(): memo hit + earlier sibling growth corrupts unmount indices": () => {
|
|
1085
|
+
const container = setup();
|
|
1086
|
+
const fired: string[] = [];
|
|
1087
|
+
const state = createState({ expanded: false, showB: true });
|
|
1088
|
+
const patch = app<typeof state>(container, state, (s) =>
|
|
1089
|
+
[DIV,
|
|
1090
|
+
[SPAN,
|
|
1091
|
+
{
|
|
1092
|
+
onUnmount: (s: unknown, ele: HTMLElement) => {
|
|
1093
|
+
fired.push("unmount A");
|
|
1094
|
+
}
|
|
1095
|
+
},
|
|
1096
|
+
s.expanded && [ASIDE,
|
|
1097
|
+
{
|
|
1098
|
+
onUnmount: (s: unknown, ele: HTMLElement) => {
|
|
1099
|
+
fired.push("unmount A-child");
|
|
1100
|
+
}
|
|
1101
|
+
},
|
|
1102
|
+
"x"
|
|
1103
|
+
],
|
|
1104
|
+
],
|
|
1105
|
+
s.showB && memo([], () => [SECTION,
|
|
1106
|
+
{
|
|
1107
|
+
onUnmount: (s: unknown, ele: HTMLElement) => {
|
|
1108
|
+
fired.push("unmount B");
|
|
1109
|
+
}
|
|
1110
|
+
},
|
|
1111
|
+
])
|
|
1112
|
+
]
|
|
1113
|
+
);
|
|
1114
|
+
|
|
1115
|
+
expect(fired).toEqual([]);
|
|
1116
|
+
|
|
1117
|
+
patch({ expanded: true });
|
|
1118
|
+
expect(fired).toEqual([]);
|
|
1119
|
+
|
|
1120
|
+
patch({ showB: false });
|
|
1121
|
+
expect(fired).toEqual(["unmount B"]);
|
|
1122
|
+
},
|
|
1123
|
+
|
|
1124
|
+
"onUnmount(): excess child removal + same-render sibling growth": () => {
|
|
1125
|
+
const container = setup();
|
|
1126
|
+
const fired: string[] = [];
|
|
1127
|
+
const state = createState({ expanded: false, showB: true });
|
|
1128
|
+
const patch = app<typeof state>(container, state, (s) =>
|
|
1129
|
+
[DIV,
|
|
1130
|
+
[SPAN,
|
|
1131
|
+
{
|
|
1132
|
+
onUnmount: (s: unknown, ele: HTMLElement) => {
|
|
1133
|
+
fired.push("unmount A");
|
|
1134
|
+
}
|
|
1135
|
+
},
|
|
1136
|
+
s.expanded && [ASIDE,
|
|
1137
|
+
{
|
|
1138
|
+
onUnmount: (s: unknown, ele: HTMLElement) => {
|
|
1139
|
+
fired.push("unmount A-child");
|
|
1140
|
+
}
|
|
1141
|
+
},
|
|
1142
|
+
"x"
|
|
1143
|
+
],
|
|
1144
|
+
],
|
|
1145
|
+
s.showB && [P,
|
|
1146
|
+
{
|
|
1147
|
+
onUnmount: (s: unknown, ele: HTMLElement) => {
|
|
1148
|
+
fired.push("unmount B");
|
|
1149
|
+
}
|
|
1150
|
+
},
|
|
1151
|
+
]
|
|
1152
|
+
]
|
|
1153
|
+
);
|
|
1154
|
+
|
|
1155
|
+
expect(fired).toEqual([]);
|
|
1156
|
+
patch({ expanded: true, showB: false });
|
|
1157
|
+
expect(fired).toEqual(["unmount B"]);
|
|
1158
|
+
},
|
|
1159
|
+
|
|
1084
1160
|
"onMount() + onUnmount: symmetry of calls": () => {
|
|
1085
1161
|
const container = setup();
|
|
1086
1162
|
const state = createState({
|
|
@@ -1137,4 +1213,192 @@ export default {
|
|
|
1137
1213
|
'Timer removed'
|
|
1138
1214
|
]);
|
|
1139
1215
|
},
|
|
1216
|
+
|
|
1217
|
+
"onMount(): with catched component, replacement vode's onMount fires when error occurs": () => {
|
|
1218
|
+
const container = setup();
|
|
1219
|
+
const mounts: string[] = [];
|
|
1220
|
+
const broken: any = () => { throw new Error("boom"); };
|
|
1221
|
+
app(container, {}, () =>
|
|
1222
|
+
[DIV,
|
|
1223
|
+
{
|
|
1224
|
+
catch: [SECTION,
|
|
1225
|
+
{
|
|
1226
|
+
onMount: (s: unknown, ele: HTMLElement) => {
|
|
1227
|
+
mounts.push("mount fallback");
|
|
1228
|
+
}
|
|
1229
|
+
},
|
|
1230
|
+
"fallback"
|
|
1231
|
+
]
|
|
1232
|
+
},
|
|
1233
|
+
broken
|
|
1234
|
+
]
|
|
1235
|
+
);
|
|
1236
|
+
|
|
1237
|
+
expect(mounts).toEqual(["mount fallback"]);
|
|
1238
|
+
},
|
|
1239
|
+
|
|
1240
|
+
"onMount(): with catched component, returned vode's onMount fires and receives error": () => {
|
|
1241
|
+
const container = setup();
|
|
1242
|
+
const mounts: string[] = [];
|
|
1243
|
+
const caughtErrors: string[] = [];
|
|
1244
|
+
const broken: any = () => { throw new Error("boom"); };
|
|
1245
|
+
app(container, {}, () =>
|
|
1246
|
+
[DIV,
|
|
1247
|
+
{
|
|
1248
|
+
catch: (s: unknown, err: Error) => {
|
|
1249
|
+
caughtErrors.push(err.message);
|
|
1250
|
+
return [SECTION,
|
|
1251
|
+
{
|
|
1252
|
+
onMount: (s: unknown, ele: HTMLElement) => {
|
|
1253
|
+
mounts.push("mount fallback");
|
|
1254
|
+
}
|
|
1255
|
+
},
|
|
1256
|
+
"fallback"
|
|
1257
|
+
];
|
|
1258
|
+
}
|
|
1259
|
+
},
|
|
1260
|
+
broken
|
|
1261
|
+
]
|
|
1262
|
+
);
|
|
1263
|
+
|
|
1264
|
+
expect(mounts).toEqual(["mount fallback"]);
|
|
1265
|
+
expect(caughtErrors).toEqual(["boom"]);
|
|
1266
|
+
},
|
|
1267
|
+
|
|
1268
|
+
"onUnmount(): with catched component, replacement vode's onUnmount fires when removed": () => {
|
|
1269
|
+
const container = setup();
|
|
1270
|
+
const unmounts: string[] = [];
|
|
1271
|
+
const state = createState({ show: true });
|
|
1272
|
+
const broken: any = () => { throw new Error("boom"); };
|
|
1273
|
+
const patch = app<typeof state>(container, state, (s) =>
|
|
1274
|
+
[DIV,
|
|
1275
|
+
s.show && [SECTION,
|
|
1276
|
+
{
|
|
1277
|
+
catch: [ARTICLE,
|
|
1278
|
+
{
|
|
1279
|
+
onUnmount: (s: unknown, ele: HTMLElement) => {
|
|
1280
|
+
unmounts.push("unmount fallback");
|
|
1281
|
+
}
|
|
1282
|
+
},
|
|
1283
|
+
"fallback"
|
|
1284
|
+
]
|
|
1285
|
+
},
|
|
1286
|
+
broken
|
|
1287
|
+
]
|
|
1288
|
+
]
|
|
1289
|
+
);
|
|
1290
|
+
|
|
1291
|
+
expect(unmounts).toEqual([]);
|
|
1292
|
+
patch({ show: false });
|
|
1293
|
+
expect(unmounts).toEqual(["unmount fallback"]);
|
|
1294
|
+
},
|
|
1295
|
+
|
|
1296
|
+
"onUnmount(): with catched component, deep replacement tree fires in post-order": () => {
|
|
1297
|
+
const container = setup();
|
|
1298
|
+
const unmounts: string[] = [];
|
|
1299
|
+
const state = createState({ show: true });
|
|
1300
|
+
const broken: any = () => { throw new Error("boom"); };
|
|
1301
|
+
const patch = app<typeof state>(container, state, (s) =>
|
|
1302
|
+
[DIV,
|
|
1303
|
+
s.show && [SECTION,
|
|
1304
|
+
{
|
|
1305
|
+
catch: [ARTICLE,
|
|
1306
|
+
{
|
|
1307
|
+
onUnmount: (s: unknown, ele: HTMLElement) => {
|
|
1308
|
+
unmounts.push("unmount article");
|
|
1309
|
+
}
|
|
1310
|
+
},
|
|
1311
|
+
[P,
|
|
1312
|
+
{
|
|
1313
|
+
onUnmount: (s: unknown, ele: HTMLElement) => {
|
|
1314
|
+
unmounts.push("unmount p");
|
|
1315
|
+
}
|
|
1316
|
+
},
|
|
1317
|
+
"x"
|
|
1318
|
+
],
|
|
1319
|
+
[SPAN,
|
|
1320
|
+
{
|
|
1321
|
+
onUnmount: (s: unknown, ele: HTMLElement) => {
|
|
1322
|
+
unmounts.push("unmount span");
|
|
1323
|
+
}
|
|
1324
|
+
},
|
|
1325
|
+
"y"
|
|
1326
|
+
]
|
|
1327
|
+
]
|
|
1328
|
+
},
|
|
1329
|
+
broken
|
|
1330
|
+
]
|
|
1331
|
+
]
|
|
1332
|
+
);
|
|
1333
|
+
|
|
1334
|
+
expect(unmounts).toEqual([]);
|
|
1335
|
+
patch({ show: false });
|
|
1336
|
+
expect(unmounts).toEqual(["unmount span", "unmount p", "unmount article"]);
|
|
1337
|
+
},
|
|
1338
|
+
|
|
1339
|
+
"onMount()/onUnmount(): with catched component, full lifecycle symmetry of catch replacement": () => {
|
|
1340
|
+
const container = setup();
|
|
1341
|
+
const logs: string[] = [];
|
|
1342
|
+
const state = createState({ show: true });
|
|
1343
|
+
const broken: any = () => { throw new Error("boom"); };
|
|
1344
|
+
const patch = app<typeof state>(container, state, (s) =>
|
|
1345
|
+
[DIV,
|
|
1346
|
+
s.show && [SECTION,
|
|
1347
|
+
{
|
|
1348
|
+
catch: [ARTICLE,
|
|
1349
|
+
{
|
|
1350
|
+
onMount: (s: unknown, ele: HTMLElement) => {
|
|
1351
|
+
logs.push("mount article");
|
|
1352
|
+
},
|
|
1353
|
+
onUnmount: (s: unknown, ele: HTMLElement) => {
|
|
1354
|
+
logs.push("unmount article");
|
|
1355
|
+
}
|
|
1356
|
+
},
|
|
1357
|
+
"fallback"
|
|
1358
|
+
]
|
|
1359
|
+
},
|
|
1360
|
+
broken
|
|
1361
|
+
]
|
|
1362
|
+
]
|
|
1363
|
+
);
|
|
1364
|
+
|
|
1365
|
+
expect(logs).toEqual(["mount article"]);
|
|
1366
|
+
patch({ show: false });
|
|
1367
|
+
expect(logs).toEqual(["mount article", "unmount article"]);
|
|
1368
|
+
},
|
|
1369
|
+
|
|
1370
|
+
"onMount(): with catched component, original element's onMount does NOT fire when error caused replacement": () => {
|
|
1371
|
+
const container = setup();
|
|
1372
|
+
const logs: string[] = [];
|
|
1373
|
+
const broken: any = () => { throw new Error("boom"); };
|
|
1374
|
+
app(container, {}, () =>
|
|
1375
|
+
[DIV,
|
|
1376
|
+
{
|
|
1377
|
+
catch: [ARTICLE,
|
|
1378
|
+
{
|
|
1379
|
+
onMount: (s: unknown, ele: HTMLElement) => {
|
|
1380
|
+
logs.push("mount fallback");
|
|
1381
|
+
}
|
|
1382
|
+
},
|
|
1383
|
+
"fallback"
|
|
1384
|
+
]
|
|
1385
|
+
},
|
|
1386
|
+
[SECTION,
|
|
1387
|
+
{
|
|
1388
|
+
onMount: (s: unknown, ele: HTMLElement) => {
|
|
1389
|
+
logs.push("mount original section");
|
|
1390
|
+
},
|
|
1391
|
+
onUnmount: (s: unknown, ele: HTMLElement) => {
|
|
1392
|
+
logs.push("unmount original section");
|
|
1393
|
+
}
|
|
1394
|
+
},
|
|
1395
|
+
broken
|
|
1396
|
+
]
|
|
1397
|
+
]
|
|
1398
|
+
);
|
|
1399
|
+
|
|
1400
|
+
// SECTION never finishes mounting (its child broke), so its onMount must not fire.
|
|
1401
|
+
// The catch on DIV replaces the broken subtree with ARTICLE whose onMount must fire.
|
|
1402
|
+
expect(logs).toEqual(["mount fallback"]);
|
|
1403
|
+
},
|
|
1140
1404
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { expect } from "./helper";
|
|
2
|
+
import { app, createState, DIV } from "../index";
|
|
3
|
+
|
|
4
|
+
function setup() {
|
|
5
|
+
const root = document.createElement("div");
|
|
6
|
+
const container = document.createElement("div");
|
|
7
|
+
root.appendChild(container);
|
|
8
|
+
return container;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
"patch(): generator function yields multiple state updates": async () => {
|
|
13
|
+
const container = setup();
|
|
14
|
+
const state: any = createState({ count: 0 });
|
|
15
|
+
app(container, state, (s: any) => [DIV, String(s.count)]);
|
|
16
|
+
|
|
17
|
+
expect(state.count).toEqual(0);
|
|
18
|
+
|
|
19
|
+
state.patch(function* () {
|
|
20
|
+
yield { count: 1 };
|
|
21
|
+
yield { count: 2 };
|
|
22
|
+
return { count: 3 };
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
await new Promise(r => setTimeout(r, 0));
|
|
26
|
+
|
|
27
|
+
expect(state.count).toEqual(3);
|
|
28
|
+
expect(container).toMatch([DIV, "3"]);
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
"patch(): async generator yields over time": async () => {
|
|
32
|
+
const container = setup();
|
|
33
|
+
const state: any = createState({ phase: "start", value: 0 });
|
|
34
|
+
app(container, state, (s: any) => [DIV, s.phase, String(s.value)]);
|
|
35
|
+
|
|
36
|
+
expect(state.phase).toEqual("start");
|
|
37
|
+
|
|
38
|
+
state.patch(async function* () {
|
|
39
|
+
yield { phase: "working", value: 10 };
|
|
40
|
+
yield { phase: "almost", value: 20 };
|
|
41
|
+
return { phase: "done", value: 30 };
|
|
42
|
+
}());
|
|
43
|
+
|
|
44
|
+
await new Promise(r => setTimeout(r, 0));
|
|
45
|
+
|
|
46
|
+
expect(state.phase).toEqual("done");
|
|
47
|
+
expect(state.value).toEqual(30);
|
|
48
|
+
expect(container).toMatch([DIV, "done", "30"]);
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
"patch(): Promise resolves and applies patch": async () => {
|
|
52
|
+
const container = setup();
|
|
53
|
+
const state: any = createState({ msg: "before" });
|
|
54
|
+
app(container, state, (s: any) => [DIV, s.msg]);
|
|
55
|
+
|
|
56
|
+
state.patch(Promise.resolve({ msg: "after" }));
|
|
57
|
+
|
|
58
|
+
await new Promise(r => setTimeout(r, 0));
|
|
59
|
+
|
|
60
|
+
expect(state.msg).toEqual("after");
|
|
61
|
+
expect(container).toMatch([DIV, "after"]);
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
"patch(): array with empty patches applies nothing": () => {
|
|
65
|
+
const container = setup();
|
|
66
|
+
const state: any = createState({ x: 1, y: 2 });
|
|
67
|
+
app(container, state, (s: any) => [DIV]);
|
|
68
|
+
|
|
69
|
+
state.patch([{}, {}]);
|
|
70
|
+
expect(state.x).toEqual(1);
|
|
71
|
+
expect(state.y).toEqual(2);
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
"patch(): array with null/undefined items skips them": () => {
|
|
75
|
+
const container = setup();
|
|
76
|
+
const state: any = createState({ x: 0, y: 0 });
|
|
77
|
+
app(container, state, (s: any) => [DIV, String(s.x), String(s.y)]);
|
|
78
|
+
|
|
79
|
+
state.patch([null, { x: 10 }, undefined, { y: 20 }]);
|
|
80
|
+
|
|
81
|
+
expect(state.x).toEqual(10);
|
|
82
|
+
expect(state.y).toEqual(20);
|
|
83
|
+
},
|
|
84
|
+
};
|