@toolstackhq/create-qa-patterns 1.0.1 → 1.0.2
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 +12 -0
- package/index.js +287 -24
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,3 +29,15 @@ create-qa-patterns playwright-template my-project
|
|
|
29
29
|
## Supported templates
|
|
30
30
|
|
|
31
31
|
- `playwright-template`
|
|
32
|
+
|
|
33
|
+
## Interactive flow
|
|
34
|
+
|
|
35
|
+
When run in a terminal, the CLI shows:
|
|
36
|
+
|
|
37
|
+
- a template picker with keyboard selection
|
|
38
|
+
- short template descriptions
|
|
39
|
+
- scaffold progress while files are generated
|
|
40
|
+
- optional post-generate actions for:
|
|
41
|
+
- `npm install`
|
|
42
|
+
- `npx playwright install`
|
|
43
|
+
- `npm test`
|
package/index.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require("node:fs");
|
|
4
4
|
const path = require("node:path");
|
|
5
|
+
const readline = require("node:readline");
|
|
6
|
+
const { spawn } = require("node:child_process");
|
|
5
7
|
|
|
6
8
|
const DEFAULT_TEMPLATE = "playwright-template";
|
|
7
9
|
const DEFAULT_GITIGNORE = `node_modules/
|
|
@@ -16,11 +18,22 @@ allure-report/
|
|
|
16
18
|
test-results/
|
|
17
19
|
playwright-report/
|
|
18
20
|
`;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
]
|
|
21
|
+
|
|
22
|
+
const TEMPLATES = [
|
|
23
|
+
{
|
|
24
|
+
id: DEFAULT_TEMPLATE,
|
|
25
|
+
aliases: ["playwright", "pw"],
|
|
26
|
+
label: "Playwright Template",
|
|
27
|
+
description: "TypeScript starter with page objects, fixtures, multi-environment config, reporting, linting, CI and Docker."
|
|
28
|
+
}
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const TEMPLATE_ALIASES = new Map(
|
|
32
|
+
TEMPLATES.flatMap((template) => [
|
|
33
|
+
[template.id, template.id],
|
|
34
|
+
...template.aliases.map((alias) => [alias, template.id])
|
|
35
|
+
])
|
|
36
|
+
);
|
|
24
37
|
|
|
25
38
|
function printHelp() {
|
|
26
39
|
process.stdout.write(`create-qa-patterns
|
|
@@ -30,6 +43,9 @@ Usage:
|
|
|
30
43
|
create-qa-patterns <target-directory>
|
|
31
44
|
create-qa-patterns <template> [target-directory]
|
|
32
45
|
|
|
46
|
+
Interactive mode:
|
|
47
|
+
When run without an explicit template, the CLI shows an interactive template picker.
|
|
48
|
+
|
|
33
49
|
Supported templates:
|
|
34
50
|
playwright-template
|
|
35
51
|
playwright
|
|
@@ -41,7 +57,138 @@ function resolveTemplate(value) {
|
|
|
41
57
|
return TEMPLATE_ALIASES.get(value);
|
|
42
58
|
}
|
|
43
59
|
|
|
44
|
-
function
|
|
60
|
+
function getTemplate(templateId) {
|
|
61
|
+
return TEMPLATES.find((template) => template.id === templateId);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function sleep(ms) {
|
|
65
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createLineInterface() {
|
|
69
|
+
return readline.createInterface({
|
|
70
|
+
input: process.stdin,
|
|
71
|
+
output: process.stdout
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function askQuestion(prompt) {
|
|
76
|
+
const lineInterface = createLineInterface();
|
|
77
|
+
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
lineInterface.question(prompt, (answer) => {
|
|
80
|
+
lineInterface.close();
|
|
81
|
+
resolve(answer.trim());
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function askYesNo(prompt, defaultValue = true) {
|
|
87
|
+
const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
|
|
88
|
+
|
|
89
|
+
while (true) {
|
|
90
|
+
const answer = (await askQuestion(`${prompt}${suffix}`)).toLowerCase();
|
|
91
|
+
|
|
92
|
+
if (!answer) {
|
|
93
|
+
return defaultValue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (["y", "yes"].includes(answer)) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (["n", "no"].includes(answer)) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
process.stdout.write("Please answer yes or no.\n");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function selectTemplateInteractively() {
|
|
109
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
110
|
+
return DEFAULT_TEMPLATE;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
readline.emitKeypressEvents(process.stdin);
|
|
114
|
+
|
|
115
|
+
if (typeof process.stdin.setRawMode === "function") {
|
|
116
|
+
process.stdin.setRawMode(true);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let selectedIndex = 0;
|
|
120
|
+
let renderedLines = 0;
|
|
121
|
+
|
|
122
|
+
const render = () => {
|
|
123
|
+
if (renderedLines > 0) {
|
|
124
|
+
readline.moveCursor(process.stdout, 0, -renderedLines);
|
|
125
|
+
readline.clearScreenDown(process.stdout);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const lines = [
|
|
129
|
+
"Select a template",
|
|
130
|
+
"Use ↑/↓ to choose and press Enter to continue.",
|
|
131
|
+
""
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
for (let index = 0; index < TEMPLATES.length; index += 1) {
|
|
135
|
+
const template = TEMPLATES[index];
|
|
136
|
+
const marker = index === selectedIndex ? ">" : " ";
|
|
137
|
+
lines.push(`${marker} ${template.label}`);
|
|
138
|
+
lines.push(` ${template.description}`);
|
|
139
|
+
lines.push("");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
renderedLines = lines.length;
|
|
143
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
render();
|
|
147
|
+
|
|
148
|
+
return new Promise((resolve) => {
|
|
149
|
+
const handleKeypress = (_, key) => {
|
|
150
|
+
if (!key) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (key.name === "up") {
|
|
155
|
+
selectedIndex = (selectedIndex - 1 + TEMPLATES.length) % TEMPLATES.length;
|
|
156
|
+
render();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (key.name === "down") {
|
|
161
|
+
selectedIndex = (selectedIndex + 1) % TEMPLATES.length;
|
|
162
|
+
render();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (key.name === "return") {
|
|
167
|
+
process.stdin.off("keypress", handleKeypress);
|
|
168
|
+
if (typeof process.stdin.setRawMode === "function") {
|
|
169
|
+
process.stdin.setRawMode(false);
|
|
170
|
+
}
|
|
171
|
+
readline.clearScreenDown(process.stdout);
|
|
172
|
+
process.stdout.write(`Selected: ${TEMPLATES[selectedIndex].label}\n\n`);
|
|
173
|
+
resolve(TEMPLATES[selectedIndex].id);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (key.ctrl && key.name === "c") {
|
|
178
|
+
process.stdin.off("keypress", handleKeypress);
|
|
179
|
+
if (typeof process.stdin.setRawMode === "function") {
|
|
180
|
+
process.stdin.setRawMode(false);
|
|
181
|
+
}
|
|
182
|
+
process.stdout.write("\n");
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
process.stdin.on("keypress", handleKeypress);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function resolveNonInteractiveArgs(args) {
|
|
45
192
|
if (args.length === 0) {
|
|
46
193
|
return {
|
|
47
194
|
templateName: DEFAULT_TEMPLATE,
|
|
@@ -52,6 +199,7 @@ function resolveScaffoldArgs(args) {
|
|
|
52
199
|
|
|
53
200
|
if (args.length === 1) {
|
|
54
201
|
const templateName = resolveTemplate(args[0]);
|
|
202
|
+
|
|
55
203
|
if (templateName) {
|
|
56
204
|
return {
|
|
57
205
|
templateName,
|
|
@@ -69,6 +217,7 @@ function resolveScaffoldArgs(args) {
|
|
|
69
217
|
|
|
70
218
|
if (args.length === 2) {
|
|
71
219
|
const templateName = resolveTemplate(args[0]);
|
|
220
|
+
|
|
72
221
|
if (!templateName) {
|
|
73
222
|
throw new Error(`Unsupported template "${args[0]}". Use "playwright-template".`);
|
|
74
223
|
}
|
|
@@ -83,6 +232,30 @@ function resolveScaffoldArgs(args) {
|
|
|
83
232
|
throw new Error("Too many arguments. Run `create-qa-patterns --help` for usage.");
|
|
84
233
|
}
|
|
85
234
|
|
|
235
|
+
async function resolveScaffoldArgs(args) {
|
|
236
|
+
const explicitTemplate = args[0] && resolveTemplate(args[0]);
|
|
237
|
+
|
|
238
|
+
if (explicitTemplate) {
|
|
239
|
+
return resolveNonInteractiveArgs(args);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
243
|
+
return resolveNonInteractiveArgs(args);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const templateName = await selectTemplateInteractively();
|
|
247
|
+
const defaultTarget = args[0] ? args[0] : ".";
|
|
248
|
+
const targetAnswer = await askQuestion(`Target directory (${defaultTarget}): `);
|
|
249
|
+
const targetValue = targetAnswer || defaultTarget;
|
|
250
|
+
const targetDirectory = path.resolve(process.cwd(), targetValue);
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
templateName,
|
|
254
|
+
targetDirectory,
|
|
255
|
+
generatedInCurrentDirectory: targetDirectory === process.cwd()
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
86
259
|
function ensureScaffoldTarget(targetDirectory) {
|
|
87
260
|
if (!fs.existsSync(targetDirectory)) {
|
|
88
261
|
fs.mkdirSync(targetDirectory, { recursive: true });
|
|
@@ -148,36 +321,126 @@ function customizeProject(targetDirectory) {
|
|
|
148
321
|
}
|
|
149
322
|
}
|
|
150
323
|
|
|
151
|
-
function
|
|
324
|
+
function renderProgress(completed, total, label) {
|
|
325
|
+
const width = 24;
|
|
326
|
+
const filled = Math.round((completed / total) * width);
|
|
327
|
+
const empty = width - filled;
|
|
328
|
+
const bar = `${"=".repeat(filled)}${" ".repeat(empty)}`;
|
|
329
|
+
const percentage = `${Math.round((completed / total) * 100)}`.padStart(3, " ");
|
|
330
|
+
process.stdout.write(`\r[${bar}] ${percentage}% ${label}`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function scaffoldProject(templateName, targetDirectory) {
|
|
152
334
|
const templateDirectory = path.resolve(__dirname, "templates", templateName);
|
|
153
335
|
|
|
154
336
|
if (!fs.existsSync(templateDirectory)) {
|
|
155
337
|
throw new Error(`Template files are missing for "${templateName}".`);
|
|
156
338
|
}
|
|
157
339
|
|
|
340
|
+
const steps = [
|
|
341
|
+
"Validating target directory",
|
|
342
|
+
"Copying template files",
|
|
343
|
+
"Customizing project files",
|
|
344
|
+
"Finalizing scaffold"
|
|
345
|
+
];
|
|
346
|
+
|
|
347
|
+
renderProgress(0, steps.length, "Preparing scaffold");
|
|
158
348
|
ensureScaffoldTarget(targetDirectory);
|
|
349
|
+
await sleep(60);
|
|
350
|
+
|
|
351
|
+
renderProgress(1, steps.length, steps[0]);
|
|
352
|
+
await sleep(80);
|
|
353
|
+
|
|
159
354
|
fs.cpSync(templateDirectory, targetDirectory, { recursive: true });
|
|
355
|
+
renderProgress(2, steps.length, steps[1]);
|
|
356
|
+
await sleep(80);
|
|
357
|
+
|
|
160
358
|
customizeProject(targetDirectory);
|
|
359
|
+
renderProgress(3, steps.length, steps[2]);
|
|
360
|
+
await sleep(80);
|
|
361
|
+
|
|
362
|
+
renderProgress(4, steps.length, steps[3]);
|
|
363
|
+
await sleep(60);
|
|
364
|
+
process.stdout.write("\n");
|
|
161
365
|
}
|
|
162
366
|
|
|
163
|
-
function
|
|
164
|
-
process.
|
|
367
|
+
function getCommandName(base) {
|
|
368
|
+
if (process.platform === "win32") {
|
|
369
|
+
return `${base}.cmd`;
|
|
370
|
+
}
|
|
165
371
|
|
|
166
|
-
|
|
167
|
-
|
|
372
|
+
return base;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function runCommand(command, args, cwd) {
|
|
376
|
+
return new Promise((resolve, reject) => {
|
|
377
|
+
const child = spawn(getCommandName(command), args, {
|
|
378
|
+
cwd,
|
|
379
|
+
stdio: "inherit"
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
child.on("close", (code) => {
|
|
383
|
+
if (code === 0) {
|
|
384
|
+
resolve();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
reject(new Error(`${command} ${args.join(" ")} exited with code ${code}`));
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
child.on("error", reject);
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function printSuccess(templateName, targetDirectory, generatedInCurrentDirectory) {
|
|
396
|
+
const template = getTemplate(templateName);
|
|
397
|
+
|
|
398
|
+
process.stdout.write(`\nSuccess
|
|
399
|
+
Generated ${template ? template.label : templateName} in ${targetDirectory}
|
|
400
|
+
\n`);
|
|
168
401
|
|
|
169
402
|
if (!generatedInCurrentDirectory) {
|
|
170
|
-
process.stdout.write(` cd ${path.relative(process.cwd(), targetDirectory) || "."}
|
|
171
|
-
`);
|
|
403
|
+
process.stdout.write(`Change directory first:\n cd ${path.relative(process.cwd(), targetDirectory) || "."}\n\n`);
|
|
172
404
|
}
|
|
405
|
+
}
|
|
173
406
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
407
|
+
function printNextSteps(targetDirectory, generatedInCurrentDirectory) {
|
|
408
|
+
process.stdout.write("Next steps:\n");
|
|
409
|
+
|
|
410
|
+
if (!generatedInCurrentDirectory) {
|
|
411
|
+
process.stdout.write(` cd ${path.relative(process.cwd(), targetDirectory) || "."}\n`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
process.stdout.write(" npm install\n");
|
|
415
|
+
process.stdout.write(" npx playwright install\n");
|
|
416
|
+
process.stdout.write(" npm test\n");
|
|
178
417
|
}
|
|
179
418
|
|
|
180
|
-
function
|
|
419
|
+
async function runPostGenerateActions(targetDirectory) {
|
|
420
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const shouldInstallDependencies = await askYesNo("Run npm install now?", true);
|
|
425
|
+
|
|
426
|
+
if (shouldInstallDependencies) {
|
|
427
|
+
await runCommand("npm", ["install"], targetDirectory);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const shouldInstallPlaywright = await askYesNo("Run npx playwright install now?", true);
|
|
431
|
+
|
|
432
|
+
if (shouldInstallPlaywright) {
|
|
433
|
+
await runCommand("npx", ["playwright", "install"], targetDirectory);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const shouldRunTests = await askYesNo("Run npm test now?", false);
|
|
437
|
+
|
|
438
|
+
if (shouldRunTests) {
|
|
439
|
+
await runCommand("npm", ["test"], targetDirectory);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function main() {
|
|
181
444
|
const args = process.argv.slice(2);
|
|
182
445
|
|
|
183
446
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -185,15 +448,15 @@ function main() {
|
|
|
185
448
|
return;
|
|
186
449
|
}
|
|
187
450
|
|
|
188
|
-
const { templateName, targetDirectory, generatedInCurrentDirectory } = resolveScaffoldArgs(args);
|
|
189
|
-
scaffoldProject(templateName, targetDirectory);
|
|
451
|
+
const { templateName, targetDirectory, generatedInCurrentDirectory } = await resolveScaffoldArgs(args);
|
|
452
|
+
await scaffoldProject(templateName, targetDirectory);
|
|
453
|
+
printSuccess(templateName, targetDirectory, generatedInCurrentDirectory);
|
|
454
|
+
await runPostGenerateActions(targetDirectory);
|
|
190
455
|
printNextSteps(targetDirectory, generatedInCurrentDirectory);
|
|
191
456
|
}
|
|
192
457
|
|
|
193
|
-
|
|
194
|
-
main();
|
|
195
|
-
} catch (error) {
|
|
458
|
+
main().catch((error) => {
|
|
196
459
|
const message = error instanceof Error ? error.message : String(error);
|
|
197
460
|
process.stderr.write(`${message}\n`);
|
|
198
461
|
process.exit(1);
|
|
199
|
-
}
|
|
462
|
+
});
|