@jskit-ai/ui-generator 0.1.49 → 0.1.50
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 +87 -11
- package/package.json +3 -3
- package/src/server/buildTemplateContext.js +36 -2
- package/src/server/subcommands/addSubpages.js +32 -20
- package/src/server/subcommands/element.js +17 -3
- package/src/server/subcommands/outlet.js +305 -60
- package/src/server/subcommands/page.js +43 -22
- package/src/server/subcommands/pageSupport.js +114 -36
- package/src/server/subcommands/support.js +126 -23
- package/test/addSubpagesSubcommand.test.js +132 -8
- package/test/buildTemplateContext.test.js +31 -0
- package/test/elementSubcommand.test.js +13 -1
- package/test/outletSubcommand.test.js +258 -0
- package/test/packageDescriptor.test.js +11 -0
- package/test/pageSubcommand.test.js +134 -5
|
@@ -75,6 +75,8 @@ import { computed } from "vue";
|
|
|
75
75
|
assert.match(topologySource, /compact: \{/);
|
|
76
76
|
assert.match(topologySource, /medium: \{/);
|
|
77
77
|
assert.match(topologySource, /expanded: \{/);
|
|
78
|
+
assert.match(topologySource, /renderers: \{/);
|
|
79
|
+
assert.match(topologySource, /link: "local\.main\.ui\.surface-aware-menu-link-item"/);
|
|
78
80
|
|
|
79
81
|
const rerun = await runGeneratorSubcommand({
|
|
80
82
|
appRoot,
|
|
@@ -90,6 +92,224 @@ import { computed } from "vue";
|
|
|
90
92
|
});
|
|
91
93
|
});
|
|
92
94
|
|
|
95
|
+
test("ui-generator outlet can inject a concrete outlet without topology", async () => {
|
|
96
|
+
await withTempApp(async (appRoot) => {
|
|
97
|
+
const targetFile = "src/components/ReportsPanel.vue";
|
|
98
|
+
const targetPath = path.join(appRoot, targetFile);
|
|
99
|
+
|
|
100
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
101
|
+
await writeFile(targetPath, "<template><section /></template>\n", "utf8");
|
|
102
|
+
|
|
103
|
+
const result = await runGeneratorSubcommand({
|
|
104
|
+
appRoot,
|
|
105
|
+
subcommand: "outlet",
|
|
106
|
+
args: [targetFile],
|
|
107
|
+
options: {
|
|
108
|
+
target: "reports:bottom-actions"
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
assert.deepEqual(result.touchedFiles, [targetFile]);
|
|
113
|
+
const output = await readFile(targetPath, "utf8");
|
|
114
|
+
assert.match(output, /<ShellOutlet target="reports:bottom-actions" \/>/);
|
|
115
|
+
const topologySource = await readFile(path.join(appRoot, "src", "placementTopology.js"), "utf8");
|
|
116
|
+
assert.doesNotMatch(topologySource, /reports\.actions/);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("ui-generator topology creates adaptive component topology without injecting outlets", async () => {
|
|
121
|
+
await withTempApp(async (appRoot) => {
|
|
122
|
+
const result = await runGeneratorSubcommand({
|
|
123
|
+
appRoot,
|
|
124
|
+
subcommand: "topology",
|
|
125
|
+
args: [],
|
|
126
|
+
options: {
|
|
127
|
+
placement: "reports.actions",
|
|
128
|
+
kind: "component",
|
|
129
|
+
"compact-target": "reports:bottom-actions",
|
|
130
|
+
"medium-target": "reports:toolbar-actions",
|
|
131
|
+
"expanded-target": "reports:side-actions",
|
|
132
|
+
surface: "admin",
|
|
133
|
+
description: "Report action controls."
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
assert.deepEqual(result.touchedFiles, ["src/placementTopology.js"]);
|
|
138
|
+
const topologySource = await readFile(path.join(appRoot, "src", "placementTopology.js"), "utf8");
|
|
139
|
+
assert.match(topologySource, /id: "reports\.actions"/);
|
|
140
|
+
assert.match(topologySource, /description: "Report action controls\."/);
|
|
141
|
+
assert.match(topologySource, /surfaces: \["admin"\]/);
|
|
142
|
+
assert.match(topologySource, /compact: \{\n {6}outlet: "reports:bottom-actions"\n {4}\}/);
|
|
143
|
+
assert.match(topologySource, /medium: \{\n {6}outlet: "reports:toolbar-actions"\n {4}\}/);
|
|
144
|
+
assert.match(topologySource, /expanded: \{\n {6}outlet: "reports:side-actions"\n {4}\}/);
|
|
145
|
+
assert.doesNotMatch(topologySource, /renderers: \{/);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("ui-generator topology creates link topology with owner inference for one target", async () => {
|
|
150
|
+
await withTempApp(async (appRoot) => {
|
|
151
|
+
const result = await runGeneratorSubcommand({
|
|
152
|
+
appRoot,
|
|
153
|
+
subcommand: "topology",
|
|
154
|
+
args: [],
|
|
155
|
+
options: {
|
|
156
|
+
placement: "page.section-nav",
|
|
157
|
+
kind: "link",
|
|
158
|
+
target: "report-view:sub-pages",
|
|
159
|
+
surface: "admin"
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
assert.deepEqual(result.touchedFiles, ["src/placementTopology.js"]);
|
|
164
|
+
const topologySource = await readFile(path.join(appRoot, "src", "placementTopology.js"), "utf8");
|
|
165
|
+
assert.match(topologySource, /id: "page\.section-nav"/);
|
|
166
|
+
assert.match(topologySource, /owner: "report-view"/);
|
|
167
|
+
assert.match(topologySource, /compact: \{\n {6}outlet: "report-view:sub-pages",/);
|
|
168
|
+
assert.match(topologySource, /medium: \{\n {6}outlet: "report-view:sub-pages",/);
|
|
169
|
+
assert.match(topologySource, /expanded: \{\n {6}outlet: "report-view:sub-pages",/);
|
|
170
|
+
assert.match(topologySource, /link: "local\.main\.ui\.surface-aware-menu-link-item"/);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("ui-generator topology treats existing app topology id and owner as already present without a marker", async () => {
|
|
175
|
+
await withTempApp(async (appRoot) => {
|
|
176
|
+
await writeFile(
|
|
177
|
+
path.join(appRoot, "src", "placementTopology.js"),
|
|
178
|
+
`export default {
|
|
179
|
+
placements: [
|
|
180
|
+
{
|
|
181
|
+
id: "page.actions",
|
|
182
|
+
owner: "customer-view",
|
|
183
|
+
surfaces: ["admin"],
|
|
184
|
+
variants: {
|
|
185
|
+
compact: { outlet: "customer-view:summary-actions" },
|
|
186
|
+
medium: { outlet: "customer-view:summary-actions" },
|
|
187
|
+
expanded: { outlet: "customer-view:summary-actions" }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
]
|
|
191
|
+
};
|
|
192
|
+
`,
|
|
193
|
+
"utf8"
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const result = await runGeneratorSubcommand({
|
|
197
|
+
appRoot,
|
|
198
|
+
subcommand: "topology",
|
|
199
|
+
args: [],
|
|
200
|
+
options: {
|
|
201
|
+
placement: "page.actions",
|
|
202
|
+
kind: "component",
|
|
203
|
+
target: "customer-view:summary-actions"
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
assert.deepEqual(result.touchedFiles, []);
|
|
208
|
+
const topologySource = await readFile(path.join(appRoot, "src", "placementTopology.js"), "utf8");
|
|
209
|
+
assert.equal((topologySource.match(/id: "page\.actions"/g) || []).length, 1);
|
|
210
|
+
assert.doesNotMatch(topologySource, /jskit:ui-generator\.topology:page\.actions:customer-view/);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("ui-generator topology rejects an existing semantic id and owner with different outlets", async () => {
|
|
215
|
+
await withTempApp(async (appRoot) => {
|
|
216
|
+
await writeFile(
|
|
217
|
+
path.join(appRoot, "src", "placementTopology.js"),
|
|
218
|
+
`export default {
|
|
219
|
+
placements: [
|
|
220
|
+
{
|
|
221
|
+
id: "page.actions",
|
|
222
|
+
owner: "customer-view",
|
|
223
|
+
surfaces: ["admin"],
|
|
224
|
+
variants: {
|
|
225
|
+
compact: { outlet: "customer-view:summary-actions" },
|
|
226
|
+
medium: { outlet: "customer-view:summary-actions" },
|
|
227
|
+
expanded: { outlet: "customer-view:summary-actions" }
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
]
|
|
231
|
+
};
|
|
232
|
+
`,
|
|
233
|
+
"utf8"
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
await assert.rejects(
|
|
237
|
+
runGeneratorSubcommand({
|
|
238
|
+
appRoot,
|
|
239
|
+
subcommand: "topology",
|
|
240
|
+
args: [],
|
|
241
|
+
options: {
|
|
242
|
+
placement: "page.actions",
|
|
243
|
+
kind: "component",
|
|
244
|
+
target: "customer-view:footer-actions"
|
|
245
|
+
}
|
|
246
|
+
}),
|
|
247
|
+
/semantic placement "page\.actions" for owner "customer-view" already exists with different outlet mapping/
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("ui-generator topology requires owner for owner-scoped placements across multiple hosts", async () => {
|
|
253
|
+
await withTempApp(async (appRoot) => {
|
|
254
|
+
await assert.rejects(
|
|
255
|
+
runGeneratorSubcommand({
|
|
256
|
+
appRoot,
|
|
257
|
+
subcommand: "topology",
|
|
258
|
+
args: [],
|
|
259
|
+
options: {
|
|
260
|
+
placement: "page.section-nav",
|
|
261
|
+
kind: "link",
|
|
262
|
+
"compact-target": "report-compact:sub-pages",
|
|
263
|
+
"medium-target": "report-medium:sub-pages",
|
|
264
|
+
"expanded-target": "report-expanded:sub-pages",
|
|
265
|
+
surface: "admin"
|
|
266
|
+
}
|
|
267
|
+
}),
|
|
268
|
+
/ui-generator topology requires --owner because semantic placement "page\.section-nav" maps to multiple outlet hosts/
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("ui-generator outlet validates topology before writing the concrete outlet", async () => {
|
|
274
|
+
await withTempApp(async (appRoot) => {
|
|
275
|
+
const targetFile = "src/components/ContactDetailsPanel.vue";
|
|
276
|
+
const targetPath = path.join(appRoot, targetFile);
|
|
277
|
+
const originalSource = "<template><div /></template>\n";
|
|
278
|
+
|
|
279
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
280
|
+
await writeFile(targetPath, originalSource, "utf8");
|
|
281
|
+
await writeFile(
|
|
282
|
+
path.join(appRoot, "src", "placementTopology.js"),
|
|
283
|
+
`export default {
|
|
284
|
+
placements: [
|
|
285
|
+
{
|
|
286
|
+
id: "page.actions",
|
|
287
|
+
owner: "contact-view",
|
|
288
|
+
variants: {}
|
|
289
|
+
}
|
|
290
|
+
]
|
|
291
|
+
};
|
|
292
|
+
`,
|
|
293
|
+
"utf8"
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
await assert.rejects(
|
|
297
|
+
runGeneratorSubcommand({
|
|
298
|
+
appRoot,
|
|
299
|
+
subcommand: "outlet",
|
|
300
|
+
args: [targetFile],
|
|
301
|
+
options: {
|
|
302
|
+
target: "contact-view:summary-actions",
|
|
303
|
+
placement: "page.actions"
|
|
304
|
+
}
|
|
305
|
+
}),
|
|
306
|
+
/requires compact topology variant/
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
assert.equal(await readFile(targetPath, "utf8"), originalSource);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
93
313
|
test("ui-generator outlet does not inject a second matching outlet", async () => {
|
|
94
314
|
await withTempApp(async (appRoot) => {
|
|
95
315
|
const targetFile = "src/pages/w/[workspaceSlug]/admin/contacts/[contactId]/index.vue";
|
|
@@ -162,6 +382,44 @@ test("ui-generator outlet creates script setup when missing", async () => {
|
|
|
162
382
|
});
|
|
163
383
|
});
|
|
164
384
|
|
|
385
|
+
test("ui-generator outlet creates script setup instead of adding template imports to normal script", async () => {
|
|
386
|
+
await withTempApp(async (appRoot) => {
|
|
387
|
+
const targetFile = "src/components/OptionsPanel.vue";
|
|
388
|
+
const targetPath = path.join(appRoot, targetFile);
|
|
389
|
+
|
|
390
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
391
|
+
await writeFile(
|
|
392
|
+
targetPath,
|
|
393
|
+
`<script>
|
|
394
|
+
export default {
|
|
395
|
+
name: "OptionsPanel"
|
|
396
|
+
};
|
|
397
|
+
</script>
|
|
398
|
+
|
|
399
|
+
<template>
|
|
400
|
+
<div>Options</div>
|
|
401
|
+
</template>
|
|
402
|
+
`,
|
|
403
|
+
"utf8"
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
await runGeneratorSubcommand({
|
|
407
|
+
appRoot,
|
|
408
|
+
subcommand: "outlet",
|
|
409
|
+
args: [targetFile],
|
|
410
|
+
options: {
|
|
411
|
+
target: "options:footer-actions",
|
|
412
|
+
placement: "page.actions"
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const output = await readFile(targetPath, "utf8");
|
|
417
|
+
assert.match(output, /<script setup>\nimport ShellOutlet from "@jskit-ai\/shell-web\/client\/components\/ShellOutlet";\n<\/script>/);
|
|
418
|
+
assert.match(output, /<script>\nexport default/);
|
|
419
|
+
assert.match(output, /<ShellOutlet target="options:footer-actions" \/>/);
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
165
423
|
test("ui-generator outlet inserts generated script after existing route block", async () => {
|
|
166
424
|
await withTempApp(async (appRoot) => {
|
|
167
425
|
const targetFile = "src/pages/w/[workspaceSlug]/admin/contacts/[contactId]/index.vue";
|
|
@@ -8,4 +8,15 @@ test("ui-generator surface options validate against enabled surface ids", () =>
|
|
|
8
8
|
assert.equal(descriptor.metadata?.generatorSubcommands?.["placed-element"]?.optionNames?.includes("surface"), true);
|
|
9
9
|
assert.equal(descriptor.metadata?.generatorSubcommands?.["placed-element"]?.requiredOptionNames?.includes("surface"), false);
|
|
10
10
|
assert.equal(descriptor.metadata?.generatorSubcommands?.page?.optionNames?.includes("force"), true);
|
|
11
|
+
assert.equal(descriptor.options?.kind?.validationType, "enum");
|
|
12
|
+
assert.equal(descriptor.options?.["navigation-role"]?.validationType, "enum");
|
|
13
|
+
assert.deepEqual(
|
|
14
|
+
descriptor.options?.["navigation-role"]?.allowedValues,
|
|
15
|
+
["primary", "secondary", "utility", "detail", "workflow", "none"]
|
|
16
|
+
);
|
|
17
|
+
assert.equal(descriptor.metadata?.generatorSubcommands?.page?.optionNames?.includes("navigation-role"), true);
|
|
18
|
+
assert.equal(descriptor.metadata?.generatorSubcommands?.outlet?.requiredOptionNames?.includes("placement"), false);
|
|
19
|
+
assert.equal(descriptor.metadata?.generatorSubcommands?.topology?.entrypoint, "src/server/subcommands/outlet.js");
|
|
20
|
+
assert.equal(descriptor.metadata?.generatorSubcommands?.topology?.optionNames?.includes("compact-target"), true);
|
|
21
|
+
assert.equal(descriptor.metadata?.generatorSubcommands?.topology?.requiredOptionNames?.includes("kind"), true);
|
|
11
22
|
});
|
|
@@ -3,6 +3,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import test from "node:test";
|
|
6
|
+
import { assertGeneratedUiSourceContract } from "@jskit-ai/kernel/shared/support/generatedUiContract";
|
|
6
7
|
import { runGeneratorSubcommand } from "../src/server/subcommands/page.js";
|
|
7
8
|
|
|
8
9
|
async function withTempApp(run) {
|
|
@@ -65,6 +66,18 @@ async function writePlacementTopology(appRoot, entries = []) {
|
|
|
65
66
|
surfaces: ["*"],
|
|
66
67
|
outlet: "shell-layout:top-right",
|
|
67
68
|
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
69
|
+
}),
|
|
70
|
+
renderTopologyEntry({
|
|
71
|
+
id: "shell.global-actions",
|
|
72
|
+
surfaces: ["*"],
|
|
73
|
+
outlet: "shell-layout:top-right",
|
|
74
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
75
|
+
}),
|
|
76
|
+
renderTopologyEntry({
|
|
77
|
+
id: "shell.secondary-nav",
|
|
78
|
+
surfaces: ["*"],
|
|
79
|
+
outlet: "shell-layout:secondary-menu",
|
|
80
|
+
linkRenderer: "local.main.ui.surface-aware-menu-link-item"
|
|
68
81
|
})
|
|
69
82
|
];
|
|
70
83
|
await writeFile(
|
|
@@ -104,6 +117,7 @@ async function writeAppFixture(appRoot, { configSource = "" } = {}) {
|
|
|
104
117
|
default
|
|
105
118
|
/>
|
|
106
119
|
<ShellOutlet target="shell-layout:top-right" />
|
|
120
|
+
<ShellOutlet target="shell-layout:secondary-menu" />
|
|
107
121
|
</div>
|
|
108
122
|
</template>
|
|
109
123
|
`,
|
|
@@ -141,7 +155,16 @@ test("ui-generator page subcommand creates an index page from an explicit target
|
|
|
141
155
|
assert.equal(result.summary, 'Generated UI page "/practice" at src/pages/w/[workspaceSlug]/admin/practice/index.vue.');
|
|
142
156
|
|
|
143
157
|
const pageSource = await readFile(path.join(appRoot, toPagePath(targetFile)), "utf8");
|
|
144
|
-
|
|
158
|
+
assertGeneratedUiSourceContract(pageSource, {
|
|
159
|
+
profile: "page",
|
|
160
|
+
sourceName: targetFile
|
|
161
|
+
});
|
|
162
|
+
assert.match(pageSource, /generated-ui-screen generated-ui-screen--operator generated-page-screen/);
|
|
163
|
+
assert.match(pageSource, /<p class="text-overline text-medium-emphasis mb-1">Workspace tool<\/p>/);
|
|
164
|
+
assert.match(pageSource, /<h1 class="generated-page-screen__title">Practice<\/h1>/);
|
|
165
|
+
assert.match(pageSource, /<v-sheet rounded="lg" border class="generated-page-screen__empty-state">/);
|
|
166
|
+
assert.match(pageSource, /No Practice activity yet/);
|
|
167
|
+
assert.doesNotMatch(pageSource, /Replace this scaffold|Use this area|This is your page|<v-card\b|v-card-title/);
|
|
145
168
|
|
|
146
169
|
const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
|
|
147
170
|
assert.match(placementSource, /id: "ui-generator\.page\.admin\.practice\.link"/);
|
|
@@ -150,7 +173,90 @@ test("ui-generator page subcommand creates an index page from an explicit target
|
|
|
150
173
|
});
|
|
151
174
|
});
|
|
152
175
|
|
|
153
|
-
test("ui-generator page subcommand
|
|
176
|
+
test("ui-generator page subcommand maps secondary navigation role to shell.secondary-nav", async () => {
|
|
177
|
+
await withTempApp(async (appRoot) => {
|
|
178
|
+
await writeAppFixture(appRoot);
|
|
179
|
+
|
|
180
|
+
const targetFile = "w/[workspaceSlug]/admin/reports/index.vue";
|
|
181
|
+
await runGeneratorSubcommand({
|
|
182
|
+
appRoot,
|
|
183
|
+
subcommand: "page",
|
|
184
|
+
args: [targetFile],
|
|
185
|
+
options: {
|
|
186
|
+
name: "Reports",
|
|
187
|
+
"navigation-role": "secondary"
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
|
|
192
|
+
assert.match(placementSource, /target: "shell\.secondary-nav"/);
|
|
193
|
+
assert.match(placementSource, /kind: "link"/);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("ui-generator page subcommand maps utility navigation role to shell.global-actions", async () => {
|
|
198
|
+
await withTempApp(async (appRoot) => {
|
|
199
|
+
await writeAppFixture(appRoot);
|
|
200
|
+
|
|
201
|
+
const targetFile = "w/[workspaceSlug]/admin/help/index.vue";
|
|
202
|
+
await runGeneratorSubcommand({
|
|
203
|
+
appRoot,
|
|
204
|
+
subcommand: "page",
|
|
205
|
+
args: [targetFile],
|
|
206
|
+
options: {
|
|
207
|
+
name: "Help",
|
|
208
|
+
"navigation-role": "utility"
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
|
|
213
|
+
assert.match(placementSource, /target: "shell\.global-actions"/);
|
|
214
|
+
assert.match(placementSource, /kind: "link"/);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("ui-generator page subcommand can generate detail pages without navigation placement", async () => {
|
|
219
|
+
await withTempApp(async (appRoot) => {
|
|
220
|
+
await writeAppFixture(appRoot);
|
|
221
|
+
|
|
222
|
+
const targetFile = "w/[workspaceSlug]/admin/reports/[reportId].vue";
|
|
223
|
+
const result = await runGeneratorSubcommand({
|
|
224
|
+
appRoot,
|
|
225
|
+
subcommand: "page",
|
|
226
|
+
args: [targetFile],
|
|
227
|
+
options: {
|
|
228
|
+
name: "Report",
|
|
229
|
+
"navigation-role": "detail"
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
assert.deepEqual(result.touchedFiles, [toPagePath(targetFile)]);
|
|
234
|
+
|
|
235
|
+
const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
|
|
236
|
+
assert.doesNotMatch(placementSource, /ui-generator\.page\.admin\.reports\.report-id\.link/);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("ui-generator page subcommand rejects no-link navigation roles with link placement options", async () => {
|
|
241
|
+
await withTempApp(async (appRoot) => {
|
|
242
|
+
await writeAppFixture(appRoot);
|
|
243
|
+
|
|
244
|
+
await assert.rejects(
|
|
245
|
+
runGeneratorSubcommand({
|
|
246
|
+
appRoot,
|
|
247
|
+
subcommand: "page",
|
|
248
|
+
args: ["w/[workspaceSlug]/admin/reports/[reportId].vue"],
|
|
249
|
+
options: {
|
|
250
|
+
"navigation-role": "detail",
|
|
251
|
+
"link-placement": "shell.primary-nav"
|
|
252
|
+
}
|
|
253
|
+
}),
|
|
254
|
+
/navigation-role "detail" cannot be combined with --link-placement/
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("ui-generator page subcommand treats dynamic file routes as detail pages by default", async () => {
|
|
154
260
|
await withTempApp(async (appRoot) => {
|
|
155
261
|
await writeAppFixture(appRoot);
|
|
156
262
|
|
|
@@ -162,10 +268,33 @@ test("ui-generator page subcommand creates a file route and derives label from t
|
|
|
162
268
|
options: {}
|
|
163
269
|
});
|
|
164
270
|
|
|
165
|
-
assert.deepEqual(result.touchedFiles, [toPagePath(targetFile)
|
|
271
|
+
assert.deepEqual(result.touchedFiles, [toPagePath(targetFile)]);
|
|
166
272
|
|
|
167
273
|
const pageSource = await readFile(path.join(appRoot, toPagePath(targetFile)), "utf8");
|
|
168
|
-
assert.match(pageSource,
|
|
274
|
+
assert.match(pageSource, /generated-ui-screen generated-ui-screen--operator generated-page-screen/);
|
|
275
|
+
assert.match(pageSource, /<h1 class="generated-page-screen__title">Contact Id<\/h1>/);
|
|
276
|
+
assert.match(pageSource, /No Contact Id activity yet/);
|
|
277
|
+
|
|
278
|
+
const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
|
|
279
|
+
assert.doesNotMatch(placementSource, /ui-generator\.page\.admin\.contacts\.contact-id\.link/);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("ui-generator page subcommand allows explicit primary navigation for dynamic routes", async () => {
|
|
284
|
+
await withTempApp(async (appRoot) => {
|
|
285
|
+
await writeAppFixture(appRoot);
|
|
286
|
+
|
|
287
|
+
const targetFile = "w/[workspaceSlug]/admin/contacts/[contactId].vue";
|
|
288
|
+
const result = await runGeneratorSubcommand({
|
|
289
|
+
appRoot,
|
|
290
|
+
subcommand: "page",
|
|
291
|
+
args: [targetFile],
|
|
292
|
+
options: {
|
|
293
|
+
"navigation-role": "primary"
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
assert.deepEqual(result.touchedFiles, [toPagePath(targetFile), "src/placement.js"]);
|
|
169
298
|
|
|
170
299
|
const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
|
|
171
300
|
assert.match(placementSource, /scopedSuffix: "\/contacts\/\[contactId\]"/);
|
|
@@ -489,7 +618,7 @@ test("ui-generator page subcommand overwrites an existing page when --force is p
|
|
|
489
618
|
assert.equal(result.summary, 'Regenerated UI page "/practice" at src/pages/w/[workspaceSlug]/admin/practice/index.vue.');
|
|
490
619
|
|
|
491
620
|
const pageSource = await readFile(path.join(appRoot, toPagePath(targetFile)), "utf8");
|
|
492
|
-
assert.match(pageSource, /<h1 class="
|
|
621
|
+
assert.match(pageSource, /<h1 class="generated-page-screen__title">Practice<\/h1>/);
|
|
493
622
|
assert.doesNotMatch(pageSource, /custom practice page/);
|
|
494
623
|
});
|
|
495
624
|
});
|