@testomatio/reporter 2.3.9-beta-bin-fix β 2.4.0
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 +3 -2
- package/lib/adapter/codecept.js +12 -9
- package/lib/bin/cli.js +40 -11
- package/lib/bin/reportXml.js +5 -2
- package/lib/client.d.ts +1 -11
- package/lib/client.js +57 -152
- package/lib/data-storage.d.ts +1 -1
- package/lib/helpers.d.ts +1 -0
- package/lib/helpers.js +4 -0
- package/lib/junit-adapter/csharp.d.ts +0 -1
- package/lib/junit-adapter/csharp.js +43 -7
- package/lib/junit-adapter/nunit-parser.d.ts +82 -0
- package/lib/junit-adapter/nunit-parser.js +433 -0
- package/lib/pipe/bitbucket.js +5 -5
- package/lib/pipe/coverage.d.ts +82 -0
- package/lib/pipe/coverage.js +373 -0
- package/lib/pipe/gitlab.js +4 -4
- package/lib/pipe/index.js +2 -0
- package/lib/pipe/testomatio.d.ts +3 -2
- package/lib/pipe/testomatio.js +44 -18
- package/lib/reporter-functions.js +14 -12
- package/lib/reporter.d.ts +31 -21
- package/lib/reporter.js +40 -5
- package/lib/services/artifacts.d.ts +1 -1
- package/lib/services/key-values.d.ts +1 -1
- package/lib/services/links.d.ts +1 -1
- package/lib/services/logger.d.ts +1 -1
- package/lib/uploader.js +4 -0
- package/lib/utils/log-formatter.d.ts +28 -0
- package/lib/utils/log-formatter.js +127 -0
- package/lib/utils/pipe_utils.d.ts +15 -0
- package/lib/utils/pipe_utils.js +44 -2
- package/lib/utils/utils.d.ts +6 -0
- package/lib/utils/utils.js +260 -25
- package/lib/xmlReader.d.ts +32 -26
- package/lib/xmlReader.js +121 -52
- package/package.json +12 -7
- package/src/adapter/codecept.js +19 -19
- package/src/adapter/mocha.js +1 -1
- package/src/adapter/playwright.js +2 -2
- package/src/bin/cli.js +51 -13
- package/src/bin/reportXml.js +5 -2
- package/src/client.js +69 -130
- package/src/helpers.js +1 -0
- package/src/junit-adapter/csharp.js +48 -6
- package/src/junit-adapter/nunit-parser.js +474 -0
- package/src/pipe/bitbucket.js +5 -5
- package/src/pipe/coverage.js +440 -0
- package/src/pipe/debug.js +1 -2
- package/src/pipe/gitlab.js +4 -4
- package/src/pipe/index.js +2 -0
- package/src/pipe/testomatio.js +109 -85
- package/src/reporter-functions.js +15 -12
- package/src/reporter.js +6 -4
- package/src/services/links.js +1 -1
- package/src/uploader.js +5 -0
- package/src/utils/log-formatter.js +113 -0
- package/src/utils/pipe_utils.js +52 -3
- package/src/utils/utils.js +277 -22
- package/src/xmlReader.js +144 -46
- package/types/types.d.ts +364 -0
- package/types/vitest.types.d.ts +93 -0
package/README.md
CHANGED
|
@@ -13,14 +13,14 @@ Testomat.io Reporter (this npm package) supports:
|
|
|
13
13
|
- π [Stack traces](./docs/stacktrace.md) and error messages
|
|
14
14
|
- π [GitHub](./docs/pipes/github.md), [GitLab](./docs/pipes/gitlab.md) & [Bitbucket](./docs/pipes/bitbucket.md) integration
|
|
15
15
|
- π
Realtime reports
|
|
16
|
-
- ποΈ Other test frameworks supported via [
|
|
16
|
+
- ποΈ Other test frameworks supported via [JUnit XML](./docs/junit.md) with [XML import configuration](./docs/xml-imports.md)
|
|
17
17
|
- πΆββοΈ Steps _(work in progress)_
|
|
18
18
|
- π [Logger](./docs/logger.md) _(work in progress, supports Jest for now)_
|
|
19
19
|
- βοΈ Custom properties and metadata _(work in progress)_
|
|
20
20
|
- π― Free & open-source.
|
|
21
21
|
- π Public and private Run reports on cloud via [Testomat.io App](https://testomat.io) π
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
<img width="1920" height="1085" alt="image" src="https://github.com/user-attachments/assets/cf823e8b-1305-4ed2-a7c5-712efec12ceb" />
|
|
24
24
|
|
|
25
25
|
## How It Works
|
|
26
26
|
|
|
@@ -129,6 +129,7 @@ Bring this reporter on CI and never lose test results again!
|
|
|
129
129
|
- [CSV](./docs/pipes/csv.md)
|
|
130
130
|
- [HTML report](./docs/pipes/html.md)
|
|
131
131
|
- [Bitbucket](./docs/pipes/bitbucket.md)
|
|
132
|
+
- π [Linking Tests](./docs/linking-tests.md)
|
|
132
133
|
- π [JUnit](./docs/junit.md)
|
|
133
134
|
- ποΈ [Artifacts](./docs/artifacts.md)
|
|
134
135
|
- π [Workflows](./docs/workflows.md)
|
package/lib/adapter/codecept.js
CHANGED
|
@@ -20,11 +20,14 @@ if (!global.codeceptjs) {
|
|
|
20
20
|
}
|
|
21
21
|
// @ts-ignore
|
|
22
22
|
const { event, recorder, codecept, output } = global.codeceptjs;
|
|
23
|
-
const [, MAJOR_VERSION, MINOR_VERSION] = codecept
|
|
23
|
+
const [, MAJOR_VERSION, MINOR_VERSION] = codecept
|
|
24
|
+
.version()
|
|
25
|
+
.match(/(\d+)\.(\d+)/)
|
|
26
|
+
.map(Number);
|
|
24
27
|
// Constants for hook execution order
|
|
25
28
|
const HOOK_EXECUTION_ORDER = {
|
|
26
29
|
PRE_TEST: ['BeforeSuiteHook', 'BeforeHook'],
|
|
27
|
-
POST_TEST: ['AfterHook', 'AfterSuiteHook']
|
|
30
|
+
POST_TEST: ['AfterHook', 'AfterSuiteHook'],
|
|
28
31
|
};
|
|
29
32
|
// codeceptjs workers are self-contained
|
|
30
33
|
data_storage_js_1.dataStorage.isFileStorage = false;
|
|
@@ -92,7 +95,7 @@ function CodeceptReporter(config) {
|
|
|
92
95
|
global.testomatioDataStore = {};
|
|
93
96
|
});
|
|
94
97
|
// Hook event listeners
|
|
95
|
-
event.dispatcher.on(event.hook.started,
|
|
98
|
+
event.dispatcher.on(event.hook.started, hook => {
|
|
96
99
|
output.stepShift = 2;
|
|
97
100
|
currentHook = hook.name;
|
|
98
101
|
let title = hook.hookName;
|
|
@@ -281,7 +284,7 @@ function captureHookStep(step, currentHook, hookSteps) {
|
|
|
281
284
|
status: step.status,
|
|
282
285
|
startTime,
|
|
283
286
|
endTime,
|
|
284
|
-
helperMethod: step.helperMethod
|
|
287
|
+
helperMethod: step.helperMethod,
|
|
285
288
|
});
|
|
286
289
|
hookSteps.set(currentHook, hookStepsArray);
|
|
287
290
|
}
|
|
@@ -369,7 +372,7 @@ function createSectionStep(metaStep) {
|
|
|
369
372
|
category: 'user',
|
|
370
373
|
title: metaStep.toString(), // Use built-in toString method
|
|
371
374
|
duration: metaStep.duration || 0, // Use built-in duration
|
|
372
|
-
steps: []
|
|
375
|
+
steps: [],
|
|
373
376
|
};
|
|
374
377
|
}
|
|
375
378
|
function createHookSection(hookName, steps) {
|
|
@@ -379,7 +382,7 @@ function createHookSection(hookName, steps) {
|
|
|
379
382
|
category: 'hook',
|
|
380
383
|
title: formatHookName(hookName),
|
|
381
384
|
duration: 0,
|
|
382
|
-
steps: []
|
|
385
|
+
steps: [],
|
|
383
386
|
};
|
|
384
387
|
for (const step of steps) {
|
|
385
388
|
const formattedStep = formatHookStep(step);
|
|
@@ -403,13 +406,13 @@ function formatCodeceptStep(step) {
|
|
|
403
406
|
const formattedStep = {
|
|
404
407
|
category,
|
|
405
408
|
title,
|
|
406
|
-
duration
|
|
409
|
+
duration,
|
|
407
410
|
};
|
|
408
411
|
// Add error if step failed
|
|
409
412
|
if (step.status === 'failed' && step.err) {
|
|
410
413
|
formattedStep.error = {
|
|
411
414
|
message: step.err.message || 'Step failed',
|
|
412
|
-
stack: step.err.stack || ''
|
|
415
|
+
stack: step.err.stack || '',
|
|
413
416
|
};
|
|
414
417
|
}
|
|
415
418
|
return formattedStep;
|
|
@@ -430,7 +433,7 @@ function formatHookStep(step) {
|
|
|
430
433
|
return {
|
|
431
434
|
category: 'hook',
|
|
432
435
|
title,
|
|
433
|
-
duration: step.duration || 0
|
|
436
|
+
duration: step.duration || 0,
|
|
434
437
|
};
|
|
435
438
|
}
|
|
436
439
|
module.exports = CodeceptReporter;
|
package/lib/bin/cli.js
CHANGED
|
@@ -37,12 +37,17 @@ program
|
|
|
37
37
|
program
|
|
38
38
|
.command('start')
|
|
39
39
|
.description('Start a new run and return its ID')
|
|
40
|
-
.
|
|
40
|
+
.option('--kind <type>', 'Specify run type: automated, manual, or mixed')
|
|
41
|
+
.action(async (opts) => {
|
|
41
42
|
(0, utils_js_1.cleanLatestRunId)();
|
|
42
43
|
console.log('Starting a new Run on Testomat.io...');
|
|
43
44
|
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config_js_1.config.TESTOMATIO;
|
|
44
45
|
const client = new client_js_1.default({ apiKey });
|
|
45
|
-
|
|
46
|
+
const createRunParams = {};
|
|
47
|
+
if (opts.kind) {
|
|
48
|
+
createRunParams.kind = opts.kind;
|
|
49
|
+
}
|
|
50
|
+
client.createRun(createRunParams).then(() => {
|
|
46
51
|
console.log(process.env.runId);
|
|
47
52
|
process.exit(0);
|
|
48
53
|
});
|
|
@@ -70,6 +75,8 @@ program
|
|
|
70
75
|
.description('Run tests with the specified command')
|
|
71
76
|
.argument('<command>', 'Test runner command')
|
|
72
77
|
.option('--filter <filter>', 'Additional execution filter')
|
|
78
|
+
.option('--filter-list <filter>', 'Get a list of all tests by filter before running')
|
|
79
|
+
.option('--kind <type>', 'Specify run type: automated, manual, or mixed')
|
|
73
80
|
.action(async (command, opts) => {
|
|
74
81
|
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config_js_1.config.TESTOMATIO;
|
|
75
82
|
const title = process.env.TESTOMATIO_TITLE;
|
|
@@ -78,17 +85,32 @@ program
|
|
|
78
85
|
return process.exit(255);
|
|
79
86
|
}
|
|
80
87
|
const client = new client_js_1.default({ apiKey, title });
|
|
81
|
-
if (opts.filter) {
|
|
82
|
-
|
|
88
|
+
if (opts.filter || opts.filterList) {
|
|
89
|
+
// Example of use: npx @testomatio/reporter run "npx jest" --filter "testomatio:tag-name=frontend"
|
|
90
|
+
// Example of use: npx @testomatio/reporter run "npx jest" --filter "coverage:file=coverage.yml"
|
|
91
|
+
// Example of use: npx @testomatio/reporter run "npx jest" --filter-list "coverage:file=coverage.yml"
|
|
92
|
+
const [pipe, ...optsArray] = opts?.filter ? opts?.filter.split(':') : opts?.filterList.split(':');
|
|
83
93
|
const pipeOptions = optsArray.join(':');
|
|
94
|
+
const prepareRunParams = { pipe, pipeOptions };
|
|
84
95
|
try {
|
|
85
|
-
const tests = await client.prepareRun(
|
|
86
|
-
if (tests
|
|
87
|
-
|
|
96
|
+
const tests = await client.prepareRun(prepareRunParams);
|
|
97
|
+
if (!tests || tests.length === 0) {
|
|
98
|
+
console.log(constants_js_1.APP_PREFIX, picocolors_1.default.yellow('No tests found.'));
|
|
99
|
+
return;
|
|
88
100
|
}
|
|
101
|
+
const pattern = `(${tests.join('|')})`;
|
|
102
|
+
const filteredCommand = (0, utils_js_1.applyFilter)(command, tests);
|
|
103
|
+
debug(`Execution pattern: "${pattern}"`);
|
|
104
|
+
if (opts.filterList) {
|
|
105
|
+
console.log(constants_js_1.APP_PREFIX, picocolors_1.default.blue(`Matched test/suite IDs: ${tests.join(', ')}`));
|
|
106
|
+
console.log(constants_js_1.APP_PREFIX, picocolors_1.default.green(`Full Running Command: ${filteredCommand}`));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
command = filteredCommand;
|
|
89
110
|
}
|
|
90
111
|
catch (err) {
|
|
91
|
-
console.log(constants_js_1.APP_PREFIX, err);
|
|
112
|
+
console.log(constants_js_1.APP_PREFIX, err.message || err);
|
|
113
|
+
return;
|
|
92
114
|
}
|
|
93
115
|
}
|
|
94
116
|
console.log(constants_js_1.APP_PREFIX, `π Running`, picocolors_1.default.green(command));
|
|
@@ -96,7 +118,7 @@ program
|
|
|
96
118
|
const testCmds = command.split(' ');
|
|
97
119
|
const cmd = (0, cross_spawn_1.spawn)(testCmds[0], testCmds.slice(1), {
|
|
98
120
|
stdio: 'inherit',
|
|
99
|
-
env: { ...process.env, TESTOMATIO_PROCEED: 'true', runId: client.runId },
|
|
121
|
+
env: { ...process.env, TESTOMATIO_PROCEED: 'true', runId: client.runId, TESTOMATIO_RUN: client.runId },
|
|
100
122
|
});
|
|
101
123
|
cmd.on('close', async (code) => {
|
|
102
124
|
const emoji = code === 0 ? 'π’' : 'π΄';
|
|
@@ -108,8 +130,15 @@ program
|
|
|
108
130
|
process.exit(code);
|
|
109
131
|
});
|
|
110
132
|
};
|
|
133
|
+
const createRunParams = {};
|
|
134
|
+
if (title) {
|
|
135
|
+
createRunParams.title = title;
|
|
136
|
+
}
|
|
137
|
+
if (opts.kind) {
|
|
138
|
+
createRunParams.kind = opts.kind;
|
|
139
|
+
}
|
|
111
140
|
if (apiKey) {
|
|
112
|
-
await client.createRun().then(runTests);
|
|
141
|
+
await client.createRun(createRunParams).then(runTests);
|
|
113
142
|
}
|
|
114
143
|
else {
|
|
115
144
|
await runTests();
|
|
@@ -145,7 +174,7 @@ program
|
|
|
145
174
|
.option('--lang <lang>', 'Language used (python, ruby, java)')
|
|
146
175
|
.option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
|
|
147
176
|
.action(async (pattern, opts) => {
|
|
148
|
-
if (!pattern.endsWith('.xml')) {
|
|
177
|
+
if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
|
|
149
178
|
pattern += '.xml';
|
|
150
179
|
}
|
|
151
180
|
let { javaTests, lang } = opts;
|
package/lib/bin/reportXml.js
CHANGED
|
@@ -25,7 +25,7 @@ program
|
|
|
25
25
|
.option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
|
|
26
26
|
.option('--env-file <envfile>', 'Load environment variables from env file')
|
|
27
27
|
.action(async (pattern, opts) => {
|
|
28
|
-
if (!pattern.endsWith('.xml')) {
|
|
28
|
+
if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
|
|
29
29
|
pattern += '.xml';
|
|
30
30
|
}
|
|
31
31
|
let { javaTests, lang } = opts;
|
|
@@ -37,7 +37,10 @@ program
|
|
|
37
37
|
lang = lang?.toLowerCase();
|
|
38
38
|
if (javaTests === true || (lang === 'java' && !javaTests))
|
|
39
39
|
javaTests = 'src/test/java';
|
|
40
|
-
const runReader = new xmlReader_js_1.default({
|
|
40
|
+
const runReader = new xmlReader_js_1.default({
|
|
41
|
+
javaTests,
|
|
42
|
+
lang,
|
|
43
|
+
});
|
|
41
44
|
const files = glob_1.glob.sync(pattern, { cwd: opts.dir || process.cwd() });
|
|
42
45
|
if (!files.length) {
|
|
43
46
|
console.log(constants_js_1.APP_PREFIX, `Report can't be created. No XML files found π₯`);
|
package/lib/client.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export class Client {
|
|
|
44
44
|
*
|
|
45
45
|
* @returns {Promise<any>} - resolves to Run id which should be used to update / add test
|
|
46
46
|
*/
|
|
47
|
-
createRun(params
|
|
47
|
+
createRun(params?: {}): Promise<any>;
|
|
48
48
|
/**
|
|
49
49
|
* Updates test status and its data
|
|
50
50
|
*
|
|
@@ -61,15 +61,5 @@ export class Client {
|
|
|
61
61
|
* @returns {Promise<any>} - A Promise that resolves when finishes the run.
|
|
62
62
|
*/
|
|
63
63
|
updateRunStatus(status: "passed" | "failed" | "skipped" | "finished"): Promise<any>;
|
|
64
|
-
/**
|
|
65
|
-
* Returns the formatted stack including the stack trace, steps, and logs.
|
|
66
|
-
* @returns {string}
|
|
67
|
-
*/
|
|
68
|
-
formatLogs({ error, steps, logs }: {
|
|
69
|
-
error: any;
|
|
70
|
-
steps: any;
|
|
71
|
-
logs: any;
|
|
72
|
-
}): string;
|
|
73
|
-
formatError(error: any, message: any): string;
|
|
74
64
|
}
|
|
75
65
|
import { S3Uploader } from './uploader.js';
|
package/lib/client.js
CHANGED
|
@@ -1,56 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
4
|
};
|
|
38
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
6
|
exports.Client = void 0;
|
|
40
7
|
const debug_1 = __importDefault(require("debug"));
|
|
41
|
-
const callsite_record_1 = __importDefault(require("callsite-record"));
|
|
42
|
-
const minimatch_1 = require("minimatch");
|
|
43
8
|
const fs_1 = __importDefault(require("fs"));
|
|
44
9
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
45
|
-
const crypto_1 = require("crypto");
|
|
46
10
|
const constants_js_1 = require("./constants.js");
|
|
47
11
|
const index_js_1 = require("./pipe/index.js");
|
|
48
12
|
const glob_1 = require("glob");
|
|
49
|
-
const path_1 =
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
50
14
|
const node_url_1 = require("node:url");
|
|
51
15
|
const uploader_js_1 = require("./uploader.js");
|
|
52
16
|
const utils_js_1 = require("./utils/utils.js");
|
|
53
17
|
const filesize_1 = require("filesize");
|
|
18
|
+
const log_formatter_js_1 = require("./utils/log-formatter.js");
|
|
54
19
|
const debug = (0, debug_1.default)('@testomatio/reporter:client');
|
|
55
20
|
// removed __dirname usage, because:
|
|
56
21
|
// 1. replaced with ESM syntax (import.meta.url), but it throws an error on tsc compilation;
|
|
@@ -99,24 +64,32 @@ class Client {
|
|
|
99
64
|
* or resolves to undefined if no valid results are found or if all pipes are disabled.
|
|
100
65
|
*/
|
|
101
66
|
async prepareRun(params) {
|
|
102
|
-
this.pipes = await (0, index_js_1.pipesFactory)(params || this.paramsForPipesFactory || {}, this.pipeStore);
|
|
103
67
|
const { pipe, pipeOptions } = params;
|
|
68
|
+
// β Validation: pipe is required
|
|
69
|
+
if (!pipe || !pipeOptions) {
|
|
70
|
+
console.warn(`β No valid pipe found in filter cmd. Expected format: <pipe>:<options>
|
|
71
|
+
Examples:
|
|
72
|
+
--filter "testomatio:tag-name=frontend"
|
|
73
|
+
--filter "coverage:file=coverage.yml"
|
|
74
|
+
--filter-list "coverage:file=coverage.yml"
|
|
75
|
+
Received: "${params}"`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
this.pipes = await (0, index_js_1.pipesFactory)(params || this.paramsForPipesFactory || {}, this.pipeStore);
|
|
104
79
|
// all pipes disabled, skipping
|
|
105
80
|
if (!this.pipes.some(p => p.isEnabled)) {
|
|
106
81
|
return Promise.resolve();
|
|
107
82
|
}
|
|
108
83
|
try {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
console.warn(constants_js_1.APP_PREFIX,
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
const results = await Promise.all(this.pipes.map(async (p) => ({ pipe: p.toString(), result: await p.prepareRun(pipeOptions) })));
|
|
116
|
-
const result = results.filter(p => p.pipe.includes('Testomatio'))[0]?.result;
|
|
117
|
-
if (!result || result.length === 0) {
|
|
84
|
+
const p = this.pipes.find(p => p.constructor.name.toLowerCase() === `${pipe.toLowerCase()}pipe`);
|
|
85
|
+
// const p = this.pipes.find(p => p.id === `${pipe.toLowerCase()}`); TODO: as future updates
|
|
86
|
+
if (!p?.isEnabled) {
|
|
87
|
+
console.warn(constants_js_1.APP_PREFIX, "π« No active pipes were found in the system. Execution aborted!");
|
|
118
88
|
return;
|
|
119
89
|
}
|
|
90
|
+
// Run only the selected pipe
|
|
91
|
+
const rawResult = await p.prepareRun(pipeOptions);
|
|
92
|
+
const result = Array.isArray(rawResult) ? rawResult : [];
|
|
120
93
|
debug('Execution tests list', result);
|
|
121
94
|
return result;
|
|
122
95
|
}
|
|
@@ -129,7 +102,7 @@ class Client {
|
|
|
129
102
|
*
|
|
130
103
|
* @returns {Promise<any>} - resolves to Run id which should be used to update / add test
|
|
131
104
|
*/
|
|
132
|
-
async createRun(params) {
|
|
105
|
+
async createRun(params = {}) {
|
|
133
106
|
if (!this.pipes || !this.pipes.length)
|
|
134
107
|
this.pipes = await (0, index_js_1.pipesFactory)(params || this.paramsForPipesFactory || {}, this.pipeStore);
|
|
135
108
|
debug('Creating run...');
|
|
@@ -137,7 +110,7 @@ class Client {
|
|
|
137
110
|
if (!this.pipes?.filter(p => p.isEnabled).length)
|
|
138
111
|
return Promise.resolve();
|
|
139
112
|
this.queue = this.queue
|
|
140
|
-
.then(() => Promise.all(this.pipes.map(p => p.createRun())))
|
|
113
|
+
.then(() => Promise.all(this.pipes.map(p => p.createRun(params))))
|
|
141
114
|
.catch(err => console.log(constants_js_1.APP_PREFIX, err))
|
|
142
115
|
.then(() => {
|
|
143
116
|
const runId = this.pipeStore?.runId;
|
|
@@ -158,17 +131,6 @@ class Client {
|
|
|
158
131
|
* @returns {Promise<PipeResult[]>}
|
|
159
132
|
*/
|
|
160
133
|
async addTestRun(status, testData) {
|
|
161
|
-
if (!this.pipes || !this.pipes.length)
|
|
162
|
-
this.pipes = await (0, index_js_1.pipesFactory)(this.paramsForPipesFactory || {}, this.pipeStore);
|
|
163
|
-
// all pipes disabled, skipping
|
|
164
|
-
if (!this.pipes?.filter(p => p.isEnabled).length)
|
|
165
|
-
return [];
|
|
166
|
-
if (isTestShouldBeExculedFromReport(testData))
|
|
167
|
-
return [];
|
|
168
|
-
if (status === constants_js_1.STATUS.SKIPPED && process.env.TESTOMATIO_EXCLUDE_SKIPPED) {
|
|
169
|
-
debug('Skipping test from report', testData?.title);
|
|
170
|
-
return []; // do not log skipped tests
|
|
171
|
-
}
|
|
172
134
|
if (!testData)
|
|
173
135
|
testData = {
|
|
174
136
|
title: 'Unknown test',
|
|
@@ -181,9 +143,12 @@ class Client {
|
|
|
181
143
|
/**
|
|
182
144
|
* @type {TestData}
|
|
183
145
|
*/
|
|
184
|
-
const { rid, error = null,
|
|
146
|
+
const { rid, error = null, steps: originalSteps, title, suite_title } = testData;
|
|
147
|
+
let steps = originalSteps;
|
|
148
|
+
const uploadedFiles = [];
|
|
149
|
+
const stackArtifactsEnabled = (0, utils_js_1.transformEnvVarToBoolean)(process.env.TESTOMATIO_STACK_ARTIFACTS);
|
|
150
|
+
const { time = 0, example = null, files = [], filesBuffers = [], code = null, file, suite_id, test_id, timestamp, links, manuallyAttachedArtifacts, overwrite, tags, } = testData;
|
|
185
151
|
let { message = '', meta = {} } = testData;
|
|
186
|
-
// stringify meta values and limit keys and values length to 255
|
|
187
152
|
meta = Object.entries(meta)
|
|
188
153
|
.filter(([, value]) => value !== null && value !== undefined)
|
|
189
154
|
.reduce((acc, [key, value]) => {
|
|
@@ -191,19 +156,38 @@ class Client {
|
|
|
191
156
|
acc[key] = value;
|
|
192
157
|
return acc;
|
|
193
158
|
}, {});
|
|
194
|
-
// Get links from storage using the test context
|
|
195
159
|
const testContext = suite_title ? `${suite_title} ${title}` : title;
|
|
196
160
|
let errorFormatted = '';
|
|
197
161
|
if (error) {
|
|
198
|
-
errorFormatted +=
|
|
162
|
+
errorFormatted += (0, log_formatter_js_1.formatError)(error) || '';
|
|
199
163
|
message = error?.message;
|
|
200
164
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
165
|
+
let fullLogs = (0, log_formatter_js_1.formatLogs)({ error: errorFormatted, steps, logs: testData.logs });
|
|
166
|
+
if (stackArtifactsEnabled && fullLogs?.trim()?.length > 0) {
|
|
167
|
+
uploadedFiles.push(this.uploader.uploadFileAsBuffer(Buffer.from((0, log_formatter_js_1.stripColors)(fullLogs), 'utf8'), [
|
|
168
|
+
this.runId,
|
|
169
|
+
rid,
|
|
170
|
+
`logs_${+new Date()}.log`,
|
|
171
|
+
]));
|
|
172
|
+
fullLogs = '';
|
|
173
|
+
steps = null;
|
|
174
|
+
}
|
|
175
|
+
if (!this.pipes || !this.pipes.length)
|
|
176
|
+
this.pipes = await (0, index_js_1.pipesFactory)(this.paramsForPipesFactory || {}, this.pipeStore);
|
|
177
|
+
if (!this.pipes?.filter(p => p.isEnabled).length) {
|
|
178
|
+
if (uploadedFiles.length > 0) {
|
|
179
|
+
await Promise.all(uploadedFiles);
|
|
180
|
+
}
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
if (isTestShouldBeExcludedFromReport(testData))
|
|
184
|
+
return [];
|
|
185
|
+
if (status === constants_js_1.STATUS.SKIPPED && process.env.TESTOMATIO_EXCLUDE_SKIPPED) {
|
|
186
|
+
debug('Skipping test from report', testData?.title);
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
204
189
|
if (manuallyAttachedArtifacts?.length)
|
|
205
190
|
files.push(...manuallyAttachedArtifacts);
|
|
206
|
-
const uploadedFiles = [];
|
|
207
191
|
for (let f of files) {
|
|
208
192
|
if (!f)
|
|
209
193
|
continue; // f === null
|
|
@@ -285,7 +269,7 @@ class Client {
|
|
|
285
269
|
const uploadedArtifacts = this.uploader.successfulUploads.map(file => ({
|
|
286
270
|
relativePath: file.path.replace(process.cwd(), ''),
|
|
287
271
|
link: file.link,
|
|
288
|
-
sizePretty: (0, filesize_1.filesize)(file.size, { round: 0 }).toString(),
|
|
272
|
+
sizePretty: file.size == null ? 'unknown' : (0, filesize_1.filesize)(file.size, { round: 0 }).toString(),
|
|
289
273
|
}));
|
|
290
274
|
uploadedArtifacts.forEach(upload => {
|
|
291
275
|
debug(`π’Uploaded artifact`, `${upload.relativePath},`, 'size:', `${upload.sizePretty},`, 'link:', `${upload.link}`);
|
|
@@ -295,7 +279,7 @@ class Client {
|
|
|
295
279
|
console.log(constants_js_1.APP_PREFIX, `ποΈ ${this.uploader.failedUploads.length} artifacts π΄${picocolors_1.default.bold('failed')} to upload`);
|
|
296
280
|
const failedUploads = this.uploader.failedUploads.map(file => ({
|
|
297
281
|
relativePath: file.path.replace(process.cwd(), ''),
|
|
298
|
-
sizePretty: (0, filesize_1.filesize)(file.size, { round: 0 }).toString(),
|
|
282
|
+
sizePretty: file.size == null ? 'unknown' : (0, filesize_1.filesize)(file.size, { round: 0 }).toString(),
|
|
299
283
|
}));
|
|
300
284
|
const pathPadding = Math.max(...failedUploads.map(upload => upload.relativePath.length)) + 1;
|
|
301
285
|
failedUploads.forEach(upload => {
|
|
@@ -323,93 +307,14 @@ class Client {
|
|
|
323
307
|
.catch(err => console.log(constants_js_1.APP_PREFIX, err));
|
|
324
308
|
return this.queue;
|
|
325
309
|
}
|
|
326
|
-
/**
|
|
327
|
-
* Returns the formatted stack including the stack trace, steps, and logs.
|
|
328
|
-
* @returns {string}
|
|
329
|
-
*/
|
|
330
|
-
formatLogs({ error, steps, logs }) {
|
|
331
|
-
error = error?.trim();
|
|
332
|
-
logs = logs?.trim().split('\n').map(l => (0, utils_js_1.truncate)(l)).join('\n');
|
|
333
|
-
if (Array.isArray(steps)) {
|
|
334
|
-
steps = steps
|
|
335
|
-
.map(step => (0, utils_js_1.formatStep)(step))
|
|
336
|
-
.flat()
|
|
337
|
-
.join('\n');
|
|
338
|
-
}
|
|
339
|
-
let testLogs = '';
|
|
340
|
-
if (steps)
|
|
341
|
-
testLogs += `${picocolors_1.default.bold(picocolors_1.default.blue('################[ Steps ]################'))}\n${steps}\n\n`;
|
|
342
|
-
if (logs)
|
|
343
|
-
testLogs += `${picocolors_1.default.bold(picocolors_1.default.gray('################[ Logs ]################'))}\n${logs}\n\n`;
|
|
344
|
-
if (error)
|
|
345
|
-
testLogs += `${picocolors_1.default.bold(picocolors_1.default.red('################[ Failure ]################'))}\n${error}`;
|
|
346
|
-
return testLogs;
|
|
347
|
-
}
|
|
348
|
-
formatError(error, message) {
|
|
349
|
-
if (!message)
|
|
350
|
-
message = error.message;
|
|
351
|
-
if (error.inspect)
|
|
352
|
-
message = error.inspect() || '';
|
|
353
|
-
let stack = '';
|
|
354
|
-
if (error.name)
|
|
355
|
-
stack += `${picocolors_1.default.red(error.name)}`;
|
|
356
|
-
if (error.operator)
|
|
357
|
-
stack += ` (${picocolors_1.default.red(error.operator)})`;
|
|
358
|
-
// add new line if something was added to stack
|
|
359
|
-
if (stack)
|
|
360
|
-
stack += ': ';
|
|
361
|
-
stack += `${message}\n`;
|
|
362
|
-
if (error.diff) {
|
|
363
|
-
// diff for vitest
|
|
364
|
-
stack += error.diff;
|
|
365
|
-
stack += '\n\n';
|
|
366
|
-
}
|
|
367
|
-
else if (error.actual && error.expected && error.actual !== error.expected) {
|
|
368
|
-
// diffs for mocha, cypress, codeceptjs style
|
|
369
|
-
stack += `\n\n${picocolors_1.default.bold(picocolors_1.default.green('+ expected'))} ${picocolors_1.default.bold(picocolors_1.default.red('- actual'))}`;
|
|
370
|
-
stack += `\n${picocolors_1.default.green(`+ ${error.expected.toString().split('\n').join('\n+ ')}`)}`;
|
|
371
|
-
stack += `\n${picocolors_1.default.red(`- ${error.actual.toString().split('\n').join('\n- ')}`)}`;
|
|
372
|
-
stack += '\n\n';
|
|
373
|
-
}
|
|
374
|
-
const customFilter = process.env.TESTOMATIO_STACK_IGNORE;
|
|
375
|
-
try {
|
|
376
|
-
let hasFrame = false;
|
|
377
|
-
const record = (0, callsite_record_1.default)({
|
|
378
|
-
forError: error,
|
|
379
|
-
isCallsiteFrame: frame => {
|
|
380
|
-
if (customFilter && (0, minimatch_1.minimatch)(frame.fileName, customFilter))
|
|
381
|
-
return false;
|
|
382
|
-
if (hasFrame)
|
|
383
|
-
return false;
|
|
384
|
-
if (isNotInternalFrame(frame))
|
|
385
|
-
hasFrame = true;
|
|
386
|
-
return hasFrame;
|
|
387
|
-
},
|
|
388
|
-
});
|
|
389
|
-
// @ts-ignore
|
|
390
|
-
if (record && !record.filename.startsWith('http')) {
|
|
391
|
-
stack += record.renderSync({ stackFilter: isNotInternalFrame });
|
|
392
|
-
}
|
|
393
|
-
return stack;
|
|
394
|
-
}
|
|
395
|
-
catch (e) {
|
|
396
|
-
console.log(e);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
310
|
}
|
|
400
311
|
exports.Client = Client;
|
|
401
|
-
function isNotInternalFrame(frame) {
|
|
402
|
-
return (frame.getFileName() &&
|
|
403
|
-
frame.getFileName().includes(path_1.sep) &&
|
|
404
|
-
!frame.getFileName().includes('node_modules') &&
|
|
405
|
-
!frame.getFileName().includes('internal'));
|
|
406
|
-
}
|
|
407
312
|
/**
|
|
408
313
|
*
|
|
409
314
|
* @param {TestData} testData
|
|
410
315
|
* @returns boolean
|
|
411
316
|
*/
|
|
412
|
-
function
|
|
317
|
+
function isTestShouldBeExcludedFromReport(testData) {
|
|
413
318
|
// const fileName = path.basename(test.location?.file || '');
|
|
414
319
|
const globExcludeFilesPattern = process.env.TESTOMATIO_EXCLUDE_FILES_FROM_REPORT_GLOB_PATTERN;
|
|
415
320
|
if (!globExcludeFilesPattern)
|
|
@@ -418,11 +323,11 @@ function isTestShouldBeExculedFromReport(testData) {
|
|
|
418
323
|
debug('No "file" property found for test ', testData.title);
|
|
419
324
|
return false;
|
|
420
325
|
}
|
|
421
|
-
const
|
|
326
|
+
const excludePatternsList = globExcludeFilesPattern.split(';');
|
|
422
327
|
// as scanning files is time consuming operation, just save the result in variable to avoid multiple scans
|
|
423
328
|
if (!listOfTestFilesToExcludeFromReport) {
|
|
424
329
|
// list of files with relative paths
|
|
425
|
-
listOfTestFilesToExcludeFromReport = glob_1.glob.sync(
|
|
330
|
+
listOfTestFilesToExcludeFromReport = glob_1.glob.sync(excludePatternsList, { ignore: '**/node_modules/**' });
|
|
426
331
|
debug('Tests from next files will not be reported:', listOfTestFilesToExcludeFromReport);
|
|
427
332
|
}
|
|
428
333
|
const testFileRelativePath = path_1.default.relative(process.cwd(), testData.file);
|
package/lib/data-storage.d.ts
CHANGED
package/lib/helpers.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const isPlaywright: boolean;
|
package/lib/helpers.js
ADDED
|
@@ -7,24 +7,60 @@ const path_1 = __importDefault(require("path"));
|
|
|
7
7
|
const adapter_js_1 = __importDefault(require("./adapter.js"));
|
|
8
8
|
class CSharpAdapter extends adapter_js_1.default {
|
|
9
9
|
formatTest(t) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
// Extract example from title if not already present
|
|
11
|
+
if (!t.example) {
|
|
12
|
+
const exampleMatch = t.title.match(/\((.*?)\)/);
|
|
13
|
+
if (exampleMatch) {
|
|
14
|
+
// Extract parameters as object with numeric keys for API
|
|
15
|
+
const params = exampleMatch[1]
|
|
16
|
+
.split(',')
|
|
17
|
+
.map(param => param.trim())
|
|
18
|
+
.filter(param => param !== '');
|
|
19
|
+
t.example = {};
|
|
20
|
+
params.forEach((param, index) => {
|
|
21
|
+
t.example[index] = param;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Remove parameters from title to avoid duplicates in Test Suite
|
|
26
|
+
// The example field will be used for grouping on import
|
|
27
|
+
t.title = t.title.replace(/\(.*?\)/, '').trim();
|
|
14
28
|
const suite = t.suite_title.split('.');
|
|
15
29
|
t.suite_title = suite.pop();
|
|
16
30
|
t.file = namespaceToFileName(t.file);
|
|
17
|
-
t.title = title.trim();
|
|
18
31
|
return t;
|
|
19
32
|
}
|
|
20
33
|
getFilePath(t) {
|
|
21
|
-
|
|
34
|
+
if (!t.file)
|
|
35
|
+
return null;
|
|
36
|
+
// Normalize path separators for cross-platform compatibility
|
|
37
|
+
let filePath = t.file.replace(/\\/g, '/');
|
|
38
|
+
// If file already has .cs extension, use it directly
|
|
39
|
+
if (filePath.endsWith('.cs')) {
|
|
40
|
+
// Make relative path if it's absolute
|
|
41
|
+
if (path_1.default.isAbsolute(filePath)) {
|
|
42
|
+
// Try to find project-relative path
|
|
43
|
+
const cwd = process.cwd().replace(/\\/g, '/');
|
|
44
|
+
if (filePath.startsWith(cwd)) {
|
|
45
|
+
filePath = path_1.default.relative(cwd, filePath).replace(/\\/g, '/');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return filePath;
|
|
49
|
+
}
|
|
50
|
+
// Convert namespace path to file path
|
|
51
|
+
const fileName = namespaceToFileName(filePath);
|
|
22
52
|
return fileName;
|
|
23
53
|
}
|
|
24
54
|
}
|
|
25
55
|
module.exports = CSharpAdapter;
|
|
26
56
|
function namespaceToFileName(fileName) {
|
|
57
|
+
if (!fileName)
|
|
58
|
+
return '';
|
|
59
|
+
// If already a .cs file path, clean it up
|
|
60
|
+
if (fileName.endsWith('.cs')) {
|
|
61
|
+
return fileName.replace(/\\/g, '/');
|
|
62
|
+
}
|
|
27
63
|
const fileParts = fileName.split('.');
|
|
28
64
|
fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
|
|
29
|
-
return `${fileParts.join(
|
|
65
|
+
return `${fileParts.join('/')}.cs`;
|
|
30
66
|
}
|