@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
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// Starts the local demo app when needed, then launches Cypress in run or open mode.
|
|
4
|
-
import process from
|
|
5
|
-
import path from
|
|
6
|
-
import { spawn } from
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
7
|
|
|
8
|
-
import dotenv from
|
|
8
|
+
import dotenv from 'dotenv';
|
|
9
9
|
|
|
10
|
-
const mode = process.argv[2] ??
|
|
10
|
+
const mode = process.argv[2] ?? 'run';
|
|
11
11
|
const args = process.argv.slice(3);
|
|
12
12
|
const cwd = process.cwd();
|
|
13
|
-
const healthUrl =
|
|
14
|
-
const environment = process.env.TEST_ENV ??
|
|
13
|
+
const healthUrl = 'http://127.0.0.1:3000/health';
|
|
14
|
+
const environment = process.env.TEST_ENV ?? 'dev';
|
|
15
15
|
const environmentDefaults = {
|
|
16
|
-
dev:
|
|
17
|
-
staging:
|
|
18
|
-
prod:
|
|
16
|
+
dev: 'http://127.0.0.1:3000',
|
|
17
|
+
staging: 'https://staging-ui.example.internal',
|
|
18
|
+
prod: 'https://ui.example.internal'
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
dotenv.config({ path: path.resolve(cwd,
|
|
22
|
-
dotenv.config({
|
|
21
|
+
dotenv.config({ path: path.resolve(cwd, '.env') });
|
|
22
|
+
dotenv.config({
|
|
23
|
+
path: path.resolve(cwd, `.env.${environment}`),
|
|
24
|
+
override: true
|
|
25
|
+
});
|
|
23
26
|
|
|
24
27
|
const uiBaseUrl =
|
|
25
28
|
process.env[`${environment.toUpperCase()}_UI_BASE_URL`] ??
|
|
@@ -28,18 +31,18 @@ const uiBaseUrl =
|
|
|
28
31
|
environmentDefaults.dev;
|
|
29
32
|
|
|
30
33
|
const shouldAutoStartDemoApp =
|
|
31
|
-
environment ===
|
|
34
|
+
environment === 'dev' &&
|
|
32
35
|
uiBaseUrl === environmentDefaults.dev &&
|
|
33
|
-
process.env.CY_DISABLE_LOCAL_DEMO_APP !==
|
|
36
|
+
process.env.CY_DISABLE_LOCAL_DEMO_APP !== 'true';
|
|
34
37
|
|
|
35
38
|
function getCommandName(command) {
|
|
36
|
-
return process.platform ===
|
|
39
|
+
return process.platform === 'win32' ? `${command}.cmd` : command;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
function spawnCommand(command, commandArgs, options = {}) {
|
|
40
43
|
return spawn(getCommandName(command), commandArgs, {
|
|
41
44
|
cwd,
|
|
42
|
-
stdio:
|
|
45
|
+
stdio: 'inherit',
|
|
43
46
|
...options
|
|
44
47
|
});
|
|
45
48
|
}
|
|
@@ -68,7 +71,7 @@ function killChild(child) {
|
|
|
68
71
|
return;
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
child.kill(
|
|
74
|
+
child.kill('SIGTERM');
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
async function run() {
|
|
@@ -76,16 +79,20 @@ async function run() {
|
|
|
76
79
|
|
|
77
80
|
try {
|
|
78
81
|
if (shouldAutoStartDemoApp) {
|
|
79
|
-
demoAppProcess = spawnCommand(
|
|
82
|
+
demoAppProcess = spawnCommand('npm', ['run', 'demo:ui']);
|
|
80
83
|
await waitForHealthcheck(healthUrl);
|
|
81
84
|
}
|
|
82
85
|
|
|
83
|
-
const cypressCommand = mode ===
|
|
84
|
-
const cypressProcess = spawnCommand(
|
|
86
|
+
const cypressCommand = mode === 'open' ? 'open' : 'run';
|
|
87
|
+
const cypressProcess = spawnCommand('npx', [
|
|
88
|
+
'cypress',
|
|
89
|
+
cypressCommand,
|
|
90
|
+
...args
|
|
91
|
+
]);
|
|
85
92
|
|
|
86
93
|
const exitCode = await new Promise((resolve) => {
|
|
87
|
-
cypressProcess.on(
|
|
88
|
-
cypressProcess.on(
|
|
94
|
+
cypressProcess.on('close', resolve);
|
|
95
|
+
cypressProcess.on('error', () => resolve(1));
|
|
89
96
|
});
|
|
90
97
|
|
|
91
98
|
if (exitCode !== 0) {
|
|
@@ -96,11 +103,13 @@ async function run() {
|
|
|
96
103
|
}
|
|
97
104
|
}
|
|
98
105
|
|
|
99
|
-
for (const signal of [
|
|
106
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
100
107
|
process.on(signal, () => process.exit(1));
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
run().catch((error) => {
|
|
104
|
-
process.stderr.write(
|
|
111
|
+
process.stderr.write(
|
|
112
|
+
`${error instanceof Error ? error.message : String(error)}\n`
|
|
113
|
+
);
|
|
105
114
|
process.exit(1);
|
|
106
115
|
});
|
|
@@ -11,5 +11,11 @@
|
|
|
11
11
|
"noEmit": true,
|
|
12
12
|
"types": ["cypress", "node"]
|
|
13
13
|
},
|
|
14
|
-
"include": [
|
|
14
|
+
"include": [
|
|
15
|
+
"cypress/**/*.ts",
|
|
16
|
+
"cypress/**/*.d.ts",
|
|
17
|
+
"config/**/*.ts",
|
|
18
|
+
"cypress.config.ts",
|
|
19
|
+
"allurerc.mjs"
|
|
20
|
+
]
|
|
15
21
|
}
|
|
@@ -3,15 +3,15 @@ TEST_RUN_ID=local
|
|
|
3
3
|
|
|
4
4
|
DEV_UI_BASE_URL=http://127.0.0.1:3000
|
|
5
5
|
DEV_API_BASE_URL=http://127.0.0.1:3001
|
|
6
|
-
DEV_APP_USERNAME=
|
|
7
|
-
DEV_APP_PASSWORD=
|
|
6
|
+
DEV_APP_USERNAME=generate-a-local-username
|
|
7
|
+
DEV_APP_PASSWORD=generate-a-local-password
|
|
8
8
|
|
|
9
9
|
STAGING_UI_BASE_URL=https://staging-ui.example.internal
|
|
10
10
|
STAGING_API_BASE_URL=https://staging-api.example.internal
|
|
11
|
-
STAGING_APP_USERNAME=staging-
|
|
12
|
-
STAGING_APP_PASSWORD=
|
|
11
|
+
STAGING_APP_USERNAME=your-staging-username
|
|
12
|
+
STAGING_APP_PASSWORD=your-staging-password
|
|
13
13
|
|
|
14
14
|
PROD_UI_BASE_URL=https://ui.example.internal
|
|
15
15
|
PROD_API_BASE_URL=https://api.example.internal
|
|
16
|
-
PROD_APP_USERNAME=prod-
|
|
17
|
-
PROD_APP_PASSWORD=
|
|
16
|
+
PROD_APP_USERNAME=your-prod-username
|
|
17
|
+
PROD_APP_PASSWORD=your-prod-password
|
|
@@ -4,9 +4,9 @@ on:
|
|
|
4
4
|
workflow_dispatch:
|
|
5
5
|
push:
|
|
6
6
|
paths:
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
7
|
+
- 'templates/playwright-template/**'
|
|
8
|
+
- 'test-apps/**'
|
|
9
|
+
- 'package.json'
|
|
10
10
|
|
|
11
11
|
jobs:
|
|
12
12
|
playwright:
|
|
@@ -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 workspace dependencies
|
|
23
23
|
run: npm ci
|
|
@@ -50,13 +50,18 @@ jobs:
|
|
|
50
50
|
|
|
51
51
|
playwright-docker:
|
|
52
52
|
runs-on: ubuntu-latest
|
|
53
|
+
env:
|
|
54
|
+
DEV_APP_USERNAME: ci-docker-user
|
|
55
|
+
DEV_APP_PASSWORD: ci-docker-password-A1!
|
|
56
|
+
UI_DEMO_USERNAME: ci-docker-user
|
|
57
|
+
UI_DEMO_PASSWORD: ci-docker-password-A1!
|
|
53
58
|
|
|
54
59
|
steps:
|
|
55
60
|
- uses: actions/checkout@v4
|
|
56
61
|
|
|
57
62
|
- uses: actions/setup-node@v4
|
|
58
63
|
with:
|
|
59
|
-
node-version:
|
|
64
|
+
node-version: '20'
|
|
60
65
|
|
|
61
66
|
- name: Install workspace dependencies
|
|
62
67
|
run: npm ci
|
|
@@ -91,6 +96,10 @@ jobs:
|
|
|
91
96
|
-e TEST_RUN_ID=ci-docker \
|
|
92
97
|
-e DEV_UI_BASE_URL=http://host.docker.internal:3000 \
|
|
93
98
|
-e DEV_API_BASE_URL=http://host.docker.internal:3001 \
|
|
99
|
+
-e DEV_APP_USERNAME=${DEV_APP_USERNAME} \
|
|
100
|
+
-e DEV_APP_PASSWORD=${DEV_APP_PASSWORD} \
|
|
101
|
+
-e UI_DEMO_USERNAME=${UI_DEMO_USERNAME} \
|
|
102
|
+
-e UI_DEMO_PASSWORD=${UI_DEMO_PASSWORD} \
|
|
94
103
|
-v "${GITHUB_WORKSPACE}/templates/playwright-template/reports:/workspace/reports" \
|
|
95
104
|
-v "${GITHUB_WORKSPACE}/templates/playwright-template/allure-results:/workspace/allure-results" \
|
|
96
105
|
-v "${GITHUB_WORKSPACE}/templates/playwright-template/test-results:/workspace/test-results" \
|
|
@@ -13,6 +13,7 @@ This is a Playwright + TypeScript automation framework template for UI and API t
|
|
|
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 and Docker](#ci-and-docker)
|
|
17
18
|
|
|
18
19
|
## Feature set
|
|
@@ -88,8 +89,7 @@ Default local values:
|
|
|
88
89
|
|
|
89
90
|
- UI base URL: `http://127.0.0.1:3000`
|
|
90
91
|
- API base URL: `http://127.0.0.1:3001`
|
|
91
|
-
-
|
|
92
|
-
- password: `Password123!`
|
|
92
|
+
- credentials: generated into local `.env` on first run
|
|
93
93
|
|
|
94
94
|
## Environment and secrets
|
|
95
95
|
|
|
@@ -109,7 +109,7 @@ The same pattern is used for credentials:
|
|
|
109
109
|
|
|
110
110
|
1. `DEV_APP_USERNAME` or `DEV_APP_PASSWORD`
|
|
111
111
|
2. `APP_USERNAME` or `APP_PASSWORD`
|
|
112
|
-
3. built-in defaults for the selected environment
|
|
112
|
+
3. built-in empty defaults for the selected environment
|
|
113
113
|
|
|
114
114
|
For local overrides, copy:
|
|
115
115
|
|
|
@@ -128,6 +128,8 @@ The template loads:
|
|
|
128
128
|
- `.env`
|
|
129
129
|
- `.env.<TEST_ENV>`
|
|
130
130
|
|
|
131
|
+
On the first local run, the template also creates a `.env` file with random demo credentials if one does not already exist.
|
|
132
|
+
|
|
131
133
|
Example:
|
|
132
134
|
|
|
133
135
|
```bash
|
|
@@ -191,7 +193,7 @@ If you only want Playwright reporting, remove the `allure-playwright` reporter e
|
|
|
191
193
|
Create tests under `tests/` and import the shared fixtures:
|
|
192
194
|
|
|
193
195
|
```ts
|
|
194
|
-
import { expect, test } from
|
|
196
|
+
import { expect, test } from '../fixtures/test-fixtures';
|
|
195
197
|
```
|
|
196
198
|
|
|
197
199
|
Keep the pattern simple:
|
|
@@ -203,7 +205,7 @@ Keep the pattern simple:
|
|
|
203
205
|
Example shape:
|
|
204
206
|
|
|
205
207
|
```ts
|
|
206
|
-
test(
|
|
208
|
+
test('do something @smoke', async ({ dataFactory, loginPage }) => {
|
|
207
209
|
const person = dataFactory.person();
|
|
208
210
|
// use page objects here
|
|
209
211
|
});
|
|
@@ -228,6 +230,24 @@ Recommended rules:
|
|
|
228
230
|
- prefer semantic selectors such as `getByRole`, `getByLabel`, and `data-testid`
|
|
229
231
|
- keep the data layer generic until the project really needs domain-specific factories
|
|
230
232
|
|
|
233
|
+
## Template upgrades
|
|
234
|
+
|
|
235
|
+
This project includes a `.qa-patterns.json` metadata file so future CLI versions can compare the current project against the managed template baseline.
|
|
236
|
+
|
|
237
|
+
Check for available safe updates:
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
npx -y @toolstackhq/create-qa-patterns upgrade check .
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Apply only safe managed-file updates:
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
npx -y @toolstackhq/create-qa-patterns upgrade apply --safe .
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
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.
|
|
250
|
+
|
|
231
251
|
## CI and Docker
|
|
232
252
|
|
|
233
253
|
The CI entrypoint is:
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { Locator, Page } from
|
|
1
|
+
import type { Locator, Page } from '@playwright/test';
|
|
2
2
|
|
|
3
3
|
export class FlashMessage {
|
|
4
4
|
private readonly message: Locator;
|
|
5
5
|
|
|
6
6
|
constructor(page: Page) {
|
|
7
|
-
this.message = page.getByTestId(
|
|
7
|
+
this.message = page.getByTestId('flash-message');
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
async getText(): Promise<string | null> {
|
|
@@ -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;
|
|
@@ -12,31 +12,33 @@ type EnvironmentDefaults = {
|
|
|
12
12
|
|
|
13
13
|
const DEFAULTS: Record<TestEnvironment, EnvironmentDefaults> = {
|
|
14
14
|
dev: {
|
|
15
|
-
uiBaseUrl:
|
|
16
|
-
apiBaseUrl:
|
|
15
|
+
uiBaseUrl: 'http://127.0.0.1:3000',
|
|
16
|
+
apiBaseUrl: 'http://127.0.0.1:3001',
|
|
17
17
|
credentials: {
|
|
18
|
-
username:
|
|
19
|
-
password:
|
|
18
|
+
username: '',
|
|
19
|
+
password: ''
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
staging: {
|
|
23
|
-
uiBaseUrl:
|
|
24
|
-
apiBaseUrl:
|
|
23
|
+
uiBaseUrl: 'https://staging-ui.example.internal',
|
|
24
|
+
apiBaseUrl: 'https://staging-api.example.internal',
|
|
25
25
|
credentials: {
|
|
26
|
-
username:
|
|
27
|
-
password:
|
|
26
|
+
username: '',
|
|
27
|
+
password: ''
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
30
|
prod: {
|
|
31
|
-
uiBaseUrl:
|
|
32
|
-
apiBaseUrl:
|
|
31
|
+
uiBaseUrl: 'https://ui.example.internal',
|
|
32
|
+
apiBaseUrl: 'https://api.example.internal',
|
|
33
33
|
credentials: {
|
|
34
|
-
username:
|
|
35
|
-
password:
|
|
34
|
+
username: '',
|
|
35
|
+
password: ''
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
export function getEnvironmentDefaults(
|
|
40
|
+
export function getEnvironmentDefaults(
|
|
41
|
+
testEnv: TestEnvironment
|
|
42
|
+
): EnvironmentDefaults {
|
|
41
43
|
return DEFAULTS[testEnv];
|
|
42
44
|
}
|
|
@@ -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
|
apiBaseUrl: z.string().url(),
|
|
@@ -41,14 +44,16 @@ export function loadRuntimeConfig(): RuntimeConfig {
|
|
|
41
44
|
|
|
42
45
|
return runtimeConfigSchema.parse({
|
|
43
46
|
testEnv: environment,
|
|
44
|
-
testRunId: process.env.TEST_RUN_ID ??
|
|
47
|
+
testRunId: process.env.TEST_RUN_ID ?? 'local',
|
|
45
48
|
uiBaseUrl,
|
|
46
49
|
apiBaseUrl,
|
|
47
50
|
credentials: {
|
|
48
51
|
username:
|
|
49
|
-
secretManager.getOptionalSecret(
|
|
52
|
+
secretManager.getOptionalSecret('APP_USERNAME', environment) ??
|
|
53
|
+
defaults.credentials.username,
|
|
50
54
|
password:
|
|
51
|
-
secretManager.getOptionalSecret(
|
|
55
|
+
secretManager.getOptionalSecret('APP_PASSWORD', environment) ??
|
|
56
|
+
defaults.credentials.password
|
|
52
57
|
}
|
|
53
58
|
});
|
|
54
59
|
}
|
|
@@ -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,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 '../generators/seeded-faker';
|
|
3
|
+
import { IdGenerator } from '../generators/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,25 +1,25 @@
|
|
|
1
|
-
const express = require(
|
|
1
|
+
const express = require('express');
|
|
2
2
|
|
|
3
|
-
const { createPerson, state } = require(
|
|
3
|
+
const { createPerson, state } = require('./store');
|
|
4
4
|
|
|
5
5
|
const app = express();
|
|
6
|
-
const host = process.env.HOST ||
|
|
7
|
-
const port = Number(process.env.PORT ||
|
|
6
|
+
const host = process.env.HOST || '0.0.0.0';
|
|
7
|
+
const port = Number(process.env.PORT || '3001');
|
|
8
8
|
|
|
9
9
|
app.use(express.json());
|
|
10
10
|
|
|
11
|
-
app.get(
|
|
12
|
-
response.json({ status:
|
|
11
|
+
app.get('/health', (_request, response) => {
|
|
12
|
+
response.json({ status: 'ok' });
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
app.get(
|
|
15
|
+
app.get('/people', (_request, response) => {
|
|
16
16
|
response.json(state.people);
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
app.post(
|
|
19
|
+
app.post('/people', (request, response) => {
|
|
20
20
|
const { name, role, email } = request.body;
|
|
21
21
|
if (!name || !role || !email) {
|
|
22
|
-
response.status(400).json({ error:
|
|
22
|
+
response.status(400).json({ error: 'name, role, and email are required' });
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
25
|
|