@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.
- package/README.md +5 -0
- package/index.js +252 -1076
- 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 +10 -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 +6 -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
|
|
@@ -91,8 +91,7 @@ npm run report:allure
|
|
|
91
91
|
Default local values:
|
|
92
92
|
|
|
93
93
|
- UI base URL: `http://127.0.0.1:3000`
|
|
94
|
-
-
|
|
95
|
-
- password: `Password123!`
|
|
94
|
+
- credentials: generated into local `.env` on first run
|
|
96
95
|
|
|
97
96
|
## Environment and secrets
|
|
98
97
|
|
|
@@ -112,7 +111,7 @@ Credentials resolve the same way:
|
|
|
112
111
|
|
|
113
112
|
1. `DEV_APP_USERNAME` or `DEV_APP_PASSWORD`
|
|
114
113
|
2. `APP_USERNAME` or `APP_PASSWORD`
|
|
115
|
-
3. built-in defaults for the selected environment
|
|
114
|
+
3. built-in empty defaults for the selected environment
|
|
116
115
|
|
|
117
116
|
For local overrides, copy:
|
|
118
117
|
|
|
@@ -126,6 +125,8 @@ to:
|
|
|
126
125
|
.env
|
|
127
126
|
```
|
|
128
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
|
+
|
|
129
130
|
If you want to disable the bundled local demo app even in `dev`, use:
|
|
130
131
|
|
|
131
132
|
```bash
|
|
@@ -172,11 +173,14 @@ Keep the pattern simple:
|
|
|
172
173
|
Example shape:
|
|
173
174
|
|
|
174
175
|
```ts
|
|
175
|
-
|
|
176
|
-
|
|
176
|
+
import { loadAppConfig } from '../support/app-config';
|
|
177
|
+
|
|
178
|
+
it('does something', () => {
|
|
179
|
+
const appConfig = loadAppConfig();
|
|
180
|
+
const dataFactory = new DataFactory('local');
|
|
177
181
|
const person = dataFactory.person();
|
|
178
182
|
|
|
179
|
-
cy.signIn(
|
|
183
|
+
cy.signIn(appConfig.credentials.username, appConfig.credentials.password);
|
|
180
184
|
cy.addPerson(person);
|
|
181
185
|
});
|
|
182
186
|
```
|
|
@@ -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 {
|