@jskit-ai/ui-generator 0.1.48 → 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.
@@ -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/element.js";
7
8
 
8
9
  async function withTempApp(run) {
@@ -14,6 +15,39 @@ async function withTempApp(run) {
14
15
  }
15
16
  }
16
17
 
18
+ function renderTopologyEntry({
19
+ id = "",
20
+ owner = "",
21
+ surfaces = ["*"],
22
+ defaultPlacement = false,
23
+ outlet = "",
24
+ linkRenderer = ""
25
+ } = {}) {
26
+ const ownerLine = owner ? ` owner: "${owner}",\n` : "";
27
+ const defaultLine = defaultPlacement ? " default: true,\n" : "";
28
+ const rendererLines = linkRenderer
29
+ ? `,
30
+ renderers: {
31
+ link: "${linkRenderer}"
32
+ }`
33
+ : "";
34
+ return ` {
35
+ id: "${id}",
36
+ ${ownerLine} surfaces: ${JSON.stringify(surfaces)},
37
+ ${defaultLine} variants: {
38
+ compact: {
39
+ outlet: "${outlet}"${rendererLines}
40
+ },
41
+ medium: {
42
+ outlet: "${outlet}"${rendererLines}
43
+ },
44
+ expanded: {
45
+ outlet: "${outlet}"${rendererLines}
46
+ }
47
+ }
48
+ }`;
49
+ }
50
+
17
51
  async function writeAppFixture(appRoot) {
18
52
  await mkdir(path.join(appRoot, "config"), { recursive: true });
19
53
  await mkdir(path.join(appRoot, "src", "components"), { recursive: true });
@@ -43,11 +77,39 @@ async function writeAppFixture(appRoot) {
43
77
  <ShellOutlet
44
78
  target="shell-layout:primary-menu"
45
79
  default
46
- default-link-component-token="local.main.ui.surface-aware-menu-link-item"
47
80
  />
48
81
  <ShellOutlet target="shell-layout:top-right" />
49
82
  </div>
50
83
  </template>
84
+ `,
85
+ "utf8"
86
+ );
87
+ await writeFile(
88
+ path.join(appRoot, "src", "placementTopology.js"),
89
+ `export default {
90
+ placements: [
91
+ ${[
92
+ renderTopologyEntry({
93
+ id: "shell.primary-nav",
94
+ surfaces: ["*"],
95
+ defaultPlacement: true,
96
+ outlet: "shell-layout:primary-menu",
97
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
98
+ }),
99
+ renderTopologyEntry({
100
+ id: "shell.status",
101
+ surfaces: ["*"],
102
+ outlet: "shell-layout:top-right"
103
+ }),
104
+ renderTopologyEntry({
105
+ id: "settings.sections",
106
+ owner: "admin-settings",
107
+ surfaces: ["admin"],
108
+ outlet: "admin-settings:forms"
109
+ })
110
+ ].join(",\n")}
111
+ ]
112
+ };
51
113
  `,
52
114
  "utf8"
53
115
  );
@@ -114,9 +176,20 @@ test("ui-generator placed-element subcommand creates component and outlet placem
114
176
  assert.match(providerSource, /import OpsPanelElement from "\/src\/components\/OpsPanelElement\.vue";/);
115
177
  assert.match(providerSource, /registerMainClientComponent\("local\.main\.ui\.element\.ops-panel", \(\) => OpsPanelElement\);/);
116
178
 
179
+ const componentSource = await readFile(path.join(appRoot, "src", "components", "OpsPanelElement.vue"), "utf8");
180
+ assertGeneratedUiSourceContract(componentSource, {
181
+ profile: "placed-element",
182
+ sourceName: "OpsPanelElement.vue"
183
+ });
184
+ assert.match(componentSource, /class="generated-element-panel"/);
185
+ assert.match(componentSource, /<h2 class="text-subtitle-1 font-weight-medium mb-0">Ops Panel<\/h2>/);
186
+ assert.match(componentSource, /<v-chip color="primary" variant="tonal" size="small">Ready<\/v-chip>/);
187
+ assert.doesNotMatch(componentSource, /Replace this scaffold|Use this area|This is your page/);
188
+
117
189
  const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
118
190
  assert.match(placementSource, /id: "ui-generator\.element\.ops-panel"/);
119
- assert.match(placementSource, /target: "shell-layout:top-right"/);
191
+ assert.match(placementSource, /target: "shell\.status"/);
192
+ assert.match(placementSource, /kind: "component"/);
120
193
  assert.match(placementSource, /componentToken: "local\.main\.ui\.element\.ops-panel"/);
121
194
  });
122
195
  });
@@ -131,16 +204,16 @@ test("ui-generator placed-element subcommand supports explicit placement overrid
131
204
  options: {
132
205
  name: "Ops Panel",
133
206
  surface: "admin",
134
- placement: "shell-layout:primary-menu"
207
+ placement: "shell.primary-nav"
135
208
  }
136
209
  });
137
210
 
138
211
  const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
139
- assert.match(placementSource, /target: "shell-layout:primary-menu"/);
212
+ assert.match(placementSource, /target: "shell\.primary-nav"/);
140
213
  });
141
214
  });
142
215
 
143
- test("ui-generator placed-element infers surface from a page-owned placement target", async () => {
216
+ test("ui-generator placed-element infers surface from an owner-scoped semantic placement", async () => {
144
217
  await withTempApp(async (appRoot) => {
145
218
  await writeAppFixture(appRoot);
146
219
 
@@ -149,12 +222,14 @@ test("ui-generator placed-element infers surface from a page-owned placement tar
149
222
  subcommand: "placed-element",
150
223
  options: {
151
224
  name: "Ops Panel",
152
- placement: "admin-settings:forms"
225
+ placement: "settings.sections",
226
+ owner: "admin-settings"
153
227
  }
154
228
  });
155
229
 
156
230
  const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
157
- assert.match(placementSource, /target: "admin-settings:forms"/);
231
+ assert.match(placementSource, /target: "settings\.sections"/);
232
+ assert.match(placementSource, /owner: "admin-settings"/);
158
233
  assert.match(placementSource, /surfaces: \["admin"\]/);
159
234
  });
160
235
  });
@@ -172,7 +247,7 @@ test("ui-generator placed-element infers the only enabled surface for shared she
172
247
  });
173
248
 
174
249
  const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
175
- assert.match(placementSource, /target: "shell-layout:top-right"/);
250
+ assert.match(placementSource, /target: "shell\.status"/);
176
251
  assert.match(placementSource, /surfaces: \["admin"\]/);
177
252
  });
178
253
  });
@@ -210,7 +285,7 @@ test("ui-generator placed-element requires explicit surface when a shared shell
210
285
  name: "Ops Panel"
211
286
  }
212
287
  }),
213
- /could not infer a surface for placement target "shell-layout:top-right". Pass --surface explicitly/
288
+ /could not infer a surface for placement target "shell.status". Pass --surface explicitly/
214
289
  );
215
290
  });
216
291
  });
@@ -226,11 +301,12 @@ test("ui-generator placed-element rejects explicit surfaces that conflict with p
226
301
  subcommand: "placed-element",
227
302
  options: {
228
303
  name: "Ops Panel",
229
- placement: "admin-settings:forms",
304
+ placement: "settings.sections",
305
+ owner: "admin-settings",
230
306
  surface: "console"
231
307
  }
232
308
  }),
233
- /target "admin-settings:forms" belongs to surface "admin", so --surface console is invalid/
309
+ /target "settings.sections" is not available on surface "console"/
234
310
  );
235
311
  });
236
312
  });
@@ -285,7 +361,8 @@ test("ui-generator placed-element subcommand overwrites an existing component wh
285
361
  ]);
286
362
 
287
363
  const componentSource = await readFile(path.join(appRoot, "src", "components", "OpsPanelElement.vue"), "utf8");
288
- assert.match(componentSource, /<h2 class="text-h6 mb-2">Ops Panel<\/h2>/);
364
+ assert.match(componentSource, /<h2 class="text-subtitle-1 font-weight-medium mb-0">Ops Panel<\/h2>/);
365
+ assert.doesNotMatch(componentSource, /custom/);
289
366
  });
290
367
  });
291
368
 
@@ -8,6 +8,23 @@ import { runGeneratorSubcommand } from "../src/server/subcommands/outlet.js";
8
8
  async function withTempApp(run) {
9
9
  const appRoot = await mkdtemp(path.join(tmpdir(), "ui-generator-outlet-"));
10
10
  try {
11
+ await mkdir(path.join(appRoot, "src"), { recursive: true });
12
+ await writeFile(
13
+ path.join(appRoot, "src", "placementTopology.js"),
14
+ `const placements = [];
15
+
16
+ function addPlacementTopology(value = {}) {
17
+ placements.push(value);
18
+ }
19
+
20
+ export { addPlacementTopology };
21
+
22
+ export default function getPlacementTopology() {
23
+ return { placements };
24
+ }
25
+ `,
26
+ "utf8"
27
+ );
11
28
  return await run(appRoot);
12
29
  } finally {
13
30
  await rm(appRoot, { recursive: true, force: true });
@@ -40,24 +57,34 @@ import { computed } from "vue";
40
57
  subcommand: "outlet",
41
58
  args: [targetFile],
42
59
  options: {
43
- target: "contact-view:sub-pages"
60
+ target: "contact-view:sub-pages",
61
+ placement: "page.section-nav"
44
62
  }
45
63
  });
46
64
 
47
- assert.deepEqual(result.touchedFiles, [targetFile]);
65
+ assert.deepEqual(result.touchedFiles, [targetFile, "src/placementTopology.js"]);
48
66
 
49
67
  const output = await readFile(targetPath, "utf8");
50
68
  assert.match(output, /import ShellOutlet from "@jskit-ai\/shell-web\/client\/components\/ShellOutlet";/);
51
69
  assert.match(output, /<ShellOutlet target="contact-view:sub-pages" \/>/);
52
70
  assert.doesNotMatch(output, /RouterView/);
53
71
  assert.doesNotMatch(output, /jskit:ui-generator\.outlet:/);
72
+ const topologySource = await readFile(path.join(appRoot, "src", "placementTopology.js"), "utf8");
73
+ assert.match(topologySource, /id: "page\.section-nav"/);
74
+ assert.match(topologySource, /owner: "contact-view"/);
75
+ assert.match(topologySource, /compact: \{/);
76
+ assert.match(topologySource, /medium: \{/);
77
+ assert.match(topologySource, /expanded: \{/);
78
+ assert.match(topologySource, /renderers: \{/);
79
+ assert.match(topologySource, /link: "local\.main\.ui\.surface-aware-menu-link-item"/);
54
80
 
55
81
  const rerun = await runGeneratorSubcommand({
56
82
  appRoot,
57
83
  subcommand: "outlet",
58
84
  args: [targetFile],
59
85
  options: {
60
- target: "contact-view:sub-pages"
86
+ target: "contact-view:sub-pages",
87
+ placement: "page.section-nav"
61
88
  }
62
89
  });
63
90
 
@@ -65,6 +92,224 @@ import { computed } from "vue";
65
92
  });
66
93
  });
67
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
+
68
313
  test("ui-generator outlet does not inject a second matching outlet", async () => {
69
314
  await withTempApp(async (appRoot) => {
70
315
  const targetFile = "src/pages/w/[workspaceSlug]/admin/contacts/[contactId]/index.vue";
@@ -91,7 +336,8 @@ import ShellOutlet from "@jskit-ai/shell-web/client/components/ShellOutlet";
91
336
  subcommand: "outlet",
92
337
  args: [targetFile],
93
338
  options: {
94
- target: "contact-view:sub-pages"
339
+ target: "contact-view:sub-pages",
340
+ placement: "page.section-nav"
95
341
  }
96
342
  });
97
343
 
@@ -124,7 +370,8 @@ test("ui-generator outlet creates script setup when missing", async () => {
124
370
  subcommand: "outlet",
125
371
  args: [targetFile],
126
372
  options: {
127
- target: "contact-view:sub-pages"
373
+ target: "contact-view:sub-pages",
374
+ placement: "page.section-nav"
128
375
  }
129
376
  });
130
377
 
@@ -135,6 +382,44 @@ test("ui-generator outlet creates script setup when missing", async () => {
135
382
  });
136
383
  });
137
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
+
138
423
  test("ui-generator outlet inserts generated script after existing route block", async () => {
139
424
  await withTempApp(async (appRoot) => {
140
425
  const targetFile = "src/pages/w/[workspaceSlug]/admin/contacts/[contactId]/index.vue";
@@ -161,7 +446,8 @@ test("ui-generator outlet inserts generated script after existing route block",
161
446
  subcommand: "outlet",
162
447
  args: [targetFile],
163
448
  options: {
164
- target: "contact-view:sub-pages"
449
+ target: "contact-view:sub-pages",
450
+ placement: "page.section-nav"
165
451
  }
166
452
  });
167
453
 
@@ -203,7 +489,8 @@ test("ui-generator outlet keeps indentation when injected into nested template b
203
489
  subcommand: "outlet",
204
490
  args: [targetFile],
205
491
  options: {
206
- target: "contact-view:sub-pages"
492
+ target: "contact-view:sub-pages",
493
+ placement: "page.section-nav"
207
494
  }
208
495
  });
209
496
 
@@ -229,6 +516,7 @@ test("ui-generator outlet rejects unsupported options", async () => {
229
516
  args: [targetFile],
230
517
  options: {
231
518
  target: "contact-view:sub-pages",
519
+ placement: "page.section-nav",
232
520
  bogus: "routed"
233
521
  }
234
522
  }),
@@ -250,7 +538,8 @@ test("ui-generator outlet supports explicit target host:position", async () => {
250
538
  subcommand: "outlet",
251
539
  args: [targetFile],
252
540
  options: {
253
- target: "customer-view:summary-actions"
541
+ target: "customer-view:summary-actions",
542
+ placement: "page.actions"
254
543
  }
255
544
  });
256
545
 
@@ -272,9 +561,10 @@ test("ui-generator outlet rejects non-vue target files without changing them", a
272
561
  runGeneratorSubcommand({
273
562
  appRoot,
274
563
  subcommand: "outlet",
275
- args: [targetFile],
276
- options: {
277
- target: "vet-view:sub-pages"
564
+ args: [targetFile],
565
+ options: {
566
+ target: "vet-view:sub-pages",
567
+ placement: "page.section-nav"
278
568
  }
279
569
  }),
280
570
  /ui-generator outlet target file must be an existing Vue SFC \(\.vue\): src\/pages\/w\/\[workspaceSlug\]\/admin\/practice\/vets\/_components\/VetAddEditFormFields\.js\./
@@ -297,9 +587,10 @@ test("ui-generator outlet validates target format", async () => {
297
587
  runGeneratorSubcommand({
298
588
  appRoot,
299
589
  subcommand: "outlet",
300
- args: [targetFile],
301
- options: {
302
- target: "customer-view:"
590
+ args: [targetFile],
591
+ options: {
592
+ target: "customer-view:",
593
+ placement: "page.actions"
303
594
  }
304
595
  }),
305
596
  /ui-generator outlet option "target" must be a target in "host:position" format\./
@@ -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
  });