@jskit-ai/shell-web 0.1.65 → 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 +74 -9
- package/package.json +8 -7
- package/src/client/components/ShellErrorHost.vue +88 -15
- package/src/client/components/ShellLayout.vue +551 -46
- 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/providers/ShellWebClientProvider.js +163 -39
- 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 -86
- 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 -21
- package/templates/src/placementTopology.js +43 -2
- 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/placementRuntime.test.js +37 -0
- package/test/provider.test.js +97 -5
- package/test/settingsPlacementContract.test.js +205 -8
- package/test/useShellLayoutState.test.js +19 -0
|
@@ -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));
|
|
@@ -47,10 +48,121 @@ test("shell-web home settings template exposes surface-derived settings outlets"
|
|
|
47
48
|
const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "pages", "home", "settings.vue"), "utf8");
|
|
48
49
|
|
|
49
50
|
assert.match(source, /target="home-settings:primary-menu"/);
|
|
51
|
+
assert.match(source, /generated-ui-screen generated-ui-screen--settings settings-shell/);
|
|
52
|
+
assert.match(source, /--generated-ui-screen-title-size/);
|
|
50
53
|
assert.doesNotMatch(source, /default-link-component-token/);
|
|
51
54
|
assert.match(source, /<RouterView \/>/);
|
|
52
55
|
});
|
|
53
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
|
+
|
|
54
166
|
test("shell-web settings landing page redirects to the starter child page", async () => {
|
|
55
167
|
const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "pages", "home", "settings", "index.vue"), "utf8");
|
|
56
168
|
|
|
@@ -59,20 +171,23 @@ test("shell-web settings landing page redirects to the starter child page", asyn
|
|
|
59
171
|
assert.match(source, /redirectToChild\("general"\)/);
|
|
60
172
|
});
|
|
61
173
|
|
|
62
|
-
test("shell-web settings general child page exposes
|
|
174
|
+
test("shell-web settings general child page exposes an adaptive drawer preference", async () => {
|
|
63
175
|
const source = await readFile(
|
|
64
176
|
path.join(PACKAGE_DIR, "templates", "src", "pages", "home", "settings", "general", "index.vue"),
|
|
65
177
|
"utf8"
|
|
66
178
|
);
|
|
67
179
|
|
|
68
180
|
assert.match(source, /useShellLayoutState/);
|
|
181
|
+
assert.match(source, /generated-ui-screen generated-ui-screen--settings settings-general-screen/);
|
|
69
182
|
assert.match(source, /drawerDefaultOpen/);
|
|
70
183
|
assert.match(source, /setDrawerDefaultOpen/);
|
|
71
|
-
assert.match(source, /
|
|
72
|
-
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/);
|
|
73
188
|
});
|
|
74
189
|
|
|
75
|
-
test("shell-web placement template seeds default Home and Settings
|
|
190
|
+
test("shell-web placement template seeds default Home and Settings adaptive navigation", async () => {
|
|
76
191
|
const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "placement.js"), "utf8");
|
|
77
192
|
|
|
78
193
|
assert.match(source, /id: "shell-web\.home\.menu\.home"/);
|
|
@@ -92,7 +207,52 @@ test("shell-web placement template seeds default Home and Settings drawer naviga
|
|
|
92
207
|
assert.doesNotMatch(source, /to: "\.\/general"/);
|
|
93
208
|
});
|
|
94
209
|
|
|
95
|
-
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
|
+
|
|
96
256
|
assert.deepEqual(
|
|
97
257
|
readOutlets("home-settings:primary-menu"),
|
|
98
258
|
[
|
|
@@ -142,7 +302,28 @@ test("shell-web descriptor metadata advertises home settings outlets, default dr
|
|
|
142
302
|
);
|
|
143
303
|
|
|
144
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
|
+
);
|
|
145
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
|
+
);
|
|
146
327
|
|
|
147
328
|
assert.deepEqual(findFileMutation("shell-web-page-home-settings-shell"), {
|
|
148
329
|
from: "templates/src/pages/home/settings.vue",
|
|
@@ -173,13 +354,29 @@ test("shell-web descriptor metadata advertises home settings outlets, default dr
|
|
|
173
354
|
category: "shell-web",
|
|
174
355
|
id: "shell-web-page-home-settings-general"
|
|
175
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
|
+
});
|
|
176
366
|
});
|
|
177
367
|
|
|
178
|
-
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 () => {
|
|
179
369
|
const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "pages", "home", "index.vue"), "utf8");
|
|
180
370
|
|
|
181
|
-
|
|
182
|
-
|
|
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/);
|
|
183
380
|
assert.doesNotMatch(source, /\/console/);
|
|
184
381
|
assert.doesNotMatch(source, /\/auth\/signout/);
|
|
185
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
|
+
});
|