@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/.github/workflows/publish.yml +5 -0
- package/.github/workflows/tests.yml +1 -0
- package/README.md +36 -2
- package/dist/vode.cjs.min.js +2 -2
- package/dist/vode.d.ts +1 -1
- package/dist/vode.es5.min.js +2 -2
- package/dist/vode.js +7 -7
- package/dist/vode.min.js +1 -1
- package/dist/vode.min.mjs +1 -1
- package/dist/vode.mjs +7 -7
- package/dist/vode.tests.mjs +393 -169
- package/log.txt +1 -0
- package/package.json +1 -1
- package/src/merge-class.ts +3 -3
- package/src/vode.ts +8 -8
- package/test/helper.ts +20 -12
- package/test/mocks.ts +26 -14
- package/test/tests-app.ts +41 -0
- package/test/tests-examples.ts +3 -3
- package/test/tests-mergeClass.ts +11 -4
- package/test/tests-mergeProps.ts +5 -0
- package/test/tests-mount-unmount.ts +219 -142
- package/test/tests-patch-advanced.ts +108 -2
- package/test/tests-state-context.ts +8 -0
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
package/src/merge-class.ts
CHANGED
|
@@ -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" &&
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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);
|
|
256
264
|
globalThis.Node ??= NodeConstants as any;
|
|
257
265
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
};
|
package/test/tests-examples.ts
CHANGED
|
@@ -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"],
|
package/test/tests-mergeClass.ts
CHANGED
|
@@ -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,
|
|
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({
|
|
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
|
};
|
package/test/tests-mergeProps.ts
CHANGED
|
@@ -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
|
},
|