generator-codedesignplus 0.6.5 → 0.7.0
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.
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# CodeDesignPlus Playwright Generator
|
|
2
|
+
|
|
3
|
+
Yeoman sub-generator that scaffolds a complete Playwright E2E test project for a CodeDesignPlus .NET microservice.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g yo generator-codedesignplus
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
yo codedesignplus:playwright --name <service-name> [options]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Options
|
|
18
|
+
|
|
19
|
+
| Option | Alias | Required | Description |
|
|
20
|
+
|--------|-------|----------|-------------|
|
|
21
|
+
| `--name` | `-n` | Yes | Microservice name (e.g., `invoicing`, `tenants`) |
|
|
22
|
+
| `--grpc` | `-g` | No | Include gRPC testing support |
|
|
23
|
+
| `--resources` | `-r` | No | Resources with DELETE endpoints for API cleanup |
|
|
24
|
+
| `--aggregates` | `-a` | No | Aggregates for MongoDB cleanup in global-teardown |
|
|
25
|
+
|
|
26
|
+
### Examples
|
|
27
|
+
|
|
28
|
+
**Basic service (no DELETE endpoints, uses MongoDB cleanup):**
|
|
29
|
+
```bash
|
|
30
|
+
yo codedesignplus:playwright --name invoicing --aggregates "FinancialDocumentAggregate,NumberSequenceAggregate"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Service with DELETE endpoints:**
|
|
34
|
+
```bash
|
|
35
|
+
yo codedesignplus:playwright --name phases --resources "phases:api/phases/{id},states:api/constructionstate/{id},types:api/constructiontype/{id}"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Service with gRPC:**
|
|
39
|
+
```bash
|
|
40
|
+
yo codedesignplus:playwright --name tenants --grpc --resources "tenants:api/Tenant/{id}"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Mixed cleanup (API + MongoDB fallback):**
|
|
44
|
+
```bash
|
|
45
|
+
yo codedesignplus:playwright --name accounting --resources "journals:api/Journal/{id}" --aggregates "AccountingRuleAggregate"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Generated Structure
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
ms-{name}-playwright/
|
|
52
|
+
├── src/
|
|
53
|
+
│ ├── config/
|
|
54
|
+
│ │ └── environments.ts # Staging/local/production config
|
|
55
|
+
│ ├── fixtures/
|
|
56
|
+
│ │ └── index.ts # Playwright fixtures (api, anonApi, env)
|
|
57
|
+
│ ├── helpers/
|
|
58
|
+
│ │ ├── api-paths.ts # URL path helper
|
|
59
|
+
│ │ ├── cleanup-tracker.ts # Resource cleanup via API
|
|
60
|
+
│ │ ├── settings-cache.ts # Token cache utility
|
|
61
|
+
│ │ └── test-data.ts # Test data generators
|
|
62
|
+
│ ├── proto/ # (only with --grpc)
|
|
63
|
+
│ │ └── {name}.proto
|
|
64
|
+
│ ├── types/
|
|
65
|
+
│ │ └── index.ts # Service DTOs
|
|
66
|
+
│ └── tests/
|
|
67
|
+
│ ├── suites/ # Full test suites (*.spec.ts)
|
|
68
|
+
│ │ └── {name}.spec.ts
|
|
69
|
+
│ ├── smoke/ # Quick health checks (*.smoke.ts)
|
|
70
|
+
│ │ └── {name}.smoke.ts
|
|
71
|
+
│ └── flows/ # Multi-step E2E workflows (*.flow.ts)
|
|
72
|
+
├── global-setup.ts # Auth token acquisition
|
|
73
|
+
├── global-teardown.ts # MongoDB cleanup fallback
|
|
74
|
+
├── playwright.config.ts # Projects: suites, smoke, flows
|
|
75
|
+
├── .env.local # Local environment vars
|
|
76
|
+
├── .env.staging # Staging environment vars
|
|
77
|
+
├── tsconfig.json
|
|
78
|
+
└── package.json
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## After Generation
|
|
82
|
+
|
|
83
|
+
1. Navigate to the project:
|
|
84
|
+
```bash
|
|
85
|
+
cd ms-{name}-playwright
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
2. Install dependencies:
|
|
89
|
+
```bash
|
|
90
|
+
npm install
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
3. Configure environment variables in `.env.staging`:
|
|
94
|
+
```env
|
|
95
|
+
TOKEN_URL=https://your-tenant.ciamlogin.com/.../token
|
|
96
|
+
CLIENT_SECRET=your-secret
|
|
97
|
+
TEST_USER=test-user@example.com
|
|
98
|
+
TEST_PASSWORD=password
|
|
99
|
+
TENANT_ID=your-tenant-uuid
|
|
100
|
+
MONGO_URI=mongodb://your-staging-uri
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
4. Run smoke tests:
|
|
104
|
+
```bash
|
|
105
|
+
npm run test:smoke:staging
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Available Scripts
|
|
109
|
+
|
|
110
|
+
| Script | Description |
|
|
111
|
+
|--------|-------------|
|
|
112
|
+
| `npm test` | Run all tests |
|
|
113
|
+
| `npm run test:suites` | Run suite tests only |
|
|
114
|
+
| `npm run test:smoke` | Run smoke tests only |
|
|
115
|
+
| `npm run test:flows` | Run flow tests only |
|
|
116
|
+
| `npm run test:staging` | Run all against staging |
|
|
117
|
+
| `npm run test:local` | Run all against local |
|
|
118
|
+
| `npm run test:smoke:staging` | Smoke tests against staging |
|
|
119
|
+
| `npm run test:ui:staging` | Interactive UI mode (staging) |
|
|
120
|
+
| `npm run test:ci` | CI mode with JUnit reporter |
|
|
121
|
+
| `npm run report` | Open HTML report |
|
|
122
|
+
|
|
123
|
+
## Cleanup Strategies
|
|
124
|
+
|
|
125
|
+
### API-based (via `--resources`)
|
|
126
|
+
Resources are tracked during tests and deleted via REST API after each test.
|
|
127
|
+
|
|
128
|
+
### MongoDB-based (via `--aggregates`)
|
|
129
|
+
Documents matching `playwright-test-*` pattern are deleted in global-teardown after all tests complete.
|
|
130
|
+
|
|
131
|
+
### Best Practice
|
|
132
|
+
- Use API cleanup for resources with DELETE endpoints (faster, more reliable)
|
|
133
|
+
- Use MongoDB cleanup as fallback for services without DELETE endpoints
|
|
134
|
+
- Always prefix test data names with `playwright-test-` for identification
|
|
135
|
+
|
|
136
|
+
## Dependencies
|
|
137
|
+
|
|
138
|
+
- `@codedesignplus/playwright-microservice` — shared utilities
|
|
139
|
+
- `@playwright/test` — test framework
|
|
140
|
+
- `cross-env` — cross-platform env vars
|
|
141
|
+
- `@grpc/grpc-js` + `@grpc/proto-loader` — (optional, with `--grpc`)
|
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
import Generator from 'yeoman-generator';
|
|
2
|
+
import figlet from 'figlet';
|
|
3
|
+
import boxen from 'boxen';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
export default class PlaywrightGenerator extends Generator {
|
|
7
|
+
|
|
8
|
+
constructor(args, opts) {
|
|
9
|
+
super(args, opts);
|
|
10
|
+
|
|
11
|
+
this.option('name', {
|
|
12
|
+
type: String,
|
|
13
|
+
alias: 'n',
|
|
14
|
+
required: true,
|
|
15
|
+
description: 'The microservice name (e.g., "invoicing", "tenants", "phases")'
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
this.option('grpc', {
|
|
19
|
+
type: Boolean,
|
|
20
|
+
alias: 'g',
|
|
21
|
+
default: false,
|
|
22
|
+
description: 'Include gRPC testing support'
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
this.option('resources', {
|
|
26
|
+
type: String,
|
|
27
|
+
alias: 'r',
|
|
28
|
+
default: '',
|
|
29
|
+
description: 'Comma-separated resources with DELETE endpoints for cleanup (e.g., "phases:api/phases/{id},types:api/constructiontype/{id}")'
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
this.option('aggregates', {
|
|
33
|
+
type: String,
|
|
34
|
+
alias: 'a',
|
|
35
|
+
default: '',
|
|
36
|
+
description: 'Comma-separated aggregate names for MongoDB cleanup (e.g., "FinancialDocumentAggregate,NumberSequenceAggregate")'
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
this.option('help', {
|
|
40
|
+
type: Boolean,
|
|
41
|
+
alias: 'h',
|
|
42
|
+
default: false,
|
|
43
|
+
description: 'Show help'
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
initializing() {
|
|
48
|
+
console.log(figlet.textSync('CodeDesignPlus'));
|
|
49
|
+
|
|
50
|
+
if (this.options.help) {
|
|
51
|
+
console.log(boxen(
|
|
52
|
+
'CodeDesignPlus Playwright Test Generator (v1.0)\n\n' +
|
|
53
|
+
'Generates a complete Playwright E2E test project for a CodeDesignPlus microservice.\n\n' +
|
|
54
|
+
'Usage:\n\n' +
|
|
55
|
+
' yo codedesignplus:playwright --name <service> [options]\n\n' +
|
|
56
|
+
'Options:\n\n' +
|
|
57
|
+
' --name, -n Microservice name (required)\n' +
|
|
58
|
+
' --grpc, -g Include gRPC testing support\n' +
|
|
59
|
+
' --resources, -r Resources with DELETE endpoints\n' +
|
|
60
|
+
' Format: "name:path,name:path"\n' +
|
|
61
|
+
' Example: "phases:api/phases/{id}"\n' +
|
|
62
|
+
' --aggregates, -a Aggregates for MongoDB cleanup\n' +
|
|
63
|
+
' Example: "PhaseAggregate,TypeAggregate"\n\n' +
|
|
64
|
+
'Examples:\n\n' +
|
|
65
|
+
' yo codedesignplus:playwright --name invoicing --aggregates "FinancialDocumentAggregate,NumberSequenceAggregate"\n' +
|
|
66
|
+
' yo codedesignplus:playwright --name tenants --grpc --resources "tenants:api/Tenant/{id}"\n' +
|
|
67
|
+
' yo codedesignplus:playwright -n phases -r "phases:api/phases/{id},states:api/constructionstate/{id}"\n\n',
|
|
68
|
+
{ padding: 1, margin: 1, borderStyle: 'round' }
|
|
69
|
+
));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!this.options.name) {
|
|
74
|
+
throw new Error('--name is required. Use --help for usage information.');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
writing() {
|
|
79
|
+
if (this.options.help) return;
|
|
80
|
+
|
|
81
|
+
const name = this.options.name.toLowerCase();
|
|
82
|
+
const serviceName = name.replace(/-/g, '');
|
|
83
|
+
const pascalName = this._toPascalCase(name);
|
|
84
|
+
const hasGrpc = this.options.grpc;
|
|
85
|
+
const resources = this._parseResources(this.options.resources);
|
|
86
|
+
const aggregates = this.options.aggregates
|
|
87
|
+
? this.options.aggregates.split(',').map(a => a.trim()).filter(Boolean)
|
|
88
|
+
: [];
|
|
89
|
+
const hasMongoCleanup = aggregates.length > 0;
|
|
90
|
+
const hasApiCleanup = resources.length > 0;
|
|
91
|
+
|
|
92
|
+
const destRoot = `ms-${name}-playwright`;
|
|
93
|
+
const dest = (p) => this.destinationPath(path.join(destRoot, p));
|
|
94
|
+
|
|
95
|
+
const ctx = {
|
|
96
|
+
name,
|
|
97
|
+
serviceName,
|
|
98
|
+
pascalName,
|
|
99
|
+
hasGrpc,
|
|
100
|
+
hasApiCleanup,
|
|
101
|
+
hasMongoCleanup,
|
|
102
|
+
resources,
|
|
103
|
+
aggregates,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// package.json
|
|
107
|
+
this.fs.writeJSON(dest('package.json'), this._generatePackageJson(ctx));
|
|
108
|
+
|
|
109
|
+
// tsconfig.json
|
|
110
|
+
this.fs.writeJSON(dest('tsconfig.json'), {
|
|
111
|
+
compilerOptions: {
|
|
112
|
+
target: 'ES2022',
|
|
113
|
+
module: 'ESNext',
|
|
114
|
+
moduleResolution: 'bundler',
|
|
115
|
+
lib: ['ES2022'],
|
|
116
|
+
strict: true,
|
|
117
|
+
esModuleInterop: true,
|
|
118
|
+
skipLibCheck: true,
|
|
119
|
+
forceConsistentCasingInFileNames: true,
|
|
120
|
+
resolveJsonModule: true,
|
|
121
|
+
outDir: './dist',
|
|
122
|
+
rootDir: '.',
|
|
123
|
+
},
|
|
124
|
+
include: ['src/**/*', 'global-setup.ts', 'global-teardown.ts', 'playwright.config.ts'],
|
|
125
|
+
exclude: ['node_modules', 'dist', 'reports'],
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// playwright.config.ts
|
|
129
|
+
this.fs.write(dest('playwright.config.ts'), this._generatePlaywrightConfig(ctx));
|
|
130
|
+
|
|
131
|
+
// global-setup.ts
|
|
132
|
+
this.fs.write(dest('global-setup.ts'), this._generateGlobalSetup(ctx));
|
|
133
|
+
|
|
134
|
+
// global-teardown.ts
|
|
135
|
+
this.fs.write(dest('global-teardown.ts'), this._generateGlobalTeardown(ctx));
|
|
136
|
+
|
|
137
|
+
// .env.local
|
|
138
|
+
this.fs.write(dest('.env.local'), this._generateEnvLocal(ctx));
|
|
139
|
+
|
|
140
|
+
// .env.staging
|
|
141
|
+
this.fs.write(dest('.env.staging'), this._generateEnvStaging(ctx));
|
|
142
|
+
|
|
143
|
+
// .gitignore
|
|
144
|
+
this.fs.write(dest('.gitignore'), [
|
|
145
|
+
'node_modules/',
|
|
146
|
+
'dist/',
|
|
147
|
+
'reports/',
|
|
148
|
+
'.settings-cache.json',
|
|
149
|
+
'*.env',
|
|
150
|
+
'!.env.local',
|
|
151
|
+
'!.env.staging',
|
|
152
|
+
].join('\n'));
|
|
153
|
+
|
|
154
|
+
// src/config/environments.ts
|
|
155
|
+
this.fs.write(dest('src/config/environments.ts'), this._generateEnvironments(ctx));
|
|
156
|
+
|
|
157
|
+
// src/fixtures/index.ts
|
|
158
|
+
this.fs.write(dest('src/fixtures/index.ts'), this._generateFixtures(ctx));
|
|
159
|
+
|
|
160
|
+
// src/helpers/api-paths.ts
|
|
161
|
+
this.fs.write(dest('src/helpers/api-paths.ts'), `import { apiPath } from '@codedesignplus/playwright-microservice';\n\nexport { apiPath };\n`);
|
|
162
|
+
|
|
163
|
+
// src/helpers/cleanup-tracker.ts
|
|
164
|
+
this.fs.write(dest('src/helpers/cleanup-tracker.ts'), this._generateCleanupTracker(ctx));
|
|
165
|
+
|
|
166
|
+
// src/helpers/settings-cache.ts
|
|
167
|
+
this.fs.write(dest('src/helpers/settings-cache.ts'), `export { readSettingsCache, isTokenExpired } from '@codedesignplus/playwright-microservice';\n`);
|
|
168
|
+
|
|
169
|
+
// src/helpers/test-data.ts
|
|
170
|
+
this.fs.write(dest('src/helpers/test-data.ts'), this._generateTestData(ctx));
|
|
171
|
+
|
|
172
|
+
// src/types/index.ts
|
|
173
|
+
this.fs.write(dest('src/types/index.ts'), this._generateTypes(ctx));
|
|
174
|
+
|
|
175
|
+
// src/tests/suites/example.spec.ts
|
|
176
|
+
this.fs.write(dest(`src/tests/suites/${name}.spec.ts`), this._generateSuiteSpec(ctx));
|
|
177
|
+
|
|
178
|
+
// src/tests/smoke/example.smoke.ts
|
|
179
|
+
this.fs.write(dest(`src/tests/smoke/${name}.smoke.ts`), this._generateSmokeSpec(ctx));
|
|
180
|
+
|
|
181
|
+
// src/tests/flows/.gitkeep
|
|
182
|
+
this.fs.write(dest('src/tests/flows/.gitkeep'), '');
|
|
183
|
+
|
|
184
|
+
// gRPC files
|
|
185
|
+
if (hasGrpc) {
|
|
186
|
+
this.fs.write(dest('src/helpers/grpc-client.ts'), this._generateGrpcHelper(ctx));
|
|
187
|
+
this.fs.write(dest(`src/proto/${name}.proto`), this._generateProto(ctx));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.log(`\n✅ Project ms-${name}-playwright generated successfully!`);
|
|
191
|
+
console.log(`\nNext steps:`);
|
|
192
|
+
console.log(` cd ${destRoot}`);
|
|
193
|
+
console.log(` npm install`);
|
|
194
|
+
console.log(` # Configure .env.local and .env.staging`);
|
|
195
|
+
console.log(` npm run test:smoke:staging`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ─── Generators ──────────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
_generatePackageJson(ctx) {
|
|
201
|
+
const deps = {
|
|
202
|
+
'@codedesignplus/playwright-microservice': '^1.0.0',
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
if (ctx.hasGrpc) {
|
|
206
|
+
deps['@grpc/grpc-js'] = '^1.10.0';
|
|
207
|
+
deps['@grpc/proto-loader'] = '^0.7.0';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
name: `ms-${ctx.name}-playwright`,
|
|
212
|
+
version: '1.0.0',
|
|
213
|
+
private: true,
|
|
214
|
+
type: 'module',
|
|
215
|
+
scripts: {
|
|
216
|
+
test: 'playwright test',
|
|
217
|
+
'test:ui': 'playwright test --ui',
|
|
218
|
+
'test:ui:local': 'cross-env ENV=local playwright test --ui',
|
|
219
|
+
'test:ui:staging': 'cross-env ENV=staging playwright test --ui',
|
|
220
|
+
'test:debug': 'playwright test --debug',
|
|
221
|
+
'test:suites': 'playwright test --project=suites',
|
|
222
|
+
'test:smoke': 'playwright test --project=smoke',
|
|
223
|
+
'test:flows': 'playwright test --project=flows',
|
|
224
|
+
'test:staging': 'cross-env ENV=staging playwright test',
|
|
225
|
+
'test:local': 'cross-env ENV=local playwright test',
|
|
226
|
+
'test:smoke:staging': 'cross-env ENV=staging playwright test --project=smoke',
|
|
227
|
+
'test:smoke:local': 'cross-env ENV=local playwright test --project=smoke',
|
|
228
|
+
'test:ci': 'cross-env ENV=staging playwright test --reporter=junit',
|
|
229
|
+
report: 'playwright show-report reports/html',
|
|
230
|
+
},
|
|
231
|
+
dependencies: deps,
|
|
232
|
+
devDependencies: {
|
|
233
|
+
'@playwright/test': '^1.44.0',
|
|
234
|
+
'@types/node': '^20.0.0',
|
|
235
|
+
'cross-env': '^7.0.3',
|
|
236
|
+
typescript: '^5.4.0',
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
_generatePlaywrightConfig(ctx) {
|
|
242
|
+
return `import { defineConfig } from '@playwright/test';
|
|
243
|
+
|
|
244
|
+
export default defineConfig({
|
|
245
|
+
testDir: './src/tests',
|
|
246
|
+
testMatch: ['**/*.spec.ts', '**/*.flow.ts', '**/*.smoke.ts'],
|
|
247
|
+
timeout: 45_000,
|
|
248
|
+
expect: { timeout: 10_000 },
|
|
249
|
+
retries: process.env.CI ? 1 : 0,
|
|
250
|
+
workers: 1,
|
|
251
|
+
globalSetup: './global-setup.ts',
|
|
252
|
+
globalTeardown: './global-teardown.ts',
|
|
253
|
+
reporter: [
|
|
254
|
+
['list'],
|
|
255
|
+
['html', { outputFolder: 'reports/html', open: 'never' }],
|
|
256
|
+
['junit', { outputFile: 'reports/junit.xml' }],
|
|
257
|
+
],
|
|
258
|
+
use: {
|
|
259
|
+
extraHTTPHeaders: {
|
|
260
|
+
'Content-Type': 'application/json',
|
|
261
|
+
Accept: 'application/json',
|
|
262
|
+
},
|
|
263
|
+
trace: 'on-first-retry',
|
|
264
|
+
ignoreHTTPSErrors: true,
|
|
265
|
+
},
|
|
266
|
+
projects: [
|
|
267
|
+
{
|
|
268
|
+
name: 'suites',
|
|
269
|
+
testMatch: '**/suites/**/*.spec.ts',
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: 'smoke',
|
|
273
|
+
testMatch: '**/smoke/**/*.smoke.ts',
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
name: 'flows',
|
|
277
|
+
testMatch: '**/flows/**/*.flow.ts',
|
|
278
|
+
use: { actionTimeout: 25_000 },
|
|
279
|
+
},
|
|
280
|
+
],
|
|
281
|
+
});
|
|
282
|
+
`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
_generateGlobalSetup(ctx) {
|
|
286
|
+
return `import { createAuthSetup } from '@codedesignplus/playwright-microservice';
|
|
287
|
+
|
|
288
|
+
export default createAuthSetup();
|
|
289
|
+
`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
_generateGlobalTeardown(ctx) {
|
|
293
|
+
if (!ctx.hasMongoCleanup) {
|
|
294
|
+
return `export default async function globalTeardown() {
|
|
295
|
+
// No MongoDB cleanup configured.
|
|
296
|
+
// Add MongoCleanup here if the service lacks DELETE endpoints.
|
|
297
|
+
}
|
|
298
|
+
`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const collectionsArr = ctx.aggregates.map(a => `'${a}'`).join(', ');
|
|
302
|
+
|
|
303
|
+
return `import { MongoCleanup } from '@codedesignplus/playwright-microservice';
|
|
304
|
+
|
|
305
|
+
export default async function globalTeardown() {
|
|
306
|
+
const cleaner = MongoCleanup.fromEnv('${ctx.serviceName}');
|
|
307
|
+
|
|
308
|
+
await cleaner.cleanupByPattern(
|
|
309
|
+
[${collectionsArr}],
|
|
310
|
+
'Name',
|
|
311
|
+
/^playwright-test-/
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
_generateEnvLocal(ctx) {
|
|
318
|
+
return `ENV=local
|
|
319
|
+
TOKEN_URL=
|
|
320
|
+
CLIENT_SECRET=
|
|
321
|
+
TEST_USER=
|
|
322
|
+
TEST_PASSWORD=
|
|
323
|
+
TENANT_ID=
|
|
324
|
+
MONGO_URI=mongodb://localhost:27017
|
|
325
|
+
`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
_generateEnvStaging(ctx) {
|
|
329
|
+
return `ENV=staging
|
|
330
|
+
TOKEN_URL=
|
|
331
|
+
CLIENT_SECRET=
|
|
332
|
+
TEST_USER=
|
|
333
|
+
TEST_PASSWORD=
|
|
334
|
+
TENANT_ID=
|
|
335
|
+
MONGO_URI=
|
|
336
|
+
`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
_generateEnvironments(ctx) {
|
|
340
|
+
let grpcFields = '';
|
|
341
|
+
let grpcConfig = '';
|
|
342
|
+
|
|
343
|
+
if (ctx.hasGrpc) {
|
|
344
|
+
grpcFields = `\n grpcUrl: string;\n grpcSecure: boolean;`;
|
|
345
|
+
grpcConfig = `
|
|
346
|
+
grpcUrl: envName === 'staging'
|
|
347
|
+
? 'ms-${ctx.name}-grpc.kappali.svc.cluster.local:5001'
|
|
348
|
+
: 'localhost:5001',
|
|
349
|
+
grpcSecure: envName === 'staging',`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return `import { readSettingsCache, EnvName } from '@codedesignplus/playwright-microservice';
|
|
353
|
+
|
|
354
|
+
export interface EnvConfig {
|
|
355
|
+
name: EnvName;
|
|
356
|
+
baseUrl: string;
|
|
357
|
+
apiPath: string;
|
|
358
|
+
authToken: string;
|
|
359
|
+
tenantId: string;${grpcFields}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function getConfig(): EnvConfig {
|
|
363
|
+
const envName = (process.env.ENV ?? 'local') as EnvName;
|
|
364
|
+
|
|
365
|
+
const baseUrl = envName === 'staging'
|
|
366
|
+
? 'https://services.kappali.com'
|
|
367
|
+
: 'http://localhost:5000';
|
|
368
|
+
|
|
369
|
+
const apiPath = envName === 'staging' ? '/ms-${ctx.name}' : '';
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
name: envName,
|
|
373
|
+
baseUrl,
|
|
374
|
+
apiPath,
|
|
375
|
+
authToken: readSettingsCache()?.accessToken ?? process.env.AUTH_TOKEN ?? '',
|
|
376
|
+
tenantId: process.env.TENANT_ID ?? '',${grpcConfig}
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
`;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
_generateFixtures(ctx) {
|
|
383
|
+
let imports = `import { test as base, APIRequestContext, request } from '@playwright/test';
|
|
384
|
+
import { getConfig, EnvConfig } from '../config/environments';
|
|
385
|
+
import { cleanupTracker } from '../helpers/cleanup-tracker';`;
|
|
386
|
+
|
|
387
|
+
let grpcFixture = '';
|
|
388
|
+
let grpcInterface = '';
|
|
389
|
+
|
|
390
|
+
if (ctx.hasGrpc) {
|
|
391
|
+
imports += `\nimport { createGrpcClient, GrpcClient } from '../helpers/grpc-client';`;
|
|
392
|
+
grpcInterface = `\n grpc: GrpcClient;`;
|
|
393
|
+
grpcFixture = `
|
|
394
|
+
grpc: async ({ env }, use) => {
|
|
395
|
+
const client = createGrpcClient(env.authToken);
|
|
396
|
+
await use(client);
|
|
397
|
+
},
|
|
398
|
+
`;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return `${imports}
|
|
402
|
+
|
|
403
|
+
interface TestFixtures {
|
|
404
|
+
env: EnvConfig;
|
|
405
|
+
api: APIRequestContext;
|
|
406
|
+
anonApi: APIRequestContext;${grpcInterface}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export const test = base.extend<TestFixtures>({
|
|
410
|
+
env: async ({}, use) => {
|
|
411
|
+
await use(getConfig());
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
api: async ({ env }, use) => {
|
|
415
|
+
const ctx = await request.newContext({
|
|
416
|
+
baseURL: \`\${env.baseUrl}\${env.apiPath}/\`,
|
|
417
|
+
ignoreHTTPSErrors: true,
|
|
418
|
+
extraHTTPHeaders: {
|
|
419
|
+
'Content-Type': 'application/json',
|
|
420
|
+
Accept: 'application/json',
|
|
421
|
+
...(env.authToken ? { Authorization: \`Bearer \${env.authToken}\` } : {}),
|
|
422
|
+
...(env.tenantId ? { 'X-Tenant': env.tenantId } : {}),
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
await use(ctx);
|
|
427
|
+
await cleanupTracker.cleanupAll(ctx);
|
|
428
|
+
await ctx.dispose();
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
anonApi: async ({ env }, use) => {
|
|
432
|
+
const ctx = await request.newContext({
|
|
433
|
+
baseURL: \`\${env.baseUrl}\${env.apiPath}/\`,
|
|
434
|
+
ignoreHTTPSErrors: true,
|
|
435
|
+
extraHTTPHeaders: {
|
|
436
|
+
'Content-Type': 'application/json',
|
|
437
|
+
Accept: 'application/json',
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
await use(ctx);
|
|
441
|
+
await ctx.dispose();
|
|
442
|
+
},
|
|
443
|
+
${grpcFixture}});
|
|
444
|
+
|
|
445
|
+
export { expect } from '@playwright/test';
|
|
446
|
+
`;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
_generateCleanupTracker(ctx) {
|
|
450
|
+
if (ctx.hasApiCleanup) {
|
|
451
|
+
const entries = ctx.resources
|
|
452
|
+
.map(r => ` ${r.name}: { path: '${r.path}' }`)
|
|
453
|
+
.join(',\n');
|
|
454
|
+
|
|
455
|
+
return `import { CleanupTracker } from '@codedesignplus/playwright-microservice';
|
|
456
|
+
|
|
457
|
+
export const cleanupTracker = new CleanupTracker({
|
|
458
|
+
${entries},
|
|
459
|
+
});
|
|
460
|
+
`;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return `import { CleanupTracker } from '@codedesignplus/playwright-microservice';
|
|
464
|
+
|
|
465
|
+
// Define resources that have DELETE endpoints for API-based cleanup.
|
|
466
|
+
// Format: { resourceName: { path: 'api/Resource/{id}', method?: 'DELETE' | 'PATCH' } }
|
|
467
|
+
export const cleanupTracker = new CleanupTracker({});
|
|
468
|
+
`;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
_generateTestData(ctx) {
|
|
472
|
+
return `import { cleanupTracker } from './cleanup-tracker';
|
|
473
|
+
|
|
474
|
+
// Generate test data for ${ctx.pascalName} resources.
|
|
475
|
+
// Always prefix names with 'playwright-test-' for MongoDB cleanup identification.
|
|
476
|
+
|
|
477
|
+
export function generate${ctx.pascalName}Data(overrides: Record<string, unknown> = {}) {
|
|
478
|
+
const id = crypto.randomUUID();
|
|
479
|
+
const timestamp = Date.now();
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
id,
|
|
483
|
+
name: \`playwright-test-\${timestamp}\`,
|
|
484
|
+
isActive: true,
|
|
485
|
+
...overrides,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
_generateTypes(ctx) {
|
|
492
|
+
return `export { PaginationResponse, ProblemDetails } from '@codedesignplus/playwright-microservice';
|
|
493
|
+
|
|
494
|
+
// Add your service-specific DTOs below:
|
|
495
|
+
|
|
496
|
+
export interface ${ctx.pascalName}Dto {
|
|
497
|
+
id: string;
|
|
498
|
+
name: string;
|
|
499
|
+
isActive: boolean;
|
|
500
|
+
}
|
|
501
|
+
`;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
_generateSuiteSpec(ctx) {
|
|
505
|
+
return `import { test, expect } from '../../fixtures';
|
|
506
|
+
import { apiPath } from '../../helpers/api-paths';
|
|
507
|
+
import { generate${ctx.pascalName}Data } from '../../helpers/test-data';
|
|
508
|
+
import { PaginationResponse, ProblemDetails, ${ctx.pascalName}Dto } from '../../types';
|
|
509
|
+
|
|
510
|
+
test.describe('${ctx.pascalName} - REST API', () => {
|
|
511
|
+
test('GET /api/${ctx.pascalName} -> 401 without auth', async ({ anonApi }) => {
|
|
512
|
+
const res = await anonApi.get(apiPath('api/${ctx.pascalName}'));
|
|
513
|
+
expect(res.status()).toBe(401);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test('GET /api/${ctx.pascalName} -> 200 with auth', async ({ api }) => {
|
|
517
|
+
const res = await api.get(apiPath('api/${ctx.pascalName}'));
|
|
518
|
+
expect(res.status()).toBe(200);
|
|
519
|
+
|
|
520
|
+
const body: PaginationResponse<${ctx.pascalName}Dto> = await res.json();
|
|
521
|
+
expect(body).toHaveProperty('data');
|
|
522
|
+
expect(body).toHaveProperty('totalCount');
|
|
523
|
+
expect(Array.isArray(body.data)).toBe(true);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test('GET /api/${ctx.pascalName}/{id} -> 401 without auth', async ({ anonApi }) => {
|
|
527
|
+
const fakeId = crypto.randomUUID();
|
|
528
|
+
const res = await anonApi.get(apiPath(\`api/${ctx.pascalName}/\${fakeId}\`));
|
|
529
|
+
expect(res.status()).toBe(401);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
test('GET /api/${ctx.pascalName}/{id} -> 400 when not found', async ({ api }) => {
|
|
533
|
+
const fakeId = crypto.randomUUID();
|
|
534
|
+
const res = await api.get(apiPath(\`api/${ctx.pascalName}/\${fakeId}\`));
|
|
535
|
+
expect(res.status()).toBe(400);
|
|
536
|
+
|
|
537
|
+
const problem: ProblemDetails = await res.json();
|
|
538
|
+
expect(problem.status).toBe(400);
|
|
539
|
+
expect(problem.title).toBeTruthy();
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test('POST /api/${ctx.pascalName} -> 204 creates resource', async ({ api }) => {
|
|
543
|
+
const data = generate${ctx.pascalName}Data();
|
|
544
|
+
const res = await api.post(apiPath('api/${ctx.pascalName}'), { data });
|
|
545
|
+
expect(res.status()).toBe(204);
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
test('POST /api/${ctx.pascalName} -> 401 without auth', async ({ anonApi }) => {
|
|
549
|
+
const data = generate${ctx.pascalName}Data();
|
|
550
|
+
const res = await anonApi.post(apiPath('api/${ctx.pascalName}'), { data });
|
|
551
|
+
expect(res.status()).toBe(401);
|
|
552
|
+
});
|
|
553
|
+
});
|
|
554
|
+
`;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
_generateSmokeSpec(ctx) {
|
|
558
|
+
return `import { test, expect } from '../../fixtures';
|
|
559
|
+
import { apiPath } from '../../helpers/api-paths';
|
|
560
|
+
|
|
561
|
+
test.describe('${ctx.pascalName} - Smoke Tests', () => {
|
|
562
|
+
test('GET /api/${ctx.pascalName} -> 401 without auth', async ({ anonApi }) => {
|
|
563
|
+
const res = await anonApi.get(apiPath('api/${ctx.pascalName}'));
|
|
564
|
+
expect(res.status()).toBe(401);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
test('GET /api/${ctx.pascalName} -> 200 with auth', async ({ api }) => {
|
|
568
|
+
const res = await api.get(apiPath('api/${ctx.pascalName}'));
|
|
569
|
+
expect(res.status()).toBe(200);
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
`;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
_generateGrpcHelper(ctx) {
|
|
576
|
+
return `import { createGrpcClient as createClient, promisifyGrpc, GrpcClient } from '@codedesignplus/playwright-microservice';
|
|
577
|
+
import { join } from 'path';
|
|
578
|
+
import { getConfig } from '../config/environments';
|
|
579
|
+
|
|
580
|
+
const PROTO_PATH = join(__dirname, '../proto/${ctx.name}.proto');
|
|
581
|
+
|
|
582
|
+
export type { GrpcClient };
|
|
583
|
+
|
|
584
|
+
export function createGrpcClient(authToken: string): GrpcClient {
|
|
585
|
+
const config = getConfig();
|
|
586
|
+
|
|
587
|
+
return createClient({
|
|
588
|
+
protoPath: PROTO_PATH,
|
|
589
|
+
packageName: '${ctx.pascalName}',
|
|
590
|
+
serviceName: '${ctx.pascalName}',
|
|
591
|
+
url: config.grpcUrl!,
|
|
592
|
+
secure: config.grpcSecure!,
|
|
593
|
+
authToken,
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export { promisifyGrpc };
|
|
598
|
+
`;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
_generateProto(ctx) {
|
|
602
|
+
return `syntax = "proto3";
|
|
603
|
+
|
|
604
|
+
package ${ctx.pascalName};
|
|
605
|
+
|
|
606
|
+
option csharp_namespace = "CodeDesignPlus.Net.Microservice.${ctx.pascalName}.gRpc.Protos";
|
|
607
|
+
|
|
608
|
+
import "google/protobuf/empty.proto";
|
|
609
|
+
import "google/protobuf/wrappers.proto";
|
|
610
|
+
|
|
611
|
+
// TODO: Define your gRPC service and messages
|
|
612
|
+
service ${ctx.pascalName} {
|
|
613
|
+
rpc Get${ctx.pascalName} (Get${ctx.pascalName}Request) returns (Get${ctx.pascalName}Response);
|
|
614
|
+
rpc Create${ctx.pascalName} (Create${ctx.pascalName}Request) returns (google.protobuf.Empty);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
message Get${ctx.pascalName}Request {
|
|
618
|
+
string Id = 1;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
message Get${ctx.pascalName}Response {
|
|
622
|
+
string id = 1;
|
|
623
|
+
string name = 2;
|
|
624
|
+
bool isActive = 3;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
message Create${ctx.pascalName}Request {
|
|
628
|
+
string id = 1;
|
|
629
|
+
string name = 2;
|
|
630
|
+
}
|
|
631
|
+
`;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
635
|
+
|
|
636
|
+
_parseResources(resourcesStr) {
|
|
637
|
+
if (!resourcesStr) return [];
|
|
638
|
+
return resourcesStr.split(',').map(r => {
|
|
639
|
+
const [name, path] = r.trim().split(':');
|
|
640
|
+
return { name: name.trim(), path: path.trim() };
|
|
641
|
+
}).filter(r => r.name && r.path);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
_toPascalCase(str) {
|
|
645
|
+
return str
|
|
646
|
+
.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
|
|
647
|
+
.map(x => x.charAt(0).toUpperCase() + x.slice(1).toLowerCase())
|
|
648
|
+
.join('');
|
|
649
|
+
}
|
|
650
|
+
}
|
package/package.json
CHANGED