@jskit-ai/shell-web 0.1.53 → 0.1.55
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 +7 -2
- package/package.json +7 -2
- package/src/client/bootstrap/bootstrapErrorStatus.js +6 -0
- package/src/client/bootstrap/bootstrapPayloadHandlerRegistry.js +68 -0
- package/src/client/bootstrap/index.js +6 -0
- package/src/client/components/ShellLayout.vue +15 -3
- package/src/client/index.js +5 -0
- package/src/client/providers/ShellWebClientProvider.js +47 -2
- package/src/client/runtime/bootstrapRuntime.js +195 -0
- package/templates/src/components/ShellLayout.vue +15 -3
- package/templates/src/pages/home/index.vue +12 -2
- package/templates/src/placement.js +1 -2
- package/test/bootstrapRuntime.test.js +187 -0
- package/test/placementRegistry.test.js +3 -3
- package/test/provider.test.js +136 -65
- package/test/settingsPlacementContract.test.js +1 -1
package/package.descriptor.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.55",
|
|
5
5
|
kind: "runtime",
|
|
6
6
|
description: "Web shell layout runtime with outlet-based placement contributions.",
|
|
7
7
|
dependsOn: [],
|
|
@@ -39,12 +39,17 @@ export default Object.freeze({
|
|
|
39
39
|
{
|
|
40
40
|
subpath: "./client/error",
|
|
41
41
|
summary: "Exports default error policy and runtime error reporter hook."
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
subpath: "./client/bootstrap",
|
|
45
|
+
summary: "Exports the shared client bootstrap handler registry used to extend /api/bootstrap handling."
|
|
42
46
|
}
|
|
43
47
|
],
|
|
44
48
|
containerTokens: {
|
|
45
49
|
server: [],
|
|
46
50
|
client: [
|
|
47
51
|
"runtime.web-placement.client",
|
|
52
|
+
"runtime.web-bootstrap.client",
|
|
48
53
|
"runtime.web-error.client",
|
|
49
54
|
"runtime.web-error.presentation-store.client",
|
|
50
55
|
"shell.web.query-client"
|
|
@@ -117,7 +122,7 @@ export default Object.freeze({
|
|
|
117
122
|
runtime: {
|
|
118
123
|
"@mdi/js": "^7.4.47",
|
|
119
124
|
"@tanstack/vue-query": "^5.90.5",
|
|
120
|
-
"@jskit-ai/kernel": "0.1.
|
|
125
|
+
"@jskit-ai/kernel": "0.1.56",
|
|
121
126
|
"vuetify": "^4.0.0"
|
|
122
127
|
},
|
|
123
128
|
dev: {}
|
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.55",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"./client": "./src/client/index.js",
|
|
10
10
|
"./client/error": "./src/client/error/index.js",
|
|
11
11
|
"./client/placement": "./src/client/placement/index.js",
|
|
12
|
+
"./client/bootstrap": "./src/client/bootstrap/index.js",
|
|
12
13
|
"./server/support/localLinkItemScaffolds": "./src/server/support/localLinkItemScaffolds.js",
|
|
13
14
|
"./client/navigation/linkResolver": "./src/client/navigation/linkResolver.js",
|
|
14
15
|
"./client/components/ShellLayout": "./src/client/components/ShellLayout.vue",
|
|
@@ -24,8 +25,12 @@
|
|
|
24
25
|
"dependencies": {
|
|
25
26
|
"@mdi/js": "^7.4.47",
|
|
26
27
|
"@tanstack/vue-query": "^5.90.5",
|
|
27
|
-
"@jskit-ai/kernel": "0.1.
|
|
28
|
+
"@jskit-ai/kernel": "0.1.56",
|
|
28
29
|
"pinia": "^3.0.4",
|
|
29
30
|
"vuetify": "^4.0.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"vue": "^3.5.13",
|
|
34
|
+
"vue-router": "^5.0.4"
|
|
30
35
|
}
|
|
31
36
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const BOOTSTRAP_PAYLOAD_HANDLER_TAG = "runtime.web-bootstrap.handlers.client";
|
|
2
|
+
|
|
3
|
+
function assertTaggableApp(app, context = "bootstrap payload handler registry") {
|
|
4
|
+
if (!app || typeof app.singleton !== "function" || typeof app.tag !== "function") {
|
|
5
|
+
throw new Error(`${context} requires application singleton()/tag().`);
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function registerBootstrapPayloadHandler(app, token, factory) {
|
|
10
|
+
assertTaggableApp(app, "registerBootstrapPayloadHandler");
|
|
11
|
+
app.singleton(token, factory);
|
|
12
|
+
app.tag(token, BOOTSTRAP_PAYLOAD_HANDLER_TAG);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeBootstrapPayloadHandler(entry) {
|
|
16
|
+
if (typeof entry === "function") {
|
|
17
|
+
return Object.freeze({
|
|
18
|
+
handlerId: String(entry.name || "anonymous"),
|
|
19
|
+
order: 0,
|
|
20
|
+
applyBootstrapPayload: entry
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!entry || typeof entry !== "object" || typeof entry.applyBootstrapPayload !== "function") {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return Object.freeze({
|
|
29
|
+
...entry,
|
|
30
|
+
handlerId: String(entry.handlerId || "anonymous"),
|
|
31
|
+
order: Number.isFinite(entry.order) ? Number(entry.order) : 0
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function resolveBootstrapPayloadHandlers(scope) {
|
|
36
|
+
if (!scope || typeof scope.resolveTag !== "function") {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const rawEntries = scope.resolveTag(BOOTSTRAP_PAYLOAD_HANDLER_TAG);
|
|
41
|
+
const queue = Array.isArray(rawEntries) ? [...rawEntries] : [rawEntries];
|
|
42
|
+
const entries = [];
|
|
43
|
+
|
|
44
|
+
while (queue.length > 0) {
|
|
45
|
+
const entry = queue.shift();
|
|
46
|
+
if (Array.isArray(entry)) {
|
|
47
|
+
queue.push(...entry);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const normalized = normalizeBootstrapPayloadHandler(entry);
|
|
51
|
+
if (normalized) {
|
|
52
|
+
entries.push(normalized);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return entries.sort((left, right) => {
|
|
57
|
+
if (left.order !== right.order) {
|
|
58
|
+
return left.order - right.order;
|
|
59
|
+
}
|
|
60
|
+
return left.handlerId.localeCompare(right.handlerId);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
BOOTSTRAP_PAYLOAD_HANDLER_TAG,
|
|
66
|
+
registerBootstrapPayloadHandler,
|
|
67
|
+
resolveBootstrapPayloadHandlers
|
|
68
|
+
};
|
|
@@ -64,9 +64,9 @@ const { drawerOpen, toggleDrawer, resolvedSurface, resolvedSurfaceLabel } = useS
|
|
|
64
64
|
</v-navigation-drawer>
|
|
65
65
|
|
|
66
66
|
<v-main class="bg-background">
|
|
67
|
-
<v-container fluid class="
|
|
68
|
-
<h1 class="text-h5
|
|
69
|
-
<p class="text-body-2 text-medium-emphasis
|
|
67
|
+
<v-container fluid class="shell-layout__content">
|
|
68
|
+
<h1 v-if="title" class="shell-layout__title text-h5">{{ title }}</h1>
|
|
69
|
+
<p v-if="subtitle" class="shell-layout__subtitle text-body-2 text-medium-emphasis">{{ subtitle }}</p>
|
|
70
70
|
<slot />
|
|
71
71
|
</v-container>
|
|
72
72
|
</v-main>
|
|
@@ -77,4 +77,16 @@ const { drawerOpen, toggleDrawer, resolvedSurface, resolvedSurfaceLabel } = useS
|
|
|
77
77
|
.shell-layout {
|
|
78
78
|
min-height: 72vh;
|
|
79
79
|
}
|
|
80
|
+
|
|
81
|
+
.shell-layout__content {
|
|
82
|
+
padding: 0.75rem 1rem 1rem;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.shell-layout__title {
|
|
86
|
+
margin-bottom: 0.25rem;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.shell-layout__subtitle {
|
|
90
|
+
margin-bottom: 0.75rem;
|
|
91
|
+
}
|
|
80
92
|
</style>
|
package/src/client/index.js
CHANGED
|
@@ -16,6 +16,11 @@ export { default as ShellTabLinkItem } from "./components/ShellTabLinkItem.vue";
|
|
|
16
16
|
export { useShellLayoutState } from "./composables/useShellLayoutState.js";
|
|
17
17
|
export { useShellLayoutStore } from "./stores/useShellLayoutStore.js";
|
|
18
18
|
export { useShellErrorPresentationStore } from "./stores/useShellErrorPresentationStore.js";
|
|
19
|
+
export {
|
|
20
|
+
BOOTSTRAP_PAYLOAD_HANDLER_TAG,
|
|
21
|
+
registerBootstrapPayloadHandler,
|
|
22
|
+
resolveBootstrapPayloadHandlers
|
|
23
|
+
} from "./bootstrap/index.js";
|
|
19
24
|
|
|
20
25
|
const clientProviders = Object.freeze([ShellWebClientProvider]);
|
|
21
26
|
|
|
@@ -24,6 +24,9 @@ import {
|
|
|
24
24
|
import { createWebPlacementRuntime } from "../placement/runtime.js";
|
|
25
25
|
import { useShellErrorPresentationStore } from "../stores/useShellErrorPresentationStore.js";
|
|
26
26
|
import { buildSurfaceConfigContext } from "../placement/surfaceContext.js";
|
|
27
|
+
import { createShellBootstrapRuntime } from "../runtime/bootstrapRuntime.js";
|
|
28
|
+
import { registerBootstrapPayloadHandler } from "../bootstrap/bootstrapPayloadHandlerRegistry.js";
|
|
29
|
+
import { resolveBootstrapErrorStatusCode } from "../bootstrap/bootstrapErrorStatus.js";
|
|
27
30
|
|
|
28
31
|
// Keep this constant for diagnostics, but keep import() below as a literal string so Vite can statically analyze it.
|
|
29
32
|
const APP_PLACEMENT_MODULE_SPECIFIER = "/src/placement.js";
|
|
@@ -229,12 +232,49 @@ class ShellWebClientProvider {
|
|
|
229
232
|
static id = "shell.web.client";
|
|
230
233
|
|
|
231
234
|
register(app) {
|
|
232
|
-
if (!app || typeof app.singleton !== "function") {
|
|
233
|
-
throw new Error("ShellWebClientProvider requires application singleton().");
|
|
235
|
+
if (!app || typeof app.singleton !== "function" || typeof app.tag !== "function") {
|
|
236
|
+
throw new Error("ShellWebClientProvider requires application singleton()/tag().");
|
|
234
237
|
}
|
|
235
238
|
|
|
236
239
|
const logger = createSharedProviderLogger(isRecord(app) ? app : null);
|
|
240
|
+
registerBootstrapPayloadHandler(app, "shell.web.bootstrap.surfaceAccessHandler", () =>
|
|
241
|
+
Object.freeze({
|
|
242
|
+
handlerId: "shell.web.bootstrap.surfaceAccess",
|
|
243
|
+
order: 0,
|
|
244
|
+
applyBootstrapPayload({ payload = {}, placementRuntime, source } = {}) {
|
|
245
|
+
placementRuntime.setContext(
|
|
246
|
+
{
|
|
247
|
+
surfaceAccess:
|
|
248
|
+
payload?.surfaceAccess && typeof payload.surfaceAccess === "object" ? payload.surfaceAccess : {}
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
source
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
},
|
|
255
|
+
handleBootstrapError({ error, placementRuntime, source } = {}) {
|
|
256
|
+
if (resolveBootstrapErrorStatusCode(error) !== 401) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
placementRuntime.setContext(
|
|
261
|
+
{
|
|
262
|
+
surfaceAccess: {}
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
source
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
);
|
|
237
271
|
app.singleton("runtime.web-placement.client", () => createWebPlacementRuntime({ app, logger }));
|
|
272
|
+
app.singleton("runtime.web-bootstrap.client", (scope) =>
|
|
273
|
+
createShellBootstrapRuntime({
|
|
274
|
+
app: scope,
|
|
275
|
+
logger
|
|
276
|
+
})
|
|
277
|
+
);
|
|
238
278
|
app.singleton("shell.web.query-client", () => createShellWebQueryClient());
|
|
239
279
|
app.singleton("runtime.web-error.presentation-store.client", () => createErrorPresentationStore());
|
|
240
280
|
app.singleton("runtime.web-error.client", (scope) =>
|
|
@@ -285,6 +325,11 @@ class ShellWebClientProvider {
|
|
|
285
325
|
const errorConfig = await loadAppErrorConfig(logger, errorRuntime);
|
|
286
326
|
applyAppErrorConfig(errorRuntime, errorConfig);
|
|
287
327
|
|
|
328
|
+
const bootstrapRuntime = app.make("runtime.web-bootstrap.client");
|
|
329
|
+
if (bootstrapRuntime && typeof bootstrapRuntime.initialize === "function") {
|
|
330
|
+
await bootstrapRuntime.initialize();
|
|
331
|
+
}
|
|
332
|
+
|
|
288
333
|
if (!app.has("jskit.client.vue.app")) {
|
|
289
334
|
return;
|
|
290
335
|
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { createProviderLogger as createSharedProviderLogger } from "@jskit-ai/kernel/shared/support/providerLogger";
|
|
2
|
+
import { resolveBootstrapPayloadHandlers } from "../bootstrap/bootstrapPayloadHandlerRegistry.js";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_BOOTSTRAP_PATH = "/api/bootstrap";
|
|
5
|
+
|
|
6
|
+
function normalizeObject(value) {
|
|
7
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function buildBootstrapUrl({ path = DEFAULT_BOOTSTRAP_PATH, query = {} } = {}) {
|
|
11
|
+
const normalizedPath = String(path || DEFAULT_BOOTSTRAP_PATH).trim() || DEFAULT_BOOTSTRAP_PATH;
|
|
12
|
+
const params = new URLSearchParams();
|
|
13
|
+
|
|
14
|
+
for (const [key, value] of Object.entries(normalizeObject(query))) {
|
|
15
|
+
const normalizedKey = String(key || "").trim();
|
|
16
|
+
const normalizedValue = String(value || "").trim();
|
|
17
|
+
if (!normalizedKey || !normalizedValue) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
params.set(normalizedKey, normalizedValue);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const queryString = params.toString();
|
|
24
|
+
return queryString ? `${normalizedPath}?${queryString}` : normalizedPath;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeBootstrapResponseError(response, url) {
|
|
28
|
+
const error = new Error(`Bootstrap payload request failed with status ${response.status}.`);
|
|
29
|
+
error.statusCode = Number(response.status || 0);
|
|
30
|
+
error.url = url;
|
|
31
|
+
return error;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createShellBootstrapRuntime({
|
|
35
|
+
app,
|
|
36
|
+
logger = null,
|
|
37
|
+
fetchImplementation = globalThis.fetch,
|
|
38
|
+
bootstrapPath = DEFAULT_BOOTSTRAP_PATH
|
|
39
|
+
} = {}) {
|
|
40
|
+
if (!app || typeof app.has !== "function" || typeof app.make !== "function" || typeof app.resolveTag !== "function") {
|
|
41
|
+
throw new Error("createShellBootstrapRuntime requires application has()/make()/resolveTag().");
|
|
42
|
+
}
|
|
43
|
+
if (!app.has("runtime.web-placement.client")) {
|
|
44
|
+
throw new Error("createShellBootstrapRuntime requires shell-web placement runtime.");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const runtimeLogger = logger || createSharedProviderLogger(app);
|
|
48
|
+
const placementRuntime = app.make("runtime.web-placement.client");
|
|
49
|
+
const router = app.has("jskit.client.router") ? app.make("jskit.client.router") : null;
|
|
50
|
+
let initialized = false;
|
|
51
|
+
let refreshQueue = Promise.resolve();
|
|
52
|
+
|
|
53
|
+
async function resolveBootstrapRequest(reason = "manual") {
|
|
54
|
+
const handlers = resolveBootstrapPayloadHandlers(app);
|
|
55
|
+
let request = {
|
|
56
|
+
path: bootstrapPath,
|
|
57
|
+
query: {},
|
|
58
|
+
meta: {}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
for (const handler of handlers) {
|
|
62
|
+
if (typeof handler.resolveBootstrapRequest !== "function") {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const contribution = normalizeObject(
|
|
67
|
+
await handler.resolveBootstrapRequest({
|
|
68
|
+
app,
|
|
69
|
+
router,
|
|
70
|
+
placementRuntime,
|
|
71
|
+
reason,
|
|
72
|
+
request: Object.freeze({
|
|
73
|
+
path: request.path,
|
|
74
|
+
query: Object.freeze({ ...request.query }),
|
|
75
|
+
meta: Object.freeze({ ...request.meta })
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
request = {
|
|
81
|
+
path: String(contribution.path || request.path || bootstrapPath).trim() || bootstrapPath,
|
|
82
|
+
query: {
|
|
83
|
+
...request.query,
|
|
84
|
+
...normalizeObject(contribution.query)
|
|
85
|
+
},
|
|
86
|
+
meta: {
|
|
87
|
+
...request.meta,
|
|
88
|
+
...normalizeObject(contribution.meta)
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return Object.freeze({
|
|
94
|
+
path: request.path,
|
|
95
|
+
query: Object.freeze({ ...request.query }),
|
|
96
|
+
meta: Object.freeze({ ...request.meta })
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function applyBootstrapPayload(payload, reason = "manual", request = Object.freeze({})) {
|
|
101
|
+
const handlers = resolveBootstrapPayloadHandlers(app);
|
|
102
|
+
const source = `shell-web.bootstrap.${String(reason || "manual").trim() || "manual"}`;
|
|
103
|
+
|
|
104
|
+
for (const handler of handlers) {
|
|
105
|
+
await handler.applyBootstrapPayload({
|
|
106
|
+
app,
|
|
107
|
+
router,
|
|
108
|
+
placementRuntime,
|
|
109
|
+
payload,
|
|
110
|
+
request,
|
|
111
|
+
reason,
|
|
112
|
+
source
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return payload;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function applyBootstrapError(error, reason = "manual", request = Object.freeze({})) {
|
|
120
|
+
const handlers = resolveBootstrapPayloadHandlers(app);
|
|
121
|
+
const source = `shell-web.bootstrap.${String(reason || "manual").trim() || "manual"}`;
|
|
122
|
+
|
|
123
|
+
for (const handler of handlers) {
|
|
124
|
+
if (typeof handler.handleBootstrapError !== "function") {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
await handler.handleBootstrapError({
|
|
129
|
+
app,
|
|
130
|
+
router,
|
|
131
|
+
placementRuntime,
|
|
132
|
+
error,
|
|
133
|
+
request,
|
|
134
|
+
reason,
|
|
135
|
+
source
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function performRefresh(reason = "manual") {
|
|
141
|
+
if (typeof fetchImplementation !== "function") {
|
|
142
|
+
throw new Error("Bootstrap payload fetch requires a fetch implementation.");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const request = await resolveBootstrapRequest(reason);
|
|
146
|
+
const url = buildBootstrapUrl(request);
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const response = await fetchImplementation(url, {
|
|
150
|
+
method: "GET",
|
|
151
|
+
credentials: "include",
|
|
152
|
+
headers: {
|
|
153
|
+
accept: "application/json"
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
throw normalizeBootstrapResponseError(response, url);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const payload = await response.json();
|
|
162
|
+
return applyBootstrapPayload(payload, reason, request);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
await applyBootstrapError(error, reason, request);
|
|
165
|
+
runtimeLogger.warn(
|
|
166
|
+
{
|
|
167
|
+
reason,
|
|
168
|
+
error: String(error?.message || error || "unknown error")
|
|
169
|
+
},
|
|
170
|
+
"shell-web bootstrap refresh failed."
|
|
171
|
+
);
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function refresh(reason = "manual") {
|
|
177
|
+
refreshQueue = refreshQueue.then(() => performRefresh(reason));
|
|
178
|
+
return refreshQueue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function initialize() {
|
|
182
|
+
if (initialized) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
initialized = true;
|
|
186
|
+
return refresh("init");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return Object.freeze({
|
|
190
|
+
initialize,
|
|
191
|
+
refresh
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export { createShellBootstrapRuntime };
|
|
@@ -64,9 +64,9 @@ const { drawerOpen, toggleDrawer, resolvedSurface, resolvedSurfaceLabel } = useS
|
|
|
64
64
|
</v-navigation-drawer>
|
|
65
65
|
|
|
66
66
|
<v-main class="bg-background">
|
|
67
|
-
<v-container fluid class="
|
|
68
|
-
<h1 class="text-h5
|
|
69
|
-
<p class="text-body-2 text-medium-emphasis
|
|
67
|
+
<v-container fluid class="shell-layout__content">
|
|
68
|
+
<h1 v-if="title" class="shell-layout__title text-h5">{{ title }}</h1>
|
|
69
|
+
<p v-if="subtitle" class="shell-layout__subtitle text-body-2 text-medium-emphasis">{{ subtitle }}</p>
|
|
70
70
|
<slot />
|
|
71
71
|
</v-container>
|
|
72
72
|
</v-main>
|
|
@@ -77,4 +77,16 @@ const { drawerOpen, toggleDrawer, resolvedSurface, resolvedSurfaceLabel } = useS
|
|
|
77
77
|
.shell-layout {
|
|
78
78
|
min-height: 72vh;
|
|
79
79
|
}
|
|
80
|
+
|
|
81
|
+
.shell-layout__content {
|
|
82
|
+
padding: 0.75rem 1rem 1rem;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.shell-layout__title {
|
|
86
|
+
margin-bottom: 0.25rem;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.shell-layout__subtitle {
|
|
90
|
+
margin-bottom: 0.75rem;
|
|
91
|
+
}
|
|
80
92
|
</style>
|
|
@@ -29,7 +29,7 @@ const health = computed(() => {
|
|
|
29
29
|
|
|
30
30
|
<template>
|
|
31
31
|
<v-card rounded="lg" elevation="1" border>
|
|
32
|
-
<v-card-item>
|
|
32
|
+
<v-card-item class="home-surface-card__header">
|
|
33
33
|
<template #prepend>
|
|
34
34
|
<v-chip color="primary" size="small" label>Home</v-chip>
|
|
35
35
|
</template>
|
|
@@ -37,7 +37,7 @@ const health = computed(() => {
|
|
|
37
37
|
<v-card-subtitle>Main public surface</v-card-subtitle>
|
|
38
38
|
</v-card-item>
|
|
39
39
|
<v-divider />
|
|
40
|
-
<v-card-text class="d-flex flex-column ga-
|
|
40
|
+
<v-card-text class="home-surface-card__body d-flex flex-column ga-3">
|
|
41
41
|
<div class="d-flex flex-wrap ga-3">
|
|
42
42
|
<v-chip color="secondary" variant="tonal" label>Route: /home</v-chip>
|
|
43
43
|
<v-chip color="info" variant="tonal" label>Health: {{ health }}</v-chip>
|
|
@@ -49,3 +49,13 @@ const health = computed(() => {
|
|
|
49
49
|
</v-card-text>
|
|
50
50
|
</v-card>
|
|
51
51
|
</template>
|
|
52
|
+
|
|
53
|
+
<style scoped>
|
|
54
|
+
.home-surface-card__header {
|
|
55
|
+
padding: 0.875rem 1rem;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.home-surface-card__body {
|
|
59
|
+
padding: 0.875rem 1rem 1rem;
|
|
60
|
+
}
|
|
61
|
+
</style>
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { registerBootstrapPayloadHandler } from "../src/client/bootstrap/bootstrapPayloadHandlerRegistry.js";
|
|
4
|
+
import { createShellBootstrapRuntime } from "../src/client/runtime/bootstrapRuntime.js";
|
|
5
|
+
|
|
6
|
+
function createPlacementRuntime(initialContext = {}) {
|
|
7
|
+
let context = Object.freeze({ ...initialContext });
|
|
8
|
+
const listeners = new Set();
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
getContext() {
|
|
12
|
+
return context;
|
|
13
|
+
},
|
|
14
|
+
setContext(value = {}, { replace = false } = {}) {
|
|
15
|
+
const next = value && typeof value === "object" ? { ...value } : {};
|
|
16
|
+
context = Object.freeze(replace ? next : { ...context, ...next });
|
|
17
|
+
for (const listener of listeners) {
|
|
18
|
+
listener({
|
|
19
|
+
type: "context.updated"
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return context;
|
|
23
|
+
},
|
|
24
|
+
subscribe(listener) {
|
|
25
|
+
if (typeof listener === "function") {
|
|
26
|
+
listeners.add(listener);
|
|
27
|
+
}
|
|
28
|
+
return () => {
|
|
29
|
+
listeners.delete(listener);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createAppDouble({ placementRuntime, realtimeSocket = null } = {}) {
|
|
36
|
+
const singletons = new Map();
|
|
37
|
+
const singletonInstances = new Map();
|
|
38
|
+
return {
|
|
39
|
+
singleton(token, factory) {
|
|
40
|
+
singletons.set(token, factory);
|
|
41
|
+
},
|
|
42
|
+
tag(token, tagName) {
|
|
43
|
+
const current = this._tags.get(tagName) || [];
|
|
44
|
+
current.push(token);
|
|
45
|
+
this._tags.set(tagName, current);
|
|
46
|
+
},
|
|
47
|
+
resolveTag(tagName) {
|
|
48
|
+
return (this._tags.get(tagName) || []).map((token) => this.make(token));
|
|
49
|
+
},
|
|
50
|
+
_tags: new Map(),
|
|
51
|
+
has(token) {
|
|
52
|
+
if (token === "runtime.web-placement.client") {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (token === "runtime.realtime.client.socket") {
|
|
56
|
+
return Boolean(realtimeSocket);
|
|
57
|
+
}
|
|
58
|
+
return singletons.has(token) || singletonInstances.has(token);
|
|
59
|
+
},
|
|
60
|
+
make(token) {
|
|
61
|
+
if (token === "runtime.web-placement.client") {
|
|
62
|
+
return placementRuntime;
|
|
63
|
+
}
|
|
64
|
+
if (token === "runtime.realtime.client.socket") {
|
|
65
|
+
return realtimeSocket;
|
|
66
|
+
}
|
|
67
|
+
if (singletonInstances.has(token)) {
|
|
68
|
+
return singletonInstances.get(token);
|
|
69
|
+
}
|
|
70
|
+
const factory = singletons.get(token);
|
|
71
|
+
if (!factory) {
|
|
72
|
+
throw new Error(`Unknown token ${String(token)}`);
|
|
73
|
+
}
|
|
74
|
+
const instance = factory(this);
|
|
75
|
+
singletonInstances.set(token, instance);
|
|
76
|
+
return instance;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
test("shell bootstrap runtime refreshes /api/bootstrap on init and applies registered handlers", async () => {
|
|
82
|
+
const placementRuntime = createPlacementRuntime({
|
|
83
|
+
auth: {}
|
|
84
|
+
});
|
|
85
|
+
const payloads = [
|
|
86
|
+
{
|
|
87
|
+
surfaceAccess: {
|
|
88
|
+
consoleowner: false
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
surfaceAccess: {
|
|
93
|
+
consoleowner: true
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
];
|
|
97
|
+
const calls = [];
|
|
98
|
+
const observedRequests = [];
|
|
99
|
+
const observedResolveMeta = [];
|
|
100
|
+
const app = createAppDouble({ placementRuntime });
|
|
101
|
+
registerBootstrapPayloadHandler(app, "test.bootstrap.request", () =>
|
|
102
|
+
Object.freeze({
|
|
103
|
+
handlerId: "test.bootstrap.request",
|
|
104
|
+
order: -10,
|
|
105
|
+
resolveBootstrapRequest() {
|
|
106
|
+
return {
|
|
107
|
+
query: {
|
|
108
|
+
workspaceSlug: "acme"
|
|
109
|
+
},
|
|
110
|
+
meta: {
|
|
111
|
+
path: "/w/acme/dashboard"
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
applyBootstrapPayload({ request }) {
|
|
116
|
+
observedRequests.push(request);
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
);
|
|
120
|
+
registerBootstrapPayloadHandler(app, "test.bootstrap.request-meta", () =>
|
|
121
|
+
Object.freeze({
|
|
122
|
+
handlerId: "test.bootstrap.request-meta",
|
|
123
|
+
order: -5,
|
|
124
|
+
resolveBootstrapRequest({ request }) {
|
|
125
|
+
observedResolveMeta.push(request?.meta?.path || "");
|
|
126
|
+
return {};
|
|
127
|
+
},
|
|
128
|
+
applyBootstrapPayload() {}
|
|
129
|
+
})
|
|
130
|
+
);
|
|
131
|
+
registerBootstrapPayloadHandler(app, "test.bootstrap.surfaceAccess", () =>
|
|
132
|
+
Object.freeze({
|
|
133
|
+
handlerId: "test.bootstrap.surfaceAccess",
|
|
134
|
+
order: 0,
|
|
135
|
+
applyBootstrapPayload({ payload, placementRuntime: targetRuntime }) {
|
|
136
|
+
targetRuntime.setContext({
|
|
137
|
+
surfaceAccess: payload.surfaceAccess || {}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const runtime = createShellBootstrapRuntime({
|
|
144
|
+
app,
|
|
145
|
+
fetchImplementation: async (url) => {
|
|
146
|
+
calls.push(String(url || ""));
|
|
147
|
+
const payload = payloads.shift() || {};
|
|
148
|
+
return {
|
|
149
|
+
ok: true,
|
|
150
|
+
async json() {
|
|
151
|
+
return payload;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await runtime.initialize();
|
|
158
|
+
assert.deepEqual(placementRuntime.getContext().surfaceAccess, {
|
|
159
|
+
consoleowner: false
|
|
160
|
+
});
|
|
161
|
+
await runtime.refresh("manual");
|
|
162
|
+
assert.deepEqual(calls, ["/api/bootstrap?workspaceSlug=acme", "/api/bootstrap?workspaceSlug=acme"]);
|
|
163
|
+
assert.deepEqual(observedResolveMeta, ["/w/acme/dashboard", "/w/acme/dashboard"]);
|
|
164
|
+
assert.deepEqual(observedRequests, [
|
|
165
|
+
{
|
|
166
|
+
path: "/api/bootstrap",
|
|
167
|
+
query: {
|
|
168
|
+
workspaceSlug: "acme"
|
|
169
|
+
},
|
|
170
|
+
meta: {
|
|
171
|
+
path: "/w/acme/dashboard"
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
path: "/api/bootstrap",
|
|
176
|
+
query: {
|
|
177
|
+
workspaceSlug: "acme"
|
|
178
|
+
},
|
|
179
|
+
meta: {
|
|
180
|
+
path: "/w/acme/dashboard"
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
]);
|
|
184
|
+
assert.deepEqual(placementRuntime.getContext().surfaceAccess, {
|
|
185
|
+
consoleowner: true
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -41,15 +41,15 @@ test("placement registry accepts explicit non-global surface ids", () => {
|
|
|
41
41
|
assert.equal(added, true);
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
test("placement registry rejects
|
|
44
|
+
test("placement registry rejects split target fields", () => {
|
|
45
45
|
const registry = createPlacementRegistry();
|
|
46
46
|
|
|
47
47
|
assert.throws(
|
|
48
48
|
() => registry.addPlacement({
|
|
49
|
-
id: "example.
|
|
49
|
+
id: "example.split",
|
|
50
50
|
host: "shell-layout",
|
|
51
51
|
position: "top-right",
|
|
52
|
-
componentToken: "example.
|
|
52
|
+
componentToken: "example.split.component"
|
|
53
53
|
}),
|
|
54
54
|
/must use "target" only/
|
|
55
55
|
);
|
package/test/provider.test.js
CHANGED
|
@@ -42,9 +42,15 @@ function createAppDouble({ surfaceRuntime = null } = {}) {
|
|
|
42
42
|
plugins,
|
|
43
43
|
pinia,
|
|
44
44
|
vueApp,
|
|
45
|
+
_tags: new Map(),
|
|
45
46
|
singleton(token, factory) {
|
|
46
47
|
singletons.set(token, factory);
|
|
47
48
|
},
|
|
49
|
+
tag(token, tagName) {
|
|
50
|
+
const current = this._tags.get(tagName) || [];
|
|
51
|
+
current.push(token);
|
|
52
|
+
this._tags.set(tagName, current);
|
|
53
|
+
},
|
|
48
54
|
has(token) {
|
|
49
55
|
if (token === "jskit.client.vue.app") {
|
|
50
56
|
return true;
|
|
@@ -78,51 +84,88 @@ function createAppDouble({ surfaceRuntime = null } = {}) {
|
|
|
78
84
|
singletonInstances.set(token, instance);
|
|
79
85
|
return instance;
|
|
80
86
|
},
|
|
81
|
-
resolveTag() {
|
|
82
|
-
return [];
|
|
87
|
+
resolveTag(tagName) {
|
|
88
|
+
return (this._tags.get(tagName) || []).map((token) => this.make(token));
|
|
83
89
|
}
|
|
84
90
|
};
|
|
85
91
|
}
|
|
86
92
|
|
|
93
|
+
async function withFetchStub(responsePayload, callback) {
|
|
94
|
+
const previousFetch = globalThis.fetch;
|
|
95
|
+
globalThis.fetch = async () => ({
|
|
96
|
+
ok: true,
|
|
97
|
+
async json() {
|
|
98
|
+
return responsePayload;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
return await callback();
|
|
104
|
+
} finally {
|
|
105
|
+
if (previousFetch === undefined) {
|
|
106
|
+
delete globalThis.fetch;
|
|
107
|
+
} else {
|
|
108
|
+
globalThis.fetch = previousFetch;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function withFetchImplementation(fetchImplementation, callback) {
|
|
114
|
+
const previousFetch = globalThis.fetch;
|
|
115
|
+
globalThis.fetch = fetchImplementation;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
return await callback();
|
|
119
|
+
} finally {
|
|
120
|
+
if (previousFetch === undefined) {
|
|
121
|
+
delete globalThis.fetch;
|
|
122
|
+
} else {
|
|
123
|
+
globalThis.fetch = previousFetch;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
87
128
|
test("shell web client provider binds runtime and injects it into Vue app", async () => {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
129
|
+
await withFetchStub({ surfaceAccess: {} }, async () => {
|
|
130
|
+
const app = createAppDouble();
|
|
131
|
+
const provider = new ShellWebClientProvider();
|
|
132
|
+
|
|
133
|
+
provider.register(app);
|
|
134
|
+
assert.equal(app.singletons.has("runtime.web-placement.client"), true);
|
|
135
|
+
assert.equal(app.singletons.has("runtime.web-error.client"), true);
|
|
136
|
+
assert.equal(app.singletons.has("runtime.web-error.presentation-store.client"), true);
|
|
137
|
+
|
|
138
|
+
await provider.boot(app);
|
|
139
|
+
assert.equal(app.plugins.length, 1);
|
|
140
|
+
assert.equal(typeof app.plugins[0].plugin.install, "function");
|
|
141
|
+
assert.equal(typeof app.plugins[0].options?.queryClient, "object");
|
|
142
|
+
|
|
143
|
+
const providedByKey = new Map(app.provided.map((entry) => [entry.key, entry.value]));
|
|
144
|
+
|
|
145
|
+
assert.equal(providedByKey.has("jskit.shell-web.runtime.web-placement.client"), true);
|
|
146
|
+
assert.equal(providedByKey.has("jskit.shell-web.runtime.web-error.client"), true);
|
|
147
|
+
assert.equal(providedByKey.has("jskit.shell-web.runtime.web-error.presentation-store.client"), true);
|
|
148
|
+
|
|
149
|
+
const placementRuntime = providedByKey.get("jskit.shell-web.runtime.web-placement.client");
|
|
150
|
+
assert.equal(typeof placementRuntime.getPlacements, "function");
|
|
151
|
+
assert.equal(typeof placementRuntime.getContext, "function");
|
|
152
|
+
assert.equal(typeof placementRuntime.setContext, "function");
|
|
153
|
+
assert.equal(typeof placementRuntime.getContext().surfaceConfig, "object");
|
|
154
|
+
|
|
155
|
+
const errorRuntime = providedByKey.get("jskit.shell-web.runtime.web-error.client");
|
|
156
|
+
assert.equal(typeof errorRuntime.report, "function");
|
|
157
|
+
assert.equal(typeof errorRuntime.configure, "function");
|
|
158
|
+
|
|
159
|
+
const errorStore = providedByKey.get("jskit.shell-web.runtime.web-error.presentation-store.client");
|
|
160
|
+
assert.equal(typeof errorStore.getState, "function");
|
|
161
|
+
assert.equal(typeof errorStore.present, "function");
|
|
162
|
+
|
|
163
|
+
const errorPresentationStore = useShellErrorPresentationStore(app.pinia);
|
|
164
|
+
assert.equal(errorPresentationStore.revision, 0);
|
|
165
|
+
assert.equal(typeof errorPresentationStore.present, "function");
|
|
166
|
+
errorStore.present("banner", { message: "Hello" });
|
|
167
|
+
assert.equal(errorPresentationStore.channels.banner[0].message, "Hello");
|
|
168
|
+
});
|
|
126
169
|
});
|
|
127
170
|
|
|
128
171
|
test("shell web client provider resolves surface config from client app config", async () => {
|
|
@@ -134,35 +177,63 @@ test("shell web client provider resolves surface config from client app config",
|
|
|
134
177
|
});
|
|
135
178
|
|
|
136
179
|
try {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
180
|
+
await withFetchStub({ surfaceAccess: {} }, async () => {
|
|
181
|
+
const app = createAppDouble({
|
|
182
|
+
surfaceRuntime: {
|
|
183
|
+
DEFAULT_SURFACE_ID: "app",
|
|
184
|
+
listEnabledSurfaceIds() {
|
|
185
|
+
return ["app", "admin", "console"];
|
|
186
|
+
},
|
|
187
|
+
listSurfaceDefinitions() {
|
|
188
|
+
return [
|
|
189
|
+
{ id: "app", pagesRoot: "w/[workspaceSlug]", requiresWorkspace: true, enabled: true },
|
|
190
|
+
{ id: "admin", pagesRoot: "w/[workspaceSlug]/admin", requiresWorkspace: true, enabled: true },
|
|
191
|
+
{ id: "console", pagesRoot: "console", requiresWorkspace: false, enabled: true }
|
|
192
|
+
];
|
|
193
|
+
}
|
|
149
194
|
}
|
|
150
|
-
}
|
|
195
|
+
});
|
|
196
|
+
const provider = new ShellWebClientProvider();
|
|
197
|
+
provider.register(app);
|
|
198
|
+
|
|
199
|
+
await provider.boot(app);
|
|
200
|
+
|
|
201
|
+
const placementRuntime = app.make("runtime.web-placement.client");
|
|
202
|
+
const context = placementRuntime.getContext();
|
|
203
|
+
assert.equal(context.surfaceConfig.tenancyMode, "workspaces");
|
|
204
|
+
assert.equal(context.surfaceConfig.defaultSurfaceId, "app");
|
|
205
|
+
assert.deepEqual(context.surfaceConfig.enabledSurfaceIds, ["app", "admin", "console"]);
|
|
206
|
+
assert.deepEqual(context.surfaceAccessPolicies, {
|
|
207
|
+
public: {}
|
|
208
|
+
});
|
|
151
209
|
});
|
|
210
|
+
} finally {
|
|
211
|
+
setClientAppConfig({});
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("shell web client provider clears generic surface access on bootstrap 401", async () => {
|
|
216
|
+
await withFetchImplementation(async () => ({
|
|
217
|
+
ok: false,
|
|
218
|
+
status: 401
|
|
219
|
+
}), async () => {
|
|
220
|
+
const app = createAppDouble();
|
|
152
221
|
const provider = new ShellWebClientProvider();
|
|
153
222
|
provider.register(app);
|
|
154
223
|
|
|
155
|
-
await provider.boot(app);
|
|
156
|
-
|
|
157
224
|
const placementRuntime = app.make("runtime.web-placement.client");
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
225
|
+
placementRuntime.setContext(
|
|
226
|
+
{
|
|
227
|
+
surfaceAccess: {
|
|
228
|
+
consoleowner: true
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
source: "test.seed"
|
|
233
|
+
}
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
await provider.boot(app);
|
|
237
|
+
assert.deepEqual(placementRuntime.getContext().surfaceAccess, {});
|
|
238
|
+
});
|
|
168
239
|
});
|
|
@@ -75,7 +75,7 @@ test("shell-web placement template seeds default Home and Settings drawer naviga
|
|
|
75
75
|
assert.match(source, /target: "home-settings:primary-menu"/);
|
|
76
76
|
assert.match(source, /label: "General"/);
|
|
77
77
|
assert.match(source, /unscopedSuffix: "\/settings\/general"/);
|
|
78
|
-
assert.
|
|
78
|
+
assert.doesNotMatch(source, /to: "\.\/general"/);
|
|
79
79
|
});
|
|
80
80
|
|
|
81
81
|
test("shell-web descriptor metadata advertises home settings outlets, default drawer links, and installs the scaffold page", () => {
|