@ryupold/vode 1.8.8 → 1.8.11

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/mocks.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { globals } from "../src/vode";
2
+
1
3
  const NodeConstants = {
2
4
  ELEMENT_NODE: 1,
3
5
  ATTRIBUTE_NODE: 2,
@@ -181,14 +183,40 @@ export class FakeTextNode {
181
183
  }
182
184
 
183
185
  export function resetMocks() {
184
- const mockDoc: any = {
186
+ let hidden = false;
187
+ let rafHandle = 0;
188
+ const rafQueue = new Map<number, FrameRequestCallback>();
189
+ let rafTimer: ReturnType<typeof setTimeout> | null = null;
190
+
191
+ function scheduleNextFrame() {
192
+ if (rafTimer !== null || hidden || rafQueue.size === 0) {
193
+ return;
194
+ }
195
+
196
+ rafTimer = setTimeout(() => {
197
+ rafTimer = null;
198
+
199
+ if (hidden || rafQueue.size === 0) {
200
+ scheduleNextFrame();
201
+ return;
202
+ }
203
+
204
+ const now = performance.now();
205
+ const callbacks = Array.from(rafQueue.values());
206
+ rafQueue.clear();
207
+
208
+ for (const cb of callbacks) {
209
+ cb(now);
210
+ }
211
+
212
+ scheduleNextFrame();
213
+ }, 16);
214
+ }
215
+
216
+ const fakeDocument: any = {
185
217
  createElement: (tag: string) => new FakeElement(tag),
186
218
  createTextNode: (text: string) => new FakeTextNode(text),
187
219
  createElementNS: (ns: string, tag: string) => new FakeElement(tag),
188
- hidden: false,
189
- };
190
- const mockWin: any = {
191
- requestAnimationFrame: (cb: any) => cb(Date.now()),
192
220
  startViewTransition: (callbackOptions: any) => {
193
221
  return {
194
222
  finished: Promise.resolve(),
@@ -196,10 +224,56 @@ export function resetMocks() {
196
224
  updateCallbackDone: Promise.resolve(),
197
225
  skipTransition() { },
198
226
  };
199
- }
227
+ },
228
+ _fake: true,
200
229
  };
201
230
 
202
- globalThis.document ??= mockDoc as Document;
203
- globalThis.window ??= mockWin as (Window & typeof globalThis);
231
+ Object.defineProperty(fakeDocument, "hidden", {
232
+ enumerable: true,
233
+ configurable: true,
234
+ get: () => hidden,
235
+ set: (value: boolean) => {
236
+ hidden = !!value;
237
+ if (!hidden) {
238
+ scheduleNextFrame();
239
+ }
240
+ },
241
+ });
242
+
243
+ const fakeWindow: any = {
244
+ requestAnimationFrame: (cb: FrameRequestCallback) => {
245
+ const id = ++rafHandle;
246
+ rafQueue.set(id, cb);
247
+ scheduleNextFrame();
248
+ return id;
249
+ },
250
+ cancelAnimationFrame: (id: number) => {
251
+ rafQueue.delete(id);
252
+ },
253
+ _fake: true,
254
+ };
255
+
256
+ if ((<typeof fakeDocument>globalThis.document)?._fake)
257
+ globalThis.document = undefined as any;
258
+ if ((<typeof fakeWindow>globalThis.window)?._fake)
259
+ globalThis.window = undefined as any;
260
+
261
+
262
+ globalThis.document ??= fakeDocument as Document;
263
+ globalThis.window ??= fakeWindow as (Window & typeof globalThis);
204
264
  globalThis.Node ??= NodeConstants as any;
205
- }
265
+
266
+ if ((<typeof fakeWindow>globalThis.window)?._fake) {
267
+ const raf = globalThis.window?.requestAnimationFrame;
268
+ if (typeof raf === "function") {
269
+ globals.requestAnimationFrame = raf.bind(globalThis.window);
270
+ }
271
+ }
272
+
273
+ if ((<typeof fakeDocument>globalThis.document)?._fake) {
274
+ const startViewTransition = (globalThis.document as any)?.startViewTransition;
275
+ globals.startViewTransition = typeof startViewTransition === "function"
276
+ ? startViewTransition.bind(globalThis.document)
277
+ : null;
278
+ }
279
+ }
@@ -0,0 +1,61 @@
1
+ import { tests } from ".";
2
+ import { expect, ExpectationError } from "./helper";
3
+ import { resetMocks } from "./mocks";
4
+
5
+ const count = {
6
+ total: 0,
7
+ passed: 0,
8
+ failed: <string[]>[],
9
+ }
10
+ const line = "----------------------------------";
11
+
12
+ async function runTest(test: [string, () => any]) {
13
+ count.total++;
14
+ resetMocks();
15
+ const start = performance.now();
16
+ try {
17
+ expect(document).toBeNotHidden();
18
+ const result = test[1]();
19
+ if (result && typeof (result as any)?.then === "function") {
20
+ await result;
21
+ }
22
+ count.passed++;
23
+ const time = (performance.now() - start).toFixed(3) + " ms";
24
+ console.log(`#${count.total} ${test[0]}\n-> 🟢 passed ${time}\n${line}`);
25
+ } catch (err: any) {
26
+ const time = (performance.now() - start).toFixed(3) + " ms";
27
+ console.error(`#${count.total} ${test[0]}\n-> 🔴 failed ${time}`);
28
+ if (err instanceof ExpectationError) {
29
+ count.failed.push(`#${count.total} ${test[0]}\n-> 🔴 failed:\n${err.message}\n${line}`);
30
+ }
31
+ else {
32
+ count.failed.push(`#${count.total} ${test[0]}\n-> 🔴 failed:\n${err.message}\n${err.stack}\n${line}`);
33
+ }
34
+ }
35
+ }
36
+
37
+ const sw = performance.now();
38
+ (async () => {
39
+ for (const test of Object.entries(tests)) {
40
+ await runTest(test);
41
+ }
42
+
43
+ const time = (performance.now() - sw).toFixed(3) + " ms";
44
+
45
+ console.log(`
46
+ total: ${count.total}
47
+ passed: ${count.passed}
48
+ failed: ${count.failed.length}
49
+
50
+ time: ${time}
51
+ `);
52
+
53
+ if (count.passed === count.total) {
54
+ console.log("\n\nall tests passed\n");
55
+ }
56
+ else {
57
+ console.error(`${line.replaceAll("-", "=")}\nError summary:\n\n${count.failed.join(`\n${line}\n`)}`);
58
+
59
+ throw "\n\nsome tests failed (see output)\n";
60
+ }
61
+ })();
package/test/tests-app.ts CHANGED
@@ -2,7 +2,7 @@ import { expect } from "./helper";
2
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,10 +221,10 @@ 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": () => {
227
+ "app(): isolated state of multiple independent vode app instances": async () => {
228
228
  const root = document.createElement("div");
229
229
 
230
230
  // APP 1 (foo) //
@@ -254,14 +254,14 @@ export default {
254
254
  ]);
255
255
  /////////////////
256
256
 
257
- expect(containerFoo).toMatch(
257
+ await expect(containerFoo).toMatch(
258
258
  [DIV,
259
259
  [P, "App 1 count: 0"],
260
260
  [BUTTON, "Sync +1"],
261
261
  ]
262
262
  );
263
263
 
264
- expect(containerBar).toMatch(
264
+ await expect(containerBar).toMatch(
265
265
  [DIV,
266
266
  [P, "App 2 count: 0"],
267
267
  ]
@@ -270,14 +270,14 @@ export default {
270
270
  // Patch state1 independently: no effect on state2
271
271
  patchFoo({ count: 5 });
272
272
 
273
- expect(containerFoo).toMatch(
273
+ await expect(containerFoo).toMatch(
274
274
  [DIV,
275
275
  [P, "App 1 count: 5"],
276
276
  [BUTTON, "Sync +1"],
277
277
  ]
278
278
  );
279
279
 
280
- expect(containerBar).toMatch(
280
+ await expect(containerBar).toMatch(
281
281
  [DIV,
282
282
  [P, "App 2 count: 0"],
283
283
  ]
@@ -286,14 +286,14 @@ export default {
286
286
  // Patch state2 independently: no effect on state1
287
287
  patchBar({ count: 3 });
288
288
 
289
- expect(containerFoo).toMatch(
289
+ await expect(containerFoo).toMatch(
290
290
  [DIV,
291
291
  [P, "App 1 count: 5"],
292
292
  [BUTTON, "Sync +1"],
293
293
  ]
294
294
  );
295
295
 
296
- expect(containerBar).toMatch(
296
+ await expect(containerBar).toMatch(
297
297
  [DIV,
298
298
  [P, "App 2 count: 3"],
299
299
  ]
@@ -302,14 +302,14 @@ export default {
302
302
  // Sync state2 via the returned patch function
303
303
  patchBar({ count: 10 });
304
304
 
305
- expect(containerBar).toMatch(
305
+ await expect(containerBar).toMatch(
306
306
  [DIV,
307
307
  [P, "App 2 count: 10"],
308
308
  ]
309
309
  );
310
310
  },
311
311
 
312
- "app(): root tag changes between renders": () => {
312
+ "app(): root tag changes between renders": async () => {
313
313
  const root = document.createElement("div");
314
314
  const container = document.createElement("div");
315
315
  root.appendChild(container);
@@ -319,11 +319,11 @@ export default {
319
319
  s.useSection ? [SECTION, "section mode"] : [DIV, "div mode"]
320
320
  );
321
321
 
322
- expect(container).toMatch([DIV, "div mode"]);
322
+ await expect(container).toMatch([DIV, "div mode"]);
323
323
 
324
324
  patch({ useSection: true });
325
325
 
326
- expect(root).toMatch([DIV, [SECTION, "section mode"]]);
326
+ await expect(root).toMatch([DIV, [SECTION, "section mode"]]);
327
327
  },
328
328
 
329
329
  "app(): event handler with object patch": () => {
@@ -339,4 +339,4 @@ export default {
339
339
  const el = (container as any)._vode.vode.node;
340
340
  expect(el.onclick).toBeA("function");
341
341
  },
342
- };
342
+ };
@@ -9,7 +9,7 @@ function setup() {
9
9
  }
10
10
 
11
11
  export default {
12
- "catch: function fallback renders instead of broken component": () => {
12
+ "catch: function fallback renders instead of broken component": async () => {
13
13
  const container = setup();
14
14
  const broken = () => { throw new Error("boom"); };
15
15
 
@@ -22,14 +22,14 @@ export default {
22
22
  ]
23
23
  );
24
24
 
25
- expect(container).toMatch(
25
+ await expect(container).toMatch(
26
26
  [DIV,
27
27
  [P, "caught: boom"]
28
28
  ]
29
29
  );
30
30
  },
31
31
 
32
- "catch: static vode fallback renders instead of broken component": () => {
32
+ "catch: static vode fallback renders instead of broken component": async () => {
33
33
  const container = setup();
34
34
  const broken = () => { throw new Error("boom"); };
35
35
 
@@ -42,14 +42,14 @@ export default {
42
42
  ]
43
43
  );
44
44
 
45
- expect(container).toMatch(
45
+ await expect(container).toMatch(
46
46
  [DIV,
47
47
  [ARTICLE, "error occurred"]
48
48
  ]
49
49
  );
50
50
  },
51
51
 
52
- "catch: nested error boundaries — inner catch handles inner error": () => {
52
+ "catch: nested error boundaries — inner catch handles inner error": async () => {
53
53
  const container = setup();
54
54
  const broken = () => { throw new Error("inner boom"); };
55
55
 
@@ -66,7 +66,7 @@ export default {
66
66
  ]
67
67
  );
68
68
 
69
- expect(container).toMatch(
69
+ await expect(container).toMatch(
70
70
  [DIV,
71
71
  [SECTION,
72
72
  [ARTICLE, "inner fallback"]
@@ -75,7 +75,7 @@ export default {
75
75
  );
76
76
  },
77
77
 
78
- "catch: nested error boundaries — outer catches when inner has no handler": () => {
78
+ "catch: nested error boundaries — outer catches when inner has no handler": async () => {
79
79
  const container = setup();
80
80
  const broken = () => { throw new Error("boom"); };
81
81
 
@@ -88,14 +88,14 @@ export default {
88
88
  ]
89
89
  );
90
90
 
91
- expect(container).toMatch(
91
+ await expect(container).toMatch(
92
92
  [DIV,
93
93
  [P, "outer caught it"]
94
94
  ]
95
95
  );
96
96
  },
97
97
 
98
- "catch: error propagates when no handler exists on entire tree": () => {
98
+ "catch: error propagates when no handler exists on entire tree": async () => {
99
99
  const container = setup();
100
100
  const broken = () => { throw new Error("crash"); };
101
101
  let threw = false;
@@ -108,10 +108,10 @@ export default {
108
108
  threw = true;
109
109
  }
110
110
 
111
- expect(threw).toEqual(true);
111
+ await expect(threw).toEqual(true);
112
112
  },
113
113
 
114
- "catch: catch handler changed on A→A path": () => {
114
+ "catch: catch handler changed on A→A path": async () => {
115
115
  const container = setup();
116
116
  const state = createState({ catchValue: "v1", showBroken: false });
117
117
  const broken = () => { throw new Error("boom"); };
@@ -125,18 +125,18 @@ export default {
125
125
  ]
126
126
  );
127
127
 
128
- expect(container).toMatch(
128
+ await expect(container).toMatch(
129
129
  [DIV, [SECTION, "ok"]]
130
130
  );
131
131
 
132
132
  patch({ catchValue: "v2", showBroken: true });
133
133
 
134
- expect(container).toMatch(
134
+ await expect(container).toMatch(
135
135
  [DIV, [P, "v2"]]
136
136
  );
137
137
  },
138
138
 
139
- "catch: error in one sibling doesn't affect the other": () => {
139
+ "catch: error in one sibling doesn't affect the other": async () => {
140
140
  const container = setup();
141
141
  const broken = () => { throw new Error("boom"); };
142
142
 
@@ -150,7 +150,7 @@ export default {
150
150
  ]
151
151
  );
152
152
 
153
- expect(container).toMatch(
153
+ await expect(container).toMatch(
154
154
  [DIV,
155
155
  [P, "whoops"],
156
156
  [ARTICLE, "i am fine"]