@zohodesk/testinglibrary 0.0.54-n20-experimental → 0.0.58-n20-experimental
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTO_CLEANUP_PLAN.md +171 -0
- package/README.md +8 -0
- package/build/common/data-generator/steps/DataGenerator.spec.js +1 -1
- package/build/common/data-generator/steps/DataGeneratorStepsHelper.js +19 -4
- package/build/core/dataGenerator/DataGenerator.js +104 -25
- package/build/core/dataGenerator/DataGeneratorHelper.js +52 -4
- package/build/core/dataGenerator/validateGenerators.js +82 -0
- package/build/core/playwright/builtInFixtures/cacheLayer.js +197 -2
- package/build/core/playwright/constants/reporterConstants.js +0 -1
- package/build/core/playwright/helpers/auth/getUsers.js +2 -2
- package/build/core/playwright/readConfigFile.js +3 -1
- package/build/core/playwright/report-generator.js +42 -0
- package/build/core/playwright/validateFeature.js +11 -0
- package/build/lib/cli.js +7 -30
- package/build/utils/commonUtils.js +0 -9
- package/build/utils/datePlaceholders.js +170 -0
- package/build/utils/logger.js +3 -1
- package/build/utils/timeFormat.js +41 -0
- package/changelog.md +27 -0
- package/npm-shrinkwrap.json +3383 -7782
- package/package.json +11 -15
- package/build/core/playwright/reporter/PlaywrightReporter.js +0 -44
- package/build/core/playwright/reporter/UnitReporter.js +0 -27
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Data Generator System — Design & Current State
|
|
2
|
+
|
|
3
|
+
## Data Generator Overview
|
|
4
|
+
|
|
5
|
+
### How It Works
|
|
6
|
+
- BDD step: `Given generate a "{Type}" entity "{name}" with generator "{GeneratorName}"`
|
|
7
|
+
- Framework builds a **global generator index** at first use — scans all `*.generators.json` files under `modules/`
|
|
8
|
+
- Generator names must be **unique across the entire UAT suite**
|
|
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`
|
|
17
|
+
|
|
18
|
+
### Generator JSON Structure
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"generators": {
|
|
22
|
+
"GeneratorName": [
|
|
23
|
+
{
|
|
24
|
+
"type": "dynamic",
|
|
25
|
+
"name": "stepName",
|
|
26
|
+
"generatorOperationId": "support.Module.operationName",
|
|
27
|
+
"dataPath": "$.response.body:$",
|
|
28
|
+
"params": { "key": "$previousStep.value" }
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
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
|
+
|
|
45
|
+
### Constraints
|
|
46
|
+
| Constraint | Detail |
|
|
47
|
+
|---|---|
|
|
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 |
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
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 |
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Auto-Cleanup — Design (Not Yet Implemented)
|
|
74
|
+
|
|
75
|
+
### Status
|
|
76
|
+
- **V1 implemented then removed** in `0.0.47` — CleanupTracker + DataCleanup were too tightly coupled
|
|
77
|
+
- **V2 planned** — proper redesign needed
|
|
78
|
+
|
|
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
|
|
91
|
+
|
|
92
|
+
### Cleanup Types (unchanged from V1 design)
|
|
93
|
+
|
|
94
|
+
#### Type 1: `oas` — DG service delete
|
|
95
|
+
```json
|
|
96
|
+
"cleanup": {
|
|
97
|
+
"type": "oas",
|
|
98
|
+
"operationId": "support.Contact.deleteContact",
|
|
99
|
+
"idPath": "$.id"
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### Type 2: `api` — Direct REST API DELETE
|
|
104
|
+
```json
|
|
105
|
+
"cleanup": {
|
|
106
|
+
"type": "api",
|
|
107
|
+
"method": "DELETE",
|
|
108
|
+
"apiPath": "/api/v1/contracts/{id}",
|
|
109
|
+
"idPath": "$.id"
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### Type 3: `api` — Disable via PATCH
|
|
114
|
+
```json
|
|
115
|
+
"cleanup": {
|
|
116
|
+
"type": "api",
|
|
117
|
+
"method": "PATCH",
|
|
118
|
+
"apiPath": "/api/v1/webhooks/{id}",
|
|
119
|
+
"idPath": "$.id",
|
|
120
|
+
"body": { "isActive": false }
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
#### No cleanup — read-only operations
|
|
125
|
+
Steps without `cleanup` property are skipped (e.g., `getDepartments`).
|
|
126
|
+
|
|
127
|
+
### V2 Implementation Plan
|
|
128
|
+
|
|
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
|
|
133
|
+
|
|
134
|
+
2. **Config flag** in `uat.config.js`:
|
|
135
|
+
```javascript
|
|
136
|
+
autoCleanup: true, // default: true
|
|
137
|
+
cleanupTimeout: 30000 // per-entity timeout
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
3. **Per-scenario opt-out** via tag:
|
|
141
|
+
```gherkin
|
|
142
|
+
@skip_cleanup
|
|
143
|
+
Scenario: Test that needs data to persist
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
4. **Cleanup order**: reverse of creation (dependent entities first)
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Seed Data System — Design (Planned)
|
|
151
|
+
|
|
152
|
+
See `SEED_DATA_PLAN.md` in the consumer repo for the full design.
|
|
153
|
+
|
|
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
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Multi-DC Portability — Design (Planned)
|
|
164
|
+
|
|
165
|
+
See `MULTI_DC_STRATEGY.md` in the consumer repo for the full design.
|
|
166
|
+
|
|
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
|
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,6 +1,6 @@
|
|
|
1
1
|
import { test } from '@zohodesk/testinglibrary';
|
|
2
2
|
import DataGenerator from '@zohodesk/testinglibrary/DataGenerator';
|
|
3
|
-
|
|
3
|
+
import {getUserForSelectedEditionAndProfile, getListOfActors} from '@zohodesk/testinglibrary/helpers'
|
|
4
4
|
|
|
5
5
|
const dataGenerator = new DataGenerator();
|
|
6
6
|
|
|
@@ -10,12 +10,27 @@ 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
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { response, generators } = await dataGenerator.generate(testInfo, actorInfo, type, identifier, scenarioName, dataTable ? dataTable.hashes() : [], cacheLayer);
|
|
30
|
+
if (cacheLayer._trackForCleanup) {
|
|
31
|
+
cacheLayer._trackForCleanup(entityName, response.data, generators, actorInfo, response.logs, identifier);
|
|
32
|
+
} else {
|
|
33
|
+
cacheLayer.set(entityName, response.data);
|
|
34
|
+
cacheLayer.set(`${entityName}_logs`, response.logs);
|
|
17
35
|
}
|
|
18
|
-
|
|
19
|
-
const generatedData = await dataGenerator.generate(testInfo, actorInfo, type, identifier, scenarioName, dataTable ? dataTable.hashes() : []);
|
|
20
|
-
await cacheLayer.set(entityName, generatedData.data);
|
|
21
36
|
}
|
|
@@ -8,61 +8,140 @@ exports.default = void 0;
|
|
|
8
8
|
var _path = _interopRequireDefault(require("path"));
|
|
9
9
|
var _fs = _interopRequireDefault(require("fs"));
|
|
10
10
|
var _logger = require("../../utils/logger");
|
|
11
|
+
var _timeFormat = require("../../utils/timeFormat");
|
|
11
12
|
var _DataGeneratorHelper = require("./DataGeneratorHelper");
|
|
12
|
-
var
|
|
13
|
+
var _datePlaceholders = require("../../utils/datePlaceholders");
|
|
13
14
|
var _DataGeneratorError = require("./DataGeneratorError");
|
|
15
|
+
var _readConfigFile = require("../playwright/readConfigFile");
|
|
16
|
+
var _configConstants = _interopRequireDefault(require("../playwright/constants/configConstants"));
|
|
17
|
+
var _ConfigurationHelper = require("../playwright/configuration/ConfigurationHelper");
|
|
14
18
|
function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); }
|
|
19
|
+
function _classPrivateFieldInitSpec(e, t, a) { _checkPrivateRedeclaration(e, t), t.set(e, a); }
|
|
15
20
|
function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); }
|
|
21
|
+
function _classPrivateFieldSet(s, a, r) { return s.set(_assertClassBrand(s, a), r), r; }
|
|
22
|
+
function _classPrivateFieldGet(s, a) { return s.get(_assertClassBrand(s, a)); }
|
|
16
23
|
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"); }
|
|
24
|
+
var _generatorIndex = /*#__PURE__*/new WeakMap();
|
|
17
25
|
var _DataGenerator_brand = /*#__PURE__*/new WeakSet();
|
|
18
26
|
class DataGenerator {
|
|
19
27
|
constructor() {
|
|
20
28
|
_classPrivateMethodInitSpec(this, _DataGenerator_brand);
|
|
29
|
+
_classPrivateFieldInitSpec(this, _generatorIndex, null);
|
|
21
30
|
}
|
|
22
|
-
async generate(testInfo, actorInfo, generatorType, generatorName, scenarioName, dataTable) {
|
|
31
|
+
async generate(testInfo, actorInfo, generatorType, generatorName, scenarioName, dataTable, cacheLayer) {
|
|
32
|
+
const startMs = Date.now();
|
|
33
|
+
const startLabel = (0, _timeFormat.formatTimestamp)(startMs);
|
|
34
|
+
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Data generation started | generator="${generatorName}" | scenario="${scenarioName}" | startTime=${startLabel}`);
|
|
23
35
|
try {
|
|
24
36
|
let generators;
|
|
25
37
|
if (generatorType === 'API') {
|
|
26
38
|
generators = await _assertClassBrand(_DataGenerator_brand, this, _generateAPIGenerator).call(this, generatorName);
|
|
27
39
|
} else {
|
|
28
|
-
generators = await _assertClassBrand(_DataGenerator_brand, this, _getGenerator).call(this,
|
|
40
|
+
generators = await _assertClassBrand(_DataGenerator_brand, this, _getGenerator).call(this, generatorName);
|
|
29
41
|
}
|
|
30
|
-
|
|
42
|
+
|
|
43
|
+
// Clone to prevent cross-test mutation of the cached generator index.
|
|
44
|
+
const generatorsCopy = generators.map(g => ({
|
|
45
|
+
...g,
|
|
46
|
+
params: g.params ? {
|
|
47
|
+
...g.params
|
|
48
|
+
} : undefined
|
|
49
|
+
}));
|
|
50
|
+
(0, _datePlaceholders.resolveDatePlaceholdersInGenerators)(generatorsCopy);
|
|
51
|
+
const dateResolvedTable = (0, _datePlaceholders.resolveDatePlaceholders)(dataTable);
|
|
52
|
+
const resolvedDataTable = await (0, _DataGeneratorHelper.resolveCacheReferences)(dateResolvedTable, cacheLayer);
|
|
53
|
+
const processedGenerators = await (0, _DataGeneratorHelper.processGenerator)(generatorsCopy, resolvedDataTable);
|
|
31
54
|
const apiPayload = await _assertClassBrand(_DataGenerator_brand, this, _constructApiPayload).call(this, scenarioName, processedGenerators, actorInfo);
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
55
|
+
const featureFlags = process.env.featureflags ? process.env.featureflags : '';
|
|
56
|
+
const headers = {
|
|
57
|
+
'Content-Type': 'application/json',
|
|
58
|
+
'featureflags': featureFlags
|
|
59
|
+
};
|
|
60
|
+
_logger.Logger.log(_logger.Logger.INFO_TYPE, 'Making request headers:', headers);
|
|
61
|
+
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Payload: ${JSON.stringify(apiPayload, null, 4)}`);
|
|
62
|
+
const response = await (0, _DataGeneratorHelper.makeRequest)(process.env.DG_SERVICE_DOMAIN + process.env.DG_SERVICE_API_PATH, apiPayload, headers);
|
|
63
|
+
const endMs = Date.now();
|
|
64
|
+
const endLabel = (0, _timeFormat.formatTimestamp)(endMs);
|
|
65
|
+
const totalLabel = (0, _timeFormat.formatDuration)(endMs - startMs);
|
|
66
|
+
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Generated response for the generator: ${generatorName} for scenario: ${scenarioName}, Response: ${JSON.stringify(response, null, 4)}`);
|
|
67
|
+
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Data generation completed | generator="${generatorName}" | scenario="${scenarioName}" | startTime=${startLabel} | endTime=${endLabel} | totalTime=${totalLabel}`);
|
|
68
|
+
return {
|
|
69
|
+
response,
|
|
70
|
+
generators
|
|
71
|
+
};
|
|
35
72
|
} catch (error) {
|
|
73
|
+
const endMs = Date.now();
|
|
74
|
+
const endLabel = (0, _timeFormat.formatTimestamp)(endMs);
|
|
75
|
+
const totalLabel = (0, _timeFormat.formatDuration)(endMs - startMs);
|
|
76
|
+
_logger.Logger.log(_logger.Logger.FAILURE_TYPE, `Data generation failed | generator="${generatorName}" | scenario="${scenarioName}" | startTime=${startLabel} | endTime=${endLabel} | totalTime=${totalLabel}`);
|
|
36
77
|
if (error instanceof _DataGeneratorError.DataGeneratorError) {
|
|
37
|
-
|
|
38
|
-
|
|
78
|
+
_logger.Logger.log(_logger.Logger.FAILURE_TYPE, error.getMessage());
|
|
79
|
+
_logger.Logger.log(_logger.Logger.FAILURE_TYPE, "Stack trace:", error.stack);
|
|
39
80
|
_logger.Logger.log(_logger.Logger.FAILURE_TYPE, error.getMessage());
|
|
40
81
|
} else {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
82
|
+
_logger.Logger.log(_logger.Logger.FAILURE_TYPE, "Error Type:", error.constructor.name);
|
|
83
|
+
_logger.Logger.log(_logger.Logger.FAILURE_TYPE, "Error Message:", error.message);
|
|
84
|
+
_logger.Logger.log(_logger.Logger.FAILURE_TYPE, "Stack trace:", error.stack);
|
|
44
85
|
_logger.Logger.log(_logger.Logger.FAILURE_TYPE, `${error.constructor.name} - Message: ${error.message}`);
|
|
45
86
|
}
|
|
46
|
-
|
|
87
|
+
_logger.Logger.log(_logger.Logger.FAILURE_TYPE, `Data Generation failed for the generator: ${generatorName}\n\nError response: ${error}`);
|
|
47
88
|
throw error;
|
|
48
89
|
}
|
|
49
90
|
}
|
|
50
91
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
92
|
+
function _matchesPattern(filename, pattern) {
|
|
93
|
+
const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
|
|
94
|
+
return regex.test(filename);
|
|
95
|
+
}
|
|
96
|
+
function _scanDir(dir, index, pattern) {
|
|
97
|
+
const entries = _fs.default.readdirSync(dir, {
|
|
98
|
+
withFileTypes: true
|
|
99
|
+
});
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
const fullPath = _path.default.join(dir, entry.name);
|
|
102
|
+
if (entry.isDirectory()) {
|
|
103
|
+
_assertClassBrand(_DataGenerator_brand, this, _scanDir).call(this, fullPath, index, pattern);
|
|
104
|
+
} else if (_assertClassBrand(_DataGenerator_brand, this, _matchesPattern).call(this, entry.name, pattern)) {
|
|
105
|
+
try {
|
|
106
|
+
const data = _fs.default.readFileSync(fullPath, 'utf8');
|
|
107
|
+
const obj = JSON.parse(data);
|
|
108
|
+
if (obj.generators) {
|
|
109
|
+
for (const [name, config] of Object.entries(obj.generators)) {
|
|
110
|
+
if (!index.has(name)) {
|
|
111
|
+
index.set(name, config);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
_logger.Logger.log(_logger.Logger.FAILURE_TYPE, `Failed to parse generator file: ${fullPath} - ${err.message}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function _buildIndex(modulesRoot, pattern) {
|
|
122
|
+
const index = new Map();
|
|
123
|
+
_assertClassBrand(_DataGenerator_brand, this, _scanDir).call(this, modulesRoot, index, pattern);
|
|
124
|
+
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Generator index built: ${index.size} generators found`);
|
|
125
|
+
return index;
|
|
126
|
+
}
|
|
127
|
+
function _getModulesRoot() {
|
|
128
|
+
const stage = (0, _ConfigurationHelper.getRunStage)();
|
|
129
|
+
return _path.default.join(process.cwd(), _configConstants.default.TEST_SLICE_FOLDER, stage, 'modules');
|
|
130
|
+
}
|
|
131
|
+
async function _getGenerator(generatorName) {
|
|
132
|
+
if (!_classPrivateFieldGet(_generatorIndex, this)) {
|
|
133
|
+
const modulesRoot = _assertClassBrand(_DataGenerator_brand, this, _getModulesRoot).call(this);
|
|
134
|
+
const {
|
|
135
|
+
generatorFilePattern: pattern = '*.generators.json'
|
|
136
|
+
} = (0, _readConfigFile.generateConfigFromFile)();
|
|
137
|
+
if (modulesRoot) {
|
|
138
|
+
_classPrivateFieldSet(_generatorIndex, this, _assertClassBrand(_DataGenerator_brand, this, _buildIndex).call(this, modulesRoot, pattern));
|
|
60
139
|
}
|
|
61
140
|
}
|
|
62
|
-
if (
|
|
63
|
-
|
|
141
|
+
if (_classPrivateFieldGet(_generatorIndex, this) && _classPrivateFieldGet(_generatorIndex, this).has(generatorName)) {
|
|
142
|
+
return _classPrivateFieldGet(_generatorIndex, this).get(generatorName);
|
|
64
143
|
}
|
|
65
|
-
|
|
144
|
+
throw new _DataGeneratorError.GeneratorError(`Generator "${generatorName}" could not be found in any generator file`);
|
|
66
145
|
}
|
|
67
146
|
async function _generateAPIGenerator(operationId) {
|
|
68
147
|
return [{
|
|
@@ -6,6 +6,54 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.getGeneratorFilePath = getGeneratorFilePath;
|
|
7
7
|
exports.makeRequest = makeRequest;
|
|
8
8
|
exports.processGenerator = processGenerator;
|
|
9
|
+
exports.resolveCacheReferences = resolveCacheReferences;
|
|
10
|
+
// Matches a single ${EntityName} or ${EntityName.field.path} cell value.
|
|
11
|
+
// Anchored — values like "foo ${E1.id} bar" are left as-is on purpose.
|
|
12
|
+
const CACHE_REF = /^\$\{([A-Za-z_][\w-]*)(?:\.([A-Za-z_][\w.-]*))?\}$/;
|
|
13
|
+
|
|
14
|
+
// Resolve ${EntityName} / ${EntityName.field.path} references in a dataTable
|
|
15
|
+
// against the cacheLayer. Lets a later `generate ...` step reference an entity
|
|
16
|
+
// that an earlier step cached (e.g. ${E1.id} for an event created in a prior
|
|
17
|
+
// generator invocation).
|
|
18
|
+
async function resolveCacheReferences(rows, cacheLayer) {
|
|
19
|
+
if (!rows || !rows.length || !cacheLayer || typeof cacheLayer.get !== 'function') {
|
|
20
|
+
return rows || [];
|
|
21
|
+
}
|
|
22
|
+
const out = [];
|
|
23
|
+
for (const row of rows) {
|
|
24
|
+
const resolved = {};
|
|
25
|
+
for (const [key, value] of Object.entries(row)) {
|
|
26
|
+
const match = typeof value === 'string' ? value.match(CACHE_REF) : null;
|
|
27
|
+
if (!match) {
|
|
28
|
+
resolved[key] = value;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const [, entityName, fieldPath] = match;
|
|
32
|
+
const entity = await cacheLayer.get(entityName);
|
|
33
|
+
if (entity === undefined || entity === null) {
|
|
34
|
+
throw new Error(`DataGenerator: ${value} references entity "${entityName}" which is not in the cache.`);
|
|
35
|
+
}
|
|
36
|
+
if (!fieldPath) {
|
|
37
|
+
resolved[key] = entity;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
let acc = entity;
|
|
41
|
+
for (const part of fieldPath.split('.')) {
|
|
42
|
+
if (acc === undefined || acc === null) {
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
acc = acc[part];
|
|
46
|
+
}
|
|
47
|
+
if (acc === undefined) {
|
|
48
|
+
throw new Error(`DataGenerator: ${value} resolved to undefined (entity "${entityName}" has no path "${fieldPath}").`);
|
|
49
|
+
}
|
|
50
|
+
resolved[key] = acc;
|
|
51
|
+
}
|
|
52
|
+
out.push(resolved);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
9
57
|
//Create payload for the generators
|
|
10
58
|
async function processGenerator(generators, dataTable) {
|
|
11
59
|
if (!dataTable) {
|
|
@@ -29,12 +77,12 @@ async function processGenerator(generators, dataTable) {
|
|
|
29
77
|
return generator;
|
|
30
78
|
});
|
|
31
79
|
}
|
|
32
|
-
async function makeRequest(url, payload
|
|
80
|
+
async function makeRequest(url, payload, headers = {
|
|
81
|
+
'Content-Type': 'application/json'
|
|
82
|
+
}) {
|
|
33
83
|
const response = await fetch(url, {
|
|
34
84
|
method: 'POST',
|
|
35
|
-
headers:
|
|
36
|
-
'Content-Type': 'application/json'
|
|
37
|
-
},
|
|
85
|
+
headers: headers,
|
|
38
86
|
body: JSON.stringify(payload)
|
|
39
87
|
});
|
|
40
88
|
if (!response.ok) {
|
|
@@ -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
|
+
}
|