@ryupold/vode 1.8.10 → 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/.github/workflows/publish.yml +5 -0
- package/.github/workflows/tests.yml +1 -0
- package/README.md +36 -2
- package/dist/vode.cjs.min.js +1 -1
- package/dist/vode.d.ts +1 -1
- package/dist/vode.es5.min.js +1 -1
- package/dist/vode.js +5 -5
- package/dist/vode.min.js +1 -1
- package/dist/vode.min.mjs +1 -1
- package/dist/vode.mjs +5 -5
- package/dist/vode.tests.mjs +331 -159
- package/log.txt +1 -0
- package/package.json +1 -1
- package/src/vode.ts +8 -8
- package/test/helper.ts +19 -11
- package/test/mocks.ts +26 -14
- package/test/tests-mount-unmount.ts +219 -142
- package/test/tests-patch-advanced.ts +108 -2
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,
|
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
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { app, ContainerNode, createState, memo } from "../src/vode"
|
|
1
|
+
import { app, Component, ContainerNode, createState, memo } from "../src/vode"
|
|
2
2
|
import { ARTICLE, ASIDE, DIV, INPUT, MAIN, NAV, P, SECTION, SPAN } from "../src/vode-tags";
|
|
3
3
|
import { expect, ExpectationError } from "./helper";
|
|
4
4
|
|
|
@@ -448,6 +448,92 @@ export default {
|
|
|
448
448
|
await expect(mounts).toEqual(["mount span"]);
|
|
449
449
|
},
|
|
450
450
|
|
|
451
|
+
"onMount(): with catched component, replacement vode's onMount fires when error occurs": async () => {
|
|
452
|
+
const container = setup();
|
|
453
|
+
const mounts: string[] = [];
|
|
454
|
+
const broken: any = () => { throw new Error("boom"); };
|
|
455
|
+
app(container, {}, () =>
|
|
456
|
+
[DIV,
|
|
457
|
+
{
|
|
458
|
+
catch: [SECTION,
|
|
459
|
+
{
|
|
460
|
+
onMount: (s: unknown, ele: HTMLElement) => {
|
|
461
|
+
mounts.push("mount fallback");
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
"fallback"
|
|
465
|
+
]
|
|
466
|
+
},
|
|
467
|
+
broken
|
|
468
|
+
]
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
await expect(mounts).toEqual(["mount fallback"]);
|
|
472
|
+
},
|
|
473
|
+
|
|
474
|
+
"onMount(): with catched component, returned vode's onMount fires and receives error": async () => {
|
|
475
|
+
const container = setup();
|
|
476
|
+
const mounts: string[] = [];
|
|
477
|
+
const caughtErrors: string[] = [];
|
|
478
|
+
const broken: any = () => { throw new Error("boom"); };
|
|
479
|
+
app(container, {}, () =>
|
|
480
|
+
[DIV,
|
|
481
|
+
{
|
|
482
|
+
catch: (s: unknown, err: Error) => {
|
|
483
|
+
caughtErrors.push(err.message);
|
|
484
|
+
return [SECTION,
|
|
485
|
+
{
|
|
486
|
+
onMount: (s: unknown, ele: HTMLElement) => {
|
|
487
|
+
mounts.push("mount fallback");
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
"fallback"
|
|
491
|
+
];
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
broken
|
|
495
|
+
]
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
await expect(mounts).toEqual(["mount fallback"]);
|
|
499
|
+
await expect(caughtErrors).toEqual(["boom"]);
|
|
500
|
+
},
|
|
501
|
+
|
|
502
|
+
"onMount(): with catched component, original element's onMount does NOT fire when error caused replacement": async () => {
|
|
503
|
+
const container = setup();
|
|
504
|
+
const logs: string[] = [];
|
|
505
|
+
const broken: any = () => { throw new Error("boom"); };
|
|
506
|
+
app(container, {}, () =>
|
|
507
|
+
[DIV,
|
|
508
|
+
{
|
|
509
|
+
catch: [ARTICLE,
|
|
510
|
+
{
|
|
511
|
+
onMount: (s: unknown, ele: HTMLElement) => {
|
|
512
|
+
logs.push("mount fallback");
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
"fallback"
|
|
516
|
+
]
|
|
517
|
+
},
|
|
518
|
+
[SECTION,
|
|
519
|
+
{
|
|
520
|
+
onMount: (s: unknown, ele: HTMLElement) => {
|
|
521
|
+
logs.push("mount original section");
|
|
522
|
+
},
|
|
523
|
+
onUnmount: (s: unknown, ele: HTMLElement) => {
|
|
524
|
+
logs.push("unmount original section");
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
broken
|
|
528
|
+
]
|
|
529
|
+
]
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
// SECTION never finishes mounting (its child broke), so its onMount must not fire.
|
|
533
|
+
// The catch on DIV replaces the broken subtree with ARTICLE whose onMount must fire.
|
|
534
|
+
await expect(logs).toEqual(["mount fallback"]);
|
|
535
|
+
},
|
|
536
|
+
|
|
451
537
|
"onUnmount(): called when node is removed from the DOM": async () => {
|
|
452
538
|
const container = setup();
|
|
453
539
|
const unmounts: string[] = [];
|
|
@@ -1173,121 +1259,6 @@ export default {
|
|
|
1173
1259
|
await expect(fired).toEqual(["unmount B"]);
|
|
1174
1260
|
},
|
|
1175
1261
|
|
|
1176
|
-
"onMount() + onUnmount: symmetry of calls": async () => {
|
|
1177
|
-
const container = setup();
|
|
1178
|
-
const state = createState({
|
|
1179
|
-
startTime: 0,
|
|
1180
|
-
inputReady: false,
|
|
1181
|
-
showInput: true,
|
|
1182
|
-
showTimer: true
|
|
1183
|
-
});
|
|
1184
|
-
type State = typeof state;
|
|
1185
|
-
const logs: string[] = [];
|
|
1186
|
-
|
|
1187
|
-
const patch = app<State>(container, state, (s) => {
|
|
1188
|
-
return [DIV,
|
|
1189
|
-
s.showInput && [INPUT, {
|
|
1190
|
-
type: 'text',
|
|
1191
|
-
placeholder: 'Auto-focused on mount',
|
|
1192
|
-
onMount: (s: State, ele: HTMLElement) => {
|
|
1193
|
-
logs.push('Input mounted');
|
|
1194
|
-
return { inputReady: true };
|
|
1195
|
-
},
|
|
1196
|
-
onUnmount: (s: State, ele: HTMLElement) => {
|
|
1197
|
-
logs.push('Input removed');
|
|
1198
|
-
return { inputReady: false };
|
|
1199
|
-
}
|
|
1200
|
-
}],
|
|
1201
|
-
|
|
1202
|
-
s.showTimer && [P, {
|
|
1203
|
-
onMount: (s: State, ele: HTMLElement) => {
|
|
1204
|
-
logs.push('Timer started');
|
|
1205
|
-
return { startTime: Date.now() };
|
|
1206
|
-
},
|
|
1207
|
-
onUnmount: (s: State, ele: HTMLElement) => {
|
|
1208
|
-
logs.push('Timer removed');
|
|
1209
|
-
}
|
|
1210
|
-
}, 'Mount/unmount lifecycle demo']
|
|
1211
|
-
]
|
|
1212
|
-
}
|
|
1213
|
-
);
|
|
1214
|
-
|
|
1215
|
-
await expect(state.inputReady)
|
|
1216
|
-
.toEqual(true);
|
|
1217
|
-
await expect(state.startTime != 0)
|
|
1218
|
-
.toEqual(true);
|
|
1219
|
-
patch({ showInput: false });
|
|
1220
|
-
|
|
1221
|
-
await expect(
|
|
1222
|
-
async () => await expect(state.inputReady).toEqual(false, "expected: inputReady == false")
|
|
1223
|
-
).toSucceedAsync();
|
|
1224
|
-
|
|
1225
|
-
patch({ showTimer: false });
|
|
1226
|
-
|
|
1227
|
-
await expect(
|
|
1228
|
-
async () => await expect(container._vode.stats.syncRenderCount >= 4)
|
|
1229
|
-
.toEqual(true)
|
|
1230
|
-
).toSucceedAsync();
|
|
1231
|
-
|
|
1232
|
-
await expect(logs).toEqual([
|
|
1233
|
-
'Input mounted',
|
|
1234
|
-
'Timer started',
|
|
1235
|
-
'Input removed',
|
|
1236
|
-
'Timer removed'
|
|
1237
|
-
]);
|
|
1238
|
-
},
|
|
1239
|
-
|
|
1240
|
-
"onMount(): with catched component, replacement vode's onMount fires when error occurs": async () => {
|
|
1241
|
-
const container = setup();
|
|
1242
|
-
const mounts: string[] = [];
|
|
1243
|
-
const broken: any = () => { throw new Error("boom"); };
|
|
1244
|
-
app(container, {}, () =>
|
|
1245
|
-
[DIV,
|
|
1246
|
-
{
|
|
1247
|
-
catch: [SECTION,
|
|
1248
|
-
{
|
|
1249
|
-
onMount: (s: unknown, ele: HTMLElement) => {
|
|
1250
|
-
mounts.push("mount fallback");
|
|
1251
|
-
}
|
|
1252
|
-
},
|
|
1253
|
-
"fallback"
|
|
1254
|
-
]
|
|
1255
|
-
},
|
|
1256
|
-
broken
|
|
1257
|
-
]
|
|
1258
|
-
);
|
|
1259
|
-
|
|
1260
|
-
await expect(mounts).toEqual(["mount fallback"]);
|
|
1261
|
-
},
|
|
1262
|
-
|
|
1263
|
-
"onMount(): with catched component, returned vode's onMount fires and receives error": async () => {
|
|
1264
|
-
const container = setup();
|
|
1265
|
-
const mounts: string[] = [];
|
|
1266
|
-
const caughtErrors: string[] = [];
|
|
1267
|
-
const broken: any = () => { throw new Error("boom"); };
|
|
1268
|
-
app(container, {}, () =>
|
|
1269
|
-
[DIV,
|
|
1270
|
-
{
|
|
1271
|
-
catch: (s: unknown, err: Error) => {
|
|
1272
|
-
caughtErrors.push(err.message);
|
|
1273
|
-
return [SECTION,
|
|
1274
|
-
{
|
|
1275
|
-
onMount: (s: unknown, ele: HTMLElement) => {
|
|
1276
|
-
mounts.push("mount fallback");
|
|
1277
|
-
}
|
|
1278
|
-
},
|
|
1279
|
-
"fallback"
|
|
1280
|
-
];
|
|
1281
|
-
}
|
|
1282
|
-
},
|
|
1283
|
-
broken
|
|
1284
|
-
]
|
|
1285
|
-
);
|
|
1286
|
-
|
|
1287
|
-
await expect(mounts).toEqual(["mount fallback"]);
|
|
1288
|
-
await expect(caughtErrors).toEqual(["boom"]);
|
|
1289
|
-
},
|
|
1290
|
-
|
|
1291
1262
|
"onUnmount(): with catched component, replacement vode's onUnmount fires when removed": async () => {
|
|
1292
1263
|
const container = setup();
|
|
1293
1264
|
const unmounts: string[] = [];
|
|
@@ -1359,7 +1330,7 @@ export default {
|
|
|
1359
1330
|
await expect(unmounts).toEqual(["unmount span", "unmount p", "unmount article"]);
|
|
1360
1331
|
},
|
|
1361
1332
|
|
|
1362
|
-
"onMount()
|
|
1333
|
+
"onMount() + onUnmount(): with catched component, full lifecycle symmetry of catch replacement": async () => {
|
|
1363
1334
|
const container = setup();
|
|
1364
1335
|
const logs: string[] = [];
|
|
1365
1336
|
const state = createState({ show: true });
|
|
@@ -1390,38 +1361,144 @@ export default {
|
|
|
1390
1361
|
await expect(logs).toEqual(["mount article", "unmount article"]);
|
|
1391
1362
|
},
|
|
1392
1363
|
|
|
1393
|
-
"onMount()
|
|
1364
|
+
"onMount() + onUnmount: symmetry of calls": async () => {
|
|
1394
1365
|
const container = setup();
|
|
1366
|
+
const state = createState({
|
|
1367
|
+
startTime: 0,
|
|
1368
|
+
inputReady: false,
|
|
1369
|
+
showInput: true,
|
|
1370
|
+
showTimer: true
|
|
1371
|
+
});
|
|
1372
|
+
type State = typeof state;
|
|
1395
1373
|
const logs: string[] = [];
|
|
1396
|
-
|
|
1397
|
-
app(container,
|
|
1374
|
+
|
|
1375
|
+
const patch = app<State>(container, state, (s) => {
|
|
1376
|
+
return [DIV,
|
|
1377
|
+
s.showInput && [INPUT, {
|
|
1378
|
+
type: 'text',
|
|
1379
|
+
placeholder: 'Auto-focused on mount',
|
|
1380
|
+
onMount: (s: State, ele: HTMLElement) => {
|
|
1381
|
+
logs.push('Input mounted');
|
|
1382
|
+
return { inputReady: true };
|
|
1383
|
+
},
|
|
1384
|
+
onUnmount: (s: State, ele: HTMLElement) => {
|
|
1385
|
+
logs.push('Input removed');
|
|
1386
|
+
return { inputReady: false };
|
|
1387
|
+
}
|
|
1388
|
+
}],
|
|
1389
|
+
|
|
1390
|
+
s.showTimer && [P, {
|
|
1391
|
+
onMount: (s: State, ele: HTMLElement) => {
|
|
1392
|
+
logs.push('Timer started');
|
|
1393
|
+
return { startTime: Date.now() };
|
|
1394
|
+
},
|
|
1395
|
+
onUnmount: (s: State, ele: HTMLElement) => {
|
|
1396
|
+
logs.push('Timer removed');
|
|
1397
|
+
}
|
|
1398
|
+
}, 'Mount/unmount lifecycle demo']
|
|
1399
|
+
]
|
|
1400
|
+
}
|
|
1401
|
+
);
|
|
1402
|
+
|
|
1403
|
+
await expect(state.inputReady)
|
|
1404
|
+
.toEqual(true);
|
|
1405
|
+
await expect(state.startTime != 0)
|
|
1406
|
+
.toEqual(true);
|
|
1407
|
+
patch({ showInput: false });
|
|
1408
|
+
|
|
1409
|
+
await expect(
|
|
1410
|
+
async () => await expect(state.inputReady).toEqual(false, "expected: inputReady == false")
|
|
1411
|
+
).toSucceedAsync();
|
|
1412
|
+
|
|
1413
|
+
patch({ showTimer: false });
|
|
1414
|
+
|
|
1415
|
+
await expect(
|
|
1416
|
+
async () => await expect(container._vode.stats.syncRenderCount >= 4)
|
|
1417
|
+
.toEqual(true)
|
|
1418
|
+
).toSucceedAsync();
|
|
1419
|
+
|
|
1420
|
+
await expect(logs).toEqual([
|
|
1421
|
+
'Input mounted',
|
|
1422
|
+
'Timer started',
|
|
1423
|
+
'Input removed',
|
|
1424
|
+
'Timer removed'
|
|
1425
|
+
]);
|
|
1426
|
+
},
|
|
1427
|
+
|
|
1428
|
+
"onMount() + onUnmount(): Not called when DOM does not require element creation or removal (same TAGs)": async () => {
|
|
1429
|
+
const container = setup();
|
|
1430
|
+
const logs = <string[]>[];
|
|
1431
|
+
|
|
1432
|
+
const Comp: (name: string) => Component = (name: string) => () => [ARTICLE,
|
|
1398
1433
|
[DIV,
|
|
1399
1434
|
{
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
onMount: (s: unknown, ele: HTMLElement) => {
|
|
1403
|
-
logs.push("mount fallback");
|
|
1404
|
-
}
|
|
1405
|
-
},
|
|
1406
|
-
"fallback"
|
|
1407
|
-
]
|
|
1435
|
+
onMount: () => logs.push("mount " + name),
|
|
1436
|
+
onUnmount: () => logs.push("unmount " + name)
|
|
1408
1437
|
},
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1438
|
+
"Component " + name]
|
|
1439
|
+
];
|
|
1440
|
+
|
|
1441
|
+
const state = createState({ showB: false, showD: false });
|
|
1442
|
+
app<typeof state>(container, state, s => [DIV,
|
|
1443
|
+
// this way they both "share a slot"
|
|
1444
|
+
s.showB ? Comp("B") : Comp("A"),
|
|
1445
|
+
|
|
1446
|
+
// this way each component occupies its own "slot"
|
|
1447
|
+
!s.showD && Comp("C"),
|
|
1448
|
+
s.showD && Comp("D"),
|
|
1449
|
+
]);
|
|
1450
|
+
|
|
1451
|
+
await expect(container).toMatch(
|
|
1452
|
+
[DIV,
|
|
1453
|
+
[ARTICLE,
|
|
1454
|
+
[DIV, "Component A"],
|
|
1455
|
+
],
|
|
1456
|
+
[ARTICLE,
|
|
1457
|
+
[DIV, "Component C"],
|
|
1458
|
+
],
|
|
1420
1459
|
]
|
|
1421
1460
|
);
|
|
1461
|
+
await expect(logs).toEqual(["mount A", "mount C"]);
|
|
1422
1462
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
await expect(
|
|
1463
|
+
state.patch({ showB: true });
|
|
1464
|
+
|
|
1465
|
+
await expect(container).toMatch(
|
|
1466
|
+
[DIV,
|
|
1467
|
+
[ARTICLE,
|
|
1468
|
+
[DIV, "Component B"],
|
|
1469
|
+
],
|
|
1470
|
+
[ARTICLE,
|
|
1471
|
+
[DIV, "Component C"],
|
|
1472
|
+
],
|
|
1473
|
+
]
|
|
1474
|
+
);
|
|
1475
|
+
|
|
1476
|
+
// as both components result in the same structure
|
|
1477
|
+
// of element types the unmount of A
|
|
1478
|
+
// and mount of B does not occur
|
|
1479
|
+
await expect(logs).toEqual(["mount A", "mount C"]);
|
|
1480
|
+
|
|
1481
|
+
|
|
1482
|
+
state.patch({ showD: true });
|
|
1483
|
+
|
|
1484
|
+
await expect(container).toMatch(
|
|
1485
|
+
[DIV,
|
|
1486
|
+
[ARTICLE,
|
|
1487
|
+
[DIV, "Component B"],
|
|
1488
|
+
],
|
|
1489
|
+
[ARTICLE,
|
|
1490
|
+
[DIV, "Component D"],
|
|
1491
|
+
],
|
|
1492
|
+
]
|
|
1493
|
+
);
|
|
1494
|
+
|
|
1495
|
+
// when the components occupy different slots in the vdom
|
|
1496
|
+
// their mount/unmount functions are called
|
|
1497
|
+
await expect(logs).toEqual([
|
|
1498
|
+
"mount A",
|
|
1499
|
+
"mount C",
|
|
1500
|
+
"unmount C",
|
|
1501
|
+
"mount D",
|
|
1502
|
+
]);
|
|
1426
1503
|
},
|
|
1427
1504
|
}
|