@testomatio/reporter 2.7.1 → 2.7.2
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 +2 -1
- package/lib/adapter/codecept.js +81 -26
- package/lib/adapter/playwright.d.ts +1 -1
- package/lib/adapter/playwright.js +54 -34
- package/lib/adapter/utils/step-formatter.d.ts +134 -0
- package/lib/adapter/utils/step-formatter.js +237 -0
- package/lib/adapter/vitest.d.ts +9 -0
- package/lib/adapter/vitest.js +75 -29
- package/lib/bin/cli.js +28 -31
- package/lib/bin/reportXml.js +5 -6
- package/lib/bin/uploadArtifacts.js +6 -6
- package/lib/client.d.ts +8 -0
- package/lib/client.js +71 -10
- package/lib/constants.d.ts +1 -0
- package/lib/constants.js +7 -1
- package/lib/pipe/bitbucket.js +2 -1
- package/lib/pipe/coverage.js +16 -15
- package/lib/pipe/debug.js +3 -3
- package/lib/pipe/github.js +3 -2
- package/lib/pipe/gitlab.js +2 -1
- package/lib/pipe/index.js +5 -5
- package/lib/pipe/testomatio.js +21 -24
- package/lib/uploader.js +3 -2
- package/lib/utils/log.d.ts +45 -0
- package/lib/utils/log.js +98 -0
- package/lib/utils/pipe_utils.js +5 -5
- package/lib/utils/utils.d.ts +10 -0
- package/lib/utils/utils.js +16 -1
- package/lib/xmlReader.js +5 -4
- package/package.json +1 -1
- package/src/adapter/codecept.js +99 -29
- package/src/adapter/playwright.js +64 -39
- package/src/adapter/utils/step-formatter.js +232 -0
- package/src/adapter/vitest.js +70 -26
- package/src/bin/cli.js +34 -31
- package/src/bin/reportXml.js +5 -6
- package/src/bin/uploadArtifacts.js +6 -6
- package/src/client.js +76 -26
- package/src/constants.js +4 -0
- package/src/pipe/bitbucket.js +2 -1
- package/src/pipe/coverage.js +16 -15
- package/src/pipe/debug.js +3 -3
- package/src/pipe/github.js +4 -3
- package/src/pipe/gitlab.js +2 -1
- package/src/pipe/index.js +5 -7
- package/src/pipe/testomatio.js +32 -25
- package/src/uploader.js +3 -2
- package/src/utils/log.js +87 -0
- package/src/utils/pipe_utils.js +5 -5
- package/src/utils/utils.js +14 -0
- package/src/xmlReader.js +5 -4
- package/types/types.d.ts +3 -0
package/lib/uploader.js
CHANGED
|
@@ -14,6 +14,7 @@ const promise_retry_1 = __importDefault(require("promise-retry"));
|
|
|
14
14
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
15
15
|
const constants_js_1 = require("./constants.js");
|
|
16
16
|
const filesize_1 = require("filesize");
|
|
17
|
+
const log_js_1 = require("./utils/log.js");
|
|
17
18
|
const debug = (0, debug_1.default)('@testomatio/reporter:file-uploader');
|
|
18
19
|
class S3Uploader {
|
|
19
20
|
constructor() {
|
|
@@ -119,7 +120,7 @@ class S3Uploader {
|
|
|
119
120
|
catch (e) {
|
|
120
121
|
this.failedUploads.push({ path: file.path, size: file.size });
|
|
121
122
|
debug('S3 uploading error:', e);
|
|
122
|
-
|
|
123
|
+
log_js_1.log.info('Upload failed:', e.message, '\nConfig:\n', this.getMaskedConfig());
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
/**
|
|
@@ -142,7 +143,7 @@ class S3Uploader {
|
|
|
142
143
|
const diffHours = diff / 1000 / 60 / 60;
|
|
143
144
|
debug('Diff hours:', diffHours);
|
|
144
145
|
if (diffHours > 3) {
|
|
145
|
-
|
|
146
|
+
log_js_1.log.info("Artifacts file is too old, can't process artifacts. Please re-run the tests.");
|
|
146
147
|
return [];
|
|
147
148
|
}
|
|
148
149
|
const data = fs_1.default.readFileSync(tempFilePath, 'utf8');
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the current log level from TESTOMATIO_LOG_LEVEL environment variable.
|
|
3
|
+
* Defaults to INFO (info, warn, and error messages).
|
|
4
|
+
* @returns {number} Numeric log level (0-2)
|
|
5
|
+
*/
|
|
6
|
+
export function getLogLevel(): number;
|
|
7
|
+
/**
|
|
8
|
+
* Check if a message should be logged based on its level.
|
|
9
|
+
* A message is logged if its level is <= the current log level,
|
|
10
|
+
* or if TESTOMATIO_DEBUG is set (for debugging with the debug package).
|
|
11
|
+
* @param {number} messageLevel - Message level (LOG_LEVELS value)
|
|
12
|
+
* @returns {boolean} True if the message should be logged
|
|
13
|
+
*/
|
|
14
|
+
export function shouldLog(messageLevel: number): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Log an info message with [TESTOMATIO] prefix.
|
|
17
|
+
* Only logs when TESTOMATIO_LOG_LEVEL is INFO.
|
|
18
|
+
* @param {...any} args - Arguments to log
|
|
19
|
+
*/
|
|
20
|
+
export function info(...args: any[]): void;
|
|
21
|
+
/**
|
|
22
|
+
* Log a warning message with [TESTOMATIO] prefix.
|
|
23
|
+
* Only logs when TESTOMATIO_LOG_LEVEL is WARN or INFO.
|
|
24
|
+
* @param {...any} args - Arguments to log
|
|
25
|
+
*/
|
|
26
|
+
export function warn(...args: any[]): void;
|
|
27
|
+
/**
|
|
28
|
+
* Log an error message with [TESTOMATIO] prefix.
|
|
29
|
+
* Logs for all TESTOMATIO_LOG_LEVEL values.
|
|
30
|
+
* @param {...any} args - Arguments to log
|
|
31
|
+
*/
|
|
32
|
+
export function error(...args: any[]): void;
|
|
33
|
+
export namespace LOG_LEVELS {
|
|
34
|
+
let ERROR: number;
|
|
35
|
+
let WARN: number;
|
|
36
|
+
let INFO: number;
|
|
37
|
+
}
|
|
38
|
+
export namespace log {
|
|
39
|
+
export { info };
|
|
40
|
+
export { warn };
|
|
41
|
+
export { error };
|
|
42
|
+
export { getLogLevel };
|
|
43
|
+
export { shouldLog };
|
|
44
|
+
export { LOG_LEVELS };
|
|
45
|
+
}
|
package/lib/utils/log.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.log = exports.LOG_LEVELS = void 0;
|
|
4
|
+
exports.getLogLevel = getLogLevel;
|
|
5
|
+
exports.shouldLog = shouldLog;
|
|
6
|
+
exports.info = info;
|
|
7
|
+
exports.warn = warn;
|
|
8
|
+
exports.error = error;
|
|
9
|
+
const constants_js_1 = require("../constants.js");
|
|
10
|
+
/**
|
|
11
|
+
* Log levels for the Testomat.io reporter.
|
|
12
|
+
* A message is logged if its level is <= the current log level.
|
|
13
|
+
* @example
|
|
14
|
+
* TESTOMATIO_LOG_LEVEL=ERROR npx codeceptjs run // Only errors
|
|
15
|
+
* TESTOMATIO_LOG_LEVEL=WARN npx codeceptjs run // Warnings and errors
|
|
16
|
+
* TESTOMATIO_LOG_LEVEL=INFO npx codeceptjs run // Info, warnings, errors (default)
|
|
17
|
+
*/
|
|
18
|
+
exports.LOG_LEVELS = {
|
|
19
|
+
ERROR: 0,
|
|
20
|
+
WARN: 1,
|
|
21
|
+
INFO: 2,
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Get the current log level from TESTOMATIO_LOG_LEVEL environment variable.
|
|
25
|
+
* Defaults to INFO (info, warn, and error messages).
|
|
26
|
+
* @returns {number} Numeric log level (0-2)
|
|
27
|
+
*/
|
|
28
|
+
function getLogLevel() {
|
|
29
|
+
const envLevel = process.env.TESTOMATIO_LOG_LEVEL?.toUpperCase();
|
|
30
|
+
return exports.LOG_LEVELS[envLevel] ?? exports.LOG_LEVELS.INFO;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Check if a message should be logged based on its level.
|
|
34
|
+
* A message is logged if its level is <= the current log level,
|
|
35
|
+
* or if TESTOMATIO_DEBUG is set (for debugging with the debug package).
|
|
36
|
+
* @param {number} messageLevel - Message level (LOG_LEVELS value)
|
|
37
|
+
* @returns {boolean} True if the message should be logged
|
|
38
|
+
*/
|
|
39
|
+
function shouldLog(messageLevel) {
|
|
40
|
+
return messageLevel <= getLogLevel() || !!process.env.TESTOMATIO_DEBUG;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Log an info message with [TESTOMATIO] prefix.
|
|
44
|
+
* Only logs when TESTOMATIO_LOG_LEVEL is INFO.
|
|
45
|
+
* @param {...any} args - Arguments to log
|
|
46
|
+
*/
|
|
47
|
+
function info(...args) {
|
|
48
|
+
if (shouldLog(exports.LOG_LEVELS.INFO)) {
|
|
49
|
+
console.log(constants_js_1.APP_PREFIX, ...args);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Log a warning message with [TESTOMATIO] prefix.
|
|
54
|
+
* Only logs when TESTOMATIO_LOG_LEVEL is WARN or INFO.
|
|
55
|
+
* @param {...any} args - Arguments to log
|
|
56
|
+
*/
|
|
57
|
+
function warn(...args) {
|
|
58
|
+
if (shouldLog(exports.LOG_LEVELS.WARN)) {
|
|
59
|
+
console.warn(constants_js_1.APP_PREFIX, ...args);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Log an error message with [TESTOMATIO] prefix.
|
|
64
|
+
* Logs for all TESTOMATIO_LOG_LEVEL values.
|
|
65
|
+
* @param {...any} args - Arguments to log
|
|
66
|
+
*/
|
|
67
|
+
function error(...args) {
|
|
68
|
+
if (shouldLog(exports.LOG_LEVELS.ERROR)) {
|
|
69
|
+
console.error(constants_js_1.APP_PREFIX, ...args);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Logging utility for Testomat.io reporter.
|
|
74
|
+
* All messages are prefixed with [TESTOMATIO] and respect TESTOMATIO_LOG_LEVEL.
|
|
75
|
+
* @example
|
|
76
|
+
* import { log } from './utils/log.js';
|
|
77
|
+
* log.info('Test started');
|
|
78
|
+
* log.warn('This is a warning');
|
|
79
|
+
* log.error('Something went wrong');
|
|
80
|
+
*/
|
|
81
|
+
exports.log = {
|
|
82
|
+
info,
|
|
83
|
+
warn,
|
|
84
|
+
error,
|
|
85
|
+
getLogLevel,
|
|
86
|
+
shouldLog,
|
|
87
|
+
LOG_LEVELS: exports.LOG_LEVELS,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
module.exports.getLogLevel = getLogLevel;
|
|
91
|
+
|
|
92
|
+
module.exports.shouldLog = shouldLog;
|
|
93
|
+
|
|
94
|
+
module.exports.info = info;
|
|
95
|
+
|
|
96
|
+
module.exports.warn = warn;
|
|
97
|
+
|
|
98
|
+
module.exports.error = error;
|
package/lib/utils/pipe_utils.js
CHANGED
|
@@ -7,7 +7,7 @@ exports.setS3Credentials = setS3Credentials;
|
|
|
7
7
|
exports.statusEmoji = statusEmoji;
|
|
8
8
|
exports.fullName = fullName;
|
|
9
9
|
exports.parsePipeOptions = parsePipeOptions;
|
|
10
|
-
const
|
|
10
|
+
const log_js_1 = require("./log.js");
|
|
11
11
|
/**
|
|
12
12
|
* Set S3 credentials from the provided artifacts object.
|
|
13
13
|
* @param {Object} artifacts - The artifacts object containing S3 credentials.
|
|
@@ -15,7 +15,7 @@ const constants_js_1 = require("../constants.js");
|
|
|
15
15
|
function setS3Credentials(artifacts) {
|
|
16
16
|
if (!Object.keys(artifacts).length)
|
|
17
17
|
return;
|
|
18
|
-
|
|
18
|
+
log_js_1.log.info('S3 credentials obtained from Testomat.io...');
|
|
19
19
|
if (artifacts.ACCESS_KEY_ID)
|
|
20
20
|
process.env.S3_ACCESS_KEY_ID = artifacts.ACCESS_KEY_ID;
|
|
21
21
|
if (artifacts.SECRET_ACCESS_KEY)
|
|
@@ -41,7 +41,7 @@ function setS3Credentials(artifacts) {
|
|
|
41
41
|
function generateFilterRequestParams(params) {
|
|
42
42
|
// Defensive check: ensure params is an object
|
|
43
43
|
if (!params || typeof params !== 'object') {
|
|
44
|
-
|
|
44
|
+
log_js_1.log.error(`Invalid parameters provided. Expected an object, got: ${typeof params}`);
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
47
|
const { type, id, apiKey } = params;
|
|
@@ -49,7 +49,7 @@ function generateFilterRequestParams(params) {
|
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
51
|
if (!id) {
|
|
52
|
-
|
|
52
|
+
log_js_1.log.error(`Please make sure your settings "${type.toUpperCase()}"= "${id}" is correct!`);
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
55
|
return {
|
|
@@ -100,7 +100,7 @@ function updateFilterType(type) {
|
|
|
100
100
|
// "ims-issue", //TODO: WIP
|
|
101
101
|
];
|
|
102
102
|
if (!filterTypes.includes(typeLowerCase)) {
|
|
103
|
-
|
|
103
|
+
log_js_1.log.error(`❗ Invalid filter: "${type}" start settings! Available option list: ${filterTypes}`);
|
|
104
104
|
return;
|
|
105
105
|
}
|
|
106
106
|
const index = filterTypes.indexOf(typeLowerCase);
|
package/lib/utils/utils.d.ts
CHANGED
|
@@ -29,6 +29,16 @@ export function getGitCommitSha(): string | null;
|
|
|
29
29
|
*/
|
|
30
30
|
export function getTestomatIdFromTestTitle(testTitle: string): string | null;
|
|
31
31
|
export function humanize(text: any): any;
|
|
32
|
+
/**
|
|
33
|
+
* Checks whether a value is an HTTP(S) URL.
|
|
34
|
+
*
|
|
35
|
+
* Used for artifact handling: if a step artifact is already a remote URL,
|
|
36
|
+
* it should not be uploaded again as a local file path.
|
|
37
|
+
*
|
|
38
|
+
* @param {*} value - Artifact value to validate
|
|
39
|
+
* @returns {boolean} true when value starts with http:// or https://
|
|
40
|
+
*/
|
|
41
|
+
export function isHttpUrl(value: any): boolean;
|
|
32
42
|
export function isValidUrl(s: any): boolean;
|
|
33
43
|
/**
|
|
34
44
|
* @param {String} suiteTitle - suite title
|
package/lib/utils/utils.js
CHANGED
|
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.validateSuiteId = exports.testRunnerHelper = exports.specificTestInfo = exports.parseSuite = exports.isValidUrl = exports.humanize = exports.getTestomatIdFromTestTitle = exports.getGitCommitSha = exports.getCurrentDateTime = exports.foundedTestLog = exports.fileSystem = exports.fetchFilesFromStackTrace = exports.fetchIdFromOutput = exports.fetchIdFromCode = exports.fetchSourceCodeFromStackTrace = exports.fetchSourceCode = exports.isSameTest = exports.ansiRegExp = exports.SUITE_ID_REGEX = exports.TEST_ID_REGEX = void 0;
|
|
39
|
+
exports.validateSuiteId = exports.testRunnerHelper = exports.specificTestInfo = exports.parseSuite = exports.isValidUrl = exports.isHttpUrl = exports.humanize = exports.getTestomatIdFromTestTitle = exports.getGitCommitSha = exports.getCurrentDateTime = exports.foundedTestLog = exports.fileSystem = exports.fetchFilesFromStackTrace = exports.fetchIdFromOutput = exports.fetchIdFromCode = exports.fetchSourceCodeFromStackTrace = exports.fetchSourceCode = exports.isSameTest = exports.ansiRegExp = exports.SUITE_ID_REGEX = exports.TEST_ID_REGEX = void 0;
|
|
40
40
|
exports.getPackageVersion = getPackageVersion;
|
|
41
41
|
exports.truncate = truncate;
|
|
42
42
|
exports.cleanLatestRunId = cleanLatestRunId;
|
|
@@ -134,6 +134,19 @@ const isValidUrl = s => {
|
|
|
134
134
|
}
|
|
135
135
|
};
|
|
136
136
|
exports.isValidUrl = isValidUrl;
|
|
137
|
+
/**
|
|
138
|
+
* Checks whether a value is an HTTP(S) URL.
|
|
139
|
+
*
|
|
140
|
+
* Used for artifact handling: if a step artifact is already a remote URL,
|
|
141
|
+
* it should not be uploaded again as a local file path.
|
|
142
|
+
*
|
|
143
|
+
* @param {*} value - Artifact value to validate
|
|
144
|
+
* @returns {boolean} true when value starts with http:// or https://
|
|
145
|
+
*/
|
|
146
|
+
const isHttpUrl = value => {
|
|
147
|
+
return /^https?:\/\//i.test(String(value || ''));
|
|
148
|
+
};
|
|
149
|
+
exports.isHttpUrl = isHttpUrl;
|
|
137
150
|
const fileMatchRegex = /file:(\/*)([A-Za-z]:[\\/].*?|\/.*?)\.(png|avi|webm|jpg|html|txt)/gi;
|
|
138
151
|
const fetchFilesFromStackTrace = (stack = '', checkExists = true) => {
|
|
139
152
|
let files = Array.from(stack.matchAll(fileMatchRegex))
|
|
@@ -738,6 +751,8 @@ module.exports.ansiRegExp = ansiRegExp;
|
|
|
738
751
|
|
|
739
752
|
module.exports.isValidUrl = isValidUrl;
|
|
740
753
|
|
|
754
|
+
module.exports.isHttpUrl = isHttpUrl;
|
|
755
|
+
|
|
741
756
|
module.exports.fetchFilesFromStackTrace = fetchFilesFromStackTrace;
|
|
742
757
|
|
|
743
758
|
module.exports.fetchSourceCodeFromStackTrace = fetchSourceCodeFromStackTrace;
|
package/lib/xmlReader.js
CHANGED
|
@@ -17,6 +17,7 @@ const index_js_1 = require("./pipe/index.js");
|
|
|
17
17
|
const index_js_2 = __importDefault(require("./junit-adapter/index.js"));
|
|
18
18
|
const config_js_1 = require("./config.js");
|
|
19
19
|
const uploader_js_1 = require("./uploader.js");
|
|
20
|
+
const log_js_1 = require("./utils/log.js");
|
|
20
21
|
// @ts-ignore this line will be removed in compiled code, because __dirname is defined in commonjs
|
|
21
22
|
const debug = (0, debug_1.default)('@testomatio/reporter:xml');
|
|
22
23
|
const ridRunId = (0, crypto_1.randomUUID)();
|
|
@@ -62,7 +63,7 @@ class XmlReader {
|
|
|
62
63
|
// @ts-ignore
|
|
63
64
|
const packageJsonPath = path_1.default.resolve(__dirname, '..', 'package.json');
|
|
64
65
|
this.version = JSON.parse(fs_1.default.readFileSync(packageJsonPath).toString()).version;
|
|
65
|
-
|
|
66
|
+
log_js_1.log.info(`Testomatio Reporter v${this.version}`);
|
|
66
67
|
}
|
|
67
68
|
connectAdapter() {
|
|
68
69
|
if (this.opts.javaTests) {
|
|
@@ -435,7 +436,7 @@ class XmlReader {
|
|
|
435
436
|
continue;
|
|
436
437
|
const runId = this.runId || this.store.runId || Date.now().toString();
|
|
437
438
|
test.artifacts = await Promise.all(files.map(f => this.uploader.uploadFileByPath(f, [runId, path_1.default.basename(f)])));
|
|
438
|
-
|
|
439
|
+
log_js_1.log.info(`🗄️ Uploaded ${picocolors_1.default.bold(`${files.length} artifacts`)} for test ${test.title}`);
|
|
439
440
|
}
|
|
440
441
|
}
|
|
441
442
|
async createRun() {
|
|
@@ -528,10 +529,10 @@ class XmlReader {
|
|
|
528
529
|
debug(`Uploaded ${uploadedTests}/${totalTests} tests`);
|
|
529
530
|
}
|
|
530
531
|
if (totalChunks > 1) {
|
|
531
|
-
|
|
532
|
+
log_js_1.log.info(`✅ Successfully uploaded ${uploadedTests} tests in ${totalChunks} chunks`);
|
|
532
533
|
}
|
|
533
534
|
else {
|
|
534
|
-
|
|
535
|
+
log_js_1.log.info(`✅ Successfully uploaded ${uploadedTests} tests`);
|
|
535
536
|
}
|
|
536
537
|
const finishData = {
|
|
537
538
|
api_key: this.requestParams.apiKey,
|
package/package.json
CHANGED
package/src/adapter/codecept.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import createDebugMessages from 'debug';
|
|
2
2
|
import pc from 'picocolors';
|
|
3
3
|
import TestomatClient from '../client.js';
|
|
4
|
-
import { STATUS, APP_PREFIX, TESTOMAT_TMP_STORAGE_DIR } from '../constants.js';
|
|
4
|
+
import { STATUS, APP_PREFIX, TESTOMAT_TMP_STORAGE_DIR, SCREENSHOTS_ON_STEPS } from '../constants.js';
|
|
5
5
|
import { getTestomatIdFromTestTitle, truncate, fileSystem } from '../utils/utils.js';
|
|
6
6
|
import { services } from '../services/index.js';
|
|
7
7
|
import { dataStorage } from '../data-storage.js';
|
|
8
|
+
import { formatStep, addStatusToStep, addArtifactsToStep, addArtifactPathToStep } from './utils/step-formatter.js';
|
|
8
9
|
import codeceptjs from 'codeceptjs';
|
|
10
|
+
import { log } from '../utils/log.js';
|
|
9
11
|
|
|
10
12
|
const debug = createDebugMessages('@testomatio/reporter:adapter:codeceptjs');
|
|
11
13
|
// @ts-ignore
|
|
@@ -48,6 +50,7 @@ function CodeceptReporter(config) {
|
|
|
48
50
|
let videos = [];
|
|
49
51
|
let traces = [];
|
|
50
52
|
const reportTestPromises = [];
|
|
53
|
+
let isRunFinalized = false;
|
|
51
54
|
|
|
52
55
|
const testTimeMap = {};
|
|
53
56
|
const { apiKey } = config;
|
|
@@ -84,6 +87,19 @@ function CodeceptReporter(config) {
|
|
|
84
87
|
const hookSteps = new Map();
|
|
85
88
|
let currentHook = null;
|
|
86
89
|
|
|
90
|
+
const finalizeRun = async origin => {
|
|
91
|
+
if (isRunFinalized) return;
|
|
92
|
+
isRunFinalized = true;
|
|
93
|
+
|
|
94
|
+
debug(`finalizing run from ${origin}`);
|
|
95
|
+
debug('waiting for all tests to be reported');
|
|
96
|
+
|
|
97
|
+
await Promise.allSettled(reportTestPromises);
|
|
98
|
+
await uploadAttachments(client, videos, '🎞️ Uploading', 'video');
|
|
99
|
+
await uploadAttachments(client, traces, '📁 Uploading', 'trace');
|
|
100
|
+
await client.updateRunStatus('finished');
|
|
101
|
+
};
|
|
102
|
+
|
|
87
103
|
event.dispatcher.on(event.workers.before, () => {
|
|
88
104
|
recorder.add('Creating new run', async () => {
|
|
89
105
|
await client.createRun();
|
|
@@ -94,7 +110,9 @@ function CodeceptReporter(config) {
|
|
|
94
110
|
});
|
|
95
111
|
|
|
96
112
|
event.dispatcher.on(event.workers.after, () => {
|
|
97
|
-
|
|
113
|
+
recorder.add('Finishing run', async () => {
|
|
114
|
+
await finalizeRun('workers.after');
|
|
115
|
+
});
|
|
98
116
|
});
|
|
99
117
|
|
|
100
118
|
// Listening to events
|
|
@@ -108,6 +126,8 @@ function CodeceptReporter(config) {
|
|
|
108
126
|
});
|
|
109
127
|
videos = [];
|
|
110
128
|
traces = [];
|
|
129
|
+
isRunFinalized = false;
|
|
130
|
+
reportTestPromises.length = 0;
|
|
111
131
|
|
|
112
132
|
if (!global.testomatioDataStore) global.testomatioDataStore = {};
|
|
113
133
|
});
|
|
@@ -141,7 +161,7 @@ function CodeceptReporter(config) {
|
|
|
141
161
|
const error = hook?.ctx?.currentTest?.err;
|
|
142
162
|
|
|
143
163
|
for (const test of suite.tests) {
|
|
144
|
-
client.addTestRun('failed', {
|
|
164
|
+
const reportTestPromise = client.addTestRun('failed', {
|
|
145
165
|
...stripExampleFromTitle(test.title),
|
|
146
166
|
rid: test.uid,
|
|
147
167
|
test_id: getTestomatIdFromTestTitle(test.title),
|
|
@@ -149,6 +169,7 @@ function CodeceptReporter(config) {
|
|
|
149
169
|
error,
|
|
150
170
|
time: hook?.runnable?.duration,
|
|
151
171
|
});
|
|
172
|
+
reportTestPromises.push(reportTestPromise);
|
|
152
173
|
}
|
|
153
174
|
});
|
|
154
175
|
|
|
@@ -170,15 +191,10 @@ function CodeceptReporter(config) {
|
|
|
170
191
|
testTimeMap[test.uid] = Date.now();
|
|
171
192
|
});
|
|
172
193
|
|
|
173
|
-
event.dispatcher.on(event.all.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
await uploadAttachments(client, videos, '🎞️ Uploading', 'video');
|
|
179
|
-
await uploadAttachments(client, traces, '📁 Uploading', 'trace');
|
|
180
|
-
|
|
181
|
-
client.updateRunStatus('finished');
|
|
194
|
+
event.dispatcher.on(event.all.after, () => {
|
|
195
|
+
recorder.add('Finishing run', async () => {
|
|
196
|
+
await finalizeRun('all.after');
|
|
197
|
+
});
|
|
182
198
|
});
|
|
183
199
|
|
|
184
200
|
event.dispatcher.on(event.test.after, test => {
|
|
@@ -190,12 +206,19 @@ function CodeceptReporter(config) {
|
|
|
190
206
|
const logs = getTestLogs(test);
|
|
191
207
|
const manuallyAttachedArtifacts = services.artifacts.get(test.fullTitle());
|
|
192
208
|
const keyValues = services.keyValues.get(test.fullTitle());
|
|
193
|
-
const stepHierarchy = buildUnifiedStepHierarchy(test.steps, hookSteps);
|
|
194
209
|
const links = services.links.get(test.fullTitle());
|
|
210
|
+
const screenshotOnFailPath = artifacts.screenshot || null;
|
|
211
|
+
|
|
212
|
+
// Build step hierarchy with screenshot from screenshotOnFail
|
|
213
|
+
const stepHierarchy = buildUnifiedStepHierarchy(
|
|
214
|
+
test.steps,
|
|
215
|
+
hookSteps,
|
|
216
|
+
screenshotOnFailPath
|
|
217
|
+
);
|
|
195
218
|
|
|
196
219
|
services.setContext(null);
|
|
197
220
|
|
|
198
|
-
client.addTestRun(test.state, {
|
|
221
|
+
const reportTestPromise = client.addTestRun(test.state, {
|
|
199
222
|
...stripExampleFromTitle(title),
|
|
200
223
|
rid: uid,
|
|
201
224
|
test_id: getTestomatIdFromTestTitle(`${title} ${tags?.join(' ')}`),
|
|
@@ -210,6 +233,7 @@ function CodeceptReporter(config) {
|
|
|
210
233
|
manuallyAttachedArtifacts,
|
|
211
234
|
meta: { ...keyValues, ...test.meta },
|
|
212
235
|
});
|
|
236
|
+
reportTestPromises.push(reportTestPromise);
|
|
213
237
|
|
|
214
238
|
processArtifactsForUpload(artifacts, uid, title, videos, traces);
|
|
215
239
|
});
|
|
@@ -229,7 +253,7 @@ async function uploadAttachments(client, attachments, messagePrefix, attachmentT
|
|
|
229
253
|
if (!attachments?.length) return;
|
|
230
254
|
|
|
231
255
|
if (client.uploader.isEnabled) {
|
|
232
|
-
|
|
256
|
+
log.info(`Attachments: ${messagePrefix} ${attachments.length} ${attachmentType} ...`);
|
|
233
257
|
}
|
|
234
258
|
|
|
235
259
|
const promises = attachments.map(async attachment => {
|
|
@@ -372,7 +396,7 @@ function getTestLogs(test) {
|
|
|
372
396
|
}
|
|
373
397
|
|
|
374
398
|
// Build step hierarchy using CodeceptJS built-in methods
|
|
375
|
-
function buildUnifiedStepHierarchy(steps, hookSteps) {
|
|
399
|
+
function buildUnifiedStepHierarchy(steps, hookSteps, screenshotOnFailPath = null) {
|
|
376
400
|
const hierarchy = [];
|
|
377
401
|
|
|
378
402
|
// Add pre-test hooks
|
|
@@ -380,7 +404,7 @@ function buildUnifiedStepHierarchy(steps, hookSteps) {
|
|
|
380
404
|
|
|
381
405
|
// Process test steps if they exist
|
|
382
406
|
if (steps && steps.length > 0) {
|
|
383
|
-
processTestSteps(steps, hierarchy);
|
|
407
|
+
processTestSteps(steps, hierarchy, screenshotOnFailPath);
|
|
384
408
|
}
|
|
385
409
|
|
|
386
410
|
// Add post-test hooks
|
|
@@ -398,11 +422,18 @@ function addHooksToHierarchy(hierarchy, hookSteps, hookNames) {
|
|
|
398
422
|
}
|
|
399
423
|
}
|
|
400
424
|
|
|
401
|
-
function processTestSteps(steps, hierarchy) {
|
|
425
|
+
function processTestSteps(steps, hierarchy, screenshotOnFailPath = null) {
|
|
402
426
|
const sectionMap = new Map();
|
|
427
|
+
let screenshotAttached = false;
|
|
403
428
|
|
|
404
429
|
for (const step of steps) {
|
|
405
|
-
|
|
430
|
+
let stepScreenshotPath = null;
|
|
431
|
+
if (screenshotOnFailPath && !screenshotAttached && step.status === 'failed') {
|
|
432
|
+
stepScreenshotPath = screenshotOnFailPath;
|
|
433
|
+
screenshotAttached = true;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const formattedStep = formatCodeceptStep(step, stepScreenshotPath);
|
|
406
437
|
if (!formattedStep) continue;
|
|
407
438
|
|
|
408
439
|
if (step.metaStep) {
|
|
@@ -460,27 +491,45 @@ function formatHookName(hookName) {
|
|
|
460
491
|
}
|
|
461
492
|
|
|
462
493
|
// Format CodeceptJS step using its built-in methods
|
|
463
|
-
function formatCodeceptStep(step) {
|
|
494
|
+
function formatCodeceptStep(step, screenshotOnFailPath = null) {
|
|
464
495
|
if (!step) return null;
|
|
465
496
|
|
|
466
497
|
const category = step.constructor.name === 'HelperStep' ? 'framework' : 'user';
|
|
467
|
-
const title = truncate(step);
|
|
468
|
-
const duration = step.duration || 0;
|
|
498
|
+
const title = truncate(String(step));
|
|
499
|
+
const duration = step.duration || 0;
|
|
469
500
|
|
|
470
|
-
const formattedStep = {
|
|
501
|
+
const formattedStep = formatStep({
|
|
471
502
|
category,
|
|
472
503
|
title,
|
|
473
504
|
duration,
|
|
474
|
-
};
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
// Add status
|
|
508
|
+
addStatusToStep(formattedStep, step.status, step.err);
|
|
475
509
|
|
|
476
510
|
// Add error if step failed
|
|
477
511
|
if (step.status === 'failed' && step.err) {
|
|
478
512
|
formattedStep.error = {
|
|
479
|
-
message: step.err.message || 'Step failed',
|
|
480
|
-
stack: step.err.stack || '',
|
|
513
|
+
message: truncate(String(step.err.message || 'Step failed'), 250),
|
|
514
|
+
stack: truncate(String(step.err.stack || ''), 250),
|
|
481
515
|
};
|
|
482
516
|
}
|
|
483
517
|
|
|
518
|
+
// Add artifacts
|
|
519
|
+
if (step.artifacts && SCREENSHOTS_ON_STEPS) {
|
|
520
|
+
addArtifactsToStep(formattedStep, step.artifacts);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Add screenshot from screenshotOnFail plugin
|
|
524
|
+
if (screenshotOnFailPath && SCREENSHOTS_ON_STEPS) {
|
|
525
|
+
addArtifactPathToStep(formattedStep, screenshotOnFailPath);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Add log if present
|
|
529
|
+
if (step.log) {
|
|
530
|
+
formattedStep.log = truncate(String(step.log), 250);
|
|
531
|
+
}
|
|
532
|
+
|
|
484
533
|
return formattedStep;
|
|
485
534
|
}
|
|
486
535
|
|
|
@@ -492,17 +541,38 @@ function formatHookStep(step) {
|
|
|
492
541
|
if (step.actor && step.name) {
|
|
493
542
|
title = `${step.actor} ${step.name}`;
|
|
494
543
|
if (step.args && step.args.length > 0) {
|
|
495
|
-
const argsStr = step.args.map(arg => truncate(JSON.stringify(arg))).join(', ');
|
|
544
|
+
const argsStr = step.args.map(arg => truncate(JSON.stringify(arg), 250)).join(', ');
|
|
496
545
|
title += ` ${argsStr}`;
|
|
497
546
|
}
|
|
498
547
|
}
|
|
499
548
|
title = truncate(title);
|
|
500
549
|
|
|
501
|
-
|
|
550
|
+
const formattedStep = formatStep({
|
|
502
551
|
category: 'hook',
|
|
503
552
|
title,
|
|
504
553
|
duration: step.duration || 0,
|
|
505
|
-
};
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
addStatusToStep(formattedStep, step.status, step.err);
|
|
557
|
+
|
|
558
|
+
if (step.status === 'failed' && step.err) {
|
|
559
|
+
formattedStep.error = {
|
|
560
|
+
message: truncate(String(step.err.message || 'Hook failed'), 250),
|
|
561
|
+
stack: truncate(String(step.err.stack || ''), 250),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Add artifacts
|
|
566
|
+
if (step.artifacts && SCREENSHOTS_ON_STEPS) {
|
|
567
|
+
addArtifactsToStep(formattedStep, step.artifacts);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Add log if present
|
|
571
|
+
if (step.log) {
|
|
572
|
+
formattedStep.log = truncate(String(step.log), 250);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return formattedStep;
|
|
506
576
|
}
|
|
507
577
|
|
|
508
578
|
export { CodeceptReporter };
|