@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/test/mocks.ts
CHANGED
|
@@ -13,84 +13,178 @@ const NodeConstants = {
|
|
|
13
13
|
NOTATION_NODE: 12,
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
class FakeNodeList implements NodeListOf<ChildNode> {
|
|
17
|
+
[index: number]: ChildNode;
|
|
18
|
+
public readonly data: ChildNode[] = [];
|
|
19
|
+
|
|
20
|
+
constructor() {
|
|
21
|
+
let self = this;
|
|
22
|
+
|
|
23
|
+
return new Proxy(this, {
|
|
24
|
+
get(target, prop) {
|
|
25
|
+
const key: string = typeof prop === "symbol" ? String(prop) : prop;
|
|
26
|
+
|
|
27
|
+
if (<any>Number(key) == key && !(prop in target)) {
|
|
28
|
+
return self.data[parseInt(key)];
|
|
29
|
+
}
|
|
30
|
+
return target[prop as any];
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
item(index: number): ChildNode {
|
|
37
|
+
return this.data[index] ?? null;
|
|
38
|
+
}
|
|
39
|
+
forEach(callbackfn: (value: ChildNode, key: number, parent: NodeListOf<ChildNode>) => void, thisArg?: any): void {
|
|
40
|
+
for (let i = 0; i < this.length; i++) {
|
|
41
|
+
callbackfn.bind(thisArg)(this.data[i], i, this);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
entries(): ArrayIterator<[number, ChildNode]> {
|
|
45
|
+
return new Array(this.length).fill(0).map((_, i) => [i, this[i]] as [number, ChildNode])[Symbol.iterator]();
|
|
46
|
+
}
|
|
47
|
+
keys(): ArrayIterator<number> {
|
|
48
|
+
return new Array(this.data.length).fill(0).map((_, i) => i)[Symbol.iterator]();
|
|
49
|
+
}
|
|
50
|
+
values(): ArrayIterator<ChildNode> {
|
|
51
|
+
return new Array(this.data.length).fill(0).map((_, i) => this[i])[Symbol.iterator]();
|
|
52
|
+
}
|
|
53
|
+
[Symbol.iterator](): ArrayIterator<ChildNode> {
|
|
54
|
+
return new Array(this.data.length).fill(0).map((_, i) => this[i])[Symbol.iterator]();
|
|
55
|
+
}
|
|
56
|
+
get length() {
|
|
57
|
+
return this.data.length;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class FakeElement {
|
|
62
|
+
public fakeAttributes: Record<string, string> = {};
|
|
63
|
+
|
|
17
64
|
nodeType = NodeConstants.ELEMENT_NODE;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return Object.entries(this._attrs).map(([name, value]) => ({ name, value }));
|
|
65
|
+
parentElement: HTMLElement | null = null;
|
|
66
|
+
childNodes: NodeListOf<ChildNode> = new FakeNodeList();
|
|
67
|
+
get children(): HTMLCollection {
|
|
68
|
+
return this.childNodes as unknown as HTMLCollection;
|
|
23
69
|
}
|
|
24
70
|
style: { cssText: string } = { cssText: "" };
|
|
25
|
-
|
|
26
|
-
|
|
71
|
+
|
|
72
|
+
readonly tagName: string;
|
|
27
73
|
|
|
28
74
|
constructor(public tag?: string) {
|
|
29
|
-
|
|
75
|
+
this.tagName = tag?.toUpperCase() || "???";
|
|
30
76
|
}
|
|
31
77
|
|
|
32
78
|
get firstChild() { return this.childNodes[0] ?? null; }
|
|
33
79
|
get lastChild() { return this.childNodes[this.childNodes.length - 1] ?? null; }
|
|
34
80
|
get nextSibling() { return null; }
|
|
81
|
+
get attributes() {
|
|
82
|
+
return Object.entries(this.fakeAttributes).map(([name, value]) => ({ name, value })) as any;
|
|
83
|
+
}
|
|
35
84
|
|
|
36
|
-
hasAttributes() { return Object.keys(this.
|
|
85
|
+
hasAttributes() { return Object.keys(this.fakeAttributes).length > 0; }
|
|
37
86
|
hasChildNodes() { return this.childNodes.length > 0; }
|
|
38
|
-
setAttribute(name: string, value: string) { this.
|
|
39
|
-
removeAttribute(name: string) { delete this.
|
|
40
|
-
|
|
41
|
-
|
|
87
|
+
setAttribute(name: string, value: string) { this.fakeAttributes[name] = value; }
|
|
88
|
+
removeAttribute(name: string) { delete this.fakeAttributes[name]; }
|
|
89
|
+
|
|
90
|
+
appendChild(child: FakeElement | FakeTextNode): FakeElement | FakeTextNode {
|
|
91
|
+
(this.childNodes as FakeNodeList).data.push(child as any);
|
|
92
|
+
(child as any).parentElement = this;
|
|
93
|
+
return child;
|
|
42
94
|
}
|
|
43
95
|
remove() {
|
|
44
|
-
if (this.parentElement) {
|
|
96
|
+
if (this.parentElement) {
|
|
97
|
+
const i = (this.parentElement.childNodes as FakeNodeList).data.indexOf(this as any);
|
|
98
|
+
if (i >= 0)
|
|
99
|
+
(this.parentElement.childNodes as FakeNodeList).data.splice(i, 1);
|
|
100
|
+
}
|
|
45
101
|
}
|
|
46
|
-
replaceWith(...nodes: (
|
|
102
|
+
replaceWith(...nodes: (FakeElement | FakeTextNode)[]) {
|
|
47
103
|
const parent = this.parentElement;
|
|
48
104
|
if (parent) {
|
|
49
|
-
const i = parent.childNodes.indexOf(this);
|
|
50
|
-
if (i >= 0) {
|
|
51
|
-
|
|
105
|
+
const i = (<FakeNodeList>parent.childNodes).data.indexOf(this as any);
|
|
106
|
+
if (i >= 0) {
|
|
107
|
+
(<FakeNodeList>parent.childNodes).data.splice(i, 1, ...nodes as any);
|
|
108
|
+
}
|
|
109
|
+
for (const n of nodes) {
|
|
110
|
+
n.parentElement = parent;
|
|
111
|
+
}
|
|
52
112
|
}
|
|
53
113
|
}
|
|
54
|
-
before(...nodes: (
|
|
114
|
+
before(...nodes: (FakeElement | FakeTextNode)[]) {
|
|
55
115
|
const parent = this.parentElement;
|
|
56
116
|
if (parent) {
|
|
57
|
-
const i = parent.childNodes.indexOf(this);
|
|
58
|
-
if (i >= 0) {
|
|
59
|
-
|
|
117
|
+
const i = (<FakeNodeList>parent.childNodes).data.indexOf(this as any);
|
|
118
|
+
if (i >= 0) {
|
|
119
|
+
for (const n of nodes) {
|
|
120
|
+
if (n === this) continue;
|
|
121
|
+
if (n.parentElement) {
|
|
122
|
+
const ni = (<FakeNodeList>n.parentElement.childNodes).data.indexOf(n as any);
|
|
123
|
+
if (ni >= 0) (<FakeNodeList>n.parentElement.childNodes).data.splice(ni, 1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const filtered = nodes.filter(n => n !== this);
|
|
127
|
+
(<FakeNodeList>parent.childNodes).data.splice(i, 0, ...filtered as any);
|
|
128
|
+
for (const n of filtered) {
|
|
129
|
+
n.parentElement = parent;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
60
132
|
}
|
|
61
133
|
}
|
|
62
|
-
get [Symbol.iterator]() {
|
|
134
|
+
get [Symbol.iterator]() {
|
|
135
|
+
return Array.prototype[Symbol.iterator].bind(this.children);
|
|
136
|
+
}
|
|
63
137
|
}
|
|
64
138
|
|
|
65
|
-
export class
|
|
139
|
+
export class FakeTextNode {
|
|
66
140
|
nodeType = NodeConstants.TEXT_NODE;
|
|
67
|
-
parentElement:
|
|
141
|
+
parentElement: HTMLElement | null = null;
|
|
68
142
|
constructor(public nodeValue: string) { }
|
|
69
143
|
get wholeText() { return this.nodeValue; }
|
|
70
|
-
remove() {
|
|
71
|
-
|
|
144
|
+
remove() {
|
|
145
|
+
if (this.parentElement) {
|
|
146
|
+
const i = (<FakeNodeList>this.parentElement.childNodes).data.indexOf(this as any);
|
|
147
|
+
if (i >= 0)
|
|
148
|
+
(<FakeNodeList>this.parentElement.childNodes).data.splice(i, 1);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
replaceWith(...nodes: (FakeElement | FakeTextNode)[]) {
|
|
72
152
|
const parent = this.parentElement;
|
|
73
153
|
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)
|
|
154
|
+
const i = (<FakeNodeList>parent.childNodes).data.indexOf(this as any);
|
|
155
|
+
if (i >= 0) { (<FakeNodeList>parent.childNodes).data.splice(i, 1, ...nodes as any); }
|
|
156
|
+
for (const n of nodes) {
|
|
157
|
+
n.parentElement = parent;
|
|
158
|
+
}
|
|
77
159
|
}
|
|
78
160
|
}
|
|
79
|
-
before(...nodes: (
|
|
161
|
+
before(...nodes: (FakeElement | FakeTextNode)[]) {
|
|
80
162
|
const parent = this.parentElement;
|
|
81
163
|
if (parent) {
|
|
82
|
-
const i = parent.childNodes.indexOf(this);
|
|
83
|
-
if (i >= 0) {
|
|
84
|
-
|
|
164
|
+
const i = (<FakeNodeList>parent.childNodes).data.indexOf(this as any);
|
|
165
|
+
if (i >= 0) {
|
|
166
|
+
for (const n of nodes) {
|
|
167
|
+
if (n === this) continue;
|
|
168
|
+
if (n.parentElement) {
|
|
169
|
+
const ni = (<FakeNodeList>n.parentElement.childNodes).data.indexOf(n as any);
|
|
170
|
+
if (ni >= 0) (<FakeNodeList>n.parentElement.childNodes).data.splice(ni, 1);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const filtered = nodes.filter(n => n !== this);
|
|
174
|
+
(<FakeNodeList>parent.childNodes).data.splice(i, 0, ...filtered as any);
|
|
175
|
+
for (const n of filtered) {
|
|
176
|
+
n.parentElement = parent;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
85
179
|
}
|
|
86
180
|
}
|
|
87
181
|
}
|
|
88
182
|
|
|
89
183
|
export function resetMocks() {
|
|
90
184
|
const mockDoc: any = {
|
|
91
|
-
createElement: (tag: string) => new
|
|
92
|
-
createTextNode: (text: string) => new
|
|
93
|
-
createElementNS: (ns: string, tag: string) => new
|
|
185
|
+
createElement: (tag: string) => new FakeElement(tag),
|
|
186
|
+
createTextNode: (text: string) => new FakeTextNode(text),
|
|
187
|
+
createElementNS: (ns: string, tag: string) => new FakeElement(tag),
|
|
94
188
|
hidden: false,
|
|
95
189
|
};
|
|
96
190
|
const mockWin: any = {
|
package/test/tests-app.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { expect } from "./helper";
|
|
2
|
-
import { app, ARTICLE, DIV, P, SPAN } from "../index";
|
|
2
|
+
import { app, ARTICLE, BUTTON, createState, DIV, P, SPAN, SECTION } from "../index";
|
|
3
3
|
|
|
4
4
|
export default {
|
|
5
5
|
"app(): successful initialization": () => {
|
|
@@ -223,4 +223,120 @@ export default {
|
|
|
223
223
|
|
|
224
224
|
expect(state.x).toEqual(1);
|
|
225
225
|
},
|
|
226
|
+
|
|
227
|
+
"app(): isolated state of multiple independent vode app instances": () => {
|
|
228
|
+
const root = document.createElement("div");
|
|
229
|
+
|
|
230
|
+
// APP 1 (foo) //
|
|
231
|
+
const containerFoo = document.createElement("div");
|
|
232
|
+
root.appendChild(containerFoo);
|
|
233
|
+
const stateFoo = createState({ count: 0 });
|
|
234
|
+
const patchFoo = app<typeof stateFoo>(containerFoo, stateFoo, (s) => [
|
|
235
|
+
DIV,
|
|
236
|
+
[P, `App 1 count: ${s.count}`],
|
|
237
|
+
[BUTTON, {
|
|
238
|
+
onclick: () => {
|
|
239
|
+
// sync state2 from app1 via the returned patch function
|
|
240
|
+
patchBar({ count: stateBar.count + 1 });
|
|
241
|
+
return { count: s.count + 1 };
|
|
242
|
+
}
|
|
243
|
+
}, "Sync +1"],
|
|
244
|
+
]);
|
|
245
|
+
/////////////////
|
|
246
|
+
|
|
247
|
+
// APP 2 (bar) //
|
|
248
|
+
const containerBar = document.createElement("div");
|
|
249
|
+
root.appendChild(containerBar);
|
|
250
|
+
const stateBar = createState({ count: 0 });
|
|
251
|
+
const patchBar = app<typeof stateBar>(containerBar, stateBar, (s) => [
|
|
252
|
+
DIV,
|
|
253
|
+
[P, `App 2 count: ${s.count}`],
|
|
254
|
+
]);
|
|
255
|
+
/////////////////
|
|
256
|
+
|
|
257
|
+
expect(containerFoo).toMatch(
|
|
258
|
+
[DIV,
|
|
259
|
+
[P, "App 1 count: 0"],
|
|
260
|
+
[BUTTON, "Sync +1"],
|
|
261
|
+
]
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
expect(containerBar).toMatch(
|
|
265
|
+
[DIV,
|
|
266
|
+
[P, "App 2 count: 0"],
|
|
267
|
+
]
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
// Patch state1 independently: no effect on state2
|
|
271
|
+
patchFoo({ count: 5 });
|
|
272
|
+
|
|
273
|
+
expect(containerFoo).toMatch(
|
|
274
|
+
[DIV,
|
|
275
|
+
[P, "App 1 count: 5"],
|
|
276
|
+
[BUTTON, "Sync +1"],
|
|
277
|
+
]
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
expect(containerBar).toMatch(
|
|
281
|
+
[DIV,
|
|
282
|
+
[P, "App 2 count: 0"],
|
|
283
|
+
]
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// Patch state2 independently: no effect on state1
|
|
287
|
+
patchBar({ count: 3 });
|
|
288
|
+
|
|
289
|
+
expect(containerFoo).toMatch(
|
|
290
|
+
[DIV,
|
|
291
|
+
[P, "App 1 count: 5"],
|
|
292
|
+
[BUTTON, "Sync +1"],
|
|
293
|
+
]
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
expect(containerBar).toMatch(
|
|
297
|
+
[DIV,
|
|
298
|
+
[P, "App 2 count: 3"],
|
|
299
|
+
]
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
// Sync state2 via the returned patch function
|
|
303
|
+
patchBar({ count: 10 });
|
|
304
|
+
|
|
305
|
+
expect(containerBar).toMatch(
|
|
306
|
+
[DIV,
|
|
307
|
+
[P, "App 2 count: 10"],
|
|
308
|
+
]
|
|
309
|
+
);
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
"app(): root tag changes between renders": () => {
|
|
313
|
+
const root = document.createElement("div");
|
|
314
|
+
const container = document.createElement("div");
|
|
315
|
+
root.appendChild(container);
|
|
316
|
+
const state = createState({ useSection: false });
|
|
317
|
+
|
|
318
|
+
const patch = app<typeof state>(container, state, (s) =>
|
|
319
|
+
s.useSection ? [SECTION, "section mode"] : [DIV, "div mode"]
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
expect(container).toMatch([DIV, "div mode"]);
|
|
323
|
+
|
|
324
|
+
patch({ useSection: true });
|
|
325
|
+
|
|
326
|
+
expect(root).toMatch([DIV, [SECTION, "section mode"]]);
|
|
327
|
+
},
|
|
328
|
+
|
|
329
|
+
"app(): event handler with object patch": () => {
|
|
330
|
+
const root = document.createElement("div");
|
|
331
|
+
const container = document.createElement("div");
|
|
332
|
+
root.appendChild(container);
|
|
333
|
+
const state: any = { count: 0 };
|
|
334
|
+
|
|
335
|
+
app(container, state, (s: any) =>
|
|
336
|
+
[DIV, { onclick: { count: 42 } }, "click me"]
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
const el = (container as any)._vode.vode.node;
|
|
340
|
+
expect(el.onclick).toBeA("function");
|
|
341
|
+
},
|
|
226
342
|
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { expect } from "./helper";
|
|
2
|
+
import { app, createState, DIV, ARTICLE, SECTION, P } 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
|
+
"catch: function fallback renders instead of broken component": () => {
|
|
13
|
+
const container = setup();
|
|
14
|
+
const broken = () => { throw new Error("boom"); };
|
|
15
|
+
|
|
16
|
+
app(container, {}, () =>
|
|
17
|
+
[DIV,
|
|
18
|
+
[SECTION,
|
|
19
|
+
{ catch: (s: unknown, err: Error) => [P, `caught: ${err.message}`] },
|
|
20
|
+
broken
|
|
21
|
+
]
|
|
22
|
+
]
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
expect(container).toMatch(
|
|
26
|
+
[DIV,
|
|
27
|
+
[P, "caught: boom"]
|
|
28
|
+
]
|
|
29
|
+
);
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
"catch: static vode fallback renders instead of broken component": () => {
|
|
33
|
+
const container = setup();
|
|
34
|
+
const broken = () => { throw new Error("boom"); };
|
|
35
|
+
|
|
36
|
+
app(container, {}, () =>
|
|
37
|
+
[DIV,
|
|
38
|
+
[SECTION,
|
|
39
|
+
{ catch: [ARTICLE, "error occurred"] },
|
|
40
|
+
broken
|
|
41
|
+
]
|
|
42
|
+
]
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
expect(container).toMatch(
|
|
46
|
+
[DIV,
|
|
47
|
+
[ARTICLE, "error occurred"]
|
|
48
|
+
]
|
|
49
|
+
);
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
"catch: nested error boundaries — inner catch handles inner error": () => {
|
|
53
|
+
const container = setup();
|
|
54
|
+
const broken = () => { throw new Error("inner boom"); };
|
|
55
|
+
|
|
56
|
+
app(container, {}, () =>
|
|
57
|
+
[DIV,
|
|
58
|
+
[SECTION,
|
|
59
|
+
[P,
|
|
60
|
+
{
|
|
61
|
+
catch: [ARTICLE, "inner fallback"]
|
|
62
|
+
},
|
|
63
|
+
broken
|
|
64
|
+
]
|
|
65
|
+
]
|
|
66
|
+
]
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
expect(container).toMatch(
|
|
70
|
+
[DIV,
|
|
71
|
+
[SECTION,
|
|
72
|
+
[ARTICLE, "inner fallback"]
|
|
73
|
+
]
|
|
74
|
+
]
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
"catch: nested error boundaries — outer catches when inner has no handler": () => {
|
|
79
|
+
const container = setup();
|
|
80
|
+
const broken = () => { throw new Error("boom"); };
|
|
81
|
+
|
|
82
|
+
app(container, {}, () =>
|
|
83
|
+
[DIV,
|
|
84
|
+
[SECTION,
|
|
85
|
+
{ catch: [P, "outer caught it"] },
|
|
86
|
+
[ARTICLE, broken]
|
|
87
|
+
]
|
|
88
|
+
]
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
expect(container).toMatch(
|
|
92
|
+
[DIV,
|
|
93
|
+
[P, "outer caught it"]
|
|
94
|
+
]
|
|
95
|
+
);
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
"catch: error propagates when no handler exists on entire tree": () => {
|
|
99
|
+
const container = setup();
|
|
100
|
+
const broken = () => { throw new Error("crash"); };
|
|
101
|
+
let threw = false;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
app(container, {}, () =>
|
|
105
|
+
[DIV, [P, broken]]
|
|
106
|
+
);
|
|
107
|
+
} catch {
|
|
108
|
+
threw = true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
expect(threw).toEqual(true);
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
"catch: catch handler changed on A→A path": () => {
|
|
115
|
+
const container = setup();
|
|
116
|
+
const state = createState({ catchValue: "v1", showBroken: false });
|
|
117
|
+
const broken = () => { throw new Error("boom"); };
|
|
118
|
+
|
|
119
|
+
const patch = app<typeof state>(container, state, (s) =>
|
|
120
|
+
[DIV,
|
|
121
|
+
[SECTION,
|
|
122
|
+
{ catch: [P, s.catchValue] },
|
|
123
|
+
s.showBroken ? broken : "ok"
|
|
124
|
+
]
|
|
125
|
+
]
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
expect(container).toMatch(
|
|
129
|
+
[DIV, [SECTION, "ok"]]
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
patch({ catchValue: "v2", showBroken: true });
|
|
133
|
+
|
|
134
|
+
expect(container).toMatch(
|
|
135
|
+
[DIV, [P, "v2"]]
|
|
136
|
+
);
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
"catch: error in one sibling doesn't affect the other": () => {
|
|
140
|
+
const container = setup();
|
|
141
|
+
const broken = () => { throw new Error("boom"); };
|
|
142
|
+
|
|
143
|
+
app(container, {}, () =>
|
|
144
|
+
[DIV,
|
|
145
|
+
[SECTION,
|
|
146
|
+
{ catch: [P, "whoops"] },
|
|
147
|
+
broken
|
|
148
|
+
],
|
|
149
|
+
[ARTICLE, "i am fine"]
|
|
150
|
+
]
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
expect(container).toMatch(
|
|
154
|
+
[DIV,
|
|
155
|
+
[P, "whoops"],
|
|
156
|
+
[ARTICLE, "i am fine"]
|
|
157
|
+
]
|
|
158
|
+
);
|
|
159
|
+
},
|
|
160
|
+
};
|
package/test/tests-defuse.ts
CHANGED
|
@@ -70,5 +70,26 @@ export default {
|
|
|
70
70
|
|
|
71
71
|
expect(state.patch)
|
|
72
72
|
.toEqual(undefined);
|
|
73
|
-
}
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
"defuse(): clears event listeners from child vodes without _vode": () => {
|
|
76
|
+
const root = document.createElement("div");
|
|
77
|
+
const container = document.createElement("div");
|
|
78
|
+
root.appendChild(container);
|
|
79
|
+
app(container, {}, () => [DIV, { onclick: () => ({}) },
|
|
80
|
+
[DIV, { onclick: () => ({}) }]
|
|
81
|
+
] as any);
|
|
82
|
+
const v = (container as any)._vode.vode;
|
|
83
|
+
const child1 = (v as any).node;
|
|
84
|
+
const child1onclick = child1.onclick;
|
|
85
|
+
const child2 = (v as any)[2].node;
|
|
86
|
+
expect(typeof child1onclick).toEqual("function");
|
|
87
|
+
expect(typeof child2.onclick).toEqual("function");
|
|
88
|
+
defuse(container as any);
|
|
89
|
+
|
|
90
|
+
expect(child1.onclick)
|
|
91
|
+
.toEqual(null);
|
|
92
|
+
expect(child2.onclick)
|
|
93
|
+
.toEqual(null);
|
|
94
|
+
},
|
|
74
95
|
};
|