@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,22 +1,22 @@
1
- const fs = require("node:fs");
2
- const http = require("node:http");
3
- const path = require("node:path");
4
- const querystring = require("node:querystring");
1
+ const fs = require('node:fs');
2
+ const http = require('node:http');
3
+ const path = require('node:path');
4
+ const querystring = require('node:querystring');
5
5
 
6
- const { createPerson, getPeople, state } = require("./store");
7
- const { layout, loginPage, peoplePage } = require("./templates");
6
+ const { createPerson, getPeople, state } = require('./store');
7
+ const { layout, loginPage, peoplePage } = require('./templates');
8
8
 
9
- const host = process.env.HOST || "0.0.0.0";
10
- const port = Number(process.env.PORT || "3000");
9
+ const host = process.env.HOST || '0.0.0.0';
10
+ const port = Number(process.env.PORT || '3000');
11
11
 
12
12
  function readBody(request) {
13
13
  return new Promise((resolve, reject) => {
14
- let body = "";
15
- request.on("data", (chunk) => {
14
+ let body = '';
15
+ request.on('data', (chunk) => {
16
16
  body += chunk.toString();
17
17
  });
18
- request.on("end", () => resolve(querystring.parse(body)));
19
- request.on("error", reject);
18
+ request.on('end', () => resolve(querystring.parse(body)));
19
+ request.on('error', reject);
20
20
  });
21
21
  }
22
22
 
@@ -26,8 +26,8 @@ function parseCookies(request) {
26
26
  return {};
27
27
  }
28
28
 
29
- return header.split(";").reduce((cookies, entry) => {
30
- const [key, value] = entry.trim().split("=");
29
+ return header.split(';').reduce((cookies, entry) => {
30
+ const [key, value] = entry.trim().split('=');
31
31
  cookies[key] = value;
32
32
  return cookies;
33
33
  }, {});
@@ -39,22 +39,22 @@ function redirect(response, location) {
39
39
  }
40
40
 
41
41
  function sendHtml(response, html) {
42
- response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
42
+ response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
43
43
  response.end(html);
44
44
  }
45
45
 
46
46
  function sendJson(response, payload) {
47
- response.writeHead(200, { "Content-Type": "application/json" });
47
+ response.writeHead(200, { 'Content-Type': 'application/json' });
48
48
  response.end(JSON.stringify(payload));
49
49
  }
50
50
 
51
51
  function isAuthenticated(request) {
52
- return parseCookies(request).session === "authenticated";
52
+ return parseCookies(request).session === 'authenticated';
53
53
  }
54
54
 
55
55
  function protectedRoute(request, response) {
56
56
  if (!isAuthenticated(request)) {
57
- redirect(response, "/login");
57
+ redirect(response, '/login');
58
58
  return false;
59
59
  }
60
60
 
@@ -62,53 +62,53 @@ function protectedRoute(request, response) {
62
62
  }
63
63
 
64
64
  function pageMessage(url) {
65
- return new URL(url, "http://127.0.0.1").searchParams.get("message") || "";
65
+ return new URL(url, 'http://127.0.0.1').searchParams.get('message') || '';
66
66
  }
67
67
 
68
68
  const server = http.createServer(async (request, response) => {
69
- const url = new URL(request.url, "http://127.0.0.1");
69
+ const url = new URL(request.url, 'http://127.0.0.1');
70
70
 
71
- if (request.method === "GET" && url.pathname === "/styles.css") {
72
- const cssPath = path.join(__dirname, "..", "public", "styles.css");
73
- response.writeHead(200, { "Content-Type": "text/css; charset=utf-8" });
74
- response.end(fs.readFileSync(cssPath, "utf8"));
71
+ if (request.method === 'GET' && url.pathname === '/styles.css') {
72
+ const cssPath = path.join(__dirname, '..', 'public', 'styles.css');
73
+ response.writeHead(200, { 'Content-Type': 'text/css; charset=utf-8' });
74
+ response.end(fs.readFileSync(cssPath, 'utf8'));
75
75
  return;
76
76
  }
77
77
 
78
- if (request.method === "GET" && url.pathname === "/health") {
79
- sendJson(response, { status: "ok", people: state.people.length });
78
+ if (request.method === 'GET' && url.pathname === '/health') {
79
+ sendJson(response, { status: 'ok', people: state.people.length });
80
80
  return;
81
81
  }
82
82
 
83
- if (request.method === "GET" && url.pathname === "/") {
84
- redirect(response, "/login");
83
+ if (request.method === 'GET' && url.pathname === '/') {
84
+ redirect(response, '/login');
85
85
  return;
86
86
  }
87
87
 
88
- if (request.method === "GET" && url.pathname === "/login") {
88
+ if (request.method === 'GET' && url.pathname === '/login') {
89
89
  sendHtml(response, loginPage());
90
90
  return;
91
91
  }
92
92
 
93
- if (request.method === "POST" && url.pathname === "/login") {
93
+ if (request.method === 'POST' && url.pathname === '/login') {
94
94
  const body = await readBody(request);
95
95
  if (
96
96
  body.username === state.credentials.username &&
97
97
  body.password === state.credentials.password
98
98
  ) {
99
99
  response.writeHead(302, {
100
- Location: "/people",
101
- "Set-Cookie": "session=authenticated; HttpOnly; Path=/; SameSite=Lax"
100
+ Location: '/people',
101
+ 'Set-Cookie': 'session=authenticated; HttpOnly; Path=/; SameSite=Lax'
102
102
  });
103
103
  response.end();
104
104
  return;
105
105
  }
106
106
 
107
- sendHtml(response, loginPage("Invalid credentials"));
107
+ sendHtml(response, loginPage('Invalid credentials'));
108
108
  return;
109
109
  }
110
110
 
111
- if (request.method === "GET" && url.pathname === "/people") {
111
+ if (request.method === 'GET' && url.pathname === '/people') {
112
112
  if (!protectedRoute(request, response)) {
113
113
  return;
114
114
  }
@@ -116,8 +116,8 @@ const server = http.createServer(async (request, response) => {
116
116
  sendHtml(
117
117
  response,
118
118
  layout({
119
- title: "People",
120
- body: peoplePage(getPeople(url.searchParams.get("search") || "")),
119
+ title: 'People',
120
+ body: peoplePage(getPeople(url.searchParams.get('search') || '')),
121
121
  flashMessage: pageMessage(request.url),
122
122
  username: state.credentials.username
123
123
  })
@@ -125,7 +125,7 @@ const server = http.createServer(async (request, response) => {
125
125
  return;
126
126
  }
127
127
 
128
- if (request.method === "POST" && url.pathname === "/people") {
128
+ if (request.method === 'POST' && url.pathname === '/people') {
129
129
  if (!protectedRoute(request, response)) {
130
130
  return;
131
131
  }
@@ -133,15 +133,18 @@ const server = http.createServer(async (request, response) => {
133
133
  try {
134
134
  const body = await readBody(request);
135
135
  createPerson(body);
136
- redirect(response, "/people?message=Person%20added");
136
+ redirect(response, '/people?message=Person%20added');
137
137
  } catch (error) {
138
- redirect(response, `/people?message=${encodeURIComponent(error.message)}`);
138
+ redirect(
139
+ response,
140
+ `/people?message=${encodeURIComponent(error.message)}`
141
+ );
139
142
  }
140
143
  return;
141
144
  }
142
145
 
143
- response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
144
- response.end("Not found");
146
+ response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
147
+ response.end('Not found');
145
148
  });
146
149
 
147
150
  server.listen(port, host, () => {
@@ -1,7 +1,35 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+
4
+ const dotenv = require('dotenv');
5
+
6
+ function loadClosestEnv(startDirectory) {
7
+ let currentDirectory = startDirectory;
8
+
9
+ while (true) {
10
+ const candidate = path.join(currentDirectory, '.env');
11
+ if (fs.existsSync(candidate)) {
12
+ dotenv.config({ path: candidate });
13
+ return;
14
+ }
15
+
16
+ const parentDirectory = path.dirname(currentDirectory);
17
+ if (parentDirectory === currentDirectory) {
18
+ return;
19
+ }
20
+
21
+ currentDirectory = parentDirectory;
22
+ }
23
+ }
24
+
25
+ loadClosestEnv(process.cwd());
26
+ loadClosestEnv(__dirname);
27
+
1
28
  const state = {
2
29
  credentials: {
3
- username: process.env.UI_DEMO_USERNAME || "tester",
4
- password: process.env.UI_DEMO_PASSWORD || "Password123!"
30
+ username:
31
+ process.env.UI_DEMO_USERNAME || process.env.DEV_APP_USERNAME || '',
32
+ password: process.env.UI_DEMO_PASSWORD || process.env.DEV_APP_PASSWORD || ''
5
33
  },
6
34
  people: []
7
35
  };
@@ -23,7 +51,7 @@ function createPerson(person) {
23
51
  });
24
52
  }
25
53
 
26
- function getPeople(search = "") {
54
+ function getPeople(search = '') {
27
55
  const query = String(search).trim().toLowerCase();
28
56
  if (!query) {
29
57
  return state.people;
@@ -1,4 +1,4 @@
1
- function layout({ title, body, flashMessage = "", username = "tester" }) {
1
+ function layout({ title, body, flashMessage = '', username = 'Local user' }) {
2
2
  return `<!DOCTYPE html>
3
3
  <html lang="en">
4
4
  <head>
@@ -15,7 +15,7 @@ function layout({ title, body, flashMessage = "", username = "tester" }) {
15
15
  <a href="/people">People</a>
16
16
  </nav>
17
17
  </header>
18
- ${flashMessage ? `<div class="flash-message" data-testid="flash-message" role="status">${flashMessage}</div>` : ""}
18
+ ${flashMessage ? `<div class="flash-message" data-testid="flash-message" role="status">${flashMessage}</div>` : ''}
19
19
  <p data-testid="welcome-message">Signed in as ${username}</p>
20
20
  ${body}
21
21
  </div>
@@ -23,7 +23,7 @@ function layout({ title, body, flashMessage = "", username = "tester" }) {
23
23
  </html>`;
24
24
  }
25
25
 
26
- function loginPage(errorMessage = "") {
26
+ function loginPage(errorMessage = '') {
27
27
  return `<!DOCTYPE html>
28
28
  <html lang="en">
29
29
  <head>
@@ -37,7 +37,7 @@ function loginPage(errorMessage = "") {
37
37
  <section class="card login-card">
38
38
  <h1>Login</h1>
39
39
  <p>Keep the example intentionally small: sign in, add one person, assert the list.</p>
40
- ${errorMessage ? `<div class="flash-message" role="status">${errorMessage}</div>` : ""}
40
+ ${errorMessage ? `<div class="flash-message" role="status">${errorMessage}</div>` : ''}
41
41
  <form action="/login" method="post">
42
42
  <label for="username">
43
43
  Username
@@ -64,7 +64,7 @@ function peoplePage(people) {
64
64
  <td>${person.email}</td>
65
65
  </tr>`;
66
66
  })
67
- .join("");
67
+ .join('');
68
68
 
69
69
  return `
70
70
  <section class="panel-grid">
@@ -1,79 +1,79 @@
1
- import js from "@eslint/js";
2
- import tseslint from "@typescript-eslint/eslint-plugin";
3
- import tsParser from "@typescript-eslint/parser";
4
- import { fileURLToPath } from "node:url";
1
+ import js from '@eslint/js';
2
+ import tseslint from '@typescript-eslint/eslint-plugin';
3
+ import tsParser from '@typescript-eslint/parser';
4
+ import { fileURLToPath } from 'node:url';
5
5
 
6
- import architecturePlugin from "./lint/architecture-plugin.cjs";
6
+ import architecturePlugin from './lint/architecture-plugin.cjs';
7
7
 
8
- const configDirectory = fileURLToPath(new globalThis.URL(".", import.meta.url));
8
+ const configDirectory = fileURLToPath(new globalThis.URL('.', import.meta.url));
9
9
 
10
10
  export default [
11
11
  {
12
12
  ignores: [
13
- "node_modules/**",
14
- "reports/**",
15
- "allure-results/**",
16
- "allure-report/**",
17
- "test-results/**",
18
- "playwright-report/**"
13
+ 'node_modules/**',
14
+ 'reports/**',
15
+ 'allure-results/**',
16
+ 'allure-report/**',
17
+ 'test-results/**',
18
+ 'playwright-report/**'
19
19
  ]
20
20
  },
21
21
  js.configs.recommended,
22
22
  {
23
- files: ["demo-apps/**/*.js"],
23
+ files: ['demo-apps/**/*.js'],
24
24
  languageOptions: {
25
25
  globals: {
26
- __dirname: "readonly",
27
- console: "readonly",
28
- module: "readonly",
29
- process: "readonly",
30
- require: "readonly",
31
- URL: "readonly"
26
+ __dirname: 'readonly',
27
+ console: 'readonly',
28
+ module: 'readonly',
29
+ process: 'readonly',
30
+ require: 'readonly',
31
+ URL: 'readonly'
32
32
  }
33
33
  }
34
34
  },
35
35
  {
36
- files: ["**/*.ts"],
36
+ files: ['**/*.ts'],
37
37
  languageOptions: {
38
38
  parser: tsParser,
39
39
  parserOptions: {
40
- project: "./tsconfig.json",
40
+ project: './tsconfig.json',
41
41
  tsconfigRootDir: configDirectory
42
42
  },
43
43
  globals: {
44
- console: "readonly",
45
- process: "readonly",
46
- URL: "readonly"
44
+ console: 'readonly',
45
+ process: 'readonly',
46
+ URL: 'readonly'
47
47
  }
48
48
  },
49
49
  plugins: {
50
- "@typescript-eslint": tseslint,
50
+ '@typescript-eslint': tseslint,
51
51
  architecture: architecturePlugin
52
52
  },
53
53
  rules: {
54
54
  ...tseslint.configs.recommended.rules,
55
- "@typescript-eslint/naming-convention": [
56
- "error",
55
+ '@typescript-eslint/naming-convention': [
56
+ 'error',
57
57
  {
58
- selector: "default",
59
- format: ["camelCase"],
60
- leadingUnderscore: "allow"
58
+ selector: 'default',
59
+ format: ['camelCase'],
60
+ leadingUnderscore: 'allow'
61
61
  },
62
62
  {
63
- selector: "typeLike",
64
- format: ["PascalCase"]
63
+ selector: 'typeLike',
64
+ format: ['PascalCase']
65
65
  },
66
66
  {
67
- selector: "variable",
68
- modifiers: ["const"],
69
- format: ["camelCase", "UPPER_CASE", "PascalCase"]
67
+ selector: 'variable',
68
+ modifiers: ['const'],
69
+ format: ['camelCase', 'UPPER_CASE', 'PascalCase']
70
70
  }
71
71
  ],
72
- "@typescript-eslint/no-explicit-any": "off",
73
- "no-empty-pattern": "off",
74
- "architecture/no-raw-locators-in-tests": "error",
75
- "architecture/no-wait-for-timeout": "error",
76
- "architecture/no-expect-in-page-objects": "error"
72
+ '@typescript-eslint/no-explicit-any': 'off',
73
+ 'no-empty-pattern': 'off',
74
+ 'architecture/no-raw-locators-in-tests': 'error',
75
+ 'architecture/no-wait-for-timeout': 'error',
76
+ 'architecture/no-expect-in-page-objects': 'error'
77
77
  }
78
78
  }
79
79
  ];
@@ -1,12 +1,15 @@
1
1
  // Shared Playwright fixtures so tests can stay small and focus on behavior.
2
- import { test as base } from "@playwright/test";
2
+ import { test as base } from '@playwright/test';
3
3
 
4
- import { loadRuntimeConfig, type RuntimeConfig } 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, type Logger } from "../utils/logger";
9
- import { StepLogger } from "../utils/test-step";
4
+ import {
5
+ loadRuntimeConfig,
6
+ type RuntimeConfig
7
+ } from '../config/runtime-config';
8
+ import { DataFactory } from '../data/factories/data-factory';
9
+ import { LoginPage } from '../pages/login-page';
10
+ import { PeoplePage } from '../pages/people-page';
11
+ import { createLogger, type Logger } from '../utils/logger';
12
+ import { StepLogger } from '../utils/test-step';
10
13
 
11
14
  type FrameworkFixtures = {
12
15
  appConfig: RuntimeConfig;
@@ -23,22 +26,34 @@ export const test = base.extend<FrameworkFixtures>({
23
26
  },
24
27
  logger: async ({}, use, testInfo) => {
25
28
  const logger = createLogger({
26
- test: testInfo.titlePath.join(" > ")
29
+ test: testInfo.titlePath.join(' > ')
27
30
  });
28
31
  await use(logger);
29
32
  },
30
33
  stepLogger: async ({ logger }, use) => {
31
- await use(new StepLogger(logger.child({ scope: "steps" })));
34
+ await use(new StepLogger(logger.child({ scope: 'steps' })));
32
35
  },
33
36
  dataFactory: async ({ appConfig }, use) => {
34
37
  await use(new DataFactory(appConfig.testRunId));
35
38
  },
36
39
  loginPage: async ({ page, appConfig, logger }, use) => {
37
- await use(new LoginPage(page, appConfig.uiBaseUrl, logger.child({ pageObject: "LoginPage" })));
40
+ await use(
41
+ new LoginPage(
42
+ page,
43
+ appConfig.uiBaseUrl,
44
+ logger.child({ pageObject: 'LoginPage' })
45
+ )
46
+ );
38
47
  },
39
48
  peoplePage: async ({ page, appConfig, logger }, use) => {
40
- await use(new PeoplePage(page, appConfig.uiBaseUrl, logger.child({ pageObject: "PeoplePage" })));
49
+ await use(
50
+ new PeoplePage(
51
+ page,
52
+ appConfig.uiBaseUrl,
53
+ logger.child({ pageObject: 'PeoplePage' })
54
+ )
55
+ );
41
56
  }
42
57
  });
43
58
 
44
- export { expect } from "@playwright/test";
59
+ export { expect } from '@playwright/test';
@@ -2,41 +2,41 @@ const TEST_FILE_PATTERN = /[/\\]tests[/\\].+\.ts$/;
2
2
  const PAGE_OBJECT_PATTERN = /[/\\](pages|components)[/\\].+\.ts$/;
3
3
 
4
4
  const locatorMethods = new Set([
5
- "locator",
6
- "getByRole",
7
- "getByLabel",
8
- "getByTestId",
9
- "getByText",
10
- "getByPlaceholder",
11
- "getByAltText",
12
- "getByTitle",
13
- "$",
14
- "$$"
5
+ 'locator',
6
+ 'getByRole',
7
+ 'getByLabel',
8
+ 'getByTestId',
9
+ 'getByText',
10
+ 'getByPlaceholder',
11
+ 'getByAltText',
12
+ 'getByTitle',
13
+ '$',
14
+ '$$'
15
15
  ]);
16
16
 
17
17
  function isIdentifierProperty(node, name) {
18
18
  return (
19
19
  node &&
20
- node.type === "MemberExpression" &&
20
+ node.type === 'MemberExpression' &&
21
21
  !node.computed &&
22
22
  node.property &&
23
- node.property.type === "Identifier" &&
23
+ node.property.type === 'Identifier' &&
24
24
  node.property.name === name
25
25
  );
26
26
  }
27
27
 
28
28
  module.exports = {
29
29
  rules: {
30
- "no-raw-locators-in-tests": {
30
+ 'no-raw-locators-in-tests': {
31
31
  meta: {
32
- type: "problem",
32
+ type: 'problem',
33
33
  docs: {
34
- description: "Disallow selectors inside test files"
34
+ description: 'Disallow selectors inside test files'
35
35
  },
36
36
  schema: [],
37
37
  messages: {
38
38
  noRawLocators:
39
- "Raw locators are not allowed in tests. Move selector logic into a page object or component."
39
+ 'Raw locators are not allowed in tests. Move selector logic into a page object or component.'
40
40
  }
41
41
  },
42
42
  create(context) {
@@ -47,54 +47,56 @@ module.exports = {
47
47
  return {
48
48
  CallExpression(node) {
49
49
  if (
50
- node.callee.type === "MemberExpression" &&
50
+ node.callee.type === 'MemberExpression' &&
51
51
  !node.callee.computed &&
52
- node.callee.property.type === "Identifier" &&
52
+ node.callee.property.type === 'Identifier' &&
53
53
  locatorMethods.has(node.callee.property.name)
54
54
  ) {
55
55
  context.report({
56
56
  node,
57
- messageId: "noRawLocators"
57
+ messageId: 'noRawLocators'
58
58
  });
59
59
  }
60
60
  }
61
61
  };
62
62
  }
63
63
  },
64
- "no-wait-for-timeout": {
64
+ 'no-wait-for-timeout': {
65
65
  meta: {
66
- type: "problem",
66
+ type: 'problem',
67
67
  docs: {
68
- description: "Disallow waitForTimeout"
68
+ description: 'Disallow waitForTimeout'
69
69
  },
70
70
  schema: [],
71
71
  messages: {
72
- noWaitForTimeout: "waitForTimeout is not allowed. Synchronize with user-visible events instead."
72
+ noWaitForTimeout:
73
+ 'waitForTimeout is not allowed. Synchronize with user-visible events instead.'
73
74
  }
74
75
  },
75
76
  create(context) {
76
77
  return {
77
78
  CallExpression(node) {
78
- if (isIdentifierProperty(node.callee, "waitForTimeout")) {
79
+ if (isIdentifierProperty(node.callee, 'waitForTimeout')) {
79
80
  context.report({
80
81
  node,
81
- messageId: "noWaitForTimeout"
82
+ messageId: 'noWaitForTimeout'
82
83
  });
83
84
  }
84
85
  }
85
86
  };
86
87
  }
87
88
  },
88
- "no-expect-in-page-objects": {
89
+ 'no-expect-in-page-objects': {
89
90
  meta: {
90
- type: "problem",
91
+ type: 'problem',
91
92
  docs: {
92
- description: "Disallow assertions inside page objects and UI components"
93
+ description:
94
+ 'Disallow assertions inside page objects and UI components'
93
95
  },
94
96
  schema: [],
95
97
  messages: {
96
98
  noExpect:
97
- "Assertions are not allowed inside page objects or components. Return state and assert from the test."
99
+ 'Assertions are not allowed inside page objects or components. Return state and assert from the test.'
98
100
  }
99
101
  },
100
102
  create(context) {
@@ -104,10 +106,13 @@ module.exports = {
104
106
 
105
107
  return {
106
108
  CallExpression(node) {
107
- if (node.callee.type === "Identifier" && node.callee.name === "expect") {
109
+ if (
110
+ node.callee.type === 'Identifier' &&
111
+ node.callee.name === 'expect'
112
+ ) {
108
113
  context.report({
109
114
  node,
110
- messageId: "noExpect"
115
+ messageId: 'noExpect'
111
116
  });
112
117
  }
113
118
  }
@@ -4,12 +4,13 @@
4
4
  "private": true,
5
5
  "description": "Production-ready Playwright automation framework template with deterministic test patterns.",
6
6
  "scripts": {
7
- "test": "playwright test",
8
- "test:smoke": "playwright test --grep @smoke",
9
- "test:regression": "playwright test --grep @regression",
10
- "test:critical": "playwright test --grep @critical",
11
- "demo:ui": "node ./demo-apps/ui-demo-app/src/server.js",
12
- "demo:api": "node ./demo-apps/api-demo-server/src/server.js",
7
+ "ensure:env": "node ./scripts/ensure-local-env.mjs",
8
+ "test": "node ./scripts/ensure-local-env.mjs && playwright test",
9
+ "test:smoke": "node ./scripts/ensure-local-env.mjs && playwright test --grep @smoke",
10
+ "test:regression": "node ./scripts/ensure-local-env.mjs && playwright test --grep @regression",
11
+ "test:critical": "node ./scripts/ensure-local-env.mjs && playwright test --grep @critical",
12
+ "demo:ui": "node ./scripts/ensure-local-env.mjs && node ./demo-apps/ui-demo-app/src/server.js",
13
+ "demo:api": "node ./scripts/ensure-local-env.mjs && node ./demo-apps/api-demo-server/src/server.js",
13
14
  "lint": "eslint .",
14
15
  "typecheck": "tsc --noEmit",
15
16
  "report:playwright": "playwright show-report reports/html",
@@ -1,8 +1,8 @@
1
1
  // Base page with shared helpers that concrete page objects can build on.
2
- import type { Page } from "@playwright/test";
2
+ import type { Page } from '@playwright/test';
3
3
 
4
- import { FlashMessage } from "../components/flash-message";
5
- import type { Logger } from "../utils/logger";
4
+ import { FlashMessage } from '../components/flash-message';
5
+ import type { Logger } from '../utils/logger';
6
6
 
7
7
  export abstract class BasePage {
8
8
  readonly flashMessage: FlashMessage;
@@ -20,6 +20,6 @@ export abstract class BasePage {
20
20
  }
21
21
 
22
22
  async getWelcomeMessage(): Promise<string> {
23
- return (await this.page.getByTestId("welcome-message").textContent()) ?? "";
23
+ return (await this.page.getByTestId('welcome-message').textContent()) ?? '';
24
24
  }
25
25
  }