@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.
- package/README.md +23 -0
- package/index.js +282 -738
- package/lib/args.js +139 -0
- package/lib/constants.js +115 -0
- package/lib/interactive.js +131 -0
- package/lib/local-env.js +65 -0
- package/lib/metadata.js +329 -0
- package/lib/output.js +326 -0
- package/lib/prereqs.js +72 -0
- package/lib/scaffold.js +120 -0
- package/lib/templates.js +40 -0
- package/package.json +5 -3
- package/templates/cypress-template/.env.example +2 -2
- package/templates/cypress-template/.github/workflows/cypress-tests.yml +2 -2
- package/templates/cypress-template/README.md +29 -6
- package/templates/cypress-template/allurerc.mjs +1 -1
- package/templates/cypress-template/config/environments.ts +13 -11
- package/templates/cypress-template/config/runtime-config.ts +17 -12
- package/templates/cypress-template/config/secret-manager.ts +1 -1
- package/templates/cypress-template/config/test-env.ts +3 -3
- package/templates/cypress-template/cypress/e2e/ui-journey.cy.ts +12 -10
- package/templates/cypress-template/cypress/support/app-config.ts +5 -5
- package/templates/cypress-template/cypress/support/commands.ts +7 -7
- package/templates/cypress-template/cypress/support/data/data-factory.ts +6 -4
- package/templates/cypress-template/cypress/support/data/id-generator.ts +1 -1
- package/templates/cypress-template/cypress/support/data/seeded-faker.ts +2 -2
- package/templates/cypress-template/cypress/support/e2e.ts +2 -2
- package/templates/cypress-template/cypress/support/pages/login-page.ts +4 -4
- package/templates/cypress-template/cypress/support/pages/people-page.ts +10 -10
- package/templates/cypress-template/cypress.config.ts +9 -9
- package/templates/cypress-template/demo-apps/ui-demo-app/public/styles.css +1 -1
- package/templates/cypress-template/demo-apps/ui-demo-app/src/server.js +44 -41
- package/templates/cypress-template/demo-apps/ui-demo-app/src/store.js +31 -3
- package/templates/cypress-template/demo-apps/ui-demo-app/src/templates.js +5 -5
- package/templates/cypress-template/eslint.config.mjs +53 -45
- package/templates/cypress-template/package.json +6 -5
- package/templates/cypress-template/scripts/ensure-local-env.mjs +36 -0
- package/templates/cypress-template/scripts/generate-allure-report.mjs +16 -10
- package/templates/cypress-template/scripts/run-cypress.mjs +33 -24
- package/templates/cypress-template/scripts/run-tests.sh +1 -0
- package/templates/cypress-template/tsconfig.json +7 -1
- package/templates/playwright-template/.env.example +6 -6
- package/templates/playwright-template/.github/workflows/playwright-tests.yml +14 -5
- package/templates/playwright-template/README.md +25 -5
- package/templates/playwright-template/allurerc.mjs +1 -1
- package/templates/playwright-template/components/flash-message.ts +2 -2
- package/templates/playwright-template/config/environments.ts +16 -14
- package/templates/playwright-template/config/runtime-config.ts +17 -12
- package/templates/playwright-template/config/secret-manager.ts +1 -1
- package/templates/playwright-template/config/test-env.ts +3 -3
- package/templates/playwright-template/data/factories/data-factory.ts +6 -4
- package/templates/playwright-template/data/generators/id-generator.ts +1 -1
- package/templates/playwright-template/data/generators/seeded-faker.ts +2 -2
- package/templates/playwright-template/demo-apps/api-demo-server/src/server.js +9 -9
- package/templates/playwright-template/demo-apps/api-demo-server/src/store.js +1 -1
- package/templates/playwright-template/demo-apps/ui-demo-app/public/styles.css +1 -1
- package/templates/playwright-template/demo-apps/ui-demo-app/src/server.js +44 -41
- package/templates/playwright-template/demo-apps/ui-demo-app/src/store.js +31 -3
- package/templates/playwright-template/demo-apps/ui-demo-app/src/templates.js +5 -5
- package/templates/playwright-template/eslint.config.mjs +40 -40
- package/templates/playwright-template/fixtures/test-fixtures.ts +27 -12
- package/templates/playwright-template/lint/architecture-plugin.cjs +36 -31
- package/templates/playwright-template/package.json +7 -6
- package/templates/playwright-template/pages/base-page.ts +4 -4
- package/templates/playwright-template/pages/login-page.ts +9 -9
- package/templates/playwright-template/pages/people-page.ts +21 -17
- package/templates/playwright-template/playwright.config.ts +22 -19
- package/templates/playwright-template/reporters/structured-reporter.ts +11 -8
- package/templates/playwright-template/scripts/ensure-local-env.mjs +37 -0
- package/templates/playwright-template/scripts/generate-allure-report.mjs +16 -10
- package/templates/playwright-template/scripts/run-tests.sh +1 -0
- package/templates/playwright-template/tests/api-people.spec.ts +8 -6
- package/templates/playwright-template/tests/ui-journey.spec.ts +13 -8
- package/templates/playwright-template/tsconfig.json +3 -11
- package/templates/playwright-template/utils/logger.ts +12 -8
- package/templates/playwright-template/utils/test-step.ts +5 -5
- package/templates/wdio-template/.env.example +14 -0
- package/templates/wdio-template/.github/workflows/wdio-tests.yml +46 -0
- package/templates/wdio-template/README.md +241 -0
- package/templates/wdio-template/allurerc.mjs +10 -0
- package/templates/wdio-template/components/README.md +5 -0
- package/templates/wdio-template/components/flash-message.ts +16 -0
- package/templates/wdio-template/config/README.md +5 -0
- package/templates/wdio-template/config/environments.ts +40 -0
- package/templates/wdio-template/config/runtime-config.ts +53 -0
- package/templates/wdio-template/config/secret-manager.ts +29 -0
- package/templates/wdio-template/config/test-env.ts +9 -0
- package/templates/wdio-template/data/README.md +9 -0
- package/templates/wdio-template/data/factories/README.md +6 -0
- package/templates/wdio-template/data/factories/data-factory.ts +36 -0
- package/templates/wdio-template/data/generators/README.md +5 -0
- package/templates/wdio-template/data/generators/id-generator.ts +18 -0
- package/templates/wdio-template/data/generators/seeded-faker.ts +14 -0
- package/templates/wdio-template/demo-apps/ui-demo-app/public/styles.css +120 -0
- package/templates/wdio-template/demo-apps/ui-demo-app/src/server.js +152 -0
- package/templates/wdio-template/demo-apps/ui-demo-app/src/store.js +71 -0
- package/templates/wdio-template/demo-apps/ui-demo-app/src/templates.js +121 -0
- package/templates/wdio-template/eslint.config.mjs +86 -0
- package/templates/wdio-template/lint/architecture-plugin.cjs +123 -0
- package/templates/wdio-template/package-lock.json +11058 -0
- package/templates/wdio-template/package.json +44 -0
- package/templates/wdio-template/pages/README.md +6 -0
- package/templates/wdio-template/pages/base-page.ts +15 -0
- package/templates/wdio-template/pages/login-page.ts +27 -0
- package/templates/wdio-template/pages/people-page.ts +54 -0
- package/templates/wdio-template/reporters/README.md +5 -0
- package/templates/wdio-template/reporters/structured-reporter.ts +78 -0
- package/templates/wdio-template/scripts/README.md +5 -0
- package/templates/wdio-template/scripts/ensure-local-env.mjs +36 -0
- package/templates/wdio-template/scripts/generate-allure-report.mjs +72 -0
- package/templates/wdio-template/scripts/run-tests.sh +7 -0
- package/templates/wdio-template/scripts/run-wdio.mjs +114 -0
- package/templates/wdio-template/tests/README.md +7 -0
- package/templates/wdio-template/tests/ui-journey.spec.ts +52 -0
- package/templates/wdio-template/tsconfig.json +22 -0
- package/templates/wdio-template/utils/README.md +5 -0
- package/templates/wdio-template/utils/logger.ts +60 -0
- package/templates/wdio-template/utils/test-step.ts +20 -0
- package/templates/wdio-template/wdio.conf.ts +58 -0
- package/tests/args.test.js +58 -0
- package/tests/local-env.test.js +70 -0
- package/tests/metadata.test.js +147 -0
- package/tests/templates.test.js +44 -0
package/lib/scaffold.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const { renderTemplateFile } = require('./metadata');
|
|
4
|
+
|
|
5
|
+
function sleep(ms) {
|
|
6
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function ensureScaffoldTarget(targetDirectory) {
|
|
10
|
+
if (!fs.existsSync(targetDirectory)) {
|
|
11
|
+
fs.mkdirSync(targetDirectory, { recursive: true });
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const entries = fs
|
|
16
|
+
.readdirSync(targetDirectory)
|
|
17
|
+
.filter((entry) => !['.git', '.DS_Store'].includes(entry));
|
|
18
|
+
|
|
19
|
+
if (entries.length > 0) {
|
|
20
|
+
throw new Error(`Target directory is not empty: ${targetDirectory}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function customizeProject(targetDirectory, template, options) {
|
|
25
|
+
const packageJsonPath = path.join(targetDirectory, 'package.json');
|
|
26
|
+
const packageLockPath = path.join(targetDirectory, 'package-lock.json');
|
|
27
|
+
const gitignorePath = path.join(targetDirectory, '.gitignore');
|
|
28
|
+
|
|
29
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
30
|
+
fs.writeFileSync(
|
|
31
|
+
packageJsonPath,
|
|
32
|
+
renderTemplateFile(template, 'package.json', targetDirectory, options),
|
|
33
|
+
'utf8'
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (fs.existsSync(packageLockPath)) {
|
|
38
|
+
fs.writeFileSync(
|
|
39
|
+
packageLockPath,
|
|
40
|
+
renderTemplateFile(
|
|
41
|
+
template,
|
|
42
|
+
'package-lock.json',
|
|
43
|
+
targetDirectory,
|
|
44
|
+
options
|
|
45
|
+
),
|
|
46
|
+
'utf8'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
51
|
+
fs.writeFileSync(gitignorePath, options.defaultGitignore, 'utf8');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function scaffoldProject(
|
|
56
|
+
template,
|
|
57
|
+
targetDirectory,
|
|
58
|
+
prerequisites,
|
|
59
|
+
options
|
|
60
|
+
) {
|
|
61
|
+
const {
|
|
62
|
+
createLocalCredentials,
|
|
63
|
+
defaultGitignore,
|
|
64
|
+
getTemplateDirectory,
|
|
65
|
+
initializeGitRepository,
|
|
66
|
+
renderProgress,
|
|
67
|
+
toPackageName,
|
|
68
|
+
writeGeneratedLocalEnv
|
|
69
|
+
} = options;
|
|
70
|
+
const templateDirectory = getTemplateDirectory(template.id);
|
|
71
|
+
|
|
72
|
+
if (!fs.existsSync(templateDirectory)) {
|
|
73
|
+
throw new Error(`Template files are missing for "${template.id}".`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const steps = [
|
|
77
|
+
'Validating target directory',
|
|
78
|
+
'Copying template files',
|
|
79
|
+
'Customizing project files',
|
|
80
|
+
'Finalizing scaffold'
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
renderProgress(0, steps.length, 'Preparing scaffold');
|
|
84
|
+
ensureScaffoldTarget(targetDirectory);
|
|
85
|
+
await sleep(60);
|
|
86
|
+
|
|
87
|
+
renderProgress(1, steps.length, steps[0]);
|
|
88
|
+
await sleep(80);
|
|
89
|
+
|
|
90
|
+
fs.cpSync(templateDirectory, targetDirectory, { recursive: true });
|
|
91
|
+
renderProgress(2, steps.length, steps[1]);
|
|
92
|
+
await sleep(80);
|
|
93
|
+
|
|
94
|
+
customizeProject(targetDirectory, template, {
|
|
95
|
+
defaultGitignore,
|
|
96
|
+
getTemplateDirectory,
|
|
97
|
+
toPackageName
|
|
98
|
+
});
|
|
99
|
+
renderProgress(3, steps.length, steps[2]);
|
|
100
|
+
await sleep(80);
|
|
101
|
+
|
|
102
|
+
if (prerequisites.git) {
|
|
103
|
+
initializeGitRepository(targetDirectory);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const localEnv = writeGeneratedLocalEnv(
|
|
107
|
+
targetDirectory,
|
|
108
|
+
template.id,
|
|
109
|
+
createLocalCredentials(targetDirectory)
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
renderProgress(4, steps.length, steps[3]);
|
|
113
|
+
await sleep(60);
|
|
114
|
+
process.stdout.write('\n');
|
|
115
|
+
return localEnv;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = {
|
|
119
|
+
scaffoldProject
|
|
120
|
+
};
|
package/lib/templates.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
|
|
3
|
+
function createTemplateAliases(templates) {
|
|
4
|
+
return new Map(
|
|
5
|
+
templates.flatMap((template) => [
|
|
6
|
+
[template.id, template.id],
|
|
7
|
+
...template.aliases.map((alias) => [alias, template.id])
|
|
8
|
+
])
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function resolveTemplate(templateAliases, value) {
|
|
13
|
+
return templateAliases.get(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getTemplate(templates, templateId) {
|
|
17
|
+
return templates.find((template) => template.id === templateId);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function toPackageName(targetDirectory, template) {
|
|
21
|
+
const baseName = path.basename(targetDirectory).toLowerCase();
|
|
22
|
+
const normalized = baseName
|
|
23
|
+
.replace(/[^a-z0-9-_]+/g, '-')
|
|
24
|
+
.replace(/^-+|-+$/g, '')
|
|
25
|
+
.replace(/-{2,}/g, '-');
|
|
26
|
+
|
|
27
|
+
return normalized || template.defaultPackageName || 'qa-patterns-template';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getTemplateDirectory(rootDirectory, templateId) {
|
|
31
|
+
return path.resolve(rootDirectory, 'templates', templateId);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = {
|
|
35
|
+
createTemplateAliases,
|
|
36
|
+
getTemplate,
|
|
37
|
+
getTemplateDirectory,
|
|
38
|
+
resolveTemplate,
|
|
39
|
+
toPackageName
|
|
40
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toolstackhq/create-qa-patterns",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.15",
|
|
4
4
|
"description": "CLI for generating QA framework templates.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -16,15 +16,17 @@
|
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
18
|
"index.js",
|
|
19
|
+
"lib",
|
|
19
20
|
"templates",
|
|
20
21
|
"README.md",
|
|
21
|
-
"LICENSE"
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"tests"
|
|
22
24
|
],
|
|
23
25
|
"bin": {
|
|
24
26
|
"create-qa-patterns": "./index.js"
|
|
25
27
|
},
|
|
26
28
|
"scripts": {
|
|
27
29
|
"start": "node ./index.js",
|
|
28
|
-
"test": "node ./
|
|
30
|
+
"test": "node --test ./tests/*.test.js"
|
|
29
31
|
}
|
|
30
32
|
}
|
|
@@ -17,7 +17,7 @@ jobs:
|
|
|
17
17
|
|
|
18
18
|
- uses: actions/setup-node@v4
|
|
19
19
|
with:
|
|
20
|
-
node-version:
|
|
20
|
+
node-version: '20'
|
|
21
21
|
|
|
22
22
|
- name: Install template dependencies
|
|
23
23
|
working-directory: templates/cypress-template
|
|
@@ -35,7 +35,7 @@ jobs:
|
|
|
35
35
|
working-directory: templates/cypress-template
|
|
36
36
|
install: false
|
|
37
37
|
start: npm run demo:ui
|
|
38
|
-
wait-on:
|
|
38
|
+
wait-on: 'http://127.0.0.1:3000/health'
|
|
39
39
|
command: npm run cy:run
|
|
40
40
|
|
|
41
41
|
- name: Upload Cypress artifacts
|
|
@@ -13,6 +13,7 @@ This is a Cypress + TypeScript automation framework template for a small determi
|
|
|
13
13
|
- [Reports and artifacts](#reports-and-artifacts)
|
|
14
14
|
- [Add a new test](#add-a-new-test)
|
|
15
15
|
- [Extend the framework](#extend-the-framework)
|
|
16
|
+
- [Template upgrades](#template-upgrades)
|
|
16
17
|
- [CI](#ci)
|
|
17
18
|
|
|
18
19
|
## Feature set
|
|
@@ -90,8 +91,7 @@ npm run report:allure
|
|
|
90
91
|
Default local values:
|
|
91
92
|
|
|
92
93
|
- UI base URL: `http://127.0.0.1:3000`
|
|
93
|
-
-
|
|
94
|
-
- password: `Password123!`
|
|
94
|
+
- credentials: generated into local `.env` on first run
|
|
95
95
|
|
|
96
96
|
## Environment and secrets
|
|
97
97
|
|
|
@@ -111,7 +111,7 @@ Credentials resolve the same way:
|
|
|
111
111
|
|
|
112
112
|
1. `DEV_APP_USERNAME` or `DEV_APP_PASSWORD`
|
|
113
113
|
2. `APP_USERNAME` or `APP_PASSWORD`
|
|
114
|
-
3. built-in defaults for the selected environment
|
|
114
|
+
3. built-in empty defaults for the selected environment
|
|
115
115
|
|
|
116
116
|
For local overrides, copy:
|
|
117
117
|
|
|
@@ -125,6 +125,8 @@ to:
|
|
|
125
125
|
.env
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
+
On the first local run, the template also creates a `.env` file with random demo credentials if one does not already exist.
|
|
129
|
+
|
|
128
130
|
If you want to disable the bundled local demo app even in `dev`, use:
|
|
129
131
|
|
|
130
132
|
```bash
|
|
@@ -171,11 +173,14 @@ Keep the pattern simple:
|
|
|
171
173
|
Example shape:
|
|
172
174
|
|
|
173
175
|
```ts
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
import { loadAppConfig } from '../support/app-config';
|
|
177
|
+
|
|
178
|
+
it('does something', () => {
|
|
179
|
+
const appConfig = loadAppConfig();
|
|
180
|
+
const dataFactory = new DataFactory('local');
|
|
176
181
|
const person = dataFactory.person();
|
|
177
182
|
|
|
178
|
-
cy.signIn(
|
|
183
|
+
cy.signIn(appConfig.credentials.username, appConfig.credentials.password);
|
|
179
184
|
cy.addPerson(person);
|
|
180
185
|
});
|
|
181
186
|
```
|
|
@@ -197,6 +202,24 @@ Recommended rules:
|
|
|
197
202
|
- use Cypress commands for workflows, not giant helper classes
|
|
198
203
|
- keep the data layer generic until the project really needs domain-specific factories
|
|
199
204
|
|
|
205
|
+
## Template upgrades
|
|
206
|
+
|
|
207
|
+
This project includes a `.qa-patterns.json` metadata file so future CLI versions can compare the current project against the managed template baseline.
|
|
208
|
+
|
|
209
|
+
Check for available safe updates:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
npx -y @toolstackhq/create-qa-patterns upgrade check .
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Apply only safe managed-file updates:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
npx -y @toolstackhq/create-qa-patterns upgrade apply --safe .
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The upgrade flow is conservative. It updates framework infrastructure such as config, scripts, workflows, and package metadata when those files are still unchanged from the generated baseline. If you changed a managed file yourself, the CLI reports a conflict instead of overwriting it.
|
|
222
|
+
|
|
200
223
|
## CI
|
|
201
224
|
|
|
202
225
|
The included workflow lives at:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Defines the built-in environment defaults used when env vars are not provided.
|
|
2
|
-
import type { TestEnvironment } from
|
|
2
|
+
import type { TestEnvironment } from './test-env';
|
|
3
3
|
|
|
4
4
|
type EnvironmentDefaults = {
|
|
5
5
|
uiBaseUrl: string;
|
|
@@ -11,28 +11,30 @@ type EnvironmentDefaults = {
|
|
|
11
11
|
|
|
12
12
|
const DEFAULTS: Record<TestEnvironment, EnvironmentDefaults> = {
|
|
13
13
|
dev: {
|
|
14
|
-
uiBaseUrl:
|
|
14
|
+
uiBaseUrl: 'http://127.0.0.1:3000',
|
|
15
15
|
credentials: {
|
|
16
|
-
username:
|
|
17
|
-
password:
|
|
16
|
+
username: '',
|
|
17
|
+
password: ''
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
20
|
staging: {
|
|
21
|
-
uiBaseUrl:
|
|
21
|
+
uiBaseUrl: 'https://staging-ui.example.internal',
|
|
22
22
|
credentials: {
|
|
23
|
-
username:
|
|
24
|
-
password:
|
|
23
|
+
username: '',
|
|
24
|
+
password: ''
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
prod: {
|
|
28
|
-
uiBaseUrl:
|
|
28
|
+
uiBaseUrl: 'https://ui.example.internal',
|
|
29
29
|
credentials: {
|
|
30
|
-
username:
|
|
31
|
-
password:
|
|
30
|
+
username: '',
|
|
31
|
+
password: ''
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
export function getEnvironmentDefaults(
|
|
36
|
+
export function getEnvironmentDefaults(
|
|
37
|
+
testEnv: TestEnvironment
|
|
38
|
+
): EnvironmentDefaults {
|
|
37
39
|
return DEFAULTS[testEnv];
|
|
38
40
|
}
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
// Builds the runtime configuration object that tests and fixtures consume.
|
|
2
|
-
import path from
|
|
2
|
+
import path from 'node:path';
|
|
3
3
|
|
|
4
|
-
import dotenv from
|
|
5
|
-
import { z } from
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
import { z } from 'zod';
|
|
6
6
|
|
|
7
|
-
import { getEnvironmentDefaults } from
|
|
8
|
-
import { EnvSecretProvider, SecretManager } from
|
|
9
|
-
import { loadTestEnvironment } from
|
|
7
|
+
import { getEnvironmentDefaults } from './environments';
|
|
8
|
+
import { EnvSecretProvider, SecretManager } from './secret-manager';
|
|
9
|
+
import { loadTestEnvironment } from './test-env';
|
|
10
10
|
|
|
11
11
|
const environment = loadTestEnvironment();
|
|
12
12
|
|
|
13
|
-
dotenv.config({ path: path.resolve(process.cwd(),
|
|
14
|
-
dotenv.config({
|
|
13
|
+
dotenv.config({ path: path.resolve(process.cwd(), '.env') });
|
|
14
|
+
dotenv.config({
|
|
15
|
+
path: path.resolve(process.cwd(), `.env.${environment}`),
|
|
16
|
+
override: true
|
|
17
|
+
});
|
|
15
18
|
|
|
16
19
|
const runtimeConfigSchema = z.object({
|
|
17
|
-
testEnv: z.enum([
|
|
20
|
+
testEnv: z.enum(['dev', 'staging', 'prod']),
|
|
18
21
|
testRunId: z.string().min(1),
|
|
19
22
|
uiBaseUrl: z.string().url(),
|
|
20
23
|
credentials: z.object({
|
|
@@ -36,13 +39,15 @@ export function loadRuntimeConfig(): RuntimeConfig {
|
|
|
36
39
|
|
|
37
40
|
return runtimeConfigSchema.parse({
|
|
38
41
|
testEnv: environment,
|
|
39
|
-
testRunId: process.env.TEST_RUN_ID ??
|
|
42
|
+
testRunId: process.env.TEST_RUN_ID ?? 'local',
|
|
40
43
|
uiBaseUrl,
|
|
41
44
|
credentials: {
|
|
42
45
|
username:
|
|
43
|
-
secretManager.getOptionalSecret(
|
|
46
|
+
secretManager.getOptionalSecret('APP_USERNAME', environment) ??
|
|
47
|
+
defaults.credentials.username,
|
|
44
48
|
password:
|
|
45
|
-
secretManager.getOptionalSecret(
|
|
49
|
+
secretManager.getOptionalSecret('APP_PASSWORD', environment) ??
|
|
50
|
+
defaults.credentials.password
|
|
46
51
|
}
|
|
47
52
|
});
|
|
48
53
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Minimal secret abstraction so env-based secrets can later be replaced cleanly.
|
|
2
|
-
import type { TestEnvironment } from
|
|
2
|
+
import type { TestEnvironment } from './test-env';
|
|
3
3
|
|
|
4
4
|
export interface SecretProvider {
|
|
5
5
|
getSecret(key: string, testEnv: TestEnvironment): string | undefined;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { z } from
|
|
1
|
+
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
export const testEnvironmentSchema = z.enum([
|
|
3
|
+
export const testEnvironmentSchema = z.enum(['dev', 'staging', 'prod']);
|
|
4
4
|
|
|
5
5
|
export type TestEnvironment = z.infer<typeof testEnvironmentSchema>;
|
|
6
6
|
|
|
7
7
|
export function loadTestEnvironment(): TestEnvironment {
|
|
8
|
-
return testEnvironmentSchema.parse(process.env.TEST_ENV ??
|
|
8
|
+
return testEnvironmentSchema.parse(process.env.TEST_ENV ?? 'dev');
|
|
9
9
|
}
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
// Starter Cypress scenario that demonstrates the preferred spec style for this template.
|
|
2
|
-
import { DataFactory } from
|
|
3
|
-
import { loadAppConfig } from
|
|
4
|
-
import { peoplePage } from
|
|
2
|
+
import { DataFactory } from '../support/data/data-factory';
|
|
3
|
+
import { loadAppConfig } from '../support/app-config';
|
|
4
|
+
import { peoplePage } from '../support/pages/people-page';
|
|
5
5
|
|
|
6
|
-
describe(
|
|
7
|
-
it(
|
|
6
|
+
describe('UI starter journey', () => {
|
|
7
|
+
it('signs in and adds one person', () => {
|
|
8
8
|
const appConfig = loadAppConfig();
|
|
9
9
|
const dataFactory = new DataFactory(appConfig.testRunId);
|
|
10
10
|
const person = dataFactory.person();
|
|
11
11
|
|
|
12
12
|
cy.signIn(appConfig.credentials.username, appConfig.credentials.password);
|
|
13
|
-
peoplePage
|
|
13
|
+
peoplePage
|
|
14
|
+
.welcomeMessage()
|
|
15
|
+
.should('contain', appConfig.credentials.username);
|
|
14
16
|
|
|
15
17
|
cy.addPerson(person);
|
|
16
|
-
peoplePage.flashMessage().should(
|
|
17
|
-
peoplePage.nameCell(person.personId).should(
|
|
18
|
-
peoplePage.roleCell(person.personId).should(
|
|
19
|
-
peoplePage.emailCell(person.personId).should(
|
|
18
|
+
peoplePage.flashMessage().should('contain', 'Person added');
|
|
19
|
+
peoplePage.nameCell(person.personId).should('have.text', person.name);
|
|
20
|
+
peoplePage.roleCell(person.personId).should('have.text', person.role);
|
|
21
|
+
peoplePage.emailCell(person.personId).should('have.text', person.email);
|
|
20
22
|
});
|
|
21
23
|
});
|
|
@@ -11,14 +11,14 @@ export type AppConfig = {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
export function loadAppConfig(): AppConfig {
|
|
14
|
-
const credentials = Cypress.env(
|
|
14
|
+
const credentials = Cypress.env('credentials') as Credentials | undefined;
|
|
15
15
|
|
|
16
16
|
return {
|
|
17
|
-
testEnv: String(Cypress.env(
|
|
18
|
-
testRunId: String(Cypress.env(
|
|
17
|
+
testEnv: String(Cypress.env('testEnv') ?? 'dev'),
|
|
18
|
+
testRunId: String(Cypress.env('testRunId') ?? 'local'),
|
|
19
19
|
credentials: {
|
|
20
|
-
username: credentials?.username ??
|
|
21
|
-
password: credentials?.password ??
|
|
20
|
+
username: credentials?.username ?? '',
|
|
21
|
+
password: credentials?.password ?? ''
|
|
22
22
|
}
|
|
23
23
|
};
|
|
24
24
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Registers the custom Cypress commands that the starter specs rely on.
|
|
2
|
-
import type { PersonRecord } from
|
|
3
|
-
import { loginPage } from
|
|
4
|
-
import { peoplePage } from
|
|
2
|
+
import type { PersonRecord } from './data/data-factory';
|
|
3
|
+
import { loginPage } from './pages/login-page';
|
|
4
|
+
import { peoplePage } from './pages/people-page';
|
|
5
5
|
|
|
6
6
|
declare global {
|
|
7
7
|
namespace Cypress {
|
|
@@ -13,17 +13,17 @@ declare global {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
Cypress.Commands.add(
|
|
16
|
+
Cypress.Commands.add('getByTestId', (testId: string) => {
|
|
17
17
|
return cy.get(`[data-testid='${testId}']`);
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
Cypress.Commands.add(
|
|
20
|
+
Cypress.Commands.add('signIn', (username: string, password: string) => {
|
|
21
21
|
loginPage.visit();
|
|
22
22
|
loginPage.login(username, password);
|
|
23
|
-
peoplePage.heading().should(
|
|
23
|
+
peoplePage.heading().should('be.visible');
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
Cypress.Commands.add(
|
|
26
|
+
Cypress.Commands.add('addPerson', (person: PersonRecord) => {
|
|
27
27
|
peoplePage.addPerson(person);
|
|
28
28
|
});
|
|
29
29
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Generic data builders that keep tests readable and deterministic.
|
|
2
|
-
import { createSeededFaker } from
|
|
3
|
-
import { IdGenerator } from
|
|
2
|
+
import { createSeededFaker } from './seeded-faker';
|
|
3
|
+
import { IdGenerator } from './id-generator';
|
|
4
4
|
|
|
5
5
|
export type PersonRecord = {
|
|
6
6
|
personId: string;
|
|
@@ -17,7 +17,7 @@ export class DataFactory {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
person(overrides?: Partial<PersonRecord>): PersonRecord {
|
|
20
|
-
const personId = overrides?.personId ?? this.idGenerator.next(
|
|
20
|
+
const personId = overrides?.personId ?? this.idGenerator.next('person');
|
|
21
21
|
const seededFaker = createSeededFaker(`${this.testRunId}:${personId}`);
|
|
22
22
|
const firstName = seededFaker.person.firstName();
|
|
23
23
|
const lastName = seededFaker.person.lastName();
|
|
@@ -27,7 +27,9 @@ export class DataFactory {
|
|
|
27
27
|
personId,
|
|
28
28
|
name,
|
|
29
29
|
role: overrides?.role ?? seededFaker.person.jobTitle(),
|
|
30
|
-
email:
|
|
30
|
+
email:
|
|
31
|
+
overrides?.email ??
|
|
32
|
+
`${firstName}.${lastName}.${personId}@example.test`.toLowerCase(),
|
|
31
33
|
...overrides
|
|
32
34
|
};
|
|
33
35
|
}
|
|
@@ -7,7 +7,7 @@ export class IdGenerator {
|
|
|
7
7
|
next(prefix: string): string {
|
|
8
8
|
const counter = (this.counters.get(prefix) ?? 0) + 1;
|
|
9
9
|
this.counters.set(prefix, counter);
|
|
10
|
-
return `${prefix}-${this.runId}-${String(counter).padStart(4,
|
|
10
|
+
return `${prefix}-${this.runId}-${String(counter).padStart(4, '0')}`;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
nextSequence(prefix: string): number {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Seeded faker wrapper so generated values stay stable for a given run id.
|
|
2
|
-
import { Faker, en } from
|
|
2
|
+
import { Faker, en } from '@faker-js/faker';
|
|
3
3
|
|
|
4
4
|
function hashSeed(value: string): number {
|
|
5
|
-
return value.split(
|
|
5
|
+
return value.split('').reduce((seed, character) => {
|
|
6
6
|
return ((seed << 5) - seed + character.charCodeAt(0)) | 0;
|
|
7
7
|
}, 0);
|
|
8
8
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import 'allure-cypress';
|
|
2
|
+
import './commands';
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
// Page module for the login screen used by custom Cypress commands.
|
|
2
2
|
export const loginPage = {
|
|
3
3
|
visit(): Cypress.Chainable {
|
|
4
|
-
return cy.visit(
|
|
4
|
+
return cy.visit('/login');
|
|
5
5
|
},
|
|
6
6
|
|
|
7
7
|
usernameInput(): Cypress.Chainable {
|
|
8
|
-
return cy.get(
|
|
8
|
+
return cy.get('#username');
|
|
9
9
|
},
|
|
10
10
|
|
|
11
11
|
passwordInput(): Cypress.Chainable {
|
|
12
|
-
return cy.get(
|
|
12
|
+
return cy.get('#password');
|
|
13
13
|
},
|
|
14
14
|
|
|
15
15
|
submitButton(): Cypress.Chainable {
|
|
16
|
-
return cy.contains(
|
|
16
|
+
return cy.contains('button', 'Sign in');
|
|
17
17
|
},
|
|
18
18
|
|
|
19
19
|
login(username: string, password: string): void {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Page module for the people screen used in the starter Cypress journey.
|
|
2
|
-
import type { PersonRecord } from
|
|
2
|
+
import type { PersonRecord } from '../data/data-factory';
|
|
3
3
|
|
|
4
4
|
export const peoplePage = {
|
|
5
5
|
heading(): Cypress.Chainable {
|
|
6
|
-
return cy.contains(
|
|
6
|
+
return cy.contains('h1', 'People');
|
|
7
7
|
},
|
|
8
8
|
|
|
9
9
|
welcomeMessage(): Cypress.Chainable {
|
|
@@ -15,23 +15,23 @@ export const peoplePage = {
|
|
|
15
15
|
},
|
|
16
16
|
|
|
17
17
|
personIdInput(): Cypress.Chainable {
|
|
18
|
-
return cy.get(
|
|
18
|
+
return cy.get('#personId');
|
|
19
19
|
},
|
|
20
20
|
|
|
21
21
|
nameInput(): Cypress.Chainable {
|
|
22
|
-
return cy.get(
|
|
22
|
+
return cy.get('#name');
|
|
23
23
|
},
|
|
24
24
|
|
|
25
25
|
roleInput(): Cypress.Chainable {
|
|
26
|
-
return cy.get(
|
|
26
|
+
return cy.get('#role');
|
|
27
27
|
},
|
|
28
28
|
|
|
29
29
|
emailInput(): Cypress.Chainable {
|
|
30
|
-
return cy.get(
|
|
30
|
+
return cy.get('#email');
|
|
31
31
|
},
|
|
32
32
|
|
|
33
33
|
submitButton(): Cypress.Chainable {
|
|
34
|
-
return cy.contains(
|
|
34
|
+
return cy.contains('button', 'Add person');
|
|
35
35
|
},
|
|
36
36
|
|
|
37
37
|
personRow(personId: string): Cypress.Chainable {
|
|
@@ -39,15 +39,15 @@ export const peoplePage = {
|
|
|
39
39
|
},
|
|
40
40
|
|
|
41
41
|
nameCell(personId: string): Cypress.Chainable {
|
|
42
|
-
return this.personRow(personId).find(
|
|
42
|
+
return this.personRow(personId).find('td').eq(0);
|
|
43
43
|
},
|
|
44
44
|
|
|
45
45
|
roleCell(personId: string): Cypress.Chainable {
|
|
46
|
-
return this.personRow(personId).find(
|
|
46
|
+
return this.personRow(personId).find('td').eq(1);
|
|
47
47
|
},
|
|
48
48
|
|
|
49
49
|
emailCell(personId: string): Cypress.Chainable {
|
|
50
|
-
return this.personRow(personId).find(
|
|
50
|
+
return this.personRow(personId).find('td').eq(2);
|
|
51
51
|
},
|
|
52
52
|
|
|
53
53
|
addPerson(person: PersonRecord): void {
|