@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
|
@@ -1,13 +1,208 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
3
4
|
Object.defineProperty(exports, "__esModule", {
|
|
4
5
|
value: true
|
|
5
6
|
});
|
|
6
7
|
exports.default = void 0;
|
|
7
|
-
|
|
8
|
+
var _path = _interopRequireDefault(require("path"));
|
|
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
|
+
}
|
|
8
121
|
var _default = exports.default = {
|
|
9
122
|
// eslint-disable-next-line no-empty-pattern
|
|
10
123
|
cacheLayer: async ({}, use) => {
|
|
11
|
-
|
|
124
|
+
const cache = new Map();
|
|
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();
|
|
12
207
|
}
|
|
13
208
|
};
|
|
@@ -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;
|
|
@@ -96,9 +96,9 @@ function getUserForSelectedEditionAndProfile(preferedEdition, preferredProfile,
|
|
|
96
96
|
throw new Error(`There is no "${edition}" edition configured.`);
|
|
97
97
|
}
|
|
98
98
|
if (testDataPortal !== null) {
|
|
99
|
-
testingPortal = userdata[edition].find(editionData => editionData.orgName === testDataPortal);
|
|
99
|
+
testingPortal = userdata[edition].find(editionData => editionData.capability === testDataPortal || editionData.orgName === testDataPortal);
|
|
100
100
|
if (!testingPortal) {
|
|
101
|
-
throw new Error(`There is no "${testDataPortal}" portal configured in "${edition}" edition.`);
|
|
101
|
+
throw new Error(`There is no "${testDataPortal}" portal (by capability or orgName) configured in "${edition}" edition.`);
|
|
102
102
|
}
|
|
103
103
|
} else {
|
|
104
104
|
testingPortal = userdata[edition] ? userdata[edition][0] : {};
|
|
@@ -55,7 +55,9 @@ 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',
|
|
60
|
+
autoCleanup: true
|
|
59
61
|
};
|
|
60
62
|
}
|
|
61
63
|
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(() => {
|
package/build/lib/cli.js
CHANGED
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
4
|
var _testRunner = _interopRequireDefault(require("../core/playwright/test-runner"));
|
|
5
|
-
var
|
|
6
|
-
var _PlaywrightReporter = _interopRequireDefault(require("../core/playwright/reporter/PlaywrightReporter"));
|
|
7
|
-
var _UnitReporter = _interopRequireDefault(require("../core/playwright/reporter/UnitReporter"));
|
|
5
|
+
var _reportGenerator = _interopRequireDefault(require("../core/playwright/report-generator"));
|
|
8
6
|
var _codegen = _interopRequireDefault(require("../core/playwright/codegen"));
|
|
9
7
|
var _logger = require("../utils/logger");
|
|
10
8
|
var _setupProject = _interopRequireDefault(require("../setup-folder-structure/setupProject"));
|
|
@@ -13,25 +11,15 @@ var _clearCaches = _interopRequireDefault(require("../core/playwright/clear-cach
|
|
|
13
11
|
var _helper = _interopRequireDefault(require("../setup-folder-structure/helper"));
|
|
14
12
|
var _parseUserArgs = _interopRequireDefault(require("../core/playwright/helpers/parseUserArgs"));
|
|
15
13
|
var _validateFeature = _interopRequireDefault(require("../core/playwright/validateFeature"));
|
|
16
|
-
|
|
14
|
+
// import createJestRunner from '../core/jest/runner/jest-runner';
|
|
15
|
+
|
|
17
16
|
const [,, option, ...otherOptions] = process.argv;
|
|
18
17
|
switch (option) {
|
|
19
18
|
case 'test':
|
|
20
19
|
{
|
|
21
20
|
_logger.Logger.log(_logger.Logger.SUCCESS_TYPE, 'Running Tests..');
|
|
22
21
|
(0, _testRunner.default)();
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
case 'unit-test':
|
|
26
|
-
{
|
|
27
|
-
const testFile = process.argv[3];
|
|
28
|
-
_logger.Logger.log(_logger.Logger.SUCCESS_TYPE, 'Running Unit Tests..');
|
|
29
|
-
const options = {};
|
|
30
|
-
if (testFile) {
|
|
31
|
-
_logger.Logger.log(_logger.Logger.INFO_TYPE, `Filtering tests with pattern: ${testFile}`);
|
|
32
|
-
options.testPathPattern = testFile;
|
|
33
|
-
}
|
|
34
|
-
(0, _unitTestingFramework.createJestRunner)(options);
|
|
22
|
+
//createJestRunner();
|
|
35
23
|
break;
|
|
36
24
|
}
|
|
37
25
|
case 'validate':
|
|
@@ -52,14 +40,9 @@ switch (option) {
|
|
|
52
40
|
}
|
|
53
41
|
case 'report':
|
|
54
42
|
{
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
case 'ut-report':
|
|
60
|
-
{
|
|
61
|
-
_logger.Logger.log(_logger.Logger.SUCCESS_TYPE, 'Generating Unit Test Reports...');
|
|
62
|
-
_UnitReporter.default.generate();
|
|
43
|
+
// console.log('\x1b[36mGenerating Reports...\x1b[0m');
|
|
44
|
+
_logger.Logger.log(_logger.Logger.SUCCESS_TYPE, 'Generating Reports...');
|
|
45
|
+
(0, _reportGenerator.default)();
|
|
63
46
|
break;
|
|
64
47
|
}
|
|
65
48
|
case 'codegen':
|
|
@@ -86,12 +69,6 @@ switch (option) {
|
|
|
86
69
|
(0, _clearCaches.default)();
|
|
87
70
|
break;
|
|
88
71
|
}
|
|
89
|
-
case 'stepsGenerator':
|
|
90
|
-
{
|
|
91
|
-
_logger.Logger.log(_logger.Logger.SUCCESS_TYPE, 'Created agents and prompts md files under .github folder...');
|
|
92
|
-
(0, _commonUtils.copyGithubFolder)();
|
|
93
|
-
break;
|
|
94
|
-
}
|
|
95
72
|
case 'help':
|
|
96
73
|
default:
|
|
97
74
|
{
|
|
@@ -5,7 +5,6 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
value: true
|
|
6
6
|
});
|
|
7
7
|
exports.copyCommonSpecs = copyCommonSpecs;
|
|
8
|
-
exports.copyGithubFolder = copyGithubFolder;
|
|
9
8
|
var _fileUtils = require("./fileUtils");
|
|
10
9
|
var _path = _interopRequireDefault(require("path"));
|
|
11
10
|
var _configConstants = _interopRequireDefault(require("../core/playwright/constants/configConstants"));
|
|
@@ -18,12 +17,4 @@ function copyCommonSpecs() {
|
|
|
18
17
|
const destDirectory = _path.default.resolve(process.cwd(), _configConstants.default.TEST_SLICE_FOLDER, stage, 'modules', '.testingLib-common');
|
|
19
18
|
(0, _fileUtils.deleteFolder)(destDirectory);
|
|
20
19
|
(0, _fileUtils.copyDirectory)(commonSpecPath, destDirectory);
|
|
21
|
-
}
|
|
22
|
-
function copyGithubFolder() {
|
|
23
|
-
const libraryPath = require.resolve("@zohodesk/testinglibrary");
|
|
24
|
-
// libraryPath will be build/index.js, go two levels up to reach the package root where .github lives
|
|
25
|
-
const githubSrcPath = _path.default.resolve(libraryPath, '../../', '.github');
|
|
26
|
-
const destDirectory = _path.default.resolve(process.cwd(), '../../', '.github');
|
|
27
|
-
(0, _fileUtils.deleteFolder)(destDirectory);
|
|
28
|
-
(0, _fileUtils.copyDirectory)(githubSrcPath, destDirectory);
|
|
29
20
|
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.resolveDatePlaceholders = resolveDatePlaceholders;
|
|
7
|
+
exports.resolveDatePlaceholdersInGenerators = resolveDatePlaceholdersInGenerators;
|
|
8
|
+
exports.resolveDatePlaceholdersInValue = resolveDatePlaceholdersInValue;
|
|
9
|
+
// Matches a full-cell date placeholder: ${TOKEN} or ${TOKEN:N}
|
|
10
|
+
// Uppercase-only token names never clash with TitleCase cache refs.
|
|
11
|
+
const DATE_PLACEHOLDER = /^\$\{([A-Z][A-Z_]*)(?::(\d+))?\}$/;
|
|
12
|
+
function _pad(n, width = 2) {
|
|
13
|
+
return String(n).padStart(width, '0');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// UTC day arithmetic — avoids DST boundary issues.
|
|
17
|
+
function _shiftDays(baseDate, days) {
|
|
18
|
+
const d = new Date(baseDate.getTime());
|
|
19
|
+
d.setUTCDate(d.getUTCDate() + days);
|
|
20
|
+
return d;
|
|
21
|
+
}
|
|
22
|
+
function _toISODate(d) {
|
|
23
|
+
return `${d.getUTCFullYear()}-${_pad(d.getUTCMonth() + 1)}-${_pad(d.getUTCDate())}`;
|
|
24
|
+
}
|
|
25
|
+
function _toISODateTime(d) {
|
|
26
|
+
return d.toISOString();
|
|
27
|
+
}
|
|
28
|
+
function _toEpochMs(d) {
|
|
29
|
+
return String(d.getTime());
|
|
30
|
+
}
|
|
31
|
+
function _toUIDate(d) {
|
|
32
|
+
return `${_pad(d.getUTCDate())}/${_pad(d.getUTCMonth() + 1)}/${d.getUTCFullYear()}`;
|
|
33
|
+
}
|
|
34
|
+
const TOKEN_HANDLERS = {
|
|
35
|
+
CURRENT_DATE: {
|
|
36
|
+
requiresN: false,
|
|
37
|
+
resolve: d => _toISODate(d)
|
|
38
|
+
},
|
|
39
|
+
PAST_DATE: {
|
|
40
|
+
requiresN: true,
|
|
41
|
+
resolve: (d, n) => _toISODate(_shiftDays(d, -n))
|
|
42
|
+
},
|
|
43
|
+
FUTURE_DATE: {
|
|
44
|
+
requiresN: true,
|
|
45
|
+
resolve: (d, n) => _toISODate(_shiftDays(d, n))
|
|
46
|
+
},
|
|
47
|
+
CURRENT_DATETIME: {
|
|
48
|
+
requiresN: false,
|
|
49
|
+
resolve: d => _toISODateTime(d)
|
|
50
|
+
},
|
|
51
|
+
PAST_DATETIME: {
|
|
52
|
+
requiresN: true,
|
|
53
|
+
resolve: (d, n) => _toISODateTime(_shiftDays(d, -n))
|
|
54
|
+
},
|
|
55
|
+
FUTURE_DATETIME: {
|
|
56
|
+
requiresN: true,
|
|
57
|
+
resolve: (d, n) => _toISODateTime(_shiftDays(d, n))
|
|
58
|
+
},
|
|
59
|
+
CURRENT_TIMESTAMP: {
|
|
60
|
+
requiresN: false,
|
|
61
|
+
resolve: d => _toEpochMs(d)
|
|
62
|
+
},
|
|
63
|
+
PAST_TIMESTAMP: {
|
|
64
|
+
requiresN: true,
|
|
65
|
+
resolve: (d, n) => _toEpochMs(_shiftDays(d, -n))
|
|
66
|
+
},
|
|
67
|
+
FUTURE_TIMESTAMP: {
|
|
68
|
+
requiresN: true,
|
|
69
|
+
resolve: (d, n) => _toEpochMs(_shiftDays(d, n))
|
|
70
|
+
},
|
|
71
|
+
CURRENT_DATE_UI: {
|
|
72
|
+
requiresN: false,
|
|
73
|
+
resolve: d => _toUIDate(d)
|
|
74
|
+
},
|
|
75
|
+
PAST_DATE_UI: {
|
|
76
|
+
requiresN: true,
|
|
77
|
+
resolve: (d, n) => _toUIDate(_shiftDays(d, -n))
|
|
78
|
+
},
|
|
79
|
+
FUTURE_DATE_UI: {
|
|
80
|
+
requiresN: true,
|
|
81
|
+
resolve: (d, n) => _toUIDate(_shiftDays(d, n))
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const VALID_TOKENS = Object.keys(TOKEN_HANDLERS).join(', ');
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Resolve a single dataTable cell value.
|
|
88
|
+
* Non-strings and strings without "${" are returned as-is.
|
|
89
|
+
* @param {*} value
|
|
90
|
+
* @param {Date} [now]
|
|
91
|
+
* @returns {*}
|
|
92
|
+
*/
|
|
93
|
+
function resolveDatePlaceholdersInValue(value, now = new Date()) {
|
|
94
|
+
if (typeof value !== 'string' || !value.includes('${')) {
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
const match = value.match(DATE_PLACEHOLDER);
|
|
98
|
+
if (!match) {
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
const [, token, rawN] = match;
|
|
102
|
+
const handler = TOKEN_HANDLERS[token];
|
|
103
|
+
if (!handler) {
|
|
104
|
+
throw new Error(`DataGenerator: Unknown date placeholder token "\${${token}}". Valid tokens are: ${VALID_TOKENS}.`);
|
|
105
|
+
}
|
|
106
|
+
if (handler.requiresN) {
|
|
107
|
+
if (rawN === undefined) {
|
|
108
|
+
throw new Error(`DataGenerator: Placeholder "\${${token}}" requires a day offset N, e.g. "\${${token}:7}".`);
|
|
109
|
+
}
|
|
110
|
+
const n = parseInt(rawN, 10);
|
|
111
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
112
|
+
throw new Error(`DataGenerator: Placeholder "\${${token}:${rawN}}" — N must be a positive integer ≥ 1.`);
|
|
113
|
+
}
|
|
114
|
+
return handler.resolve(now, n);
|
|
115
|
+
} else {
|
|
116
|
+
if (rawN !== undefined) {
|
|
117
|
+
const suggestionToken = token.replace('CURRENT_', 'FUTURE_');
|
|
118
|
+
throw new Error(`DataGenerator: Placeholder "\${${token}}" does not accept an offset. Did you mean "\${${suggestionToken}:${rawN}}"?`);
|
|
119
|
+
}
|
|
120
|
+
return handler.resolve(now);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Resolve date placeholders in all cells of a dataTable rows array.
|
|
126
|
+
* @param {Object[]} rows
|
|
127
|
+
* @param {Date} [now]
|
|
128
|
+
* @returns {Object[]}
|
|
129
|
+
*/
|
|
130
|
+
function resolveDatePlaceholders(rows, now = new Date()) {
|
|
131
|
+
if (!rows || !rows.length) {
|
|
132
|
+
return rows || [];
|
|
133
|
+
}
|
|
134
|
+
return rows.map(row => {
|
|
135
|
+
const resolved = {};
|
|
136
|
+
for (const [key, value] of Object.entries(row)) {
|
|
137
|
+
resolved[key] = resolveDatePlaceholdersInValue(value, now);
|
|
138
|
+
}
|
|
139
|
+
return resolved;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function _resolveObjectValues(obj, now) {
|
|
143
|
+
for (const key of Object.keys(obj)) {
|
|
144
|
+
const val = obj[key];
|
|
145
|
+
if (typeof val === 'string') {
|
|
146
|
+
obj[key] = resolveDatePlaceholdersInValue(val, now);
|
|
147
|
+
} else if (val !== null && typeof val === 'object') {
|
|
148
|
+
_resolveObjectValues(val, now);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Resolve date placeholders in static params of a loaded generators array.
|
|
155
|
+
* Mutates in place — call only on a cloned copy of the generator index entry.
|
|
156
|
+
* @param {Object[]} generators
|
|
157
|
+
* @param {Date} [now]
|
|
158
|
+
* @returns {Object[]}
|
|
159
|
+
*/
|
|
160
|
+
function resolveDatePlaceholdersInGenerators(generators, now = new Date()) {
|
|
161
|
+
if (!generators || !generators.length) {
|
|
162
|
+
return generators || [];
|
|
163
|
+
}
|
|
164
|
+
for (const gen of generators) {
|
|
165
|
+
if (gen.params && typeof gen.params === 'object') {
|
|
166
|
+
_resolveObjectValues(gen.params, now);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return generators;
|
|
170
|
+
}
|
package/build/utils/logger.js
CHANGED
|
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.Logger = void 0;
|
|
7
|
+
var _util = require("util");
|
|
7
8
|
class LoggerImpl {
|
|
8
9
|
constructor() {
|
|
9
10
|
this.SUCCESS_TYPE = 'success';
|
|
@@ -20,8 +21,9 @@ class LoggerImpl {
|
|
|
20
21
|
this.consoleLogger.error(err);
|
|
21
22
|
}
|
|
22
23
|
info() {}
|
|
23
|
-
log(type,
|
|
24
|
+
log(type, ...messageParts) {
|
|
24
25
|
const color = this.colors[type];
|
|
26
|
+
const message = (0, _util.format)(...messageParts);
|
|
25
27
|
this.consoleLogger.log(`${color[0]}${message}${color[1]}\n`);
|
|
26
28
|
}
|
|
27
29
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.formatDuration = formatDuration;
|
|
7
|
+
exports.formatTimestamp = formatTimestamp;
|
|
8
|
+
/**
|
|
9
|
+
* Format a Date (or epoch ms) as a human-readable local timestamp in 12-hour format.
|
|
10
|
+
* Example: "2026-04-10 11:35:23.123 AM"
|
|
11
|
+
*/
|
|
12
|
+
function formatTimestamp(input) {
|
|
13
|
+
const d = input instanceof Date ? input : new Date(input);
|
|
14
|
+
const pad = (n, w = 2) => String(n).padStart(w, '0');
|
|
15
|
+
const hours24 = d.getHours();
|
|
16
|
+
const period = hours24 >= 12 ? 'PM' : 'AM';
|
|
17
|
+
const hours12 = hours24 % 12 || 12;
|
|
18
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` + `${pad(hours12)}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)} ${period}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Format a duration in milliseconds as a human-readable string.
|
|
23
|
+
* Examples: "850ms", "4.32s", "1m 23.45s", "1h 02m 03.45s"
|
|
24
|
+
*/
|
|
25
|
+
function formatDuration(ms) {
|
|
26
|
+
if (ms < 1000) {
|
|
27
|
+
return `${ms}ms`;
|
|
28
|
+
}
|
|
29
|
+
const totalSeconds = ms / 1000;
|
|
30
|
+
if (totalSeconds < 60) {
|
|
31
|
+
return `${totalSeconds.toFixed(2)}s`;
|
|
32
|
+
}
|
|
33
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
34
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
35
|
+
const seconds = totalSeconds % 60;
|
|
36
|
+
const pad = n => String(n).padStart(2, '0');
|
|
37
|
+
if (hours > 0) {
|
|
38
|
+
return `${hours}h ${pad(minutes)}m ${seconds.toFixed(2)}s`;
|
|
39
|
+
}
|
|
40
|
+
return `${minutes}m ${seconds.toFixed(2)}s`;
|
|
41
|
+
}
|
package/changelog.md
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
# Testing Framework
|
|
2
2
|
|
|
3
3
|
## Framework that abstracts the configuration for playwright and Jest
|
|
4
|
+
|
|
5
|
+
# 0.0.48-n20-experimental
|
|
6
|
+
|
|
7
|
+
## Data Generator — Global Index Discovery
|
|
8
|
+
- **Global generator index**: Replaced walk-up directory search with index-based global discovery. Generators in any module are now accessible from any feature file.
|
|
9
|
+
- **Deterministic modules root**: Uses `configConstants.TEST_SLICE_FOLDER + stage + 'modules'` instead of unreliable directory walk-up.
|
|
10
|
+
- **Configurable file pattern**: `generatorFilePattern` in `uat.config.js` (default: `*.generators.json`).
|
|
11
|
+
- **TicketBasic generator**: New generator without product step for Express/Free editions.
|
|
12
|
+
|
|
13
|
+
## Data Generator — Profile & Auth Improvements
|
|
14
|
+
- **Org-level DG fallback**: When scenario profile has no `data-generator` config, falls back to org-level from edition JSON.
|
|
15
|
+
|
|
16
|
+
## Auto-Cleanup V2
|
|
17
|
+
- **Co-located `*.cleanup.json` registry**: Each module defines cleanup rules alongside generators. Scanned globally.
|
|
18
|
+
- **Fixture teardown cleanup**: Runs after each scenario (pass or fail). Supports OAS, REST DELETE, REST PATCH.
|
|
19
|
+
- **`autoCleanup` config**: Enable/disable via `uat.config.js` (default: `true`).
|
|
20
|
+
- **Detailed logging**: Cleanup started/completed/success/failed with entity names and IDs.
|
|
21
|
+
|
|
22
|
+
## Portal Resolution — Capability Support
|
|
23
|
+
- **`capability` field**: Portals can define a `capability` field for DC-agnostic resolution.
|
|
24
|
+
- **Dual resolution**: `@portal_` tags resolve by `capability` first, then `orgName` fallback.
|
|
25
|
+
- **Backward compatible**: Existing `orgName`-based tags still work.
|
|
26
|
+
|
|
27
|
+
## Bug Fixes
|
|
28
|
+
- Fixed `#getModulesRoot` hitting nested `modules/` directories.
|
|
29
|
+
- Removed cleanup V1 — replaced by V2.
|
|
30
|
+
|
|
4
31
|
# 0.2.4
|
|
5
32
|
- Issue fixes on custom fixtures
|
|
6
33
|
- Page Fixture
|