@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
@@ -1,23 +1,23 @@
1
1
  // Central Cypress configuration for specs, retries, artifacts, and runtime env values.
2
- import * as os from "node:os";
2
+ import * as os from 'node:os';
3
3
 
4
- import { allureCypress } from "allure-cypress/reporter";
5
- import { defineConfig } from "cypress";
4
+ import { allureCypress } from 'allure-cypress/reporter';
5
+ import { defineConfig } from 'cypress';
6
6
 
7
- import { loadRuntimeConfig } from "./config/runtime-config";
7
+ import { loadRuntimeConfig } from './config/runtime-config';
8
8
 
9
9
  const runtimeConfig = loadRuntimeConfig();
10
10
 
11
11
  export default defineConfig({
12
12
  e2e: {
13
13
  baseUrl: runtimeConfig.uiBaseUrl,
14
- specPattern: "cypress/e2e/**/*.cy.ts",
15
- supportFile: "cypress/support/e2e.ts",
14
+ specPattern: 'cypress/e2e/**/*.cy.ts',
15
+ supportFile: 'cypress/support/e2e.ts',
16
16
  setupNodeEvents(on, config) {
17
17
  // Keep Cypress terminal output as the default path most users expect.
18
18
  // Remove the Allure line below if you prefer to stay with Cypress output only.
19
19
  allureCypress(on, config, {
20
- resultsDir: "allure-results",
20
+ resultsDir: 'allure-results',
21
21
  environmentInfo: {
22
22
  os_platform: os.platform(),
23
23
  os_release: os.release(),
@@ -43,6 +43,6 @@ export default defineConfig({
43
43
  },
44
44
  screenshotOnRunFailure: true,
45
45
  video: true,
46
- videosFolder: "reports/videos",
47
- screenshotsFolder: "reports/screenshots"
46
+ videosFolder: 'reports/videos',
47
+ screenshotsFolder: 'reports/screenshots'
48
48
  });
@@ -1,6 +1,6 @@
1
1
  body {
2
2
  margin: 0;
3
- font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
3
+ font-family: 'IBM Plex Sans', 'Segoe UI', sans-serif;
4
4
  background: linear-gradient(180deg, #f5f7fb 0%, #eef2f7 100%);
5
5
  color: #102038;
6
6
  }
@@ -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,37 +1,43 @@
1
- import js from "@eslint/js";
2
- import tsParser from "@typescript-eslint/parser";
3
- import tsPlugin from "@typescript-eslint/eslint-plugin";
1
+ import js from '@eslint/js';
2
+ import tsParser from '@typescript-eslint/parser';
3
+ import tsPlugin from '@typescript-eslint/eslint-plugin';
4
4
 
5
5
  const tsconfigRootDir = import.meta.dirname;
6
6
 
7
7
  const cypressGlobals = {
8
- Cypress: "readonly",
9
- cy: "readonly",
10
- describe: "readonly",
11
- it: "readonly",
12
- before: "readonly",
13
- beforeEach: "readonly",
14
- after: "readonly",
15
- afterEach: "readonly"
8
+ Cypress: 'readonly',
9
+ cy: 'readonly',
10
+ describe: 'readonly',
11
+ it: 'readonly',
12
+ before: 'readonly',
13
+ beforeEach: 'readonly',
14
+ after: 'readonly',
15
+ afterEach: 'readonly'
16
16
  };
17
17
 
18
18
  const nodeGlobals = {
19
- process: "readonly",
20
- console: "readonly"
19
+ process: 'readonly',
20
+ console: 'readonly'
21
21
  };
22
22
 
23
23
  export default [
24
24
  {
25
- ignores: ["demo-apps/**", "node_modules/**", "reports/**", "allure-results/**", "allure-report/**"]
25
+ ignores: [
26
+ 'demo-apps/**',
27
+ 'node_modules/**',
28
+ 'reports/**',
29
+ 'allure-results/**',
30
+ 'allure-report/**'
31
+ ]
26
32
  },
27
33
  js.configs.recommended,
28
34
  {
29
- files: ["**/*.ts"],
30
- ignores: ["**/*.d.ts"],
35
+ files: ['**/*.ts'],
36
+ ignores: ['**/*.d.ts'],
31
37
  languageOptions: {
32
38
  parser: tsParser,
33
39
  parserOptions: {
34
- project: "./tsconfig.json",
40
+ project: './tsconfig.json',
35
41
  tsconfigRootDir
36
42
  },
37
43
  globals: {
@@ -40,22 +46,22 @@ export default [
40
46
  }
41
47
  },
42
48
  plugins: {
43
- "@typescript-eslint": tsPlugin
49
+ '@typescript-eslint': tsPlugin
44
50
  },
45
51
  rules: {
46
52
  ...tsPlugin.configs.recommended.rules,
47
- "no-undef": "off",
48
- "@typescript-eslint/no-unused-vars": [
49
- "error",
53
+ 'no-undef': 'off',
54
+ '@typescript-eslint/no-unused-vars': [
55
+ 'error',
50
56
  {
51
- argsIgnorePattern: "^_",
52
- varsIgnorePattern: "^_"
57
+ argsIgnorePattern: '^_',
58
+ varsIgnorePattern: '^_'
53
59
  }
54
60
  ]
55
61
  }
56
62
  },
57
63
  {
58
- files: ["**/*.d.ts"],
64
+ files: ['**/*.d.ts'],
59
65
  languageOptions: {
60
66
  parser: tsParser,
61
67
  parserOptions: {
@@ -67,53 +73,55 @@ export default [
67
73
  }
68
74
  },
69
75
  rules: {
70
- "no-undef": "off",
71
- "no-unused-vars": "off",
72
- "@typescript-eslint/no-unused-vars": "off"
76
+ 'no-undef': 'off',
77
+ 'no-unused-vars': 'off',
78
+ '@typescript-eslint/no-unused-vars': 'off'
73
79
  }
74
80
  },
75
81
  {
76
- files: ["scripts/**/*.mjs"],
82
+ files: ['scripts/**/*.mjs'],
77
83
  languageOptions: {
78
84
  globals: {
79
85
  ...nodeGlobals,
80
- fetch: "readonly",
81
- setTimeout: "readonly"
86
+ fetch: 'readonly',
87
+ setTimeout: 'readonly'
82
88
  }
83
89
  }
84
90
  },
85
91
  {
86
- files: ["cypress/e2e/**/*.ts"],
92
+ files: ['cypress/e2e/**/*.ts'],
87
93
  rules: {
88
- "no-restricted-properties": [
89
- "error",
94
+ 'no-restricted-properties': [
95
+ 'error',
90
96
  {
91
- object: "cy",
92
- property: "get",
93
- message: "Keep selectors in support/page modules or custom commands, not in spec files."
97
+ object: 'cy',
98
+ property: 'get',
99
+ message:
100
+ 'Keep selectors in support/page modules or custom commands, not in spec files.'
94
101
  },
95
102
  {
96
- object: "cy",
97
- property: "contains",
98
- message: "Keep selectors in support/page modules or custom commands, not in spec files."
103
+ object: 'cy',
104
+ property: 'contains',
105
+ message:
106
+ 'Keep selectors in support/page modules or custom commands, not in spec files.'
99
107
  }
100
108
  ]
101
109
  }
102
110
  },
103
111
  {
104
- files: ["cypress/support/commands.ts"],
112
+ files: ['cypress/support/commands.ts'],
105
113
  rules: {
106
- "@typescript-eslint/no-namespace": "off"
114
+ '@typescript-eslint/no-namespace': 'off'
107
115
  }
108
116
  },
109
117
  {
110
- files: ["cypress/support/pages/**/*.ts"],
118
+ files: ['cypress/support/pages/**/*.ts'],
111
119
  rules: {
112
- "no-restricted-syntax": [
113
- "error",
120
+ 'no-restricted-syntax': [
121
+ 'error',
114
122
  {
115
123
  selector: "CallExpression[callee.name='expect']",
116
- message: "Keep assertions in Cypress spec files."
124
+ message: 'Keep assertions in Cypress spec files.'
117
125
  }
118
126
  ]
119
127
  }
@@ -4,11 +4,12 @@
4
4
  "private": true,
5
5
  "description": "Production-ready Cypress automation framework template with a deterministic UI starter app.",
6
6
  "scripts": {
7
- "test": "node ./scripts/run-cypress.mjs run",
8
- "open": "node ./scripts/run-cypress.mjs open",
9
- "cy:run": "cypress run",
10
- "cy:open": "cypress open",
11
- "demo:ui": "node ./demo-apps/ui-demo-app/src/server.js",
7
+ "ensure:env": "node ./scripts/ensure-local-env.mjs",
8
+ "test": "node ./scripts/ensure-local-env.mjs && node ./scripts/run-cypress.mjs run",
9
+ "open": "node ./scripts/ensure-local-env.mjs && node ./scripts/run-cypress.mjs open",
10
+ "cy:run": "node ./scripts/ensure-local-env.mjs && cypress run",
11
+ "cy:open": "node ./scripts/ensure-local-env.mjs && cypress open",
12
+ "demo:ui": "node ./scripts/ensure-local-env.mjs && node ./demo-apps/ui-demo-app/src/server.js",
12
13
  "lint": "eslint .",
13
14
  "typecheck": "tsc --noEmit",
14
15
  "report:allure": "node ./scripts/generate-allure-report.mjs",
@@ -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
+ );
@@ -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) => {