@testomatio/reporter 2.1.0-beta-nightwatch → 2.1.0-beta.1-codeceptjs
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 +1 -0
- package/lib/adapter/codecept.js +288 -202
- package/lib/adapter/cypress-plugin/index.js +0 -2
- package/lib/adapter/mocha.js +0 -1
- package/lib/adapter/nightwatch.js +5 -5
- package/lib/adapter/playwright.js +11 -3
- package/lib/adapter/webdriver.d.ts +1 -1
- package/lib/adapter/webdriver.js +18 -8
- package/lib/bin/cli.js +73 -8
- package/lib/bin/reportXml.js +4 -2
- package/lib/bin/startTest.js +3 -2
- package/lib/bin/uploadArtifacts.js +5 -4
- package/lib/client.js +30 -10
- package/lib/data-storage.d.ts +5 -5
- package/lib/data-storage.js +23 -13
- package/lib/junit-adapter/csharp.d.ts +1 -0
- package/lib/junit-adapter/csharp.js +11 -1
- package/lib/pipe/bitbucket.d.ts +2 -0
- package/lib/pipe/bitbucket.js +38 -26
- package/lib/pipe/debug.js +27 -6
- package/lib/pipe/github.d.ts +2 -2
- package/lib/pipe/github.js +35 -3
- package/lib/pipe/gitlab.d.ts +2 -0
- package/lib/pipe/gitlab.js +27 -9
- package/lib/pipe/html.js +0 -3
- package/lib/pipe/index.js +17 -7
- package/lib/pipe/testomatio.d.ts +3 -2
- package/lib/pipe/testomatio.js +85 -75
- package/lib/replay.d.ts +31 -0
- package/lib/replay.js +255 -0
- package/lib/reporter-functions.d.ts +7 -0
- package/lib/reporter-functions.js +36 -0
- package/lib/reporter.d.ts +15 -12
- package/lib/reporter.js +4 -1
- package/lib/services/artifacts.d.ts +1 -1
- package/lib/services/index.d.ts +2 -0
- package/lib/services/index.js +2 -0
- package/lib/services/key-values.d.ts +1 -1
- package/lib/services/labels.d.ts +22 -0
- package/lib/services/labels.js +62 -0
- package/lib/services/logger.d.ts +1 -1
- package/lib/services/logger.js +1 -2
- package/lib/template/testomatio.hbs +443 -68
- package/lib/uploader.js +10 -6
- package/lib/utils/constants.d.ts +12 -0
- package/lib/utils/constants.js +15 -0
- package/lib/utils/utils.d.ts +10 -1
- package/lib/utils/utils.js +70 -22
- package/lib/xmlReader.js +54 -19
- package/package.json +16 -11
- package/src/adapter/codecept.js +320 -214
- package/src/adapter/cypress-plugin/index.js +0 -2
- package/src/adapter/mocha.js +0 -1
- package/src/adapter/nightwatch.js +1 -1
- package/src/adapter/playwright.js +10 -7
- package/src/adapter/webdriver.js +2 -2
- package/src/bin/cli.js +70 -2
- package/src/bin/reportXml.js +4 -1
- package/src/bin/startTest.js +2 -1
- package/src/bin/uploadArtifacts.js +2 -1
- package/src/client.js +18 -3
- package/src/data-storage.js +6 -6
- package/src/junit-adapter/csharp.js +13 -1
- package/src/pipe/bitbucket.js +22 -24
- package/src/pipe/debug.js +26 -5
- package/src/pipe/github.js +1 -2
- package/src/pipe/gitlab.js +27 -9
- package/src/pipe/html.js +1 -4
- package/src/pipe/testomatio.js +106 -105
- package/src/replay.js +262 -0
- package/src/reporter-functions.js +41 -0
- package/src/reporter.js +3 -0
- package/src/services/index.js +2 -0
- package/src/services/labels.js +59 -0
- package/src/services/logger.js +1 -2
- package/src/template/testomatio.hbs +443 -68
- package/src/uploader.js +11 -6
- package/src/utils/constants.js +12 -0
- package/src/utils/utils.js +46 -13
- package/src/xmlReader.js +70 -18
package/src/utils/utils.js
CHANGED
|
@@ -5,9 +5,16 @@ import fs from 'fs';
|
|
|
5
5
|
import isValid from 'is-valid-path';
|
|
6
6
|
import createDebugMessages from 'debug';
|
|
7
7
|
import os from 'os';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
8
9
|
|
|
9
10
|
const debug = createDebugMessages('@testomatio/reporter:util');
|
|
10
11
|
|
|
12
|
+
// Use __dirname directly since we're compiling to CommonJS
|
|
13
|
+
// prettier-ignore
|
|
14
|
+
// @ts-ignore
|
|
15
|
+
// eslint-disable-next-line max-len
|
|
16
|
+
const __dirname = typeof global.__dirname !== 'undefined' ? global.__dirname : path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
|
|
11
18
|
/**
|
|
12
19
|
* @param {String} testTitle - Test title
|
|
13
20
|
*
|
|
@@ -33,12 +40,24 @@ const getTestomatIdFromTestTitle = testTitle => {
|
|
|
33
40
|
const parseSuite = suiteTitle => {
|
|
34
41
|
const captures = suiteTitle.match(/@S[\w\d]{8}/);
|
|
35
42
|
if (captures) {
|
|
36
|
-
return captures[
|
|
43
|
+
return captures[0];
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
return null;
|
|
40
47
|
};
|
|
41
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Validates TESTOMATIO_SUITE environment variable format
|
|
51
|
+
* @param {String} suiteId - suite ID to validate
|
|
52
|
+
* @returns {String|null} validated suite ID or null if invalid
|
|
53
|
+
*/
|
|
54
|
+
const validateSuiteId = suiteId => {
|
|
55
|
+
if (!suiteId) return null;
|
|
56
|
+
|
|
57
|
+
const match = suiteId.match(SUITE_ID_REGEX);
|
|
58
|
+
return match ? match[0] : null;
|
|
59
|
+
};
|
|
60
|
+
|
|
42
61
|
const ansiRegExp = () => {
|
|
43
62
|
const pattern = [
|
|
44
63
|
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
|
|
@@ -50,7 +69,6 @@ const ansiRegExp = () => {
|
|
|
50
69
|
|
|
51
70
|
const isValidUrl = s => {
|
|
52
71
|
try {
|
|
53
|
-
// eslint-disable-next-line no-new
|
|
54
72
|
new URL(s);
|
|
55
73
|
return true;
|
|
56
74
|
} catch (err) {
|
|
@@ -58,16 +76,25 @@ const isValidUrl = s => {
|
|
|
58
76
|
}
|
|
59
77
|
};
|
|
60
78
|
|
|
61
|
-
const fileMatchRegex = /file:(
|
|
79
|
+
const fileMatchRegex = /file:(\/+(?:[A-Za-z]:[\\/]|\/)?[^\s]*?\.(png|avi|webm|jpg|html|txt))/gi;
|
|
62
80
|
|
|
63
|
-
const fetchFilesFromStackTrace = (stack = '') => {
|
|
81
|
+
const fetchFilesFromStackTrace = (stack = '', checkExists = true) => {
|
|
64
82
|
const files = Array.from(stack.matchAll(fileMatchRegex))
|
|
65
83
|
.map(f => f[1].trim())
|
|
66
|
-
.map(f =>
|
|
84
|
+
.map(f => f.replace(/^\/+/, '/').replace(/^\/([A-Za-z]:)/, '$1')) // Remove extra slashes, handle Windows paths
|
|
85
|
+
.map(f => {
|
|
86
|
+
// Convert Windows paths to Linux paths for testing purposes
|
|
87
|
+
if (f.match(/^[A-Za-z]:[\\\/]/)) {
|
|
88
|
+
// Convert Windows path to Linux equivalent for test scenarios
|
|
89
|
+
return f.replace(/^[A-Za-z]:[\\\/]/, '/').replace(/\\/g, '/');
|
|
90
|
+
}
|
|
91
|
+
return f;
|
|
92
|
+
});
|
|
67
93
|
|
|
68
94
|
debug('Found files in stack trace: ', files);
|
|
69
95
|
|
|
70
96
|
return files.filter(f => {
|
|
97
|
+
if (!checkExists) return true;
|
|
71
98
|
const isFile = fs.existsSync(f);
|
|
72
99
|
if (!isFile) debug('File %s could not be found and uploaded as artifact', f);
|
|
73
100
|
return isFile;
|
|
@@ -108,7 +135,8 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
|
|
|
108
135
|
.join('\n');
|
|
109
136
|
};
|
|
110
137
|
|
|
111
|
-
const TEST_ID_REGEX = /@T([\w\d]{8})/;
|
|
138
|
+
export const TEST_ID_REGEX = /@T([\w\d]{8})/;
|
|
139
|
+
export const SUITE_ID_REGEX = /@S([\w\d]{8})/;
|
|
112
140
|
|
|
113
141
|
const fetchIdFromCode = (code, opts = {}) => {
|
|
114
142
|
const comments = code
|
|
@@ -128,12 +156,9 @@ const fetchIdFromCode = (code, opts = {}) => {
|
|
|
128
156
|
};
|
|
129
157
|
|
|
130
158
|
const fetchIdFromOutput = output => {
|
|
131
|
-
const
|
|
132
|
-
.split('\n')
|
|
133
|
-
.map(l => l.trim())
|
|
134
|
-
.filter(l => l.startsWith('tid://'));
|
|
159
|
+
const TID_FULL_PATTERN = new RegExp(`tid:\\/\\/.*?(${TEST_ID_REGEX.source})`);
|
|
135
160
|
|
|
136
|
-
return
|
|
161
|
+
return output.match(TID_FULL_PATTERN)?.[2];
|
|
137
162
|
};
|
|
138
163
|
|
|
139
164
|
const fetchSourceCode = (contents, opts = {}) => {
|
|
@@ -154,6 +179,9 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
154
179
|
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`@DisplayName("${title}`));
|
|
155
180
|
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
|
|
156
181
|
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
182
|
+
} else if (opts.lang === 'csharp') {
|
|
183
|
+
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
|
|
184
|
+
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
157
185
|
} else {
|
|
158
186
|
lineIndex = lines.findIndex(l => l.includes(title));
|
|
159
187
|
}
|
|
@@ -300,7 +328,6 @@ const decamelize = text => {
|
|
|
300
328
|
* @returns
|
|
301
329
|
*/
|
|
302
330
|
function removeColorCodes(input) {
|
|
303
|
-
// eslint-disable-next-line no-control-regex
|
|
304
331
|
return input.replace(/\x1b\[[0-9;]*m/g, '');
|
|
305
332
|
}
|
|
306
333
|
|
|
@@ -313,7 +340,6 @@ const testRunnerHelper = {
|
|
|
313
340
|
try {
|
|
314
341
|
// TODO: expect?.getState()?.testPath + ' ' + expect?.getState()?.currentTestName
|
|
315
342
|
// @ts-expect-error "expect" could only be defined inside Jest environement (forbidden to import it outside)
|
|
316
|
-
// eslint-disable-next-line no-undef
|
|
317
343
|
return expect?.getState()?.currentTestName;
|
|
318
344
|
} catch (e) {
|
|
319
345
|
return null;
|
|
@@ -359,6 +385,12 @@ function formatStep(step, shift = 0) {
|
|
|
359
385
|
return lines;
|
|
360
386
|
}
|
|
361
387
|
|
|
388
|
+
export function getPackageVersion() {
|
|
389
|
+
const packageJsonPath = path.resolve(__dirname, '../../package.json');
|
|
390
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
391
|
+
return packageJson.version;
|
|
392
|
+
}
|
|
393
|
+
|
|
362
394
|
export {
|
|
363
395
|
ansiRegExp,
|
|
364
396
|
isSameTest,
|
|
@@ -380,4 +412,5 @@ export {
|
|
|
380
412
|
specificTestInfo,
|
|
381
413
|
storeRunId,
|
|
382
414
|
testRunnerHelper,
|
|
415
|
+
validateSuiteId,
|
|
383
416
|
};
|
package/src/xmlReader.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
fetchSourceCodeFromStackTrace,
|
|
14
14
|
fetchIdFromCode,
|
|
15
15
|
humanize,
|
|
16
|
+
TEST_ID_REGEX,
|
|
16
17
|
} from './utils/utils.js';
|
|
17
18
|
import { pipesFactory } from './pipe/index.js';
|
|
18
19
|
import adapterFactory from './junit-adapter/index.js';
|
|
@@ -26,8 +27,15 @@ const debug = createDebugMessages('@testomatio/reporter:xml');
|
|
|
26
27
|
const ridRunId = randomUUID();
|
|
27
28
|
|
|
28
29
|
const TESTOMATIO_URL = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
|
|
29
|
-
const {
|
|
30
|
-
|
|
30
|
+
const {
|
|
31
|
+
TESTOMATIO_RUNGROUP_TITLE,
|
|
32
|
+
TESTOMATIO_SUITE,
|
|
33
|
+
TESTOMATIO_MAX_STACK_TRACE,
|
|
34
|
+
TESTOMATIO_TITLE,
|
|
35
|
+
TESTOMATIO_ENV,
|
|
36
|
+
TESTOMATIO_RUN,
|
|
37
|
+
TESTOMATIO_MARK_DETACHED,
|
|
38
|
+
} = process.env;
|
|
31
39
|
|
|
32
40
|
const options = {
|
|
33
41
|
ignoreDeclaration: true,
|
|
@@ -37,6 +45,8 @@ const options = {
|
|
|
37
45
|
parseTagValue: true,
|
|
38
46
|
};
|
|
39
47
|
|
|
48
|
+
const MAX_OUTPUT_LENGTH = parseInt(TESTOMATIO_MAX_STACK_TRACE, 10) || 10000;
|
|
49
|
+
|
|
40
50
|
const reduceOptions = {};
|
|
41
51
|
|
|
42
52
|
class XmlReader {
|
|
@@ -91,7 +101,7 @@ class XmlReader {
|
|
|
91
101
|
];
|
|
92
102
|
|
|
93
103
|
for (const regex of cutRegexes) {
|
|
94
|
-
xmlData = xmlData.replace(regex, (_, p1, p2, p3) => `${p1}${p2.substring(0,
|
|
104
|
+
xmlData = xmlData.replace(regex, (_, p1, p2, p3) => `${p1}${p2.substring(0, MAX_OUTPUT_LENGTH)}${p3}`);
|
|
95
105
|
}
|
|
96
106
|
|
|
97
107
|
const jsonResult = this.parser.parse(xmlData);
|
|
@@ -226,7 +236,7 @@ class XmlReader {
|
|
|
226
236
|
|
|
227
237
|
return {
|
|
228
238
|
status,
|
|
229
|
-
create_tests:
|
|
239
|
+
create_tests: !process.env.IGNORE_NEW_TESTS,
|
|
230
240
|
tests_count: parseInt(counters.total, 10),
|
|
231
241
|
passed_count: parseInt(counters.passed, 10),
|
|
232
242
|
skipped_count: parseInt(counters.notExecuted, 10),
|
|
@@ -341,6 +351,7 @@ class XmlReader {
|
|
|
341
351
|
if (file.endsWith('.rb')) this.stats.language = 'ruby';
|
|
342
352
|
if (file.endsWith('.js')) this.stats.language = 'js';
|
|
343
353
|
if (file.endsWith('.ts')) this.stats.language = 'ts';
|
|
354
|
+
if (file.endsWith('.cs')) this.stats.language = 'csharp';
|
|
344
355
|
}
|
|
345
356
|
|
|
346
357
|
if (!fs.existsSync(file)) {
|
|
@@ -394,13 +405,14 @@ class XmlReader {
|
|
|
394
405
|
async uploadArtifacts() {
|
|
395
406
|
for (const test of this.tests.filter(t => !!t.stack)) {
|
|
396
407
|
let files = [];
|
|
397
|
-
if (test.files?.length)
|
|
398
|
-
|
|
408
|
+
if (!test.files?.length) continue;
|
|
409
|
+
|
|
410
|
+
files = test.files.map(f => (path.isAbsolute(f) ? f : path.join(process.cwd(), f)));
|
|
399
411
|
|
|
400
412
|
if (!files.length) continue;
|
|
401
413
|
|
|
402
414
|
const runId = this.runId || this.store.runId || Date.now().toString();
|
|
403
|
-
test.artifacts = await Promise.all(files.map(f => this.uploader.uploadFileByPath(f, [runId])));
|
|
415
|
+
test.artifacts = await Promise.all(files.map(f => this.uploader.uploadFileByPath(f, [runId, path.basename(f)])));
|
|
404
416
|
console.log(APP_PREFIX, `🗄️ Uploaded ${pc.bold(`${files.length} artifacts`)} for test ${test.title}`);
|
|
405
417
|
}
|
|
406
418
|
}
|
|
@@ -460,14 +472,18 @@ function reduceTestCases(prev, item) {
|
|
|
460
472
|
}
|
|
461
473
|
|
|
462
474
|
// suite inside test case
|
|
463
|
-
|
|
475
|
+
const testCase = item['test-suite']?.['test-case'];
|
|
476
|
+
if (testCase) {
|
|
477
|
+
const nestedCases = Array.isArray(testCase) ? testCase : [testCase];
|
|
478
|
+
testCases.push(...nestedCases);
|
|
479
|
+
}
|
|
464
480
|
|
|
465
481
|
const suiteOutput = item['system-out'] || item.output || item.log || '';
|
|
466
482
|
const suiteErr = item['system-err'] || item.output || item.log || '';
|
|
467
483
|
testCases
|
|
468
484
|
.filter(t => !!t)
|
|
469
485
|
.forEach(testCaseItem => {
|
|
470
|
-
const file = testCaseItem.file || item.filepath || '';
|
|
486
|
+
const file = testCaseItem.file || item.filepath || item.fullname || item.package || '';
|
|
471
487
|
|
|
472
488
|
let stack = '';
|
|
473
489
|
let message = '';
|
|
@@ -485,7 +501,7 @@ function reduceTestCases(prev, item) {
|
|
|
485
501
|
const preferClassname = reduceOptions.preferClassname || isParametrized;
|
|
486
502
|
|
|
487
503
|
// SpecFlow config
|
|
488
|
-
let { title, tags } = fetchProperties(isParametrized ? item : testCaseItem);
|
|
504
|
+
let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
|
|
489
505
|
let example = null;
|
|
490
506
|
const suiteTitle = preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname;
|
|
491
507
|
|
|
@@ -498,19 +514,44 @@ function reduceTestCases(prev, item) {
|
|
|
498
514
|
title = title.replace(/\(.*?\)/, '').trim();
|
|
499
515
|
}
|
|
500
516
|
|
|
501
|
-
// eslint-disable-next-line
|
|
502
517
|
stack = `${
|
|
503
518
|
testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''
|
|
504
519
|
}\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim();
|
|
505
|
-
|
|
520
|
+
|
|
521
|
+
if (!testId) testId = fetchIdFromOutput(stack);
|
|
522
|
+
|
|
523
|
+
if (tags?.length && !testId) {
|
|
524
|
+
testId = tags
|
|
525
|
+
.filter(t => t.startsWith('T'))
|
|
526
|
+
.map(t => `@${t}`)
|
|
527
|
+
.find(t => t.match(TEST_ID_REGEX))
|
|
528
|
+
?.slice(2);
|
|
529
|
+
}
|
|
506
530
|
|
|
507
531
|
let status = STATUS.PASSED.toString();
|
|
508
532
|
if ('failure' in testCaseItem || 'error' in testCaseItem) status = STATUS.FAILED;
|
|
509
533
|
if ('skipped' in testCaseItem) status = STATUS.SKIPPED;
|
|
534
|
+
if (testCaseItem.result && Object.values(STATUS).includes(testCaseItem.result.toLowerCase())) {
|
|
535
|
+
status = testCaseItem.result.toLowerCase();
|
|
536
|
+
}
|
|
510
537
|
|
|
511
538
|
let rid = null;
|
|
512
539
|
if (testCaseItem.id) rid = `${ridRunId}-${testCaseItem.id}`;
|
|
513
540
|
|
|
541
|
+
// Extract attachments
|
|
542
|
+
let files = [];
|
|
543
|
+
if (testCaseItem.attachments) {
|
|
544
|
+
const attachments = Array.isArray(testCaseItem.attachments.attachment)
|
|
545
|
+
? testCaseItem.attachments.attachment
|
|
546
|
+
: [testCaseItem.attachments.attachment];
|
|
547
|
+
|
|
548
|
+
files = attachments.filter(a => a && a.filePath).map(a => a.filePath);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Extract files from stack trace using existing utility
|
|
552
|
+
const stackFiles = fetchFilesFromStackTrace(stack);
|
|
553
|
+
files = [...new Set([...files, ...stackFiles])]; // Remove duplicates
|
|
554
|
+
|
|
514
555
|
prev.push({
|
|
515
556
|
rid,
|
|
516
557
|
file,
|
|
@@ -525,7 +566,9 @@ function reduceTestCases(prev, item) {
|
|
|
525
566
|
run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
|
|
526
567
|
status,
|
|
527
568
|
title,
|
|
569
|
+
root_suite_id: TESTOMATIO_SUITE,
|
|
528
570
|
suite_title: suiteTitle,
|
|
571
|
+
files,
|
|
529
572
|
});
|
|
530
573
|
});
|
|
531
574
|
return prev;
|
|
@@ -552,11 +595,20 @@ function fetchProperties(item) {
|
|
|
552
595
|
|
|
553
596
|
if (!item.properties) return {};
|
|
554
597
|
|
|
555
|
-
|
|
598
|
+
// Handle both single property and array of properties
|
|
599
|
+
const properties = Array.isArray(item.properties.property)
|
|
600
|
+
? item.properties.property
|
|
601
|
+
: [item.properties.property].filter(Boolean);
|
|
602
|
+
|
|
603
|
+
const prop = properties.find(p => p.name === 'Description');
|
|
556
604
|
if (prop) title = prop.value;
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
605
|
+
|
|
606
|
+
let testId = properties.find(p => p.name === 'ID')?.value;
|
|
607
|
+
|
|
608
|
+
if (testId?.startsWith('@')) testId = testId.slice(1);
|
|
609
|
+
if (testId?.startsWith('T')) testId = testId.slice(1);
|
|
610
|
+
|
|
611
|
+
properties.filter(p => p.name === 'Category').forEach(p => tags.push(p.value));
|
|
612
|
+
|
|
613
|
+
return { title, tags, testId };
|
|
562
614
|
}
|