@toolstackhq/create-qa-patterns 1.0.13 → 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 +23 -0
  2. package/index.js +282 -738
  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 +29 -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 +25 -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
@@ -1,8 +1,8 @@
1
1
  // Page object for the login screen and its user actions.
2
- import type { Page } from "@playwright/test";
2
+ import type { Page } from '@playwright/test';
3
3
 
4
- import type { Logger } from "../utils/logger";
5
- import { BasePage } from "./base-page";
4
+ import type { Logger } from '../utils/logger';
5
+ import { BasePage } from './base-page';
6
6
 
7
7
  export class LoginPage extends BasePage {
8
8
  constructor(page: Page, baseUrl: string, logger: Logger) {
@@ -10,14 +10,14 @@ export class LoginPage extends BasePage {
10
10
  }
11
11
 
12
12
  async goto(): Promise<void> {
13
- this.logger.info("page.goto", { page: "login" });
14
- await this.page.goto(this.buildUrl("/login"));
13
+ this.logger.info('page.goto', { page: 'login' });
14
+ await this.page.goto(this.buildUrl('/login'));
15
15
  }
16
16
 
17
17
  async login(username: string, password: string): Promise<void> {
18
- this.logger.info("login.submit", { username });
19
- await this.page.getByLabel("Username").fill(username);
20
- await this.page.getByLabel("Password").fill(password);
21
- await this.page.getByRole("button", { name: "Sign in" }).click();
18
+ this.logger.info('login.submit', { username });
19
+ await this.page.getByLabel('Username').fill(username);
20
+ await this.page.getByLabel('Password').fill(password);
21
+ await this.page.getByRole('button', { name: 'Sign in' }).click();
22
22
  }
23
23
  }
@@ -1,9 +1,9 @@
1
1
  // Page object for the people screen used in the starter UI workflow.
2
- import type { Page } from "@playwright/test";
2
+ import type { Page } from '@playwright/test';
3
3
 
4
- import type { PersonRecord } from "../data/factories/data-factory";
5
- import type { Logger } from "../utils/logger";
6
- import { BasePage } from "./base-page";
4
+ import type { PersonRecord } from '../data/factories/data-factory';
5
+ import type { Logger } from '../utils/logger';
6
+ import { BasePage } from './base-page';
7
7
 
8
8
  export class PeoplePage extends BasePage {
9
9
  constructor(page: Page, baseUrl: string, logger: Logger) {
@@ -11,30 +11,34 @@ export class PeoplePage extends BasePage {
11
11
  }
12
12
 
13
13
  async waitForReady(): Promise<void> {
14
- await this.page.getByRole("heading", { level: 1, name: "People", exact: true }).waitFor();
14
+ await this.page
15
+ .getByRole('heading', { level: 1, name: 'People', exact: true })
16
+ .waitFor();
15
17
  }
16
18
 
17
19
  async addPerson(person: PersonRecord): Promise<void> {
18
- this.logger.info("person.create", { personId: person.personId });
19
- await this.page.getByLabel("Person ID").fill(person.personId);
20
- await this.page.getByLabel("Name").fill(person.name);
21
- await this.page.getByLabel("Role").fill(person.role);
22
- await this.page.getByLabel("Email").fill(person.email);
23
- await this.page.getByRole("button", { name: "Add person" }).click();
24
- await this.page.waitForLoadState("networkidle");
20
+ this.logger.info('person.create', { personId: person.personId });
21
+ await this.page.getByLabel('Person ID').fill(person.personId);
22
+ await this.page.getByLabel('Name').fill(person.name);
23
+ await this.page.getByLabel('Role').fill(person.role);
24
+ await this.page.getByLabel('Email').fill(person.email);
25
+ await this.page.getByRole('button', { name: 'Add person' }).click();
26
+ await this.page.waitForLoadState('networkidle');
25
27
  }
26
28
 
27
- async getPersonSummary(personId: string): Promise<{ name: string; role: string; email: string } | null> {
29
+ async getPersonSummary(
30
+ personId: string
31
+ ): Promise<{ name: string; role: string; email: string } | null> {
28
32
  const row = this.page.getByTestId(`person-row-${personId}`);
29
33
  if (!(await row.count())) {
30
34
  return null;
31
35
  }
32
36
 
33
- const cells = row.getByRole("cell");
37
+ const cells = row.getByRole('cell');
34
38
  return {
35
- name: (await cells.nth(0).textContent()) ?? "",
36
- role: (await cells.nth(1).textContent()) ?? "",
37
- email: (await cells.nth(2).textContent()) ?? ""
39
+ name: (await cells.nth(0).textContent()) ?? '',
40
+ role: (await cells.nth(1).textContent()) ?? '',
41
+ email: (await cells.nth(2).textContent()) ?? ''
38
42
  };
39
43
  }
40
44
  }
@@ -1,17 +1,17 @@
1
1
  // Central Playwright configuration for local runs, CI, reporters, and demo app startup.
2
- import { defineConfig, devices } from "@playwright/test";
2
+ import { defineConfig, devices } from '@playwright/test';
3
3
 
4
- import { loadRuntimeConfig } from "./config/runtime-config";
4
+ import { loadRuntimeConfig } from './config/runtime-config';
5
5
 
6
6
  const runtimeConfig = loadRuntimeConfig();
7
7
  const shouldAutoStartDemoApps =
8
- runtimeConfig.testEnv === "dev" &&
9
- runtimeConfig.uiBaseUrl === "http://127.0.0.1:3000" &&
10
- runtimeConfig.apiBaseUrl === "http://127.0.0.1:3001" &&
11
- process.env.PW_DISABLE_LOCAL_DEMO_APPS !== "true";
8
+ runtimeConfig.testEnv === 'dev' &&
9
+ runtimeConfig.uiBaseUrl === 'http://127.0.0.1:3000' &&
10
+ runtimeConfig.apiBaseUrl === 'http://127.0.0.1:3001' &&
11
+ process.env.PW_DISABLE_LOCAL_DEMO_APPS !== 'true';
12
12
 
13
13
  export default defineConfig({
14
- testDir: "./tests",
14
+ testDir: './tests',
15
15
  fullyParallel: false,
16
16
  forbidOnly: Boolean(process.env.CI),
17
17
  retries: process.env.CI ? 1 : 0,
@@ -20,20 +20,23 @@ export default defineConfig({
20
20
  expect: {
21
21
  timeout: 10_000
22
22
  },
23
- outputDir: "test-results",
23
+ outputDir: 'test-results',
24
24
  reporter: [
25
- ["list"],
26
- ["html", { open: "never", outputFolder: "reports/html" }],
25
+ ['list'],
26
+ ['html', { open: 'never', outputFolder: 'reports/html' }],
27
27
  // Keep the HTML reporter as the default path most users expect.
28
28
  // Remove the Allure line below if you prefer to stay with Playwright's built-in reporters only.
29
- ["allure-playwright", { resultsDir: "allure-results" }],
30
- ["./reporters/structured-reporter.ts", { outputFile: "reports/logs/playwright-events.jsonl" }]
29
+ ['allure-playwright', { resultsDir: 'allure-results' }],
30
+ [
31
+ './reporters/structured-reporter.ts',
32
+ { outputFile: 'reports/logs/playwright-events.jsonl' }
33
+ ]
31
34
  ],
32
35
  use: {
33
36
  baseURL: runtimeConfig.uiBaseUrl,
34
- trace: "retain-on-failure",
35
- screenshot: "only-on-failure",
36
- video: "retain-on-failure",
37
+ trace: 'retain-on-failure',
38
+ screenshot: 'only-on-failure',
39
+ video: 'retain-on-failure',
37
40
  headless: !process.env.PWDEBUG
38
41
  },
39
42
  metadata: {
@@ -44,13 +47,13 @@ export default defineConfig({
44
47
  webServer: shouldAutoStartDemoApps
45
48
  ? [
46
49
  {
47
- command: "npm run demo:ui",
50
+ command: 'npm run demo:ui',
48
51
  url: `${runtimeConfig.uiBaseUrl}/health`,
49
52
  reuseExistingServer: !process.env.CI,
50
53
  timeout: 30_000
51
54
  },
52
55
  {
53
- command: "npm run demo:api",
56
+ command: 'npm run demo:api',
54
57
  url: `${runtimeConfig.apiBaseUrl}/health`,
55
58
  reuseExistingServer: !process.env.CI,
56
59
  timeout: 30_000
@@ -59,9 +62,9 @@ export default defineConfig({
59
62
  : undefined,
60
63
  projects: [
61
64
  {
62
- name: "chromium",
65
+ name: 'chromium',
63
66
  use: {
64
- ...devices["Desktop Chrome"]
67
+ ...devices['Desktop Chrome']
65
68
  }
66
69
  }
67
70
  ]
@@ -1,6 +1,6 @@
1
1
  // Custom reporter that writes machine-readable test lifecycle events to disk.
2
- import fs from "node:fs";
3
- import path from "node:path";
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
4
 
5
5
  import type {
6
6
  FullConfig,
@@ -9,14 +9,17 @@ import type {
9
9
  Suite,
10
10
  TestCase,
11
11
  TestResult
12
- } from "@playwright/test/reporter";
12
+ } from '@playwright/test/reporter';
13
13
 
14
14
  type ReporterOptions = {
15
15
  outputFile?: string;
16
16
  };
17
17
 
18
18
  class StructuredReporter implements Reporter {
19
- private outputFile = path.resolve(process.cwd(), "reports/logs/playwright-events.jsonl");
19
+ private outputFile = path.resolve(
20
+ process.cwd(),
21
+ 'reports/logs/playwright-events.jsonl'
22
+ );
20
23
 
21
24
  constructor(options?: ReporterOptions) {
22
25
  if (options?.outputFile) {
@@ -27,7 +30,7 @@ class StructuredReporter implements Reporter {
27
30
 
28
31
  onBegin(config: FullConfig, suite: Suite): void {
29
32
  this.write({
30
- event: "run.started",
33
+ event: 'run.started',
31
34
  projectCount: config.projects.length,
32
35
  testCount: suite.allTests().length
33
36
  });
@@ -35,7 +38,7 @@ class StructuredReporter implements Reporter {
35
38
 
36
39
  onTestEnd(test: TestCase, result: TestResult): void {
37
40
  this.write({
38
- event: "test.finished",
41
+ event: 'test.finished',
39
42
  title: test.title,
40
43
  tags: test.title.match(/@\w+/g) ?? [],
41
44
  status: result.status,
@@ -45,7 +48,7 @@ class StructuredReporter implements Reporter {
45
48
 
46
49
  onEnd(result: FullResult): void {
47
50
  this.write({
48
- event: "run.finished",
51
+ event: 'run.finished',
49
52
  status: result.status
50
53
  });
51
54
  }
@@ -54,7 +57,7 @@ class StructuredReporter implements Reporter {
54
57
  fs.appendFileSync(
55
58
  this.outputFile,
56
59
  `${JSON.stringify({ timestamp: new Date().toISOString(), ...payload })}\n`,
57
- "utf8"
60
+ 'utf8'
58
61
  );
59
62
  }
60
63
  }
@@ -0,0 +1,37 @@
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_API_BASE_URL=http://127.0.0.1:3001',
28
+ `DEV_APP_USERNAME=${username}`,
29
+ `DEV_APP_PASSWORD=${password}`,
30
+ `UI_DEMO_USERNAME=${username}`,
31
+ `UI_DEMO_PASSWORD=${password}`
32
+ ].join('\n');
33
+
34
+ fs.writeFileSync(envPath, `${envContents}\n`, 'utf8');
35
+ currentProcess.stdout.write(
36
+ `Generated local .env with demo credentials for ${username}\n`
37
+ );
@@ -1,12 +1,12 @@
1
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";
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
6
 
7
7
  const cwd = process.cwd();
8
- const resultsDir = resolve(cwd, "allure-results");
9
- const outputDir = resolve(cwd, "reports/allure");
8
+ const resultsDir = resolve(cwd, 'allure-results');
9
+ const outputDir = resolve(cwd, 'reports/allure');
10
10
 
11
11
  const collectResultFiles = async () => {
12
12
  const entries = (await readdir(resultsDir)).sort();
@@ -28,20 +28,24 @@ const generateReport = async () => {
28
28
  try {
29
29
  await stat(resultsDir);
30
30
  } catch {
31
- process.stdout.write("Skipping Allure report generation because allure-results does not exist.\n");
31
+ process.stdout.write(
32
+ 'Skipping Allure report generation because allure-results does not exist.\n'
33
+ );
32
34
  return;
33
35
  }
34
36
 
35
37
  const files = await collectResultFiles();
36
38
 
37
39
  if (files.length === 0) {
38
- process.stdout.write("Skipping Allure report generation because no result files were found.\n");
40
+ process.stdout.write(
41
+ 'Skipping Allure report generation because no result files were found.\n'
42
+ );
39
43
  return;
40
44
  }
41
45
 
42
46
  await rm(outputDir, { force: true, recursive: true });
43
47
 
44
- const config = await readConfig(cwd, "allurerc.mjs", { output: outputDir });
48
+ const config = await readConfig(cwd, 'allurerc.mjs', { output: outputDir });
45
49
  const report = new AllureReport(config);
46
50
 
47
51
  await report.start();
@@ -52,7 +56,9 @@ const generateReport = async () => {
52
56
 
53
57
  await report.done();
54
58
 
55
- process.stdout.write("Allure report generated at reports/allure/index.html\n");
59
+ process.stdout.write(
60
+ 'Allure report generated at reports/allure/index.html\n'
61
+ );
56
62
  };
57
63
 
58
64
  generateReport().catch((error) => {
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
 
4
+ node ./scripts/ensure-local-env.mjs
4
5
  npm run lint
5
6
  npm run typecheck
6
7
  npx playwright test "$@"
@@ -1,8 +1,8 @@
1
1
  // Starter API journey that pairs with the deterministic demo API server.
2
- import { expect, test } from "../fixtures/test-fixtures";
2
+ import { expect, test } from '../fixtures/test-fixtures';
3
3
 
4
- test.describe("API starter flow", () => {
5
- test("create and list one person @regression", async ({
4
+ test.describe('API starter flow', () => {
5
+ test('create and list one person @regression', async ({
6
6
  appConfig,
7
7
  dataFactory,
8
8
  request,
@@ -10,7 +10,7 @@ test.describe("API starter flow", () => {
10
10
  }) => {
11
11
  const person = dataFactory.person();
12
12
 
13
- await stepLogger.run("Create one person through the API", async () => {
13
+ await stepLogger.run('Create one person through the API', async () => {
14
14
  const response = await request.post(`${appConfig.apiBaseUrl}/people`, {
15
15
  data: person
16
16
  });
@@ -18,12 +18,14 @@ test.describe("API starter flow", () => {
18
18
  expect(await response.json()).toMatchObject(person);
19
19
  });
20
20
 
21
- await stepLogger.run("List people and verify the new record", async () => {
21
+ await stepLogger.run('List people and verify the new record', async () => {
22
22
  const response = await request.get(`${appConfig.apiBaseUrl}/people`);
23
23
  expect(response.ok()).toBeTruthy();
24
24
 
25
25
  const people = await response.json();
26
- expect(people).toContainEqual(expect.objectContaining({ personId: person.personId }));
26
+ expect(people).toContainEqual(
27
+ expect.objectContaining({ personId: person.personId })
28
+ );
27
29
  });
28
30
  });
29
31
  });
@@ -1,8 +1,8 @@
1
1
  // Starter UI journey that shows the preferred Playwright test style in this template.
2
- import { expect, test } from "../fixtures/test-fixtures";
2
+ import { expect, test } from '../fixtures/test-fixtures';
3
3
 
4
- test.describe("UI starter journey", () => {
5
- test("login and add one person @smoke @critical", async ({
4
+ test.describe('UI starter journey', () => {
5
+ test('login and add one person @smoke @critical', async ({
6
6
  appConfig,
7
7
  loginPage,
8
8
  peoplePage,
@@ -11,16 +11,21 @@ test.describe("UI starter journey", () => {
11
11
  }) => {
12
12
  const person = dataFactory.person();
13
13
 
14
- await stepLogger.run("Sign in to the demo app", async () => {
14
+ await stepLogger.run('Sign in to the demo app', async () => {
15
15
  await loginPage.goto();
16
- await loginPage.login(appConfig.credentials.username, appConfig.credentials.password);
16
+ await loginPage.login(
17
+ appConfig.credentials.username,
18
+ appConfig.credentials.password
19
+ );
17
20
  await peoplePage.waitForReady();
18
- expect(await peoplePage.getWelcomeMessage()).toContain(appConfig.credentials.username);
21
+ expect(await peoplePage.getWelcomeMessage()).toContain(
22
+ appConfig.credentials.username
23
+ );
19
24
  });
20
25
 
21
- await stepLogger.run("Add one person and verify the list", async () => {
26
+ await stepLogger.run('Add one person and verify the list', async () => {
22
27
  await peoplePage.addPerson(person);
23
- expect(await peoplePage.flashMessage.getText()).toContain("Person added");
28
+ expect(await peoplePage.flashMessage.getText()).toContain('Person added');
24
29
  expect(await peoplePage.getPersonSummary(person.personId)).toEqual({
25
30
  name: person.name,
26
31
  role: person.role,
@@ -3,23 +3,15 @@
3
3
  "target": "ES2022",
4
4
  "module": "commonjs",
5
5
  "moduleResolution": "node",
6
- "lib": [
7
- "ES2022",
8
- "DOM"
9
- ],
10
- "types": [
11
- "node",
12
- "@playwright/test"
13
- ],
6
+ "lib": ["ES2022", "DOM"],
7
+ "types": ["node", "@playwright/test"],
14
8
  "strict": true,
15
9
  "esModuleInterop": true,
16
10
  "resolveJsonModule": true,
17
11
  "skipLibCheck": true,
18
12
  "baseUrl": "."
19
13
  },
20
- "include": [
21
- "**/*.ts"
22
- ],
14
+ "include": ["**/*.ts"],
23
15
  "exclude": [
24
16
  "node_modules",
25
17
  "reports",
@@ -1,18 +1,22 @@
1
1
  // Structured JSON logger used by tests, pages, and helper classes.
2
- import fs from "node:fs";
3
- import path from "node:path";
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
4
 
5
- type LogLevel = "info" | "error";
5
+ type LogLevel = 'info' | 'error';
6
6
 
7
7
  type LogContext = Record<string, string | number | boolean | null | undefined>;
8
8
 
9
- const LOG_FILE = path.resolve(process.cwd(), "reports/logs/execution.log");
9
+ const LOG_FILE = path.resolve(process.cwd(), 'reports/logs/execution.log');
10
10
 
11
11
  function ensureLogDirectory(): void {
12
12
  fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
13
13
  }
14
14
 
15
- function serialize(level: LogLevel, message: string, context?: LogContext): string {
15
+ function serialize(
16
+ level: LogLevel,
17
+ message: string,
18
+ context?: LogContext
19
+ ): string {
16
20
  return JSON.stringify({
17
21
  timestamp: new Date().toISOString(),
18
22
  level,
@@ -34,11 +38,11 @@ export class Logger {
34
38
  }
35
39
 
36
40
  info(message: string, context?: LogContext): void {
37
- this.write("info", message, context);
41
+ this.write('info', message, context);
38
42
  }
39
43
 
40
44
  error(message: string, context?: LogContext): void {
41
- this.write("error", message, context);
45
+ this.write('error', message, context);
42
46
  }
43
47
 
44
48
  private write(level: LogLevel, message: string, context?: LogContext): void {
@@ -46,7 +50,7 @@ export class Logger {
46
50
  ...this.context,
47
51
  ...context
48
52
  });
49
- fs.appendFileSync(LOG_FILE, `${line}\n`, "utf8");
53
+ fs.appendFileSync(LOG_FILE, `${line}\n`, 'utf8');
50
54
  console.log(line);
51
55
  }
52
56
  }
@@ -1,19 +1,19 @@
1
1
  // Small wrapper that records named test steps in a consistent way.
2
- import { test } from "@playwright/test";
2
+ import { test } from '@playwright/test';
3
3
 
4
- import type { Logger } from "./logger";
4
+ import type { Logger } from './logger';
5
5
 
6
6
  export class StepLogger {
7
7
  constructor(private readonly logger: Logger) {}
8
8
 
9
9
  async run<T>(message: string, callback: () => Promise<T>): Promise<T> {
10
- this.logger.info("step.started", { step: message });
10
+ this.logger.info('step.started', { step: message });
11
11
  try {
12
12
  const result = await test.step(message, callback);
13
- this.logger.info("step.passed", { step: message });
13
+ this.logger.info('step.passed', { step: message });
14
14
  return result;
15
15
  } catch (error) {
16
- this.logger.error("step.failed", {
16
+ this.logger.error('step.failed', {
17
17
  step: message,
18
18
  error: error instanceof Error ? error.message : String(error)
19
19
  });
@@ -0,0 +1,14 @@
1
+ TEST_ENV=dev
2
+ TEST_RUN_ID=local
3
+
4
+ DEV_UI_BASE_URL=http://127.0.0.1:3000
5
+ DEV_APP_USERNAME=generate-a-local-username
6
+ DEV_APP_PASSWORD=generate-a-local-password
7
+
8
+ STAGING_UI_BASE_URL=https://staging-ui.example.internal
9
+ STAGING_APP_USERNAME=your-staging-username
10
+ STAGING_APP_PASSWORD=your-staging-password
11
+
12
+ PROD_UI_BASE_URL=https://ui.example.internal
13
+ PROD_APP_USERNAME=your-prod-username
14
+ PROD_APP_PASSWORD=your-prod-password
@@ -0,0 +1,46 @@
1
+ name: WebdriverIO Template Validation
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ paths:
7
+ - 'templates/wdio-template/**'
8
+ - 'test-apps/**'
9
+ - 'package.json'
10
+
11
+ jobs:
12
+ wdio:
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 template dependencies
23
+ working-directory: templates/wdio-template
24
+ run: npm ci
25
+
26
+ - name: Run WebdriverIO validation
27
+ working-directory: templates/wdio-template
28
+ env:
29
+ TEST_ENV: dev
30
+ TEST_RUN_ID: ci
31
+ run: bash ./scripts/run-tests.sh
32
+
33
+ - name: Generate Allure report
34
+ if: always()
35
+ working-directory: templates/wdio-template
36
+ run: npm run report:allure
37
+
38
+ - name: Upload WebdriverIO artifacts
39
+ if: always()
40
+ uses: actions/upload-artifact@v4
41
+ with:
42
+ name: wdio-template-artifacts
43
+ if-no-files-found: ignore
44
+ path: |
45
+ templates/wdio-template/reports
46
+ templates/wdio-template/allure-results