@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.
@@ -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 generatedData = await dataGenerator.generate(testInfo, actorInfo, type, identifier, scenarioName, dataTable ? dataTable.hashes() : []);
20
- await cacheLayer.set(entityName, generatedData.data);
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 response;
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
- async function _getGenerator(testInfo, generatorName) {
52
- let featureFilePath = await (0, _DataGeneratorHelper.getGeneratorFilePath)(testInfo.file);
53
- let searchDir = _path.default.dirname(featureFilePath);
54
- const rootDir = _path.default.parse(searchDir).root;
55
- while (searchDir !== rootDir) {
56
- const dataGeneratorsDir = _path.default.join(searchDir, "data-generators");
57
- if (_fs.default.existsSync(dataGeneratorsDir)) {
58
- const jsonFiles = _fs.default.readdirSync(dataGeneratorsDir).filter(file => file.endsWith('.json'));
59
- for (const file of jsonFiles) {
60
- const filePath = _path.default.join(dataGeneratorsDir, file);
61
- const data = _fs.default.readFileSync(filePath, 'utf8');
62
- const generatorObj = JSON.parse(data);
63
- if (generatorObj.generators && generatorObj.generators[generatorName]) {
64
- return generatorObj.generators[generatorName];
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
- throw new _DataGeneratorError.GeneratorError(`Generator "${generatorName}" could not be found in any data-generators directory from "${_path.default.dirname(featureFilePath)}"`);
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
- function findDataGeneratorDirs(dir, results = []) {
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
- const fullPath = _path.default.join(dir, entry.name);
18
- if (entry.name === 'data-generators') {
19
- results.push(fullPath);
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 dataGenDirs = findDataGeneratorDirs(modulesRoot);
37
- for (const dataGenDir of dataGenDirs) {
38
- const jsonFiles = _fs.default.readdirSync(dataGenDir).filter(file => file.endsWith('.json'));
39
- for (const file of jsonFiles) {
40
- const filePath = _path.default.join(dataGenDir, file);
41
- try {
42
- const data = _fs.default.readFileSync(filePath, 'utf8');
43
- const generatorObj = JSON.parse(data);
44
- if (generatorObj.generators) {
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 ${dataGenDirs.length} data-generators directories.`);
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) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@zohodesk/testinglibrary",
3
- "version": "0.0.44-n20-experimental",
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.44-n20-experimental",
9
+ "version": "0.0.46-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.44-n20-experimental",
3
+ "version": "0.0.46-n20-experimental",
4
4
  "main": "./build/index.js",
5
5
  "scripts": {
6
6
  "postinstall": "node bin/postinstall.js",