@jskit-ai/shell-web 0.1.162 → 0.1.164
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/fixtures/adaptive-shell/src/ScreenPage.vue +1 -1
- package/package.json +8 -253
- package/patterns/application-shell/PATTERN.md +66 -0
- package/{templates → patterns/application-shell/example}/expected-existing/src/pages/home/index.vue +8 -8
- package/{templates → patterns/application-shell/example}/src/pages/home/index.vue +8 -8
- package/{templates → patterns/application-shell/example}/src/pages/home/settings/general/index.vue +1 -1
- package/{templates → patterns/application-shell/example}/src/pages/home/settings.vue +8 -8
- package/patterns/page-and-placement/PATTERN.md +76 -0
- package/patterns/page-and-placement/example/packages/main/src/client/providers/MainClientProvider.js +14 -0
- package/patterns/page-and-placement/example/src/components/SyncStatusElement.vue +16 -0
- package/patterns/page-and-placement/example/src/pages/home/reports/activity.vue +8 -0
- package/patterns/page-and-placement/example/src/pages/home/reports/index.vue +7 -0
- package/patterns/page-and-placement/example/src/pages/home/reports/overview.vue +8 -0
- package/patterns/page-and-placement/example/src/pages/home/reports.vue +33 -0
- package/patterns/page-and-placement/example/src/placement.js +52 -0
- package/patterns/page-and-placement/example/src/placementTopology.js +29 -0
- package/src/client/bootstrap/bootstrapPayloadHandlerRegistry.js +30 -42
- package/src/client/bootstrap/index.js +2 -3
- package/src/client/index.js +2 -11
- package/src/client/placement/runtime.js +9 -12
- package/src/client/providers/ShellWebClientProvider.js +95 -129
- package/src/client/requestRecovery/runtime.js +9 -29
- package/src/client/runtime/bootstrapRuntime.js +18 -29
- package/src/test/adaptiveShellSmoke.js +3 -9
- package/test/bootstrapRuntime.test.js +10 -61
- package/test/pageAndPlacementPattern.test.js +38 -0
- package/test/placementRuntime.test.js +36 -50
- package/test/playwrightContract.test.js +2 -12
- package/test/provider.test.js +126 -737
- package/test/settingsPlacementContract.test.js +75 -126
- package/src/server/support/localLinkItemScaffolds.js +0 -80
- package/test/bootstrapClaimContract.test.js +0 -76
- package/test/linkItemScaffoldContract.test.js +0 -248
- /package/{templates → patterns/application-shell/example}/expected-existing/src/App.vue +0 -0
- /package/{templates → patterns/application-shell/example}/expected-existing/src/pages/home.vue +0 -0
- /package/{templates → patterns/application-shell/example}/src/App.vue +0 -0
- /package/{templates → patterns/application-shell/example}/src/components/ShellLayout.vue +0 -0
- /package/{templates → patterns/application-shell/example}/src/components/menus/MenuLinkItem.vue +0 -0
- /package/{templates → patterns/application-shell/example}/src/components/menus/SurfaceAwareMenuLinkItem.vue +0 -0
- /package/{templates → patterns/application-shell/example}/src/components/menus/TabLinkItem.vue +0 -0
- /package/{templates → patterns/application-shell/example}/src/error.js +0 -0
- /package/{templates → patterns/application-shell/example}/src/pages/home/settings/index.vue +0 -0
- /package/{templates → patterns/application-shell/example}/src/pages/home.vue +0 -0
- /package/{templates → patterns/application-shell/example}/src/placement.js +0 -0
- /package/{templates → patterns/application-shell/example}/src/placementTopology.js +0 -0
- /package/{templates → patterns/application-shell/example}/tests/e2e/adaptive-shell.spec.ts +0 -0
package/test/provider.test.js
CHANGED
|
@@ -1,796 +1,185 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { createPinia } from "pinia";
|
|
4
|
+
import { createCapabilityRuntime, defineProvider } from "@jskit-ai/kernel/shared/capabilities";
|
|
5
|
+
import { createClientComponentRegistry } from "../../kernel/client/componentRegistry.js";
|
|
6
|
+
import { createSurfaceRuntime } from "@jskit-ai/kernel/shared/surface/runtime";
|
|
4
7
|
import {
|
|
5
8
|
ShellWebClientProvider,
|
|
6
9
|
resolveAppPlacementTopologyExport
|
|
7
10
|
} from "../src/client/providers/ShellWebClientProvider.js";
|
|
8
|
-
|
|
9
|
-
isMissingDynamicModule
|
|
10
|
-
} from "../src/client/providers/appModuleLoadFailure.js";
|
|
11
|
-
import {
|
|
12
|
-
SHELL_ASYNC_MODULE_RECOVERY_RUNTIME_KEY
|
|
13
|
-
} from "../src/client/asyncModuleRecovery/index.js";
|
|
14
|
-
import {
|
|
15
|
-
SHELL_REQUEST_RECOVERY_RUNTIME_KEY
|
|
16
|
-
} from "../src/client/requestRecovery/index.js";
|
|
17
|
-
import { useShellErrorPresentationStore } from "../src/client/stores/useShellErrorPresentationStore.js";
|
|
11
|
+
|
|
18
12
|
const CLIENT_APP_CONFIG_GLOBAL_KEY = "__JSKIT_CLIENT_APP_CONFIG__";
|
|
19
13
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
function response(payload = {}, { ok = true, status = 200 } = {}) {
|
|
15
|
+
return {
|
|
16
|
+
ok,
|
|
17
|
+
status,
|
|
18
|
+
async json() {
|
|
19
|
+
return payload;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
24
23
|
|
|
24
|
+
async function withGlobal(name, value, callback) {
|
|
25
|
+
const hadPrevious = Object.hasOwn(globalThis, name);
|
|
26
|
+
const previous = globalThis[name];
|
|
27
|
+
globalThis[name] = value;
|
|
25
28
|
try {
|
|
26
29
|
return await callback();
|
|
27
30
|
} finally {
|
|
28
31
|
if (hadPrevious) {
|
|
29
|
-
globalThis[name] =
|
|
32
|
+
globalThis[name] = previous;
|
|
30
33
|
} else {
|
|
31
34
|
delete globalThis[name];
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
function
|
|
37
|
-
const
|
|
38
|
-
source && typeof source === "object" && !Array.isArray(source) ? Object.freeze({ ...source }) : Object.freeze({});
|
|
39
|
-
if (typeof globalThis === "object" && globalThis) {
|
|
40
|
-
globalThis[CLIENT_APP_CONFIG_GLOBAL_KEY] = normalized;
|
|
41
|
-
}
|
|
42
|
-
return normalized;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function createAppDouble({ surfaceRuntime = null, queryClient = null, router = null } = {}) {
|
|
46
|
-
const singletons = new Map();
|
|
47
|
-
const singletonInstances = new Map();
|
|
48
|
-
const provided = [];
|
|
49
|
-
const plugins = [];
|
|
50
|
-
const pinia = createPinia();
|
|
51
|
-
|
|
52
|
-
const vueApp = {
|
|
53
|
-
config: {
|
|
54
|
-
globalProperties: {
|
|
55
|
-
$pinia: pinia
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
use(plugin, options) {
|
|
59
|
-
plugins.push({ plugin, options });
|
|
60
|
-
return this;
|
|
61
|
-
},
|
|
62
|
-
provide(key, value) {
|
|
63
|
-
provided.push({ key, value });
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
|
|
39
|
+
function createVueApp() {
|
|
40
|
+
const provided = new Map();
|
|
67
41
|
return {
|
|
68
|
-
|
|
42
|
+
config: {},
|
|
69
43
|
provided,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
vueApp,
|
|
73
|
-
_tags: new Map(),
|
|
74
|
-
singleton(token, factory) {
|
|
75
|
-
singletons.set(token, factory);
|
|
76
|
-
},
|
|
77
|
-
tag(token, tagName) {
|
|
78
|
-
const current = this._tags.get(tagName) || [];
|
|
79
|
-
current.push(token);
|
|
80
|
-
this._tags.set(tagName, current);
|
|
81
|
-
},
|
|
82
|
-
has(token) {
|
|
83
|
-
if (token === "jskit.client.vue.app") {
|
|
84
|
-
return true;
|
|
85
|
-
}
|
|
86
|
-
if (token === "jskit.client.pinia") {
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
if (token === "jskit.client.surface.runtime") {
|
|
90
|
-
return Boolean(surfaceRuntime);
|
|
91
|
-
}
|
|
92
|
-
if (token === "jskit.client.query-client") {
|
|
93
|
-
return Boolean(queryClient);
|
|
94
|
-
}
|
|
95
|
-
if (token === "jskit.client.router") {
|
|
96
|
-
return Boolean(router);
|
|
97
|
-
}
|
|
98
|
-
return singletons.has(token) || singletonInstances.has(token);
|
|
99
|
-
},
|
|
100
|
-
make(token) {
|
|
101
|
-
if (token === "jskit.client.vue.app") {
|
|
102
|
-
return vueApp;
|
|
103
|
-
}
|
|
104
|
-
if (token === "jskit.client.pinia") {
|
|
105
|
-
return pinia;
|
|
106
|
-
}
|
|
107
|
-
if (token === "jskit.client.surface.runtime" && surfaceRuntime) {
|
|
108
|
-
return surfaceRuntime;
|
|
109
|
-
}
|
|
110
|
-
if (token === "jskit.client.query-client" && queryClient) {
|
|
111
|
-
return queryClient;
|
|
112
|
-
}
|
|
113
|
-
if (token === "jskit.client.router" && router) {
|
|
114
|
-
return router;
|
|
115
|
-
}
|
|
116
|
-
if (singletonInstances.has(token)) {
|
|
117
|
-
return singletonInstances.get(token);
|
|
118
|
-
}
|
|
119
|
-
const factory = singletons.get(token);
|
|
120
|
-
if (!factory) {
|
|
121
|
-
throw new Error(`Unknown token ${String(token)}`);
|
|
122
|
-
}
|
|
123
|
-
const instance = factory(this);
|
|
124
|
-
singletonInstances.set(token, instance);
|
|
125
|
-
return instance;
|
|
44
|
+
provide(key, value) {
|
|
45
|
+
provided.set(key, value);
|
|
126
46
|
},
|
|
127
|
-
|
|
128
|
-
return
|
|
47
|
+
use() {
|
|
48
|
+
return this;
|
|
129
49
|
}
|
|
130
50
|
};
|
|
131
51
|
}
|
|
132
52
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
ok: true,
|
|
136
|
-
async json() {
|
|
137
|
-
return responsePayload;
|
|
138
|
-
}
|
|
139
|
-
}), callback);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function withFetchImplementation(fetchImplementation, callback) {
|
|
143
|
-
return withTemporaryGlobal("fetch", fetchImplementation, callback);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async function withWindowDouble(windowObject, callback) {
|
|
147
|
-
return withTemporaryGlobal("window", windowObject, callback);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function createWindowDouble({ href = "https://example.test/home" } = {}) {
|
|
151
|
-
const listeners = new Map();
|
|
152
|
-
const reloadCalls = [];
|
|
153
|
-
const location = {
|
|
154
|
-
href,
|
|
155
|
-
reload() {
|
|
156
|
-
reloadCalls.push(location.href);
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
|
|
53
|
+
function createRouter() {
|
|
54
|
+
const errorHandlers = [];
|
|
160
55
|
return {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
listeners.set(type, handler);
|
|
166
|
-
},
|
|
167
|
-
removeEventListener(type, handler) {
|
|
168
|
-
if (listeners.get(type) === handler) {
|
|
169
|
-
listeners.delete(type);
|
|
170
|
-
}
|
|
56
|
+
currentRoute: { value: { path: "/" } },
|
|
57
|
+
onError(handler) {
|
|
58
|
+
errorHandlers.push(handler);
|
|
59
|
+
return () => {};
|
|
171
60
|
}
|
|
172
61
|
};
|
|
173
62
|
}
|
|
174
63
|
|
|
175
|
-
function
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
queries,
|
|
181
|
-
getAll() {
|
|
182
|
-
return queries;
|
|
64
|
+
function createSurfaceRuntimeFixture() {
|
|
65
|
+
return createSurfaceRuntime({
|
|
66
|
+
allMode: "all",
|
|
67
|
+
surfaces: {
|
|
68
|
+
app: { id: "app", pagesRoot: "app", enabled: true }
|
|
183
69
|
},
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
return () => {
|
|
187
|
-
const index = listeners.indexOf(listener);
|
|
188
|
-
if (index >= 0) {
|
|
189
|
-
listeners.splice(index, 1);
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
},
|
|
193
|
-
emit(event) {
|
|
194
|
-
for (const listener of [...listeners]) {
|
|
195
|
-
listener(event);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
};
|
|
70
|
+
defaultSurfaceId: "app"
|
|
71
|
+
});
|
|
199
72
|
}
|
|
200
73
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
74
|
+
async function startShell({
|
|
75
|
+
bootstrapPayload = {},
|
|
76
|
+
bootstrapResponse = null,
|
|
77
|
+
queryClient = null
|
|
78
|
+
} = {}) {
|
|
79
|
+
let shell = null;
|
|
80
|
+
const Probe = defineProvider({
|
|
81
|
+
id: "test.shell.probe",
|
|
82
|
+
requires: { value: "client.shell" },
|
|
83
|
+
setup({ value }) {
|
|
84
|
+
shell = value;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
const vueApp = createVueApp();
|
|
88
|
+
const pinia = createPinia();
|
|
89
|
+
const router = createRouter();
|
|
90
|
+
const components = createClientComponentRegistry();
|
|
91
|
+
const surfaceRuntime = createSurfaceRuntimeFixture();
|
|
92
|
+
const logger = { debug() {}, info() {}, warn() {}, error() {} };
|
|
93
|
+
const fetchImplementation = async () => bootstrapResponse || response(bootstrapPayload);
|
|
94
|
+
|
|
95
|
+
return withGlobal("fetch", fetchImplementation, async () =>
|
|
96
|
+
withGlobal(CLIENT_APP_CONFIG_GLOBAL_KEY, Object.freeze({}), async () => {
|
|
97
|
+
const runtime = createCapabilityRuntime({
|
|
98
|
+
profile: "test",
|
|
99
|
+
providers: [ShellWebClientProvider, Probe],
|
|
100
|
+
inputs: {
|
|
101
|
+
"client.components": components,
|
|
102
|
+
"client.logger": logger,
|
|
103
|
+
"client.pinia": pinia,
|
|
104
|
+
"client.query": queryClient,
|
|
105
|
+
"client.router": router,
|
|
106
|
+
"client.surface": surfaceRuntime,
|
|
107
|
+
"client.vue": vueApp
|
|
212
108
|
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
|
|
109
|
+
});
|
|
110
|
+
await runtime.start();
|
|
111
|
+
return { components, pinia, router, runtime, shell, vueApp };
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
}
|
|
216
115
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
116
|
+
test("shell capability owns placement, bootstrap, refresh, errors, and recovery without a container", async () => {
|
|
117
|
+
const fixture = await startShell({
|
|
118
|
+
bootstrapPayload: {
|
|
119
|
+
surfaceAccess: {
|
|
120
|
+
app: { allowed: true }
|
|
121
|
+
}
|
|
220
122
|
}
|
|
221
123
|
});
|
|
222
124
|
|
|
223
|
-
assert.equal(
|
|
224
|
-
assert.deepEqual(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
125
|
+
assert.equal(fixture.runtime.diagnostics().lifecycleState, "started");
|
|
126
|
+
assert.deepEqual(Object.keys(fixture.shell).sort(), [
|
|
127
|
+
"asyncModuleRecovery",
|
|
128
|
+
"bootstrap",
|
|
129
|
+
"bootstrapHandlers",
|
|
130
|
+
"error",
|
|
131
|
+
"errorPresentationStore",
|
|
132
|
+
"placement",
|
|
133
|
+
"refresh",
|
|
134
|
+
"requestRecovery"
|
|
135
|
+
]);
|
|
136
|
+
assert.deepEqual(fixture.shell.placement.getContext().surfaceAccess, {
|
|
137
|
+
app: { allowed: true }
|
|
138
|
+
});
|
|
228
139
|
assert.equal(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
"/src/placement.js"
|
|
232
|
-
),
|
|
233
|
-
true
|
|
140
|
+
fixture.vueApp.provided.get("jskit.shell-web.runtime.web-placement.client"),
|
|
141
|
+
fixture.shell.placement
|
|
234
142
|
);
|
|
235
143
|
assert.equal(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
"/src/placement.js"
|
|
239
|
-
),
|
|
240
|
-
false
|
|
144
|
+
fixture.vueApp.provided.get("jskit.shell-web.runtime.web-error.client"),
|
|
145
|
+
fixture.shell.error
|
|
241
146
|
);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
test("shell web client provider binds runtime and injects it into Vue app", async () => {
|
|
245
|
-
await withFetchStub({ surfaceAccess: {} }, async () => {
|
|
246
|
-
const app = createAppDouble();
|
|
247
|
-
const provider = new ShellWebClientProvider();
|
|
248
|
-
|
|
249
|
-
provider.register(app);
|
|
250
|
-
assert.equal(app.singletons.has("runtime.web-placement.client"), true);
|
|
251
|
-
assert.equal(app.singletons.has("runtime.web-error.client"), true);
|
|
252
|
-
assert.equal(app.singletons.has("runtime.web-error.presentation-store.client"), true);
|
|
253
|
-
assert.equal(app.singletons.has("runtime.web-refresh.client"), true);
|
|
254
|
-
assert.equal(app.singletons.has("runtime.web-async-module-recovery.client"), true);
|
|
255
|
-
assert.equal(app.singletons.has("runtime.web-request-recovery.client"), true);
|
|
256
|
-
|
|
257
|
-
await provider.boot(app);
|
|
258
|
-
assert.equal(app.plugins.length, 0);
|
|
259
|
-
|
|
260
|
-
const providedByKey = new Map(app.provided.map((entry) => [entry.key, entry.value]));
|
|
261
|
-
|
|
262
|
-
assert.equal(providedByKey.has("jskit.shell-web.runtime.web-placement.client"), true);
|
|
263
|
-
assert.equal(providedByKey.has("jskit.shell-web.runtime.web-refresh.client"), true);
|
|
264
|
-
assert.equal(providedByKey.has("jskit.shell-web.runtime.web-error.client"), true);
|
|
265
|
-
assert.equal(providedByKey.has(SHELL_ASYNC_MODULE_RECOVERY_RUNTIME_KEY), true);
|
|
266
|
-
assert.equal(providedByKey.has(SHELL_REQUEST_RECOVERY_RUNTIME_KEY), true);
|
|
267
|
-
assert.equal(providedByKey.has("jskit.shell-web.runtime.web-error.presentation-store.client"), true);
|
|
268
|
-
|
|
269
|
-
const placementRuntime = providedByKey.get("jskit.shell-web.runtime.web-placement.client");
|
|
270
|
-
assert.equal(typeof placementRuntime.getPlacements, "function");
|
|
271
|
-
assert.equal(typeof placementRuntime.getContext, "function");
|
|
272
|
-
assert.equal(typeof placementRuntime.setContext, "function");
|
|
273
|
-
assert.equal(typeof placementRuntime.getContext().surfaceConfig, "object");
|
|
274
|
-
|
|
275
|
-
const errorRuntime = providedByKey.get("jskit.shell-web.runtime.web-error.client");
|
|
276
|
-
assert.equal(typeof errorRuntime.report, "function");
|
|
277
|
-
assert.equal(typeof errorRuntime.configure, "function");
|
|
278
|
-
|
|
279
|
-
const refreshRuntime = providedByKey.get("jskit.shell-web.runtime.web-refresh.client");
|
|
280
|
-
assert.equal(typeof refreshRuntime.refresh, "function");
|
|
281
147
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
assert.equal(typeof asyncModuleRecoveryRuntime.notify, "function");
|
|
285
|
-
assert.equal(typeof asyncModuleRecoveryRuntime.reload, "function");
|
|
286
|
-
|
|
287
|
-
const requestRecoveryRuntime = providedByKey.get(SHELL_REQUEST_RECOVERY_RUNTIME_KEY);
|
|
288
|
-
assert.equal(typeof requestRecoveryRuntime.install, "function");
|
|
289
|
-
assert.equal(typeof requestRecoveryRuntime.report, "function");
|
|
290
|
-
assert.equal(typeof requestRecoveryRuntime.reload, "function");
|
|
291
|
-
|
|
292
|
-
const errorStore = providedByKey.get("jskit.shell-web.runtime.web-error.presentation-store.client");
|
|
293
|
-
assert.equal(typeof errorStore.getState, "function");
|
|
294
|
-
assert.equal(typeof errorStore.present, "function");
|
|
295
|
-
|
|
296
|
-
const errorPresentationStore = useShellErrorPresentationStore(app.pinia);
|
|
297
|
-
assert.equal(errorPresentationStore.revision, 0);
|
|
298
|
-
assert.equal(typeof errorPresentationStore.present, "function");
|
|
299
|
-
errorStore.present("banner", { message: "Hello" });
|
|
300
|
-
assert.equal(errorPresentationStore.channels.banner[0].message, "Hello");
|
|
301
|
-
});
|
|
148
|
+
await fixture.runtime.shutdown();
|
|
149
|
+
assert.equal(fixture.runtime.diagnostics().lifecycleState, "stopped");
|
|
302
150
|
});
|
|
303
151
|
|
|
304
|
-
test("shell refresh
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
async refetchQueries(filters = {}, options = {}) {
|
|
309
|
-
refetchCalls.push({ filters, options });
|
|
310
|
-
}
|
|
311
|
-
};
|
|
312
|
-
const app = createAppDouble({ queryClient });
|
|
313
|
-
const provider = new ShellWebClientProvider();
|
|
314
|
-
provider.register(app);
|
|
315
|
-
|
|
316
|
-
const refreshRuntime = app.make("runtime.web-refresh.client");
|
|
317
|
-
const result = await refreshRuntime.refresh("test-refresh");
|
|
318
|
-
|
|
319
|
-
assert.equal(result.reason, "test-refresh");
|
|
320
|
-
assert.equal(result.bootstrapRefreshed, true);
|
|
321
|
-
assert.equal(result.queriesRefetched, true);
|
|
322
|
-
assert.equal(refetchCalls.length, 1);
|
|
323
|
-
assert.equal(refetchCalls[0].filters.type, "active");
|
|
324
|
-
assert.equal(refetchCalls[0].options.throwOnError, false);
|
|
325
|
-
assert.equal(refetchCalls[0].filters.predicate({}), true);
|
|
326
|
-
assert.equal(refetchCalls[0].filters.predicate({ meta: {} }), true);
|
|
327
|
-
assert.equal(refetchCalls[0].filters.predicate({ meta: { jskit: { refreshOnPull: true } } }), true);
|
|
328
|
-
assert.equal(refetchCalls[0].filters.predicate({ meta: { jskitRefresh: "pull" } }), true);
|
|
329
|
-
assert.equal(refetchCalls[0].filters.predicate({ meta: { jskit: { refreshOnPull: false } } }), false);
|
|
330
|
-
assert.equal(refetchCalls[0].filters.predicate({ meta: { jskitRefresh: false } }), false);
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
test("shell refresh runtime reports recoverable retry errors as banners", async () => {
|
|
335
|
-
await withFetchStub({ surfaceAccess: { home: true } }, async () => {
|
|
336
|
-
const queryClient = {
|
|
337
|
-
async refetchQueries() {
|
|
338
|
-
throw new Error("Network unavailable");
|
|
339
|
-
}
|
|
340
|
-
};
|
|
341
|
-
const app = createAppDouble({ queryClient });
|
|
342
|
-
const provider = new ShellWebClientProvider();
|
|
343
|
-
provider.register(app);
|
|
344
|
-
|
|
345
|
-
const refreshRuntime = app.make("runtime.web-refresh.client");
|
|
346
|
-
const result = await refreshRuntime.refresh("test-refresh");
|
|
347
|
-
|
|
348
|
-
assert.equal(result.error instanceof Error, true);
|
|
349
|
-
|
|
350
|
-
const errorStore = app.make("runtime.web-error.presentation-store.client");
|
|
351
|
-
const state = errorStore.getState();
|
|
352
|
-
assert.equal(state.channels.banner.length, 1);
|
|
353
|
-
assert.equal(state.channels.banner[0].message, "Unable to refresh. Check the connection and try again.");
|
|
354
|
-
assert.equal(state.channels.banner[0].action.label, "Retry");
|
|
355
|
-
});
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
test("shell request recovery reports active query transport failures with retry actions", async () => {
|
|
359
|
-
await withFetchStub({ surfaceAccess: {} }, async () => {
|
|
360
|
-
const refetchCalls = [];
|
|
361
|
-
const queryCache = createQueryCacheDouble();
|
|
362
|
-
const queryClient = {
|
|
363
|
-
getQueryCache() {
|
|
364
|
-
return queryCache;
|
|
365
|
-
},
|
|
366
|
-
async refetchQueries(filters = {}, options = {}) {
|
|
367
|
-
refetchCalls.push({ filters, options });
|
|
368
|
-
}
|
|
369
|
-
};
|
|
370
|
-
const app = createAppDouble({ queryClient });
|
|
371
|
-
const provider = new ShellWebClientProvider();
|
|
372
|
-
provider.register(app);
|
|
373
|
-
await provider.boot(app);
|
|
374
|
-
const reportEvents = [];
|
|
375
|
-
app.make("runtime.web-error.client").subscribe((event = {}) => {
|
|
376
|
-
reportEvents.push(event);
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
const failedQuery = {
|
|
380
|
-
queryHash: "[\"project-access\"]",
|
|
381
|
-
queryKey: ["project-access"],
|
|
382
|
-
meta: {
|
|
383
|
-
jskit: {
|
|
384
|
-
requestRecoveryLabel: "Project access",
|
|
385
|
-
requestRecoverySource: "project-access.panel",
|
|
386
|
-
requestRecoveryDedupeKey: "project-access",
|
|
387
|
-
requestRecoveryMethod: "GET",
|
|
388
|
-
requestRecoveryDedupeWindowMs: 100
|
|
389
|
-
}
|
|
390
|
-
},
|
|
391
|
-
state: {
|
|
392
|
-
status: "error",
|
|
393
|
-
fetchStatus: "idle",
|
|
394
|
-
error: {
|
|
395
|
-
status: 0,
|
|
396
|
-
message: "Network request failed."
|
|
397
|
-
},
|
|
398
|
-
errorUpdateCount: 1
|
|
399
|
-
},
|
|
400
|
-
isActive() {
|
|
401
|
-
return true;
|
|
402
|
-
}
|
|
403
|
-
};
|
|
404
|
-
queryCache.emit({ type: "updated", query: failedQuery });
|
|
405
|
-
|
|
406
|
-
const errorStore = app.make("runtime.web-error.presentation-store.client");
|
|
407
|
-
const state = errorStore.getState();
|
|
408
|
-
assert.equal(state.channels.banner.length, 1);
|
|
409
|
-
assert.equal(
|
|
410
|
-
state.channels.banner[0].message,
|
|
411
|
-
"Project access could not reach the server or network. Check the connection and try again."
|
|
412
|
-
);
|
|
413
|
-
assert.equal(state.channels.banner[0].action.label, "Retry");
|
|
414
|
-
assert.equal(state.channels.banner[0].dedupeKey, "project-access");
|
|
415
|
-
assert.equal(reportEvents[0]?.result?.event?.source, "project-access.panel");
|
|
416
|
-
assert.equal(reportEvents[0]?.result?.decision?.dedupeWindowMs, 100);
|
|
417
|
-
|
|
418
|
-
await state.channels.banner[0].action.handler();
|
|
419
|
-
assert.equal(refetchCalls.length, 1);
|
|
420
|
-
assert.deepEqual(refetchCalls[0].filters, {
|
|
421
|
-
queryKey: ["project-access"],
|
|
422
|
-
exact: true,
|
|
423
|
-
type: "active"
|
|
424
|
-
});
|
|
425
|
-
assert.deepEqual(refetchCalls[0].options, {
|
|
426
|
-
throwOnError: false
|
|
427
|
-
});
|
|
428
|
-
});
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
test("shell request recovery dismisses query presentations after recovery", async () => {
|
|
432
|
-
await withFetchStub({ surfaceAccess: {} }, async () => {
|
|
433
|
-
const queryCache = createQueryCacheDouble();
|
|
434
|
-
const queryClient = {
|
|
152
|
+
test("shell refresh updates bootstrap state and active pull queries", async () => {
|
|
153
|
+
let refetchCount = 0;
|
|
154
|
+
const fixture = await startShell({
|
|
155
|
+
queryClient: {
|
|
435
156
|
getQueryCache() {
|
|
436
|
-
return
|
|
437
|
-
},
|
|
438
|
-
async refetchQueries() {}
|
|
439
|
-
};
|
|
440
|
-
const app = createAppDouble({ queryClient });
|
|
441
|
-
const provider = new ShellWebClientProvider();
|
|
442
|
-
provider.register(app);
|
|
443
|
-
await provider.boot(app);
|
|
444
|
-
|
|
445
|
-
const query = {
|
|
446
|
-
queryHash: "[\"project-access\"]",
|
|
447
|
-
queryKey: ["project-access"],
|
|
448
|
-
meta: {
|
|
449
|
-
jskit: {
|
|
450
|
-
requestRecoveryLabel: "Project access",
|
|
451
|
-
requestRecoverySource: "project-access.panel",
|
|
452
|
-
requestRecoveryDedupeKey: "project-access",
|
|
453
|
-
requestRecoveryMethod: "GET"
|
|
454
|
-
}
|
|
455
|
-
},
|
|
456
|
-
state: {
|
|
457
|
-
status: "error",
|
|
458
|
-
fetchStatus: "idle",
|
|
459
|
-
error: {
|
|
460
|
-
status: 0,
|
|
461
|
-
message: "Network request failed."
|
|
462
|
-
},
|
|
463
|
-
errorUpdateCount: 1
|
|
157
|
+
return null;
|
|
464
158
|
},
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
468
|
-
};
|
|
469
|
-
queryCache.emit({ type: "updated", query });
|
|
470
|
-
|
|
471
|
-
const errorStore = app.make("runtime.web-error.presentation-store.client");
|
|
472
|
-
assert.equal(errorStore.getState().channels.banner.length, 1);
|
|
473
|
-
|
|
474
|
-
query.state = {
|
|
475
|
-
status: "success",
|
|
476
|
-
fetchStatus: "idle",
|
|
477
|
-
data: {
|
|
478
|
-
ok: true
|
|
479
|
-
},
|
|
480
|
-
error: null,
|
|
481
|
-
errorUpdateCount: 1
|
|
482
|
-
};
|
|
483
|
-
queryCache.emit({ type: "updated", query });
|
|
484
|
-
|
|
485
|
-
assert.equal(errorStore.getState().channels.banner.length, 0);
|
|
486
|
-
});
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
test("shell request recovery ignores query transport failures without a safe read method", async () => {
|
|
490
|
-
await withFetchStub({ surfaceAccess: {} }, async () => {
|
|
491
|
-
const queryCache = createQueryCacheDouble();
|
|
492
|
-
const queryClient = {
|
|
493
|
-
getQueryCache() {
|
|
494
|
-
return queryCache;
|
|
495
|
-
},
|
|
496
|
-
async refetchQueries() {}
|
|
497
|
-
};
|
|
498
|
-
const app = createAppDouble({ queryClient });
|
|
499
|
-
const provider = new ShellWebClientProvider();
|
|
500
|
-
provider.register(app);
|
|
501
|
-
await provider.boot(app);
|
|
502
|
-
|
|
503
|
-
for (const [queryKey, meta] of [
|
|
504
|
-
[["unmarked"], {}],
|
|
505
|
-
[["unsafe"], {
|
|
506
|
-
jskit: {
|
|
507
|
-
requestRecoveryMethod: "POST"
|
|
508
|
-
}
|
|
509
|
-
}]
|
|
510
|
-
]) {
|
|
511
|
-
queryCache.emit({
|
|
512
|
-
type: "updated",
|
|
513
|
-
query: {
|
|
514
|
-
queryHash: JSON.stringify(queryKey),
|
|
515
|
-
queryKey,
|
|
516
|
-
meta,
|
|
517
|
-
state: {
|
|
518
|
-
status: "error",
|
|
519
|
-
fetchStatus: "idle",
|
|
520
|
-
error: {
|
|
521
|
-
status: 0,
|
|
522
|
-
message: "Network request failed."
|
|
523
|
-
},
|
|
524
|
-
errorUpdateCount: 1
|
|
525
|
-
},
|
|
526
|
-
isActive() {
|
|
527
|
-
return true;
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
const errorStore = app.make("runtime.web-error.presentation-store.client");
|
|
534
|
-
assert.equal(errorStore.getState().channels.banner.length, 0);
|
|
535
|
-
});
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
test("shell request recovery ignores ordinary active query validation failures", async () => {
|
|
539
|
-
await withFetchStub({ surfaceAccess: {} }, async () => {
|
|
540
|
-
const queryCache = createQueryCacheDouble();
|
|
541
|
-
const queryClient = {
|
|
542
|
-
getQueryCache() {
|
|
543
|
-
return queryCache;
|
|
544
|
-
},
|
|
545
|
-
async refetchQueries() {}
|
|
546
|
-
};
|
|
547
|
-
const app = createAppDouble({ queryClient });
|
|
548
|
-
const provider = new ShellWebClientProvider();
|
|
549
|
-
provider.register(app);
|
|
550
|
-
await provider.boot(app);
|
|
551
|
-
|
|
552
|
-
queryCache.emit({
|
|
553
|
-
type: "updated",
|
|
554
|
-
query: {
|
|
555
|
-
queryHash: "[\"project-access\"]",
|
|
556
|
-
queryKey: ["project-access"],
|
|
557
|
-
meta: {
|
|
558
|
-
jskit: {
|
|
559
|
-
requestRecoveryMethod: "GET"
|
|
560
|
-
}
|
|
561
|
-
},
|
|
562
|
-
state: {
|
|
563
|
-
status: "error",
|
|
564
|
-
fetchStatus: "idle",
|
|
565
|
-
error: {
|
|
566
|
-
status: 422,
|
|
567
|
-
message: "Invalid input."
|
|
568
|
-
},
|
|
569
|
-
errorUpdateCount: 1
|
|
570
|
-
},
|
|
571
|
-
isActive() {
|
|
572
|
-
return true;
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
const errorStore = app.make("runtime.web-error.presentation-store.client");
|
|
578
|
-
assert.equal(errorStore.getState().channels.banner.length, 0);
|
|
579
|
-
});
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
test("shell bootstrap transport failures report through request recovery", async () => {
|
|
583
|
-
let fetchCalls = 0;
|
|
584
|
-
await withFetchImplementation(async () => {
|
|
585
|
-
fetchCalls += 1;
|
|
586
|
-
throw new TypeError("Failed to fetch");
|
|
587
|
-
}, async () => {
|
|
588
|
-
const app = createAppDouble();
|
|
589
|
-
const provider = new ShellWebClientProvider();
|
|
590
|
-
provider.register(app);
|
|
591
|
-
await provider.boot(app);
|
|
592
|
-
|
|
593
|
-
const errorStore = app.make("runtime.web-error.presentation-store.client");
|
|
594
|
-
const state = errorStore.getState();
|
|
595
|
-
assert.equal(state.channels.banner.length, 1);
|
|
596
|
-
assert.equal(
|
|
597
|
-
state.channels.banner[0].message,
|
|
598
|
-
"App data could not reach the server or network. Check the connection and try again."
|
|
599
|
-
);
|
|
600
|
-
assert.equal(state.channels.banner[0].action.label, "Retry");
|
|
601
|
-
|
|
602
|
-
await state.channels.banner[0].action.handler();
|
|
603
|
-
assert.equal(fetchCalls, 2);
|
|
604
|
-
});
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
test("shell web client provider reports dynamic import failures through async module recovery", async () => {
|
|
608
|
-
await withFetchStub({ surfaceAccess: {} }, async () => {
|
|
609
|
-
const routerErrorHandlers = [];
|
|
610
|
-
const replacedPaths = [];
|
|
611
|
-
const router = {
|
|
612
|
-
onError(handler) {
|
|
613
|
-
routerErrorHandlers.push(handler);
|
|
614
|
-
return () => null;
|
|
615
|
-
},
|
|
616
|
-
replace(fullPath) {
|
|
617
|
-
replacedPaths.push(fullPath);
|
|
159
|
+
async refetchQueries() {
|
|
160
|
+
refetchCount += 1;
|
|
618
161
|
}
|
|
619
|
-
};
|
|
620
|
-
const app = createAppDouble({ router });
|
|
621
|
-
const provider = new ShellWebClientProvider();
|
|
622
|
-
provider.register(app);
|
|
623
|
-
await provider.boot(app);
|
|
624
|
-
|
|
625
|
-
assert.equal(routerErrorHandlers.length, 2);
|
|
626
|
-
|
|
627
|
-
const chunkError = new Error("Failed to fetch dynamically imported module: /assets/page.js");
|
|
628
|
-
for (const handler of routerErrorHandlers) {
|
|
629
|
-
handler(chunkError, {
|
|
630
|
-
fullPath: "/app/dashboard"
|
|
631
|
-
});
|
|
632
162
|
}
|
|
633
|
-
|
|
634
|
-
const errorStore = app.make("runtime.web-error.presentation-store.client");
|
|
635
|
-
const state = errorStore.getState();
|
|
636
|
-
assert.equal(state.channels.banner.length, 1);
|
|
637
|
-
assert.equal(
|
|
638
|
-
state.channels.banner[0].message,
|
|
639
|
-
"Page did not download. The app may have been updated, or the network request failed."
|
|
640
|
-
);
|
|
641
|
-
assert.equal(state.channels.banner[0].severity, "warning");
|
|
642
|
-
assert.equal(state.channels.banner[0].action.label, "Reload");
|
|
643
|
-
assert.deepEqual(replacedPaths, []);
|
|
644
|
-
});
|
|
645
|
-
});
|
|
646
|
-
|
|
647
|
-
test("shell web async module recovery reload action checks the document before reloading", async () => {
|
|
648
|
-
const windowObject = createWindowDouble();
|
|
649
|
-
await withWindowDouble(windowObject, async () => {
|
|
650
|
-
const fetchCalls = [];
|
|
651
|
-
await withFetchImplementation(async (input, init = {}) => {
|
|
652
|
-
fetchCalls.push({ input, init });
|
|
653
|
-
return {
|
|
654
|
-
ok: true,
|
|
655
|
-
status: 200,
|
|
656
|
-
async json() {
|
|
657
|
-
return { surfaceAccess: {} };
|
|
658
|
-
}
|
|
659
|
-
};
|
|
660
|
-
}, async () => {
|
|
661
|
-
const routerErrorHandlers = [];
|
|
662
|
-
const router = {
|
|
663
|
-
onError(handler) {
|
|
664
|
-
routerErrorHandlers.push(handler);
|
|
665
|
-
return () => null;
|
|
666
|
-
},
|
|
667
|
-
replace() {}
|
|
668
|
-
};
|
|
669
|
-
const app = createAppDouble({ router });
|
|
670
|
-
const provider = new ShellWebClientProvider();
|
|
671
|
-
provider.register(app);
|
|
672
|
-
await provider.boot(app);
|
|
673
|
-
|
|
674
|
-
const chunkError = new Error("Failed to fetch dynamically imported module: /assets/page.js");
|
|
675
|
-
routerErrorHandlers[0](chunkError, {
|
|
676
|
-
fullPath: "/home/settings"
|
|
677
|
-
});
|
|
678
|
-
|
|
679
|
-
const errorStore = app.make("runtime.web-error.presentation-store.client");
|
|
680
|
-
const banner = errorStore.getState().channels.banner[0];
|
|
681
|
-
await banner.action.handler();
|
|
682
|
-
|
|
683
|
-
assert.deepEqual(windowObject.reloadCalls, ["https://example.test/home"]);
|
|
684
|
-
const reloadFetch = fetchCalls.find((entry) => entry.input === "https://example.test/home");
|
|
685
|
-
assert.equal(reloadFetch?.init?.cache, "no-store");
|
|
686
|
-
assert.equal(reloadFetch?.init?.credentials, "same-origin");
|
|
687
|
-
});
|
|
688
163
|
});
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
test("shell web async module recovery keeps the page alive when reload check fails", async () => {
|
|
692
|
-
const windowObject = createWindowDouble();
|
|
693
|
-
await withWindowDouble(windowObject, async () => {
|
|
694
|
-
await withFetchImplementation(async (input) => {
|
|
695
|
-
if (input === "https://example.test/home") {
|
|
696
|
-
throw new TypeError("Failed to fetch");
|
|
697
|
-
}
|
|
698
|
-
return {
|
|
699
|
-
ok: true,
|
|
700
|
-
status: 200,
|
|
701
|
-
async json() {
|
|
702
|
-
return { surfaceAccess: {} };
|
|
703
|
-
}
|
|
704
|
-
};
|
|
705
|
-
}, async () => {
|
|
706
|
-
const app = createAppDouble();
|
|
707
|
-
const provider = new ShellWebClientProvider();
|
|
708
|
-
provider.register(app);
|
|
709
|
-
await provider.boot(app);
|
|
710
|
-
|
|
711
|
-
const recoveryRuntime = app.make("runtime.web-async-module-recovery.client");
|
|
712
|
-
const reloaded = await recoveryRuntime.reload();
|
|
713
164
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
assert.equal(
|
|
720
|
-
state.channels.banner[0].message,
|
|
721
|
-
"The app cannot reload because the app server is not reachable. Restart the server, then click Reload."
|
|
722
|
-
);
|
|
723
|
-
assert.equal(state.channels.banner[0].action.label, "Reload");
|
|
724
|
-
});
|
|
725
|
-
});
|
|
165
|
+
const result = await fixture.shell.refresh.refresh("test");
|
|
166
|
+
assert.equal(result.bootstrapRefreshed, true);
|
|
167
|
+
assert.equal(result.queriesRefetched, true);
|
|
168
|
+
assert.equal(refetchCount, 1);
|
|
169
|
+
await fixture.runtime.shutdown();
|
|
726
170
|
});
|
|
727
171
|
|
|
728
|
-
test("shell
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
surfaceAccessPolicies: {
|
|
732
|
-
public: {}
|
|
733
|
-
}
|
|
172
|
+
test("shell bootstrap clears surface access after an unauthenticated response", async () => {
|
|
173
|
+
const fixture = await startShell({
|
|
174
|
+
bootstrapResponse: response({}, { ok: false, status: 401 })
|
|
734
175
|
});
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
await withFetchStub({ surfaceAccess: {} }, async () => {
|
|
738
|
-
const app = createAppDouble({
|
|
739
|
-
surfaceRuntime: {
|
|
740
|
-
DEFAULT_SURFACE_ID: "app",
|
|
741
|
-
listEnabledSurfaceIds() {
|
|
742
|
-
return ["app", "admin", "console"];
|
|
743
|
-
},
|
|
744
|
-
listSurfaceDefinitions() {
|
|
745
|
-
return [
|
|
746
|
-
{ id: "app", pagesRoot: "w/[workspaceSlug]", requiresWorkspace: true, enabled: true },
|
|
747
|
-
{ id: "admin", pagesRoot: "w/[workspaceSlug]/admin", requiresWorkspace: true, enabled: true },
|
|
748
|
-
{ id: "console", pagesRoot: "console", requiresWorkspace: false, enabled: true }
|
|
749
|
-
];
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
});
|
|
753
|
-
const provider = new ShellWebClientProvider();
|
|
754
|
-
provider.register(app);
|
|
755
|
-
|
|
756
|
-
await provider.boot(app);
|
|
757
|
-
|
|
758
|
-
const placementRuntime = app.make("runtime.web-placement.client");
|
|
759
|
-
const context = placementRuntime.getContext();
|
|
760
|
-
assert.equal(context.surfaceConfig.tenancyMode, "workspaces");
|
|
761
|
-
assert.equal(context.surfaceConfig.defaultSurfaceId, "app");
|
|
762
|
-
assert.deepEqual(context.surfaceConfig.enabledSurfaceIds, ["app", "admin", "console"]);
|
|
763
|
-
assert.deepEqual(context.surfaceAccessPolicies, {
|
|
764
|
-
public: {}
|
|
765
|
-
});
|
|
766
|
-
});
|
|
767
|
-
} finally {
|
|
768
|
-
setClientAppConfig({});
|
|
769
|
-
}
|
|
176
|
+
assert.deepEqual(fixture.shell.placement.getContext().surfaceAccess, {});
|
|
177
|
+
await fixture.runtime.shutdown();
|
|
770
178
|
});
|
|
771
179
|
|
|
772
|
-
test("shell
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
const app = createAppDouble();
|
|
778
|
-
const provider = new ShellWebClientProvider();
|
|
779
|
-
provider.register(app);
|
|
780
|
-
|
|
781
|
-
const placementRuntime = app.make("runtime.web-placement.client");
|
|
782
|
-
placementRuntime.setContext(
|
|
783
|
-
{
|
|
784
|
-
surfaceAccess: {
|
|
785
|
-
consoleowner: true
|
|
786
|
-
}
|
|
787
|
-
},
|
|
788
|
-
{
|
|
789
|
-
source: "test.seed"
|
|
790
|
-
}
|
|
791
|
-
);
|
|
792
|
-
|
|
793
|
-
await provider.boot(app);
|
|
794
|
-
assert.deepEqual(placementRuntime.getContext().surfaceAccess, {});
|
|
795
|
-
});
|
|
180
|
+
test("shell placement topology export accepts append-only objects and arrays", () => {
|
|
181
|
+
const logger = { warn() {} };
|
|
182
|
+
const topology = { mode: "append", placements: [] };
|
|
183
|
+
assert.equal(resolveAppPlacementTopologyExport(topology, logger), topology);
|
|
184
|
+
assert.deepEqual(resolveAppPlacementTopologyExport([], logger), []);
|
|
796
185
|
});
|