@testomatio/reporter 2.0.1-beta.1 → 2.0.1-beta.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/lib/adapter/webdriver.js +1 -0
- package/lib/bin/cli.js +25 -78
- package/lib/pipe/debug.js +1 -1
- package/lib/pipe/testomatio.d.ts +1 -1
- package/lib/pipe/testomatio.js +6 -2
- package/lib/replay.d.ts +31 -0
- package/lib/replay.js +237 -0
- package/lib/uploader.js +8 -4
- package/lib/utils/utils.d.ts +1 -1
- package/lib/utils/utils.js +13 -3
- package/package.json +1 -1
- package/src/adapter/webdriver.js +1 -0
- package/src/bin/cli.js +25 -86
- package/src/pipe/debug.js +1 -1
- package/src/pipe/testomatio.js +8 -2
- package/src/replay.js +245 -0
- package/src/uploader.js +9 -4
- package/src/utils/utils.js +12 -3
package/lib/adapter/webdriver.js
CHANGED
|
@@ -101,6 +101,7 @@ class WebdriverReporter extends reporter_1.default {
|
|
|
101
101
|
.filter(el => el.endpoint === screenshotEndpoint && el.result && el.result.value)
|
|
102
102
|
.map(el => Buffer.from(el.result.value, 'base64'));
|
|
103
103
|
await this.client.addTestRun(state, {
|
|
104
|
+
rid: test.uid || '',
|
|
104
105
|
manuallyAttachedArtifacts: test.artifacts,
|
|
105
106
|
error,
|
|
106
107
|
logs: test.logs,
|
package/lib/bin/cli.js
CHANGED
|
@@ -17,9 +17,7 @@ const utils_js_2 = require("../utils/utils.js");
|
|
|
17
17
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
18
18
|
const filesize_1 = require("filesize");
|
|
19
19
|
const dotenv_1 = __importDefault(require("dotenv"));
|
|
20
|
-
const
|
|
21
|
-
const path_1 = __importDefault(require("path"));
|
|
22
|
-
const os_1 = __importDefault(require("os"));
|
|
20
|
+
const replay_js_1 = __importDefault(require("../replay.js"));
|
|
23
21
|
const debug = (0, debug_1.default)('@testomatio/reporter:xml-cli');
|
|
24
22
|
const version = (0, utils_js_1.getPackageVersion)();
|
|
25
23
|
console.log(picocolors_1.default.cyan(picocolors_1.default.bold(` 🤩 Testomat.io Reporter v${version}`)));
|
|
@@ -251,92 +249,41 @@ program
|
|
|
251
249
|
.command('replay')
|
|
252
250
|
.description('Replay test data from debug file and re-send to Testomat.io')
|
|
253
251
|
.argument('[debug-file]', 'Path to debug file (defaults to /tmp/testomatio.debug.latest.json)')
|
|
252
|
+
.option('--dry-run', 'Preview the data without sending to Testomat.io')
|
|
254
253
|
.action(async (debugFile, opts) => {
|
|
255
|
-
// Use default debug file if none provided
|
|
256
|
-
if (!debugFile) {
|
|
257
|
-
debugFile = path_1.default.join(os_1.default.tmpdir(), 'testomatio.debug.latest.json');
|
|
258
|
-
}
|
|
259
|
-
if (!fs_1.default.existsSync(debugFile)) {
|
|
260
|
-
console.log(constants_js_1.APP_PREFIX, `❌ Debug file not found: ${debugFile}`);
|
|
261
|
-
return process.exit(1);
|
|
262
|
-
}
|
|
263
|
-
console.log(constants_js_1.APP_PREFIX, `🪲 Replaying data from debug file: ${debugFile}`);
|
|
264
254
|
try {
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
console.log(constants_js_1.APP_PREFIX,
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
let parseErrors = 0;
|
|
274
|
-
const allTests = [];
|
|
275
|
-
// Parse debug file line by line
|
|
276
|
-
for (const [lineIndex, line] of lines.entries()) {
|
|
277
|
-
try {
|
|
278
|
-
const logEntry = JSON.parse(line);
|
|
279
|
-
if (logEntry.data === 'variables' && logEntry.testomatioEnvVars) {
|
|
280
|
-
Object.assign(process.env, logEntry.testomatioEnvVars, process.env);
|
|
281
|
-
}
|
|
282
|
-
else if (logEntry.action === 'createRun') {
|
|
283
|
-
runParams = logEntry.params || {};
|
|
284
|
-
}
|
|
285
|
-
else if (logEntry.action === 'addTestsBatch' && logEntry.tests) {
|
|
286
|
-
allTests.push(...logEntry.tests);
|
|
287
|
-
}
|
|
288
|
-
else if (logEntry.action === 'addTest' && logEntry.testId) {
|
|
289
|
-
allTests.push(logEntry.testId);
|
|
290
|
-
}
|
|
291
|
-
else if (logEntry.actions === 'finishRun') {
|
|
292
|
-
finishParams = logEntry.params || {};
|
|
255
|
+
const replayService = new replay_js_1.default({
|
|
256
|
+
apiKey: config_js_1.config.TESTOMATIO,
|
|
257
|
+
dryRun: opts.dryRun,
|
|
258
|
+
onLog: (message) => console.log(constants_js_1.APP_PREFIX, message),
|
|
259
|
+
onError: (message) => console.error(constants_js_1.APP_PREFIX, '⚠️ ', message),
|
|
260
|
+
onProgress: ({ current, total }) => {
|
|
261
|
+
if (current % 10 === 0 || current === total) {
|
|
262
|
+
console.log(constants_js_1.APP_PREFIX, `📊 Progress: ${current}/${total} tests processed`);
|
|
293
263
|
}
|
|
294
264
|
}
|
|
295
|
-
catch (err) {
|
|
296
|
-
parseErrors++;
|
|
297
|
-
if (parseErrors <= 3) {
|
|
298
|
-
// Only show first 3 parse errors
|
|
299
|
-
console.warn(constants_js_1.APP_PREFIX, `⚠️ Failed to parse line ${lineIndex + 1}: ${line.substring(0, 100)}...`);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
if (parseErrors > 3) {
|
|
304
|
-
console.warn(constants_js_1.APP_PREFIX, `⚠️ ${parseErrors - 3} more parse errors occurred`);
|
|
305
|
-
}
|
|
306
|
-
console.log(constants_js_1.APP_PREFIX, `📊 Found ${allTests.length} tests to replay`);
|
|
307
|
-
if (allTests.length === 0) {
|
|
308
|
-
console.log(constants_js_1.APP_PREFIX, '❌ No test data found in debug file');
|
|
309
|
-
return process.exit(1);
|
|
310
|
-
}
|
|
311
|
-
const apiKey = config_js_1.config.TESTOMATIO;
|
|
312
|
-
if (!apiKey) {
|
|
313
|
-
console.log(constants_js_1.APP_PREFIX, '❌ TESTOMATIO API key not found. Set TESTOMATIO environment variable.');
|
|
314
|
-
return process.exit(1);
|
|
315
|
-
}
|
|
316
|
-
// Create client and restore the run
|
|
317
|
-
const client = new client_js_1.default({
|
|
318
|
-
apiKey,
|
|
319
|
-
isBatchEnabled: true,
|
|
320
|
-
...runParams,
|
|
321
265
|
});
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
266
|
+
const result = await replayService.replay(debugFile);
|
|
267
|
+
if (result.dryRun) {
|
|
268
|
+
console.log(constants_js_1.APP_PREFIX, '🔍 Dry run completed:');
|
|
269
|
+
console.log(constants_js_1.APP_PREFIX, ` - Tests found: ${result.testsCount}`);
|
|
270
|
+
console.log(constants_js_1.APP_PREFIX, ` - Environment variables: ${Object.keys(result.envVars).length}`);
|
|
271
|
+
console.log(constants_js_1.APP_PREFIX, ` - Run parameters:`, result.runParams);
|
|
272
|
+
console.log(constants_js_1.APP_PREFIX, ' Use without --dry-run to actually send the data');
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
console.log(constants_js_1.APP_PREFIX, `✅ Successfully replayed ${result.successCount}/${result.testsCount} tests`);
|
|
276
|
+
if (result.failureCount > 0) {
|
|
277
|
+
console.log(constants_js_1.APP_PREFIX, `⚠️ ${result.failureCount} tests failed to upload`);
|
|
331
278
|
}
|
|
332
279
|
}
|
|
333
|
-
await client.updateRunStatus(finishParams.status || constants_js_1.STATUS.FINISHED, finishParams.parallel || false);
|
|
334
|
-
console.log(constants_js_1.APP_PREFIX, `✅ Successfully replayed ${allTests.length} tests from debug file`);
|
|
335
280
|
process.exit(0);
|
|
336
281
|
}
|
|
337
282
|
catch (err) {
|
|
338
283
|
console.error(constants_js_1.APP_PREFIX, '❌ Error replaying debug data:', err.message);
|
|
339
|
-
|
|
284
|
+
if (err.message.includes('Debug file not found')) {
|
|
285
|
+
console.error(constants_js_1.APP_PREFIX, '💡 Hint: Run tests with TESTOMATIO_DEBUG=1 to generate debug files');
|
|
286
|
+
}
|
|
340
287
|
process.exit(1);
|
|
341
288
|
}
|
|
342
289
|
});
|
package/lib/pipe/debug.js
CHANGED
|
@@ -110,7 +110,7 @@ class DebugPipe {
|
|
|
110
110
|
await this.batchUpload();
|
|
111
111
|
if (this.batch.intervalFunction)
|
|
112
112
|
clearInterval(this.batch.intervalFunction);
|
|
113
|
-
this.logToFile({
|
|
113
|
+
this.logToFile({ action: 'finishRun', params });
|
|
114
114
|
console.log(constants_js_1.APP_PREFIX, '🪲 Debug Saved to', this.logFilePath);
|
|
115
115
|
}
|
|
116
116
|
toString() {
|
package/lib/pipe/testomatio.d.ts
CHANGED
|
@@ -59,7 +59,7 @@ declare class TestomatioPipe implements Pipe {
|
|
|
59
59
|
/**
|
|
60
60
|
* Adds a test to the batch uploader (or reports a single test if batch uploading is disabled)
|
|
61
61
|
*/
|
|
62
|
-
addTest(data: any): void
|
|
62
|
+
addTest(data: any): Promise<void | import("gaxios").GaxiosResponse<any>>;
|
|
63
63
|
/**
|
|
64
64
|
* @param {import('../../types/types.js').RunData} params
|
|
65
65
|
* @returns
|
package/lib/pipe/testomatio.js
CHANGED
|
@@ -331,6 +331,7 @@ class TestomatioPipe {
|
|
|
331
331
|
* Adds a test to the batch uploader (or reports a single test if batch uploading is disabled)
|
|
332
332
|
*/
|
|
333
333
|
addTest(data) {
|
|
334
|
+
this.isEnabled = this.apiKey ?? this.isEnabled;
|
|
334
335
|
if (!this.isEnabled)
|
|
335
336
|
return;
|
|
336
337
|
if (!this.runId)
|
|
@@ -340,13 +341,16 @@ class TestomatioPipe {
|
|
|
340
341
|
data.rid = `${this.runId}-${data.rid}`;
|
|
341
342
|
data.api_key = this.apiKey;
|
|
342
343
|
data.create = this.createNewTests;
|
|
344
|
+
let uploading = null;
|
|
343
345
|
if (!this.batch.isEnabled)
|
|
344
|
-
this.#uploadSingleTest(data);
|
|
346
|
+
uploading = this.#uploadSingleTest(data);
|
|
345
347
|
else
|
|
346
348
|
this.batch.tests.push(data);
|
|
347
349
|
// if test is added after run which is already finished
|
|
348
350
|
if (!this.batch.intervalFunction)
|
|
349
|
-
this.#batchUpload();
|
|
351
|
+
uploading = this.#batchUpload();
|
|
352
|
+
// return promise to be able to wait for it
|
|
353
|
+
return uploading;
|
|
350
354
|
}
|
|
351
355
|
/**
|
|
352
356
|
* @param {import('../../types/types.js').RunData} params
|
package/lib/replay.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export class Replay {
|
|
2
|
+
constructor(options?: {});
|
|
3
|
+
apiKey: any;
|
|
4
|
+
dryRun: any;
|
|
5
|
+
onProgress: any;
|
|
6
|
+
onLog: any;
|
|
7
|
+
onError: any;
|
|
8
|
+
/**
|
|
9
|
+
* Get the default debug file path
|
|
10
|
+
* @returns {string} Path to the latest debug file
|
|
11
|
+
*/
|
|
12
|
+
getDefaultDebugFile(): string;
|
|
13
|
+
/**
|
|
14
|
+
* Parse a debug file and extract test data
|
|
15
|
+
* @param {string} debugFile - Path to the debug file
|
|
16
|
+
* @returns {Object} Parsed debug data
|
|
17
|
+
*/
|
|
18
|
+
parseDebugFile(debugFile: string): any;
|
|
19
|
+
/**
|
|
20
|
+
* Restore environment variables from debug data
|
|
21
|
+
* @param {Object} envVars - Environment variables to restore
|
|
22
|
+
*/
|
|
23
|
+
restoreEnvironmentVariables(envVars: any): void;
|
|
24
|
+
/**
|
|
25
|
+
* Replay test data to Testomat.io
|
|
26
|
+
* @param {string} debugFile - Path to debug file (optional, uses default if not provided)
|
|
27
|
+
* @returns {Promise<Object>} Replay results
|
|
28
|
+
*/
|
|
29
|
+
replay(debugFile: string): Promise<any>;
|
|
30
|
+
}
|
|
31
|
+
export default Replay;
|
package/lib/replay.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Replay = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
const client_js_1 = __importDefault(require("./client.js"));
|
|
11
|
+
const constants_js_1 = require("./constants.js");
|
|
12
|
+
const config_js_1 = require("./config.js");
|
|
13
|
+
class Replay {
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this.apiKey = options.apiKey || config_js_1.config.TESTOMATIO || undefined;
|
|
16
|
+
this.dryRun = options.dryRun || false;
|
|
17
|
+
this.onProgress = options.onProgress || (() => { });
|
|
18
|
+
this.onLog = options.onLog || console.log;
|
|
19
|
+
this.onError = options.onError || console.error;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get the default debug file path
|
|
23
|
+
* @returns {string} Path to the latest debug file
|
|
24
|
+
*/
|
|
25
|
+
getDefaultDebugFile() {
|
|
26
|
+
return path_1.default.join(os_1.default.tmpdir(), 'testomatio.debug.latest.json');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Parse a debug file and extract test data
|
|
30
|
+
* @param {string} debugFile - Path to the debug file
|
|
31
|
+
* @returns {Object} Parsed debug data
|
|
32
|
+
*/
|
|
33
|
+
parseDebugFile(debugFile) {
|
|
34
|
+
if (!fs_1.default.existsSync(debugFile)) {
|
|
35
|
+
throw new Error(`Debug file not found: ${debugFile}`);
|
|
36
|
+
}
|
|
37
|
+
const fileContent = fs_1.default.readFileSync(debugFile, 'utf-8');
|
|
38
|
+
const lines = fileContent.trim().split('\n').filter(line => line.trim() !== '');
|
|
39
|
+
if (lines.length === 0) {
|
|
40
|
+
throw new Error('Debug file is empty');
|
|
41
|
+
}
|
|
42
|
+
let runParams = {};
|
|
43
|
+
let finishParams = {};
|
|
44
|
+
let parseErrors = 0;
|
|
45
|
+
const testsMap = new Map(); // Use Map to deduplicate by rid
|
|
46
|
+
const testsWithoutRid = []; // For tests without rid (backward compatibility)
|
|
47
|
+
const envVars = {};
|
|
48
|
+
// Parse debug file line by line
|
|
49
|
+
for (const [lineIndex, line] of lines.entries()) {
|
|
50
|
+
try {
|
|
51
|
+
const logEntry = JSON.parse(line);
|
|
52
|
+
if (logEntry.data === 'variables' && logEntry.testomatioEnvVars) {
|
|
53
|
+
Object.assign(envVars, logEntry.testomatioEnvVars);
|
|
54
|
+
}
|
|
55
|
+
else if (logEntry.action === 'createRun') {
|
|
56
|
+
runParams = logEntry.params || {};
|
|
57
|
+
}
|
|
58
|
+
else if (logEntry.action === 'addTestsBatch' && logEntry.tests) {
|
|
59
|
+
// Process each test in the batch
|
|
60
|
+
for (const test of logEntry.tests) {
|
|
61
|
+
if (test.rid) {
|
|
62
|
+
// Handle tests with rid (deduplicate)
|
|
63
|
+
const existingTest = testsMap.get(test.rid);
|
|
64
|
+
if (existingTest) {
|
|
65
|
+
// Merge test data - prioritize non-null/non-empty values
|
|
66
|
+
const mergedTest = { ...existingTest };
|
|
67
|
+
Object.keys(test).forEach(key => {
|
|
68
|
+
if (test[key] !== null && test[key] !== undefined) {
|
|
69
|
+
if (key === 'files' && Array.isArray(test[key]) && test[key].length > 0) {
|
|
70
|
+
// Merge files arrays
|
|
71
|
+
mergedTest.files = [...(existingTest.files || []), ...test[key]];
|
|
72
|
+
}
|
|
73
|
+
else if (key === 'artifacts' && Array.isArray(test[key]) && test[key].length > 0) {
|
|
74
|
+
// Merge artifacts arrays
|
|
75
|
+
mergedTest.artifacts = [...(existingTest.artifacts || []), ...test[key]];
|
|
76
|
+
}
|
|
77
|
+
else if (existingTest[key] === null || existingTest[key] === undefined ||
|
|
78
|
+
(Array.isArray(existingTest[key]) && existingTest[key].length === 0)) {
|
|
79
|
+
// Use new value if existing is null/undefined/empty array
|
|
80
|
+
mergedTest[key] = test[key];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
testsMap.set(test.rid, mergedTest);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
testsMap.set(test.rid, { ...test });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// Handle tests without rid (no deduplication)
|
|
92
|
+
testsWithoutRid.push({ ...test });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else if (logEntry.action === 'addTest' && logEntry.testId) {
|
|
97
|
+
const test = logEntry.testId;
|
|
98
|
+
if (test.rid) {
|
|
99
|
+
// Handle tests with rid (deduplicate)
|
|
100
|
+
const existingTest = testsMap.get(test.rid);
|
|
101
|
+
if (existingTest) {
|
|
102
|
+
// Merge with existing test
|
|
103
|
+
const mergedTest = { ...existingTest, ...test };
|
|
104
|
+
testsMap.set(test.rid, mergedTest);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
testsMap.set(test.rid, { ...test });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// Handle tests without rid (no deduplication)
|
|
112
|
+
testsWithoutRid.push({ ...test });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else if (logEntry.actions === 'finishRun') {
|
|
116
|
+
finishParams = logEntry.params || {};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
parseErrors++;
|
|
121
|
+
if (parseErrors <= 3) {
|
|
122
|
+
// Only show first 3 parse errors
|
|
123
|
+
this.onError(`Failed to parse line ${lineIndex + 1}: ${line.substring(0, 100)}...`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (parseErrors > 3) {
|
|
128
|
+
this.onError(`${parseErrors - 3} more parse errors occurred`);
|
|
129
|
+
}
|
|
130
|
+
// Combine tests with rid and tests without rid
|
|
131
|
+
const allTests = [...Array.from(testsMap.values()), ...testsWithoutRid];
|
|
132
|
+
return {
|
|
133
|
+
runParams,
|
|
134
|
+
finishParams,
|
|
135
|
+
tests: allTests,
|
|
136
|
+
envVars,
|
|
137
|
+
parseErrors,
|
|
138
|
+
totalLines: lines.length
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Restore environment variables from debug data
|
|
143
|
+
* @param {Object} envVars - Environment variables to restore
|
|
144
|
+
*/
|
|
145
|
+
restoreEnvironmentVariables(envVars) {
|
|
146
|
+
// Only restore env vars that aren't already set (don't override current values)
|
|
147
|
+
Object.keys(envVars).forEach(key => {
|
|
148
|
+
if (process.env[key] === undefined || process.env[key] === '') {
|
|
149
|
+
process.env[key] = envVars[key];
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Replay test data to Testomat.io
|
|
155
|
+
* @param {string} debugFile - Path to debug file (optional, uses default if not provided)
|
|
156
|
+
* @returns {Promise<Object>} Replay results
|
|
157
|
+
*/
|
|
158
|
+
async replay(debugFile) {
|
|
159
|
+
if (!debugFile) {
|
|
160
|
+
debugFile = this.getDefaultDebugFile();
|
|
161
|
+
}
|
|
162
|
+
if (!this.apiKey) {
|
|
163
|
+
throw new Error('TESTOMATIO API key not found. Set TESTOMATIO environment variable.');
|
|
164
|
+
}
|
|
165
|
+
this.onLog(`Replaying data from debug file: ${debugFile}`);
|
|
166
|
+
// Parse the debug file
|
|
167
|
+
const debugData = this.parseDebugFile(debugFile);
|
|
168
|
+
const { runParams, finishParams, tests, envVars } = debugData;
|
|
169
|
+
this.onLog(`Found ${tests.length} tests to replay`);
|
|
170
|
+
if (tests.length === 0) {
|
|
171
|
+
throw new Error('No test data found in debug file');
|
|
172
|
+
}
|
|
173
|
+
// Restore environment variables
|
|
174
|
+
this.restoreEnvironmentVariables(envVars);
|
|
175
|
+
if (this.dryRun) {
|
|
176
|
+
return {
|
|
177
|
+
success: true,
|
|
178
|
+
testsCount: tests.length,
|
|
179
|
+
runParams,
|
|
180
|
+
finishParams,
|
|
181
|
+
envVars,
|
|
182
|
+
dryRun: true
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
// Create client and restore the run
|
|
186
|
+
const client = new client_js_1.default({
|
|
187
|
+
apiKey: this.apiKey,
|
|
188
|
+
isBatchEnabled: true,
|
|
189
|
+
...runParams,
|
|
190
|
+
});
|
|
191
|
+
this.onLog('Publishing to run...');
|
|
192
|
+
await client.createRun(runParams);
|
|
193
|
+
// Send each test result
|
|
194
|
+
let successCount = 0;
|
|
195
|
+
let failureCount = 0;
|
|
196
|
+
for (const [index, test] of tests.entries()) {
|
|
197
|
+
try {
|
|
198
|
+
await client.addTestRun(test.status, test);
|
|
199
|
+
successCount++;
|
|
200
|
+
this.onProgress({
|
|
201
|
+
current: index + 1,
|
|
202
|
+
total: tests.length,
|
|
203
|
+
test,
|
|
204
|
+
success: true
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
failureCount++;
|
|
209
|
+
this.onError(`Failed to send test ${index + 1}: ${err.message}`);
|
|
210
|
+
this.onProgress({
|
|
211
|
+
current: index + 1,
|
|
212
|
+
total: tests.length,
|
|
213
|
+
test,
|
|
214
|
+
success: false,
|
|
215
|
+
error: err.message
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
await client.updateRunStatus(finishParams.status || constants_js_1.STATUS.FINISHED, finishParams.parallel || false);
|
|
220
|
+
const result = {
|
|
221
|
+
success: true,
|
|
222
|
+
testsCount: tests.length,
|
|
223
|
+
successCount,
|
|
224
|
+
failureCount,
|
|
225
|
+
runParams,
|
|
226
|
+
finishParams,
|
|
227
|
+
envVars,
|
|
228
|
+
runId: client.runId
|
|
229
|
+
};
|
|
230
|
+
this.onLog(`Successfully replayed ${successCount}/${tests.length} tests from debug file`);
|
|
231
|
+
return result;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
exports.Replay = Replay;
|
|
235
|
+
module.exports = Replay;
|
|
236
|
+
|
|
237
|
+
module.exports.Replay = Replay;
|
package/lib/uploader.js
CHANGED
|
@@ -113,7 +113,7 @@ class S3Uploader {
|
|
|
113
113
|
const upload = new lib_storage_1.Upload({ client: s3, params });
|
|
114
114
|
const link = await this.getS3LocationLink(upload);
|
|
115
115
|
this.successfulUploads.push({ path: file.path, size: file.size, link });
|
|
116
|
-
debug(`📤 Uploaded artifact. File: ${file.path}, size: ${(0, filesize_1.filesize)(file.size)}, link: ${link}`);
|
|
116
|
+
debug(`📤 Uploaded artifact. File: ${file.path}, size: ${(0, filesize_1.filesize)(file.size || 0)}, link: ${link}`);
|
|
117
117
|
return link;
|
|
118
118
|
}
|
|
119
119
|
catch (e) {
|
|
@@ -180,7 +180,8 @@ class S3Uploader {
|
|
|
180
180
|
* @returns
|
|
181
181
|
*/
|
|
182
182
|
async uploadFileByPath(filePath, pathInS3) {
|
|
183
|
-
|
|
183
|
+
/* WDIO: some artifacts uploading started before createRun function completion
|
|
184
|
+
probably, the reason is that run is NOT created in adapter (but via cli) */
|
|
184
185
|
this.isEnabled = this.isEnabled ?? this.checkEnabled();
|
|
185
186
|
const [runId, rid] = pathInS3;
|
|
186
187
|
if (!filePath)
|
|
@@ -189,7 +190,7 @@ class S3Uploader {
|
|
|
189
190
|
let fileSizeInMb = null;
|
|
190
191
|
try {
|
|
191
192
|
// file may not exist
|
|
192
|
-
fileSize = fs_1.default.statSync(filePath).size;
|
|
193
|
+
fileSize = fs_1.default.statSync(filePath).size || 0;
|
|
193
194
|
fileSizeInMb = Number((fileSize / (1024 * 1024)).toFixed(2));
|
|
194
195
|
}
|
|
195
196
|
catch (e) {
|
|
@@ -230,10 +231,13 @@ class S3Uploader {
|
|
|
230
231
|
* @returns
|
|
231
232
|
*/
|
|
232
233
|
async uploadFileAsBuffer(buffer, pathInS3) {
|
|
234
|
+
/* WDIO: some artifacts uploading started before createRun function completion
|
|
235
|
+
probably, the reason is that run is NOT created in adapter (but via cli) */
|
|
236
|
+
this.isEnabled = this.isEnabled ?? this.checkEnabled();
|
|
233
237
|
if (!this.isEnabled)
|
|
234
238
|
return;
|
|
235
239
|
let Key = pathInS3.filter(p => !!p).join('/');
|
|
236
|
-
const ext = this.#getFileExtBase64(buffer);
|
|
240
|
+
const ext = this.#getFileExtBase64(buffer.toString('base64'));
|
|
237
241
|
if (ext) {
|
|
238
242
|
Key = `${Key}.${ext}`;
|
|
239
243
|
}
|
package/lib/utils/utils.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export function fetchSourceCode(contents: any, opts?: {}): string;
|
|
|
6
6
|
export function fetchSourceCodeFromStackTrace(stack?: string): string;
|
|
7
7
|
export function fetchIdFromCode(code: any, opts?: {}): any;
|
|
8
8
|
export function fetchIdFromOutput(output: any): any;
|
|
9
|
-
export function fetchFilesFromStackTrace(stack?: string): string[];
|
|
9
|
+
export function fetchFilesFromStackTrace(stack?: string, checkExists?: boolean): string[];
|
|
10
10
|
export namespace fileSystem {
|
|
11
11
|
function createDir(dirPath: any): void;
|
|
12
12
|
function clearDir(dirPath: any): void;
|
package/lib/utils/utils.js
CHANGED
|
@@ -101,13 +101,23 @@ const isValidUrl = s => {
|
|
|
101
101
|
}
|
|
102
102
|
};
|
|
103
103
|
exports.isValidUrl = isValidUrl;
|
|
104
|
-
const fileMatchRegex = /file:(
|
|
105
|
-
const fetchFilesFromStackTrace = (stack = '') => {
|
|
104
|
+
const fileMatchRegex = /file:(\/+(?:[A-Za-z]:[\\/]|\/)?[^\s]*?\.(png|avi|webm|jpg|html|txt))/gi;
|
|
105
|
+
const fetchFilesFromStackTrace = (stack = '', checkExists = true) => {
|
|
106
106
|
const files = Array.from(stack.matchAll(fileMatchRegex))
|
|
107
107
|
.map(f => f[1].trim())
|
|
108
|
-
.map(f =>
|
|
108
|
+
.map(f => f.replace(/^\/+/, '/').replace(/^\/([A-Za-z]:)/, '$1')) // Remove extra slashes, handle Windows paths
|
|
109
|
+
.map(f => {
|
|
110
|
+
// Convert Windows paths to Linux paths for testing purposes
|
|
111
|
+
if (f.match(/^[A-Za-z]:[\\\/]/)) {
|
|
112
|
+
// Convert Windows path to Linux equivalent for test scenarios
|
|
113
|
+
return f.replace(/^[A-Za-z]:[\\\/]/, '/').replace(/\\/g, '/');
|
|
114
|
+
}
|
|
115
|
+
return f;
|
|
116
|
+
});
|
|
109
117
|
debug('Found files in stack trace: ', files);
|
|
110
118
|
return files.filter(f => {
|
|
119
|
+
if (!checkExists)
|
|
120
|
+
return true;
|
|
111
121
|
const isFile = fs_1.default.existsSync(f);
|
|
112
122
|
if (!isFile)
|
|
113
123
|
debug('File %s could not be found and uploaded as artifact', f);
|
package/package.json
CHANGED
package/src/adapter/webdriver.js
CHANGED
package/src/bin/cli.js
CHANGED
|
@@ -13,9 +13,7 @@ import { readLatestRunId } from '../utils/utils.js';
|
|
|
13
13
|
import pc from 'picocolors';
|
|
14
14
|
import { filesize as prettyBytes } from 'filesize';
|
|
15
15
|
import dotenv from 'dotenv';
|
|
16
|
-
import
|
|
17
|
-
import path from 'path';
|
|
18
|
-
import os from 'os';
|
|
16
|
+
import Replay from '../replay.js';
|
|
19
17
|
|
|
20
18
|
const debug = createDebugMessages('@testomatio/reporter:xml-cli');
|
|
21
19
|
const version = getPackageVersion();
|
|
@@ -303,101 +301,42 @@ program
|
|
|
303
301
|
.command('replay')
|
|
304
302
|
.description('Replay test data from debug file and re-send to Testomat.io')
|
|
305
303
|
.argument('[debug-file]', 'Path to debug file (defaults to /tmp/testomatio.debug.latest.json)')
|
|
304
|
+
.option('--dry-run', 'Preview the data without sending to Testomat.io')
|
|
306
305
|
.action(async (debugFile, opts) => {
|
|
307
|
-
// Use default debug file if none provided
|
|
308
|
-
if (!debugFile) {
|
|
309
|
-
debugFile = path.join(os.tmpdir(), 'testomatio.debug.latest.json');
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
if (!fs.existsSync(debugFile)) {
|
|
313
|
-
console.log(APP_PREFIX, `❌ Debug file not found: ${debugFile}`);
|
|
314
|
-
return process.exit(1);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
console.log(APP_PREFIX, `🪲 Replaying data from debug file: ${debugFile}`);
|
|
318
|
-
|
|
319
306
|
try {
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
console.
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
let runParams = {};
|
|
329
|
-
let finishParams = {};
|
|
330
|
-
let parseErrors = 0;
|
|
331
|
-
const allTests = [];
|
|
332
|
-
|
|
333
|
-
// Parse debug file line by line
|
|
334
|
-
for (const [lineIndex, line] of lines.entries()) {
|
|
335
|
-
try {
|
|
336
|
-
const logEntry = JSON.parse(line);
|
|
337
|
-
|
|
338
|
-
if (logEntry.data === 'variables' && logEntry.testomatioEnvVars) {
|
|
339
|
-
Object.assign(process.env, logEntry.testomatioEnvVars, process.env);
|
|
340
|
-
} else if (logEntry.action === 'createRun') {
|
|
341
|
-
runParams = logEntry.params || {};
|
|
342
|
-
} else if (logEntry.action === 'addTestsBatch' && logEntry.tests) {
|
|
343
|
-
allTests.push(...logEntry.tests);
|
|
344
|
-
} else if (logEntry.action === 'addTest' && logEntry.testId) {
|
|
345
|
-
allTests.push(logEntry.testId);
|
|
346
|
-
} else if (logEntry.actions === 'finishRun') {
|
|
347
|
-
finishParams = logEntry.params || {};
|
|
348
|
-
}
|
|
349
|
-
} catch (err) {
|
|
350
|
-
parseErrors++;
|
|
351
|
-
if (parseErrors <= 3) {
|
|
352
|
-
// Only show first 3 parse errors
|
|
353
|
-
console.warn(APP_PREFIX, `⚠️ Failed to parse line ${lineIndex + 1}: ${line.substring(0, 100)}...`);
|
|
307
|
+
const replayService = new Replay({
|
|
308
|
+
apiKey: config.TESTOMATIO,
|
|
309
|
+
dryRun: opts.dryRun,
|
|
310
|
+
onLog: (message) => console.log(APP_PREFIX, message),
|
|
311
|
+
onError: (message) => console.error(APP_PREFIX, '⚠️ ', message),
|
|
312
|
+
onProgress: ({ current, total }) => {
|
|
313
|
+
if (current % 10 === 0 || current === total) {
|
|
314
|
+
console.log(APP_PREFIX, `📊 Progress: ${current}/${total} tests processed`);
|
|
354
315
|
}
|
|
355
316
|
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
if (parseErrors > 3) {
|
|
359
|
-
console.warn(APP_PREFIX, `⚠️ ${parseErrors - 3} more parse errors occurred`);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
console.log(APP_PREFIX, `📊 Found ${allTests.length} tests to replay`);
|
|
363
|
-
|
|
364
|
-
if (allTests.length === 0) {
|
|
365
|
-
console.log(APP_PREFIX, '❌ No test data found in debug file');
|
|
366
|
-
return process.exit(1);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const apiKey = config.TESTOMATIO;
|
|
370
|
-
if (!apiKey) {
|
|
371
|
-
console.log(APP_PREFIX, '❌ TESTOMATIO API key not found. Set TESTOMATIO environment variable.');
|
|
372
|
-
return process.exit(1);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Create client and restore the run
|
|
376
|
-
const client = new TestomatClient({
|
|
377
|
-
apiKey,
|
|
378
|
-
isBatchEnabled: true,
|
|
379
|
-
...runParams,
|
|
380
317
|
});
|
|
381
318
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
319
|
+
const result = await replayService.replay(debugFile);
|
|
320
|
+
|
|
321
|
+
if (result.dryRun) {
|
|
322
|
+
console.log(APP_PREFIX, '🔍 Dry run completed:');
|
|
323
|
+
console.log(APP_PREFIX, ` - Tests found: ${result.testsCount}`);
|
|
324
|
+
console.log(APP_PREFIX, ` - Environment variables: ${Object.keys(result.envVars).length}`);
|
|
325
|
+
console.log(APP_PREFIX, ` - Run parameters:`, result.runParams);
|
|
326
|
+
console.log(APP_PREFIX, ' Use without --dry-run to actually send the data');
|
|
327
|
+
} else {
|
|
328
|
+
console.log(APP_PREFIX, `✅ Successfully replayed ${result.successCount}/${result.testsCount} tests`);
|
|
329
|
+
if (result.failureCount > 0) {
|
|
330
|
+
console.log(APP_PREFIX, `⚠️ ${result.failureCount} tests failed to upload`);
|
|
391
331
|
}
|
|
392
332
|
}
|
|
393
333
|
|
|
394
|
-
await client.updateRunStatus(finishParams.status || STATUS.FINISHED, finishParams.parallel || false);
|
|
395
|
-
|
|
396
|
-
console.log(APP_PREFIX, `✅ Successfully replayed ${allTests.length} tests from debug file`);
|
|
397
334
|
process.exit(0);
|
|
398
335
|
} catch (err) {
|
|
399
336
|
console.error(APP_PREFIX, '❌ Error replaying debug data:', err.message);
|
|
400
|
-
|
|
337
|
+
if (err.message.includes('Debug file not found')) {
|
|
338
|
+
console.error(APP_PREFIX, '💡 Hint: Run tests with TESTOMATIO_DEBUG=1 to generate debug files');
|
|
339
|
+
}
|
|
401
340
|
process.exit(1);
|
|
402
341
|
}
|
|
403
342
|
});
|
package/src/pipe/debug.js
CHANGED
|
@@ -109,7 +109,7 @@ export class DebugPipe {
|
|
|
109
109
|
if (!this.isEnabled) return;
|
|
110
110
|
await this.batchUpload();
|
|
111
111
|
if (this.batch.intervalFunction) clearInterval(this.batch.intervalFunction);
|
|
112
|
-
this.logToFile({
|
|
112
|
+
this.logToFile({ action: 'finishRun', params });
|
|
113
113
|
console.log(APP_PREFIX, '🪲 Debug Saved to', this.logFilePath);
|
|
114
114
|
}
|
|
115
115
|
|
package/src/pipe/testomatio.js
CHANGED
|
@@ -367,6 +367,8 @@ class TestomatioPipe {
|
|
|
367
367
|
* Adds a test to the batch uploader (or reports a single test if batch uploading is disabled)
|
|
368
368
|
*/
|
|
369
369
|
addTest(data) {
|
|
370
|
+
this.isEnabled = this.apiKey ?? this.isEnabled;
|
|
371
|
+
|
|
370
372
|
if (!this.isEnabled) return;
|
|
371
373
|
if (!this.runId) return;
|
|
372
374
|
|
|
@@ -375,11 +377,15 @@ class TestomatioPipe {
|
|
|
375
377
|
data.api_key = this.apiKey;
|
|
376
378
|
data.create = this.createNewTests;
|
|
377
379
|
|
|
378
|
-
|
|
380
|
+
let uploading = null;
|
|
381
|
+
if (!this.batch.isEnabled) uploading = this.#uploadSingleTest(data);
|
|
379
382
|
else this.batch.tests.push(data);
|
|
380
383
|
|
|
381
384
|
// if test is added after run which is already finished
|
|
382
|
-
|
|
385
|
+
if (!this.batch.intervalFunction) uploading = this.#batchUpload();
|
|
386
|
+
|
|
387
|
+
// return promise to be able to wait for it
|
|
388
|
+
return uploading;
|
|
383
389
|
}
|
|
384
390
|
|
|
385
391
|
/**
|
package/src/replay.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import TestomatClient from './client.js';
|
|
5
|
+
import { STATUS } from './constants.js';
|
|
6
|
+
import { config } from './config.js';
|
|
7
|
+
|
|
8
|
+
export class Replay {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.apiKey = options.apiKey || config.TESTOMATIO || undefined;
|
|
11
|
+
this.dryRun = options.dryRun || false;
|
|
12
|
+
this.onProgress = options.onProgress || (() => {});
|
|
13
|
+
this.onLog = options.onLog || console.log;
|
|
14
|
+
this.onError = options.onError || console.error;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the default debug file path
|
|
19
|
+
* @returns {string} Path to the latest debug file
|
|
20
|
+
*/
|
|
21
|
+
getDefaultDebugFile() {
|
|
22
|
+
return path.join(os.tmpdir(), 'testomatio.debug.latest.json');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse a debug file and extract test data
|
|
27
|
+
* @param {string} debugFile - Path to the debug file
|
|
28
|
+
* @returns {Object} Parsed debug data
|
|
29
|
+
*/
|
|
30
|
+
parseDebugFile(debugFile) {
|
|
31
|
+
if (!fs.existsSync(debugFile)) {
|
|
32
|
+
throw new Error(`Debug file not found: ${debugFile}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const fileContent = fs.readFileSync(debugFile, 'utf-8');
|
|
36
|
+
const lines = fileContent.trim().split('\n').filter(line => line.trim() !== '');
|
|
37
|
+
|
|
38
|
+
if (lines.length === 0) {
|
|
39
|
+
throw new Error('Debug file is empty');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let runParams = {};
|
|
43
|
+
let finishParams = {};
|
|
44
|
+
let parseErrors = 0;
|
|
45
|
+
const testsMap = new Map(); // Use Map to deduplicate by rid
|
|
46
|
+
const testsWithoutRid = []; // For tests without rid (backward compatibility)
|
|
47
|
+
const envVars = {};
|
|
48
|
+
|
|
49
|
+
// Parse debug file line by line
|
|
50
|
+
for (const [lineIndex, line] of lines.entries()) {
|
|
51
|
+
try {
|
|
52
|
+
const logEntry = JSON.parse(line);
|
|
53
|
+
|
|
54
|
+
if (logEntry.data === 'variables' && logEntry.testomatioEnvVars) {
|
|
55
|
+
Object.assign(envVars, logEntry.testomatioEnvVars);
|
|
56
|
+
} else if (logEntry.action === 'createRun') {
|
|
57
|
+
runParams = logEntry.params || {};
|
|
58
|
+
} else if (logEntry.action === 'addTestsBatch' && logEntry.tests) {
|
|
59
|
+
// Process each test in the batch
|
|
60
|
+
for (const test of logEntry.tests) {
|
|
61
|
+
if (test.rid) {
|
|
62
|
+
// Handle tests with rid (deduplicate)
|
|
63
|
+
const existingTest = testsMap.get(test.rid);
|
|
64
|
+
if (existingTest) {
|
|
65
|
+
// Merge test data - prioritize non-null/non-empty values
|
|
66
|
+
const mergedTest = { ...existingTest };
|
|
67
|
+
Object.keys(test).forEach(key => {
|
|
68
|
+
if (test[key] !== null && test[key] !== undefined) {
|
|
69
|
+
if (key === 'files' && Array.isArray(test[key]) && test[key].length > 0) {
|
|
70
|
+
// Merge files arrays
|
|
71
|
+
mergedTest.files = [...(existingTest.files || []), ...test[key]];
|
|
72
|
+
} else if (key === 'artifacts' && Array.isArray(test[key]) && test[key].length > 0) {
|
|
73
|
+
// Merge artifacts arrays
|
|
74
|
+
mergedTest.artifacts = [...(existingTest.artifacts || []), ...test[key]];
|
|
75
|
+
} else if (existingTest[key] === null || existingTest[key] === undefined ||
|
|
76
|
+
(Array.isArray(existingTest[key]) && existingTest[key].length === 0)) {
|
|
77
|
+
// Use new value if existing is null/undefined/empty array
|
|
78
|
+
mergedTest[key] = test[key];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
testsMap.set(test.rid, mergedTest);
|
|
83
|
+
} else {
|
|
84
|
+
testsMap.set(test.rid, { ...test });
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
// Handle tests without rid (no deduplication)
|
|
88
|
+
testsWithoutRid.push({ ...test });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} else if (logEntry.action === 'addTest' && logEntry.testId) {
|
|
92
|
+
const test = logEntry.testId;
|
|
93
|
+
if (test.rid) {
|
|
94
|
+
// Handle tests with rid (deduplicate)
|
|
95
|
+
const existingTest = testsMap.get(test.rid);
|
|
96
|
+
if (existingTest) {
|
|
97
|
+
// Merge with existing test
|
|
98
|
+
const mergedTest = { ...existingTest, ...test };
|
|
99
|
+
testsMap.set(test.rid, mergedTest);
|
|
100
|
+
} else {
|
|
101
|
+
testsMap.set(test.rid, { ...test });
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
// Handle tests without rid (no deduplication)
|
|
105
|
+
testsWithoutRid.push({ ...test });
|
|
106
|
+
}
|
|
107
|
+
} else if (logEntry.actions === 'finishRun') {
|
|
108
|
+
finishParams = logEntry.params || {};
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
parseErrors++;
|
|
112
|
+
if (parseErrors <= 3) {
|
|
113
|
+
// Only show first 3 parse errors
|
|
114
|
+
this.onError(`Failed to parse line ${lineIndex + 1}: ${line.substring(0, 100)}...`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (parseErrors > 3) {
|
|
120
|
+
this.onError(`${parseErrors - 3} more parse errors occurred`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Combine tests with rid and tests without rid
|
|
124
|
+
const allTests = [...Array.from(testsMap.values()), ...testsWithoutRid];
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
runParams,
|
|
128
|
+
finishParams,
|
|
129
|
+
tests: allTests,
|
|
130
|
+
envVars,
|
|
131
|
+
parseErrors,
|
|
132
|
+
totalLines: lines.length
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Restore environment variables from debug data
|
|
138
|
+
* @param {Object} envVars - Environment variables to restore
|
|
139
|
+
*/
|
|
140
|
+
restoreEnvironmentVariables(envVars) {
|
|
141
|
+
// Only restore env vars that aren't already set (don't override current values)
|
|
142
|
+
Object.keys(envVars).forEach(key => {
|
|
143
|
+
if (process.env[key] === undefined || process.env[key] === '') {
|
|
144
|
+
process.env[key] = envVars[key];
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Replay test data to Testomat.io
|
|
151
|
+
* @param {string} debugFile - Path to debug file (optional, uses default if not provided)
|
|
152
|
+
* @returns {Promise<Object>} Replay results
|
|
153
|
+
*/
|
|
154
|
+
async replay(debugFile) {
|
|
155
|
+
if (!debugFile) {
|
|
156
|
+
debugFile = this.getDefaultDebugFile();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!this.apiKey) {
|
|
160
|
+
throw new Error('TESTOMATIO API key not found. Set TESTOMATIO environment variable.');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this.onLog(`Replaying data from debug file: ${debugFile}`);
|
|
164
|
+
|
|
165
|
+
// Parse the debug file
|
|
166
|
+
const debugData = this.parseDebugFile(debugFile);
|
|
167
|
+
const { runParams, finishParams, tests, envVars } = debugData;
|
|
168
|
+
|
|
169
|
+
this.onLog(`Found ${tests.length} tests to replay`);
|
|
170
|
+
|
|
171
|
+
if (tests.length === 0) {
|
|
172
|
+
throw new Error('No test data found in debug file');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Restore environment variables
|
|
176
|
+
this.restoreEnvironmentVariables(envVars);
|
|
177
|
+
|
|
178
|
+
if (this.dryRun) {
|
|
179
|
+
return {
|
|
180
|
+
success: true,
|
|
181
|
+
testsCount: tests.length,
|
|
182
|
+
runParams,
|
|
183
|
+
finishParams,
|
|
184
|
+
envVars,
|
|
185
|
+
dryRun: true
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Create client and restore the run
|
|
190
|
+
const client = new TestomatClient({
|
|
191
|
+
apiKey: this.apiKey,
|
|
192
|
+
isBatchEnabled: true,
|
|
193
|
+
...runParams,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
this.onLog('Publishing to run...');
|
|
197
|
+
await client.createRun(runParams);
|
|
198
|
+
|
|
199
|
+
// Send each test result
|
|
200
|
+
let successCount = 0;
|
|
201
|
+
let failureCount = 0;
|
|
202
|
+
|
|
203
|
+
for (const [index, test] of tests.entries()) {
|
|
204
|
+
try {
|
|
205
|
+
await client.addTestRun(test.status, test);
|
|
206
|
+
successCount++;
|
|
207
|
+
this.onProgress({
|
|
208
|
+
current: index + 1,
|
|
209
|
+
total: tests.length,
|
|
210
|
+
test,
|
|
211
|
+
success: true
|
|
212
|
+
});
|
|
213
|
+
} catch (err) {
|
|
214
|
+
failureCount++;
|
|
215
|
+
this.onError(`Failed to send test ${index + 1}: ${err.message}`);
|
|
216
|
+
this.onProgress({
|
|
217
|
+
current: index + 1,
|
|
218
|
+
total: tests.length,
|
|
219
|
+
test,
|
|
220
|
+
success: false,
|
|
221
|
+
error: err.message
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
await client.updateRunStatus(finishParams.status || STATUS.FINISHED, finishParams.parallel || false);
|
|
227
|
+
|
|
228
|
+
const result = {
|
|
229
|
+
success: true,
|
|
230
|
+
testsCount: tests.length,
|
|
231
|
+
successCount,
|
|
232
|
+
failureCount,
|
|
233
|
+
runParams,
|
|
234
|
+
finishParams,
|
|
235
|
+
envVars,
|
|
236
|
+
runId: client.runId
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
this.onLog(`Successfully replayed ${successCount}/${tests.length} tests from debug file`);
|
|
240
|
+
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export default Replay;
|
package/src/uploader.js
CHANGED
|
@@ -128,7 +128,7 @@ export class S3Uploader {
|
|
|
128
128
|
|
|
129
129
|
const link = await this.getS3LocationLink(upload);
|
|
130
130
|
this.successfulUploads.push({ path: file.path, size: file.size, link });
|
|
131
|
-
debug(`📤 Uploaded artifact. File: ${file.path}, size: ${prettyBytes(file.size)}, link: ${link}`);
|
|
131
|
+
debug(`📤 Uploaded artifact. File: ${file.path}, size: ${prettyBytes(file.size || 0)}, link: ${link}`);
|
|
132
132
|
return link;
|
|
133
133
|
} catch (e) {
|
|
134
134
|
this.failedUploads.push({ path: file.path, size: file.size });
|
|
@@ -205,7 +205,8 @@ export class S3Uploader {
|
|
|
205
205
|
* @returns
|
|
206
206
|
*/
|
|
207
207
|
async uploadFileByPath(filePath, pathInS3) {
|
|
208
|
-
|
|
208
|
+
/* WDIO: some artifacts uploading started before createRun function completion
|
|
209
|
+
probably, the reason is that run is NOT created in adapter (but via cli) */
|
|
209
210
|
this.isEnabled = this.isEnabled ?? this.checkEnabled();
|
|
210
211
|
|
|
211
212
|
const [runId, rid] = pathInS3;
|
|
@@ -217,7 +218,7 @@ export class S3Uploader {
|
|
|
217
218
|
|
|
218
219
|
try {
|
|
219
220
|
// file may not exist
|
|
220
|
-
fileSize = fs.statSync(filePath).size;
|
|
221
|
+
fileSize = fs.statSync(filePath).size || 0;
|
|
221
222
|
fileSizeInMb = Number((fileSize / (1024 * 1024)).toFixed(2));
|
|
222
223
|
} catch (e) {
|
|
223
224
|
debug(`File ${filePath} does not exist`);
|
|
@@ -270,10 +271,14 @@ export class S3Uploader {
|
|
|
270
271
|
* @returns
|
|
271
272
|
*/
|
|
272
273
|
async uploadFileAsBuffer(buffer, pathInS3) {
|
|
274
|
+
/* WDIO: some artifacts uploading started before createRun function completion
|
|
275
|
+
probably, the reason is that run is NOT created in adapter (but via cli) */
|
|
276
|
+
|
|
277
|
+
this.isEnabled = this.isEnabled ?? this.checkEnabled();
|
|
273
278
|
if (!this.isEnabled) return;
|
|
274
279
|
|
|
275
280
|
let Key = pathInS3.filter(p => !!p).join('/');
|
|
276
|
-
const ext = this.#getFileExtBase64(buffer);
|
|
281
|
+
const ext = this.#getFileExtBase64(buffer.toString('base64'));
|
|
277
282
|
|
|
278
283
|
if (ext) {
|
|
279
284
|
Key = `${Key}.${ext}`;
|
package/src/utils/utils.js
CHANGED
|
@@ -64,16 +64,25 @@ const isValidUrl = s => {
|
|
|
64
64
|
}
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
const fileMatchRegex = /file:(
|
|
67
|
+
const fileMatchRegex = /file:(\/+(?:[A-Za-z]:[\\/]|\/)?[^\s]*?\.(png|avi|webm|jpg|html|txt))/gi;
|
|
68
68
|
|
|
69
|
-
const fetchFilesFromStackTrace = (stack = '') => {
|
|
69
|
+
const fetchFilesFromStackTrace = (stack = '', checkExists = true) => {
|
|
70
70
|
const files = Array.from(stack.matchAll(fileMatchRegex))
|
|
71
71
|
.map(f => f[1].trim())
|
|
72
|
-
.map(f =>
|
|
72
|
+
.map(f => f.replace(/^\/+/, '/').replace(/^\/([A-Za-z]:)/, '$1')) // Remove extra slashes, handle Windows paths
|
|
73
|
+
.map(f => {
|
|
74
|
+
// Convert Windows paths to Linux paths for testing purposes
|
|
75
|
+
if (f.match(/^[A-Za-z]:[\\\/]/)) {
|
|
76
|
+
// Convert Windows path to Linux equivalent for test scenarios
|
|
77
|
+
return f.replace(/^[A-Za-z]:[\\\/]/, '/').replace(/\\/g, '/');
|
|
78
|
+
}
|
|
79
|
+
return f;
|
|
80
|
+
});
|
|
73
81
|
|
|
74
82
|
debug('Found files in stack trace: ', files);
|
|
75
83
|
|
|
76
84
|
return files.filter(f => {
|
|
85
|
+
if (!checkExists) return true;
|
|
77
86
|
const isFile = fs.existsSync(f);
|
|
78
87
|
if (!isFile) debug('File %s could not be found and uploaded as artifact', f);
|
|
79
88
|
return isFile;
|