@toolstackhq/create-qa-patterns 1.0.0 → 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 +29 -10
- package/index.js +460 -1
- package/package.json +4 -3
- package/templates/playwright-template/.env.example +17 -0
- package/templates/playwright-template/.github/workflows/playwright-tests.yml +126 -0
- package/templates/playwright-template/README.md +234 -0
- package/templates/playwright-template/allurerc.mjs +10 -0
- package/templates/playwright-template/components/flash-message.ts +16 -0
- package/templates/playwright-template/config/environments.ts +41 -0
- package/templates/playwright-template/config/runtime-config.ts +53 -0
- package/templates/playwright-template/config/secret-manager.ts +28 -0
- package/templates/playwright-template/config/test-env.ts +9 -0
- package/templates/playwright-template/data/README.md +9 -0
- package/templates/playwright-template/data/factories/data-factory.ts +33 -0
- package/templates/playwright-template/data/generators/id-generator.ts +17 -0
- package/templates/playwright-template/data/generators/seeded-faker.ts +13 -0
- package/templates/playwright-template/docker/Dockerfile +21 -0
- package/templates/playwright-template/eslint.config.mjs +66 -0
- package/templates/playwright-template/fixtures/test-fixtures.ts +43 -0
- package/templates/playwright-template/lint/architecture-plugin.cjs +118 -0
- package/templates/playwright-template/package-lock.json +4724 -0
- package/templates/playwright-template/package.json +34 -0
- package/templates/playwright-template/pages/base-page.ts +24 -0
- package/templates/playwright-template/pages/login-page.ts +22 -0
- package/templates/playwright-template/pages/people-page.ts +39 -0
- package/templates/playwright-template/playwright.config.ts +46 -0
- package/templates/playwright-template/reporters/structured-reporter.ts +61 -0
- package/templates/playwright-template/scripts/generate-allure-report.mjs +57 -0
- package/templates/playwright-template/scripts/run-tests.sh +6 -0
- package/templates/playwright-template/tests/api-people.spec.ts +28 -0
- package/templates/playwright-template/tests/ui-journey.spec.ts +30 -0
- package/templates/playwright-template/tsconfig.json +31 -0
- package/templates/playwright-template/utils/logger.ts +55 -0
- package/templates/playwright-template/utils/test-step.ts +22 -0
package/README.md
CHANGED
|
@@ -1,24 +1,43 @@
|
|
|
1
1
|
# @toolstackhq/create-qa-patterns
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
CLI for generating QA framework templates from `qa-patterns`.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @toolstackhq/create-qa-patterns
|
|
9
|
+
```
|
|
8
10
|
|
|
9
11
|
## Usage
|
|
10
12
|
|
|
11
13
|
```bash
|
|
12
|
-
npm install -g @toolstackhq/create-qa-patterns
|
|
13
14
|
create-qa-patterns
|
|
14
15
|
```
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
Generate into a new directory:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
create-qa-patterns my-project
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Generate the Playwright template explicitly:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
create-qa-patterns playwright-template my-project
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Supported templates
|
|
30
|
+
|
|
31
|
+
- `playwright-template`
|
|
17
32
|
|
|
18
|
-
|
|
33
|
+
## Interactive flow
|
|
19
34
|
|
|
20
|
-
|
|
35
|
+
When run in a terminal, the CLI shows:
|
|
21
36
|
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
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
|
@@ -1,3 +1,462 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const readline = require("node:readline");
|
|
6
|
+
const { spawn } = require("node:child_process");
|
|
7
|
+
|
|
8
|
+
const DEFAULT_TEMPLATE = "playwright-template";
|
|
9
|
+
const DEFAULT_GITIGNORE = `node_modules/
|
|
10
|
+
|
|
11
|
+
.env
|
|
12
|
+
.env.*
|
|
13
|
+
!.env.example
|
|
14
|
+
|
|
15
|
+
reports/
|
|
16
|
+
allure-results/
|
|
17
|
+
allure-report/
|
|
18
|
+
test-results/
|
|
19
|
+
playwright-report/
|
|
20
|
+
`;
|
|
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
|
+
);
|
|
37
|
+
|
|
38
|
+
function printHelp() {
|
|
39
|
+
process.stdout.write(`create-qa-patterns
|
|
40
|
+
|
|
41
|
+
Usage:
|
|
42
|
+
create-qa-patterns
|
|
43
|
+
create-qa-patterns <target-directory>
|
|
44
|
+
create-qa-patterns <template> [target-directory]
|
|
45
|
+
|
|
46
|
+
Interactive mode:
|
|
47
|
+
When run without an explicit template, the CLI shows an interactive template picker.
|
|
48
|
+
|
|
49
|
+
Supported templates:
|
|
50
|
+
playwright-template
|
|
51
|
+
playwright
|
|
52
|
+
pw
|
|
53
|
+
`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveTemplate(value) {
|
|
57
|
+
return TEMPLATE_ALIASES.get(value);
|
|
58
|
+
}
|
|
59
|
+
|
|
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) {
|
|
192
|
+
if (args.length === 0) {
|
|
193
|
+
return {
|
|
194
|
+
templateName: DEFAULT_TEMPLATE,
|
|
195
|
+
targetDirectory: process.cwd(),
|
|
196
|
+
generatedInCurrentDirectory: true
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (args.length === 1) {
|
|
201
|
+
const templateName = resolveTemplate(args[0]);
|
|
202
|
+
|
|
203
|
+
if (templateName) {
|
|
204
|
+
return {
|
|
205
|
+
templateName,
|
|
206
|
+
targetDirectory: process.cwd(),
|
|
207
|
+
generatedInCurrentDirectory: true
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
templateName: DEFAULT_TEMPLATE,
|
|
213
|
+
targetDirectory: path.resolve(process.cwd(), args[0]),
|
|
214
|
+
generatedInCurrentDirectory: false
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (args.length === 2) {
|
|
219
|
+
const templateName = resolveTemplate(args[0]);
|
|
220
|
+
|
|
221
|
+
if (!templateName) {
|
|
222
|
+
throw new Error(`Unsupported template "${args[0]}". Use "playwright-template".`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
templateName,
|
|
227
|
+
targetDirectory: path.resolve(process.cwd(), args[1]),
|
|
228
|
+
generatedInCurrentDirectory: false
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
throw new Error("Too many arguments. Run `create-qa-patterns --help` for usage.");
|
|
233
|
+
}
|
|
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
|
+
|
|
259
|
+
function ensureScaffoldTarget(targetDirectory) {
|
|
260
|
+
if (!fs.existsSync(targetDirectory)) {
|
|
261
|
+
fs.mkdirSync(targetDirectory, { recursive: true });
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const entries = fs
|
|
266
|
+
.readdirSync(targetDirectory)
|
|
267
|
+
.filter((entry) => ![".git", ".DS_Store"].includes(entry));
|
|
268
|
+
|
|
269
|
+
if (entries.length > 0) {
|
|
270
|
+
throw new Error(`Target directory is not empty: ${targetDirectory}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function toPackageName(targetDirectory) {
|
|
275
|
+
const baseName = path.basename(targetDirectory).toLowerCase();
|
|
276
|
+
const normalized = baseName
|
|
277
|
+
.replace(/[^a-z0-9-_]+/g, "-")
|
|
278
|
+
.replace(/^-+|-+$/g, "")
|
|
279
|
+
.replace(/-{2,}/g, "-");
|
|
280
|
+
|
|
281
|
+
return normalized || "playwright-template";
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function updateJsonFile(filePath, update) {
|
|
285
|
+
const current = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
286
|
+
const next = update(current);
|
|
287
|
+
fs.writeFileSync(filePath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function customizeProject(targetDirectory) {
|
|
291
|
+
const packageName = toPackageName(targetDirectory);
|
|
292
|
+
const packageJsonPath = path.join(targetDirectory, "package.json");
|
|
293
|
+
const packageLockPath = path.join(targetDirectory, "package-lock.json");
|
|
294
|
+
const gitignorePath = path.join(targetDirectory, ".gitignore");
|
|
295
|
+
|
|
296
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
297
|
+
updateJsonFile(packageJsonPath, (pkg) => ({
|
|
298
|
+
...pkg,
|
|
299
|
+
name: packageName
|
|
300
|
+
}));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (fs.existsSync(packageLockPath)) {
|
|
304
|
+
updateJsonFile(packageLockPath, (lock) => ({
|
|
305
|
+
...lock,
|
|
306
|
+
name: packageName,
|
|
307
|
+
packages: lock.packages
|
|
308
|
+
? {
|
|
309
|
+
...lock.packages,
|
|
310
|
+
"": {
|
|
311
|
+
...lock.packages[""],
|
|
312
|
+
name: packageName
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
: lock.packages
|
|
316
|
+
}));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
320
|
+
fs.writeFileSync(gitignorePath, DEFAULT_GITIGNORE, "utf8");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
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) {
|
|
334
|
+
const templateDirectory = path.resolve(__dirname, "templates", templateName);
|
|
335
|
+
|
|
336
|
+
if (!fs.existsSync(templateDirectory)) {
|
|
337
|
+
throw new Error(`Template files are missing for "${templateName}".`);
|
|
338
|
+
}
|
|
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");
|
|
348
|
+
ensureScaffoldTarget(targetDirectory);
|
|
349
|
+
await sleep(60);
|
|
350
|
+
|
|
351
|
+
renderProgress(1, steps.length, steps[0]);
|
|
352
|
+
await sleep(80);
|
|
353
|
+
|
|
354
|
+
fs.cpSync(templateDirectory, targetDirectory, { recursive: true });
|
|
355
|
+
renderProgress(2, steps.length, steps[1]);
|
|
356
|
+
await sleep(80);
|
|
357
|
+
|
|
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");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function getCommandName(base) {
|
|
368
|
+
if (process.platform === "win32") {
|
|
369
|
+
return `${base}.cmd`;
|
|
370
|
+
}
|
|
371
|
+
|
|
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`);
|
|
401
|
+
|
|
402
|
+
if (!generatedInCurrentDirectory) {
|
|
403
|
+
process.stdout.write(`Change directory first:\n cd ${path.relative(process.cwd(), targetDirectory) || "."}\n\n`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
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");
|
|
417
|
+
}
|
|
418
|
+
|
|
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() {
|
|
444
|
+
const args = process.argv.slice(2);
|
|
445
|
+
|
|
446
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
447
|
+
printHelp();
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const { templateName, targetDirectory, generatedInCurrentDirectory } = await resolveScaffoldArgs(args);
|
|
452
|
+
await scaffoldProject(templateName, targetDirectory);
|
|
453
|
+
printSuccess(templateName, targetDirectory, generatedInCurrentDirectory);
|
|
454
|
+
await runPostGenerateActions(targetDirectory);
|
|
455
|
+
printNextSteps(targetDirectory, generatedInCurrentDirectory);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
main().catch((error) => {
|
|
459
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
460
|
+
process.stderr.write(`${message}\n`);
|
|
461
|
+
process.exit(1);
|
|
462
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toolstackhq/create-qa-patterns",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "CLI for generating QA framework templates.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"index.js",
|
|
19
|
+
"templates",
|
|
19
20
|
"README.md",
|
|
20
21
|
"LICENSE"
|
|
21
22
|
],
|
|
@@ -24,6 +25,6 @@
|
|
|
24
25
|
},
|
|
25
26
|
"scripts": {
|
|
26
27
|
"start": "node ./index.js",
|
|
27
|
-
"test": "node ./index.js"
|
|
28
|
+
"test": "node ./index.js --help"
|
|
28
29
|
}
|
|
29
30
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
TEST_ENV=dev
|
|
2
|
+
TEST_RUN_ID=local
|
|
3
|
+
|
|
4
|
+
DEV_UI_BASE_URL=http://127.0.0.1:3000
|
|
5
|
+
DEV_API_BASE_URL=http://127.0.0.1:3001
|
|
6
|
+
DEV_APP_USERNAME=tester
|
|
7
|
+
DEV_APP_PASSWORD=Password123!
|
|
8
|
+
|
|
9
|
+
STAGING_UI_BASE_URL=https://staging-ui.example.internal
|
|
10
|
+
STAGING_API_BASE_URL=https://staging-api.example.internal
|
|
11
|
+
STAGING_APP_USERNAME=staging-user
|
|
12
|
+
STAGING_APP_PASSWORD=replace-me
|
|
13
|
+
|
|
14
|
+
PROD_UI_BASE_URL=https://ui.example.internal
|
|
15
|
+
PROD_API_BASE_URL=https://api.example.internal
|
|
16
|
+
PROD_APP_USERNAME=prod-user
|
|
17
|
+
PROD_APP_PASSWORD=replace-me
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
name: Playwright Template Validation
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
paths:
|
|
7
|
+
- "templates/playwright-template/**"
|
|
8
|
+
- "test-apps/**"
|
|
9
|
+
- "package.json"
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
playwright:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-node@v4
|
|
19
|
+
with:
|
|
20
|
+
node-version: "20"
|
|
21
|
+
|
|
22
|
+
- name: Install workspace dependencies
|
|
23
|
+
run: npm ci
|
|
24
|
+
|
|
25
|
+
- name: Install Playwright browsers
|
|
26
|
+
working-directory: templates/playwright-template
|
|
27
|
+
run: npx playwright install --with-deps chromium
|
|
28
|
+
|
|
29
|
+
- name: Start API demo server
|
|
30
|
+
run: npm run dev --workspace @toolstackhq/api-demo-server &
|
|
31
|
+
|
|
32
|
+
- name: Start UI demo app
|
|
33
|
+
run: npm run dev --workspace @toolstackhq/ui-demo-app &
|
|
34
|
+
|
|
35
|
+
- name: Wait for demo services
|
|
36
|
+
run: |
|
|
37
|
+
for target in http://127.0.0.1:3000/health http://127.0.0.1:3001/health; do
|
|
38
|
+
until curl --silent --fail "$target" >/dev/null; do
|
|
39
|
+
sleep 1
|
|
40
|
+
done
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
- name: Run Playwright validation
|
|
44
|
+
working-directory: templates/playwright-template
|
|
45
|
+
env:
|
|
46
|
+
TEST_ENV: dev
|
|
47
|
+
TEST_RUN_ID: ci
|
|
48
|
+
run: bash ./scripts/run-tests.sh
|
|
49
|
+
|
|
50
|
+
- name: Generate Allure report
|
|
51
|
+
if: always()
|
|
52
|
+
working-directory: templates/playwright-template
|
|
53
|
+
run: npm run report:allure
|
|
54
|
+
|
|
55
|
+
- name: Upload Playwright artifacts
|
|
56
|
+
if: always()
|
|
57
|
+
uses: actions/upload-artifact@v4
|
|
58
|
+
with:
|
|
59
|
+
name: playwright-template-artifacts
|
|
60
|
+
path: |
|
|
61
|
+
templates/playwright-template/reports
|
|
62
|
+
templates/playwright-template/allure-results
|
|
63
|
+
templates/playwright-template/test-results
|
|
64
|
+
|
|
65
|
+
playwright-docker:
|
|
66
|
+
runs-on: ubuntu-latest
|
|
67
|
+
|
|
68
|
+
steps:
|
|
69
|
+
- uses: actions/checkout@v4
|
|
70
|
+
|
|
71
|
+
- uses: actions/setup-node@v4
|
|
72
|
+
with:
|
|
73
|
+
node-version: "20"
|
|
74
|
+
|
|
75
|
+
- name: Install workspace dependencies
|
|
76
|
+
run: npm ci
|
|
77
|
+
|
|
78
|
+
- name: Start API demo server
|
|
79
|
+
run: npm run dev --workspace @toolstackhq/api-demo-server &
|
|
80
|
+
|
|
81
|
+
- name: Start UI demo app
|
|
82
|
+
run: npm run dev --workspace @toolstackhq/ui-demo-app &
|
|
83
|
+
|
|
84
|
+
- name: Wait for demo services
|
|
85
|
+
run: |
|
|
86
|
+
for target in http://127.0.0.1:3000/health http://127.0.0.1:3001/health; do
|
|
87
|
+
until curl --silent --fail "$target" >/dev/null; do
|
|
88
|
+
sleep 1
|
|
89
|
+
done
|
|
90
|
+
done
|
|
91
|
+
|
|
92
|
+
- name: Prepare Docker artifacts directories
|
|
93
|
+
run: |
|
|
94
|
+
rm -rf templates/playwright-template/reports templates/playwright-template/test-results templates/playwright-template/allure-results
|
|
95
|
+
mkdir -p templates/playwright-template/reports templates/playwright-template/test-results templates/playwright-template/allure-results
|
|
96
|
+
|
|
97
|
+
- name: Build Playwright template Docker image
|
|
98
|
+
run: docker build -t qa-patterns-playwright-template:ci -f templates/playwright-template/docker/Dockerfile templates/playwright-template
|
|
99
|
+
|
|
100
|
+
- name: Run Playwright template inside Docker
|
|
101
|
+
run: |
|
|
102
|
+
docker run --rm \
|
|
103
|
+
--add-host=host.docker.internal:host-gateway \
|
|
104
|
+
-e TEST_ENV=dev \
|
|
105
|
+
-e TEST_RUN_ID=ci-docker \
|
|
106
|
+
-e DEV_UI_BASE_URL=http://host.docker.internal:3000 \
|
|
107
|
+
-e DEV_API_BASE_URL=http://host.docker.internal:3001 \
|
|
108
|
+
-v "${GITHUB_WORKSPACE}/templates/playwright-template/reports:/workspace/reports" \
|
|
109
|
+
-v "${GITHUB_WORKSPACE}/templates/playwright-template/allure-results:/workspace/allure-results" \
|
|
110
|
+
-v "${GITHUB_WORKSPACE}/templates/playwright-template/test-results:/workspace/test-results" \
|
|
111
|
+
qa-patterns-playwright-template:ci
|
|
112
|
+
|
|
113
|
+
- name: Generate Allure report
|
|
114
|
+
if: always()
|
|
115
|
+
working-directory: templates/playwright-template
|
|
116
|
+
run: npm run report:allure
|
|
117
|
+
|
|
118
|
+
- name: Upload Docker Playwright artifacts
|
|
119
|
+
if: always()
|
|
120
|
+
uses: actions/upload-artifact@v4
|
|
121
|
+
with:
|
|
122
|
+
name: playwright-template-docker-artifacts
|
|
123
|
+
path: |
|
|
124
|
+
templates/playwright-template/reports
|
|
125
|
+
templates/playwright-template/allure-results
|
|
126
|
+
templates/playwright-template/test-results
|