@jskit-ai/shell-web 0.1.84 → 0.1.86
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/package.descriptor.mjs +6 -3
- package/package.json +2 -2
- package/src/client/components/ShellRouteTransition.vue +20 -1
- package/src/client/providers/ShellWebClientProvider.js +158 -17
- package/src/client/providers/appModuleLoadFailure.js +29 -0
- package/src/client/support/routeTransitionKey.js +22 -0
- package/templates/expected-existing/src/pages/home/index.vue +8 -2
- package/templates/src/pages/home/index.vue +6 -2
- package/templates/src/pages/home/settings.vue +36 -3
- package/test/bootstrapClaimContract.test.js +2 -2
- package/test/menuIcons.test.js +5 -0
- package/test/provider.test.js +201 -24
- package/test/settingsPlacementContract.test.js +59 -0
package/package.descriptor.mjs
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
packageVersion: 1,
|
|
3
3
|
packageId: "@jskit-ai/shell-web",
|
|
4
|
-
version: "0.1.
|
|
4
|
+
version: "0.1.86",
|
|
5
5
|
kind: "runtime",
|
|
6
6
|
description: "Web shell layout runtime with outlet-based placement contributions.",
|
|
7
7
|
dependsOn: [],
|
|
8
8
|
capabilities: {
|
|
9
9
|
provides: [
|
|
10
10
|
"runtime.web-placement",
|
|
11
|
-
"runtime.web-error"
|
|
11
|
+
"runtime.web-error",
|
|
12
|
+
"runtime.web-async-module-recovery"
|
|
12
13
|
],
|
|
13
14
|
requires: []
|
|
14
15
|
},
|
|
@@ -54,6 +55,8 @@ export default Object.freeze({
|
|
|
54
55
|
client: [
|
|
55
56
|
"runtime.web-placement.client",
|
|
56
57
|
"runtime.web-bootstrap.client",
|
|
58
|
+
"runtime.web-refresh.client",
|
|
59
|
+
"runtime.web-async-module-recovery.client",
|
|
57
60
|
"runtime.web-error.client",
|
|
58
61
|
"runtime.web-error.presentation-store.client"
|
|
59
62
|
]
|
|
@@ -291,7 +294,7 @@ export default Object.freeze({
|
|
|
291
294
|
dependencies: {
|
|
292
295
|
runtime: {
|
|
293
296
|
"@mdi/js": "^7.4.47",
|
|
294
|
-
"@jskit-ai/kernel": "0.1.
|
|
297
|
+
"@jskit-ai/kernel": "0.1.87"
|
|
295
298
|
},
|
|
296
299
|
dev: {}
|
|
297
300
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/shell-web",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.86",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@mdi/js": "^7.4.47",
|
|
29
|
-
"@jskit-ai/kernel": "0.1.
|
|
29
|
+
"@jskit-ai/kernel": "0.1.87"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"pinia": "^3.0.4",
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
normalizeMenuLinkPathname,
|
|
25
25
|
resolveMenuLinkTarget
|
|
26
26
|
} from "../support/menuLinkTarget.js";
|
|
27
|
+
import { resolveShellRouteTransitionKey } from "../support/routeTransitionKey.js";
|
|
27
28
|
|
|
28
29
|
const props = defineProps({
|
|
29
30
|
enabled: {
|
|
@@ -232,7 +233,16 @@ const routeTransitionName = computed(() => {
|
|
|
232
233
|
return "";
|
|
233
234
|
});
|
|
234
235
|
|
|
235
|
-
const routeTransitionKey = computed(() =>
|
|
236
|
+
const routeTransitionKey = computed(() => {
|
|
237
|
+
const routePathKey = routeTransitionName.value
|
|
238
|
+
? normalizeComparablePathname(route?.path || route?.fullPath || "/")
|
|
239
|
+
: "";
|
|
240
|
+
return resolveShellRouteTransitionKey({
|
|
241
|
+
routePathKey,
|
|
242
|
+
routeTransitionName: routeTransitionName.value,
|
|
243
|
+
surfaceId: currentSurfaceId.value
|
|
244
|
+
});
|
|
245
|
+
});
|
|
236
246
|
|
|
237
247
|
const swipeNavigationEnabled = computed(() =>
|
|
238
248
|
Boolean(
|
|
@@ -409,7 +419,12 @@ function isSwipeIgnoredTarget(target) {
|
|
|
409
419
|
<style scoped>
|
|
410
420
|
.shell-route-transition {
|
|
411
421
|
--shell-route-transition-distance: 100%;
|
|
422
|
+
align-items: stretch;
|
|
412
423
|
--shell-route-transition-opacity: 1;
|
|
424
|
+
display: flex;
|
|
425
|
+
flex: 1 1 auto;
|
|
426
|
+
flex-direction: column;
|
|
427
|
+
min-height: 0;
|
|
413
428
|
overflow-x: clip;
|
|
414
429
|
position: relative;
|
|
415
430
|
}
|
|
@@ -420,6 +435,10 @@ function isSwipeIgnoredTarget(target) {
|
|
|
420
435
|
|
|
421
436
|
.shell-route-transition__pane {
|
|
422
437
|
background: rgb(var(--v-theme-background));
|
|
438
|
+
display: flex;
|
|
439
|
+
flex: 1 1 auto;
|
|
440
|
+
flex-direction: column;
|
|
441
|
+
min-height: 0;
|
|
423
442
|
width: 100%;
|
|
424
443
|
}
|
|
425
444
|
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { getClientAppConfig } from "@jskit-ai/kernel/client";
|
|
2
|
+
import {
|
|
3
|
+
createAsyncModuleRecoveryState,
|
|
4
|
+
guardedReloadApp,
|
|
5
|
+
installAsyncModuleRecoveryHandlers,
|
|
6
|
+
isDynamicImportError,
|
|
7
|
+
notifyAsyncModuleLoadError
|
|
8
|
+
} from "@jskit-ai/kernel/client/asyncModuleRecovery";
|
|
9
|
+
import {
|
|
10
|
+
isMissingDynamicModule,
|
|
11
|
+
notifyDynamicImportFailure
|
|
12
|
+
} from "./appModuleLoadFailure.js";
|
|
2
13
|
import {
|
|
3
14
|
isRecord
|
|
4
15
|
} from "@jskit-ai/kernel/shared/support";
|
|
@@ -29,18 +40,10 @@ import { resolveBootstrapErrorStatusCode } from "../bootstrap/bootstrapErrorStat
|
|
|
29
40
|
const APP_PLACEMENT_MODULE_SPECIFIER = "/src/placement.js";
|
|
30
41
|
const APP_PLACEMENT_TOPOLOGY_MODULE_SPECIFIER = "/src/placementTopology.js";
|
|
31
42
|
const APP_ERROR_MODULE_SPECIFIER = "/src/error.js";
|
|
43
|
+
const ASYNC_MODULE_RELOAD_FAILURE_MESSAGE =
|
|
44
|
+
"The app cannot reload because the app server is not reachable. Restart the server, then click Reload.";
|
|
32
45
|
|
|
33
|
-
function
|
|
34
|
-
const message = String(error?.message || error || "");
|
|
35
|
-
return (
|
|
36
|
-
message.includes(moduleSpecifier) &&
|
|
37
|
-
(message.includes("Cannot find module") ||
|
|
38
|
-
message.includes("Failed to fetch dynamically imported module") ||
|
|
39
|
-
message.includes("ERR_MODULE_NOT_FOUND"))
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function loadAppPlacementDefinitions(logger) {
|
|
46
|
+
async function loadAppPlacementDefinitions(logger, asyncModuleRecoveryRuntime = null) {
|
|
44
47
|
try {
|
|
45
48
|
const moduleNamespace = await import("/src/placement.js");
|
|
46
49
|
const exported = moduleNamespace?.default;
|
|
@@ -60,6 +63,9 @@ async function loadAppPlacementDefinitions(logger) {
|
|
|
60
63
|
if (isMissingDynamicModule(error, APP_PLACEMENT_MODULE_SPECIFIER)) {
|
|
61
64
|
return [];
|
|
62
65
|
}
|
|
66
|
+
notifyDynamicImportFailure(asyncModuleRecoveryRuntime, error, {
|
|
67
|
+
label: "Placement config"
|
|
68
|
+
});
|
|
63
69
|
|
|
64
70
|
logger.warn(
|
|
65
71
|
{
|
|
@@ -73,7 +79,7 @@ async function loadAppPlacementDefinitions(logger) {
|
|
|
73
79
|
return [];
|
|
74
80
|
}
|
|
75
81
|
|
|
76
|
-
async function loadAppPlacementTopology(logger) {
|
|
82
|
+
async function loadAppPlacementTopology(logger, asyncModuleRecoveryRuntime = null) {
|
|
77
83
|
try {
|
|
78
84
|
const moduleNamespace = await import("/src/placementTopology.js");
|
|
79
85
|
const exported = moduleNamespace?.default;
|
|
@@ -82,6 +88,9 @@ async function loadAppPlacementTopology(logger) {
|
|
|
82
88
|
if (isMissingDynamicModule(error, APP_PLACEMENT_TOPOLOGY_MODULE_SPECIFIER)) {
|
|
83
89
|
return [];
|
|
84
90
|
}
|
|
91
|
+
notifyDynamicImportFailure(asyncModuleRecoveryRuntime, error, {
|
|
92
|
+
label: "Placement topology"
|
|
93
|
+
});
|
|
85
94
|
|
|
86
95
|
logger.warn(
|
|
87
96
|
{
|
|
@@ -127,7 +136,7 @@ function createErrorConfigToolkit(errorRuntime) {
|
|
|
127
136
|
});
|
|
128
137
|
}
|
|
129
138
|
|
|
130
|
-
async function loadAppErrorConfig(logger, errorRuntime) {
|
|
139
|
+
async function loadAppErrorConfig(logger, errorRuntime, asyncModuleRecoveryRuntime = null) {
|
|
131
140
|
try {
|
|
132
141
|
const moduleNamespace = await import("/src/error.js");
|
|
133
142
|
const exported = moduleNamespace?.default;
|
|
@@ -149,6 +158,9 @@ async function loadAppErrorConfig(logger, errorRuntime) {
|
|
|
149
158
|
if (isMissingDynamicModule(error, APP_ERROR_MODULE_SPECIFIER)) {
|
|
150
159
|
return {};
|
|
151
160
|
}
|
|
161
|
+
notifyDynamicImportFailure(asyncModuleRecoveryRuntime, error, {
|
|
162
|
+
label: "Error config"
|
|
163
|
+
});
|
|
152
164
|
|
|
153
165
|
logger.warn(
|
|
154
166
|
{
|
|
@@ -362,6 +374,10 @@ function installRouterErrorBridge(app, errorRuntime, logger) {
|
|
|
362
374
|
}
|
|
363
375
|
|
|
364
376
|
router.onError((error) => {
|
|
377
|
+
if (isDynamicImportError(error)) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
365
381
|
try {
|
|
366
382
|
errorRuntime.report({
|
|
367
383
|
source: "shell-web.router.on-error",
|
|
@@ -384,6 +400,121 @@ function installRouterErrorBridge(app, errorRuntime, logger) {
|
|
|
384
400
|
});
|
|
385
401
|
}
|
|
386
402
|
|
|
403
|
+
function createShellAsyncModuleRecoveryRuntime({
|
|
404
|
+
app,
|
|
405
|
+
logger = null
|
|
406
|
+
} = {}) {
|
|
407
|
+
if (!app || typeof app.has !== "function" || typeof app.make !== "function") {
|
|
408
|
+
throw new Error("createShellAsyncModuleRecoveryRuntime requires application has()/make().");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const runtimeLogger = logger || createSharedProviderLogger(app);
|
|
412
|
+
const state = createAsyncModuleRecoveryState();
|
|
413
|
+
let installedRecovery = null;
|
|
414
|
+
|
|
415
|
+
function errorRuntime() {
|
|
416
|
+
if (!app.has("runtime.web-error.client")) {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
const runtime = app.make("runtime.web-error.client");
|
|
420
|
+
return runtime && typeof runtime.report === "function" ? runtime : null;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function report(nextState = state) {
|
|
424
|
+
const runtime = errorRuntime();
|
|
425
|
+
if (!runtime) {
|
|
426
|
+
runtimeLogger.warn(
|
|
427
|
+
{
|
|
428
|
+
label: String(nextState?.label || "")
|
|
429
|
+
},
|
|
430
|
+
"Async module recovery could not report because the shell error runtime is unavailable."
|
|
431
|
+
);
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
return runtime.report({
|
|
437
|
+
source: "shell-web.async-module-recovery",
|
|
438
|
+
message: String(nextState?.message || "A required app module could not load."),
|
|
439
|
+
cause: nextState?.error || null,
|
|
440
|
+
intent: "app-recoverable",
|
|
441
|
+
severity: "warning",
|
|
442
|
+
dedupeKey: `shell-web.async-module-recovery.${Number(nextState?.attempt || 0)}`,
|
|
443
|
+
action: {
|
|
444
|
+
label: "Reload",
|
|
445
|
+
dismissOnRun: true,
|
|
446
|
+
handler() {
|
|
447
|
+
return reload();
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
} catch (error) {
|
|
452
|
+
runtimeLogger.error(
|
|
453
|
+
{
|
|
454
|
+
label: String(nextState?.label || ""),
|
|
455
|
+
error: String(error?.message || error || "unknown error")
|
|
456
|
+
},
|
|
457
|
+
"Async module recovery could not report through the shell error runtime."
|
|
458
|
+
);
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function reload() {
|
|
464
|
+
const reloaded = await guardedReloadApp({
|
|
465
|
+
state,
|
|
466
|
+
label: "App",
|
|
467
|
+
message: ASYNC_MODULE_RELOAD_FAILURE_MESSAGE
|
|
468
|
+
});
|
|
469
|
+
if (!reloaded) {
|
|
470
|
+
report(state);
|
|
471
|
+
}
|
|
472
|
+
return reloaded;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function notify(error = null, {
|
|
476
|
+
label = "App module",
|
|
477
|
+
message = "",
|
|
478
|
+
stale = isDynamicImportError(error)
|
|
479
|
+
} = {}) {
|
|
480
|
+
notifyAsyncModuleLoadError(state, error, {
|
|
481
|
+
label,
|
|
482
|
+
message,
|
|
483
|
+
retry: state.retry,
|
|
484
|
+
stale
|
|
485
|
+
});
|
|
486
|
+
return report(state);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function install() {
|
|
490
|
+
if (installedRecovery) {
|
|
491
|
+
return installedRecovery;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
installedRecovery = installAsyncModuleRecoveryHandlers({
|
|
495
|
+
state,
|
|
496
|
+
label: "App module",
|
|
497
|
+
router: app.has("jskit.client.router") ? app.make("jskit.client.router") : null,
|
|
498
|
+
onNotify: report
|
|
499
|
+
});
|
|
500
|
+
return installedRecovery;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function dispose() {
|
|
504
|
+
installedRecovery?.dispose?.();
|
|
505
|
+
installedRecovery = null;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return Object.freeze({
|
|
509
|
+
dispose,
|
|
510
|
+
install,
|
|
511
|
+
notify,
|
|
512
|
+
reload,
|
|
513
|
+
report,
|
|
514
|
+
state
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
387
518
|
class ShellWebClientProvider {
|
|
388
519
|
static id = "shell.web.client";
|
|
389
520
|
|
|
@@ -437,6 +568,12 @@ class ShellWebClientProvider {
|
|
|
437
568
|
logger
|
|
438
569
|
})
|
|
439
570
|
);
|
|
571
|
+
app.singleton("runtime.web-async-module-recovery.client", (scope) =>
|
|
572
|
+
createShellAsyncModuleRecoveryRuntime({
|
|
573
|
+
app: scope,
|
|
574
|
+
logger
|
|
575
|
+
})
|
|
576
|
+
);
|
|
440
577
|
app.singleton("runtime.web-error.presentation-store.client", () => createErrorPresentationStore());
|
|
441
578
|
app.singleton("runtime.web-error.client", (scope) =>
|
|
442
579
|
createErrorRuntime({
|
|
@@ -456,13 +593,17 @@ class ShellWebClientProvider {
|
|
|
456
593
|
}
|
|
457
594
|
|
|
458
595
|
const logger = createSharedProviderLogger(isRecord(app) ? app : null);
|
|
596
|
+
const errorRuntime = app.make("runtime.web-error.client");
|
|
597
|
+
const asyncModuleRecoveryRuntime = app.make("runtime.web-async-module-recovery.client");
|
|
598
|
+
asyncModuleRecoveryRuntime.install();
|
|
599
|
+
|
|
459
600
|
const placementRuntime = app.make("runtime.web-placement.client");
|
|
460
601
|
if (placementRuntime && typeof placementRuntime.replacePlacements === "function") {
|
|
461
|
-
const placementTopology = await loadAppPlacementTopology(logger);
|
|
602
|
+
const placementTopology = await loadAppPlacementTopology(logger, asyncModuleRecoveryRuntime);
|
|
462
603
|
if (typeof placementRuntime.replacePlacementTopology === "function") {
|
|
463
604
|
placementRuntime.replacePlacementTopology(placementTopology, { source: APP_PLACEMENT_TOPOLOGY_MODULE_SPECIFIER });
|
|
464
605
|
}
|
|
465
|
-
const placements = await loadAppPlacementDefinitions(logger);
|
|
606
|
+
const placements = await loadAppPlacementDefinitions(logger, asyncModuleRecoveryRuntime);
|
|
466
607
|
placementRuntime.replacePlacements(placements, { source: APP_PLACEMENT_MODULE_SPECIFIER });
|
|
467
608
|
const appConfig = getClientAppConfig();
|
|
468
609
|
const surfaceRuntime = app.has("jskit.client.surface.runtime")
|
|
@@ -486,8 +627,7 @@ class ShellWebClientProvider {
|
|
|
486
627
|
);
|
|
487
628
|
}
|
|
488
629
|
|
|
489
|
-
const
|
|
490
|
-
const errorConfig = await loadAppErrorConfig(logger, errorRuntime);
|
|
630
|
+
const errorConfig = await loadAppErrorConfig(logger, errorRuntime, asyncModuleRecoveryRuntime);
|
|
491
631
|
applyAppErrorConfig(errorRuntime, errorConfig);
|
|
492
632
|
|
|
493
633
|
const bootstrapRuntime = app.make("runtime.web-bootstrap.client");
|
|
@@ -514,6 +654,7 @@ class ShellWebClientProvider {
|
|
|
514
654
|
vueApp.provide("jskit.shell-web.runtime.web-placement.client", placementRuntime);
|
|
515
655
|
vueApp.provide("jskit.shell-web.runtime.web-refresh.client", refreshRuntime);
|
|
516
656
|
vueApp.provide("jskit.shell-web.runtime.web-error.client", errorRuntime);
|
|
657
|
+
vueApp.provide("jskit.shell-web.runtime.web-async-module-recovery.client", asyncModuleRecoveryRuntime);
|
|
517
658
|
vueApp.provide(
|
|
518
659
|
"jskit.shell-web.runtime.web-error.presentation-store.client",
|
|
519
660
|
errorPresentationStore
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isDynamicImportError
|
|
3
|
+
} from "@jskit-ai/kernel/client/asyncModuleRecovery";
|
|
4
|
+
|
|
5
|
+
function isMissingDynamicModule(error, moduleSpecifier) {
|
|
6
|
+
const message = String(error?.message || error || "");
|
|
7
|
+
return (
|
|
8
|
+
message.includes(moduleSpecifier) &&
|
|
9
|
+
(message.includes("Cannot find module") ||
|
|
10
|
+
message.includes("ERR_MODULE_NOT_FOUND"))
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function notifyDynamicImportFailure(asyncModuleRecoveryRuntime, error, {
|
|
15
|
+
label = "App module"
|
|
16
|
+
} = {}) {
|
|
17
|
+
if (!isDynamicImportError(error) || typeof asyncModuleRecoveryRuntime?.notify !== "function") {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
asyncModuleRecoveryRuntime.notify(error, {
|
|
22
|
+
label
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
isMissingDynamicModule,
|
|
28
|
+
notifyDynamicImportFailure
|
|
29
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function normalizeText(value = "") {
|
|
2
|
+
return String(value || "").trim();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function resolveShellRouteTransitionKey({
|
|
6
|
+
routePathKey = "",
|
|
7
|
+
routeTransitionName = "",
|
|
8
|
+
surfaceId = ""
|
|
9
|
+
} = {}) {
|
|
10
|
+
if (normalizeText(routeTransitionName)) {
|
|
11
|
+
return normalizeText(routePathKey) || "/";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const surfaceKey = normalizeText(surfaceId);
|
|
15
|
+
if (surfaceKey && surfaceKey !== "*") {
|
|
16
|
+
return `surface:${surfaceKey}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return "stable";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { resolveShellRouteTransitionKey };
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
<style scoped>
|
|
21
21
|
.generated-ui-screen {
|
|
22
|
-
--generated-ui-screen-title-size:
|
|
22
|
+
--generated-ui-screen-title-size: 2rem;
|
|
23
23
|
--generated-ui-screen-panel-padding: 1rem;
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
.home-start-screen__title {
|
|
32
32
|
font-size: var(--generated-ui-screen-title-size);
|
|
33
33
|
font-weight: 700;
|
|
34
|
-
letter-spacing:
|
|
34
|
+
letter-spacing: 0;
|
|
35
35
|
line-height: 1.08;
|
|
36
36
|
margin: 0 0 0.45rem;
|
|
37
37
|
}
|
|
@@ -39,4 +39,10 @@
|
|
|
39
39
|
.home-start-screen__panel {
|
|
40
40
|
padding: var(--generated-ui-screen-panel-padding);
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
@media (max-width: 640px) {
|
|
44
|
+
.generated-ui-screen {
|
|
45
|
+
--generated-ui-screen-title-size: 1.5rem;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
42
48
|
</style>
|
|
@@ -56,7 +56,7 @@ const health = computed(() => {
|
|
|
56
56
|
|
|
57
57
|
<style scoped>
|
|
58
58
|
.generated-ui-screen {
|
|
59
|
-
--generated-ui-screen-title-size:
|
|
59
|
+
--generated-ui-screen-title-size: 2rem;
|
|
60
60
|
--generated-ui-screen-panel-padding: 1rem;
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -70,7 +70,7 @@ const health = computed(() => {
|
|
|
70
70
|
.home-surface-screen__title {
|
|
71
71
|
font-size: var(--generated-ui-screen-title-size);
|
|
72
72
|
font-weight: 700;
|
|
73
|
-
letter-spacing:
|
|
73
|
+
letter-spacing: 0;
|
|
74
74
|
line-height: 1.1;
|
|
75
75
|
margin: 0 0 0.4rem;
|
|
76
76
|
}
|
|
@@ -90,6 +90,10 @@ const health = computed(() => {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
@media (max-width: 640px) {
|
|
93
|
+
.generated-ui-screen {
|
|
94
|
+
--generated-ui-screen-title-size: 1.5rem;
|
|
95
|
+
}
|
|
96
|
+
|
|
93
97
|
.home-surface-screen__header {
|
|
94
98
|
flex-direction: column;
|
|
95
99
|
}
|
|
@@ -14,7 +14,7 @@ import { RouterView } from "vue-router";
|
|
|
14
14
|
<v-sheet rounded="lg" border class="settings-shell__panel">
|
|
15
15
|
<div class="settings-shell__body">
|
|
16
16
|
<nav class="settings-shell__nav" aria-label="Home settings sections">
|
|
17
|
-
<v-list nav density="
|
|
17
|
+
<v-list nav density="compact" rounded="lg">
|
|
18
18
|
<ShellOutlet target="home-settings:primary-menu" />
|
|
19
19
|
</v-list>
|
|
20
20
|
</nav>
|
|
@@ -29,14 +29,14 @@ import { RouterView } from "vue-router";
|
|
|
29
29
|
|
|
30
30
|
<style scoped>
|
|
31
31
|
.generated-ui-screen {
|
|
32
|
-
--generated-ui-screen-title-size:
|
|
32
|
+
--generated-ui-screen-title-size: 1.85rem;
|
|
33
33
|
--generated-ui-screen-panel-padding: 1rem;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
.settings-shell__title {
|
|
37
37
|
font-size: var(--generated-ui-screen-title-size);
|
|
38
38
|
font-weight: 650;
|
|
39
|
-
letter-spacing:
|
|
39
|
+
letter-spacing: 0;
|
|
40
40
|
line-height: 1.15;
|
|
41
41
|
margin: 0 0 0.35rem;
|
|
42
42
|
}
|
|
@@ -53,10 +53,38 @@ import { RouterView } from "vue-router";
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
.settings-shell__content {
|
|
56
|
+
border-left: 1px solid rgba(var(--v-theme-on-surface), 0.18);
|
|
56
57
|
min-width: 0;
|
|
58
|
+
padding-left: 1rem;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.settings-shell__nav :deep(.v-list) {
|
|
62
|
+
padding: 0.35rem;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.settings-shell__nav :deep(.v-list-item) {
|
|
66
|
+
min-height: 48px;
|
|
67
|
+
padding-inline: 0.6rem 0.7rem;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.settings-shell__nav :deep(.v-list-item__prepend) {
|
|
71
|
+
margin-inline-end: 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.settings-shell__nav :deep(.v-list-item__spacer) {
|
|
75
|
+
min-width: 0.5rem;
|
|
76
|
+
width: 0.5rem;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.settings-shell__nav :deep(.v-list-item-title) {
|
|
80
|
+
line-height: 1.2;
|
|
57
81
|
}
|
|
58
82
|
|
|
59
83
|
@media (max-width: 960px) {
|
|
84
|
+
.generated-ui-screen {
|
|
85
|
+
--generated-ui-screen-title-size: 1.35rem;
|
|
86
|
+
}
|
|
87
|
+
|
|
60
88
|
.settings-shell__body {
|
|
61
89
|
grid-template-columns: 1fr;
|
|
62
90
|
}
|
|
@@ -72,5 +100,10 @@ import { RouterView } from "vue-router";
|
|
|
72
100
|
flex: 0 0 auto;
|
|
73
101
|
min-height: 48px;
|
|
74
102
|
}
|
|
103
|
+
|
|
104
|
+
.settings-shell__content {
|
|
105
|
+
border-left: 0;
|
|
106
|
+
padding-left: 0;
|
|
107
|
+
}
|
|
75
108
|
}
|
|
76
109
|
</style>
|
|
@@ -7,7 +7,7 @@ import descriptor from "../package.descriptor.mjs";
|
|
|
7
7
|
|
|
8
8
|
const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const PACKAGE_DIR = path.resolve(TEST_DIRECTORY, "..");
|
|
10
|
-
const CREATE_APP_TEMPLATE_DIR = path.resolve(PACKAGE_DIR, "..", "..", "tooling", "create-app", "templates", "
|
|
10
|
+
const CREATE_APP_TEMPLATE_DIR = path.resolve(PACKAGE_DIR, "..", "..", "tooling", "create-app", "templates", "minimal-shell");
|
|
11
11
|
|
|
12
12
|
function findFileMutation(id) {
|
|
13
13
|
const files = descriptor?.mutations?.files;
|
|
@@ -48,7 +48,7 @@ test("shell-web claims starter shell files as app-owned scaffolds", () => {
|
|
|
48
48
|
});
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
test("shell-web expected-existing starter files stay aligned with create-app
|
|
51
|
+
test("shell-web expected-existing starter files stay aligned with create-app minimal-shell", async () => {
|
|
52
52
|
const comparedFiles = [
|
|
53
53
|
"src/App.vue",
|
|
54
54
|
"src/pages/home.vue",
|
package/test/menuIcons.test.js
CHANGED
|
@@ -4,6 +4,7 @@ import test from "node:test";
|
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import {
|
|
7
|
+
mdiAccountKeyOutline,
|
|
7
8
|
mdiCogOutline,
|
|
8
9
|
mdiConsoleNetworkOutline,
|
|
9
10
|
mdiViewListOutline
|
|
@@ -29,6 +30,10 @@ test("shell-web leaves unknown explicit mdi metadata icons unchanged", () => {
|
|
|
29
30
|
assert.equal(resolveMenuLinkIcon({ icon: "mdi-not-a-real-supported-icon" }), "mdi-not-a-real-supported-icon");
|
|
30
31
|
});
|
|
31
32
|
|
|
33
|
+
test("shell-web accepts imported @mdi/js path constants as explicit menu icons", () => {
|
|
34
|
+
assert.equal(resolveMenuLinkIcon({ icon: mdiAccountKeyOutline }), mdiAccountKeyOutline);
|
|
35
|
+
});
|
|
36
|
+
|
|
32
37
|
test("shell-web menu icon resolution does not import the full mdi namespace", async () => {
|
|
33
38
|
const source = await readFile(path.join(PACKAGE_DIR, "src", "client", "lib", "menuIcons.js"), "utf8");
|
|
34
39
|
|
package/test/provider.test.js
CHANGED
|
@@ -5,9 +5,28 @@ import {
|
|
|
5
5
|
ShellWebClientProvider,
|
|
6
6
|
resolveAppPlacementTopologyExport
|
|
7
7
|
} from "../src/client/providers/ShellWebClientProvider.js";
|
|
8
|
+
import {
|
|
9
|
+
isMissingDynamicModule
|
|
10
|
+
} from "../src/client/providers/appModuleLoadFailure.js";
|
|
8
11
|
import { useShellErrorPresentationStore } from "../src/client/stores/useShellErrorPresentationStore.js";
|
|
9
12
|
const CLIENT_APP_CONFIG_GLOBAL_KEY = "__JSKIT_CLIENT_APP_CONFIG__";
|
|
10
13
|
|
|
14
|
+
async function withTemporaryGlobal(name, value, callback) {
|
|
15
|
+
const hadPrevious = Object.prototype.hasOwnProperty.call(globalThis, name);
|
|
16
|
+
const previousValue = globalThis[name];
|
|
17
|
+
globalThis[name] = value;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
return await callback();
|
|
21
|
+
} finally {
|
|
22
|
+
if (hadPrevious) {
|
|
23
|
+
globalThis[name] = previousValue;
|
|
24
|
+
} else {
|
|
25
|
+
delete globalThis[name];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
11
30
|
function setClientAppConfig(source = {}) {
|
|
12
31
|
const normalized =
|
|
13
32
|
source && typeof source === "object" && !Array.isArray(source) ? Object.freeze({ ...source }) : Object.freeze({});
|
|
@@ -17,7 +36,7 @@ function setClientAppConfig(source = {}) {
|
|
|
17
36
|
return normalized;
|
|
18
37
|
}
|
|
19
38
|
|
|
20
|
-
function createAppDouble({ surfaceRuntime = null, queryClient = null } = {}) {
|
|
39
|
+
function createAppDouble({ surfaceRuntime = null, queryClient = null, router = null } = {}) {
|
|
21
40
|
const singletons = new Map();
|
|
22
41
|
const singletonInstances = new Map();
|
|
23
42
|
const provided = [];
|
|
@@ -67,6 +86,9 @@ function createAppDouble({ surfaceRuntime = null, queryClient = null } = {}) {
|
|
|
67
86
|
if (token === "jskit.client.query-client") {
|
|
68
87
|
return Boolean(queryClient);
|
|
69
88
|
}
|
|
89
|
+
if (token === "jskit.client.router") {
|
|
90
|
+
return Boolean(router);
|
|
91
|
+
}
|
|
70
92
|
return singletons.has(token) || singletonInstances.has(token);
|
|
71
93
|
},
|
|
72
94
|
make(token) {
|
|
@@ -82,6 +104,9 @@ function createAppDouble({ surfaceRuntime = null, queryClient = null } = {}) {
|
|
|
82
104
|
if (token === "jskit.client.query-client" && queryClient) {
|
|
83
105
|
return queryClient;
|
|
84
106
|
}
|
|
107
|
+
if (token === "jskit.client.router" && router) {
|
|
108
|
+
return router;
|
|
109
|
+
}
|
|
85
110
|
if (singletonInstances.has(token)) {
|
|
86
111
|
return singletonInstances.get(token);
|
|
87
112
|
}
|
|
@@ -100,38 +125,45 @@ function createAppDouble({ surfaceRuntime = null, queryClient = null } = {}) {
|
|
|
100
125
|
}
|
|
101
126
|
|
|
102
127
|
async function withFetchStub(responsePayload, callback) {
|
|
103
|
-
|
|
104
|
-
globalThis.fetch = async () => ({
|
|
128
|
+
return withFetchImplementation(async () => ({
|
|
105
129
|
ok: true,
|
|
106
130
|
async json() {
|
|
107
131
|
return responsePayload;
|
|
108
132
|
}
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
return await callback();
|
|
113
|
-
} finally {
|
|
114
|
-
if (previousFetch === undefined) {
|
|
115
|
-
delete globalThis.fetch;
|
|
116
|
-
} else {
|
|
117
|
-
globalThis.fetch = previousFetch;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
133
|
+
}), callback);
|
|
120
134
|
}
|
|
121
135
|
|
|
122
136
|
async function withFetchImplementation(fetchImplementation, callback) {
|
|
123
|
-
|
|
124
|
-
|
|
137
|
+
return withTemporaryGlobal("fetch", fetchImplementation, callback);
|
|
138
|
+
}
|
|
125
139
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
140
|
+
async function withWindowDouble(windowObject, callback) {
|
|
141
|
+
return withTemporaryGlobal("window", windowObject, callback);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function createWindowDouble({ href = "https://example.test/home" } = {}) {
|
|
145
|
+
const listeners = new Map();
|
|
146
|
+
const reloadCalls = [];
|
|
147
|
+
const location = {
|
|
148
|
+
href,
|
|
149
|
+
reload() {
|
|
150
|
+
reloadCalls.push(location.href);
|
|
133
151
|
}
|
|
134
|
-
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
listeners,
|
|
156
|
+
reloadCalls,
|
|
157
|
+
location,
|
|
158
|
+
addEventListener(type, handler) {
|
|
159
|
+
listeners.set(type, handler);
|
|
160
|
+
},
|
|
161
|
+
removeEventListener(type, handler) {
|
|
162
|
+
if (listeners.get(type) === handler) {
|
|
163
|
+
listeners.delete(type);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
135
167
|
}
|
|
136
168
|
|
|
137
169
|
test("shell web client provider preserves append-only placement topology object exports", () => {
|
|
@@ -160,6 +192,23 @@ test("shell web client provider preserves append-only placement topology object
|
|
|
160
192
|
assert.deepEqual(warnings, []);
|
|
161
193
|
});
|
|
162
194
|
|
|
195
|
+
test("shell web boot import classifier does not swallow fetched dynamic module failures", () => {
|
|
196
|
+
assert.equal(
|
|
197
|
+
isMissingDynamicModule(
|
|
198
|
+
new Error("Cannot find module '/src/placement.js' imported from /src/main.js"),
|
|
199
|
+
"/src/placement.js"
|
|
200
|
+
),
|
|
201
|
+
true
|
|
202
|
+
);
|
|
203
|
+
assert.equal(
|
|
204
|
+
isMissingDynamicModule(
|
|
205
|
+
new Error("Failed to fetch dynamically imported module: http://localhost:5173/src/placement.js?t=123"),
|
|
206
|
+
"/src/placement.js"
|
|
207
|
+
),
|
|
208
|
+
false
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
|
|
163
212
|
test("shell web client provider binds runtime and injects it into Vue app", async () => {
|
|
164
213
|
await withFetchStub({ surfaceAccess: {} }, async () => {
|
|
165
214
|
const app = createAppDouble();
|
|
@@ -170,6 +219,7 @@ test("shell web client provider binds runtime and injects it into Vue app", asyn
|
|
|
170
219
|
assert.equal(app.singletons.has("runtime.web-error.client"), true);
|
|
171
220
|
assert.equal(app.singletons.has("runtime.web-error.presentation-store.client"), true);
|
|
172
221
|
assert.equal(app.singletons.has("runtime.web-refresh.client"), true);
|
|
222
|
+
assert.equal(app.singletons.has("runtime.web-async-module-recovery.client"), true);
|
|
173
223
|
|
|
174
224
|
await provider.boot(app);
|
|
175
225
|
assert.equal(app.plugins.length, 0);
|
|
@@ -179,6 +229,7 @@ test("shell web client provider binds runtime and injects it into Vue app", asyn
|
|
|
179
229
|
assert.equal(providedByKey.has("jskit.shell-web.runtime.web-placement.client"), true);
|
|
180
230
|
assert.equal(providedByKey.has("jskit.shell-web.runtime.web-refresh.client"), true);
|
|
181
231
|
assert.equal(providedByKey.has("jskit.shell-web.runtime.web-error.client"), true);
|
|
232
|
+
assert.equal(providedByKey.has("jskit.shell-web.runtime.web-async-module-recovery.client"), true);
|
|
182
233
|
assert.equal(providedByKey.has("jskit.shell-web.runtime.web-error.presentation-store.client"), true);
|
|
183
234
|
|
|
184
235
|
const placementRuntime = providedByKey.get("jskit.shell-web.runtime.web-placement.client");
|
|
@@ -194,6 +245,11 @@ test("shell web client provider binds runtime and injects it into Vue app", asyn
|
|
|
194
245
|
const refreshRuntime = providedByKey.get("jskit.shell-web.runtime.web-refresh.client");
|
|
195
246
|
assert.equal(typeof refreshRuntime.refresh, "function");
|
|
196
247
|
|
|
248
|
+
const asyncModuleRecoveryRuntime = providedByKey.get("jskit.shell-web.runtime.web-async-module-recovery.client");
|
|
249
|
+
assert.equal(typeof asyncModuleRecoveryRuntime.install, "function");
|
|
250
|
+
assert.equal(typeof asyncModuleRecoveryRuntime.notify, "function");
|
|
251
|
+
assert.equal(typeof asyncModuleRecoveryRuntime.reload, "function");
|
|
252
|
+
|
|
197
253
|
const errorStore = providedByKey.get("jskit.shell-web.runtime.web-error.presentation-store.client");
|
|
198
254
|
assert.equal(typeof errorStore.getState, "function");
|
|
199
255
|
assert.equal(typeof errorStore.present, "function");
|
|
@@ -260,6 +316,127 @@ test("shell refresh runtime reports recoverable retry errors as banners", async
|
|
|
260
316
|
});
|
|
261
317
|
});
|
|
262
318
|
|
|
319
|
+
test("shell web client provider reports dynamic import failures through async module recovery", async () => {
|
|
320
|
+
await withFetchStub({ surfaceAccess: {} }, async () => {
|
|
321
|
+
const routerErrorHandlers = [];
|
|
322
|
+
const replacedPaths = [];
|
|
323
|
+
const router = {
|
|
324
|
+
onError(handler) {
|
|
325
|
+
routerErrorHandlers.push(handler);
|
|
326
|
+
return () => null;
|
|
327
|
+
},
|
|
328
|
+
replace(fullPath) {
|
|
329
|
+
replacedPaths.push(fullPath);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
const app = createAppDouble({ router });
|
|
333
|
+
const provider = new ShellWebClientProvider();
|
|
334
|
+
provider.register(app);
|
|
335
|
+
await provider.boot(app);
|
|
336
|
+
|
|
337
|
+
assert.equal(routerErrorHandlers.length, 2);
|
|
338
|
+
|
|
339
|
+
const chunkError = new Error("Failed to fetch dynamically imported module: /assets/page.js");
|
|
340
|
+
for (const handler of routerErrorHandlers) {
|
|
341
|
+
handler(chunkError, {
|
|
342
|
+
fullPath: "/app/dashboard"
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const errorStore = app.make("runtime.web-error.presentation-store.client");
|
|
347
|
+
const state = errorStore.getState();
|
|
348
|
+
assert.equal(state.channels.banner.length, 1);
|
|
349
|
+
assert.equal(
|
|
350
|
+
state.channels.banner[0].message,
|
|
351
|
+
"Page did not download. The app may have been updated, or the network request failed."
|
|
352
|
+
);
|
|
353
|
+
assert.equal(state.channels.banner[0].severity, "warning");
|
|
354
|
+
assert.equal(state.channels.banner[0].action.label, "Reload");
|
|
355
|
+
assert.deepEqual(replacedPaths, []);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("shell web async module recovery reload action checks the document before reloading", async () => {
|
|
360
|
+
const windowObject = createWindowDouble();
|
|
361
|
+
await withWindowDouble(windowObject, async () => {
|
|
362
|
+
const fetchCalls = [];
|
|
363
|
+
await withFetchImplementation(async (input, init = {}) => {
|
|
364
|
+
fetchCalls.push({ input, init });
|
|
365
|
+
return {
|
|
366
|
+
ok: true,
|
|
367
|
+
status: 200,
|
|
368
|
+
async json() {
|
|
369
|
+
return { surfaceAccess: {} };
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}, async () => {
|
|
373
|
+
const routerErrorHandlers = [];
|
|
374
|
+
const router = {
|
|
375
|
+
onError(handler) {
|
|
376
|
+
routerErrorHandlers.push(handler);
|
|
377
|
+
return () => null;
|
|
378
|
+
},
|
|
379
|
+
replace() {}
|
|
380
|
+
};
|
|
381
|
+
const app = createAppDouble({ router });
|
|
382
|
+
const provider = new ShellWebClientProvider();
|
|
383
|
+
provider.register(app);
|
|
384
|
+
await provider.boot(app);
|
|
385
|
+
|
|
386
|
+
const chunkError = new Error("Failed to fetch dynamically imported module: /assets/page.js");
|
|
387
|
+
routerErrorHandlers[0](chunkError, {
|
|
388
|
+
fullPath: "/home/settings"
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const errorStore = app.make("runtime.web-error.presentation-store.client");
|
|
392
|
+
const banner = errorStore.getState().channels.banner[0];
|
|
393
|
+
await banner.action.handler();
|
|
394
|
+
|
|
395
|
+
assert.deepEqual(windowObject.reloadCalls, ["https://example.test/home"]);
|
|
396
|
+
const reloadFetch = fetchCalls.find((entry) => entry.input === "https://example.test/home");
|
|
397
|
+
assert.equal(reloadFetch?.init?.cache, "no-store");
|
|
398
|
+
assert.equal(reloadFetch?.init?.credentials, "same-origin");
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("shell web async module recovery keeps the page alive when reload check fails", async () => {
|
|
404
|
+
const windowObject = createWindowDouble();
|
|
405
|
+
await withWindowDouble(windowObject, async () => {
|
|
406
|
+
await withFetchImplementation(async (input) => {
|
|
407
|
+
if (input === "https://example.test/home") {
|
|
408
|
+
throw new TypeError("Failed to fetch");
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
ok: true,
|
|
412
|
+
status: 200,
|
|
413
|
+
async json() {
|
|
414
|
+
return { surfaceAccess: {} };
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
}, async () => {
|
|
418
|
+
const app = createAppDouble();
|
|
419
|
+
const provider = new ShellWebClientProvider();
|
|
420
|
+
provider.register(app);
|
|
421
|
+
await provider.boot(app);
|
|
422
|
+
|
|
423
|
+
const recoveryRuntime = app.make("runtime.web-async-module-recovery.client");
|
|
424
|
+
const reloaded = await recoveryRuntime.reload();
|
|
425
|
+
|
|
426
|
+
assert.equal(reloaded, false);
|
|
427
|
+
assert.deepEqual(windowObject.reloadCalls, []);
|
|
428
|
+
const errorStore = app.make("runtime.web-error.presentation-store.client");
|
|
429
|
+
const state = errorStore.getState();
|
|
430
|
+
assert.equal(state.channels.banner.length, 1);
|
|
431
|
+
assert.equal(
|
|
432
|
+
state.channels.banner[0].message,
|
|
433
|
+
"The app cannot reload because the app server is not reachable. Restart the server, then click Reload."
|
|
434
|
+
);
|
|
435
|
+
assert.equal(state.channels.banner[0].action.label, "Reload");
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
263
440
|
test("shell web client provider resolves surface config from client app config", async () => {
|
|
264
441
|
setClientAppConfig({
|
|
265
442
|
tenancyMode: "workspaces",
|
|
@@ -5,6 +5,7 @@ import { readFile } from "node:fs/promises";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { assertGeneratedUiSourceContract } from "@jskit-ai/kernel/shared/support/generatedUiContract";
|
|
7
7
|
import descriptor from "../package.descriptor.mjs";
|
|
8
|
+
import { resolveShellRouteTransitionKey } from "../src/client/support/routeTransitionKey.js";
|
|
8
9
|
|
|
9
10
|
const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
10
11
|
const PACKAGE_DIR = path.resolve(TEST_DIRECTORY, "..");
|
|
@@ -37,6 +38,11 @@ function readTopology(id = "", owner = "") {
|
|
|
37
38
|
: [];
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
function readClientContainerTokens() {
|
|
42
|
+
const tokens = descriptor?.metadata?.apiSummary?.containerTokens?.client;
|
|
43
|
+
return Array.isArray(tokens) ? tokens : [];
|
|
44
|
+
}
|
|
45
|
+
|
|
40
46
|
function findFileMutation(id) {
|
|
41
47
|
const files = descriptor?.mutations?.files;
|
|
42
48
|
return Array.isArray(files)
|
|
@@ -159,10 +165,54 @@ test("shell-web route transition keeps mobile route motion placement-driven", as
|
|
|
159
165
|
assert.match(source, /router\.push\(nextEntry\.href\)/);
|
|
160
166
|
assert.match(source, /isSwipeIgnoredTarget/);
|
|
161
167
|
assert.match(source, /touch-action:\s*pan-y/);
|
|
168
|
+
assert.match(source, /\.shell-route-transition\s*\{[\s\S]*display:\s*flex/);
|
|
169
|
+
assert.match(source, /\.shell-route-transition\s*\{[\s\S]*min-height:\s*0/);
|
|
170
|
+
assert.match(source, /\.shell-route-transition__pane\s*\{[\s\S]*flex:\s*1 1 auto/);
|
|
171
|
+
assert.match(source, /\.shell-route-transition__pane\s*\{[\s\S]*min-height:\s*0/);
|
|
162
172
|
assert.match(source, /transitionDirection\.value = nextIndex > previousIndex \? "forward" : "reverse"/);
|
|
173
|
+
assert.match(
|
|
174
|
+
source,
|
|
175
|
+
/const routeTransitionKey = computed\(\(\) => \{[\s\S]*const routePathKey = routeTransitionName\.value[\s\S]*normalizeComparablePathname\(route\?\.path \|\| route\?\.fullPath \|\| "\/"\)[\s\S]*resolveShellRouteTransitionKey\(\{[\s\S]*routeTransitionName: routeTransitionName\.value,[\s\S]*surfaceId: currentSurfaceId\.value[\s\S]*\}\);[\s\S]*\}\);/
|
|
176
|
+
);
|
|
163
177
|
assert.match(source, /prefers-reduced-motion:\s*reduce/);
|
|
164
178
|
});
|
|
165
179
|
|
|
180
|
+
test("shell-web route transition key preserves no-motion surfaces and animated path transitions", () => {
|
|
181
|
+
const previewSurfaceKey = "surface:vibe64-preview";
|
|
182
|
+
assert.equal(
|
|
183
|
+
resolveShellRouteTransitionKey({
|
|
184
|
+
routeTransitionName: "",
|
|
185
|
+
routePathKey: "/preview",
|
|
186
|
+
surfaceId: "vibe64-preview"
|
|
187
|
+
}),
|
|
188
|
+
previewSurfaceKey
|
|
189
|
+
);
|
|
190
|
+
assert.equal(
|
|
191
|
+
resolveShellRouteTransitionKey({
|
|
192
|
+
routeTransitionName: "",
|
|
193
|
+
routePathKey: "/preview/dashboard",
|
|
194
|
+
surfaceId: "vibe64-preview"
|
|
195
|
+
}),
|
|
196
|
+
previewSurfaceKey
|
|
197
|
+
);
|
|
198
|
+
assert.equal(
|
|
199
|
+
resolveShellRouteTransitionKey({
|
|
200
|
+
routeTransitionName: "shell-route-slide-forward",
|
|
201
|
+
routePathKey: "/dashboard",
|
|
202
|
+
surfaceId: "home"
|
|
203
|
+
}),
|
|
204
|
+
"/dashboard"
|
|
205
|
+
);
|
|
206
|
+
assert.equal(
|
|
207
|
+
resolveShellRouteTransitionKey({
|
|
208
|
+
routeTransitionName: "",
|
|
209
|
+
routePathKey: "/dashboard",
|
|
210
|
+
surfaceId: "*"
|
|
211
|
+
}),
|
|
212
|
+
"stable"
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
|
|
166
216
|
test("shell-web settings landing page redirects to the starter child page", async () => {
|
|
167
217
|
const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "pages", "home", "settings", "index.vue"), "utf8");
|
|
168
218
|
|
|
@@ -220,6 +270,15 @@ test("shell-web placement topology seeds global actions as a semantic shell plac
|
|
|
220
270
|
});
|
|
221
271
|
|
|
222
272
|
test("shell-web descriptor metadata advertises adaptive shell outlets, default links, and installs the scaffold page", () => {
|
|
273
|
+
assert.deepEqual(readClientContainerTokens(), [
|
|
274
|
+
"runtime.web-placement.client",
|
|
275
|
+
"runtime.web-bootstrap.client",
|
|
276
|
+
"runtime.web-refresh.client",
|
|
277
|
+
"runtime.web-async-module-recovery.client",
|
|
278
|
+
"runtime.web-error.client",
|
|
279
|
+
"runtime.web-error.presentation-store.client"
|
|
280
|
+
]);
|
|
281
|
+
|
|
223
282
|
assert.deepEqual(
|
|
224
283
|
readOutlets("shell-layout:primary-bottom-nav"),
|
|
225
284
|
[
|