@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.
Files changed (34) hide show
  1. package/README.md +29 -10
  2. package/index.js +460 -1
  3. package/package.json +4 -3
  4. package/templates/playwright-template/.env.example +17 -0
  5. package/templates/playwright-template/.github/workflows/playwright-tests.yml +126 -0
  6. package/templates/playwright-template/README.md +234 -0
  7. package/templates/playwright-template/allurerc.mjs +10 -0
  8. package/templates/playwright-template/components/flash-message.ts +16 -0
  9. package/templates/playwright-template/config/environments.ts +41 -0
  10. package/templates/playwright-template/config/runtime-config.ts +53 -0
  11. package/templates/playwright-template/config/secret-manager.ts +28 -0
  12. package/templates/playwright-template/config/test-env.ts +9 -0
  13. package/templates/playwright-template/data/README.md +9 -0
  14. package/templates/playwright-template/data/factories/data-factory.ts +33 -0
  15. package/templates/playwright-template/data/generators/id-generator.ts +17 -0
  16. package/templates/playwright-template/data/generators/seeded-faker.ts +13 -0
  17. package/templates/playwright-template/docker/Dockerfile +21 -0
  18. package/templates/playwright-template/eslint.config.mjs +66 -0
  19. package/templates/playwright-template/fixtures/test-fixtures.ts +43 -0
  20. package/templates/playwright-template/lint/architecture-plugin.cjs +118 -0
  21. package/templates/playwright-template/package-lock.json +4724 -0
  22. package/templates/playwright-template/package.json +34 -0
  23. package/templates/playwright-template/pages/base-page.ts +24 -0
  24. package/templates/playwright-template/pages/login-page.ts +22 -0
  25. package/templates/playwright-template/pages/people-page.ts +39 -0
  26. package/templates/playwright-template/playwright.config.ts +46 -0
  27. package/templates/playwright-template/reporters/structured-reporter.ts +61 -0
  28. package/templates/playwright-template/scripts/generate-allure-report.mjs +57 -0
  29. package/templates/playwright-template/scripts/run-tests.sh +6 -0
  30. package/templates/playwright-template/tests/api-people.spec.ts +28 -0
  31. package/templates/playwright-template/tests/ui-journey.spec.ts +30 -0
  32. package/templates/playwright-template/tsconfig.json +31 -0
  33. package/templates/playwright-template/utils/logger.ts +55 -0
  34. 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
- This package is the future scaffolding entrypoint for the `qa-patterns` repository.
3
+ CLI for generating QA framework templates from `qa-patterns`.
4
4
 
5
- ## Current status
5
+ ## Install
6
6
 
7
- The package is intentionally minimal today. It exists so the repository can reserve the CLI name and publish a stable install target while the scaffold commands evolve.
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
- Current output:
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
- - prints a placeholder message
33
+ ## Interactive flow
19
34
 
20
- Planned direction:
35
+ When run in a terminal, the CLI shows:
21
36
 
22
- - scaffold a Playwright automation framework
23
- - scaffold reference demo applications
24
- - generate standardized config, lint, CI, and reporting assets
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
- console.log("create-qa-patterns is reserved for future repository scaffolding workflows.");
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.0",
4
- "description": "Placeholder CLI for future test framework scaffolding.",
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