@zohodesk/testinglibrary 0.0.44-n20-experimental → 0.0.46-n20-experimental
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/AUTO_CLEANUP_PLAN.md +332 -0
- package/build/common/data-generator/steps/DataGenerator.spec.js +8 -8
- package/build/common/data-generator/steps/DataGeneratorStepsHelper.js +12 -3
- package/build/core/dataGenerator/CleanupTracker.js +29 -0
- package/build/core/dataGenerator/DataCleanup.js +98 -0
- package/build/core/dataGenerator/DataGenerator.js +64 -17
- package/build/core/dataGenerator/validateGenerators.js +26 -24
- package/build/core/playwright/builtInFixtures/cacheLayer.js +14 -0
- package/build/core/playwright/readConfigFile.js +2 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# Data Generator System — Design & Constraints
|
|
2
|
+
|
|
3
|
+
## Data Generator Overview
|
|
4
|
+
|
|
5
|
+
### How It Works
|
|
6
|
+
- BDD step: `Given generate a "{Type}" entity "{name}" with generator "{GeneratorName}"`
|
|
7
|
+
- Framework resolves generator template from `data-generators/*.json` files
|
|
8
|
+
- Walks up directory tree from feature file to find `data-generators/` folder
|
|
9
|
+
- Scans ALL `.json` files within (not hardcoded to `generators.json`)
|
|
10
|
+
- Generator names must be **unique across the entire UAT suite**
|
|
11
|
+
- `ZDTestingFramework validate` checks for duplicate generator names before test run
|
|
12
|
+
|
|
13
|
+
### Generator JSON Structure
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"generators": {
|
|
17
|
+
"GeneratorName": [
|
|
18
|
+
{
|
|
19
|
+
"type": "dynamic",
|
|
20
|
+
"name": "stepName",
|
|
21
|
+
"generatorOperationId": "support.Module.operationName",
|
|
22
|
+
"dataPath": "$.response.body:$",
|
|
23
|
+
"params": { "key": "$previousStep.value" },
|
|
24
|
+
"cleanup": { ... }
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Constraints
|
|
32
|
+
| Constraint | Detail |
|
|
33
|
+
|---|---|
|
|
34
|
+
| **Unique generator names** | Generator names must be unique across all `data-generators/*.json` files in the UAT suite. Walk-up discovery returns the first match. |
|
|
35
|
+
| **Walk-up discovery** | Framework walks from feature file directory toward filesystem root, scanning each level for `data-generators/` folder. |
|
|
36
|
+
| **Multiple JSON files** | Any `.json` file in a `data-generators/` folder is scanned (e.g., `ticket.json`, `account.json`, `contact.json`). |
|
|
37
|
+
| **Parent serves children** | A `data-generators/` folder at a parent module level serves all sub-modules via walk-up (reduces duplication). |
|
|
38
|
+
| **DG_API_NAME matching** | In Gherkin data tables, `DG_API_NAME` column matches generator step's `name` field to inject params. |
|
|
39
|
+
| **Chained steps** | Multi-step generators use `$previousStep.value` syntax to pass output from one step to the next. |
|
|
40
|
+
| **dataPath extraction** | `dataPath` uses JSONPath to extract specific fields from DG service response (server-side). |
|
|
41
|
+
| **Cached response** | Generated data is cached via `cacheLayer.set(entityName, response.data)` for use in subsequent steps. |
|
|
42
|
+
| **Validation** | `ZDTestingFramework validate` scans all `data-generators/` directories and fails if duplicate generator names are found. |
|
|
43
|
+
|
|
44
|
+
### File Discovery Pattern
|
|
45
|
+
```
|
|
46
|
+
feature-files/MyTest.feature ← test starts here
|
|
47
|
+
↑ walks up
|
|
48
|
+
data-generators/ticket.json ← found at same level or parent
|
|
49
|
+
data-generators/account.json ← multiple files supported
|
|
50
|
+
↑ keeps walking up
|
|
51
|
+
../data-generators/shared.json ← parent level generators serve all children
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
# Auto-Cleanup Generated Test Data After Scenario
|
|
57
|
+
|
|
58
|
+
## Context
|
|
59
|
+
|
|
60
|
+
The framework generates test data (contacts, tickets, accounts, etc.) via the DG service but never cleans it up. Generated data accumulates in test portals. We need auto-cleanup that deletes all generated entities after each scenario ends.
|
|
61
|
+
|
|
62
|
+
**Approach:** Auto-cleanup all generated entities after each scenario via fixture teardown. No explicit step needed in feature files. Cleanup config is defined in the generator JSON via a `cleanup` property on each generator step.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Three Cleanup Types
|
|
67
|
+
|
|
68
|
+
### Type 1: `oas` — DG service delete (OAS supported)
|
|
69
|
+
```json
|
|
70
|
+
"cleanup": {
|
|
71
|
+
"type": "oas",
|
|
72
|
+
"operationId": "support.Contact.deleteContact",
|
|
73
|
+
"idPath": "$.id"
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Type 2: `api` — Direct REST API (OAS not supported, DELETE)
|
|
78
|
+
```json
|
|
79
|
+
"cleanup": {
|
|
80
|
+
"type": "api",
|
|
81
|
+
"method": "DELETE",
|
|
82
|
+
"apiPath": "/api/v1/contracts/{id}",
|
|
83
|
+
"idPath": "$.id"
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Type 3: `api` — Disable instead of delete (PATCH)
|
|
88
|
+
```json
|
|
89
|
+
"cleanup": {
|
|
90
|
+
"type": "api",
|
|
91
|
+
"method": "PATCH",
|
|
92
|
+
"apiPath": "/api/v1/webhooks/{id}",
|
|
93
|
+
"idPath": "$.id",
|
|
94
|
+
"body": { "isActive": false }
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### No cleanup — read-only operations
|
|
99
|
+
Steps without `cleanup` property are skipped (e.g., `getDepartments`).
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Full Generator JSON Examples
|
|
104
|
+
|
|
105
|
+
### Contact (OAS supported)
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"generators": {
|
|
109
|
+
"CreateContactRecord": [
|
|
110
|
+
{
|
|
111
|
+
"type": "dynamic",
|
|
112
|
+
"name": "CreateContact",
|
|
113
|
+
"generatorOperationId": "support.Contact.createContact",
|
|
114
|
+
"dataPath": "$.response.body:$",
|
|
115
|
+
"cleanup": {
|
|
116
|
+
"type": "oas",
|
|
117
|
+
"operationId": "support.Contact.deleteContact",
|
|
118
|
+
"idPath": "$.id"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Ticket with dependencies (OAS supported, multi-step)
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"generators": {
|
|
130
|
+
"TicketWithDepartment": [
|
|
131
|
+
{
|
|
132
|
+
"type": "dynamic",
|
|
133
|
+
"name": "departments",
|
|
134
|
+
"generatorOperationId": "support.Department.getDepartments",
|
|
135
|
+
"dataPath": "$.response.body:$.data[0].id"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"type": "dynamic",
|
|
139
|
+
"name": "products",
|
|
140
|
+
"generatorOperationId": "support.Product.createProduct",
|
|
141
|
+
"dataPath": "$.response.body:$.id",
|
|
142
|
+
"cleanup": {
|
|
143
|
+
"type": "oas",
|
|
144
|
+
"operationId": "support.Product.deleteProduct",
|
|
145
|
+
"idPath": "$.id"
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"type": "dynamic",
|
|
150
|
+
"name": "CreateTicket",
|
|
151
|
+
"generatorOperationId": "support.Ticket.createTicket",
|
|
152
|
+
"dataPath": "$.response.body:$",
|
|
153
|
+
"cleanup": {
|
|
154
|
+
"type": "oas",
|
|
155
|
+
"operationId": "support.Ticket.deleteTicket",
|
|
156
|
+
"idPath": "$.id"
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Webhook (OAS not supported — disable via REST API)
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"generators": {
|
|
168
|
+
"CreateWebhookRecord": [
|
|
169
|
+
{
|
|
170
|
+
"type": "dynamic",
|
|
171
|
+
"name": "CreateWebhook",
|
|
172
|
+
"generatorOperationId": "support.Webhook.createWebhook",
|
|
173
|
+
"dataPath": "$.response.body:$",
|
|
174
|
+
"cleanup": {
|
|
175
|
+
"type": "api",
|
|
176
|
+
"method": "PATCH",
|
|
177
|
+
"apiPath": "/api/v1/webhooks/{id}",
|
|
178
|
+
"idPath": "$.id",
|
|
179
|
+
"body": { "isActive": false }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Contract (OAS not supported — delete via REST API)
|
|
188
|
+
```json
|
|
189
|
+
{
|
|
190
|
+
"generators": {
|
|
191
|
+
"CreateContractRecord": [
|
|
192
|
+
{
|
|
193
|
+
"type": "dynamic",
|
|
194
|
+
"name": "CreateContract",
|
|
195
|
+
"generatorOperationId": "support.Contract.createContract",
|
|
196
|
+
"dataPath": "$.response.body:$",
|
|
197
|
+
"cleanup": {
|
|
198
|
+
"type": "api",
|
|
199
|
+
"method": "DELETE",
|
|
200
|
+
"apiPath": "/api/v1/contracts/{id}",
|
|
201
|
+
"idPath": "$.id"
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
]
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## How It Works
|
|
212
|
+
|
|
213
|
+
### Cleanup flow
|
|
214
|
+
```
|
|
215
|
+
Scenario ends (pass OR fail)
|
|
216
|
+
↓
|
|
217
|
+
cleanupTracker fixture teardown fires (guaranteed by Playwright)
|
|
218
|
+
↓
|
|
219
|
+
For each tracked entity (reverse order):
|
|
220
|
+
- Read cleanup config from the generator step
|
|
221
|
+
- Skip if no cleanup property
|
|
222
|
+
- Extract entity ID using cleanup.idPath from cached response
|
|
223
|
+
- Based on cleanup.type:
|
|
224
|
+
- "oas" → POST to DG service with cleanup.operationId + entity ID
|
|
225
|
+
- "api" → Direct HTTP request (cleanup.method + cleanup.apiPath with {id} replaced)
|
|
226
|
+
- Log result (success or failure — never fail the test)
|
|
227
|
+
↓
|
|
228
|
+
Clear tracker for next scenario
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Cleanup on scenario failure
|
|
232
|
+
|
|
233
|
+
Playwright fixtures guarantee that teardown code (everything after `await use()`) runs **regardless of whether the test passes or fails**. This means:
|
|
234
|
+
|
|
235
|
+
- **Scenario passes** → cleanup runs
|
|
236
|
+
- **Scenario fails** → cleanup still runs
|
|
237
|
+
- **Individual cleanup step fails** → remaining cleanup steps still run (each step is wrapped in try/catch)
|
|
238
|
+
|
|
239
|
+
This is a core Playwright design principle — fixtures always clean up after themselves.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Files to Modify/Create
|
|
244
|
+
|
|
245
|
+
### 1. New: `src/core/dataGenerator/CleanupTracker.js`
|
|
246
|
+
|
|
247
|
+
Tracks generated entities for cleanup:
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
class CleanupTracker {
|
|
251
|
+
#entries = [];
|
|
252
|
+
|
|
253
|
+
track({ entityName, generators, cachedData, actorInfo }) {
|
|
254
|
+
this.#entries.push({ entityName, generators, cachedData, actorInfo });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
getEntries() { return [...this.#entries]; }
|
|
258
|
+
clear() { this.#entries = []; }
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### 2. New: `src/core/dataGenerator/DataCleanup.js`
|
|
263
|
+
|
|
264
|
+
Performs the actual deletion with two strategies:
|
|
265
|
+
|
|
266
|
+
```js
|
|
267
|
+
class DataCleanup {
|
|
268
|
+
async cleanup(entries) {
|
|
269
|
+
for (const entry of entries.reverse()) {
|
|
270
|
+
for (const step of [...entry.generators].reverse()) {
|
|
271
|
+
if (!step.cleanup) continue;
|
|
272
|
+
try {
|
|
273
|
+
const entityId = this.#extractId(entry.cachedData, step.cleanup.idPath);
|
|
274
|
+
|
|
275
|
+
if (step.cleanup.type === 'oas') {
|
|
276
|
+
await this.#cleanupViaOAS(step.cleanup.operationId, entityId, entry.actorInfo);
|
|
277
|
+
} else if (step.cleanup.type === 'api') {
|
|
278
|
+
await this.#cleanupViaAPI(step.cleanup, entityId, entry.actorInfo);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
Logger.log(Logger.SUCCESS_TYPE, `Cleanup: ${step.cleanup.method || 'delete'} ${step.name} (${entityId})`);
|
|
282
|
+
} catch (err) {
|
|
283
|
+
Logger.log(Logger.INFO_TYPE, `Cleanup warning for ${step.name}: ${err.message}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async #cleanupViaOAS(operationId, entityId, actorInfo) {
|
|
290
|
+
// POST to DG service with delete operationId + entity ID as param
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async #cleanupViaAPI(cleanupConfig, entityId, actorInfo) {
|
|
294
|
+
// Direct HTTP call: cleanupConfig.method + cleanupConfig.apiPath.replace('{id}', entityId)
|
|
295
|
+
// If cleanupConfig.body exists, send as request body (for PATCH/disable)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Key behaviors:
|
|
301
|
+
- Delete in **reverse order** (dependent entities first)
|
|
302
|
+
- `type: "oas"` → POST to DG service (same auth/env as generation)
|
|
303
|
+
- `type: "api"` → Direct REST call (supports DELETE, PATCH, any HTTP method)
|
|
304
|
+
- `body` field for PATCH operations (e.g., `{ "isActive": false }`)
|
|
305
|
+
- `{id}` placeholder in `apiPath` replaced with extracted entity ID
|
|
306
|
+
- **Never fail the test** — log warnings only
|
|
307
|
+
|
|
308
|
+
### 3. Modify: `src/core/playwright/builtInFixtures/cacheLayer.js`
|
|
309
|
+
|
|
310
|
+
Add `cleanupTracker` fixture. Code after `await use()` runs after scenario ends:
|
|
311
|
+
|
|
312
|
+
### 4. Modify: `src/core/dataGenerator/DataGenerator.js`
|
|
313
|
+
|
|
314
|
+
Return generator steps alongside response: `return { response, generators }`
|
|
315
|
+
|
|
316
|
+
### 5. Modify: `src/common/data-generator/steps/DataGeneratorStepsHelper.js`
|
|
317
|
+
|
|
318
|
+
Track each generated entity with its generators and actorInfo for cleanup.
|
|
319
|
+
|
|
320
|
+
### 6. Modify: `src/common/data-generator/steps/DataGenerator.spec.js`
|
|
321
|
+
|
|
322
|
+
Pass `cleanupTracker` fixture to all 4 step definitions.
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Verification
|
|
327
|
+
|
|
328
|
+
1. `npm run build` to confirm compilation
|
|
329
|
+
2. Add `cleanup` config to generator JSONs (contact, ticket, webhook)
|
|
330
|
+
3. Run a scenario → verify cleanup logs show correct operation after scenario ends
|
|
331
|
+
4. Verify PATCH operations send the body (webhook disable)
|
|
332
|
+
5. Verify reverse order (ticket deleted before product)
|
|
@@ -3,17 +3,17 @@ import { generateAndCacheTestData } from './DataGeneratorStepsHelper';
|
|
|
3
3
|
|
|
4
4
|
const { Given } = createBdd();
|
|
5
5
|
|
|
6
|
-
Given('generate a {string} entity {string} with generator {string}', async ({ page, context, i18N, cacheLayer, executionContext}, module, entityName, generatorName, dataTable) => {
|
|
7
|
-
await generateAndCacheTestData(executionContext, "template", generatorName, dataTable, cacheLayer, entityName);
|
|
6
|
+
Given('generate a {string} entity {string} with generator {string}', async ({ page, context, i18N, cacheLayer, cleanupTracker, executionContext}, module, entityName, generatorName, dataTable) => {
|
|
7
|
+
await generateAndCacheTestData(executionContext, "template", generatorName, dataTable, cacheLayer, entityName, null, cleanupTracker);
|
|
8
8
|
});
|
|
9
9
|
|
|
10
|
-
Given('generate a {string} entity {string} with API {string}', async ({ page, context, i18N, cacheLayer, executionContext}, module, entityName, operationId, dataTable) => {
|
|
11
|
-
await generateAndCacheTestData(executionContext, "API", operationId, dataTable, cacheLayer, entityName);
|
|
10
|
+
Given('generate a {string} entity {string} with API {string}', async ({ page, context, i18N, cacheLayer, cleanupTracker, executionContext}, module, entityName, operationId, dataTable) => {
|
|
11
|
+
await generateAndCacheTestData(executionContext, "API", operationId, dataTable, cacheLayer, entityName, null, cleanupTracker);
|
|
12
12
|
});
|
|
13
|
-
Given('generate a {string} entity {string} with generator {string} using {string} profile', async ({ page, context, i18N, cacheLayer, executionContext}, module, entityName, generatorName, profile, dataTable) => {
|
|
14
|
-
await generateAndCacheTestData(executionContext, "template", generatorName, dataTable, cacheLayer, entityName, profile);
|
|
13
|
+
Given('generate a {string} entity {string} with generator {string} using {string} profile', async ({ page, context, i18N, cacheLayer, cleanupTracker, executionContext}, module, entityName, generatorName, profile, dataTable) => {
|
|
14
|
+
await generateAndCacheTestData(executionContext, "template", generatorName, dataTable, cacheLayer, entityName, profile, cleanupTracker);
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
Given('generate a {string} entity {string} with API {string} using {string} profile', async ({ page, context, i18N, cacheLayer, executionContext}, module, entityName, operationId, profile, dataTable) => {
|
|
18
|
-
await generateAndCacheTestData(executionContext, "API", operationId, dataTable, cacheLayer, entityName, profile);
|
|
17
|
+
Given('generate a {string} entity {string} with API {string} using {string} profile', async ({ page, context, i18N, cacheLayer, cleanupTracker, executionContext}, module, entityName, operationId, profile, dataTable) => {
|
|
18
|
+
await generateAndCacheTestData(executionContext, "API", operationId, dataTable, cacheLayer, entityName, profile, cleanupTracker);
|
|
19
19
|
});
|
|
@@ -4,7 +4,7 @@ import DataGenerator from '@zohodesk/testinglibrary/DataGenerator';
|
|
|
4
4
|
|
|
5
5
|
const dataGenerator = new DataGenerator();
|
|
6
6
|
|
|
7
|
-
export async function generateAndCacheTestData(executionContext, type, identifier, dataTable, cacheLayer, entityName, profile = null) {
|
|
7
|
+
export async function generateAndCacheTestData(executionContext, type, identifier, dataTable, cacheLayer, entityName, profile = null, cleanupTracker = null) {
|
|
8
8
|
let actorInfo;
|
|
9
9
|
const testInfo = test.info();
|
|
10
10
|
const scenarioName = testInfo.title.split('/').pop() || 'Unknown Scenario';
|
|
@@ -16,6 +16,15 @@ export async function generateAndCacheTestData(executionContext, type, identifie
|
|
|
16
16
|
actorInfo = executionContext.actorInfo;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const
|
|
20
|
-
await cacheLayer.set(entityName,
|
|
19
|
+
const { response, generators } = await dataGenerator.generate(testInfo, actorInfo, type, identifier, scenarioName, dataTable ? dataTable.hashes() : []);
|
|
20
|
+
await cacheLayer.set(entityName, response.data);
|
|
21
|
+
|
|
22
|
+
if (cleanupTracker) {
|
|
23
|
+
cleanupTracker.track({
|
|
24
|
+
entityName,
|
|
25
|
+
generators,
|
|
26
|
+
cachedData: response.data,
|
|
27
|
+
actorInfo
|
|
28
|
+
});
|
|
29
|
+
}
|
|
21
30
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
class CleanupTracker {
|
|
8
|
+
#entries = [];
|
|
9
|
+
track({
|
|
10
|
+
entityName,
|
|
11
|
+
generators,
|
|
12
|
+
cachedData,
|
|
13
|
+
actorInfo
|
|
14
|
+
}) {
|
|
15
|
+
this.#entries.push({
|
|
16
|
+
entityName,
|
|
17
|
+
generators,
|
|
18
|
+
cachedData,
|
|
19
|
+
actorInfo
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
getEntries() {
|
|
23
|
+
return [...this.#entries];
|
|
24
|
+
}
|
|
25
|
+
clear() {
|
|
26
|
+
this.#entries = [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
var _default = exports.default = CleanupTracker;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
Object.defineProperty(exports, "__esModule", {
|
|
5
|
+
value: true
|
|
6
|
+
});
|
|
7
|
+
exports.default = void 0;
|
|
8
|
+
var _jsonpath = _interopRequireDefault(require("jsonpath"));
|
|
9
|
+
var _logger = require("../../utils/logger");
|
|
10
|
+
var _DataGeneratorHelper = require("./DataGeneratorHelper");
|
|
11
|
+
function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); }
|
|
12
|
+
function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); }
|
|
13
|
+
function _assertClassBrand(e, t, n) { if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n; throw new TypeError("Private element is not present on this object"); }
|
|
14
|
+
var _DataCleanup_brand = /*#__PURE__*/new WeakSet();
|
|
15
|
+
class DataCleanup {
|
|
16
|
+
constructor() {
|
|
17
|
+
_classPrivateMethodInitSpec(this, _DataCleanup_brand);
|
|
18
|
+
}
|
|
19
|
+
async cleanup(entries) {
|
|
20
|
+
for (const entry of [...entries].reverse()) {
|
|
21
|
+
for (const step of [...entry.generators].reverse()) {
|
|
22
|
+
if (!step.cleanup) continue;
|
|
23
|
+
try {
|
|
24
|
+
const entityId = _assertClassBrand(_DataCleanup_brand, this, _extractId).call(this, entry.cachedData, step.cleanup.idPath);
|
|
25
|
+
if (step.cleanup.type === 'oas') {
|
|
26
|
+
await _assertClassBrand(_DataCleanup_brand, this, _cleanupViaOAS).call(this, step.cleanup, entityId, entry.actorInfo);
|
|
27
|
+
} else if (step.cleanup.type === 'api') {
|
|
28
|
+
await _assertClassBrand(_DataCleanup_brand, this, _cleanupViaAPI).call(this, step.cleanup, entityId, entry.actorInfo);
|
|
29
|
+
}
|
|
30
|
+
_logger.Logger.log(_logger.Logger.SUCCESS_TYPE, `Cleanup: ${step.cleanup.method || 'delete'} ${step.name} (ID: ${entityId})`);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Cleanup warning for ${step.name}: ${err.message}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function _extractId(cachedData, idPath) {
|
|
39
|
+
if (!cachedData || !idPath) {
|
|
40
|
+
throw new Error('Missing cached data or idPath for cleanup');
|
|
41
|
+
}
|
|
42
|
+
const result = _jsonpath.default.query(cachedData, idPath);
|
|
43
|
+
if (result.length === 0) {
|
|
44
|
+
throw new Error(`Could not extract ID using path "${idPath}" from cached data`);
|
|
45
|
+
}
|
|
46
|
+
return result[0];
|
|
47
|
+
}
|
|
48
|
+
async function _cleanupViaOAS(cleanupConfig, entityId, actorInfo) {
|
|
49
|
+
const dataGeneratorObj = actorInfo['data-generator'];
|
|
50
|
+
if (!dataGeneratorObj) {
|
|
51
|
+
throw new Error(`Data Generator configuration is missing for cleanup`);
|
|
52
|
+
}
|
|
53
|
+
const deleteGenerator = [{
|
|
54
|
+
type: 'dynamic',
|
|
55
|
+
generatorOperationId: cleanupConfig.operationId,
|
|
56
|
+
dataPath: '$.response.body:$',
|
|
57
|
+
name: cleanupConfig.operationId,
|
|
58
|
+
params: {
|
|
59
|
+
id: String(entityId)
|
|
60
|
+
}
|
|
61
|
+
}];
|
|
62
|
+
const apiPayload = {
|
|
63
|
+
scenario_name: 'cleanup',
|
|
64
|
+
data_generation_templates: deleteGenerator,
|
|
65
|
+
...dataGeneratorObj
|
|
66
|
+
};
|
|
67
|
+
const account = apiPayload.account;
|
|
68
|
+
if (account) {
|
|
69
|
+
account.email = actorInfo.email;
|
|
70
|
+
account.password = actorInfo.password;
|
|
71
|
+
}
|
|
72
|
+
const environmentDetails = apiPayload.environmentDetails || {};
|
|
73
|
+
environmentDetails.iam_url = process.env.DG_IAM_DOMAIN;
|
|
74
|
+
const domainUrl = new URL(process.env.domain);
|
|
75
|
+
environmentDetails.host = domainUrl.origin;
|
|
76
|
+
apiPayload.environmentDetails = environmentDetails;
|
|
77
|
+
await (0, _DataGeneratorHelper.makeRequest)(process.env.DG_SERVICE_DOMAIN + process.env.DG_SERVICE_API_PATH, apiPayload);
|
|
78
|
+
}
|
|
79
|
+
async function _cleanupViaAPI(cleanupConfig, entityId, actorInfo) {
|
|
80
|
+
const domainUrl = new URL(process.env.domain);
|
|
81
|
+
const apiPath = cleanupConfig.apiPath.replace('{id}', entityId);
|
|
82
|
+
const url = `${domainUrl.origin}${apiPath}`;
|
|
83
|
+
const fetchOptions = {
|
|
84
|
+
method: cleanupConfig.method,
|
|
85
|
+
headers: {
|
|
86
|
+
'Content-Type': 'application/json'
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
if (cleanupConfig.body) {
|
|
90
|
+
fetchOptions.body = JSON.stringify(cleanupConfig.body);
|
|
91
|
+
}
|
|
92
|
+
const response = await fetch(url, fetchOptions);
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
const errorBody = await response.text();
|
|
95
|
+
throw new Error(`API cleanup failed: ${cleanupConfig.method} ${apiPath} - status: ${response.status}, body: ${errorBody}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
var _default = exports.default = DataCleanup;
|
|
@@ -11,13 +11,19 @@ var _logger = require("../../utils/logger");
|
|
|
11
11
|
var _DataGeneratorHelper = require("./DataGeneratorHelper");
|
|
12
12
|
var _helpers = require("@zohodesk/testinglibrary/helpers");
|
|
13
13
|
var _DataGeneratorError = require("./DataGeneratorError");
|
|
14
|
+
var _readConfigFile = require("../playwright/readConfigFile");
|
|
14
15
|
function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); }
|
|
16
|
+
function _classPrivateFieldInitSpec(e, t, a) { _checkPrivateRedeclaration(e, t), t.set(e, a); }
|
|
15
17
|
function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); }
|
|
18
|
+
function _classPrivateFieldSet(s, a, r) { return s.set(_assertClassBrand(s, a), r), r; }
|
|
19
|
+
function _classPrivateFieldGet(s, a) { return s.get(_assertClassBrand(s, a)); }
|
|
16
20
|
function _assertClassBrand(e, t, n) { if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n; throw new TypeError("Private element is not present on this object"); }
|
|
21
|
+
var _generatorIndex = /*#__PURE__*/new WeakMap();
|
|
17
22
|
var _DataGenerator_brand = /*#__PURE__*/new WeakSet();
|
|
18
23
|
class DataGenerator {
|
|
19
24
|
constructor() {
|
|
20
25
|
_classPrivateMethodInitSpec(this, _DataGenerator_brand);
|
|
26
|
+
_classPrivateFieldInitSpec(this, _generatorIndex, null);
|
|
21
27
|
}
|
|
22
28
|
async generate(testInfo, actorInfo, generatorType, generatorName, scenarioName, dataTable) {
|
|
23
29
|
try {
|
|
@@ -31,7 +37,10 @@ class DataGenerator {
|
|
|
31
37
|
const apiPayload = await _assertClassBrand(_DataGenerator_brand, this, _constructApiPayload).call(this, scenarioName, processedGenerators, actorInfo);
|
|
32
38
|
const response = await (0, _DataGeneratorHelper.makeRequest)(process.env.DG_SERVICE_DOMAIN + process.env.DG_SERVICE_API_PATH, apiPayload);
|
|
33
39
|
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Generated response for the generator: ${generatorName} for scenario: ${scenarioName}, Response: ${JSON.stringify(response)}`);
|
|
34
|
-
return
|
|
40
|
+
return {
|
|
41
|
+
response,
|
|
42
|
+
generators
|
|
43
|
+
};
|
|
35
44
|
} catch (error) {
|
|
36
45
|
if (error instanceof _DataGeneratorError.DataGeneratorError) {
|
|
37
46
|
console.error(error.getMessage());
|
|
@@ -48,26 +57,64 @@ class DataGenerator {
|
|
|
48
57
|
}
|
|
49
58
|
}
|
|
50
59
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
function _matchesPattern(filename, pattern) {
|
|
61
|
+
const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
|
|
62
|
+
return regex.test(filename);
|
|
63
|
+
}
|
|
64
|
+
function _scanDir(dir, index, pattern) {
|
|
65
|
+
const entries = _fs.default.readdirSync(dir, {
|
|
66
|
+
withFileTypes: true
|
|
67
|
+
});
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
const fullPath = _path.default.join(dir, entry.name);
|
|
70
|
+
if (entry.isDirectory()) {
|
|
71
|
+
_assertClassBrand(_DataGenerator_brand, this, _scanDir).call(this, fullPath, index, pattern);
|
|
72
|
+
} else if (_assertClassBrand(_DataGenerator_brand, this, _matchesPattern).call(this, entry.name, pattern)) {
|
|
73
|
+
try {
|
|
74
|
+
const data = _fs.default.readFileSync(fullPath, 'utf8');
|
|
75
|
+
const obj = JSON.parse(data);
|
|
76
|
+
if (obj.generators) {
|
|
77
|
+
for (const [name, config] of Object.entries(obj.generators)) {
|
|
78
|
+
if (!index.has(name)) {
|
|
79
|
+
index.set(name, config);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
65
82
|
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
_logger.Logger.log(_logger.Logger.FAILURE_TYPE, `Failed to parse generator file: ${fullPath} - ${err.message}`);
|
|
66
85
|
}
|
|
67
86
|
}
|
|
68
|
-
searchDir = _path.default.dirname(searchDir);
|
|
69
87
|
}
|
|
70
|
-
|
|
88
|
+
}
|
|
89
|
+
function _buildIndex(modulesRoot, pattern) {
|
|
90
|
+
const index = new Map();
|
|
91
|
+
_assertClassBrand(_DataGenerator_brand, this, _scanDir).call(this, modulesRoot, index, pattern);
|
|
92
|
+
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Generator index built: ${index.size} generators found`);
|
|
93
|
+
return index;
|
|
94
|
+
}
|
|
95
|
+
function _getModulesRoot(featureFilePath) {
|
|
96
|
+
let dir = _path.default.dirname(featureFilePath);
|
|
97
|
+
while (dir !== _path.default.parse(dir).root) {
|
|
98
|
+
if (_path.default.basename(dir) === 'modules') return dir;
|
|
99
|
+
dir = _path.default.dirname(dir);
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
async function _getGenerator(testInfo, generatorName) {
|
|
104
|
+
if (!_classPrivateFieldGet(_generatorIndex, this)) {
|
|
105
|
+
const featureFilePath = await (0, _DataGeneratorHelper.getGeneratorFilePath)(testInfo.file);
|
|
106
|
+
const modulesRoot = _assertClassBrand(_DataGenerator_brand, this, _getModulesRoot).call(this, featureFilePath);
|
|
107
|
+
const {
|
|
108
|
+
generatorFilePattern: pattern = '*.generators.json'
|
|
109
|
+
} = (0, _readConfigFile.generateConfigFromFile)();
|
|
110
|
+
if (modulesRoot) {
|
|
111
|
+
_classPrivateFieldSet(_generatorIndex, this, _assertClassBrand(_DataGenerator_brand, this, _buildIndex).call(this, modulesRoot, pattern));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (_classPrivateFieldGet(_generatorIndex, this) && _classPrivateFieldGet(_generatorIndex, this).has(generatorName)) {
|
|
115
|
+
return _classPrivateFieldGet(_generatorIndex, this).get(generatorName);
|
|
116
|
+
}
|
|
117
|
+
throw new _DataGeneratorError.GeneratorError(`Generator "${generatorName}" could not be found in any generator file`);
|
|
71
118
|
}
|
|
72
119
|
async function _generateAPIGenerator(operationId) {
|
|
73
120
|
return [{
|
|
@@ -8,18 +8,21 @@ exports.validateGenerators = validateGenerators;
|
|
|
8
8
|
var _path = _interopRequireDefault(require("path"));
|
|
9
9
|
var _fs = _interopRequireDefault(require("fs"));
|
|
10
10
|
var _logger = require("../../utils/logger");
|
|
11
|
-
|
|
11
|
+
var _readConfigFile = require("../playwright/readConfigFile");
|
|
12
|
+
function matchesPattern(filename, pattern) {
|
|
13
|
+
const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
|
|
14
|
+
return regex.test(filename);
|
|
15
|
+
}
|
|
16
|
+
function findGeneratorFiles(dir, pattern, results = []) {
|
|
12
17
|
const entries = _fs.default.readdirSync(dir, {
|
|
13
18
|
withFileTypes: true
|
|
14
19
|
});
|
|
15
20
|
for (const entry of entries) {
|
|
21
|
+
const fullPath = _path.default.join(dir, entry.name);
|
|
16
22
|
if (entry.isDirectory()) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} else {
|
|
21
|
-
findDataGeneratorDirs(fullPath, results);
|
|
22
|
-
}
|
|
23
|
+
findGeneratorFiles(fullPath, pattern, results);
|
|
24
|
+
} else if (matchesPattern(entry.name, pattern)) {
|
|
25
|
+
results.push(fullPath);
|
|
23
26
|
}
|
|
24
27
|
}
|
|
25
28
|
return results;
|
|
@@ -32,26 +35,25 @@ function validateGenerators(modulesRoot) {
|
|
|
32
35
|
duplicates: []
|
|
33
36
|
};
|
|
34
37
|
}
|
|
38
|
+
const {
|
|
39
|
+
generatorFilePattern: pattern = '*.generators.json'
|
|
40
|
+
} = (0, _readConfigFile.generateConfigFromFile)();
|
|
35
41
|
const generatorMap = {};
|
|
36
|
-
const
|
|
37
|
-
for (const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
for (const name of Object.keys(generatorObj.generators)) {
|
|
46
|
-
if (!generatorMap[name]) {
|
|
47
|
-
generatorMap[name] = [];
|
|
48
|
-
}
|
|
49
|
-
generatorMap[name].push(filePath);
|
|
42
|
+
const generatorFiles = findGeneratorFiles(modulesRoot, pattern);
|
|
43
|
+
for (const filePath of generatorFiles) {
|
|
44
|
+
try {
|
|
45
|
+
const data = _fs.default.readFileSync(filePath, 'utf8');
|
|
46
|
+
const generatorObj = JSON.parse(data);
|
|
47
|
+
if (generatorObj.generators) {
|
|
48
|
+
for (const name of Object.keys(generatorObj.generators)) {
|
|
49
|
+
if (!generatorMap[name]) {
|
|
50
|
+
generatorMap[name] = [];
|
|
50
51
|
}
|
|
52
|
+
generatorMap[name].push(filePath);
|
|
51
53
|
}
|
|
52
|
-
} catch (err) {
|
|
53
|
-
_logger.Logger.log(_logger.Logger.FAILURE_TYPE, `Failed to parse generator file: ${filePath} - ${err.message}`);
|
|
54
54
|
}
|
|
55
|
+
} catch (err) {
|
|
56
|
+
_logger.Logger.log(_logger.Logger.FAILURE_TYPE, `Failed to parse generator file: ${filePath} - ${err.message}`);
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
59
|
const duplicates = Object.entries(generatorMap).filter(([, files]) => files.length > 1).map(([name, files]) => ({
|
|
@@ -72,7 +74,7 @@ function validateGenerators(modulesRoot) {
|
|
|
72
74
|
};
|
|
73
75
|
}
|
|
74
76
|
const generatorCount = Object.keys(generatorMap).length;
|
|
75
|
-
_logger.Logger.log(_logger.Logger.SUCCESS_TYPE, `Generator validation passed. ${generatorCount} unique generators found across ${
|
|
77
|
+
_logger.Logger.log(_logger.Logger.SUCCESS_TYPE, `Generator validation passed. ${generatorCount} unique generators found across ${generatorFiles.length} generator files.`);
|
|
76
78
|
return {
|
|
77
79
|
valid: true,
|
|
78
80
|
duplicates: []
|
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
3
4
|
Object.defineProperty(exports, "__esModule", {
|
|
4
5
|
value: true
|
|
5
6
|
});
|
|
6
7
|
exports.default = void 0;
|
|
8
|
+
var _CleanupTracker = _interopRequireDefault(require("../../dataGenerator/CleanupTracker"));
|
|
9
|
+
var _DataCleanup = _interopRequireDefault(require("../../dataGenerator/DataCleanup"));
|
|
7
10
|
const cacheMap = new Map();
|
|
8
11
|
var _default = exports.default = {
|
|
9
12
|
// eslint-disable-next-line no-empty-pattern
|
|
10
13
|
cacheLayer: async ({}, use) => {
|
|
11
14
|
await use(cacheMap);
|
|
15
|
+
},
|
|
16
|
+
// eslint-disable-next-line no-empty-pattern
|
|
17
|
+
cleanupTracker: async ({}, use) => {
|
|
18
|
+
const tracker = new _CleanupTracker.default();
|
|
19
|
+
await use(tracker);
|
|
20
|
+
const entries = tracker.getEntries();
|
|
21
|
+
if (entries.length > 0) {
|
|
22
|
+
const cleanup = new _DataCleanup.default();
|
|
23
|
+
await cleanup.cleanup(entries);
|
|
24
|
+
}
|
|
25
|
+
tracker.clear();
|
|
12
26
|
}
|
|
13
27
|
};
|
|
@@ -55,7 +55,8 @@ function getDefaultConfig() {
|
|
|
55
55
|
featureFilesFolder: 'feature-files',
|
|
56
56
|
stepDefinitionsFolder: 'steps',
|
|
57
57
|
testSetup: {},
|
|
58
|
-
editionOrder: ['Free', 'Express', 'Standard', 'Professional', 'Enterprise']
|
|
58
|
+
editionOrder: ['Free', 'Express', 'Standard', 'Professional', 'Enterprise'],
|
|
59
|
+
generatorFilePattern: '*.generators.json'
|
|
59
60
|
};
|
|
60
61
|
}
|
|
61
62
|
function combineDefaultConfigWithUserConfig(userConfiguration) {
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zohodesk/testinglibrary",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.46-n20-experimental",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@zohodesk/testinglibrary",
|
|
9
|
-
"version": "0.0.
|
|
9
|
+
"version": "0.0.46-n20-experimental",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "ISC",
|
|
12
12
|
"dependencies": {
|