@toolstackhq/create-qa-patterns 1.0.14 → 1.0.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.
Files changed (123) hide show
  1. package/README.md +5 -0
  2. package/index.js +252 -1076
  3. package/lib/args.js +139 -0
  4. package/lib/constants.js +115 -0
  5. package/lib/interactive.js +131 -0
  6. package/lib/local-env.js +65 -0
  7. package/lib/metadata.js +329 -0
  8. package/lib/output.js +326 -0
  9. package/lib/prereqs.js +72 -0
  10. package/lib/scaffold.js +120 -0
  11. package/lib/templates.js +40 -0
  12. package/package.json +5 -3
  13. package/templates/cypress-template/.env.example +2 -2
  14. package/templates/cypress-template/.github/workflows/cypress-tests.yml +2 -2
  15. package/templates/cypress-template/README.md +10 -6
  16. package/templates/cypress-template/allurerc.mjs +1 -1
  17. package/templates/cypress-template/config/environments.ts +13 -11
  18. package/templates/cypress-template/config/runtime-config.ts +17 -12
  19. package/templates/cypress-template/config/secret-manager.ts +1 -1
  20. package/templates/cypress-template/config/test-env.ts +3 -3
  21. package/templates/cypress-template/cypress/e2e/ui-journey.cy.ts +12 -10
  22. package/templates/cypress-template/cypress/support/app-config.ts +5 -5
  23. package/templates/cypress-template/cypress/support/commands.ts +7 -7
  24. package/templates/cypress-template/cypress/support/data/data-factory.ts +6 -4
  25. package/templates/cypress-template/cypress/support/data/id-generator.ts +1 -1
  26. package/templates/cypress-template/cypress/support/data/seeded-faker.ts +2 -2
  27. package/templates/cypress-template/cypress/support/e2e.ts +2 -2
  28. package/templates/cypress-template/cypress/support/pages/login-page.ts +4 -4
  29. package/templates/cypress-template/cypress/support/pages/people-page.ts +10 -10
  30. package/templates/cypress-template/cypress.config.ts +9 -9
  31. package/templates/cypress-template/demo-apps/ui-demo-app/public/styles.css +1 -1
  32. package/templates/cypress-template/demo-apps/ui-demo-app/src/server.js +44 -41
  33. package/templates/cypress-template/demo-apps/ui-demo-app/src/store.js +31 -3
  34. package/templates/cypress-template/demo-apps/ui-demo-app/src/templates.js +5 -5
  35. package/templates/cypress-template/eslint.config.mjs +53 -45
  36. package/templates/cypress-template/package.json +6 -5
  37. package/templates/cypress-template/scripts/ensure-local-env.mjs +36 -0
  38. package/templates/cypress-template/scripts/generate-allure-report.mjs +16 -10
  39. package/templates/cypress-template/scripts/run-cypress.mjs +33 -24
  40. package/templates/cypress-template/scripts/run-tests.sh +1 -0
  41. package/templates/cypress-template/tsconfig.json +7 -1
  42. package/templates/playwright-template/.env.example +6 -6
  43. package/templates/playwright-template/.github/workflows/playwright-tests.yml +14 -5
  44. package/templates/playwright-template/README.md +6 -5
  45. package/templates/playwright-template/allurerc.mjs +1 -1
  46. package/templates/playwright-template/components/flash-message.ts +2 -2
  47. package/templates/playwright-template/config/environments.ts +16 -14
  48. package/templates/playwright-template/config/runtime-config.ts +17 -12
  49. package/templates/playwright-template/config/secret-manager.ts +1 -1
  50. package/templates/playwright-template/config/test-env.ts +3 -3
  51. package/templates/playwright-template/data/factories/data-factory.ts +6 -4
  52. package/templates/playwright-template/data/generators/id-generator.ts +1 -1
  53. package/templates/playwright-template/data/generators/seeded-faker.ts +2 -2
  54. package/templates/playwright-template/demo-apps/api-demo-server/src/server.js +9 -9
  55. package/templates/playwright-template/demo-apps/api-demo-server/src/store.js +1 -1
  56. package/templates/playwright-template/demo-apps/ui-demo-app/public/styles.css +1 -1
  57. package/templates/playwright-template/demo-apps/ui-demo-app/src/server.js +44 -41
  58. package/templates/playwright-template/demo-apps/ui-demo-app/src/store.js +31 -3
  59. package/templates/playwright-template/demo-apps/ui-demo-app/src/templates.js +5 -5
  60. package/templates/playwright-template/eslint.config.mjs +40 -40
  61. package/templates/playwright-template/fixtures/test-fixtures.ts +27 -12
  62. package/templates/playwright-template/lint/architecture-plugin.cjs +36 -31
  63. package/templates/playwright-template/package.json +7 -6
  64. package/templates/playwright-template/pages/base-page.ts +4 -4
  65. package/templates/playwright-template/pages/login-page.ts +9 -9
  66. package/templates/playwright-template/pages/people-page.ts +21 -17
  67. package/templates/playwright-template/playwright.config.ts +22 -19
  68. package/templates/playwright-template/reporters/structured-reporter.ts +11 -8
  69. package/templates/playwright-template/scripts/ensure-local-env.mjs +37 -0
  70. package/templates/playwright-template/scripts/generate-allure-report.mjs +16 -10
  71. package/templates/playwright-template/scripts/run-tests.sh +1 -0
  72. package/templates/playwright-template/tests/api-people.spec.ts +8 -6
  73. package/templates/playwright-template/tests/ui-journey.spec.ts +13 -8
  74. package/templates/playwright-template/tsconfig.json +3 -11
  75. package/templates/playwright-template/utils/logger.ts +12 -8
  76. package/templates/playwright-template/utils/test-step.ts +5 -5
  77. package/templates/wdio-template/.env.example +14 -0
  78. package/templates/wdio-template/.github/workflows/wdio-tests.yml +46 -0
  79. package/templates/wdio-template/README.md +241 -0
  80. package/templates/wdio-template/allurerc.mjs +10 -0
  81. package/templates/wdio-template/components/README.md +5 -0
  82. package/templates/wdio-template/components/flash-message.ts +16 -0
  83. package/templates/wdio-template/config/README.md +5 -0
  84. package/templates/wdio-template/config/environments.ts +40 -0
  85. package/templates/wdio-template/config/runtime-config.ts +53 -0
  86. package/templates/wdio-template/config/secret-manager.ts +29 -0
  87. package/templates/wdio-template/config/test-env.ts +9 -0
  88. package/templates/wdio-template/data/README.md +9 -0
  89. package/templates/wdio-template/data/factories/README.md +6 -0
  90. package/templates/wdio-template/data/factories/data-factory.ts +36 -0
  91. package/templates/wdio-template/data/generators/README.md +5 -0
  92. package/templates/wdio-template/data/generators/id-generator.ts +18 -0
  93. package/templates/wdio-template/data/generators/seeded-faker.ts +14 -0
  94. package/templates/wdio-template/demo-apps/ui-demo-app/public/styles.css +120 -0
  95. package/templates/wdio-template/demo-apps/ui-demo-app/src/server.js +152 -0
  96. package/templates/wdio-template/demo-apps/ui-demo-app/src/store.js +71 -0
  97. package/templates/wdio-template/demo-apps/ui-demo-app/src/templates.js +121 -0
  98. package/templates/wdio-template/eslint.config.mjs +86 -0
  99. package/templates/wdio-template/lint/architecture-plugin.cjs +123 -0
  100. package/templates/wdio-template/package-lock.json +11058 -0
  101. package/templates/wdio-template/package.json +44 -0
  102. package/templates/wdio-template/pages/README.md +6 -0
  103. package/templates/wdio-template/pages/base-page.ts +15 -0
  104. package/templates/wdio-template/pages/login-page.ts +27 -0
  105. package/templates/wdio-template/pages/people-page.ts +54 -0
  106. package/templates/wdio-template/reporters/README.md +5 -0
  107. package/templates/wdio-template/reporters/structured-reporter.ts +78 -0
  108. package/templates/wdio-template/scripts/README.md +5 -0
  109. package/templates/wdio-template/scripts/ensure-local-env.mjs +36 -0
  110. package/templates/wdio-template/scripts/generate-allure-report.mjs +72 -0
  111. package/templates/wdio-template/scripts/run-tests.sh +7 -0
  112. package/templates/wdio-template/scripts/run-wdio.mjs +114 -0
  113. package/templates/wdio-template/tests/README.md +7 -0
  114. package/templates/wdio-template/tests/ui-journey.spec.ts +52 -0
  115. package/templates/wdio-template/tsconfig.json +22 -0
  116. package/templates/wdio-template/utils/README.md +5 -0
  117. package/templates/wdio-template/utils/logger.ts +60 -0
  118. package/templates/wdio-template/utils/test-step.ts +20 -0
  119. package/templates/wdio-template/wdio.conf.ts +58 -0
  120. package/tests/args.test.js +58 -0
  121. package/tests/local-env.test.js +70 -0
  122. package/tests/metadata.test.js +147 -0
  123. package/tests/templates.test.js +44 -0
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@toolstackhq/wdio-template",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "Production-ready WebdriverIO automation framework template with deterministic test patterns.",
6
+ "scripts": {
7
+ "ensure:env": "node ./scripts/ensure-local-env.mjs",
8
+ "test": "node ./scripts/ensure-local-env.mjs && node ./scripts/run-wdio.mjs",
9
+ "test:smoke": "node ./scripts/ensure-local-env.mjs && node ./scripts/run-wdio.mjs --mochaOpts.grep @smoke",
10
+ "test:regression": "node ./scripts/ensure-local-env.mjs && node ./scripts/run-wdio.mjs --mochaOpts.grep @regression",
11
+ "test:critical": "node ./scripts/ensure-local-env.mjs && node ./scripts/run-wdio.mjs --mochaOpts.grep @critical",
12
+ "demo:ui": "node ./scripts/ensure-local-env.mjs && node ./demo-apps/ui-demo-app/src/server.js",
13
+ "lint": "eslint .",
14
+ "typecheck": "tsc --noEmit",
15
+ "report:allure": "node ./scripts/generate-allure-report.mjs",
16
+ "report:allure:open": "allure open reports/allure",
17
+ "ci": "bash ./scripts/run-tests.sh"
18
+ },
19
+ "dependencies": {
20
+ "@faker-js/faker": "^8.4.1",
21
+ "dotenv": "^16.4.5",
22
+ "express": "^4.21.0",
23
+ "zod": "^3.23.8"
24
+ },
25
+ "devDependencies": {
26
+ "@eslint/js": "^9.12.0",
27
+ "@types/node": "^22.7.4",
28
+ "@typescript-eslint/eslint-plugin": "^8.8.1",
29
+ "@typescript-eslint/parser": "^8.8.1",
30
+ "@wdio/allure-reporter": "^9.0.0",
31
+ "@wdio/cli": "^9.0.0",
32
+ "@wdio/globals": "^9.0.0",
33
+ "@wdio/local-runner": "^9.0.0",
34
+ "@wdio/mocha-framework": "^9.0.0",
35
+ "@wdio/reporter": "^9.0.0",
36
+ "@wdio/spec-reporter": "^9.0.0",
37
+ "@wdio/types": "^9.0.0",
38
+ "allure": "^3.3.1",
39
+ "expect-webdriverio": "^5.4.3",
40
+ "eslint": "^9.12.0",
41
+ "typescript": "^5.6.2",
42
+ "webdriverio": "^9.0.0"
43
+ }
44
+ }
@@ -0,0 +1,6 @@
1
+ # Pages
2
+
3
+ This folder holds Playwright page objects.
4
+
5
+ - Page classes own locators and UI actions.
6
+ - Tests should talk to these classes instead of raw selectors.
@@ -0,0 +1,15 @@
1
+ // Base page object for WebdriverIO pages.
2
+ import { browser } from '@wdio/globals';
3
+
4
+ import type { Logger } from '../utils/logger';
5
+
6
+ export class BasePage {
7
+ constructor(
8
+ protected readonly baseUrl: string,
9
+ protected readonly logger: Logger
10
+ ) {}
11
+
12
+ async open(pathname: string): Promise<void> {
13
+ await browser.url(new globalThis.URL(pathname, this.baseUrl).toString());
14
+ }
15
+ }
@@ -0,0 +1,27 @@
1
+ // Page object for the login screen and its user actions.
2
+ import { $ } from '@wdio/globals';
3
+
4
+ import type { Logger } from '../utils/logger';
5
+
6
+ import { BasePage } from './base-page';
7
+
8
+ export class LoginPage extends BasePage {
9
+ constructor(baseUrl: string, logger: Logger) {
10
+ super(baseUrl, logger);
11
+ }
12
+
13
+ async goto(): Promise<void> {
14
+ this.logger.info('page.goto', { pageObject: 'LoginPage', page: 'login' });
15
+ await this.open('/login');
16
+ }
17
+
18
+ async login(username: string, password: string): Promise<void> {
19
+ this.logger.info('login.submit', {
20
+ pageObject: 'LoginPage',
21
+ username
22
+ });
23
+ await $('#username').setValue(username);
24
+ await $('#password').setValue(password);
25
+ await $('button=Sign in').click();
26
+ }
27
+ }
@@ -0,0 +1,54 @@
1
+ // Page object for the people screen used in the starter WDIO workflow.
2
+ import { $ } from '@wdio/globals';
3
+
4
+ import type { PersonRecord } from '../data/factories/data-factory';
5
+ import type { Logger } from '../utils/logger';
6
+
7
+ import { BasePage } from './base-page';
8
+
9
+ export class PeoplePage extends BasePage {
10
+ constructor(baseUrl: string, logger: Logger) {
11
+ super(baseUrl, logger);
12
+ }
13
+
14
+ async waitForReady(): Promise<void> {
15
+ await $('h1=People').waitForDisplayed();
16
+ }
17
+
18
+ async getWelcomeMessage(): Promise<string> {
19
+ return $('[data-testid="welcome-message"]').getText();
20
+ }
21
+
22
+ async addPerson(person: PersonRecord): Promise<void> {
23
+ this.logger.info('person.create', {
24
+ pageObject: 'PeoplePage',
25
+ personId: person.personId
26
+ });
27
+ await $('#personId').setValue(person.personId);
28
+ await $('#name').setValue(person.name);
29
+ await $('#role').setValue(person.role);
30
+ await $('#email').setValue(person.email);
31
+ await $('button=Add person').click();
32
+ await $('[data-testid="flash-message"]').waitForDisplayed();
33
+ }
34
+
35
+ async getFlashMessage(): Promise<string> {
36
+ return $('[data-testid="flash-message"]').getText();
37
+ }
38
+
39
+ async getPersonSummary(
40
+ personId: string
41
+ ): Promise<{ name: string; role: string; email: string } | null> {
42
+ const row = await $(`[data-testid="person-row-${personId}"]`);
43
+ if (!(await row.isExisting())) {
44
+ return null;
45
+ }
46
+
47
+ const cells = await row.$$('td');
48
+ return {
49
+ name: await cells[0].getText(),
50
+ role: await cells[1].getText(),
51
+ email: await cells[2].getText()
52
+ };
53
+ }
54
+ }
@@ -0,0 +1,5 @@
1
+ # Reporters
2
+
3
+ This folder contains custom Playwright reporters.
4
+
5
+ - Use these when you need machine-readable output in addition to built-in reports.
@@ -0,0 +1,78 @@
1
+ // Custom reporter that writes machine-readable WebdriverIO lifecycle events to disk.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ import reporter from '@wdio/reporter';
6
+
7
+ type ReporterOptions = {
8
+ outputFile?: string;
9
+ };
10
+
11
+ export default class StructuredReporter extends reporter {
12
+ private readonly outputFile: string;
13
+
14
+ constructor(options: ReporterOptions = {}) {
15
+ super({ ...options, stdout: false });
16
+ this.outputFile = path.resolve(
17
+ process.cwd(),
18
+ options.outputFile ?? 'reports/logs/wdio-events.jsonl'
19
+ );
20
+ fs.mkdirSync(path.dirname(this.outputFile), { recursive: true });
21
+ }
22
+
23
+ onRunnerStart(runnerStats: { cid?: string; specs?: string[] }): void {
24
+ this.writeEvent({
25
+ event: 'run.started',
26
+ cid: runnerStats.cid ?? 'unknown',
27
+ specCount: runnerStats.specs?.length ?? 0
28
+ });
29
+ }
30
+
31
+ onTestPass(testStats: { title: string; duration?: number }): void {
32
+ this.writeTestEvent('passed', testStats);
33
+ }
34
+
35
+ onTestFail(testStats: {
36
+ title: string;
37
+ duration?: number;
38
+ errors?: Array<{ message?: string }>;
39
+ }): void {
40
+ this.writeTestEvent('failed', testStats, {
41
+ error: testStats.errors?.[0]?.message
42
+ });
43
+ }
44
+
45
+ onTestSkip(testStats: { title: string; duration?: number }): void {
46
+ this.writeTestEvent('skipped', testStats);
47
+ }
48
+
49
+ onRunnerEnd(runnerStats: { failures?: number }): void {
50
+ this.writeEvent({
51
+ event: 'run.finished',
52
+ status: runnerStats.failures ? 'failed' : 'passed'
53
+ });
54
+ }
55
+
56
+ private writeTestEvent(
57
+ status: string,
58
+ testStats: { title: string; duration?: number },
59
+ extra: Record<string, unknown> = {}
60
+ ): void {
61
+ this.writeEvent({
62
+ event: 'test.finished',
63
+ title: testStats.title,
64
+ tags: testStats.title.match(/@\w+/g) ?? [],
65
+ status,
66
+ durationMs: testStats.duration ?? 0,
67
+ ...extra
68
+ });
69
+ }
70
+
71
+ private writeEvent(payload: Record<string, unknown>): void {
72
+ fs.appendFileSync(
73
+ this.outputFile,
74
+ `${JSON.stringify({ timestamp: new Date().toISOString(), ...payload })}\n`,
75
+ 'utf8'
76
+ );
77
+ }
78
+ }
@@ -0,0 +1,5 @@
1
+ # Scripts
2
+
3
+ This folder contains helper scripts used by local development and CI.
4
+
5
+ - Keep shell and Node entrypoints here instead of inside workflow files.
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+
3
+ import crypto from 'node:crypto';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+
7
+ const currentProcess = globalThis.process;
8
+ const envPath = path.resolve(currentProcess.cwd(), '.env');
9
+
10
+ if (fs.existsSync(envPath)) {
11
+ currentProcess.exit(0);
12
+ }
13
+
14
+ const projectSlug = path
15
+ .basename(currentProcess.cwd())
16
+ .toLowerCase()
17
+ .replace(/[^a-z0-9]+/g, '-')
18
+ .replace(/^-+|-+$/g, '')
19
+ .slice(0, 12);
20
+ const username = `${projectSlug || 'local'}-${crypto.randomBytes(3).toString('hex')}`;
21
+ const password = `${crypto.randomBytes(9).toString('base64url')}A1!`;
22
+
23
+ const envContents = [
24
+ 'TEST_ENV=dev',
25
+ 'TEST_RUN_ID=local',
26
+ 'DEV_UI_BASE_URL=http://127.0.0.1:3000',
27
+ `DEV_APP_USERNAME=${username}`,
28
+ `DEV_APP_PASSWORD=${password}`,
29
+ `UI_DEMO_USERNAME=${username}`,
30
+ `UI_DEMO_PASSWORD=${password}`
31
+ ].join('\n');
32
+
33
+ fs.writeFileSync(envPath, `${envContents}\n`, 'utf8');
34
+ currentProcess.stdout.write(
35
+ `Generated local .env with demo credentials for ${username}\n`
36
+ );
@@ -0,0 +1,72 @@
1
+ // Builds a local Allure report from raw test results after a test run completes.
2
+ import { AllureReport, readConfig } from '@allurereport/core';
3
+ import { readdir, rm, stat } from 'node:fs/promises';
4
+ import { join, resolve } from 'node:path';
5
+ import process from 'node:process';
6
+
7
+ const cwd = process.cwd();
8
+ const resultsDir = resolve(cwd, 'allure-results');
9
+ const outputDir = resolve(cwd, 'reports/allure');
10
+
11
+ const collectResultFiles = async () => {
12
+ const entries = (await readdir(resultsDir)).sort();
13
+ const files = [];
14
+
15
+ for (const entry of entries) {
16
+ const filePath = join(resultsDir, entry);
17
+ const entryStat = await stat(filePath);
18
+
19
+ if (entryStat.isFile()) {
20
+ files.push(filePath);
21
+ }
22
+ }
23
+
24
+ return files;
25
+ };
26
+
27
+ const generateReport = async () => {
28
+ try {
29
+ await stat(resultsDir);
30
+ } catch {
31
+ process.stdout.write(
32
+ 'Skipping Allure report generation because allure-results does not exist.\n'
33
+ );
34
+ return;
35
+ }
36
+
37
+ const files = await collectResultFiles();
38
+
39
+ if (files.length === 0) {
40
+ process.stdout.write(
41
+ 'Skipping Allure report generation because no result files were found.\n'
42
+ );
43
+ return;
44
+ }
45
+
46
+ await rm(outputDir, { force: true, recursive: true });
47
+
48
+ const config = await readConfig(cwd, 'allurerc.mjs', { output: outputDir });
49
+ const report = new AllureReport(config);
50
+
51
+ await report.start();
52
+
53
+ for (const file of files) {
54
+ await report.readFile(file);
55
+ }
56
+
57
+ await report.done();
58
+
59
+ process.stdout.write(
60
+ 'Allure report generated at reports/allure/index.html\n'
61
+ );
62
+ };
63
+
64
+ generateReport().catch((error) => {
65
+ if (error instanceof Error) {
66
+ process.stderr.write(`${error.message}\n`);
67
+ } else {
68
+ process.stderr.write(`${String(error)}\n`);
69
+ }
70
+
71
+ process.exit(1);
72
+ });
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ node ./scripts/ensure-local-env.mjs
5
+ npm run lint
6
+ npm run typecheck
7
+ npm test -- "$@"
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Starts the local demo app when needed, then launches the WebdriverIO runner.
4
+ import process from 'node:process';
5
+ import path from 'node:path';
6
+ import { spawn } from 'node:child_process';
7
+
8
+ import dotenv from 'dotenv';
9
+
10
+ const args = process.argv.slice(2);
11
+ const cwd = process.cwd();
12
+ const healthUrl = 'http://127.0.0.1:3000/health';
13
+ const environment = process.env.TEST_ENV ?? 'dev';
14
+ const environmentDefaults = {
15
+ dev: 'http://127.0.0.1:3000',
16
+ staging: 'https://staging-ui.example.internal',
17
+ prod: 'https://ui.example.internal'
18
+ };
19
+
20
+ dotenv.config({ path: path.resolve(cwd, '.env') });
21
+ dotenv.config({
22
+ path: path.resolve(cwd, `.env.${environment}`),
23
+ override: true
24
+ });
25
+
26
+ const uiBaseUrl =
27
+ process.env[`${environment.toUpperCase()}_UI_BASE_URL`] ??
28
+ process.env.UI_BASE_URL ??
29
+ environmentDefaults[environment] ??
30
+ environmentDefaults.dev;
31
+
32
+ const shouldAutoStartDemoApp =
33
+ environment === 'dev' &&
34
+ uiBaseUrl === environmentDefaults.dev &&
35
+ process.env.WDIO_DISABLE_LOCAL_DEMO_APP !== 'true';
36
+
37
+ function getCommandName(command) {
38
+ return process.platform === 'win32' ? `${command}.cmd` : command;
39
+ }
40
+
41
+ function spawnCommand(command, commandArgs, options = {}) {
42
+ return spawn(getCommandName(command), commandArgs, {
43
+ cwd,
44
+ stdio: 'inherit',
45
+ ...options
46
+ });
47
+ }
48
+
49
+ async function waitForHealthcheck(url, timeoutMs = 30_000) {
50
+ const start = Date.now();
51
+
52
+ while (Date.now() - start < timeoutMs) {
53
+ try {
54
+ const response = await globalThis.fetch(url);
55
+ if (response.ok) {
56
+ return;
57
+ }
58
+ } catch {
59
+ // Service is not ready yet.
60
+ }
61
+
62
+ await new Promise((resolve) => globalThis.setTimeout(resolve, 500));
63
+ }
64
+
65
+ throw new Error(`Timed out waiting for ${url}`);
66
+ }
67
+
68
+ function killChild(child) {
69
+ if (!child || child.killed) {
70
+ return;
71
+ }
72
+
73
+ child.kill('SIGTERM');
74
+ }
75
+
76
+ async function run() {
77
+ let demoAppProcess;
78
+
79
+ try {
80
+ if (shouldAutoStartDemoApp) {
81
+ demoAppProcess = spawnCommand('npm', ['run', 'demo:ui']);
82
+ await waitForHealthcheck(healthUrl);
83
+ }
84
+
85
+ const wdioProcess = spawnCommand('npx', [
86
+ 'wdio',
87
+ 'run',
88
+ './wdio.conf.ts',
89
+ ...args
90
+ ]);
91
+
92
+ const exitCode = await new Promise((resolve) => {
93
+ wdioProcess.on('close', resolve);
94
+ wdioProcess.on('error', () => resolve(1));
95
+ });
96
+
97
+ if (exitCode !== 0) {
98
+ process.exit(Number(exitCode) || 1);
99
+ }
100
+ } finally {
101
+ killChild(demoAppProcess);
102
+ }
103
+ }
104
+
105
+ for (const signal of ['SIGINT', 'SIGTERM']) {
106
+ process.on(signal, () => process.exit(1));
107
+ }
108
+
109
+ run().catch((error) => {
110
+ process.stderr.write(
111
+ `${error instanceof Error ? error.message : String(error)}\n`
112
+ );
113
+ process.exit(1);
114
+ });
@@ -0,0 +1,7 @@
1
+ # Tests
2
+
3
+ This folder holds user-facing Playwright scenarios.
4
+
5
+ - Keep tests focused on behavior and business flow.
6
+ - Use fixtures from `../fixtures/test-fixtures`.
7
+ - Keep selectors out of test files.
@@ -0,0 +1,52 @@
1
+ // Starter WDIO scenario that demonstrates the preferred spec style for this template.
2
+ import assert from 'node:assert/strict';
3
+
4
+ import { loadRuntimeConfig } from '../config/runtime-config';
5
+ import { DataFactory } from '../data/factories/data-factory';
6
+ import { LoginPage } from '../pages/login-page';
7
+ import { PeoplePage } from '../pages/people-page';
8
+ import { createLogger } from '../utils/logger';
9
+ import { StepLogger } from '../utils/test-step';
10
+
11
+ describe('UI starter journey', () => {
12
+ it('login and add one person @smoke @critical', async () => {
13
+ const appConfig = loadRuntimeConfig();
14
+ const logger = createLogger({
15
+ test: 'ui-journey.spec.ts > UI starter journey > login and add one person @smoke @critical'
16
+ });
17
+ const stepLogger = new StepLogger(logger.child({ scope: 'steps' }));
18
+ const dataFactory = new DataFactory(appConfig.testRunId);
19
+ const loginPage = new LoginPage(
20
+ appConfig.uiBaseUrl,
21
+ logger.child({ pageObject: 'LoginPage' })
22
+ );
23
+ const peoplePage = new PeoplePage(
24
+ appConfig.uiBaseUrl,
25
+ logger.child({ pageObject: 'PeoplePage' })
26
+ );
27
+ const person = dataFactory.person();
28
+
29
+ await stepLogger.run('Sign in to the demo app', async () => {
30
+ await loginPage.goto();
31
+ await loginPage.login(
32
+ appConfig.credentials.username,
33
+ appConfig.credentials.password
34
+ );
35
+ await peoplePage.waitForReady();
36
+ assert.match(
37
+ await peoplePage.getWelcomeMessage(),
38
+ new RegExp(appConfig.credentials.username)
39
+ );
40
+ });
41
+
42
+ await stepLogger.run('Add one person and verify the list', async () => {
43
+ await peoplePage.addPerson(person);
44
+ assert.match(await peoplePage.getFlashMessage(), /Person added/);
45
+ assert.deepEqual(await peoplePage.getPersonSummary(person.personId), {
46
+ name: person.name,
47
+ role: person.role,
48
+ email: person.email
49
+ });
50
+ });
51
+ });
52
+ });
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "esnext",
5
+ "moduleResolution": "node",
6
+ "lib": ["ES2022", "DOM"],
7
+ "types": ["node", "mocha", "@wdio/globals/types"],
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "resolveJsonModule": true,
11
+ "skipLibCheck": true,
12
+ "baseUrl": "."
13
+ },
14
+ "include": ["**/*.ts"],
15
+ "exclude": [
16
+ "node_modules",
17
+ "reports",
18
+ "allure-results",
19
+ "allure-report",
20
+ "test-results"
21
+ ]
22
+ }
@@ -0,0 +1,5 @@
1
+ # Utils
2
+
3
+ This folder holds small framework helpers.
4
+
5
+ - Put generic logging and step wrappers here.
@@ -0,0 +1,60 @@
1
+ // Structured JSON logger used by tests, pages, and helper classes.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ type LogLevel = 'info' | 'error';
6
+
7
+ type LogContext = Record<string, string | number | boolean | null | undefined>;
8
+
9
+ const LOG_FILE = path.resolve(process.cwd(), 'reports/logs/execution.log');
10
+
11
+ function ensureLogDirectory(): void {
12
+ fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
13
+ }
14
+
15
+ function serialize(
16
+ level: LogLevel,
17
+ message: string,
18
+ context?: LogContext
19
+ ): string {
20
+ return JSON.stringify({
21
+ timestamp: new Date().toISOString(),
22
+ level,
23
+ message,
24
+ ...context
25
+ });
26
+ }
27
+
28
+ export class Logger {
29
+ constructor(private readonly context: LogContext = {}) {
30
+ ensureLogDirectory();
31
+ }
32
+
33
+ child(context: LogContext): Logger {
34
+ return new Logger({
35
+ ...this.context,
36
+ ...context
37
+ });
38
+ }
39
+
40
+ info(message: string, context?: LogContext): void {
41
+ this.write('info', message, context);
42
+ }
43
+
44
+ error(message: string, context?: LogContext): void {
45
+ this.write('error', message, context);
46
+ }
47
+
48
+ private write(level: LogLevel, message: string, context?: LogContext): void {
49
+ const line = serialize(level, message, {
50
+ ...this.context,
51
+ ...context
52
+ });
53
+ fs.appendFileSync(LOG_FILE, `${line}\n`, 'utf8');
54
+ console.log(line);
55
+ }
56
+ }
57
+
58
+ export function createLogger(context: LogContext = {}): Logger {
59
+ return new Logger(context);
60
+ }
@@ -0,0 +1,20 @@
1
+ import type { Logger } from './logger';
2
+
3
+ export class StepLogger {
4
+ constructor(private readonly logger: Logger) {}
5
+
6
+ async run<T>(message: string, callback: () => Promise<T>): Promise<T> {
7
+ this.logger.info('step.started', { step: message });
8
+ try {
9
+ const result = await callback();
10
+ this.logger.info('step.passed', { step: message });
11
+ return result;
12
+ } catch (error) {
13
+ this.logger.error('step.failed', {
14
+ step: message,
15
+ error: error instanceof Error ? error.message : String(error)
16
+ });
17
+ throw error;
18
+ }
19
+ }
20
+ }