@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/test/tests-app.ts CHANGED
@@ -1,8 +1,8 @@
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
- "app(): successful initialization": () => {
5
+ "app(): successful initialization": async () => {
6
6
  const root = document.createElement("div");
7
7
  const container = document.createElement("div");
8
8
  root.appendChild(container);
@@ -19,7 +19,7 @@ export default {
19
19
 
20
20
  expect(patch).toBeA("function");
21
21
 
22
- expect(container).toMatch(
22
+ await expect(container).toMatch(
23
23
  [DIV,
24
24
  [ARTICLE,
25
25
  [P, "foo", [SPAN, "bar"]]
@@ -30,15 +30,15 @@ export default {
30
30
 
31
31
  //=== FAILURE CASES ===
32
32
 
33
- "app(): fails when the container has no parent": () => {
33
+ "app(): fails when the container has no parent": async () => {
34
34
  const container = document.createElement("div");
35
35
  const err = expect(() => app(container, {}, () => [DIV]))
36
36
  .toFail();
37
37
 
38
- expect(err.message).toEqual("first argument to app() must be a valid HTMLElement inside the <html></html> document");
38
+ await expect(err.message).toEqual("first argument to app() must be a valid HTMLElement inside the <html></html> document");
39
39
  },
40
40
 
41
- "app(): fails when the state is not an object": () => {
41
+ "app(): fails when the state is not an object": async () => {
42
42
  const root = document.createElement("div");
43
43
  const container = document.createElement("div");
44
44
  root.appendChild(container);
@@ -46,10 +46,10 @@ export default {
46
46
  const err = expect(() => app(container, "oops", () => [DIV]))
47
47
  .toFail();
48
48
 
49
- expect(err.message).toEqual("second argument to app() must be a state object");
49
+ await expect(err.message).toEqual("second argument to app() must be a state object");
50
50
  },
51
51
 
52
- "app(): fails when the dom factory is not a function": () => {
52
+ "app(): fails when the dom factory is not a function": async () => {
53
53
  const root = document.createElement("div");
54
54
  const container = document.createElement("div");
55
55
  root.appendChild(container);
@@ -57,12 +57,12 @@ export default {
57
57
  const err = expect(() => app(container, {}, [DIV] as any))
58
58
  .toFail();
59
59
 
60
- expect(err.message).toEqual("third argument to app() must be a function that returns a vode");
60
+ await expect(err.message).toEqual("third argument to app() must be a function that returns a vode");
61
61
  },
62
62
 
63
63
  //=== INITIAL PATCHES ===
64
64
 
65
- "app(): executes initial patches after first render": () => {
65
+ "app(): executes initial patches after first render": async () => {
66
66
  const root = document.createElement("div");
67
67
  const container = document.createElement("div");
68
68
  root.appendChild(container);
@@ -74,12 +74,12 @@ export default {
74
74
  () => ({ start: 2 })
75
75
  );
76
76
 
77
- expect(state).toEqual({ count: 7, start: 2 });
77
+ await expect(state).toEqual({ count: 7, start: 2 });
78
78
  },
79
79
 
80
80
  //=== STATE PATCHING ===
81
81
 
82
- "app(): patch with object updates state and re-renders DOM": () => {
82
+ "app(): patch with object updates state and re-renders DOM": async () => {
83
83
  const root = document.createElement("div");
84
84
  const container = document.createElement("div");
85
85
  root.appendChild(container);
@@ -87,16 +87,16 @@ export default {
87
87
  const state: any = { msg: "hello" };
88
88
  app(container, state, (s: any) => [DIV, s.msg]);
89
89
 
90
- expect(state.msg).toEqual("hello");
91
- expect(container).toMatch([DIV, "hello"]);
90
+ await expect(state.msg).toEqual("hello");
91
+ await expect(container).toMatch([DIV, "hello"]);
92
92
 
93
93
  state.patch({ msg: "world" });
94
94
 
95
- expect(state.msg).toEqual("world");
96
- expect(container).toMatch([DIV, "world"]);
95
+ await expect(state.msg).toEqual("world");
96
+ await expect(container).toMatch([DIV, "world"]);
97
97
  },
98
98
 
99
- "app(): patch with effect function executes and applies result": () => {
99
+ "app(): patch with effect function executes and applies result": async () => {
100
100
  const root = document.createElement("div");
101
101
  const container = document.createElement("div");
102
102
  root.appendChild(container);
@@ -106,11 +106,11 @@ export default {
106
106
 
107
107
  state.patch(() => ({ count: 5 }));
108
108
 
109
- expect(state.count).toEqual(5);
110
- expect(container).toMatch([DIV, "5"]);
109
+ await expect(state.count).toEqual(5);
110
+ await expect(container).toMatch([DIV, "5"]);
111
111
  },
112
112
 
113
- "app(): patch with array applies multiple patches in sequence": () => {
113
+ "app(): patch with array applies multiple patches in sequence": async () => {
114
114
  const root = document.createElement("div");
115
115
  const container = document.createElement("div");
116
116
  root.appendChild(container);
@@ -118,13 +118,13 @@ export default {
118
118
  const state: any = { a: 1, b: 2 };
119
119
  app(container, state, () => [DIV]);
120
120
 
121
- state.patch([{ a: 10 }, { b: 20 }]);
121
+ await state.patch([{ a: 10 }, { b: 20 }]);
122
122
 
123
- expect(state.a).toEqual(10);
124
- expect(state.b).toEqual(20);
123
+ await expect(state.a).toEqual(10);
124
+ await expect(state.b).toEqual(20);
125
125
  },
126
126
 
127
- "app(): multiple sequential patches both apply": () => {
127
+ "app(): multiple sequential patches both apply": async () => {
128
128
  const root = document.createElement("div");
129
129
  const container = document.createElement("div");
130
130
  root.appendChild(container);
@@ -135,12 +135,12 @@ export default {
135
135
  state.patch({ x: 1 });
136
136
  state.patch({ y: 2 });
137
137
 
138
- expect(state).toEqual({ x: 1, y: 2 });
138
+ await expect(state).toEqual({ x: 1, y: 2 });
139
139
  },
140
140
 
141
141
  //=== LIFECYCLE ===
142
142
 
143
- "app(): onMount callback is called on newly created child elements": () => {
143
+ "app(): onMount callback is called on newly created child elements": async () => {
144
144
  const root = document.createElement("div");
145
145
  const container = document.createElement("div");
146
146
  root.appendChild(container);
@@ -152,12 +152,12 @@ export default {
152
152
  ] as any
153
153
  );
154
154
 
155
- expect(mountCalled).toEqual(true);
155
+ await expect(mountCalled).toEqual(true);
156
156
  },
157
157
 
158
158
  //=== COMPONENTS ===
159
159
 
160
- "app(): component function as child renders correctly": () => {
160
+ "app(): component function as child renders correctly": async () => {
161
161
  const root = document.createElement("div");
162
162
  const container = document.createElement("div");
163
163
  root.appendChild(container);
@@ -168,12 +168,12 @@ export default {
168
168
  ]
169
169
  );
170
170
 
171
- expect(container).toMatch(
171
+ await expect(container).toMatch(
172
172
  [DIV, [SPAN, "component rendered"]]
173
173
  );
174
174
  },
175
175
 
176
- "app(): component accesses state and renders dynamic content": () => {
176
+ "app(): component accesses state and renders dynamic content": async () => {
177
177
  const root = document.createElement("div");
178
178
  const container = document.createElement("div");
179
179
  root.appendChild(container);
@@ -185,12 +185,12 @@ export default {
185
185
  ]
186
186
  );
187
187
 
188
- expect(container).toMatch([DIV, [SPAN, "dynamic"]]);
188
+ await expect(container).toMatch([DIV, [SPAN, "dynamic"]]);
189
189
  },
190
190
 
191
191
  //=== DEEP STATE ===
192
192
 
193
- "app(): deep nested state merges correctly via patch": () => {
193
+ "app(): deep nested state merges correctly via patch": async () => {
194
194
  const root = document.createElement("div");
195
195
  const container = document.createElement("div");
196
196
  root.appendChild(container);
@@ -200,14 +200,14 @@ export default {
200
200
 
201
201
  state.patch({ nested: { value: 2 } });
202
202
 
203
- expect(state.nested.value).toEqual(2);
204
- expect(state.nested.other).toEqual("keep");
205
- expect(container).toMatch([DIV, "2"]);
203
+ await expect(state.nested.value).toEqual(2);
204
+ await expect(state.nested.other).toEqual("keep");
205
+ await expect(container).toMatch([DIV, "2"]);
206
206
  },
207
207
 
208
208
  //=== IGNORED PATCHES ===
209
209
 
210
- "app(): patching with ignored types is a no-op": () => {
210
+ "app(): patching with ignored types is a no-op": async () => {
211
211
  const root = document.createElement("div");
212
212
  const container = document.createElement("div");
213
213
  root.appendChild(container);
@@ -221,6 +221,122 @@ export default {
221
221
  state.patch("ignored");
222
222
  state.patch(true);
223
223
 
224
- expect(state.x).toEqual(1);
224
+ await expect(state.x).toEqual(1);
225
225
  },
226
- };
226
+
227
+ "app(): isolated state of multiple independent vode app instances": async () => {
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
+ await expect(containerFoo).toMatch(
258
+ [DIV,
259
+ [P, "App 1 count: 0"],
260
+ [BUTTON, "Sync +1"],
261
+ ]
262
+ );
263
+
264
+ await 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
+ await expect(containerFoo).toMatch(
274
+ [DIV,
275
+ [P, "App 1 count: 5"],
276
+ [BUTTON, "Sync +1"],
277
+ ]
278
+ );
279
+
280
+ await 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
+ await expect(containerFoo).toMatch(
290
+ [DIV,
291
+ [P, "App 1 count: 5"],
292
+ [BUTTON, "Sync +1"],
293
+ ]
294
+ );
295
+
296
+ await 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
+ await expect(containerBar).toMatch(
306
+ [DIV,
307
+ [P, "App 2 count: 10"],
308
+ ]
309
+ );
310
+ },
311
+
312
+ "app(): root tag changes between renders": async () => {
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
+ await expect(container).toMatch([DIV, "div mode"]);
323
+
324
+ patch({ useSection: true });
325
+
326
+ await 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
+ },
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": async () => {
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
+ await expect(container).toMatch(
26
+ [DIV,
27
+ [P, "caught: boom"]
28
+ ]
29
+ );
30
+ },
31
+
32
+ "catch: static vode fallback renders instead of broken component": async () => {
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
+ await expect(container).toMatch(
46
+ [DIV,
47
+ [ARTICLE, "error occurred"]
48
+ ]
49
+ );
50
+ },
51
+
52
+ "catch: nested error boundaries — inner catch handles inner error": async () => {
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
+ await 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": async () => {
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
+ await 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": async () => {
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
+ await expect(threw).toEqual(true);
112
+ },
113
+
114
+ "catch: catch handler changed on A→A path": async () => {
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
+ await expect(container).toMatch(
129
+ [DIV, [SECTION, "ok"]]
130
+ );
131
+
132
+ patch({ catchValue: "v2", showBroken: true });
133
+
134
+ await expect(container).toMatch(
135
+ [DIV, [P, "v2"]]
136
+ );
137
+ },
138
+
139
+ "catch: error in one sibling doesn't affect the other": async () => {
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
+ await expect(container).toMatch(
154
+ [DIV,
155
+ [P, "whoops"],
156
+ [ARTICLE, "i am fine"]
157
+ ]
158
+ );
159
+ },
160
+ };
@@ -2,68 +2,68 @@ import { children, child, childCount, childrenStart, DIV, SPAN, P, Vode } from "
2
2
  import { expect } from "./helper";
3
3
 
4
4
  export default {
5
- "children(): tag+props+children returns children array": () => {
5
+ "children(): tag+props+children returns children array": async () => {
6
6
  const v: Vode = [DIV, { class: "x" }, [SPAN, "a"], [P, "b"]];
7
7
  const c = children(v);
8
- expect(Array.isArray(c)).toEqual(true);
9
- expect(c!.length).toEqual(2);
8
+ await expect(Array.isArray(c)).toEqual(true);
9
+ await expect(c!.length).toEqual(2);
10
10
  },
11
11
 
12
- "children(): tag+children (no props) returns children array": () => {
12
+ "children(): tag+children (no props) returns children array": async () => {
13
13
  const v: Vode = [DIV, [SPAN, "a"], [P, "b"]];
14
14
  const c = children(v);
15
- expect(Array.isArray(c)).toEqual(true);
16
- expect(c!.length).toEqual(2);
15
+ await expect(Array.isArray(c)).toEqual(true);
16
+ await expect(c!.length).toEqual(2);
17
17
  },
18
18
 
19
- "children(): just-tag vode returns null": () => {
20
- expect(children([DIV])).toEqual(null);
19
+ "children(): just-tag vode returns null": async () => {
20
+ await expect(children([DIV])).toEqual(null);
21
21
  },
22
22
 
23
- "children(): text vode returns null": () => {
24
- expect(children("hello")).toEqual(null);
23
+ "children(): text vode returns null": async () => {
24
+ await expect(children("hello")).toEqual(null);
25
25
  },
26
26
 
27
- "childrenStart(): with props+children returns 2": () => {
28
- expect(childrenStart([DIV, { class: "x" }, [SPAN]])).toEqual(2);
27
+ "childrenStart(): with props+children returns 2": async () => {
28
+ await expect(childrenStart([DIV, { class: "x" }, [SPAN]])).toEqual(2);
29
29
  },
30
30
 
31
- "childrenStart(): without props but with children returns 1": () => {
32
- expect(childrenStart([DIV, [SPAN]])).toEqual(1);
31
+ "childrenStart(): without props but with children returns 1": async () => {
32
+ await expect(childrenStart([DIV, [SPAN]])).toEqual(1);
33
33
  },
34
34
 
35
- "childrenStart(): just-tag returns -1": () => {
36
- expect(childrenStart([DIV])).toEqual(-1);
35
+ "childrenStart(): just-tag returns -1": async () => {
36
+ await expect(childrenStart([DIV])).toEqual(-1);
37
37
  },
38
38
 
39
- "childrenStart(): text vode returns -1": () => {
40
- expect(childrenStart("hello")).toEqual(-1);
39
+ "childrenStart(): text vode returns -1": async () => {
40
+ await expect(childrenStart("hello")).toEqual(-1);
41
41
  },
42
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);
43
+ "childCount(): matches actual child count": async () => {
44
+ await expect(childCount([DIV, { class: "x" }, [SPAN, "a"], [P, "b"]])).toEqual(2);
45
+ await expect(childCount([DIV, [SPAN]])).toEqual(1);
46
46
  },
47
47
 
48
- "childCount(): returns 0 for no-children vode": () => {
49
- expect(childCount([DIV])).toEqual(0);
50
- expect(childCount("hello" as any)).toEqual(0);
48
+ "childCount(): returns 0 for no-children vode": async () => {
49
+ await expect(childCount([DIV])).toEqual(0);
50
+ await expect(childCount("hello" as any)).toEqual(0);
51
51
  },
52
52
 
53
- "child(): returns correct child at index": () => {
53
+ "child(): returns correct child at index": async () => {
54
54
  const v: Vode = [DIV, { class: "x" }, [SPAN, "a"], [P, "b"]];
55
55
 
56
- expect(child(v, 0)).toEqual([SPAN, "a"]);
57
- expect(child(v, 1)).toEqual([P, "b"]);
56
+ await expect(child(v, 0)).toEqual([SPAN, "a"]);
57
+ await expect(child(v, 1)).toEqual([P, "b"]);
58
58
  },
59
59
 
60
- "child(): returns undefined for out-of-bounds": () => {
61
- expect(child([DIV, { class: "x" }, [SPAN]], 5))
60
+ "child(): returns undefined for out-of-bounds": async () => {
61
+ await expect(child([DIV, { class: "x" }, [SPAN]], 5))
62
62
  .toEqual(undefined);
63
63
  },
64
64
 
65
- "child(): returns undefined for text vode": () => {
66
- expect(child("hello" as any, 0))
65
+ "child(): returns undefined for text vode": async () => {
66
+ await expect(child("hello" as any, 0))
67
67
  .toEqual(undefined);
68
68
  }
69
69
  };
@@ -2,27 +2,27 @@ import { createPatch } from "../src/vode";
2
2
  import { expect } from "./helper";
3
3
 
4
4
  export default {
5
- "createPatch(): just returns the input": () => {
5
+ "createPatch(): just returns the input": async () => {
6
6
  const p = { a: 123 };
7
- expect(createPatch(p) === p).toEqual(true);
7
+ await expect(createPatch(p) === p).toEqual(true);
8
8
  },
9
9
 
10
- "createPatch(): returns undefined as-is": () => {
11
- expect(createPatch(undefined)).toEqual(undefined);
10
+ "createPatch(): returns undefined as-is": async () => {
11
+ await expect(createPatch(undefined)).toEqual(undefined);
12
12
  },
13
13
 
14
- "createPatch(): returns null as-is": () => {
15
- expect(createPatch(null)).toEqual(null);
14
+ "createPatch(): returns null as-is": async () => {
15
+ await expect(createPatch(null)).toEqual(null);
16
16
  },
17
17
 
18
- "createPatch(): returns function as-is": () => {
18
+ "createPatch(): returns function as-is": async () => {
19
19
  const fn = () => ({});
20
- expect(createPatch(fn) === fn).toEqual(true);
20
+ await expect(createPatch(fn) === fn).toEqual(true);
21
21
  },
22
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);
23
+ "createPatch(): returns primitive as-is": async () => {
24
+ await expect(createPatch(42)).toEqual(42);
25
+ await expect(createPatch("ignored")).toEqual("ignored");
26
+ await expect(createPatch(false)).toEqual(false);
27
27
  }
28
28
  };