@zohodesk/testinglibrary 0.0.57-n20-experimental → 0.0.59-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/README.md +0 -8
- package/build/common/data-generator/steps/DataGenerator.spec.js +1 -1
- package/build/common/data-generator/steps/DataGeneratorStepsHelper.js +4 -19
- package/build/core/dataGenerator/DataGenerator.js +25 -93
- package/build/core/dataGenerator/DataGeneratorHelper.js +4 -52
- package/build/core/playwright/builtInFixtures/cacheLayer.js +2 -197
- package/build/core/playwright/helpers/auth/getUsers.js +2 -2
- package/build/core/playwright/readConfigFile.js +2 -2
- package/build/core/playwright/setup/custom-reporter.js +72 -1
- package/build/core/playwright/validateFeature.js +0 -11
- package/build/utils/logger.js +1 -3
- package/changelog.md +0 -27
- package/npm-shrinkwrap.json +70 -70
- package/package.json +1 -1
- package/.claude/worktrees/thirsty-yalow/.babelrc +0 -24
- package/.claude/worktrees/thirsty-yalow/.eslintrc.js +0 -31
- package/.claude/worktrees/thirsty-yalow/.gitlab-ci.yml +0 -208
- package/.claude/worktrees/thirsty-yalow/.prettierrc +0 -6
- package/.claude/worktrees/thirsty-yalow/README.md +0 -234
- package/.claude/worktrees/thirsty-yalow/bin/cli.js +0 -3
- package/.claude/worktrees/thirsty-yalow/bin/postinstall.js +0 -1
- package/.claude/worktrees/thirsty-yalow/changelog.md +0 -167
- package/.claude/worktrees/thirsty-yalow/jest.config.js +0 -82
- package/.claude/worktrees/thirsty-yalow/package.json +0 -62
- package/.claude/worktrees/thirsty-yalow/playwright.config.js +0 -62
- package/AUTO_CLEANUP_PLAN.md +0 -171
- package/build/core/dataGenerator/validateGenerators.js +0 -82
- package/build/utils/timeFormat.js +0 -41
- package/unit_reports/unit-report.html +0 -277
package/README.md
CHANGED
|
@@ -17,14 +17,6 @@
|
|
|
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
|
-
|
|
28
20
|
### v4.1.1/v3.3.0 - 28-01-2026
|
|
29
21
|
|
|
30
22
|
#### Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { test } from '@zohodesk/testinglibrary';
|
|
2
2
|
import DataGenerator from '@zohodesk/testinglibrary/DataGenerator';
|
|
3
|
-
import {getUserForSelectedEditionAndProfile
|
|
3
|
+
import {getUserForSelectedEditionAndProfile} from '@zohodesk/testinglibrary/helpers'
|
|
4
4
|
|
|
5
5
|
const dataGenerator = new DataGenerator();
|
|
6
6
|
|
|
@@ -10,27 +10,12 @@ 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
|
|
14
13
|
const { edition, orgName: portal, beta } = executionContext.actorInfo;
|
|
15
14
|
actorInfo = await getUserForSelectedEditionAndProfile(edition, profile, beta, portal);
|
|
16
15
|
} else {
|
|
17
|
-
// Default — use current actor, fall back to org-level data-generator if profile has none
|
|
18
16
|
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);
|
|
35
17
|
}
|
|
18
|
+
|
|
19
|
+
const generatedData = await dataGenerator.generate(testInfo, actorInfo, type, identifier, scenarioName, dataTable ? dataTable.hashes() : []);
|
|
20
|
+
await cacheLayer.set(entityName, generatedData.data);
|
|
36
21
|
}
|
|
@@ -8,129 +8,61 @@ 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");
|
|
12
11
|
var _DataGeneratorHelper = require("./DataGeneratorHelper");
|
|
12
|
+
var _helpers = require("@zohodesk/testinglibrary/helpers");
|
|
13
13
|
var _DataGeneratorError = require("./DataGeneratorError");
|
|
14
|
-
var _readConfigFile = require("../playwright/readConfigFile");
|
|
15
|
-
var _configConstants = _interopRequireDefault(require("../playwright/constants/configConstants"));
|
|
16
|
-
var _ConfigurationHelper = require("../playwright/configuration/ConfigurationHelper");
|
|
17
14
|
function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); }
|
|
18
|
-
function _classPrivateFieldInitSpec(e, t, a) { _checkPrivateRedeclaration(e, t), t.set(e, a); }
|
|
19
15
|
function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); }
|
|
20
|
-
function _classPrivateFieldSet(s, a, r) { return s.set(_assertClassBrand(s, a), r), r; }
|
|
21
|
-
function _classPrivateFieldGet(s, a) { return s.get(_assertClassBrand(s, a)); }
|
|
22
16
|
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"); }
|
|
23
|
-
var _generatorIndex = /*#__PURE__*/new WeakMap();
|
|
24
17
|
var _DataGenerator_brand = /*#__PURE__*/new WeakSet();
|
|
25
18
|
class DataGenerator {
|
|
26
19
|
constructor() {
|
|
27
20
|
_classPrivateMethodInitSpec(this, _DataGenerator_brand);
|
|
28
|
-
_classPrivateFieldInitSpec(this, _generatorIndex, null);
|
|
29
21
|
}
|
|
30
|
-
async generate(testInfo, actorInfo, generatorType, generatorName, scenarioName, dataTable
|
|
31
|
-
const startMs = Date.now();
|
|
32
|
-
const startLabel = (0, _timeFormat.formatTimestamp)(startMs);
|
|
33
|
-
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Data generation started | generator="${generatorName}" | scenario="${scenarioName}" | startTime=${startLabel}`);
|
|
22
|
+
async generate(testInfo, actorInfo, generatorType, generatorName, scenarioName, dataTable) {
|
|
34
23
|
try {
|
|
35
24
|
let generators;
|
|
36
25
|
if (generatorType === 'API') {
|
|
37
26
|
generators = await _assertClassBrand(_DataGenerator_brand, this, _generateAPIGenerator).call(this, generatorName);
|
|
38
27
|
} else {
|
|
39
|
-
generators = await _assertClassBrand(_DataGenerator_brand, this, _getGenerator).call(this, generatorName);
|
|
28
|
+
generators = await _assertClassBrand(_DataGenerator_brand, this, _getGenerator).call(this, testInfo, generatorName);
|
|
40
29
|
}
|
|
41
|
-
const
|
|
42
|
-
const processedGenerators = await (0, _DataGeneratorHelper.processGenerator)(generators, resolvedDataTable);
|
|
30
|
+
const processedGenerators = await (0, _DataGeneratorHelper.processGenerator)(generators, dataTable);
|
|
43
31
|
const apiPayload = await _assertClassBrand(_DataGenerator_brand, this, _constructApiPayload).call(this, scenarioName, processedGenerators, actorInfo);
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
'featureflags': featureFlags
|
|
48
|
-
};
|
|
49
|
-
_logger.Logger.log(_logger.Logger.INFO_TYPE, 'Making request headers:', headers);
|
|
50
|
-
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Payload: ${JSON.stringify(apiPayload, null, 4)}`);
|
|
51
|
-
const response = await (0, _DataGeneratorHelper.makeRequest)(process.env.DG_SERVICE_DOMAIN + process.env.DG_SERVICE_API_PATH, apiPayload, headers);
|
|
52
|
-
const endMs = Date.now();
|
|
53
|
-
const endLabel = (0, _timeFormat.formatTimestamp)(endMs);
|
|
54
|
-
const totalLabel = (0, _timeFormat.formatDuration)(endMs - startMs);
|
|
55
|
-
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Generated response for the generator: ${generatorName} for scenario: ${scenarioName}, Response: ${JSON.stringify(response, null, 4)}`);
|
|
56
|
-
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Data generation completed | generator="${generatorName}" | scenario="${scenarioName}" | startTime=${startLabel} | endTime=${endLabel} | totalTime=${totalLabel}`);
|
|
57
|
-
return {
|
|
58
|
-
response,
|
|
59
|
-
generators
|
|
60
|
-
};
|
|
32
|
+
const response = await (0, _DataGeneratorHelper.makeRequest)(process.env.DG_SERVICE_DOMAIN + process.env.DG_SERVICE_API_PATH, apiPayload);
|
|
33
|
+
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Generated response for the generator: ${generatorName} for scenario: ${scenarioName}, Response: ${JSON.stringify(response)}`);
|
|
34
|
+
return response;
|
|
61
35
|
} catch (error) {
|
|
62
|
-
const endMs = Date.now();
|
|
63
|
-
const endLabel = (0, _timeFormat.formatTimestamp)(endMs);
|
|
64
|
-
const totalLabel = (0, _timeFormat.formatDuration)(endMs - startMs);
|
|
65
|
-
_logger.Logger.log(_logger.Logger.FAILURE_TYPE, `Data generation failed | generator="${generatorName}" | scenario="${scenarioName}" | startTime=${startLabel} | endTime=${endLabel} | totalTime=${totalLabel}`);
|
|
66
36
|
if (error instanceof _DataGeneratorError.DataGeneratorError) {
|
|
67
|
-
|
|
68
|
-
|
|
37
|
+
console.error(error.getMessage());
|
|
38
|
+
console.error("Stack trace:", error.stack);
|
|
69
39
|
_logger.Logger.log(_logger.Logger.FAILURE_TYPE, error.getMessage());
|
|
70
40
|
} else {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
41
|
+
console.error("Error Type:", error.constructor.name);
|
|
42
|
+
console.error("Error Message:", error.message);
|
|
43
|
+
console.error("Stack trace:", error.stack);
|
|
74
44
|
_logger.Logger.log(_logger.Logger.FAILURE_TYPE, `${error.constructor.name} - Message: ${error.message}`);
|
|
75
45
|
}
|
|
76
|
-
|
|
46
|
+
console.error('Data Generation failed for the generator: ', generatorName, "\n\nError response :", error);
|
|
77
47
|
throw error;
|
|
78
48
|
}
|
|
79
49
|
}
|
|
80
50
|
}
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const fullPath = _path.default.join(dir, entry.name);
|
|
91
|
-
if (entry.isDirectory()) {
|
|
92
|
-
_assertClassBrand(_DataGenerator_brand, this, _scanDir).call(this, fullPath, index, pattern);
|
|
93
|
-
} else if (_assertClassBrand(_DataGenerator_brand, this, _matchesPattern).call(this, entry.name, pattern)) {
|
|
94
|
-
try {
|
|
95
|
-
const data = _fs.default.readFileSync(fullPath, 'utf8');
|
|
96
|
-
const obj = JSON.parse(data);
|
|
97
|
-
if (obj.generators) {
|
|
98
|
-
for (const [name, config] of Object.entries(obj.generators)) {
|
|
99
|
-
if (!index.has(name)) {
|
|
100
|
-
index.set(name, config);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
} catch (err) {
|
|
105
|
-
_logger.Logger.log(_logger.Logger.FAILURE_TYPE, `Failed to parse generator file: ${fullPath} - ${err.message}`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
function _buildIndex(modulesRoot, pattern) {
|
|
111
|
-
const index = new Map();
|
|
112
|
-
_assertClassBrand(_DataGenerator_brand, this, _scanDir).call(this, modulesRoot, index, pattern);
|
|
113
|
-
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Generator index built: ${index.size} generators found`);
|
|
114
|
-
return index;
|
|
115
|
-
}
|
|
116
|
-
function _getModulesRoot() {
|
|
117
|
-
const stage = (0, _ConfigurationHelper.getRunStage)();
|
|
118
|
-
return _path.default.join(process.cwd(), _configConstants.default.TEST_SLICE_FOLDER, stage, 'modules');
|
|
119
|
-
}
|
|
120
|
-
async function _getGenerator(generatorName) {
|
|
121
|
-
if (!_classPrivateFieldGet(_generatorIndex, this)) {
|
|
122
|
-
const modulesRoot = _assertClassBrand(_DataGenerator_brand, this, _getModulesRoot).call(this);
|
|
123
|
-
const {
|
|
124
|
-
generatorFilePattern: pattern = '*.generators.json'
|
|
125
|
-
} = (0, _readConfigFile.generateConfigFromFile)();
|
|
126
|
-
if (modulesRoot) {
|
|
127
|
-
_classPrivateFieldSet(_generatorIndex, this, _assertClassBrand(_DataGenerator_brand, this, _buildIndex).call(this, modulesRoot, pattern));
|
|
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;
|
|
128
60
|
}
|
|
129
61
|
}
|
|
130
|
-
if (
|
|
131
|
-
|
|
62
|
+
if (!generator) {
|
|
63
|
+
throw new _DataGeneratorError.GeneratorError(`Generator "${generatorName}" could not be found in the path located at "${generatorFilePath}"`);
|
|
132
64
|
}
|
|
133
|
-
|
|
65
|
+
return generator;
|
|
134
66
|
}
|
|
135
67
|
async function _generateAPIGenerator(operationId) {
|
|
136
68
|
return [{
|
|
@@ -6,54 +6,6 @@ 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
|
-
|
|
57
9
|
//Create payload for the generators
|
|
58
10
|
async function processGenerator(generators, dataTable) {
|
|
59
11
|
if (!dataTable) {
|
|
@@ -77,12 +29,12 @@ async function processGenerator(generators, dataTable) {
|
|
|
77
29
|
return generator;
|
|
78
30
|
});
|
|
79
31
|
}
|
|
80
|
-
async function makeRequest(url, payload
|
|
81
|
-
'Content-Type': 'application/json'
|
|
82
|
-
}) {
|
|
32
|
+
async function makeRequest(url, payload) {
|
|
83
33
|
const response = await fetch(url, {
|
|
84
34
|
method: 'POST',
|
|
85
|
-
headers:
|
|
35
|
+
headers: {
|
|
36
|
+
'Content-Type': 'application/json'
|
|
37
|
+
},
|
|
86
38
|
body: JSON.stringify(payload)
|
|
87
39
|
});
|
|
88
40
|
if (!response.ok) {
|
|
@@ -1,208 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
3
|
Object.defineProperty(exports, "__esModule", {
|
|
5
4
|
value: true
|
|
6
5
|
});
|
|
7
6
|
exports.default = void 0;
|
|
8
|
-
|
|
9
|
-
var _fs = _interopRequireDefault(require("fs"));
|
|
10
|
-
var _logger = require("../../../utils/logger");
|
|
11
|
-
var _timeFormat = require("../../../utils/timeFormat");
|
|
12
|
-
var _DataGeneratorHelper = require("../../dataGenerator/DataGeneratorHelper");
|
|
13
|
-
var _readConfigFile = require("../readConfigFile");
|
|
14
|
-
var _jsonpath = _interopRequireDefault(require("jsonpath"));
|
|
15
|
-
let cleanupRegistry = null;
|
|
16
|
-
function getModulesRoot() {
|
|
17
|
-
const configConstants = require('../constants/configConstants');
|
|
18
|
-
const {
|
|
19
|
-
getRunStage
|
|
20
|
-
} = require('../configuration/ConfigurationHelper');
|
|
21
|
-
const stage = getRunStage();
|
|
22
|
-
return _path.default.join(process.cwd(), configConstants.TEST_SLICE_FOLDER, stage, 'modules');
|
|
23
|
-
}
|
|
24
|
-
function buildCleanupRegistry() {
|
|
25
|
-
const modulesRoot = getModulesRoot();
|
|
26
|
-
const registry = {};
|
|
27
|
-
if (!_fs.default.existsSync(modulesRoot)) return registry;
|
|
28
|
-
scanDir(modulesRoot, registry);
|
|
29
|
-
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Cleanup registry built: ${Object.keys(registry).length} generator chains from ${modulesRoot}`);
|
|
30
|
-
return registry;
|
|
31
|
-
}
|
|
32
|
-
function scanDir(dir, registry) {
|
|
33
|
-
const entries = _fs.default.readdirSync(dir, {
|
|
34
|
-
withFileTypes: true
|
|
35
|
-
});
|
|
36
|
-
for (const entry of entries) {
|
|
37
|
-
const fullPath = _path.default.join(dir, entry.name);
|
|
38
|
-
if (entry.isDirectory()) {
|
|
39
|
-
scanDir(fullPath, registry);
|
|
40
|
-
} else if (entry.name.endsWith('.cleanup.js')) {
|
|
41
|
-
try {
|
|
42
|
-
const cleanupModule = require(fullPath);
|
|
43
|
-
for (const [generatorName, chain] of Object.entries(cleanupModule)) {
|
|
44
|
-
if (registry[generatorName]) {
|
|
45
|
-
throw new Error(`Duplicate cleanup chain for generator "${generatorName}" in ${fullPath}. ` + `Each generator must have exactly one cleanup chain.`);
|
|
46
|
-
}
|
|
47
|
-
registry[generatorName] = chain;
|
|
48
|
-
}
|
|
49
|
-
} catch (err) {
|
|
50
|
-
if (err.message.includes('Duplicate cleanup chain')) {
|
|
51
|
-
throw err;
|
|
52
|
-
}
|
|
53
|
-
_logger.Logger.log(_logger.Logger.FAILURE_TYPE, `Failed to load cleanup file: ${fullPath} - ${err.message}`);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
function extractId(data, idPath) {
|
|
59
|
-
const result = _jsonpath.default.query(data, idPath);
|
|
60
|
-
if (result.length === 0) {
|
|
61
|
-
throw new Error(`Could not extract ID using path "${idPath}"`);
|
|
62
|
-
}
|
|
63
|
-
return result[0];
|
|
64
|
-
}
|
|
65
|
-
function parseLogBody(stepLog) {
|
|
66
|
-
var _stepLog$response;
|
|
67
|
-
if (!(stepLog !== null && stepLog !== void 0 && (_stepLog$response = stepLog.response) !== null && _stepLog$response !== void 0 && _stepLog$response.body)) return null;
|
|
68
|
-
const body = stepLog.response.body;
|
|
69
|
-
return typeof body === 'string' ? JSON.parse(body) : body;
|
|
70
|
-
}
|
|
71
|
-
async function cleanupViaOAS(config, entityId, actorInfo) {
|
|
72
|
-
const dataGeneratorObj = actorInfo['data-generator'];
|
|
73
|
-
if (!dataGeneratorObj) {
|
|
74
|
-
throw new Error('No data-generator config available for cleanup');
|
|
75
|
-
}
|
|
76
|
-
const payload = {
|
|
77
|
-
scenario_name: 'cleanup',
|
|
78
|
-
data_generation_templates: [{
|
|
79
|
-
type: 'dynamic',
|
|
80
|
-
generatorOperationId: config.operationId,
|
|
81
|
-
dataPath: '$.response.body:$',
|
|
82
|
-
name: config.operationId,
|
|
83
|
-
params: {
|
|
84
|
-
[config.paramName || 'id']: String(entityId)
|
|
85
|
-
}
|
|
86
|
-
}],
|
|
87
|
-
...dataGeneratorObj
|
|
88
|
-
};
|
|
89
|
-
if (payload.account) {
|
|
90
|
-
payload.account.email = actorInfo.email;
|
|
91
|
-
payload.account.password = actorInfo.password;
|
|
92
|
-
}
|
|
93
|
-
const environmentDetails = payload.environmentDetails || {};
|
|
94
|
-
environmentDetails.iam_url = process.env.DG_IAM_DOMAIN;
|
|
95
|
-
environmentDetails.host = new URL(process.env.domain).origin;
|
|
96
|
-
payload.environmentDetails = environmentDetails;
|
|
97
|
-
const response = await (0, _DataGeneratorHelper.makeRequest)(process.env.DG_SERVICE_DOMAIN + process.env.DG_SERVICE_API_PATH, payload);
|
|
98
|
-
return response;
|
|
99
|
-
}
|
|
100
|
-
async function cleanupViaAPI(config, entityId) {
|
|
101
|
-
const url = `${new URL(process.env.domain).origin}${config.apiPath.replace('{id}', entityId)}`;
|
|
102
|
-
const options = {
|
|
103
|
-
method: config.method,
|
|
104
|
-
headers: {
|
|
105
|
-
'Content-Type': 'application/json'
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
if (config.body) {
|
|
109
|
-
options.body = JSON.stringify(config.body);
|
|
110
|
-
}
|
|
111
|
-
const response = await fetch(url, options);
|
|
112
|
-
const responseBody = await response.text();
|
|
113
|
-
if (!response.ok) {
|
|
114
|
-
throw new Error(`${config.method} ${config.apiPath} - status: ${response.status}, body: ${responseBody}`);
|
|
115
|
-
}
|
|
116
|
-
return {
|
|
117
|
-
status: response.status,
|
|
118
|
-
body: responseBody
|
|
119
|
-
};
|
|
120
|
-
}
|
|
7
|
+
const cacheMap = new Map();
|
|
121
8
|
var _default = exports.default = {
|
|
122
9
|
// eslint-disable-next-line no-empty-pattern
|
|
123
10
|
cacheLayer: async ({}, use) => {
|
|
124
|
-
|
|
125
|
-
const cleanupEntries = [];
|
|
126
|
-
cache._trackForCleanup = (entityName, data, generators, actorInfo, logs, generatorName) => {
|
|
127
|
-
cache.set(entityName, data);
|
|
128
|
-
cache.set(`${entityName}_logs`, logs);
|
|
129
|
-
cleanupEntries.push({
|
|
130
|
-
entityName,
|
|
131
|
-
data,
|
|
132
|
-
generators,
|
|
133
|
-
actorInfo,
|
|
134
|
-
logs: logs || [],
|
|
135
|
-
generatorName
|
|
136
|
-
});
|
|
137
|
-
};
|
|
138
|
-
await use(cache);
|
|
139
|
-
|
|
140
|
-
// TEARDOWN — runs after scenario ends (pass or fail)
|
|
141
|
-
const {
|
|
142
|
-
autoCleanup = true
|
|
143
|
-
} = (0, _readConfigFile.generateConfigFromFile)();
|
|
144
|
-
if (!autoCleanup || cleanupEntries.length === 0) {
|
|
145
|
-
cache.clear();
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
if (!cleanupRegistry) {
|
|
149
|
-
cleanupRegistry = buildCleanupRegistry();
|
|
150
|
-
}
|
|
151
|
-
const cleanupStartMs = Date.now();
|
|
152
|
-
const cleanupStartLabel = (0, _timeFormat.formatTimestamp)(cleanupStartMs);
|
|
153
|
-
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Cleanup started | entities=${cleanupEntries.length} | startTime=${cleanupStartLabel}`);
|
|
154
|
-
let cleaned = 0;
|
|
155
|
-
let skipped = 0;
|
|
156
|
-
let failed = 0;
|
|
157
|
-
for (const entry of [...cleanupEntries].reverse()) {
|
|
158
|
-
const cleanupChain = cleanupRegistry[entry.generatorName];
|
|
159
|
-
if (!cleanupChain) {
|
|
160
|
-
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Cleanup: no chain for generator "${entry.generatorName}" — skipping`);
|
|
161
|
-
skipped++;
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
const entityStartMs = Date.now();
|
|
165
|
-
const entityStartLabel = (0, _timeFormat.formatTimestamp)(entityStartMs);
|
|
166
|
-
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Cleanup entity started | entity="${entry.entityName}" | generator="${entry.generatorName}" | steps=${cleanupChain.length} | startTime=${entityStartLabel}`);
|
|
167
|
-
for (const cleanupStep of cleanupChain) {
|
|
168
|
-
const stepStartMs = Date.now();
|
|
169
|
-
const stepStartLabel = (0, _timeFormat.formatTimestamp)(stepStartMs);
|
|
170
|
-
const actionDesc = cleanupStep.operationId || `${cleanupStep.method} ${cleanupStep.apiPath}`;
|
|
171
|
-
try {
|
|
172
|
-
// Find the step's response from logs by matching operationId
|
|
173
|
-
const stepLog = entry.logs.find(log => log.generationOperationId === cleanupStep.operationId || log.name === cleanupStep.operationId);
|
|
174
|
-
const stepData = parseLogBody(stepLog) || entry.data;
|
|
175
|
-
const entityId = extractId(stepData, cleanupStep.idPath);
|
|
176
|
-
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Cleanup step started | [${cleanupStep.type}] ${actionDesc} (id: ${entityId}) | startTime=${stepStartLabel}`);
|
|
177
|
-
let cleanupResponse;
|
|
178
|
-
if (cleanupStep.type === 'oas') {
|
|
179
|
-
cleanupResponse = await cleanupViaOAS(cleanupStep, entityId, entry.actorInfo);
|
|
180
|
-
} else if (cleanupStep.type === 'api') {
|
|
181
|
-
cleanupResponse = await cleanupViaAPI(cleanupStep, entityId);
|
|
182
|
-
}
|
|
183
|
-
cleaned++;
|
|
184
|
-
const stepEndMs = Date.now();
|
|
185
|
-
const stepEndLabel = (0, _timeFormat.formatTimestamp)(stepEndMs);
|
|
186
|
-
const stepTotalLabel = (0, _timeFormat.formatDuration)(stepEndMs - stepStartMs);
|
|
187
|
-
_logger.Logger.log(_logger.Logger.SUCCESS_TYPE, `Cleanup step success | ${actionDesc} (id: ${entityId}) | response=${JSON.stringify(cleanupResponse, null, 4)} | startTime=${stepStartLabel} | endTime=${stepEndLabel} | totalTime=${stepTotalLabel}`);
|
|
188
|
-
} catch (err) {
|
|
189
|
-
failed++;
|
|
190
|
-
const stepEndMs = Date.now();
|
|
191
|
-
const stepEndLabel = (0, _timeFormat.formatTimestamp)(stepEndMs);
|
|
192
|
-
const stepTotalLabel = (0, _timeFormat.formatDuration)(stepEndMs - stepStartMs);
|
|
193
|
-
_logger.Logger.log(_logger.Logger.FAILURE_TYPE, `Cleanup step failed | ${actionDesc} — ${err.message} | startTime=${stepStartLabel} | endTime=${stepEndLabel} | totalTime=${stepTotalLabel}`);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
const entityEndMs = Date.now();
|
|
197
|
-
const entityEndLabel = (0, _timeFormat.formatTimestamp)(entityEndMs);
|
|
198
|
-
const entityTotalLabel = (0, _timeFormat.formatDuration)(entityEndMs - entityStartMs);
|
|
199
|
-
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Cleanup entity completed | entity="${entry.entityName}" | generator="${entry.generatorName}" | startTime=${entityStartLabel} | endTime=${entityEndLabel} | totalTime=${entityTotalLabel}`);
|
|
200
|
-
}
|
|
201
|
-
const cleanupEndMs = Date.now();
|
|
202
|
-
const cleanupEndLabel = (0, _timeFormat.formatTimestamp)(cleanupEndMs);
|
|
203
|
-
const cleanupTotalLabel = (0, _timeFormat.formatDuration)(cleanupEndMs - cleanupStartMs);
|
|
204
|
-
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Cleanup completed | cleaned=${cleaned} | skipped=${skipped} (no chain) | failed=${failed} | startTime=${cleanupStartLabel} | endTime=${cleanupEndLabel} | totalTime=${cleanupTotalLabel}`);
|
|
205
|
-
cleanupEntries.length = 0;
|
|
206
|
-
cache.clear();
|
|
11
|
+
await use(cacheMap);
|
|
207
12
|
}
|
|
208
13
|
};
|
|
@@ -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.
|
|
99
|
+
testingPortal = userdata[edition].find(editionData => editionData.orgName === testDataPortal);
|
|
100
100
|
if (!testingPortal) {
|
|
101
|
-
throw new Error(`There is no "${testDataPortal}" portal
|
|
101
|
+
throw new Error(`There is no "${testDataPortal}" portal configured in "${edition}" edition.`);
|
|
102
102
|
}
|
|
103
103
|
} else {
|
|
104
104
|
testingPortal = userdata[edition] ? userdata[edition][0] : {};
|
|
@@ -56,8 +56,7 @@ function getDefaultConfig() {
|
|
|
56
56
|
stepDefinitionsFolder: 'steps',
|
|
57
57
|
testSetup: {},
|
|
58
58
|
editionOrder: ['Free', 'Express', 'Standard', 'Professional', 'Enterprise'],
|
|
59
|
-
|
|
60
|
-
autoCleanup: true
|
|
59
|
+
showCaseTimings: true
|
|
61
60
|
};
|
|
62
61
|
}
|
|
63
62
|
function combineDefaultConfigWithUserConfig(userConfiguration) {
|
|
@@ -115,6 +114,7 @@ function combineDefaultConfigWithUserConfig(userConfiguration) {
|
|
|
115
114
|
* @property {string} testIdAttribute: Change the default data-testid attribute. configure what attribute to search while calling getByTestId
|
|
116
115
|
* @property {Array} editionOrder: Order in the form of larger editions in the back. Edition with the most privelages should be last
|
|
117
116
|
* @property {testSetupConfig} testSetup: Specify page and context functions that will be called while intilaizing fixtures.
|
|
117
|
+
* @property {boolean} showCaseTimings: When true, the console reporter prints per-case start/end wall-clock times (h:mm:ss AM/PM) and a case-timings.json sidecar is written to reportPath for the HTML report addon. Default: true.
|
|
118
118
|
*/
|
|
119
119
|
|
|
120
120
|
/**
|
|
@@ -12,6 +12,22 @@ var _readConfigFile = require("../readConfigFile");
|
|
|
12
12
|
var _logger = require("../../../utils/logger");
|
|
13
13
|
var _configFileNameProvider = require("../helpers/configFileNameProvider");
|
|
14
14
|
var _mergeAbortedTests = _interopRequireDefault(require("../reporter/helpers/mergeAbortedTests"));
|
|
15
|
+
function formatTime(date) {
|
|
16
|
+
return date.toLocaleTimeString('en-US', {
|
|
17
|
+
hour: 'numeric',
|
|
18
|
+
minute: '2-digit',
|
|
19
|
+
second: '2-digit',
|
|
20
|
+
hour12: true
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function formatDuration(ms) {
|
|
24
|
+
if (ms < 1000) return `${ms}ms`;
|
|
25
|
+
const totalSeconds = ms / 1000;
|
|
26
|
+
if (totalSeconds < 60) return `${totalSeconds.toFixed(2)}s`;
|
|
27
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
28
|
+
const seconds = (totalSeconds - minutes * 60).toFixed(2);
|
|
29
|
+
return `${minutes}m ${seconds}s`;
|
|
30
|
+
}
|
|
15
31
|
class JSONSummaryReporter {
|
|
16
32
|
constructor() {
|
|
17
33
|
this.durationInMS = -1;
|
|
@@ -26,11 +42,22 @@ class JSONSummaryReporter {
|
|
|
26
42
|
this.failedSteps = [];
|
|
27
43
|
this.status = 'unknown';
|
|
28
44
|
this.startedAt = 0;
|
|
29
|
-
|
|
45
|
+
const config = (0, _readConfigFile.generateConfigFromFile)();
|
|
46
|
+
this._open = config.openReportOn;
|
|
47
|
+
this._showCaseTimings = config.showCaseTimings !== false;
|
|
48
|
+
this._caseTimings = [];
|
|
30
49
|
}
|
|
31
50
|
onBegin() {
|
|
32
51
|
this.startedAt = Date.now();
|
|
33
52
|
}
|
|
53
|
+
onTestBegin(test) {
|
|
54
|
+
if (!this._showCaseTimings) return;
|
|
55
|
+
const {
|
|
56
|
+
fullTitle
|
|
57
|
+
} = this.getTitle(test);
|
|
58
|
+
const startedAtLabel = formatTime(new Date());
|
|
59
|
+
_logger.Logger.log(_logger.Logger.INFO_TYPE, `▶ ${fullTitle} — started at ${startedAtLabel}`);
|
|
60
|
+
}
|
|
34
61
|
getTitle(test) {
|
|
35
62
|
const title = [];
|
|
36
63
|
const fileName = [];
|
|
@@ -76,6 +103,40 @@ class JSONSummaryReporter {
|
|
|
76
103
|
this[status].push(fileName);
|
|
77
104
|
}
|
|
78
105
|
this[status].push(fileName);
|
|
106
|
+
if (this._showCaseTimings && result.startTime) {
|
|
107
|
+
const startDate = new Date(result.startTime);
|
|
108
|
+
const endDate = new Date(startDate.getTime() + (result.duration || 0));
|
|
109
|
+
const startLabel = formatTime(startDate);
|
|
110
|
+
const endLabel = formatTime(endDate);
|
|
111
|
+
const durationLabel = formatDuration(result.duration || 0);
|
|
112
|
+
const statusGlyph = result.status === 'passed' ? '✓' : result.status === 'skipped' ? '○' : '✗';
|
|
113
|
+
const logType = result.status === 'passed' ? _logger.Logger.SUCCESS_TYPE : result.status === 'skipped' ? _logger.Logger.INFO_TYPE : _logger.Logger.FAILURE_TYPE;
|
|
114
|
+
_logger.Logger.log(logType, `${statusGlyph} ${fullTitle} — ended at ${endLabel} (started ${startLabel}, took ${durationLabel})`);
|
|
115
|
+
const isFailure = result.status !== 'passed' && result.status !== 'skipped';
|
|
116
|
+
if (isFailure) {
|
|
117
|
+
var _result$error;
|
|
118
|
+
this._caseTimings.push({
|
|
119
|
+
title: fullTitle,
|
|
120
|
+
fileName,
|
|
121
|
+
status: result.status,
|
|
122
|
+
retry: result.retry,
|
|
123
|
+
startTime: startDate.toISOString(),
|
|
124
|
+
endTime: endDate.toISOString(),
|
|
125
|
+
startTimeFormatted: startLabel,
|
|
126
|
+
endTimeFormatted: endLabel,
|
|
127
|
+
duration: result.duration || 0,
|
|
128
|
+
durationFormatted: durationLabel,
|
|
129
|
+
errorMessage: (_result$error = result.error) === null || _result$error === void 0 ? void 0 : _result$error.message,
|
|
130
|
+
failedSteps: (result.steps || []).filter(step => step.error).map(step => {
|
|
131
|
+
var _step$error;
|
|
132
|
+
return {
|
|
133
|
+
title: step.title,
|
|
134
|
+
error: (_step$error = step.error) === null || _step$error === void 0 ? void 0 : _step$error.message
|
|
135
|
+
};
|
|
136
|
+
})
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
79
140
|
}
|
|
80
141
|
onError(error) {
|
|
81
142
|
this.errored.push({
|
|
@@ -122,6 +183,16 @@ class JSONSummaryReporter {
|
|
|
122
183
|
reportPath
|
|
123
184
|
} = (0, _readConfigFile.generateConfigFromFile)();
|
|
124
185
|
(0, _fileUtils.writeFileContents)(_path.default.join(reportPath, './', (0, _configFileNameProvider.getReportFileName)()), JSON.stringify(this, null, ' '));
|
|
186
|
+
if (this._showCaseTimings && this._caseTimings.length > 0) {
|
|
187
|
+
const timingsPayload = {
|
|
188
|
+
suiteStartedAt: new Date(this.startedAt).toISOString(),
|
|
189
|
+
suiteEndedAt: new Date(this.startedAt + this.durationInMS).toISOString(),
|
|
190
|
+
suiteDurationMs: this.durationInMS,
|
|
191
|
+
failedCount: this._caseTimings.length,
|
|
192
|
+
cases: this._caseTimings
|
|
193
|
+
};
|
|
194
|
+
(0, _fileUtils.writeFileContents)(_path.default.join(reportPath, 'case-timings.json'), JSON.stringify(timingsPayload, null, ' '));
|
|
195
|
+
}
|
|
125
196
|
}
|
|
126
197
|
onExit() {
|
|
127
198
|
// Update .last-run.json with aborted tests due to timing out or interruption
|