@jskit-ai/shell-web 0.1.64 → 0.1.66
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 +200 -16
- package/package.json +8 -7
- package/src/client/components/ShellErrorHost.vue +88 -15
- package/src/client/components/ShellLayout.vue +551 -50
- package/src/client/components/ShellOutlet.vue +34 -4
- package/src/client/components/ShellOutletMenuWidget.vue +1 -8
- package/src/client/components/ShellRouteTransition.vue +480 -0
- package/src/client/components/ShellTabLinkItem.vue +22 -6
- package/src/client/composables/useShellLayoutState.js +12 -1
- package/src/client/error/normalize.js +17 -0
- package/src/client/error/policy.js +25 -11
- package/src/client/error/runtime.js +2 -0
- package/src/client/index.js +1 -0
- package/src/client/placement/index.js +5 -0
- package/src/client/placement/runtime.js +149 -16
- package/src/client/placement/validators.js +36 -8
- package/src/client/providers/ShellWebClientProvider.js +189 -24
- package/src/client/stores/useShellLayoutStore.js +21 -1
- package/src/test/adaptiveShellSmoke.js +121 -0
- package/templates/expected-existing/src/pages/home/index.vue +40 -10
- package/templates/src/components/ShellLayout.vue +10 -90
- package/templates/src/components/menus/TabLinkItem.vue +4 -0
- package/templates/src/error.js +7 -1
- package/templates/src/pages/home/index.vue +64 -23
- package/templates/src/pages/home/settings/general/index.vue +12 -9
- package/templates/src/pages/home/settings.vue +68 -24
- package/templates/src/placement.js +7 -6
- package/templates/src/placementTopology.js +149 -0
- package/templates/tests/e2e/adaptive-shell.spec.ts +4 -0
- package/test/errorRuntime.test.js +42 -0
- package/test/linkItemScaffoldContract.test.js +9 -2
- package/test/outletMenuWidgetContract.test.js +2 -2
- package/test/placementRegistry.test.js +3 -3
- package/test/placementRuntime.test.js +144 -14
- package/test/provider.test.js +97 -5
- package/test/settingsPlacementContract.test.js +234 -20
- package/test/useShellLayoutState.test.js +19 -0
package/test/provider.test.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { createPinia } from "pinia";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
ShellWebClientProvider,
|
|
6
|
+
resolveAppPlacementTopologyExport
|
|
7
|
+
} from "../src/client/providers/ShellWebClientProvider.js";
|
|
5
8
|
import { useShellErrorPresentationStore } from "../src/client/stores/useShellErrorPresentationStore.js";
|
|
6
9
|
const CLIENT_APP_CONFIG_GLOBAL_KEY = "__JSKIT_CLIENT_APP_CONFIG__";
|
|
7
10
|
|
|
@@ -14,7 +17,7 @@ function setClientAppConfig(source = {}) {
|
|
|
14
17
|
return normalized;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
function createAppDouble({ surfaceRuntime = null } = {}) {
|
|
20
|
+
function createAppDouble({ surfaceRuntime = null, queryClient = null } = {}) {
|
|
18
21
|
const singletons = new Map();
|
|
19
22
|
const singletonInstances = new Map();
|
|
20
23
|
const provided = [];
|
|
@@ -61,6 +64,9 @@ function createAppDouble({ surfaceRuntime = null } = {}) {
|
|
|
61
64
|
if (token === "jskit.client.surface.runtime") {
|
|
62
65
|
return Boolean(surfaceRuntime);
|
|
63
66
|
}
|
|
67
|
+
if (token === "jskit.client.query-client") {
|
|
68
|
+
return Boolean(queryClient);
|
|
69
|
+
}
|
|
64
70
|
return singletons.has(token) || singletonInstances.has(token);
|
|
65
71
|
},
|
|
66
72
|
make(token) {
|
|
@@ -73,6 +79,9 @@ function createAppDouble({ surfaceRuntime = null } = {}) {
|
|
|
73
79
|
if (token === "jskit.client.surface.runtime" && surfaceRuntime) {
|
|
74
80
|
return surfaceRuntime;
|
|
75
81
|
}
|
|
82
|
+
if (token === "jskit.client.query-client" && queryClient) {
|
|
83
|
+
return queryClient;
|
|
84
|
+
}
|
|
76
85
|
if (singletonInstances.has(token)) {
|
|
77
86
|
return singletonInstances.get(token);
|
|
78
87
|
}
|
|
@@ -125,6 +134,32 @@ async function withFetchImplementation(fetchImplementation, callback) {
|
|
|
125
134
|
}
|
|
126
135
|
}
|
|
127
136
|
|
|
137
|
+
test("shell web client provider preserves append-only placement topology object exports", () => {
|
|
138
|
+
const warnings = [];
|
|
139
|
+
const topology = {
|
|
140
|
+
placements: [
|
|
141
|
+
{
|
|
142
|
+
id: "shell.primary-nav",
|
|
143
|
+
surfaces: ["*"],
|
|
144
|
+
variants: {
|
|
145
|
+
compact: { outlet: "shell-layout:primary-menu" },
|
|
146
|
+
medium: { outlet: "shell-layout:primary-menu" },
|
|
147
|
+
expanded: { outlet: "shell-layout:primary-menu" }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const resolved = resolveAppPlacementTopologyExport(topology, {
|
|
154
|
+
warn(payload, message) {
|
|
155
|
+
warnings.push({ payload, message });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
assert.equal(resolved, topology);
|
|
160
|
+
assert.deepEqual(warnings, []);
|
|
161
|
+
});
|
|
162
|
+
|
|
128
163
|
test("shell web client provider binds runtime and injects it into Vue app", async () => {
|
|
129
164
|
await withFetchStub({ surfaceAccess: {} }, async () => {
|
|
130
165
|
const app = createAppDouble();
|
|
@@ -134,15 +169,15 @@ test("shell web client provider binds runtime and injects it into Vue app", asyn
|
|
|
134
169
|
assert.equal(app.singletons.has("runtime.web-placement.client"), true);
|
|
135
170
|
assert.equal(app.singletons.has("runtime.web-error.client"), true);
|
|
136
171
|
assert.equal(app.singletons.has("runtime.web-error.presentation-store.client"), true);
|
|
172
|
+
assert.equal(app.singletons.has("runtime.web-refresh.client"), true);
|
|
137
173
|
|
|
138
174
|
await provider.boot(app);
|
|
139
|
-
assert.equal(app.plugins.length,
|
|
140
|
-
assert.equal(typeof app.plugins[0].plugin.install, "function");
|
|
141
|
-
assert.equal(typeof app.plugins[0].options?.queryClient, "object");
|
|
175
|
+
assert.equal(app.plugins.length, 0);
|
|
142
176
|
|
|
143
177
|
const providedByKey = new Map(app.provided.map((entry) => [entry.key, entry.value]));
|
|
144
178
|
|
|
145
179
|
assert.equal(providedByKey.has("jskit.shell-web.runtime.web-placement.client"), true);
|
|
180
|
+
assert.equal(providedByKey.has("jskit.shell-web.runtime.web-refresh.client"), true);
|
|
146
181
|
assert.equal(providedByKey.has("jskit.shell-web.runtime.web-error.client"), true);
|
|
147
182
|
assert.equal(providedByKey.has("jskit.shell-web.runtime.web-error.presentation-store.client"), true);
|
|
148
183
|
|
|
@@ -156,6 +191,9 @@ test("shell web client provider binds runtime and injects it into Vue app", asyn
|
|
|
156
191
|
assert.equal(typeof errorRuntime.report, "function");
|
|
157
192
|
assert.equal(typeof errorRuntime.configure, "function");
|
|
158
193
|
|
|
194
|
+
const refreshRuntime = providedByKey.get("jskit.shell-web.runtime.web-refresh.client");
|
|
195
|
+
assert.equal(typeof refreshRuntime.refresh, "function");
|
|
196
|
+
|
|
159
197
|
const errorStore = providedByKey.get("jskit.shell-web.runtime.web-error.presentation-store.client");
|
|
160
198
|
assert.equal(typeof errorStore.getState, "function");
|
|
161
199
|
assert.equal(typeof errorStore.present, "function");
|
|
@@ -168,6 +206,60 @@ test("shell web client provider binds runtime and injects it into Vue app", asyn
|
|
|
168
206
|
});
|
|
169
207
|
});
|
|
170
208
|
|
|
209
|
+
test("shell refresh runtime refreshes bootstrap and active queries by default", async () => {
|
|
210
|
+
await withFetchStub({ surfaceAccess: { home: true } }, async () => {
|
|
211
|
+
const refetchCalls = [];
|
|
212
|
+
const queryClient = {
|
|
213
|
+
async refetchQueries(filters = {}, options = {}) {
|
|
214
|
+
refetchCalls.push({ filters, options });
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
const app = createAppDouble({ queryClient });
|
|
218
|
+
const provider = new ShellWebClientProvider();
|
|
219
|
+
provider.register(app);
|
|
220
|
+
|
|
221
|
+
const refreshRuntime = app.make("runtime.web-refresh.client");
|
|
222
|
+
const result = await refreshRuntime.refresh("test-refresh");
|
|
223
|
+
|
|
224
|
+
assert.equal(result.reason, "test-refresh");
|
|
225
|
+
assert.equal(result.bootstrapRefreshed, true);
|
|
226
|
+
assert.equal(result.queriesRefetched, true);
|
|
227
|
+
assert.equal(refetchCalls.length, 1);
|
|
228
|
+
assert.equal(refetchCalls[0].filters.type, "active");
|
|
229
|
+
assert.equal(refetchCalls[0].options.throwOnError, false);
|
|
230
|
+
assert.equal(refetchCalls[0].filters.predicate({}), true);
|
|
231
|
+
assert.equal(refetchCalls[0].filters.predicate({ meta: {} }), true);
|
|
232
|
+
assert.equal(refetchCalls[0].filters.predicate({ meta: { jskit: { refreshOnPull: true } } }), true);
|
|
233
|
+
assert.equal(refetchCalls[0].filters.predicate({ meta: { jskitRefresh: "pull" } }), true);
|
|
234
|
+
assert.equal(refetchCalls[0].filters.predicate({ meta: { jskit: { refreshOnPull: false } } }), false);
|
|
235
|
+
assert.equal(refetchCalls[0].filters.predicate({ meta: { jskitRefresh: false } }), false);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("shell refresh runtime reports recoverable retry errors as banners", async () => {
|
|
240
|
+
await withFetchStub({ surfaceAccess: { home: true } }, async () => {
|
|
241
|
+
const queryClient = {
|
|
242
|
+
async refetchQueries() {
|
|
243
|
+
throw new Error("Network unavailable");
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
const app = createAppDouble({ queryClient });
|
|
247
|
+
const provider = new ShellWebClientProvider();
|
|
248
|
+
provider.register(app);
|
|
249
|
+
|
|
250
|
+
const refreshRuntime = app.make("runtime.web-refresh.client");
|
|
251
|
+
const result = await refreshRuntime.refresh("test-refresh");
|
|
252
|
+
|
|
253
|
+
assert.equal(result.error instanceof Error, true);
|
|
254
|
+
|
|
255
|
+
const errorStore = app.make("runtime.web-error.presentation-store.client");
|
|
256
|
+
const state = errorStore.getState();
|
|
257
|
+
assert.equal(state.channels.banner.length, 1);
|
|
258
|
+
assert.equal(state.channels.banner[0].message, "Unable to refresh. Check the connection and try again.");
|
|
259
|
+
assert.equal(state.channels.banner[0].action.label, "Retry");
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
171
263
|
test("shell web client provider resolves surface config from client app config", async () => {
|
|
172
264
|
setClientAppConfig({
|
|
173
265
|
tenancyMode: "workspaces",
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import test from "node:test";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { assertGeneratedUiSourceContract } from "@jskit-ai/kernel/shared/support/generatedUiContract";
|
|
6
7
|
import descriptor from "../package.descriptor.mjs";
|
|
7
8
|
|
|
8
9
|
const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -24,6 +25,18 @@ function readContributions(target = "") {
|
|
|
24
25
|
: [];
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
function readTopology(id = "", owner = "") {
|
|
29
|
+
const placements = descriptor?.metadata?.ui?.placements?.topology?.placements;
|
|
30
|
+
const normalizedId = String(id || "").trim();
|
|
31
|
+
const normalizedOwner = String(owner || "").trim();
|
|
32
|
+
return Array.isArray(placements)
|
|
33
|
+
? placements.filter((entry) =>
|
|
34
|
+
String(entry?.id || "").trim() === normalizedId &&
|
|
35
|
+
String(entry?.owner || "").trim() === normalizedOwner
|
|
36
|
+
)
|
|
37
|
+
: [];
|
|
38
|
+
}
|
|
39
|
+
|
|
27
40
|
function findFileMutation(id) {
|
|
28
41
|
const files = descriptor?.mutations?.files;
|
|
29
42
|
return Array.isArray(files)
|
|
@@ -35,10 +48,121 @@ test("shell-web home settings template exposes surface-derived settings outlets"
|
|
|
35
48
|
const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "pages", "home", "settings.vue"), "utf8");
|
|
36
49
|
|
|
37
50
|
assert.match(source, /target="home-settings:primary-menu"/);
|
|
38
|
-
assert.match(source, /
|
|
51
|
+
assert.match(source, /generated-ui-screen generated-ui-screen--settings settings-shell/);
|
|
52
|
+
assert.match(source, /--generated-ui-screen-title-size/);
|
|
53
|
+
assert.doesNotMatch(source, /default-link-component-token/);
|
|
39
54
|
assert.match(source, /<RouterView \/>/);
|
|
40
55
|
});
|
|
41
56
|
|
|
57
|
+
test("shell-web shell layout registers navigation at the app layout level", async () => {
|
|
58
|
+
const source = await readFile(path.join(PACKAGE_DIR, "src", "client", "components", "ShellLayout.vue"), "utf8");
|
|
59
|
+
|
|
60
|
+
assert.doesNotMatch(source, /<v-layout\b/);
|
|
61
|
+
assert.doesNotMatch(source, /overflow-hidden/);
|
|
62
|
+
assert.doesNotMatch(source, /min-height:\s*72vh/);
|
|
63
|
+
assert.match(source, /ShellRouteTransition/);
|
|
64
|
+
assert.match(source, /<ShellRouteTransition>[\s\S]*<slot \/>[\s\S]*<\/ShellRouteTransition>/);
|
|
65
|
+
assert.match(source, /data-testid="jskit-shell-app-bar"/);
|
|
66
|
+
assert.match(source, /:density="isCompactLayout \? 'compact' : 'comfortable'"/);
|
|
67
|
+
assert.match(source, /shell-layout__surface-label/);
|
|
68
|
+
assert.doesNotMatch(source, /shell-layout__surface-chip/);
|
|
69
|
+
assert.doesNotMatch(source, /<v-chip[^>]*resolvedSurfaceLabel/);
|
|
70
|
+
assert.match(source, /shell-layout__top-right[\s\S]*max-width:\s*min\(45vw, 18rem\)/);
|
|
71
|
+
assert.match(source, /<v-bottom-navigation[\s\S]*target="shell-layout:primary-bottom-nav"/);
|
|
72
|
+
assert.match(source, /<v-bottom-sheet[\s\S]*target="shell-layout:supporting-bottom-sheet"/);
|
|
73
|
+
assert.match(source, /target="shell-layout:supporting-side-panel"/);
|
|
74
|
+
assert.match(source, /data-testid="jskit-shell-supporting-bottom-sheet"/);
|
|
75
|
+
assert.match(source, /data-testid="jskit-shell-supporting-side-panel"/);
|
|
76
|
+
assert.match(source, /inject\("jskit\.shell-web\.runtime\.web-refresh\.client", null\)/);
|
|
77
|
+
assert.match(source, /window\.addEventListener\("pointerdown", handlePullPointerDown/);
|
|
78
|
+
assert.match(source, /window\.addEventListener\("touchmove", handlePullTouchMove/);
|
|
79
|
+
assert.match(source, /refreshRuntime\.refresh\("pull-to-refresh"\)/);
|
|
80
|
+
assert.match(source, /data-testid="jskit-shell-pull-refresh"/);
|
|
81
|
+
assert.match(source, /target="shell-layout:primary-menu"[\s\S]*default/);
|
|
82
|
+
assert.doesNotMatch(source, /target="shell-layout:primary-bottom-nav"[\s\S]*default/);
|
|
83
|
+
assert.match(source, /data-testid="jskit-shell-drawer"/);
|
|
84
|
+
assert.match(source, /data-testid="jskit-shell-bottom-nav"/);
|
|
85
|
+
assert.match(source, /padding:\s*0\.75rem 1rem calc\(1rem \+ env\(safe-area-inset-bottom, 0px\)\)/);
|
|
86
|
+
|
|
87
|
+
const template = await readFile(path.join(PACKAGE_DIR, "templates", "src", "components", "ShellLayout.vue"), "utf8");
|
|
88
|
+
|
|
89
|
+
assert.match(template, /PackageShellLayout from "@jskit-ai\/shell-web\/client\/components\/ShellLayout"/);
|
|
90
|
+
assert.match(template, /h\(PackageShellLayout, attrs, slots\)/);
|
|
91
|
+
assert.doesNotMatch(template, /ShellOutlet|ShellRouteTransition|useShellLayoutState|pointerdown|v-navigation-drawer|v-bottom-navigation/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("shell-web error host uses one explicit close affordance for banner errors", async () => {
|
|
95
|
+
const source = await readFile(path.join(PACKAGE_DIR, "src", "client", "components", "ShellErrorHost.vue"), "utf8");
|
|
96
|
+
|
|
97
|
+
assert.match(source, /function resolveSeverityIcon/);
|
|
98
|
+
assert.match(source, /mdi-alert-outline/);
|
|
99
|
+
assert.match(source, /:icon="resolveSeverityIcon\(entry\.severity\)"/);
|
|
100
|
+
assert.match(source, /closable[\s\S]*@click:close="dismiss\(entry\)"/);
|
|
101
|
+
assert.doesNotMatch(source, /mdi-close/);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("shell-web error host keeps snackbar color stable while closing", async () => {
|
|
105
|
+
const source = await readFile(path.join(PACKAGE_DIR, "src", "client", "components", "ShellErrorHost.vue"), "utf8");
|
|
106
|
+
|
|
107
|
+
assert.match(source, /displayedSnackbarEntry/);
|
|
108
|
+
assert.match(source, /@after-leave="onSnackbarAfterLeave"/);
|
|
109
|
+
assert.match(source, /:color="displayedSnackbarEntry \? resolveSeverityColor\(displayedSnackbarEntry\.severity\) : undefined"/);
|
|
110
|
+
assert.doesNotMatch(source, /resolveSeverityColor\(snackbarEntry\?\.severity\)/);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("shell-web error template uses intent-driven default presentation", async () => {
|
|
114
|
+
const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "error.js"), "utf8");
|
|
115
|
+
|
|
116
|
+
assert.match(source, /resourceLoadChannel:\s*"silent"/);
|
|
117
|
+
assert.match(source, /actionFeedbackChannel:\s*"snackbar"/);
|
|
118
|
+
assert.match(source, /appRecoverableChannel:\s*"banner"/);
|
|
119
|
+
assert.match(source, /blockingChannel:\s*"dialog"/);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("shell-web installs generated adaptive shell Playwright smoke coverage", async () => {
|
|
123
|
+
const source = await readFile(path.join(PACKAGE_DIR, "templates", "tests", "e2e", "adaptive-shell.spec.ts"), "utf8");
|
|
124
|
+
const helperSource = await readFile(path.join(PACKAGE_DIR, "src", "test", "adaptiveShellSmoke.js"), "utf8");
|
|
125
|
+
|
|
126
|
+
assertGeneratedUiSourceContract(helperSource, {
|
|
127
|
+
profile: "responsive-smoke",
|
|
128
|
+
sourceName: "adaptiveShellSmoke.js"
|
|
129
|
+
});
|
|
130
|
+
assert.match(source, /runAdaptiveShellSmoke/);
|
|
131
|
+
assert.match(source, /@jskit-ai\/shell-web\/test\/adaptiveShellSmoke/);
|
|
132
|
+
assert.match(helperSource, /generated adaptive shell smoke/);
|
|
133
|
+
assert.match(helperSource, /390/);
|
|
134
|
+
assert.match(helperSource, /768/);
|
|
135
|
+
assert.match(helperSource, /1280/);
|
|
136
|
+
assert.match(helperSource, /jskit-shell-bottom-nav/);
|
|
137
|
+
assert.match(helperSource, /jskit-shell-drawer/);
|
|
138
|
+
assert.match(helperSource, /scrollWidth/);
|
|
139
|
+
assert.match(helperSource, /toBeGreaterThanOrEqual\(48\)/);
|
|
140
|
+
|
|
141
|
+
const packageJson = JSON.parse(await readFile(path.join(PACKAGE_DIR, "package.json"), "utf8"));
|
|
142
|
+
assert.equal(packageJson?.exports?.["./test/adaptiveShellSmoke"], "./src/test/adaptiveShellSmoke.js");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("shell-web route transition keeps mobile route motion placement-driven", async () => {
|
|
146
|
+
const source = await readFile(
|
|
147
|
+
path.join(PACKAGE_DIR, "src", "client", "components", "ShellRouteTransition.vue"),
|
|
148
|
+
"utf8"
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
assert.match(source, /target:\s*\{[\s\S]*default:\s*"shell-layout:primary-bottom-nav"/);
|
|
152
|
+
assert.match(source, /semanticTarget:\s*\{[\s\S]*default:\s*"shell\.primary-nav"/);
|
|
153
|
+
assert.match(source, /placementRuntime\s*\.\s*getPlacements/);
|
|
154
|
+
assert.match(source, /useRouter\(\)/);
|
|
155
|
+
assert.match(source, /swipeEnabled:\s*\{[\s\S]*default:\s*true/);
|
|
156
|
+
assert.match(source, /window\.addEventListener\("pointerdown", handlePointerDown/);
|
|
157
|
+
assert.match(source, /document\.documentElement\.classList\.toggle\("shell-route-swipe-enabled", enabled\)/);
|
|
158
|
+
assert.match(source, /navigateBySwipe\(deltaX < 0 \? 1 : -1\)/);
|
|
159
|
+
assert.match(source, /router\.push\(nextEntry\.href\)/);
|
|
160
|
+
assert.match(source, /isSwipeIgnoredTarget/);
|
|
161
|
+
assert.match(source, /touch-action:\s*pan-y/);
|
|
162
|
+
assert.match(source, /transitionDirection\.value = nextIndex > previousIndex \? "forward" : "reverse"/);
|
|
163
|
+
assert.match(source, /prefers-reduced-motion:\s*reduce/);
|
|
164
|
+
});
|
|
165
|
+
|
|
42
166
|
test("shell-web settings landing page redirects to the starter child page", async () => {
|
|
43
167
|
const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "pages", "home", "settings", "index.vue"), "utf8");
|
|
44
168
|
|
|
@@ -47,24 +171,28 @@ test("shell-web settings landing page redirects to the starter child page", asyn
|
|
|
47
171
|
assert.match(source, /redirectToChild\("general"\)/);
|
|
48
172
|
});
|
|
49
173
|
|
|
50
|
-
test("shell-web settings general child page exposes
|
|
174
|
+
test("shell-web settings general child page exposes an adaptive drawer preference", async () => {
|
|
51
175
|
const source = await readFile(
|
|
52
176
|
path.join(PACKAGE_DIR, "templates", "src", "pages", "home", "settings", "general", "index.vue"),
|
|
53
177
|
"utf8"
|
|
54
178
|
);
|
|
55
179
|
|
|
56
180
|
assert.match(source, /useShellLayoutState/);
|
|
181
|
+
assert.match(source, /generated-ui-screen generated-ui-screen--settings settings-general-screen/);
|
|
57
182
|
assert.match(source, /drawerDefaultOpen/);
|
|
58
183
|
assert.match(source, /setDrawerDefaultOpen/);
|
|
59
|
-
assert.match(source, /
|
|
60
|
-
assert.match(source, /
|
|
184
|
+
assert.match(source, /Phone layouts keep primary navigation in the bottom bar/);
|
|
185
|
+
assert.match(source, /Open drawer by default on wider screens/);
|
|
186
|
+
assert.match(source, /min-height:\s*48px/);
|
|
187
|
+
assert.doesNotMatch(source, /live in this browser only|tiny example|starter settings/);
|
|
61
188
|
});
|
|
62
189
|
|
|
63
|
-
test("shell-web placement template seeds default Home and Settings
|
|
190
|
+
test("shell-web placement template seeds default Home and Settings adaptive navigation", async () => {
|
|
64
191
|
const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "placement.js"), "utf8");
|
|
65
192
|
|
|
66
193
|
assert.match(source, /id: "shell-web\.home\.menu\.home"/);
|
|
67
|
-
assert.match(source, /target: "shell
|
|
194
|
+
assert.match(source, /target: "shell\.primary-nav"/);
|
|
195
|
+
assert.match(source, /kind: "link"/);
|
|
68
196
|
assert.match(source, /surfaces: \["home"\]/);
|
|
69
197
|
assert.match(source, /label: "Home"/);
|
|
70
198
|
assert.match(source, /unscopedSuffix: "\/"/);
|
|
@@ -72,19 +200,64 @@ test("shell-web placement template seeds default Home and Settings drawer naviga
|
|
|
72
200
|
assert.match(source, /label: "Settings"/);
|
|
73
201
|
assert.match(source, /unscopedSuffix: "\/settings"/);
|
|
74
202
|
assert.match(source, /id: "shell-web\.home\.settings\.general"/);
|
|
75
|
-
assert.match(source, /target: "
|
|
203
|
+
assert.match(source, /target: "page\.section-nav"/);
|
|
204
|
+
assert.match(source, /owner: "home-settings"/);
|
|
76
205
|
assert.match(source, /label: "General"/);
|
|
77
206
|
assert.match(source, /unscopedSuffix: "\/settings\/general"/);
|
|
78
207
|
assert.doesNotMatch(source, /to: "\.\/general"/);
|
|
79
208
|
});
|
|
80
209
|
|
|
81
|
-
test("shell-web
|
|
210
|
+
test("shell-web placement topology seeds global actions as a semantic shell placement", async () => {
|
|
211
|
+
const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "placementTopology.js"), "utf8");
|
|
212
|
+
|
|
213
|
+
assert.match(source, /id: "shell\.global-actions"/);
|
|
214
|
+
assert.match(source, /description: "Global surface actions that should stay outside primary navigation\."/);
|
|
215
|
+
assert.match(source, /outlet: "shell-layout:top-right"/);
|
|
216
|
+
assert.match(source, /renderers: menuLinkRenderers/);
|
|
217
|
+
assert.match(source, /id: "page\.supporting-content"/);
|
|
218
|
+
assert.match(source, /outlet: "shell-layout:supporting-bottom-sheet"/);
|
|
219
|
+
assert.match(source, /outlet: "shell-layout:supporting-side-panel"/);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("shell-web descriptor metadata advertises adaptive shell outlets, default links, and installs the scaffold page", () => {
|
|
223
|
+
assert.deepEqual(
|
|
224
|
+
readOutlets("shell-layout:primary-bottom-nav"),
|
|
225
|
+
[
|
|
226
|
+
{
|
|
227
|
+
target: "shell-layout:primary-bottom-nav",
|
|
228
|
+
surfaces: ["*"],
|
|
229
|
+
source: "src/client/components/ShellLayout.vue"
|
|
230
|
+
}
|
|
231
|
+
]
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
assert.deepEqual(
|
|
235
|
+
readOutlets("shell-layout:supporting-bottom-sheet"),
|
|
236
|
+
[
|
|
237
|
+
{
|
|
238
|
+
target: "shell-layout:supporting-bottom-sheet",
|
|
239
|
+
surfaces: ["*"],
|
|
240
|
+
source: "src/client/components/ShellLayout.vue"
|
|
241
|
+
}
|
|
242
|
+
]
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
assert.deepEqual(
|
|
246
|
+
readOutlets("shell-layout:supporting-side-panel"),
|
|
247
|
+
[
|
|
248
|
+
{
|
|
249
|
+
target: "shell-layout:supporting-side-panel",
|
|
250
|
+
surfaces: ["*"],
|
|
251
|
+
source: "src/client/components/ShellLayout.vue"
|
|
252
|
+
}
|
|
253
|
+
]
|
|
254
|
+
);
|
|
255
|
+
|
|
82
256
|
assert.deepEqual(
|
|
83
257
|
readOutlets("home-settings:primary-menu"),
|
|
84
258
|
[
|
|
85
259
|
{
|
|
86
260
|
target: "home-settings:primary-menu",
|
|
87
|
-
defaultLinkComponentToken: "local.main.ui.surface-aware-menu-link-item",
|
|
88
261
|
surfaces: ["home"],
|
|
89
262
|
source: "templates/src/pages/home/settings.vue"
|
|
90
263
|
}
|
|
@@ -92,41 +265,66 @@ test("shell-web descriptor metadata advertises home settings outlets, default dr
|
|
|
92
265
|
);
|
|
93
266
|
|
|
94
267
|
assert.deepEqual(
|
|
95
|
-
readContributions("shell
|
|
268
|
+
readContributions("shell.primary-nav"),
|
|
96
269
|
[
|
|
97
270
|
{
|
|
98
271
|
id: "shell-web.home.menu.home",
|
|
99
|
-
target: "shell
|
|
272
|
+
target: "shell.primary-nav",
|
|
273
|
+
kind: "link",
|
|
100
274
|
surfaces: ["home"],
|
|
101
275
|
order: 50,
|
|
102
|
-
componentToken: "local.main.ui.surface-aware-menu-link-item",
|
|
103
276
|
source: "templates/src/placement.js"
|
|
104
277
|
},
|
|
105
278
|
{
|
|
106
279
|
id: "shell-web.home.menu.settings",
|
|
107
|
-
target: "shell
|
|
280
|
+
target: "shell.primary-nav",
|
|
281
|
+
kind: "link",
|
|
108
282
|
surfaces: ["home"],
|
|
109
283
|
order: 100,
|
|
110
|
-
componentToken: "local.main.ui.surface-aware-menu-link-item",
|
|
111
284
|
source: "templates/src/placement.js"
|
|
112
285
|
}
|
|
113
286
|
]
|
|
114
287
|
);
|
|
115
288
|
|
|
116
289
|
assert.deepEqual(
|
|
117
|
-
readContributions("
|
|
290
|
+
readContributions("page.section-nav"),
|
|
118
291
|
[
|
|
119
292
|
{
|
|
120
293
|
id: "shell-web.home.settings.general",
|
|
121
|
-
target: "
|
|
294
|
+
target: "page.section-nav",
|
|
295
|
+
owner: "home-settings",
|
|
296
|
+
kind: "link",
|
|
122
297
|
surfaces: ["home"],
|
|
123
298
|
order: 100,
|
|
124
|
-
componentToken: "local.main.ui.surface-aware-menu-link-item",
|
|
125
299
|
source: "templates/src/placement.js"
|
|
126
300
|
}
|
|
127
301
|
]
|
|
128
302
|
);
|
|
129
303
|
|
|
304
|
+
assert.equal(readTopology("shell.primary-nav").length, 1);
|
|
305
|
+
assert.equal(readTopology("shell.primary-nav")[0]?.variants?.compact?.outlet, "shell-layout:primary-bottom-nav");
|
|
306
|
+
assert.equal(
|
|
307
|
+
readTopology("shell.primary-nav")[0]?.variants?.compact?.renderers?.link,
|
|
308
|
+
"local.main.ui.tab-link-item"
|
|
309
|
+
);
|
|
310
|
+
assert.equal(readTopology("shell.primary-nav")[0]?.variants?.medium?.outlet, "shell-layout:primary-menu");
|
|
311
|
+
assert.equal(readTopology("shell.global-actions").length, 1);
|
|
312
|
+
assert.equal(readTopology("shell.global-actions")[0]?.variants?.compact?.outlet, "shell-layout:top-right");
|
|
313
|
+
assert.equal(
|
|
314
|
+
readTopology("shell.global-actions")[0]?.variants?.compact?.renderers?.link,
|
|
315
|
+
"local.main.ui.surface-aware-menu-link-item"
|
|
316
|
+
);
|
|
317
|
+
assert.equal(readTopology("page.section-nav", "home-settings").length, 1);
|
|
318
|
+
assert.equal(readTopology("page.supporting-content").length, 1);
|
|
319
|
+
assert.equal(
|
|
320
|
+
readTopology("page.supporting-content")[0]?.variants?.compact?.outlet,
|
|
321
|
+
"shell-layout:supporting-bottom-sheet"
|
|
322
|
+
);
|
|
323
|
+
assert.equal(
|
|
324
|
+
readTopology("page.supporting-content")[0]?.variants?.expanded?.outlet,
|
|
325
|
+
"shell-layout:supporting-side-panel"
|
|
326
|
+
);
|
|
327
|
+
|
|
130
328
|
assert.deepEqual(findFileMutation("shell-web-page-home-settings-shell"), {
|
|
131
329
|
from: "templates/src/pages/home/settings.vue",
|
|
132
330
|
toSurface: "home",
|
|
@@ -156,13 +354,29 @@ test("shell-web descriptor metadata advertises home settings outlets, default dr
|
|
|
156
354
|
category: "shell-web",
|
|
157
355
|
id: "shell-web-page-home-settings-general"
|
|
158
356
|
});
|
|
357
|
+
|
|
358
|
+
assert.deepEqual(findFileMutation("shell-web-test-adaptive-shell-smoke"), {
|
|
359
|
+
from: "templates/tests/e2e/adaptive-shell.spec.ts",
|
|
360
|
+
to: "tests/e2e/adaptive-shell.spec.ts",
|
|
361
|
+
ownership: "app",
|
|
362
|
+
reason: "Install compact/medium/expanded Playwright smoke coverage for the adaptive shell.",
|
|
363
|
+
category: "shell-web",
|
|
364
|
+
id: "shell-web-test-adaptive-shell-smoke"
|
|
365
|
+
});
|
|
159
366
|
});
|
|
160
367
|
|
|
161
|
-
test("shell-web home starter page relies on
|
|
368
|
+
test("shell-web home starter page relies on adaptive shell navigation instead of dead feature buttons", async () => {
|
|
162
369
|
const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "pages", "home", "index.vue"), "utf8");
|
|
163
370
|
|
|
164
|
-
|
|
165
|
-
|
|
371
|
+
assertGeneratedUiSourceContract(source, {
|
|
372
|
+
profile: "shell-home",
|
|
373
|
+
sourceName: "shell-web home/index.vue"
|
|
374
|
+
});
|
|
375
|
+
assert.match(source, /generated-ui-screen generated-ui-screen--app home-surface-screen/);
|
|
376
|
+
assert.match(source, /--generated-ui-screen-title-size/);
|
|
377
|
+
assert.match(source, /Core services are available\./);
|
|
378
|
+
assert.match(source, /to="\/home\/settings\/general"/);
|
|
379
|
+
assert.doesNotMatch(source, /Use bottom navigation|Replace this content|Main public surface/);
|
|
166
380
|
assert.doesNotMatch(source, /\/console/);
|
|
167
381
|
assert.doesNotMatch(source, /\/auth\/signout/);
|
|
168
382
|
});
|
|
@@ -65,3 +65,22 @@ test("shell layout store keeps drawer state and default preference in sync", ()
|
|
|
65
65
|
store.setDrawerOpen(false);
|
|
66
66
|
assert.equal(store.drawerOpen, false);
|
|
67
67
|
});
|
|
68
|
+
|
|
69
|
+
test("shell layout store keeps supporting content closed until explicitly opened", () => {
|
|
70
|
+
const pinia = createPinia();
|
|
71
|
+
const store = useShellLayoutStore(pinia);
|
|
72
|
+
|
|
73
|
+
assert.equal(store.supportingContentOpen, false);
|
|
74
|
+
assert.equal(store.supportingContentTitle, "");
|
|
75
|
+
|
|
76
|
+
store.openSupportingContent({ title: "Customer details" });
|
|
77
|
+
assert.equal(store.supportingContentOpen, true);
|
|
78
|
+
assert.equal(store.supportingContentTitle, "Customer details");
|
|
79
|
+
|
|
80
|
+
store.setSupportingContentOpen(false);
|
|
81
|
+
assert.equal(store.supportingContentOpen, false);
|
|
82
|
+
assert.equal(store.supportingContentTitle, "Customer details");
|
|
83
|
+
|
|
84
|
+
store.closeSupportingContent();
|
|
85
|
+
assert.equal(store.supportingContentOpen, false);
|
|
86
|
+
});
|