@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
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# WebdriverIO Template
|
|
2
|
+
|
|
3
|
+
This is a WebdriverIO + TypeScript automation framework template for UI tests.
|
|
4
|
+
|
|
5
|
+
## Table of contents
|
|
6
|
+
|
|
7
|
+
- [Feature set](#feature-set)
|
|
8
|
+
- [How it works](#how-it-works)
|
|
9
|
+
- [Project structure](#project-structure)
|
|
10
|
+
- [Quick start](#quick-start)
|
|
11
|
+
- [Environment and secrets](#environment-and-secrets)
|
|
12
|
+
- [Main commands](#main-commands)
|
|
13
|
+
- [Reports and artifacts](#reports-and-artifacts)
|
|
14
|
+
- [Add a new test](#add-a-new-test)
|
|
15
|
+
- [Extend the framework](#extend-the-framework)
|
|
16
|
+
- [Template upgrades](#template-upgrades)
|
|
17
|
+
- [CI](#ci)
|
|
18
|
+
|
|
19
|
+
## Feature set
|
|
20
|
+
|
|
21
|
+
- WebdriverIO + TypeScript setup
|
|
22
|
+
- Mocha-based specs with page objects and step logging
|
|
23
|
+
- generic data factory pattern with `DataFactory`
|
|
24
|
+
- folder-level `README.md` guides and file-header comments for easier onboarding
|
|
25
|
+
- multi-environment runtime config with `dev`, `staging`, and `prod`
|
|
26
|
+
- env-based secret resolution with a replaceable `SecretProvider`
|
|
27
|
+
- WebdriverIO spec reporter by default
|
|
28
|
+
- optional Allure single-file report
|
|
29
|
+
- screenshots and structured logs for debugging
|
|
30
|
+
- ESLint rules that protect framework conventions
|
|
31
|
+
- GitHub Actions workflow for the template
|
|
32
|
+
|
|
33
|
+
## How it works
|
|
34
|
+
|
|
35
|
+
- specs in `tests/` own the workflow and assertions
|
|
36
|
+
- page objects in `pages/` own locators and browser actions
|
|
37
|
+
- runtime config is loaded from `config/runtime-config.ts`
|
|
38
|
+
- application URLs and credentials are resolved from `TEST_ENV`
|
|
39
|
+
- the bundled demo app auto-starts during `npm test` in local `dev` mode when the default local URL is in use
|
|
40
|
+
- reports and artifacts are written under `reports/`, `allure-results/`, and `test-results/`
|
|
41
|
+
|
|
42
|
+
## Project structure
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
wdio-template
|
|
46
|
+
├── tests
|
|
47
|
+
├── pages
|
|
48
|
+
├── components
|
|
49
|
+
├── data
|
|
50
|
+
├── config
|
|
51
|
+
├── reporters
|
|
52
|
+
├── utils
|
|
53
|
+
├── lint
|
|
54
|
+
├── scripts
|
|
55
|
+
├── wdio.conf.ts
|
|
56
|
+
└── package.json
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Quick start
|
|
60
|
+
|
|
61
|
+
1. Install dependencies.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm install
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
2. Run tests.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm test
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
In local `dev`, the template starts its bundled demo app automatically before the tests run.
|
|
74
|
+
|
|
75
|
+
If you want to run the demo app manually for debugging:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm run demo:ui
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Default local values:
|
|
82
|
+
|
|
83
|
+
- UI base URL: `http://127.0.0.1:3000`
|
|
84
|
+
- credentials: generated into local `.env` on first run
|
|
85
|
+
|
|
86
|
+
## Environment and secrets
|
|
87
|
+
|
|
88
|
+
The template supports:
|
|
89
|
+
|
|
90
|
+
- `TEST_ENV=dev`
|
|
91
|
+
- `TEST_ENV=staging`
|
|
92
|
+
- `TEST_ENV=prod`
|
|
93
|
+
|
|
94
|
+
Runtime values are resolved in this order:
|
|
95
|
+
|
|
96
|
+
1. environment-specific variables such as `DEV_UI_BASE_URL`
|
|
97
|
+
2. generic variables such as `UI_BASE_URL`
|
|
98
|
+
3. built-in defaults from `config/environments.ts`
|
|
99
|
+
|
|
100
|
+
The same pattern is used for credentials:
|
|
101
|
+
|
|
102
|
+
1. `DEV_APP_USERNAME` or `DEV_APP_PASSWORD`
|
|
103
|
+
2. `APP_USERNAME` or `APP_PASSWORD`
|
|
104
|
+
3. built-in empty defaults for the selected environment
|
|
105
|
+
|
|
106
|
+
For local overrides, copy:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
.env.example
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
to:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
.env
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The template loads:
|
|
119
|
+
|
|
120
|
+
- `.env`
|
|
121
|
+
- `.env.<TEST_ENV>`
|
|
122
|
+
|
|
123
|
+
On the first local run, the template also creates a `.env` file with random demo credentials if one does not already exist.
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
TEST_ENV=staging \
|
|
129
|
+
STAGING_UI_BASE_URL=https://staging-ui.example.internal \
|
|
130
|
+
STAGING_APP_USERNAME=my-user \
|
|
131
|
+
STAGING_APP_PASSWORD=my-password \
|
|
132
|
+
npm test
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
If you want to disable the bundled local demo app even in `dev`, use:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
WDIO_DISABLE_LOCAL_DEMO_APP=true npm test
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
If your team uses a real secret system later, replace the implementation behind `config/secret-manager.ts`.
|
|
142
|
+
|
|
143
|
+
## Main commands
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
npm test
|
|
147
|
+
npm run test:smoke
|
|
148
|
+
npm run test:regression
|
|
149
|
+
npm run test:critical
|
|
150
|
+
npm run demo:ui
|
|
151
|
+
npm run lint
|
|
152
|
+
npm run typecheck
|
|
153
|
+
npm run report:allure
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Reports and artifacts
|
|
157
|
+
|
|
158
|
+
Optional Allure report:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
npm run report:allure
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Outputs:
|
|
165
|
+
|
|
166
|
+
- Allure single file: `reports/allure/index.html`
|
|
167
|
+
- structured event log: `reports/logs/wdio-events.jsonl`
|
|
168
|
+
- raw Allure results: `allure-results`
|
|
169
|
+
- failure screenshots: `test-results`
|
|
170
|
+
|
|
171
|
+
If you only want WebdriverIO's built-in terminal reporting, remove the `allure` reporter entry in `wdio.conf.ts`.
|
|
172
|
+
|
|
173
|
+
## Add a new test
|
|
174
|
+
|
|
175
|
+
Create specs under `tests/`.
|
|
176
|
+
|
|
177
|
+
Keep the pattern simple:
|
|
178
|
+
|
|
179
|
+
- create data with `DataFactory`
|
|
180
|
+
- interact through page objects
|
|
181
|
+
- assert in the spec
|
|
182
|
+
|
|
183
|
+
Example shape:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
describe('do something @smoke', () => {
|
|
187
|
+
it('uses page objects and data factories', async () => {
|
|
188
|
+
// use page objects here
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Extend the framework
|
|
194
|
+
|
|
195
|
+
Common extension points:
|
|
196
|
+
|
|
197
|
+
- update or replace the bundled demo app under `demo-apps/`
|
|
198
|
+
- add page objects under `pages/`
|
|
199
|
+
- add reusable UI pieces under `components/`
|
|
200
|
+
- add more generic builders under `data/factories/`
|
|
201
|
+
- add stronger custom lint rules in `lint/architecture-plugin.cjs`
|
|
202
|
+
- add custom reporters under `reporters/`
|
|
203
|
+
|
|
204
|
+
Recommended rules:
|
|
205
|
+
|
|
206
|
+
- keep selectors in page objects
|
|
207
|
+
- keep assertions in spec files
|
|
208
|
+
- prefer stable selectors such as labels, button text, and `data-testid`
|
|
209
|
+
- keep the data layer generic until the project really needs domain-specific factories
|
|
210
|
+
|
|
211
|
+
## Template upgrades
|
|
212
|
+
|
|
213
|
+
This project includes a `.qa-patterns.json` metadata file so future CLI versions can compare the current project against the managed template baseline.
|
|
214
|
+
|
|
215
|
+
Check for available safe updates:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
npx -y @toolstackhq/create-qa-patterns upgrade check .
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Apply only safe managed-file updates:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
npx -y @toolstackhq/create-qa-patterns upgrade apply --safe .
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
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.
|
|
228
|
+
|
|
229
|
+
## CI
|
|
230
|
+
|
|
231
|
+
The CI entrypoint is:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
scripts/run-tests.sh
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The bundled workflow lives in:
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
.github/workflows/wdio-tests.yml
|
|
241
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Locator, Page } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
export class FlashMessage {
|
|
4
|
+
private readonly message: Locator;
|
|
5
|
+
|
|
6
|
+
constructor(page: Page) {
|
|
7
|
+
this.message = page.getByTestId('flash-message');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async getText(): Promise<string | null> {
|
|
11
|
+
if (!(await this.message.count())) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return this.message.textContent();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Defines the built-in environment defaults used when env vars are not provided.
|
|
2
|
+
import type { TestEnvironment } from './test-env';
|
|
3
|
+
|
|
4
|
+
type EnvironmentDefaults = {
|
|
5
|
+
uiBaseUrl: string;
|
|
6
|
+
credentials: {
|
|
7
|
+
username: string;
|
|
8
|
+
password: string;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const DEFAULTS: Record<TestEnvironment, EnvironmentDefaults> = {
|
|
13
|
+
dev: {
|
|
14
|
+
uiBaseUrl: 'http://127.0.0.1:3000',
|
|
15
|
+
credentials: {
|
|
16
|
+
username: '',
|
|
17
|
+
password: ''
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
staging: {
|
|
21
|
+
uiBaseUrl: 'https://staging-ui.example.internal',
|
|
22
|
+
credentials: {
|
|
23
|
+
username: '',
|
|
24
|
+
password: ''
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
prod: {
|
|
28
|
+
uiBaseUrl: 'https://ui.example.internal',
|
|
29
|
+
credentials: {
|
|
30
|
+
username: '',
|
|
31
|
+
password: ''
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function getEnvironmentDefaults(
|
|
37
|
+
testEnv: TestEnvironment
|
|
38
|
+
): EnvironmentDefaults {
|
|
39
|
+
return DEFAULTS[testEnv];
|
|
40
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Builds the runtime configuration object that tests and fixtures consume.
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import dotenv from 'dotenv';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
import { getEnvironmentDefaults } from './environments';
|
|
8
|
+
import { EnvSecretProvider, SecretManager } from './secret-manager';
|
|
9
|
+
import { loadTestEnvironment } from './test-env';
|
|
10
|
+
|
|
11
|
+
const environment = loadTestEnvironment();
|
|
12
|
+
|
|
13
|
+
dotenv.config({ path: path.resolve(process.cwd(), '.env') });
|
|
14
|
+
dotenv.config({
|
|
15
|
+
path: path.resolve(process.cwd(), `.env.${environment}`),
|
|
16
|
+
override: true
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const runtimeConfigSchema = z.object({
|
|
20
|
+
testEnv: z.enum(['dev', 'staging', 'prod']),
|
|
21
|
+
testRunId: z.string().min(1),
|
|
22
|
+
uiBaseUrl: z.string().url(),
|
|
23
|
+
credentials: z.object({
|
|
24
|
+
username: z.string().min(1),
|
|
25
|
+
password: z.string().min(1)
|
|
26
|
+
})
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export type RuntimeConfig = z.infer<typeof runtimeConfigSchema>;
|
|
30
|
+
|
|
31
|
+
export function loadRuntimeConfig(): RuntimeConfig {
|
|
32
|
+
const defaults = getEnvironmentDefaults(environment);
|
|
33
|
+
const secretManager = new SecretManager(new EnvSecretProvider());
|
|
34
|
+
|
|
35
|
+
const uiBaseUrl =
|
|
36
|
+
process.env[`${environment.toUpperCase()}_UI_BASE_URL`] ??
|
|
37
|
+
process.env.UI_BASE_URL ??
|
|
38
|
+
defaults.uiBaseUrl;
|
|
39
|
+
|
|
40
|
+
return runtimeConfigSchema.parse({
|
|
41
|
+
testEnv: environment,
|
|
42
|
+
testRunId: process.env.TEST_RUN_ID ?? 'local',
|
|
43
|
+
uiBaseUrl,
|
|
44
|
+
credentials: {
|
|
45
|
+
username:
|
|
46
|
+
secretManager.getOptionalSecret('APP_USERNAME', environment) ??
|
|
47
|
+
defaults.credentials.username,
|
|
48
|
+
password:
|
|
49
|
+
secretManager.getOptionalSecret('APP_PASSWORD', environment) ??
|
|
50
|
+
defaults.credentials.password
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Minimal secret abstraction so env-based secrets can later be replaced cleanly.
|
|
2
|
+
import type { TestEnvironment } from './test-env';
|
|
3
|
+
|
|
4
|
+
export interface SecretProvider {
|
|
5
|
+
getSecret(key: string, testEnv: TestEnvironment): string | undefined;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class EnvSecretProvider implements SecretProvider {
|
|
9
|
+
getSecret(key: string, testEnv: TestEnvironment): string | undefined {
|
|
10
|
+
const envPrefix = testEnv.toUpperCase();
|
|
11
|
+
return process.env[`${envPrefix}_${key}`] ?? process.env[key];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class SecretManager {
|
|
16
|
+
constructor(private readonly provider: SecretProvider) {}
|
|
17
|
+
|
|
18
|
+
getRequiredSecret(key: string, testEnv: TestEnvironment): string {
|
|
19
|
+
const value = this.provider.getSecret(key, testEnv);
|
|
20
|
+
if (!value) {
|
|
21
|
+
throw new Error(`Missing secret "${key}" for TEST_ENV=${testEnv}`);
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getOptionalSecret(key: string, testEnv: TestEnvironment): string | undefined {
|
|
27
|
+
return this.provider.getSecret(key, testEnv);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const testEnvironmentSchema = z.enum(['dev', 'staging', 'prod']);
|
|
4
|
+
|
|
5
|
+
export type TestEnvironment = z.infer<typeof testEnvironmentSchema>;
|
|
6
|
+
|
|
7
|
+
export function loadTestEnvironment(): TestEnvironment {
|
|
8
|
+
return testEnvironmentSchema.parse(process.env.TEST_ENV ?? 'dev');
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Data Layer
|
|
2
|
+
|
|
3
|
+
Keep the starter data layer generic.
|
|
4
|
+
|
|
5
|
+
- Start with `DataFactory.person()` or similarly small builders.
|
|
6
|
+
- Add domain-specific factories only when the project actually needs them.
|
|
7
|
+
- Introduce stricter validation later only if your team actually needs it.
|
|
8
|
+
|
|
9
|
+
The template should feel approachable before it feels comprehensive.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Generic data builders that keep tests readable and deterministic.
|
|
2
|
+
import { createSeededFaker } from '../generators/seeded-faker';
|
|
3
|
+
import { IdGenerator } from '../generators/id-generator';
|
|
4
|
+
|
|
5
|
+
export type PersonRecord = {
|
|
6
|
+
personId: string;
|
|
7
|
+
name: string;
|
|
8
|
+
role: string;
|
|
9
|
+
email: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export class DataFactory {
|
|
13
|
+
private readonly idGenerator: IdGenerator;
|
|
14
|
+
|
|
15
|
+
constructor(private readonly testRunId: string) {
|
|
16
|
+
this.idGenerator = new IdGenerator(testRunId);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
person(overrides?: Partial<PersonRecord>): PersonRecord {
|
|
20
|
+
const personId = overrides?.personId ?? this.idGenerator.next('person');
|
|
21
|
+
const seededFaker = createSeededFaker(`${this.testRunId}:${personId}`);
|
|
22
|
+
const firstName = seededFaker.person.firstName();
|
|
23
|
+
const lastName = seededFaker.person.lastName();
|
|
24
|
+
const name = `${firstName} ${lastName}`;
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
personId,
|
|
28
|
+
name,
|
|
29
|
+
role: overrides?.role ?? seededFaker.person.jobTitle(),
|
|
30
|
+
email:
|
|
31
|
+
overrides?.email ??
|
|
32
|
+
`${firstName}.${lastName}.${personId}@example.test`.toLowerCase(),
|
|
33
|
+
...overrides
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Deterministic id generator for repeatable local and CI runs.
|
|
2
|
+
export class IdGenerator {
|
|
3
|
+
private readonly counters = new Map<string, number>();
|
|
4
|
+
|
|
5
|
+
constructor(private readonly runId: string) {}
|
|
6
|
+
|
|
7
|
+
next(prefix: string): string {
|
|
8
|
+
const counter = (this.counters.get(prefix) ?? 0) + 1;
|
|
9
|
+
this.counters.set(prefix, counter);
|
|
10
|
+
return `${prefix}-${this.runId}-${String(counter).padStart(4, '0')}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
nextSequence(prefix: string): number {
|
|
14
|
+
const counter = (this.counters.get(prefix) ?? 0) + 1;
|
|
15
|
+
this.counters.set(prefix, counter);
|
|
16
|
+
return counter;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Seeded faker wrapper so generated values stay stable for a given run id.
|
|
2
|
+
import { Faker, en } from '@faker-js/faker';
|
|
3
|
+
|
|
4
|
+
function hashSeed(value: string): number {
|
|
5
|
+
return value.split('').reduce((seed, character) => {
|
|
6
|
+
return ((seed << 5) - seed + character.charCodeAt(0)) | 0;
|
|
7
|
+
}, 0);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createSeededFaker(seedInput: string): Faker {
|
|
11
|
+
const instance = new Faker({ locale: [en] });
|
|
12
|
+
instance.seed(Math.abs(hashSeed(seedInput)));
|
|
13
|
+
return instance;
|
|
14
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
body {
|
|
2
|
+
margin: 0;
|
|
3
|
+
font-family: 'IBM Plex Sans', 'Segoe UI', sans-serif;
|
|
4
|
+
background: linear-gradient(180deg, #f5f7fb 0%, #eef2f7 100%);
|
|
5
|
+
color: #102038;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.layout {
|
|
9
|
+
max-width: 1080px;
|
|
10
|
+
margin: 0 auto;
|
|
11
|
+
padding: 32px 24px 48px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.header {
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
justify-content: space-between;
|
|
18
|
+
gap: 16px;
|
|
19
|
+
margin-bottom: 24px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.brand {
|
|
23
|
+
font-size: 1.5rem;
|
|
24
|
+
font-weight: 700;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.nav {
|
|
28
|
+
display: flex;
|
|
29
|
+
gap: 12px;
|
|
30
|
+
flex-wrap: wrap;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.nav a {
|
|
34
|
+
text-decoration: none;
|
|
35
|
+
color: #102038;
|
|
36
|
+
background: #d8e6ff;
|
|
37
|
+
padding: 10px 14px;
|
|
38
|
+
border-radius: 999px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.card-grid {
|
|
42
|
+
display: grid;
|
|
43
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
44
|
+
gap: 16px;
|
|
45
|
+
margin-bottom: 24px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.card,
|
|
49
|
+
.panel {
|
|
50
|
+
background: #ffffff;
|
|
51
|
+
border-radius: 18px;
|
|
52
|
+
padding: 20px;
|
|
53
|
+
box-shadow: 0 20px 40px rgba(16, 32, 56, 0.08);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.panel-grid {
|
|
57
|
+
display: grid;
|
|
58
|
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
59
|
+
gap: 24px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
form {
|
|
63
|
+
display: grid;
|
|
64
|
+
gap: 14px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
label {
|
|
68
|
+
display: grid;
|
|
69
|
+
gap: 6px;
|
|
70
|
+
font-weight: 600;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
input,
|
|
74
|
+
select,
|
|
75
|
+
button {
|
|
76
|
+
font: inherit;
|
|
77
|
+
padding: 10px 12px;
|
|
78
|
+
border-radius: 12px;
|
|
79
|
+
border: 1px solid #c4d3eb;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
button {
|
|
83
|
+
background: #174ea6;
|
|
84
|
+
color: #ffffff;
|
|
85
|
+
border: none;
|
|
86
|
+
font-weight: 700;
|
|
87
|
+
cursor: pointer;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
table {
|
|
91
|
+
width: 100%;
|
|
92
|
+
border-collapse: collapse;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
th,
|
|
96
|
+
td {
|
|
97
|
+
text-align: left;
|
|
98
|
+
padding: 12px 8px;
|
|
99
|
+
border-bottom: 1px solid #e1e9f5;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.flash-message {
|
|
103
|
+
background: #dff6e7;
|
|
104
|
+
border: 1px solid #9dd5ae;
|
|
105
|
+
color: #174f2c;
|
|
106
|
+
padding: 12px 14px;
|
|
107
|
+
border-radius: 14px;
|
|
108
|
+
margin-bottom: 16px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.login-shell {
|
|
112
|
+
min-height: 100vh;
|
|
113
|
+
display: grid;
|
|
114
|
+
place-items: center;
|
|
115
|
+
padding: 24px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.login-card {
|
|
119
|
+
width: min(420px, 100%);
|
|
120
|
+
}
|