@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
|
@@ -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
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { delay, expect } from "./helper";
|
|
2
|
-
import { app, createState, DIV } from "../index";
|
|
2
|
+
import { app, ContainerNode, createState, DIV } from "../index";
|
|
3
3
|
|
|
4
4
|
function setup() {
|
|
5
5
|
const root = document.createElement("div");
|
|
6
6
|
const container = document.createElement("div");
|
|
7
7
|
root.appendChild(container);
|
|
8
|
-
return container;
|
|
8
|
+
return container as unknown as ContainerNode;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export default {
|
|
@@ -36,8 +36,11 @@ export default {
|
|
|
36
36
|
await expect(state.phase).toEqual("start");
|
|
37
37
|
|
|
38
38
|
state.patch(async function* () {
|
|
39
|
+
await expect(container._vode.stats.syncRenderPatchCount).toEqual(0);
|
|
39
40
|
yield { phase: "working", value: 10 };
|
|
41
|
+
await expect(container._vode.stats.syncRenderPatchCount).toEqual(1);
|
|
40
42
|
yield { phase: "almost", value: 20 };
|
|
43
|
+
await expect(container._vode.stats.syncRenderPatchCount).toEqual(2);
|
|
41
44
|
return { phase: "done", value: 30 };
|
|
42
45
|
}());
|
|
43
46
|
|
|
@@ -83,4 +86,107 @@ export default {
|
|
|
83
86
|
await expect(state.x).toEqual(10);
|
|
84
87
|
await expect(state.y).toEqual(20);
|
|
85
88
|
},
|
|
89
|
+
|
|
90
|
+
"patch(): returns Promise for generator functions, can be awaited": async () => {
|
|
91
|
+
const container = setup();
|
|
92
|
+
const state = createState({ count: 0 });
|
|
93
|
+
app<typeof state>(container, state, (s) => [DIV, String(s.count)]);
|
|
94
|
+
|
|
95
|
+
await expect(container._vode.stats.patchCount).toEqual(0);
|
|
96
|
+
const result = state.patch(function* () {
|
|
97
|
+
yield { count: 1 };
|
|
98
|
+
return { count: 2 };
|
|
99
|
+
});
|
|
100
|
+
await expect(container._vode.stats.patchCount).toEqual(1);
|
|
101
|
+
|
|
102
|
+
expect(result).toBeA("object");
|
|
103
|
+
await expect(result instanceof Promise).toEqual(true);
|
|
104
|
+
|
|
105
|
+
await result;
|
|
106
|
+
await expect(container._vode.stats.patchCount).toEqual(3);
|
|
107
|
+
|
|
108
|
+
await expect(state.count).toEqual(2);
|
|
109
|
+
await expect(container).toMatch([DIV, "2"]);
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
"patch(): returns Promise for Promise patches, can be awaited": async () => {
|
|
113
|
+
const container = setup();
|
|
114
|
+
const state = createState({ msg: "before" });
|
|
115
|
+
app<typeof state>(container, state, (s) => [DIV, s.msg]);
|
|
116
|
+
|
|
117
|
+
const result = state.patch(Promise.resolve({ msg: "after" }));
|
|
118
|
+
|
|
119
|
+
expect(result).toBeA("object");
|
|
120
|
+
await expect(result instanceof Promise).toEqual(true);
|
|
121
|
+
|
|
122
|
+
await result;
|
|
123
|
+
|
|
124
|
+
await expect(state.msg).toEqual("after");
|
|
125
|
+
await expect(container).toMatch([DIV, "after"]);
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
"patch(): returns void for object patches": async () => {
|
|
129
|
+
const container = setup();
|
|
130
|
+
const state = createState({ x: 1 });
|
|
131
|
+
app<typeof state>(container, state, (s) => [DIV, String(s.x)]);
|
|
132
|
+
|
|
133
|
+
const result = state.patch({ x: 2 });
|
|
134
|
+
|
|
135
|
+
expect(result).toBeA("undefined");
|
|
136
|
+
|
|
137
|
+
await expect(state.x).toEqual(2);
|
|
138
|
+
await expect(container).toMatch([DIV, "2"]);
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
"patch(): forward promise error when one happens during patch": async () => {
|
|
142
|
+
const container = setup();
|
|
143
|
+
const state = createState({ msg: "before" });
|
|
144
|
+
app<typeof state>(container, state, (s) => [DIV, s.msg]);
|
|
145
|
+
|
|
146
|
+
const mockPromise = Promise.withResolvers<void>();
|
|
147
|
+
const promisePatchResult = state.patch(mockPromise.promise);
|
|
148
|
+
mockPromise.reject(new Error("promise error"));
|
|
149
|
+
|
|
150
|
+
let err = await expect(() => promisePatchResult)
|
|
151
|
+
.toFailAsync("promise (1) error expected");
|
|
152
|
+
expect(err.message).toEqual("promise error");
|
|
153
|
+
|
|
154
|
+
err = await expect(() => state.patch(async () => {
|
|
155
|
+
await delay(1);
|
|
156
|
+
throw new Error("promise error")
|
|
157
|
+
})).toFailAsync("promise (2) error expected");
|
|
158
|
+
expect(err.message).toEqual("promise error");
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
"patch(): forward generator error when one happens during patch": async () => {
|
|
162
|
+
const container = setup();
|
|
163
|
+
const state = createState({ msg: "before" });
|
|
164
|
+
app<typeof state>(container, state, (s) => [DIV, s.msg]);
|
|
165
|
+
|
|
166
|
+
const err = await expect(
|
|
167
|
+
() => state.patch(
|
|
168
|
+
async function* () {
|
|
169
|
+
yield {};
|
|
170
|
+
await delay(1);
|
|
171
|
+
yield {};
|
|
172
|
+
throw new Error("generator error");
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
).toFailAsync("generator error expected");
|
|
176
|
+
expect(err.message).toEqual("generator error");
|
|
177
|
+
},
|
|
178
|
+
"patch(): forward error when one happens during patch": async () => {
|
|
179
|
+
const container = setup();
|
|
180
|
+
const state = createState({ msg: "before" });
|
|
181
|
+
app<typeof state>(container, state, (s) => [DIV, s.msg]);
|
|
182
|
+
|
|
183
|
+
const err = await expect(
|
|
184
|
+
() => state.patch(
|
|
185
|
+
() => {
|
|
186
|
+
throw new Error("void error");
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
).toFailAsync("void error expected");
|
|
190
|
+
expect(err.message).toEqual("void error");
|
|
191
|
+
},
|
|
86
192
|
};
|
|
@@ -134,4 +134,12 @@ export default {
|
|
|
134
134
|
await expect(state.a.x?.z).toEqual("deep");
|
|
135
135
|
await expect(state.a.y).toEqual(1);
|
|
136
136
|
},
|
|
137
|
+
|
|
138
|
+
"StateContext.put() merges into existing object properties via Object.assign": async () => {
|
|
139
|
+
const state = createState({ items: { count: 0, name: "test", hidden: false } });
|
|
140
|
+
const ctx = context(state);
|
|
141
|
+
// Line 110-111: when existing value is object and new value is object, Object.assign merges
|
|
142
|
+
ctx.items.put({ count: 5 });
|
|
143
|
+
await expect(state.items).toEqual({ count: 5, name: "test", hidden: false });
|
|
144
|
+
},
|
|
137
145
|
};
|