@zohodesk/testinglibrary 0.0.46-n20-experimental → 0.0.48-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.
@@ -1,14 +1,19 @@
1
- # Data Generator System — Design & Constraints
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 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`)
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 `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
- ```
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
- # Auto-Cleanup Generated Test Data After Scenario
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
- ## Context
71
+ ---
59
72
 
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.
73
+ ## Auto-Cleanup Design (Not Yet Implemented)
61
74
 
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.
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
- ## Three Cleanup Types
92
+ ### Cleanup Types (unchanged from V1 design)
67
93
 
68
- ### Type 1: `oas` — DG service delete (OAS supported)
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
- ### Type 2: `api` — Direct REST API (OAS not supported, DELETE)
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
- ### Type 3: `api` — Disable instead of delete (PATCH)
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
- ### No cleanup — read-only operations
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
- ## Full Generator JSON Examples
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
- ### 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
- ```
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
- ### 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
- ```
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
- ### 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
- ```
146
+ 4. **Cleanup order**: reverse of creation (dependent entities first)
208
147
 
209
148
  ---
210
149
 
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:
150
+ ## Seed Data System — Design (Planned)
234
151
 
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)
152
+ See `SEED_DATA_PLAN.md` in the consumer repo for the full design.
238
153
 
239
- This is a core Playwright design principle — fixtures always clean up after themselves.
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
- ## 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
- ---
163
+ ## Multi-DC Portability — Design (Planned)
325
164
 
326
- ## Verification
165
+ See `MULTI_DC_STRATEGY.md` in the consumer repo for the full design.
327
166
 
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)
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
@@ -1,19 +1,19 @@
1
1
  import { createBdd , test } from '@zohodesk/testinglibrary';
2
- import { generateAndCacheTestData } from './DataGeneratorStepsHelper';
2
+ 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, cleanupTracker, executionContext}, module, entityName, generatorName, dataTable) => {
7
- await generateAndCacheTestData(executionContext, "template", generatorName, dataTable, cacheLayer, entityName, null, cleanupTracker);
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);
8
8
  });
9
9
 
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);
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);
12
12
  });
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);
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);
15
15
  });
16
16
 
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);
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);
19
19
  });
@@ -1,30 +1,35 @@
1
1
  import { test } from '@zohodesk/testinglibrary';
2
2
  import DataGenerator from '@zohodesk/testinglibrary/DataGenerator';
3
- import {getUserForSelectedEditionAndProfile} from '@zohodesk/testinglibrary/helpers'
3
+ import {getUserForSelectedEditionAndProfile, getListOfActors} from '@zohodesk/testinglibrary/helpers'
4
4
 
5
5
  const dataGenerator = new DataGenerator();
6
6
 
7
- export async function generateAndCacheTestData(executionContext, type, identifier, dataTable, cacheLayer, entityName, profile = null, cleanupTracker = null) {
7
+ export async function generateAndCacheTestData(executionContext, type, identifier, dataTable, cacheLayer, entityName, profile = null) {
8
8
  let actorInfo;
9
9
  const testInfo = test.info();
10
10
  const scenarioName = testInfo.title.split('/').pop() || 'Unknown Scenario';
11
11
 
12
12
  if (profile) {
13
+ // Explicit profile requested — resolve that profile's credentials
13
14
  const { edition, orgName: portal, beta } = executionContext.actorInfo;
14
15
  actorInfo = await getUserForSelectedEditionAndProfile(edition, profile, beta, portal);
15
16
  } else {
17
+ // Default — use current actor, fall back to org-level data-generator if profile has none
16
18
  actorInfo = executionContext.actorInfo;
19
+ if (!actorInfo['data-generator']) {
20
+ const { edition, orgName: portal, beta } = actorInfo;
21
+ const actorsData = getListOfActors(beta);
22
+ const portalData = actorsData.editions[edition]?.find(e => e.orgName === portal);
23
+ if (portalData?.['data-generator']) {
24
+ actorInfo = { ...actorInfo, 'data-generator': portalData['data-generator'] };
25
+ }
26
+ }
17
27
  }
18
-
19
- const { response, generators } = await dataGenerator.generate(testInfo, actorInfo, type, identifier, scenarioName, dataTable ? dataTable.hashes() : []);
20
- await cacheLayer.set(entityName, response.data);
21
28
 
22
- if (cleanupTracker) {
23
- cleanupTracker.track({
24
- entityName,
25
- generators,
26
- cachedData: response.data,
27
- actorInfo
28
- });
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);
29
34
  }
30
35
  }
@@ -9,9 +9,10 @@ var _path = _interopRequireDefault(require("path"));
9
9
  var _fs = _interopRequireDefault(require("fs"));
10
10
  var _logger = require("../../utils/logger");
11
11
  var _DataGeneratorHelper = require("./DataGeneratorHelper");
