@ryupold/vode 1.8.10 → 1.8.12

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/log.txt CHANGED
@@ -0,0 +1 @@
1
+ npm warn gitignore-fallback No .npmignore file found, using .gitignore for file exclusion. Consider creating a .npmignore file to explicitly control published files.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryupold/vode",
3
- "version": "1.8.10",
3
+ "version": "1.8.12",
4
4
  "description": "a minimalist web framework",
5
5
  "author": "Michael Scherbakow (ryupold)",
6
6
  "license": "MIT",
@@ -38,9 +38,7 @@ export function mergeClass(...classes: ClassProp[]): ClassProp {
38
38
  else if (typeof a === "object" && typeof b === "string") {
39
39
  finalClass = { ...a, [b]: true };
40
40
  }
41
- else if (typeof a === "object" && typeof b === "object") {
42
- finalClass = { ...a, ...b };
43
- } else if (typeof a === "object" && Array.isArray(b)) {
41
+ else if (typeof a === "object" && Array.isArray(b)) {
44
42
  const aa = { ...a };
45
43
  for (const item of b as string[]) {
46
44
  (<Record<string, boolean | null | undefined>>aa)[item] = true;
@@ -55,6 +53,8 @@ export function mergeClass(...classes: ClassProp[]): ClassProp {
55
53
  aa[bKey] = (<Record<string, boolean | null | undefined>>b)[bKey];
56
54
  }
57
55
  finalClass = aa;
56
+ } else if (typeof a === "object" && typeof b === "object") {
57
+ finalClass = { ...a, ...b };
58
58
  }
59
59
  else throw new Error(`cannot merge classes of ${a} (${typeof a}) and ${b} (${typeof b})`);
60
60
  }
package/src/vode.ts CHANGED
@@ -72,7 +72,7 @@ export type PropertyValue<S> =
72
72
  | StyleProp | ClassProp
73
73
  | Patch<S>;
74
74
 
75
- export type Dispatch<S> = (action: Patch<S>) => void;
75
+ export type Dispatch<S> = (action: Patch<S>) => void | Promise<void>;
76
76
  export interface Patchable<S = object> { patch: Dispatch<S>; }
77
77
  export type PatchableState<S = object> = S & Patchable<S>;
78
78
 
@@ -149,7 +149,7 @@ export function app<S extends PatchableState = PatchableState>(
149
149
  _vode.qAsync = null;
150
150
  _vode.stats = { lastSyncRenderTime: 0, lastAsyncRenderTime: 0, syncRenderCount: 0, asyncRenderCount: 0, liveEffectCount: 0, patchCount: 0, syncRenderPatchCount: 0, asyncRenderPatchCount: 0 };
151
151
 
152
- const patchableState = state as PatchableState<S> & { patch: (action: Patch<S>, animate?: boolean) => void };
152
+ const patchableState = state as PatchableState<S> & { patch: (action: Patch<S>, animate?: boolean) => void | Promise<void> };
153
153
 
154
154
  if ("patch" in state && typeof state.patch === "function" && Array.isArray((state as any).patch.initialPatches)) {
155
155
  initialPatches = [...(state as any).patch.initialPatches, ...initialPatches];
@@ -159,7 +159,7 @@ export function app<S extends PatchableState = PatchableState>(
159
159
  _vode.stats.liveEffectCount++;
160
160
  try {
161
161
  const resolvedPatch = await (action as Promise<S>);
162
- patchableState.patch(<Patch<S>>resolvedPatch, isAnimated);
162
+ await patchableState.patch(<Patch<S>>resolvedPatch, isAnimated);
163
163
  } finally {
164
164
  _vode.stats.liveEffectCount--;
165
165
  }
@@ -173,13 +173,13 @@ export function app<S extends PatchableState = PatchableState>(
173
173
  while (v.done === false) {
174
174
  _vode.stats.liveEffectCount++;
175
175
  try {
176
- patchableState.patch(v.value, isAnimated);
176
+ await patchableState.patch(v.value, isAnimated);
177
177
  v = await generator.next();
178
178
  } finally {
179
179
  _vode.stats.liveEffectCount--;
180
180
  }
181
181
  }
182
- patchableState.patch(v.value as Patch<S>, isAnimated);
182
+ await patchableState.patch(v.value as Patch<S>, isAnimated);
183
183
  } finally {
184
184
  _vode.stats.liveEffectCount--;
185
185
  }
@@ -187,7 +187,7 @@ export function app<S extends PatchableState = PatchableState>(
187
187
 
188
188
  Object.defineProperty(state, "patch", {
189
189
  enumerable: false, configurable: true,
190
- writable: false, value: (action: Patch<S>, isAnimated?: boolean) => {
190
+ writable: false, value: (action: Patch<S>, isAnimated?: boolean): void | Promise<void> => {
191
191
  while (typeof action === "function") {
192
192
  action = (<(s: S) => unknown>action)(_vode.state);
193
193
  }
@@ -197,9 +197,9 @@ export function app<S extends PatchableState = PatchableState>(
197
197
  _vode.stats.patchCount++;
198
198
 
199
199
  if ((action as AsyncGenerator<Patch<S>>)?.next) {
200
- generatorPatch(action as AsyncGenerator<Patch<S>>, isAnimated);
200
+ return generatorPatch(action as AsyncGenerator<Patch<S>>, isAnimated);
201
201
  } else if ((action as Promise<S>).then) {
202
- promisePatch(action as Promise<S>, isAnimated);
202
+ return promisePatch(action as Promise<S>, isAnimated);
203
203
  } else if (Array.isArray(action)) {
204
204
  if (action.length > 0) {
205
205
  for (const p of action) {
package/test/helper.ts CHANGED
@@ -143,16 +143,18 @@ export class Expectation {
143
143
  );
144
144
  }
145
145
 
146
- toSucceed<Result>(): Result {
146
+ toSucceed<Result>(failMessage?: string): Result {
147
+ const failSuffix = failMessage ? `\n\n${failMessage}` : "";
147
148
  if (typeof this.what !== "function") {
148
- throw new ExpectationError(this, `expected a function\n\nbut it is a ${typeof this.what}`);
149
+ throw new ExpectationError(this, `expected a function\n\nbut it is a ${typeof this.what}${failSuffix}`);
149
150
  }
150
151
  return this.what();
151
152
  }
152
153
 
153
- toFail(): Error {
154
+ toFail(failMessage?: string): Error {
155
+ const failSuffix = failMessage ? `\n\n${failMessage}` : "";
154
156
  if (typeof this.what !== "function") {
155
- throw new ExpectationError(this, `expected a function\n\nbut it is a ${typeof this.what}`);
157
+ throw new ExpectationError(this, `expected a function\n\nbut it is a ${typeof this.what}${failSuffix}`);
156
158
  }
157
159
 
158
160
  let r: any;
@@ -161,28 +163,34 @@ export class Expectation {
161
163
  } catch (err: any) {
162
164
  return err;
163
165
  }
164
- throw new ExpectationError(this, `expected function to fail\n\nbut it succeeded with a result of type ${typeof r}\n\n${r}`);
166
+ throw new ExpectationError(this, `expected function to fail\n\nbut it succeeded with a result of type ${typeof r}\n\n${r}${failSuffix}`);
165
167
  }
166
168
 
167
- toSucceedAsync<Result>(waitTime: number = 100): Promise<Result> {
169
+ toSucceedAsync<Result>(failMessage?: string, waitTime: number = 100): Promise<Result> {
170
+ const failSuffix = failMessage ? `\n\n${failMessage}` : "";
168
171
  if (typeof this.what !== "function") {
169
- throw new ExpectationError(this, `expected a function\n\nbut it is a ${typeof this.what}`);
172
+ throw new ExpectationError(this, `expected a function\n\nbut it is a ${typeof this.what}${failSuffix}`);
170
173
  }
171
174
  return retry<Result>(() => this.what(), waitTime);
172
175
  }
173
176
 
174
- async toFailAsync(): Promise<Error> {
177
+ async toFailAsync(failMessage?: string): Promise<Error> {
178
+ const failSuffix = failMessage ? `\n\n${failMessage}` : "";
179
+
175
180
  if (typeof this.what !== "function") {
176
- throw new ExpectationError(this, `expected a function\n\nbut it is a ${typeof this.what}`);
181
+ throw new ExpectationError(this, `expected a function\n\nbut it is a ${typeof this.what}${failSuffix}`);
177
182
  }
178
183
 
179
184
  let r: any;
180
185
  try {
181
- r = await this.what();
186
+ if(typeof this.what === "function")
187
+ r = await this.what();
188
+ else
189
+ r = await this.what;
182
190
  } catch (err: any) {
183
191
  return err;
184
192
  }
185
- throw new ExpectationError(this, `expected function to fail\n\nbut it succeeded with a result of type ${typeof r}\n\n${r}`);
193
+ throw new ExpectationError(this, `expected function to fail\n\nbut it succeeded with a result of type ${typeof r}\n\n${r}${failSuffix}`);
186
194
  }
187
195
 
188
196
  async toMatch(v: ChildVode,
@@ -247,7 +255,7 @@ export class Expectation {
247
255
  } else {
248
256
  attributeValue = (e as HTMLElement).getAttribute(k);
249
257
  }
250
- if (!attributeValue) {
258
+ if (attributeValue === null) {
251
259
  throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\nan element <${tag(cv)?.toUpperCase()}>\n\nwith attribute [${k}="${val}"]\n\nbut it was not found${failSuffix}`);
252
260
  }
253
261
  if (attributeValue !== val) {
package/test/mocks.ts CHANGED
@@ -213,7 +213,7 @@ export function resetMocks() {
213
213
  }, 16);
214
214
  }
215
215
 
216
- const mockDoc: any = {
216
+ const fakeDocument: any = {
217
217
  createElement: (tag: string) => new FakeElement(tag),
218
218
  createTextNode: (text: string) => new FakeTextNode(text),
219
219
  createElementNS: (ns: string, tag: string) => new FakeElement(tag),
@@ -224,10 +224,11 @@ export function resetMocks() {
224
224
  updateCallbackDone: Promise.resolve(),
225
225
  skipTransition() { },
226
226
  };
227
- }
227
+ },
228
+ _fake: true,
228
229
  };
229
230
 
230
- Object.defineProperty(mockDoc, "hidden", {
231
+ Object.defineProperty(fakeDocument, "hidden", {
231
232
  enumerable: true,
232
233
  configurable: true,
233
234
  get: () => hidden,
@@ -239,7 +240,7 @@ export function resetMocks() {
239
240
  },
240
241
  });
241
242
 
242
- const mockWin: any = {
243
+ const fakeWindow: any = {
243
244
  requestAnimationFrame: (cb: FrameRequestCallback) => {
244
245
  const id = ++rafHandle;
245
246
  rafQueue.set(id, cb);
@@ -248,20 +249,31 @@ export function resetMocks() {
248
249
  },
249
250
  cancelAnimationFrame: (id: number) => {
250
251
  rafQueue.delete(id);
251
- }
252
+ },
253
+ _fake: true,
252
254
  };
253
255
 
254
- globalThis.document ??= mockDoc as Document;
255
- globalThis.window ??= mockWin as (Window & typeof globalThis);
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);
256
264
  globalThis.Node ??= NodeConstants as any;
257
265
 
258
- const raf = globalThis.window?.requestAnimationFrame;
259
- if (typeof raf === "function") {
260
- globals.requestAnimationFrame = raf.bind(globalThis.window);
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
+ }
261
271
  }
262
272
 
263
- const startViewTransition = (globalThis.document as any)?.startViewTransition;
264
- globals.startViewTransition = typeof startViewTransition === "function"
265
- ? startViewTransition.bind(globalThis.document)
266
- : null;
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
+ }
267
279
  }
package/test/tests-app.ts CHANGED
@@ -339,4 +339,45 @@ export default {
339
339
  const el = (container as any)._vode.vode.node;
340
340
  expect(el.onclick).toBeA("function");
341
341
  },
342
+
343
+ "app(): class as array renders correctly": async () => {
344
+ const root = document.createElement("div");
345
+ const container = document.createElement("div");
346
+ root.appendChild(container);
347
+
348
+ app(container, {}, () =>
349
+ [DIV, { class: ["foo", "bar", "baz"] }, "text"]
350
+ );
351
+
352
+ await expect(container).toMatch([DIV, { class: "foo bar baz" }, "text"]);
353
+ },
354
+
355
+ "app(): class as number becomes empty string": async () => {
356
+ const root = document.createElement("div");
357
+ const container = document.createElement("div");
358
+ root.appendChild(container);
359
+
360
+ app(container, {}, () =>
361
+ [DIV, { class: 123 as any }, "text"]
362
+ );
363
+
364
+ await expect(container).toMatch([DIV, { class: "" }, "text"]);
365
+ },
366
+
367
+ "app(): style object to string transition": async () => {
368
+ const root = document.createElement("div");
369
+ const container = document.createElement("div");
370
+ root.appendChild(container);
371
+ const state: any = { useObject: true };
372
+
373
+ app(container, state, (s: any) =>
374
+ [DIV, { style: s.useObject ? { color: "red" } : "color: blue" }, "text"]
375
+ );
376
+
377
+ await expect(container).toMatch([DIV, "text"]);
378
+
379
+ state.patch({ useObject: false });
380
+
381
+ await expect(container).toMatch([DIV, "text"]);
382
+ },
342
383
  };
@@ -91,7 +91,7 @@ export default {
91
91
  [INPUT],
92
92
  [BUTTON, "Add"],
93
93
  [NAV,
94
- [BUTTON, "All"],
94
+ [BUTTON, { class: 'active' }, "All"],
95
95
  [BUTTON, "Active"],
96
96
  [BUTTON, "Done"],
97
97
  ],
@@ -115,7 +115,7 @@ export default {
115
115
  [BUTTON, "Add"],
116
116
  [NAV,
117
117
  [BUTTON, "All"],
118
- [BUTTON, "Active"],
118
+ [BUTTON, { class: 'active' }, "Active"],
119
119
  [BUTTON, "Done"],
120
120
  ],
121
121
  [UL,
@@ -135,7 +135,7 @@ export default {
135
135
  [NAV,
136
136
  [BUTTON, "All"],
137
137
  [BUTTON, "Active"],
138
- [BUTTON, "Done"],
138
+ [BUTTON, { class: 'active' }, "Done"],
139
139
  ],
140
140
  [UL,
141
141
  [LI, "[X] Walk dog"],
@@ -43,12 +43,14 @@ export default {
43
43
  await expect(mergeClass({ foo: true, bar: true }, { bar: false, baz: true })).toEqual({ foo: true, bar: false, baz: true });
44
44
  },
45
45
 
46
- "mergeClass(): object and array": async () => {
47
- await expect(mergeClass({ foo: true }, ["bar", "baz"])).toEqual({ foo: true, 0: "bar", 1: "baz" });
46
+ "mergeClass(): object and array (array items become class names with true)": async () => {
47
+ await expect(mergeClass({ foo: true }, ["bar", "baz"])).toEqual({ foo: true, bar: true, baz: true });
48
+ await expect(mergeClass({ active: true }, ["btn", "primary"])).toEqual({ active: true, btn: true, primary: true });
48
49
  },
49
50
 
50
- "mergeClass(): array and object": async () => {
51
- await expect(mergeClass(["foo", "bar"], { baz: true, qux: false })).toEqual({ 0: "foo", 1: "bar", baz: true, qux: false });
51
+ "mergeClass(): array and object (object keys become class names)": async () => {
52
+ await expect(mergeClass(["foo", "bar"], { baz: true, qux: false })).toEqual({ foo: true, bar: true, baz: true, qux: false });
53
+ await expect(mergeClass(["a", "b"], { c: true, d: false })).toEqual({ a: true, b: true, c: true, d: false });
52
54
  },
53
55
 
54
56
  "mergeClass(): falsy entries are skipped": async () => {
@@ -59,5 +61,10 @@ export default {
59
61
  "mergeClass(): multiple args (3+)": async () => {
60
62
  await expect(mergeClass("a", "b", "c")).toEqual("a b c");
61
63
  await expect(mergeClass("x", null, ["y", "z"], "w")).toEqual("y z x w");
64
+ },
65
+
66
+ "mergeClass(): incompatible types throw": async () => {
67
+ await expect(() => (mergeClass as any)(123 as any, "foo")).toFail();
68
+ await expect(() => (mergeClass as any)("foo", 456 as any)).toFail();
62
69
  }
63
70
  };
@@ -11,6 +11,11 @@ export default {
11
11
  await expect(mergeProps(p) === p).toEqual(true);
12
12
  },
13
13
 
14
+ "mergeProps(): single falsy arg returns undefined": async () => {
15
+ await expect(mergeProps(null)).toEqual(undefined);
16
+ await expect(mergeProps(undefined)).toEqual(undefined);
17
+ },
18
+
14
19
  "mergeProps(): two plain objects merged": async () => {
15
20
  await expect(mergeProps({ a: 1 }, { b: 2 })).toEqual({ a: 1, b: 2 });
16
21
  },