@zohodesk/testinglibrary 0.0.45-n20-experimental → 0.0.47-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)
package/README.md CHANGED
@@ -17,6 +17,14 @@
17
17
 
18
18
  - npm run report
19
19
 
20
+ ### v0.0.44-n20-experimental - 23-03-2026
21
+
22
+ #### Enhancement
23
+ - DataGenerator now walks up the directory tree to discover data-generators folders
24
+ - Supports multiple JSON files in data-generators directory (no longer hardcoded to generators.json)
25
+ - Removed hardcoded folder name dependencies for flexible generator file placement
26
+ - Added duplicate generator name validation to `ZDTestingFramework validate` command
27
+
20
28
  ### v4.1.1/v3.3.0 - 28-01-2026
21
29
 
22
30
  #### Features
@@ -1,5 +1,5 @@
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
 
@@ -1,6 +1,6 @@
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
 
@@ -10,12 +10,22 @@ export async function generateAndCacheTestData(executionContext, type, identifie
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 generatedData = await dataGenerator.generate(testInfo, actorInfo, type, identifier, scenarioName, dataTable ? dataTable.hashes() : []);
20
- await cacheLayer.set(entityName, generatedData.data);
28
+
29
+ const { response } = await dataGenerator.generate(testInfo, actorInfo, type, identifier, scenarioName, dataTable ? dataTable.hashes() : []);
30
+ await cacheLayer.set(entityName, response.data);
21
31
  }
@@ -9,15 +9,22 @@ 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");
13
+ var _readConfigFile = require("../playwright/readConfigFile");
14
+ var _configConstants = _interopRequireDefault(require("../playwright/constants/configConstants"));
15
+ var _ConfigurationHelper = require("../playwright/configuration/ConfigurationHelper");
14
16
  function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); }
17
+ function _classPrivateFieldInitSpec(e, t, a) { _checkPrivateRedeclaration(e, t), t.set(e, a); }
15
18
  function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); }
19
+ function _classPrivateFieldSet(s, a, r) { return s.set(_assertClassBrand(s, a), r), r; }
20
+ function _classPrivateFieldGet(s, a) { return s.get(_assertClassBrand(s, a)); }
16
21
  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"); }
22
+ var _generatorIndex = /*#__PURE__*/new WeakMap();
17
23
  var _DataGenerator_brand = /*#__PURE__*/new WeakSet();
