@zohodesk/testinglibrary 0.0.47-n20-experimental → 0.0.49-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 +92 -253
- package/build/common/data-generator/steps/DataGeneratorStepsHelper.js +6 -2
- package/build/core/playwright/builtInFixtures/cacheLayer.js +151 -2
- package/build/core/playwright/helpers/auth/getUsers.js +2 -2
- package/build/core/playwright/readConfigFile.js +2 -1
- package/changelog.md +27 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/AUTO_CLEANUP_PLAN.md
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
|
-
# Data Generator System — Design &
|
|
1
|
+
# Data Generator System — Design & Current State
|
|
2
2
|
|
|
3
3
|
## Data Generator Overview
|
|
4
4
|
|
|
5
5
|
### How It Works
|
|
6
6
|
- BDD step: `Given generate a "{Type}" entity "{name}" with generator "{GeneratorName}"`
|
|
7
|
-
- Framework
|
|
8
|
-
- Walks up directory tree from feature file to find `data-generators/` folder
|
|
9
|
-
- Scans ALL `.json` files within (not hardcoded to `generators.json`)
|
|
7
|
+
- Framework builds a **global generator index** at first use — scans all `*.generators.json` files under `modules/`
|
|
10
8
|
- Generator names must be **unique across the entire UAT suite**
|
|
11
9
|
- `ZDTestingFramework validate` checks for duplicate generator names before test run
|
|
10
|
+
- Data generated using **org-level OAuth credentials** (org-oauth) by default
|
|
11
|
+
- When scenario runs under a non-admin profile (`@profile_agent`), data generation still uses org-level `data-generator` — falls back automatically if profile has no own credentials
|
|
12
|
+
|
|
13
|
+
### Generator File Convention
|
|
14
|
+
- **Pattern:** `*.generators.json` (configurable via `generatorFilePattern` in `uat.config.js`)
|
|
15
|
+
- **Location:** anywhere under `modules/` — discovered globally via recursive scan
|
|
16
|
+
- **Examples:** `ticket.generators.json`, `account.generators.json`, `webhook.generators.json`
|
|
12
17
|
|
|
13
18
|
### Generator JSON Structure
|
|
14
19
|
```json
|
|
@@ -20,52 +25,73 @@
|
|
|
20
25
|
"name": "stepName",
|
|
21
26
|
"generatorOperationId": "support.Module.operationName",
|
|
22
27
|
"dataPath": "$.response.body:$",
|
|
23
|
-
"params": { "key": "$previousStep.value" }
|
|
24
|
-
"cleanup": { ... }
|
|
28
|
+
"params": { "key": "$previousStep.value" }
|
|
25
29
|
}
|
|
26
30
|
]
|
|
27
31
|
}
|
|
28
32
|
}
|
|
29
33
|
```
|
|
30
34
|
|
|
35
|
+
### Discovery Mechanism
|
|
36
|
+
- **Index-based** — built once, O(1) lookups
|
|
37
|
+
- `#getModulesRoot()` uses `configConstants.TEST_SLICE_FOLDER + stage + 'modules'` (deterministic)
|
|
38
|
+
- Scans recursively for files matching `generatorFilePattern` (default: `*.generators.json`)
|
|
39
|
+
- First generator name match wins (no duplicates enforced by validator)
|
|
40
|
+
|
|
41
|
+
### Profile Resolution for Data Generation
|
|
42
|
+
- **Default (no `using` profile):** Uses org-level `data-generator` from edition JSON. If scenario profile (e.g., `@profile_agent`) has no `data-generator`, falls back to org-level via `getListOfActors()`.
|
|
43
|
+
- **Explicit profile (`using "agent" profile`):** Resolves that profile's credentials via `getUserForSelectedEditionAndProfile()`.
|
|
44
|
+
|
|
31
45
|
### Constraints
|
|
32
46
|
| Constraint | Detail |
|
|
33
47
|
|---|---|
|
|
34
|
-
| **Unique generator names** | Generator names must be unique across all
|
|
35
|
-
| **
|
|
36
|
-
| **
|
|
37
|
-
| **
|
|
38
|
-
| **
|
|
39
|
-
| **
|
|
40
|
-
| **
|
|
41
|
-
| **
|
|
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
|
-
```
|
|
48
|
+
| **Unique generator names** | Generator names must be unique across all `*.generators.json` files |
|
|
49
|
+
| **Global discovery** | Framework scans entire `modules/` tree — generators in any module are accessible from any feature file |
|
|
50
|
+
| **File pattern** | Configurable via `generatorFilePattern` in `uat.config.js` (default: `*.generators.json`) |
|
|
51
|
+
| **DG_API_NAME matching** | In Gherkin data tables, `DG_API_NAME` column matches generator step's `name` field to inject params |
|
|
52
|
+
| **Chained steps** | Multi-step generators use `$previousStep.value` syntax to pass output between steps |
|
|
53
|
+
| **dataPath extraction** | `dataPath` uses JSONPath to extract specific fields from DG service response |
|
|
54
|
+
| **Cached response** | Generated data is cached via `cacheLayer.set(entityName, response.data)` for use in subsequent steps |
|
|
55
|
+
| **Org-level auth fallback** | If profile has no `data-generator`, org-level config from edition JSON is used automatically |
|
|
53
56
|
|
|
54
57
|
---
|
|
55
58
|
|
|
56
|
-
|
|
59
|
+
## Current Generators
|
|
60
|
+
|
|
61
|
+
| Generator | File | Steps | Used By |
|
|
62
|
+
|---|---|---|---|
|
|
63
|
+
| `TicketWithDepartment` | `Ticket/data-generators/ticket.generators.json` | getDepartments → createProduct → createTicket | 452+ scenarios |
|
|
64
|
+
| `TicketBasic` | `Ticket/data-generators/ticket.generators.json` | getDepartments → createTicket (no product) | Express/Free editions |
|
|
65
|
+
| `CreateAccountRecord` | `Account/data-generators/account.generators.json` | createAccount | 37 scenarios |
|
|
66
|
+
| `CreateContactRecord` | `Contact/data-generators/contact.generators.json` | createContact | 42 scenarios |
|
|
67
|
+
| `CreateContractRecord` | `Contract/DV/Subtabs/*/data-generators/contract.generators.json` | getDepartments → createAccount → createContract | 4 scenarios |
|
|
68
|
+
| `CreateWebhookRecord` | `Webhooks/List/data-generators/webhook.generators.json` | createWebhook (with default subscriptions) | Webhook scenarios |
|
|
69
|
+
| `ProductWithDepartment` | `Search/products/data-generators/products.generators.json` | getDepartments → createProduct | Product/Search scenarios |
|
|
57
70
|
|
|
58
|
-
|
|
71
|
+
---
|
|
59
72
|
|
|
60
|
-
|
|
73
|
+
## Auto-Cleanup — Design (Not Yet Implemented)
|
|
61
74
|
|
|
62
|
-
|
|
75
|
+
### Status
|
|
76
|
+
- **V1 implemented then removed** in `0.0.47` — CleanupTracker + DataCleanup were too tightly coupled
|
|
77
|
+
- **V2 planned** — proper redesign needed
|
|
63
78
|
|
|
64
|
-
|
|
79
|
+
### Why Cleanup Was Removed
|
|
80
|
+
1. `cleanupTracker` fixture added complexity to all step definitions (extra parameter)
|
|
81
|
+
2. Cleanup ran inside fixture teardown — failures were hard to debug
|
|
82
|
+
3. REST API cleanup needed auth that wasn't available in the cleanup context
|
|
83
|
+
4. No way to control cleanup order for cross-entity dependencies
|
|
84
|
+
|
|
85
|
+
### V2 Design Principles
|
|
86
|
+
1. **Cleanup config stays in generator JSON** — `cleanup` property on each step (same as V1)
|
|
87
|
+
2. **Separate cleanup phase** — not in fixture teardown, but as explicit post-scenario hook
|
|
88
|
+
3. **Auth-aware** — cleanup uses the same `actorInfo` that created the data
|
|
89
|
+
4. **Configurable** — cleanup can be disabled per scenario or globally
|
|
90
|
+
5. **Non-blocking** — cleanup failures never fail the test
|
|
65
91
|
|
|
66
|
-
|
|
92
|
+
### Cleanup Types (unchanged from V1 design)
|
|
67
93
|
|
|
68
|
-
|
|
94
|
+
#### Type 1: `oas` — DG service delete
|
|
69
95
|
```json
|
|
70
96
|
"cleanup": {
|
|
71
97
|
"type": "oas",
|
|
@@ -74,7 +100,7 @@ The framework generates test data (contacts, tickets, accounts, etc.) via the DG
|
|
|
74
100
|
}
|
|
75
101
|
```
|
|
76
102
|
|
|
77
|
-
|
|
103
|
+
#### Type 2: `api` — Direct REST API DELETE
|
|
78
104
|
```json
|
|
79
105
|
"cleanup": {
|
|
80
106
|
"type": "api",
|
|
@@ -84,7 +110,7 @@ The framework generates test data (contacts, tickets, accounts, etc.) via the DG
|
|
|
84
110
|
}
|
|
85
111
|
```
|
|
86
112
|
|
|
87
|
-
|
|
113
|
+
#### Type 3: `api` — Disable via PATCH
|
|
88
114
|
```json
|
|
89
115
|
"cleanup": {
|
|
90
116
|
"type": "api",
|
|
@@ -95,238 +121,51 @@ The framework generates test data (contacts, tickets, accounts, etc.) via the DG
|
|
|
95
121
|
}
|
|
96
122
|
```
|
|
97
123
|
|
|
98
|
-
|
|
124
|
+
#### No cleanup — read-only operations
|
|
99
125
|
Steps without `cleanup` property are skipped (e.g., `getDepartments`).
|
|
100
126
|
|
|
101
|
-
|
|
127
|
+
### V2 Implementation Plan
|
|
102
128
|
|
|
103
|
-
|
|
129
|
+
1. **CleanupManager** (replaces CleanupTracker + DataCleanup)
|
|
130
|
+
- Single class managing both tracking and execution
|
|
131
|
+
- Runs after scenario via `testSetup` hook (not fixture teardown)
|
|
132
|
+
- Uses `actorInfo` from the generation step for auth
|
|
104
133
|
|
|
105
|
-
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
```
|
|
134
|
+
2. **Config flag** in `uat.config.js`:
|
|
135
|
+
```javascript
|
|
136
|
+
autoCleanup: true, // default: true
|
|
137
|
+
cleanupTimeout: 30000 // per-entity timeout
|
|
138
|
+
```
|
|
163
139
|
|
|
164
|
-
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
```
|
|
140
|
+
3. **Per-scenario opt-out** via tag:
|
|
141
|
+
```gherkin
|
|
142
|
+
@skip_cleanup
|
|
143
|
+
Scenario: Test that needs data to persist
|
|
144
|
+
```
|
|
186
145
|
|
|
187
|
-
|
|
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
|
-
```
|
|
146
|
+
4. **Cleanup order**: reverse of creation (dependent entities first)
|
|
208
147
|
|
|
209
148
|
---
|
|
210
149
|
|
|
211
|
-
##
|
|
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:
|
|
150
|
+
## Seed Data System — Design (Planned)
|
|
234
151
|
|
|
235
|
-
|
|
236
|
-
- **Scenario fails** → cleanup still runs
|
|
237
|
-
- **Individual cleanup step fails** → remaining cleanup steps still run (each step is wrapped in try/catch)
|
|
152
|
+
See `SEED_DATA_PLAN.md` in the consumer repo for the full design.
|
|
238
153
|
|
|
239
|
-
|
|
154
|
+
### Summary
|
|
155
|
+
- **Edition-scoped** — data maps to edition tiers, not portal names
|
|
156
|
+
- **Profile-based** — support creating data as specific profiles (agent login)
|
|
157
|
+
- **OAS + REST** — both API types supported
|
|
158
|
+
- **Runs before scenarios** — in `page.js` fixture after login
|
|
159
|
+
- **Idempotent** — skips existing data, creates only missing
|
|
240
160
|
|
|
241
161
|
---
|
|
242
162
|
|
|
243
|
-
##
|
|
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
|
-
---
|
|
163
|
+
## Multi-DC Portability — Design (Planned)
|
|
325
164
|
|
|
326
|
-
|
|
165
|
+
See `MULTI_DC_STRATEGY.md` in the consumer repo for the full design.
|
|
327
166
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
167
|
+
### Summary
|
|
168
|
+
- **`capability` field** on each portal for DC-agnostic resolution
|
|
169
|
+
- **Department aliases** in portal config for cross-DC mapping
|
|
170
|
+
- **Framework resolves** by `capability` first, `orgName` fallback
|
|
171
|
+
- **Same feature files** work across all DCs
|
|
@@ -26,6 +26,10 @@ export async function generateAndCacheTestData(executionContext, type, identifie
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const { response } = await dataGenerator.generate(testInfo, actorInfo, type, identifier, scenarioName, dataTable ? dataTable.hashes() : []);
|
|
30
|
-
|
|
29
|
+
const { response, generators } = await dataGenerator.generate(testInfo, actorInfo, type, identifier, scenarioName, dataTable ? dataTable.hashes() : []);
|
|
30
|
+
if (cacheLayer._trackForCleanup) {
|
|
31
|
+
cacheLayer._trackForCleanup(entityName, response.data, generators, actorInfo);
|
|
32
|
+
} else {
|
|
33
|
+
cacheLayer.set(entityName, response.data);
|
|
34
|
+
}
|
|
31
35
|
}
|
|
@@ -1,13 +1,162 @@
|
|
|
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;
|
|
7
|
-
|
|
8
|
+
var _path = _interopRequireDefault(require("path"));
|
|
9
|
+
var _fs = _interopRequireDefault(require("fs"));
|
|
10
|
+
var _logger = require("../../../utils/logger");
|
|
11
|
+
var _DataGeneratorHelper = require("../../dataGenerator/DataGeneratorHelper");
|
|
12
|
+
var _readConfigFile = require("../readConfigFile");
|
|
13
|
+
var _configConstants = _interopRequireDefault(require("../constants/configConstants"));
|
|
14
|
+
var _ConfigurationHelper = require("../configuration/ConfigurationHelper");
|
|
15
|
+
var _jsonpath = _interopRequireDefault(require("jsonpath"));
|
|
16
|
+
let cleanupRegistry = null;
|
|
17
|
+
function buildCleanupRegistry() {
|
|
18
|
+
const stage = (0, _ConfigurationHelper.getRunStage)();
|
|
19
|
+
const modulesRoot = _path.default.join(process.cwd(), _configConstants.default.TEST_SLICE_FOLDER, stage, 'modules');
|
|
20
|
+
const registry = {};
|
|
21
|
+
if (!_fs.default.existsSync(modulesRoot)) return registry;
|
|
22
|
+
scanDir(modulesRoot, registry);
|
|
23
|
+
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Cleanup registry built: ${Object.keys(registry).length} rules from ${modulesRoot}`);
|
|
24
|
+
return registry;
|
|
25
|
+
}
|
|
26
|
+
function scanDir(dir, registry) {
|
|
27
|
+
const entries = _fs.default.readdirSync(dir, {
|
|
28
|
+
withFileTypes: true
|
|
29
|
+
});
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
const fullPath = _path.default.join(dir, entry.name);
|
|
32
|
+
if (entry.isDirectory()) {
|
|
33
|
+
scanDir(fullPath, registry);
|
|
34
|
+
} else if (entry.name.endsWith('.cleanup.json')) {
|
|
35
|
+
try {
|
|
36
|
+
const data = JSON.parse(_fs.default.readFileSync(fullPath, 'utf8'));
|
|
37
|
+
for (const [operationId, config] of Object.entries(data)) {
|
|
38
|
+
if (registry[operationId]) {
|
|
39
|
+
throw new Error(`Duplicate cleanup rule for "${operationId}" found in ${fullPath}. ` + `Each operationId must have exactly one cleanup definition across all *.cleanup.json files.`);
|
|
40
|
+
}
|
|
41
|
+
registry[operationId] = config;
|
|
42
|
+
}
|
|
43
|
+
} catch (err) {
|
|
44
|
+
_logger.Logger.log(_logger.Logger.FAILURE_TYPE, `Failed to parse cleanup file: ${fullPath} - ${err.message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function extractId(cachedData, idPath) {
|
|
50
|
+
const result = _jsonpath.default.query(cachedData, idPath);
|
|
51
|
+
if (result.length === 0) {
|
|
52
|
+
throw new Error(`Could not extract ID using path "${idPath}"`);
|
|
53
|
+
}
|
|
54
|
+
return result[0];
|
|
55
|
+
}
|
|
56
|
+
async function cleanupViaOAS(config, entityId, actorInfo) {
|
|
57
|
+
const dataGeneratorObj = actorInfo['data-generator'];
|
|
58
|
+
if (!dataGeneratorObj) {
|
|
59
|
+
throw new Error('No data-generator config available for cleanup');
|
|
60
|
+
}
|
|
61
|
+
const payload = {
|
|
62
|
+
scenario_name: 'cleanup',
|
|
63
|
+
data_generation_templates: [{
|
|
64
|
+
type: 'dynamic',
|
|
65
|
+
generatorOperationId: config.operationId,
|
|
66
|
+
dataPath: '$.response.body:$',
|
|
67
|
+
name: config.operationId,
|
|
68
|
+
params: {
|
|
69
|
+
id: String(entityId)
|
|
70
|
+
}
|
|
71
|
+
}],
|
|
72
|
+
...dataGeneratorObj
|
|
73
|
+
};
|
|
74
|
+
if (payload.account) {
|
|
75
|
+
payload.account.email = actorInfo.email;
|
|
76
|
+
payload.account.password = actorInfo.password;
|
|
77
|
+
}
|
|
78
|
+
const environmentDetails = payload.environmentDetails || {};
|
|
79
|
+
environmentDetails.iam_url = process.env.DG_IAM_DOMAIN;
|
|
80
|
+
environmentDetails.host = new URL(process.env.domain).origin;
|
|
81
|
+
payload.environmentDetails = environmentDetails;
|
|
82
|
+
await (0, _DataGeneratorHelper.makeRequest)(process.env.DG_SERVICE_DOMAIN + process.env.DG_SERVICE_API_PATH, payload);
|
|
83
|
+
}
|
|
84
|
+
async function cleanupViaAPI(config, entityId) {
|
|
85
|
+
const url = `${new URL(process.env.domain).origin}${config.apiPath.replace('{id}', entityId)}`;
|
|
86
|
+
const options = {
|
|
87
|
+
method: config.method,
|
|
88
|
+
headers: {
|
|
89
|
+
'Content-Type': 'application/json'
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
if (config.body) {
|
|
93
|
+
options.body = JSON.stringify(config.body);
|
|
94
|
+
}
|
|
95
|
+
const response = await fetch(url, options);
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
const errorBody = await response.text();
|
|
98
|
+
throw new Error(`${config.method} ${config.apiPath} - status: ${response.status}, body: ${errorBody}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
8
101
|
var _default = exports.default = {
|
|
9
102
|
// eslint-disable-next-line no-empty-pattern
|
|
10
103
|
cacheLayer: async ({}, use) => {
|
|
11
|
-
|
|
104
|
+
const cache = new Map();
|
|
105
|
+
const cleanupEntries = [];
|
|
106
|
+
cache._trackForCleanup = (entityName, data, generators, actorInfo) => {
|
|
107
|
+
cache.set(entityName, data);
|
|
108
|
+
cleanupEntries.push({
|
|
109
|
+
entityName,
|
|
110
|
+
data,
|
|
111
|
+
generators,
|
|
112
|
+
actorInfo
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
await use(cache);
|
|
116
|
+
|
|
117
|
+
// TEARDOWN — runs after scenario ends (pass or fail)
|
|
118
|
+
const {
|
|
119
|
+
autoCleanup = true
|
|
120
|
+
} = (0, _readConfigFile.generateConfigFromFile)();
|
|
121
|
+
if (!autoCleanup || cleanupEntries.length === 0) {
|
|
122
|
+
cache.clear();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (!cleanupRegistry) {
|
|
126
|
+
cleanupRegistry = buildCleanupRegistry();
|
|
127
|
+
}
|
|
128
|
+
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Cleanup started: ${cleanupEntries.length} entities to process`);
|
|
129
|
+
let cleaned = 0;
|
|
130
|
+
let skipped = 0;
|
|
131
|
+
let failed = 0;
|
|
132
|
+
for (const entry of [...cleanupEntries].reverse()) {
|
|
133
|
+
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Cleanup entity: "${entry.entityName}" (${entry.generators.length} steps)`);
|
|
134
|
+
for (const step of [...entry.generators].reverse()) {
|
|
135
|
+
const operationId = step.generatorOperationId;
|
|
136
|
+
const cleanupConfig = cleanupRegistry[operationId];
|
|
137
|
+
if (!cleanupConfig) {
|
|
138
|
+
skipped++;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const entityId = extractId(entry.data, cleanupConfig.idPath);
|
|
143
|
+
const actionDesc = cleanupConfig.operationId || `${cleanupConfig.method} ${cleanupConfig.apiPath}`;
|
|
144
|
+
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Cleanup [${cleanupConfig.type}] ${step.name}: ${actionDesc} (id: ${entityId})`);
|
|
145
|
+
if (cleanupConfig.type === 'oas') {
|
|
146
|
+
await cleanupViaOAS(cleanupConfig, entityId, entry.actorInfo);
|
|
147
|
+
} else if (cleanupConfig.type === 'api') {
|
|
148
|
+
await cleanupViaAPI(cleanupConfig, entityId);
|
|
149
|
+
}
|
|
150
|
+
cleaned++;
|
|
151
|
+
_logger.Logger.log(_logger.Logger.SUCCESS_TYPE, `Cleanup success: ${step.name} (id: ${entityId})`);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
failed++;
|
|
154
|
+
_logger.Logger.log(_logger.Logger.FAILURE_TYPE, `Cleanup failed: ${step.name} — ${err.message}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Cleanup completed: ${cleaned} cleaned, ${skipped} skipped (no cleanup rule), ${failed} failed`);
|
|
159
|
+
cleanupEntries.length = 0;
|
|
160
|
+
cache.clear();
|
|
12
161
|
}
|
|
13
162
|
};
|
|
@@ -96,9 +96,9 @@ function getUserForSelectedEditionAndProfile(preferedEdition, preferredProfile,
|
|
|
96
96
|
throw new Error(`There is no "${edition}" edition configured.`);
|
|
97
97
|
}
|
|
98
98
|
if (testDataPortal !== null) {
|
|
99
|
-
testingPortal = userdata[edition].find(editionData => editionData.orgName === testDataPortal);
|
|
99
|
+
testingPortal = userdata[edition].find(editionData => editionData.capability === testDataPortal || editionData.orgName === testDataPortal);
|
|
100
100
|
if (!testingPortal) {
|
|
101
|
-
throw new Error(`There is no "${testDataPortal}" portal configured in "${edition}" edition.`);
|
|
101
|
+
throw new Error(`There is no "${testDataPortal}" portal (by capability or orgName) configured in "${edition}" edition.`);
|
|
102
102
|
}
|
|
103
103
|
} else {
|
|
104
104
|
testingPortal = userdata[edition] ? userdata[edition][0] : {};
|
|
@@ -56,7 +56,8 @@ function getDefaultConfig() {
|
|
|
56
56
|
stepDefinitionsFolder: 'steps',
|
|
57
57
|
testSetup: {},
|
|
58
58
|
editionOrder: ['Free', 'Express', 'Standard', 'Professional', 'Enterprise'],
|
|
59
|
-
generatorFilePattern: '*.generators.json'
|
|
59
|
+
generatorFilePattern: '*.generators.json',
|
|
60
|
+
autoCleanup: true
|
|
60
61
|
};
|
|
61
62
|
}
|
|
62
63
|
function combineDefaultConfigWithUserConfig(userConfiguration) {
|
package/changelog.md
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
# Testing Framework
|
|
2
2
|
|
|
3
3
|
## Framework that abstracts the configuration for playwright and Jest
|
|
4
|
+
|
|
5
|
+
# 0.0.48-n20-experimental
|
|
6
|
+
|
|
7
|
+
## Data Generator — Global Index Discovery
|
|
8
|
+
- **Global generator index**: Replaced walk-up directory search with index-based global discovery. Generators in any module are now accessible from any feature file.
|
|
9
|
+
- **Deterministic modules root**: Uses `configConstants.TEST_SLICE_FOLDER + stage + 'modules'` instead of unreliable directory walk-up.
|
|
10
|
+
- **Configurable file pattern**: `generatorFilePattern` in `uat.config.js` (default: `*.generators.json`).
|
|
11
|
+
- **TicketBasic generator**: New generator without product step for Express/Free editions.
|
|
12
|
+
|
|
13
|
+
## Data Generator — Profile & Auth Improvements
|
|
14
|
+
- **Org-level DG fallback**: When scenario profile has no `data-generator` config, falls back to org-level from edition JSON.
|
|
15
|
+
|
|
16
|
+
## Auto-Cleanup V2
|
|
17
|
+
- **Co-located `*.cleanup.json` registry**: Each module defines cleanup rules alongside generators. Scanned globally.
|
|
18
|
+
- **Fixture teardown cleanup**: Runs after each scenario (pass or fail). Supports OAS, REST DELETE, REST PATCH.
|
|
19
|
+
- **`autoCleanup` config**: Enable/disable via `uat.config.js` (default: `true`).
|
|
20
|
+
- **Detailed logging**: Cleanup started/completed/success/failed with entity names and IDs.
|
|
21
|
+
|
|
22
|
+
## Portal Resolution — Capability Support
|
|
23
|
+
- **`capability` field**: Portals can define a `capability` field for DC-agnostic resolution.
|
|
24
|
+
- **Dual resolution**: `@portal_` tags resolve by `capability` first, then `orgName` fallback.
|
|
25
|
+
- **Backward compatible**: Existing `orgName`-based tags still work.
|
|
26
|
+
|
|
27
|
+
## Bug Fixes
|
|
28
|
+
- Fixed `#getModulesRoot` hitting nested `modules/` directories.
|
|
29
|
+
- Removed cleanup V1 — replaced by V2.
|
|
30
|
+
|
|
4
31
|
# 0.2.4
|
|
5
32
|
- Issue fixes on custom fixtures
|
|
6
33
|
- Page Fixture
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zohodesk/testinglibrary",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.49-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.49-n20-experimental",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "ISC",
|
|
12
12
|
"dependencies": {
|