@jskit-ai/ui-generator 0.1.13 → 0.1.15
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/README.md +147 -123
- package/package.descriptor.mjs +182 -82
- package/package.json +2 -2
- package/src/server/buildTemplateContext.js +31 -136
- package/src/server/subcommands/addSubpages.js +73 -0
- package/src/server/subcommands/element.js +30 -15
- package/src/server/subcommands/outlet.js +26 -126
- package/src/server/subcommands/page.js +142 -0
- package/src/server/subcommands/pageSupport.js +552 -0
- package/src/server/subcommands/support.js +145 -1
- package/test/addSubpagesSubcommand.test.js +321 -0
- package/test/buildTemplateContext.test.js +426 -65
- package/test/elementSubcommand.test.js +79 -6
- package/test/outletSubcommand.test.js +92 -29
- package/test/packageDescriptor.test.js +10 -0
- package/test/pageSubcommand.test.js +352 -0
- package/src/server/subcommands/container.js +0 -644
- package/templates/src/pages/admin/ui-generator/Page.vue +0 -6
- package/test/containerSubcommand.test.js +0 -307
|
@@ -14,7 +14,7 @@ async function withTempApp(run) {
|
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
test("ui-generator outlet injects
|
|
17
|
+
test("ui-generator outlet injects a generic ShellOutlet into an existing page", async () => {
|
|
18
18
|
await withTempApp(async (appRoot) => {
|
|
19
19
|
const targetFile = "src/pages/w/[workspaceSlug]/admin/contacts/[contactId]/index.vue";
|
|
20
20
|
const targetPath = path.join(appRoot, targetFile);
|
|
@@ -38,9 +38,9 @@ import { computed } from "vue";
|
|
|
38
38
|
const result = await runGeneratorSubcommand({
|
|
39
39
|
appRoot,
|
|
40
40
|
subcommand: "outlet",
|
|
41
|
+
args: [targetFile],
|
|
41
42
|
options: {
|
|
42
|
-
|
|
43
|
-
host: "contact-view"
|
|
43
|
+
target: "contact-view"
|
|
44
44
|
}
|
|
45
45
|
});
|
|
46
46
|
|
|
@@ -48,17 +48,16 @@ import { computed } from "vue";
|
|
|
48
48
|
|
|
49
49
|
const output = await readFile(targetPath, "utf8");
|
|
50
50
|
assert.match(output, /import ShellOutlet from "@jskit-ai\/shell-web\/client\/components\/ShellOutlet";/);
|
|
51
|
-
assert.match(output, /import \{ RouterView \} from "vue-router";/);
|
|
52
51
|
assert.match(output, /<ShellOutlet host="contact-view" position="sub-pages" \/>/);
|
|
53
|
-
assert.
|
|
52
|
+
assert.doesNotMatch(output, /RouterView/);
|
|
54
53
|
assert.doesNotMatch(output, /jskit:ui-generator\.outlet:/);
|
|
55
54
|
|
|
56
55
|
const rerun = await runGeneratorSubcommand({
|
|
57
56
|
appRoot,
|
|
58
57
|
subcommand: "outlet",
|
|
58
|
+
args: [targetFile],
|
|
59
59
|
options: {
|
|
60
|
-
|
|
61
|
-
host: "contact-view"
|
|
60
|
+
target: "contact-view"
|
|
62
61
|
}
|
|
63
62
|
});
|
|
64
63
|
|
|
@@ -66,7 +65,7 @@ import { computed } from "vue";
|
|
|
66
65
|
});
|
|
67
66
|
});
|
|
68
67
|
|
|
69
|
-
test("ui-generator outlet does not
|
|
68
|
+
test("ui-generator outlet does not inject a second matching outlet", async () => {
|
|
70
69
|
await withTempApp(async (appRoot) => {
|
|
71
70
|
const targetFile = "src/pages/w/[workspaceSlug]/admin/contacts/[contactId]/index.vue";
|
|
72
71
|
const targetPath = path.join(appRoot, targetFile);
|
|
@@ -75,12 +74,12 @@ test("ui-generator outlet does not add second RouterView when one already exists
|
|
|
75
74
|
await writeFile(
|
|
76
75
|
targetPath,
|
|
77
76
|
`<script setup>
|
|
78
|
-
import
|
|
77
|
+
import ShellOutlet from "@jskit-ai/shell-web/client/components/ShellOutlet";
|
|
79
78
|
</script>
|
|
80
79
|
|
|
81
80
|
<template>
|
|
82
81
|
<section>
|
|
83
|
-
<
|
|
82
|
+
<ShellOutlet host="contact-view" position="sub-pages" />
|
|
84
83
|
</section>
|
|
85
84
|
</template>
|
|
86
85
|
`,
|
|
@@ -90,20 +89,22 @@ import { RouterView } from "vue-router";
|
|
|
90
89
|
await runGeneratorSubcommand({
|
|
91
90
|
appRoot,
|
|
92
91
|
subcommand: "outlet",
|
|
92
|
+
args: [targetFile],
|
|
93
93
|
options: {
|
|
94
|
-
|
|
95
|
-
host: "contact-view"
|
|
94
|
+
target: "contact-view"
|
|
96
95
|
}
|
|
97
96
|
});
|
|
98
97
|
|
|
99
98
|
const output = await readFile(targetPath, "utf8");
|
|
100
|
-
assert.match(
|
|
101
|
-
assert.equal(
|
|
102
|
-
|
|
99
|
+
assert.equal((output.match(/<ShellOutlet host="contact-view" position="sub-pages" \/>/g) || []).length, 1);
|
|
100
|
+
assert.equal(
|
|
101
|
+
(output.match(/import ShellOutlet from "@jskit-ai\/shell-web\/client\/components\/ShellOutlet";/g) || []).length,
|
|
102
|
+
1
|
|
103
|
+
);
|
|
103
104
|
});
|
|
104
105
|
});
|
|
105
106
|
|
|
106
|
-
test("ui-generator outlet
|
|
107
|
+
test("ui-generator outlet creates script setup when missing", async () => {
|
|
107
108
|
await withTempApp(async (appRoot) => {
|
|
108
109
|
const targetFile = "src/components/ContactDetailsPanel.vue";
|
|
109
110
|
const targetPath = path.join(appRoot, targetFile);
|
|
@@ -121,19 +122,16 @@ test("ui-generator outlet-only mode injects only ShellOutlet and creates script
|
|
|
121
122
|
await runGeneratorSubcommand({
|
|
122
123
|
appRoot,
|
|
123
124
|
subcommand: "outlet",
|
|
125
|
+
args: [targetFile],
|
|
124
126
|
options: {
|
|
125
|
-
|
|
126
|
-
host: "contact-view",
|
|
127
|
-
position: "sub-pages",
|
|
128
|
-
mode: "outlet-only"
|
|
127
|
+
target: "contact-view"
|
|
129
128
|
}
|
|
130
129
|
});
|
|
131
130
|
|
|
132
131
|
const output = await readFile(targetPath, "utf8");
|
|
133
132
|
assert.match(output, /<script setup>/);
|
|
134
133
|
assert.match(output, /import ShellOutlet from "@jskit-ai\/shell-web\/client\/components\/ShellOutlet";/);
|
|
135
|
-
assert.doesNotMatch(output, /
|
|
136
|
-
assert.doesNotMatch(output, /<RouterView \/>/);
|
|
134
|
+
assert.doesNotMatch(output, /RouterView/);
|
|
137
135
|
});
|
|
138
136
|
});
|
|
139
137
|
|
|
@@ -161,10 +159,9 @@ test("ui-generator outlet inserts generated script after existing route block",
|
|
|
161
159
|
await runGeneratorSubcommand({
|
|
162
160
|
appRoot,
|
|
163
161
|
subcommand: "outlet",
|
|
162
|
+
args: [targetFile],
|
|
164
163
|
options: {
|
|
165
|
-
|
|
166
|
-
host: "contact-view",
|
|
167
|
-
mode: "outlet-only"
|
|
164
|
+
target: "contact-view"
|
|
168
165
|
}
|
|
169
166
|
});
|
|
170
167
|
|
|
@@ -204,16 +201,82 @@ test("ui-generator outlet keeps indentation when injected into nested template b
|
|
|
204
201
|
await runGeneratorSubcommand({
|
|
205
202
|
appRoot,
|
|
206
203
|
subcommand: "outlet",
|
|
204
|
+
args: [targetFile],
|
|
207
205
|
options: {
|
|
208
|
-
|
|
209
|
-
host: "contact-view",
|
|
210
|
-
mode: "routed"
|
|
206
|
+
target: "contact-view"
|
|
211
207
|
}
|
|
212
208
|
});
|
|
213
209
|
|
|
214
210
|
const output = await readFile(targetPath, "utf8");
|
|
215
|
-
assert.match(output, /\n\s{2}<\/section>\n\s{2}<ShellOutlet host="contact-view" position="sub-pages" \/>\n
|
|
211
|
+
assert.match(output, /\n\s{2}<\/section>\n\s{2}<ShellOutlet host="contact-view" position="sub-pages" \/>\n<\/template>/);
|
|
216
212
|
assert.match(output, /<template v-else-if="view\.isLoading">\n\s*<v-skeleton-loader type="heading, text@2, article" \/>\n\s*<\/template>/);
|
|
217
213
|
assert.doesNotMatch(output, /jskit:ui-generator\.outlet:/);
|
|
218
214
|
});
|
|
219
215
|
});
|
|
216
|
+
|
|
217
|
+
test("ui-generator outlet rejects unsupported options", async () => {
|
|
218
|
+
await withTempApp(async (appRoot) => {
|
|
219
|
+
const targetFile = "src/components/ContactDetailsPanel.vue";
|
|
220
|
+
const targetPath = path.join(appRoot, targetFile);
|
|
221
|
+
|
|
222
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
223
|
+
await writeFile(targetPath, "<template><div /></template>\n", "utf8");
|
|
224
|
+
|
|
225
|
+
await assert.rejects(
|
|
226
|
+
runGeneratorSubcommand({
|
|
227
|
+
appRoot,
|
|
228
|
+
subcommand: "outlet",
|
|
229
|
+
args: [targetFile],
|
|
230
|
+
options: {
|
|
231
|
+
target: "contact-view",
|
|
232
|
+
bogus: "routed"
|
|
233
|
+
}
|
|
234
|
+
}),
|
|
235
|
+
/ui-generator outlet received unsupported option: --bogus\./
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("ui-generator outlet supports explicit target host:position", async () => {
|
|
241
|
+
await withTempApp(async (appRoot) => {
|
|
242
|
+
const targetFile = "src/components/ContactDetailsPanel.vue";
|
|
243
|
+
const targetPath = path.join(appRoot, targetFile);
|
|
244
|
+
|
|
245
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
246
|
+
await writeFile(targetPath, "<template><div /></template>\n", "utf8");
|
|
247
|
+
|
|
248
|
+
await runGeneratorSubcommand({
|
|
249
|
+
appRoot,
|
|
250
|
+
subcommand: "outlet",
|
|
251
|
+
args: [targetFile],
|
|
252
|
+
options: {
|
|
253
|
+
target: "customer-view:summary-actions"
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const output = await readFile(targetPath, "utf8");
|
|
258
|
+
assert.match(output, /<ShellOutlet host="customer-view" position="summary-actions" \/>/);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("ui-generator outlet validates target format", async () => {
|
|
263
|
+
await withTempApp(async (appRoot) => {
|
|
264
|
+
const targetFile = "src/components/ContactDetailsPanel.vue";
|
|
265
|
+
const targetPath = path.join(appRoot, targetFile);
|
|
266
|
+
|
|
267
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
268
|
+
await writeFile(targetPath, "<template><div /></template>\n", "utf8");
|
|
269
|
+
|
|
270
|
+
await assert.rejects(
|
|
271
|
+
runGeneratorSubcommand({
|
|
272
|
+
appRoot,
|
|
273
|
+
subcommand: "outlet",
|
|
274
|
+
args: [targetFile],
|
|
275
|
+
options: {
|
|
276
|
+
target: "customer-view:"
|
|
277
|
+
}
|
|
278
|
+
}),
|
|
279
|
+
/ui-generator outlet option "target" must be "host" or "host:position"\./
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import descriptor from "../package.descriptor.mjs";
|
|
4
|
+
|
|
5
|
+
test("ui-generator surface options validate against enabled surface ids", () => {
|
|
6
|
+
assert.equal(descriptor.kind, "generator");
|
|
7
|
+
assert.equal(descriptor.options?.surface?.validationType, "enabled-surface-id");
|
|
8
|
+
assert.equal(descriptor.metadata?.generatorSubcommands?.["placed-element"]?.optionNames?.includes("surface"), true);
|
|
9
|
+
assert.equal(descriptor.metadata?.generatorSubcommands?.page?.optionNames?.includes("force"), true);
|
|
10
|
+
});
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { runGeneratorSubcommand } from "../src/server/subcommands/page.js";
|
|
7
|
+
|
|
8
|
+
async function withTempApp(run) {
|
|
9
|
+
const appRoot = await mkdtemp(path.join(tmpdir(), "ui-generator-page-"));
|
|
10
|
+
try {
|
|
11
|
+
return await run(appRoot);
|
|
12
|
+
} finally {
|
|
13
|
+
await rm(appRoot, { recursive: true, force: true });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function toPagePath(targetFile = "") {
|
|
18
|
+
return path.join("src/pages", targetFile);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function writeAppFixture(appRoot, { configSource = "" } = {}) {
|
|
22
|
+
await mkdir(path.join(appRoot, "config"), { recursive: true });
|
|
23
|
+
await mkdir(path.join(appRoot, "src", "components"), { recursive: true });
|
|
24
|
+
await mkdir(path.join(appRoot, "src"), { recursive: true });
|
|
25
|
+
|
|
26
|
+
await writeFile(
|
|
27
|
+
path.join(appRoot, "config", "public.js"),
|
|
28
|
+
configSource ||
|
|
29
|
+
`export const config = {
|
|
30
|
+
surfaceDefinitions: {
|
|
31
|
+
admin: { id: "admin", pagesRoot: "w/[workspaceSlug]/admin", enabled: true, requiresAuth: true, requiresWorkspace: true }
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
`,
|
|
35
|
+
"utf8"
|
|
36
|
+
);
|
|
37
|
+
await writeFile(
|
|
38
|
+
path.join(appRoot, "src", "components", "ShellLayout.vue"),
|
|
39
|
+
`<template>
|
|
40
|
+
<div>
|
|
41
|
+
<ShellOutlet host="shell-layout" position="primary-menu" default />
|
|
42
|
+
<ShellOutlet host="shell-layout" position="top-right" />
|
|
43
|
+
</div>
|
|
44
|
+
</template>
|
|
45
|
+
`,
|
|
46
|
+
"utf8"
|
|
47
|
+
);
|
|
48
|
+
await writeFile(
|
|
49
|
+
path.join(appRoot, "src", "placement.js"),
|
|
50
|
+
`function addPlacement() {}
|
|
51
|
+
|
|
52
|
+
export { addPlacement };
|
|
53
|
+
export default function getPlacements() {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
`,
|
|
57
|
+
"utf8"
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
test("ui-generator page subcommand creates an index page from an explicit target file", async () => {
|
|
62
|
+
await withTempApp(async (appRoot) => {
|
|
63
|
+
await writeAppFixture(appRoot);
|
|
64
|
+
|
|
65
|
+
const targetFile = "w/[workspaceSlug]/admin/practice/index.vue";
|
|
66
|
+
const result = await runGeneratorSubcommand({
|
|
67
|
+
appRoot,
|
|
68
|
+
subcommand: "page",
|
|
69
|
+
args: [targetFile],
|
|
70
|
+
options: {
|
|
71
|
+
name: "Practice"
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
assert.deepEqual(result.touchedFiles, [toPagePath(targetFile), "src/placement.js"]);
|
|
76
|
+
|
|
77
|
+
const pageSource = await readFile(path.join(appRoot, toPagePath(targetFile)), "utf8");
|
|
78
|
+
assert.match(pageSource, /<h1 class="text-h5 mb-2">Practice<\/h1>/);
|
|
79
|
+
|
|
80
|
+
const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
|
|
81
|
+
assert.match(placementSource, /id: "ui-generator\.page\.admin\.practice\.link"/);
|
|
82
|
+
assert.match(placementSource, /workspaceSuffix: "\/practice"/);
|
|
83
|
+
assert.match(placementSource, /label: "Practice"/);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("ui-generator page subcommand creates a file route and derives label from the file path", async () => {
|
|
88
|
+
await withTempApp(async (appRoot) => {
|
|
89
|
+
await writeAppFixture(appRoot);
|
|
90
|
+
|
|
91
|
+
const targetFile = "w/[workspaceSlug]/admin/contacts/[contactId].vue";
|
|
92
|
+
const result = await runGeneratorSubcommand({
|
|
93
|
+
appRoot,
|
|
94
|
+
subcommand: "page",
|
|
95
|
+
args: [targetFile],
|
|
96
|
+
options: {}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
assert.deepEqual(result.touchedFiles, [toPagePath(targetFile), "src/placement.js"]);
|
|
100
|
+
|
|
101
|
+
const pageSource = await readFile(path.join(appRoot, toPagePath(targetFile)), "utf8");
|
|
102
|
+
assert.match(pageSource, /<h1 class="text-h5 mb-2">Contact Id<\/h1>/);
|
|
103
|
+
|
|
104
|
+
const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
|
|
105
|
+
assert.match(placementSource, /workspaceSuffix: "\/contacts\/\[contactId\]"/);
|
|
106
|
+
assert.match(placementSource, /id: "ui-generator\.page\.admin\.contacts\.contact-id\.link"/);
|
|
107
|
+
assert.match(placementSource, /label: "Contact Id"/);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("ui-generator page subcommand supports link placement options", async () => {
|
|
112
|
+
await withTempApp(async (appRoot) => {
|
|
113
|
+
await writeAppFixture(appRoot);
|
|
114
|
+
|
|
115
|
+
const targetFile = "w/[workspaceSlug]/admin/contacts/[contactId]/index/notes/index.vue";
|
|
116
|
+
await runGeneratorSubcommand({
|
|
117
|
+
appRoot,
|
|
118
|
+
subcommand: "page",
|
|
119
|
+
args: [targetFile],
|
|
120
|
+
options: {
|
|
121
|
+
"link-placement": "shell-layout:top-right",
|
|
122
|
+
"link-component-token": "local.main.ui.tab-link-item",
|
|
123
|
+
"link-to": "./notes"
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
|
|
128
|
+
assert.match(placementSource, /host: "shell-layout"/);
|
|
129
|
+
assert.match(placementSource, /position: "top-right"/);
|
|
130
|
+
assert.match(placementSource, /componentToken: "local\.main\.ui\.tab-link-item"/);
|
|
131
|
+
assert.match(placementSource, /to: "\.\/notes"/);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("ui-generator page subcommand infers subpage link placement, tab token, and link-to from the nearest parent host", async () => {
|
|
136
|
+
await withTempApp(async (appRoot) => {
|
|
137
|
+
await writeAppFixture(appRoot);
|
|
138
|
+
|
|
139
|
+
const parentFile = "w/[workspaceSlug]/admin/contacts/[contactId].vue";
|
|
140
|
+
await mkdir(path.dirname(path.join(appRoot, toPagePath(parentFile))), { recursive: true });
|
|
141
|
+
await writeFile(
|
|
142
|
+
path.join(appRoot, toPagePath(parentFile)),
|
|
143
|
+
`<template>
|
|
144
|
+
<SectionContainerShell>
|
|
145
|
+
<template #tabs>
|
|
146
|
+
<ShellOutlet host="contact-view" position="sub-pages" />
|
|
147
|
+
</template>
|
|
148
|
+
<RouterView />
|
|
149
|
+
</SectionContainerShell>
|
|
150
|
+
</template>
|
|
151
|
+
`,
|
|
152
|
+
"utf8"
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const targetFile = "w/[workspaceSlug]/admin/contacts/[contactId]/notes/index.vue";
|
|
156
|
+
await runGeneratorSubcommand({
|
|
157
|
+
appRoot,
|
|
158
|
+
subcommand: "page",
|
|
159
|
+
args: [targetFile],
|
|
160
|
+
options: {}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
|
|
164
|
+
assert.match(placementSource, /host: "contact-view"/);
|
|
165
|
+
assert.match(placementSource, /position: "sub-pages"/);
|
|
166
|
+
assert.match(placementSource, /componentToken: "local\.main\.ui\.tab-link-item"/);
|
|
167
|
+
assert.match(placementSource, /to: "\.\/notes"/);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("ui-generator page subcommand prefers the nearest index-route parent host", async () => {
|
|
172
|
+
await withTempApp(async (appRoot) => {
|
|
173
|
+
await writeAppFixture(appRoot);
|
|
174
|
+
|
|
175
|
+
await mkdir(path.join(appRoot, "src/pages/w/[workspaceSlug]/admin/catalog/index/products"), {
|
|
176
|
+
recursive: true
|
|
177
|
+
});
|
|
178
|
+
await writeFile(
|
|
179
|
+
path.join(appRoot, "src/pages/w/[workspaceSlug]/admin/catalog/index.vue"),
|
|
180
|
+
`<template>
|
|
181
|
+
<SectionContainerShell>
|
|
182
|
+
<template #tabs>
|
|
183
|
+
<ShellOutlet host="catalog" position="sub-pages" />
|
|
184
|
+
</template>
|
|
185
|
+
<RouterView />
|
|
186
|
+
</SectionContainerShell>
|
|
187
|
+
</template>
|
|
188
|
+
`,
|
|
189
|
+
"utf8"
|
|
190
|
+
);
|
|
191
|
+
await writeFile(
|
|
192
|
+
path.join(appRoot, "src/pages/w/[workspaceSlug]/admin/catalog/index/products/index.vue"),
|
|
193
|
+
`<template>
|
|
194
|
+
<SectionContainerShell>
|
|
195
|
+
<template #tabs>
|
|
196
|
+
<ShellOutlet host="catalog-products" position="sub-pages" />
|
|
197
|
+
</template>
|
|
198
|
+
<RouterView />
|
|
199
|
+
</SectionContainerShell>
|
|
200
|
+
</template>
|
|
201
|
+
`,
|
|
202
|
+
"utf8"
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const targetFile =
|
|
206
|
+
"w/[workspaceSlug]/admin/catalog/index/products/index/variants/index.vue";
|
|
207
|
+
await runGeneratorSubcommand({
|
|
208
|
+
appRoot,
|
|
209
|
+
subcommand: "page",
|
|
210
|
+
args: [targetFile],
|
|
211
|
+
options: {}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
|
|
215
|
+
assert.match(placementSource, /host: "catalog-products"/);
|
|
216
|
+
assert.match(placementSource, /position: "sub-pages"/);
|
|
217
|
+
assert.match(placementSource, /componentToken: "local\.main\.ui\.tab-link-item"/);
|
|
218
|
+
assert.match(placementSource, /to: "\.\/variants"/);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("ui-generator page subcommand chooses the most specific matching surface pagesRoot", async () => {
|
|
223
|
+
await withTempApp(async (appRoot) => {
|
|
224
|
+
await writeAppFixture(appRoot, {
|
|
225
|
+
configSource: `export const config = {
|
|
226
|
+
surfaceDefinitions: {
|
|
227
|
+
app: { id: "app", pagesRoot: "", enabled: true, requiresAuth: false, requiresWorkspace: false },
|
|
228
|
+
admin: { id: "admin", pagesRoot: "w/[workspaceSlug]/admin", enabled: true, requiresAuth: true, requiresWorkspace: true }
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
`
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const targetFile = "w/[workspaceSlug]/admin/practice/index.vue";
|
|
235
|
+
const result = await runGeneratorSubcommand({
|
|
236
|
+
appRoot,
|
|
237
|
+
subcommand: "page",
|
|
238
|
+
args: [targetFile],
|
|
239
|
+
options: {}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
assert.deepEqual(result.touchedFiles, [toPagePath(targetFile), "src/placement.js"]);
|
|
243
|
+
|
|
244
|
+
const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
|
|
245
|
+
assert.match(placementSource, /id: "ui-generator\.page\.admin\.practice\.link"/);
|
|
246
|
+
assert.match(placementSource, /workspaceSuffix: "\/practice"/);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("ui-generator page subcommand rejects unsupported options", async () => {
|
|
251
|
+
await withTempApp(async (appRoot) => {
|
|
252
|
+
await writeAppFixture(appRoot);
|
|
253
|
+
|
|
254
|
+
await assert.rejects(
|
|
255
|
+
runGeneratorSubcommand({
|
|
256
|
+
appRoot,
|
|
257
|
+
subcommand: "page",
|
|
258
|
+
args: ["w/[workspaceSlug]/admin/practice/index.vue"],
|
|
259
|
+
options: {
|
|
260
|
+
bogus: "true"
|
|
261
|
+
}
|
|
262
|
+
}),
|
|
263
|
+
/ui-generator page received unsupported option: --bogus\./
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("ui-generator page subcommand rejects target files with a src/pages prefix", async () => {
|
|
269
|
+
await withTempApp(async (appRoot) => {
|
|
270
|
+
await writeAppFixture(appRoot);
|
|
271
|
+
|
|
272
|
+
await assert.rejects(
|
|
273
|
+
runGeneratorSubcommand({
|
|
274
|
+
appRoot,
|
|
275
|
+
subcommand: "page",
|
|
276
|
+
args: ["src/pages/w/[workspaceSlug]/admin/practice/index.vue"],
|
|
277
|
+
options: {}
|
|
278
|
+
}),
|
|
279
|
+
/must be relative to src\/pages\/, without the src\/pages\/ prefix/
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("ui-generator page subcommand refuses to overwrite an existing page without --force", async () => {
|
|
285
|
+
await withTempApp(async (appRoot) => {
|
|
286
|
+
await writeAppFixture(appRoot);
|
|
287
|
+
|
|
288
|
+
const targetFile = "w/[workspaceSlug]/admin/practice/index.vue";
|
|
289
|
+
await mkdir(path.join(appRoot, "src/pages/w/[workspaceSlug]/admin/practice"), {
|
|
290
|
+
recursive: true
|
|
291
|
+
});
|
|
292
|
+
await writeFile(
|
|
293
|
+
path.join(appRoot, toPagePath(targetFile)),
|
|
294
|
+
`<template>
|
|
295
|
+
<div>custom practice page</div>
|
|
296
|
+
</template>
|
|
297
|
+
`,
|
|
298
|
+
"utf8"
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
await assert.rejects(
|
|
302
|
+
runGeneratorSubcommand({
|
|
303
|
+
appRoot,
|
|
304
|
+
subcommand: "page",
|
|
305
|
+
args: [targetFile],
|
|
306
|
+
options: {
|
|
307
|
+
name: "Practice"
|
|
308
|
+
}
|
|
309
|
+
}),
|
|
310
|
+
/ui-generator page will not overwrite existing page src\/pages\/w\/\[workspaceSlug\]\/admin\/practice\/index\.vue\. Re-run with --force to overwrite it\./
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const pageSource = await readFile(path.join(appRoot, toPagePath(targetFile)), "utf8");
|
|
314
|
+
assert.match(pageSource, /custom practice page/);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("ui-generator page subcommand overwrites an existing page when --force is passed", async () => {
|
|
319
|
+
await withTempApp(async (appRoot) => {
|
|
320
|
+
await writeAppFixture(appRoot);
|
|
321
|
+
|
|
322
|
+
const targetFile = "w/[workspaceSlug]/admin/practice/index.vue";
|
|
323
|
+
await mkdir(path.join(appRoot, "src/pages/w/[workspaceSlug]/admin/practice"), {
|
|
324
|
+
recursive: true
|
|
325
|
+
});
|
|
326
|
+
await writeFile(
|
|
327
|
+
path.join(appRoot, toPagePath(targetFile)),
|
|
328
|
+
`<template>
|
|
329
|
+
<div>custom practice page</div>
|
|
330
|
+
</template>
|
|
331
|
+
`,
|
|
332
|
+
"utf8"
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
const result = await runGeneratorSubcommand({
|
|
336
|
+
appRoot,
|
|
337
|
+
subcommand: "page",
|
|
338
|
+
args: [targetFile],
|
|
339
|
+
options: {
|
|
340
|
+
name: "Practice",
|
|
341
|
+
force: "true"
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
assert.deepEqual(result.touchedFiles, [toPagePath(targetFile), "src/placement.js"]);
|
|
346
|
+
assert.equal(result.summary, 'Regenerated UI page "/practice" at src/pages/w/[workspaceSlug]/admin/practice/index.vue.');
|
|
347
|
+
|
|
348
|
+
const pageSource = await readFile(path.join(appRoot, toPagePath(targetFile)), "utf8");
|
|
349
|
+
assert.match(pageSource, /<h1 class="text-h5 mb-2">Practice<\/h1>/);
|
|
350
|
+
assert.doesNotMatch(pageSource, /custom practice page/);
|
|
351
|
+
});
|
|
352
|
+
});
|