18
24
  class DataGenerator {
19
25
  constructor() {
20
26
  _classPrivateMethodInitSpec(this, _DataGenerator_brand);
27
+ _classPrivateFieldInitSpec(this, _generatorIndex, null);
21
28
  }
22
29
  async generate(testInfo, actorInfo, generatorType, generatorName, scenarioName, dataTable) {
23
30
  try {
@@ -25,13 +32,16 @@ class DataGenerator {
25
32
  if (generatorType === 'API') {
26
33
  generators = await _assertClassBrand(_DataGenerator_brand, this, _generateAPIGenerator).call(this, generatorName);
27
34
  } else {
28
- generators = await _assertClassBrand(_DataGenerator_brand, this, _getGenerator).call(this, testInfo, generatorName);
35
+ generators = await _assertClassBrand(_DataGenerator_brand, this, _getGenerator).call(this, generatorName);
29
36
  }
30
37
  const processedGenerators = await (0, _DataGeneratorHelper.processGenerator)(generators, dataTable);
31
38
  const apiPayload = await _assertClassBrand(_DataGenerator_brand, this, _constructApiPayload).call(this, scenarioName, processedGenerators, actorInfo);
32
39
  const response = await (0, _DataGeneratorHelper.makeRequest)(process.env.DG_SERVICE_DOMAIN + process.env.DG_SERVICE_API_PATH, apiPayload);
33
40
  _logger.Logger.log(_logger.Logger.INFO_TYPE, `Generated response for the generator: ${generatorName} for scenario: ${scenarioName}, Response: ${JSON.stringify(response)}`);
34
- return response;
41
+ return {
42
+ response,
43
+ generators
44
+ };
35
45
  } catch (error) {
36
46
  if (error instanceof _DataGeneratorError.DataGeneratorError) {
37
47
  console.error(error.getMessage());
@@ -48,21 +58,59 @@ class DataGenerator {
48
58
  }
49
59
  }
50
60
  }
51
- async function _getGenerator(testInfo, generatorName) {
52
- let generator = null;
53
- let generatorFilePath = await (0, _DataGeneratorHelper.getGeneratorFilePath)(testInfo.file);
54
- generatorFilePath = _path.default.join(generatorFilePath, "../../data-generators/generators.json");
55
- if (_fs.default.existsSync(generatorFilePath)) {
56
- const data = _fs.default.readFileSync(generatorFilePath, 'utf8');
57
- const generatorObj = JSON.parse(data);
58
- if (generatorName || generatorObj.generators) {
59
- generator = generatorObj.generators[generatorName] || null;
61
+ function _matchesPattern(filename, pattern) {
62
+ const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
63
+ return regex.test(filename);
64
+ }
65
+ function _scanDir(dir, index, pattern) {
66
+ const entries = _fs.default.readdirSync(dir, {
67
+ withFileTypes: true
68
+ });
69
+ for (const entry of entries) {
70
+ const fullPath = _path.default.join(dir, entry.name);
71
+ if (entry.isDirectory()) {
72
+ _assertClassBrand(_DataGenerator_brand, this, _scanDir).call(this, fullPath, index, pattern);
73
+ } else if (_assertClassBrand(_DataGenerator_brand, this, _matchesPattern).call(this, entry.name, pattern)) {
74
+ try {
75
+ const data = _fs.default.readFileSync(fullPath, 'utf8');
76
+ const obj = JSON.parse(data);
77
+ if (obj.generators) {
78
+ for (const [name, config] of Object.entries(obj.generators)) {
79
+ if (!index.has(name)) {
80
+ index.set(name, config);
81
+ }
82
+ }
83
+ }
84
+ } catch (err) {
85
+ _logger.Logger.log(_logger.Logger.FAILURE_TYPE, `Failed to parse generator file: ${fullPath} - ${err.message}`);
86
+ }
87
+ }
88
+ }
89
+ }
90
+ function _buildIndex(modulesRoot, pattern) {
91
+ const index = new Map();
92
+ _assertClassBrand(_DataGenerator_brand, this, _scanDir).call(this, modulesRoot, index, pattern);
93
+ _logger.Logger.log(_logger.Logger.INFO_TYPE, `Generator index built: ${index.size} generators found`);
94
+ return index;
95
+ }
96
+ function _getModulesRoot() {
97
+ const stage = (0, _ConfigurationHelper.getRunStage)();
98
+ return _path.default.join(process.cwd(), _configConstants.default.TEST_SLICE_FOLDER, stage, 'modules');
99
+ }
100
+ async function _getGenerator(generatorName) {
101
+ if (!_classPrivateFieldGet(_generatorIndex, this)) {
102
+ const modulesRoot = _assertClassBrand(_DataGenerator_brand, this, _getModulesRoot).call(this);
103
+ const {
104
+ generatorFilePattern: pattern = '*.generators.json'
105
+ } = (0, _readConfigFile.generateConfigFromFile)();
106
+ if (modulesRoot) {
107
+ _classPrivateFieldSet(_generatorIndex, this, _assertClassBrand(_DataGenerator_brand, this, _buildIndex).call(this, modulesRoot, pattern));
60
108
  }
61
109
  }
62
- if (!generator) {
63
- throw new _DataGeneratorError.GeneratorError(`Generator "${generatorName}" could not be found in the path located at "${generatorFilePath}"`);
110
+ if (_classPrivateFieldGet(_generatorIndex, this) && _classPrivateFieldGet(_generatorIndex, this).has(generatorName)) {
111
+ return _classPrivateFieldGet(_generatorIndex, this).get(generatorName);
64
112
  }
65
- return generator;
113
+ throw new _DataGeneratorError.GeneratorError(`Generator "${generatorName}" could not be found in any generator file`);
66
114
  }
67
115
  async function _generateAPIGenerator(operationId) {
68
116
  return [{
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.validateGenerators = validateGenerators;
8
+ var _path = _interopRequireDefault(require("path"));
9
+ var _fs = _interopRequireDefault(require("fs"));
10
+ var _logger = require("../../utils/logger");
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 = []) {
17
+ const entries = _fs.default.readdirSync(dir, {
18
+ withFileTypes: true
19
+ });
20
+ for (const entry of entries) {
21
+ const fullPath = _path.default.join(dir, entry.name);
22
+ if (entry.isDirectory()) {
23
+ findGeneratorFiles(fullPath, pattern, results);
24
+ } else if (matchesPattern(entry.name, pattern)) {
25
+ results.push(fullPath);
26
+ }
27
+ }
28
+ return results;
29
+ }
30
+ function validateGenerators(modulesRoot) {
31
+ if (!_fs.default.existsSync(modulesRoot)) {
32
+ _logger.Logger.log(_logger.Logger.INFO_TYPE, `Modules directory not found: ${modulesRoot}. Skipping generator validation.`);
33
+ return {
34
+ valid: true,
35
+ duplicates: []
36
+ };
37
+ }
38
+ const {
39
+ generatorFilePattern: pattern = '*.generators.json'
40
+ } = (0, _readConfigFile.generateConfigFromFile)();
41
+ const generatorMap = {};
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] = [];
51
+ }
52
+ generatorMap[name].push(filePath);
53
+ }
54
+ }
55
+ } catch (err) {
56
+ _logger.Logger.log(_logger.Logger.FAILURE_TYPE, `Failed to parse generator file: ${filePath} - ${err.message}`);
57
+ }
58
+ }
59
+ const duplicates = Object.entries(generatorMap).filter(([, files]) => files.length > 1).map(([name, files]) => ({
60
+ name,
61
+ files
62
+ }));
63
+ if (duplicates.length > 0) {
64
+ _logger.Logger.log(_logger.Logger.FAILURE_TYPE, 'Duplicate generator names found:');
65
+ for (const dup of duplicates) {
66
+ _logger.Logger.log(_logger.Logger.FAILURE_TYPE, ` Generator "${dup.name}" defined in:`);
67
+ for (const file of dup.files) {
68
+ _logger.Logger.log(_logger.Logger.FAILURE_TYPE, ` - ${file}`);
69
+ }
70
+ }
71
+ return {
72
+ valid: false,
73
+ duplicates
74
+ };
75
+ }
76
+ const generatorCount = Object.keys(generatorMap).length;
77
+ _logger.Logger.log(_logger.Logger.SUCCESS_TYPE, `Generator validation passed. ${generatorCount} unique generators found across ${generatorFiles.length} generator files.`);
78
+ return {
79
+ valid: true,
80
+ duplicates: []
81
+ };
82
+ }
@@ -11,6 +11,5 @@ const stage = (0, _ConfigurationHelper.getRunStage)();
11
11
  class ReporterConstants {
12
12
  static DEFAULT_REPORTER_PATH = `${_configConstants.default.TEST_SLICE_FOLDER}/${stage}/test-results/playwright-test-results.json`;
13
13
  static LAST_RUN_REPORTER_PATH = `${_configConstants.default.TEST_SLICE_FOLDER}/${stage}/test-results/.last-run.json`;
14
- static DEFAULT_UNIT_TEST_REPORTER_PATH = `${_configConstants.default.TEST_SLICE_FOLDER}/unit-test/unit_reports/report.html`;
15
14
  }
16
15
  exports.default = ReporterConstants;
@@ -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) {
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+
3
+ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4
+ Object.defineProperty(exports, "__esModule", {
5
+ value: true
6
+ });
7
+ exports.default = generateReport;
8
+ var _child_process = require("child_process");
9
+ var _path = _interopRequireDefault(require("path"));
10
+ var _logger = require("../../utils/logger");
11
+ var _rootPath = require("../../utils/rootPath");
12
+ var _readConfigFile = require("./readConfigFile");
13
+ async function generateReport() {
14
+ // await preProcessReport()
15
+ const userArgs = process.argv.slice(3);
16
+ const playwrightPath = _path.default.resolve((0, _rootPath.getExecutableBinaryPath)('playwright'));
17
+ const command = playwrightPath;
18
+ const {
19
+ reportPath: htmlPath
20
+ } = (0, _readConfigFile.generateConfigFromFile)();
21
+ const args = ['show-report', htmlPath].concat(userArgs);
22
+ const childProcess = (0, _child_process.spawn)(command, args, {
23
+ stdio: 'inherit'
24
+ });
25
+ childProcess.on('error', error => {
26
+ _logger.Logger.log(_logger.Logger.FAILURE_TYPE, error);
27
+ });
28
+ childProcess.on('exit', (code, signal) => {
29
+ _logger.Logger.log(_logger.Logger.FAILURE_TYPE, `Child Process Exited with Code ${code} and Signal ${signal}`);
30
+ process.exit();
31
+ });
32
+ process.on('exit', () => {
33
+ _logger.Logger.log(_logger.Logger.INFO_TYPE, 'Terminating Playwright Process...');
34
+ childProcess.kill();
35
+ return;
36
+ });
37
+ process.on('SIGINT', () => {
38
+ _logger.Logger.log(_logger.Logger.INFO_TYPE, 'Cleaning up...');
39
+ childProcess.kill();
40
+ process.exit();
41
+ });
42
+ }
@@ -5,11 +5,15 @@ Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
7
  exports.default = void 0;
8
+ var _path = _interopRequireDefault(require("path"));
8
9
  var _parseUserArgs = _interopRequireDefault(require("./helpers/parseUserArgs"));
9
10
  var _readConfigFile = require("./readConfigFile");
10
11
  var _tagProcessor = _interopRequireDefault(require("./tagProcessor"));
11
12
  var _testRunner = require("./test-runner");
12
13
  var _logger = require("../../utils/logger");
14
+ var _validateGenerators = require("../dataGenerator/validateGenerators");
15
+ var _configConstants = _interopRequireDefault(require("./constants/configConstants"));
16
+ var _ConfigurationHelper = require("./configuration/ConfigurationHelper");
13
17
  const validateFeatureFiles = () => {
14
18
  const userArgsObject = (0, _parseUserArgs.default)();
15
19
  const uatConfig = (0, _readConfigFile.generateConfigFromFile)();
@@ -17,6 +21,13 @@ const validateFeatureFiles = () => {
17
21
  editionOrder
18
22
  } = uatConfig;
19
23
  const configPath = (0, _readConfigFile.isUserConfigFileAvailable)() ? require.resolve('./setup/config-creator.js') : require.resolve('../../../playwright.config.js');
24
+ const stage = (0, _ConfigurationHelper.getRunStage)();
25
+ const modulesRoot = _path.default.join(process.cwd(), _configConstants.default.TEST_SLICE_FOLDER, stage, 'modules');
26
+ const generatorResult = (0, _validateGenerators.validateGenerators)(modulesRoot);
27
+ if (!generatorResult.valid) {
28
+ _logger.Logger.log(_logger.Logger.FAILURE_TYPE, 'Generator validation failed. Fix duplicate generator names before running tests.');
29
+ process.exit(1);
30
+ }
20
31
  const tagProcessor = new _tagProcessor.default(editionOrder);
21
32
  const tagArgs = tagProcessor.processTags(userArgsObject);
22
33
  (0, _testRunner.runPreprocessing)(tagArgs, configPath).then(() => {