12
- var _helpers = require("@zohodesk/testinglibrary/helpers");
13
12
  var _DataGeneratorError = require("./DataGeneratorError");
14
13
  var _readConfigFile = require("../playwright/readConfigFile");
14
+ var _configConstants = _interopRequireDefault(require("../playwright/constants/configConstants"));
15
+ var _ConfigurationHelper = require("../playwright/configuration/ConfigurationHelper");
15
16
  function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); }
16
17
  function _classPrivateFieldInitSpec(e, t, a) { _checkPrivateRedeclaration(e, t), t.set(e, a); }
17
18
  function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); }
@@ -31,7 +32,7 @@ class DataGenerator {
31
32
  if (generatorType === 'API') {
32
33
  generators = await _assertClassBrand(_DataGenerator_brand, this, _generateAPIGenerator).call(this, generatorName);
33
34
  } else {
34
- generators = await _assertClassBrand(_DataGenerator_brand, this, _getGenerator).call(this, testInfo, generatorName);
35
+ generators = await _assertClassBrand(_DataGenerator_brand, this, _getGenerator).call(this, generatorName);
35
36
  }
36
37
  const processedGenerators = await (0, _DataGeneratorHelper.processGenerator)(generators, dataTable);
37
38
  const apiPayload = await _assertClassBrand(_DataGenerator_brand, this, _constructApiPayload).call(this, scenarioName, processedGenerators, actorInfo);
@@ -92,18 +93,13 @@ function _buildIndex(modulesRoot, pattern) {
92
93
  _logger.Logger.log(_logger.Logger.INFO_TYPE, `Generator index built: ${index.size} generators found`);
93
94
  return index;
94
95
  }
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;
96
+ function _getModulesRoot() {
97
+ const stage = (0, _ConfigurationHelper.getRunStage)();
98
+ return _path.default.join(process.cwd(), _configConstants.default.TEST_SLICE_FOLDER, stage, 'modules');
102
99
  }
103
- async function _getGenerator(testInfo, generatorName) {
100
+ async function _getGenerator(generatorName) {
104
101
  if (!_classPrivateFieldGet(_generatorIndex, this)) {
105
- const featureFilePath = await (0, _DataGeneratorHelper.getGeneratorFilePath)(testInfo.file);
106
- const modulesRoot = _assertClassBrand(_DataGenerator_brand, this, _getModulesRoot).call(this, featureFilePath);
102
+ const modulesRoot = _assertClassBrand(_DataGenerator_brand, this, _getModulesRoot).call(this);
107
103
  const {
108
104
  generatorFilePattern: pattern = '*.generators.json'
109
105
  } = (0, _readConfigFile.generateConfigFromFile)();
@@ -5,23 +5,157 @@ Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
7
  exports.default = void 0;
8
- var _CleanupTracker = _interopRequireDefault(require("../../dataGenerator/CleanupTracker"));
9
- var _DataCleanup = _interopRequireDefault(require("../../dataGenerator/DataCleanup"));
10
- const cacheMap = new Map();
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
+ registry[operationId] = config;
40
+ }
41
+ }
42
+ } catch (err) {
43
+ _logger.Logger.log(_logger.Logger.FAILURE_TYPE, `Failed to parse cleanup file: ${fullPath} - ${err.message}`);
44
+ }
45
+ }
46
+ }
47
+ }
48
+ function extractId(cachedData, idPath) {
49
+ const result = _jsonpath.default.query(cachedData, idPath);
50
+ if (result.length === 0) {
51
+ throw new Error(`Could not extract ID using path "${idPath}"`);
52
+ }
53
+ return result[0];
54
+ }
55
+ async function cleanupViaOAS(config, entityId, actorInfo) {
56
+ const dataGeneratorObj = actorInfo['data-generator'];
57
+ if (!dataGeneratorObj) {
58
+ throw new Error('No data-generator config available for cleanup');
59
+ }
60
+ const payload = {
61
+ scenario_name: 'cleanup',
62
+ data_generation_templates: [{
63
+ type: 'dynamic',
64
+ generatorOperationId: config.operationId,
65
+ dataPath: '$.response.body:$',
66
+ name: config.operationId,
67
+ params: {
68
+ id: String(entityId)
69
+ }
70
+ }],
71
+ ...dataGeneratorObj
72
+ };
73
+ if (payload.account) {
74
+ payload.account.email = actorInfo.email;
75
+ payload.account.password = actorInfo.password;
76
+ }
77
+ const environmentDetails = payload.environmentDetails || {};
78
+ environmentDetails.iam_url = process.env.DG_IAM_DOMAIN;
79
+ environmentDetails.host = new URL(process.env.domain).origin;
80
+ payload.environmentDetails = environmentDetails;
81
+ await (0, _DataGeneratorHelper.makeRequest)(process.env.DG_SERVICE_DOMAIN + process.env.DG_SERVICE_API_PATH, payload);
82
+ }
83
+ async function cleanupViaAPI(config, entityId) {
84
+ const url = `${new URL(process.env.domain).origin}${config.apiPath.replace('{id}', entityId)}`;
85
+ const options = {
86
+ method: config.method,
87
+ headers: {
88
+ 'Content-Type': 'application/json'
89
+ }
90
+ };
91
+ if (config.body) {
92
+ options.body = JSON.stringify(config.body);
93
+ }
94
+ const response = await fetch(url, options);
95
+ if (!response.ok) {
96
+ const errorBody = await response.text();
97
+ throw new Error(`${config.method} ${config.apiPath} - status: ${response.status}, body: ${errorBody}`);
98
+ }
99
+ }
11
100
  var _default = exports.default = {
12
101
  // eslint-disable-next-line no-empty-pattern
13
102
  cacheLayer: async ({}, use) => {
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);
103
+ const cache = new Map();
104
+ const cleanupEntries = [];
105
+ cache._trackForCleanup = (entityName, data, generators, actorInfo) => {
106
+ cache.set(entityName, data);
107
+ cleanupEntries.push({
108
+ entityName,
109
+ data,
110
+ generators,
111
+ actorInfo
112
+ });
113
+ };
114
+ await use(cache);
115
+
116
+ // TEARDOWN — runs after scenario ends (pass or fail)
117
+ const {
118
+ autoCleanup = true
119
+ } = (0, _readConfigFile.generateConfigFromFile)();
120
+ if (!autoCleanup || cleanupEntries.length === 0) {
121
+ cache.clear();
122
+ return;
123
+ }
124
+ if (!cleanupRegistry) {
125
+ cleanupRegistry = buildCleanupRegistry();
126
+ }
127
+ _logger.Logger.log(_logger.Logger.INFO_TYPE, `Cleanup started: ${cleanupEntries.length} entities to process`);
128
+ let cleaned = 0;
129
+ let skipped = 0;
130
+ let failed = 0;
131
+ for (const entry of [...cleanupEntries].reverse()) {
132
+ _logger.Logger.log(_logger.Logger.INFO_TYPE, `Cleanup entity: "${entry.entityName}" (${entry.generators.length} steps)`);
133
+ for (const step of [...entry.generators].reverse()) {
134
+ const operationId = step.generatorOperationId;
135
+ const cleanupConfig = cleanupRegistry[operationId];
136
+ if (!cleanupConfig) {
137
+ skipped++;
138
+ continue;
139
+ }
140
+ try {
141
+ const entityId = extractId(entry.data, cleanupConfig.idPath);
142
+ const actionDesc = cleanupConfig.operationId || `${cleanupConfig.method} ${cleanupConfig.apiPath}`;
143
+ _logger.Logger.log(_logger.Logger.INFO_TYPE, `Cleanup [${cleanupConfig.type}] ${step.name}: ${actionDesc} (id: ${entityId})`);
144
+ if (cleanupConfig.type === 'oas') {
145
+ await cleanupViaOAS(cleanupConfig, entityId, entry.actorInfo);
146
+ } else if (cleanupConfig.type === 'api') {
147
+ await cleanupViaAPI(cleanupConfig, entityId);
148
+ }
149
+ cleaned++;
150
+ _logger.Logger.log(_logger.Logger.SUCCESS_TYPE, `Cleanup success: ${step.name} (id: ${entityId})`);
151
+ } catch (err) {
152
+ failed++;
153
+ _logger.Logger.log(_logger.Logger.FAILURE_TYPE, `Cleanup failed: ${step.name} — ${err.message}`);
154
+ }
155
+ }
24
156
  }
25
- tracker.clear();
157
+ _logger.Logger.log(_logger.Logger.INFO_TYPE, `Cleanup completed: ${cleaned} cleaned, ${skipped} skipped (no cleanup rule), ${failed} failed`);
158
+ cleanupEntries.length = 0;
159
+ cache.clear();
26
160
  }
27
161
  };
@@ -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
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@zohodesk/testinglibrary",
3
- "version": "0.0.46-n20-experimental",
3
+ "version": "0.0.48-n20-experimental",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@zohodesk/testinglibrary",
9
- "version": "0.0.46-n20-experimental",
9
+ "version": "0.0.48-n20-experimental",
10
10
  "hasInstallScript": true,
11
11
  "license": "ISC",
12
12
  "dependencies": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zohodesk/testinglibrary",
3
- "version": "0.0.46-n20-experimental",
3
+ "version": "0.0.48-n20-experimental",
4
4
  "main": "./build/index.js",
5
5
  "scripts": {
6
6
  "postinstall": "node bin/postinstall.js",
@@ -1,29 +0,0 @@
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;
@@ -1,98 +0,0 @@
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;