@testomatio/reporter 2.0.1-beta.1 → 2.0.1-beta.3

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.
@@ -98,6 +98,10 @@ class PlaywrightReporter {
98
98
  projectDependencies: project.dependencies?.length ? project.dependencies : null,
99
99
  ...testMeta,
100
100
  ...project.metadata, // metadata has any type (in playwright), but we will stringify it in client.js
101
+ ...test.annotations?.reduce((acc, annotation) => {
102
+ acc[annotation.type] = annotation.description;
103
+ return acc;
104
+ }, {}),
101
105
  },
102
106
  file: test.location?.file,
103
107
  });
@@ -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 fs_1 = __importDefault(require("fs"));
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 fileContent = fs_1.default.readFileSync(debugFile, 'utf-8');
266
- const lines = fileContent.trim().split('\n').filter(Boolean);
267
- if (lines.length === 0) {
268
- console.log(constants_js_1.APP_PREFIX, '❌ Debug file is empty');
269
- return process.exit(1);
270
- }
271
- let runParams = {};
272
- let finishParams = {};
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
- console.log(constants_js_1.APP_PREFIX, '🚀 Publishing to run...');
323
- await client.createRun(runParams);
324
- // Send each test result - let client.js handle the data mapping and formatting
325
- for (const [index, test] of allTests.entries()) {
326
- try {
327
- await client.addTestRun(test.status, test);
328
- }
329
- catch (err) {
330
- console.warn(constants_js_1.APP_PREFIX, `⚠️ Failed to send test ${index + 1}: ${err.message}`);
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
- console.error(err.stack);
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/client.js CHANGED
@@ -240,6 +240,9 @@ class Client {
240
240
  uploadedFiles.push(this.uploader.uploadFileAsBuffer(buffer, [this.runId, rid, fileName]));
241
241
  }
242
242
  const artifacts = (await Promise.all(uploadedFiles)).filter(n => !!n);
243
+ const workspaceDir = process.env.TESTOMATIO_WORKDIR || process.cwd();
244
+ const relativeFile = file ? path_1.default.relative(workspaceDir, file) : file;
245
+ const rootSuiteId = (0, utils_js_1.validateSuiteId)(process.env.TESTOMATIO_SUITE);
243
246
  const data = {
244
247
  rid,
245
248
  files,
@@ -247,7 +250,7 @@ class Client {
247
250
  status,
248
251
  stack: fullLogs,
249
252
  example,
250
- file,
253
+ file: relativeFile,
251
254
  code,
252
255
  title,
253
256
  suite_title,
@@ -257,6 +260,7 @@ class Client {
257
260
  run_time: typeof time === 'number' ? time : parseFloat(time),
258
261
  artifacts,
259
262
  meta,
263
+ ...(rootSuiteId && { root_suite_id: rootSuiteId }),
260
264
  };
261
265
  // debug('Adding test run...', data);
262
266
  // @ts-ignore
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({ actions: 'finishRun', params });
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() {
@@ -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
@@ -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
@@ -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
- // sometimes artifacts uploading started before createRun function completion
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
  }
@@ -1,12 +1,13 @@
1
1
  export function getPackageVersion(): any;
2
2
  export const TEST_ID_REGEX: RegExp;
3
+ export const SUITE_ID_REGEX: RegExp;
3
4
  export function ansiRegExp(): RegExp;
4
5
  export function isSameTest(test: any, t: any): boolean;
5
6
  export function fetchSourceCode(contents: any, opts?: {}): string;
6
7
  export function fetchSourceCodeFromStackTrace(stack?: string): string;
7
8
  export function fetchIdFromCode(code: any, opts?: {}): any;
8
9
  export function fetchIdFromOutput(output: any): any;
9
- export function fetchFilesFromStackTrace(stack?: string): string[];
10
+ export function fetchFilesFromStackTrace(stack?: string, checkExists?: boolean): string[];
10
11
  export namespace fileSystem {
11
12
  function createDir(dirPath: any): void;
12
13
  function clearDir(dirPath: any): void;
@@ -45,3 +46,9 @@ export function storeRunId(runId: any): void;
45
46
  export namespace testRunnerHelper {
46
47
  function getNameOfCurrentlyRunningTest(): any;
47
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
+ export function validateSuiteId(suiteId: string): string | null;
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.testRunnerHelper = exports.specificTestInfo = exports.parseSuite = exports.isValidUrl = exports.humanize = exports.getTestomatIdFromTestTitle = exports.getCurrentDateTime = exports.foundedTestLog = exports.fileSystem = exports.fetchFilesFromStackTrace = exports.fetchIdFromOutput = exports.fetchIdFromCode = exports.fetchSourceCodeFromStackTrace = exports.fetchSourceCode = exports.isSameTest = exports.ansiRegExp = exports.TEST_ID_REGEX = void 0;
39
+ exports.validateSuiteId = exports.testRunnerHelper = exports.specificTestInfo = exports.parseSuite = exports.isValidUrl = exports.humanize = exports.getTestomatIdFromTestTitle = exports.getCurrentDateTime = exports.foundedTestLog = exports.fileSystem = exports.fetchFilesFromStackTrace = exports.fetchIdFromOutput = exports.fetchIdFromCode = exports.fetchSourceCodeFromStackTrace = exports.fetchSourceCode = exports.isSameTest = exports.ansiRegExp = exports.SUITE_ID_REGEX = exports.TEST_ID_REGEX = void 0;
40
40
  exports.getPackageVersion = getPackageVersion;
41
41
  exports.formatStep = formatStep;
42
42
  exports.readLatestRunId = readLatestRunId;
@@ -78,11 +78,23 @@ exports.getTestomatIdFromTestTitle = getTestomatIdFromTestTitle;
78
78
  const parseSuite = suiteTitle => {
79
79
  const captures = suiteTitle.match(/@S[\w\d]{8}/);
80
80
  if (captures) {
81
- return captures[1];
81
+ return captures[0];
82
82
  }
83
83
  return null;
84
84
  };
85
85
  exports.parseSuite = parseSuite;
86
+ /**
87
+ * Validates TESTOMATIO_SUITE environment variable format
88
+ * @param {String} suiteId - suite ID to validate
89
+ * @returns {String|null} validated suite ID or null if invalid
90
+ */
91
+ const validateSuiteId = suiteId => {
92
+ if (!suiteId)
93
+ return null;
94
+ const match = suiteId.match(exports.SUITE_ID_REGEX);
95
+ return match ? match[0] : null;
96
+ };
97
+ exports.validateSuiteId = validateSuiteId;
86
98
  const ansiRegExp = () => {
87
99
  const pattern = [
88
100
  '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
@@ -101,13 +113,23 @@ const isValidUrl = s => {
101
113
  }
102
114
  };
103
115
  exports.isValidUrl = isValidUrl;
104
- const fileMatchRegex = /file:(\/\/?[^:\s]+?\.(png|avi|webm|jpg|html|txt))/gi;
105
- const fetchFilesFromStackTrace = (stack = '') => {
116
+ const fileMatchRegex = /file:(\/+(?:[A-Za-z]:[\\/]|\/)?[^\s]*?\.(png|avi|webm|jpg|html|txt))/gi;
117
+ const fetchFilesFromStackTrace = (stack = '', checkExists = true) => {
106
118
  const files = Array.from(stack.matchAll(fileMatchRegex))
107
119
  .map(f => f[1].trim())
108
- .map(f => (f.startsWith('//') ? f.substring(1) : f));
120
+ .map(f => f.replace(/^\/+/, '/').replace(/^\/([A-Za-z]:)/, '$1')) // Remove extra slashes, handle Windows paths
121
+ .map(f => {
122
+ // Convert Windows paths to Linux paths for testing purposes
123
+ if (f.match(/^[A-Za-z]:[\\\/]/)) {
124
+ // Convert Windows path to Linux equivalent for test scenarios
125
+ return f.replace(/^[A-Za-z]:[\\\/]/, '/').replace(/\\/g, '/');
126
+ }
127
+ return f;
128
+ });
109
129
  debug('Found files in stack trace: ', files);
110
130
  return files.filter(f => {
131
+ if (!checkExists)
132
+ return true;
111
133
  const isFile = fs_1.default.existsSync(f);
112
134
  if (!isFile)
113
135
  debug('File %s could not be found and uploaded as artifact', f);
@@ -147,6 +169,7 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
147
169
  };
148
170
  exports.fetchSourceCodeFromStackTrace = fetchSourceCodeFromStackTrace;
149
171
  exports.TEST_ID_REGEX = /@T([\w\d]{8})/;
172
+ exports.SUITE_ID_REGEX = /@S([\w\d]{8})/;
150
173
  const fetchIdFromCode = (code, opts = {}) => {
151
174
  const comments = code
152
175
  .split('\n')
@@ -416,6 +439,8 @@ module.exports.getTestomatIdFromTestTitle = getTestomatIdFromTestTitle;
416
439
 
417
440
  module.exports.parseSuite = parseSuite;
418
441
 
442
+ module.exports.validateSuiteId = validateSuiteId;
443
+
419
444
  module.exports.ansiRegExp = ansiRegExp;
420
445
 
421
446
  module.exports.isValidUrl = isValidUrl;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.0.1-beta.1",
3
+ "version": "2.0.1-beta.3",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"
@@ -54,7 +54,9 @@
54
54
  "lint": "eslint src",
55
55
  "lint:fix": "eslint src --fix",
56
56
  "format": "npm run lint:fix && npm run pretty:fix",
57
- "test": "mocha tests/**",
57
+ "test": "mocha tests/** --ignore tests/adapter/playwright.test.js --ignore tests/adapter/codecept.test.js",
58
+ "test:frameworks": "mocha tests/adapter/playwright.test.js tests/adapter/codecept.test.js",
59
+ "install-example-deps": "cd example/playwright && npm install && cd ../codecept && npm install",
58
60
  "init": "cd ./tests/adapter/examples/cucumber && npm i",
59
61
  "test:adapter": "node node_modules/mocha/bin/mocha './tests/adapter/index.test.js'",
60
62
  "test:pipes": "mocha './tests/pipes/*_test.js'",
@@ -102,6 +102,10 @@ class PlaywrightReporter {
102
102
  projectDependencies: project.dependencies?.length ? project.dependencies : null,
103
103
  ...testMeta,
104
104
  ...project.metadata, // metadata has any type (in playwright), but we will stringify it in client.js
105
+ ...test.annotations?.reduce((acc, annotation) => {
106
+ acc[annotation.type] = annotation.description;
107
+ return acc;
108
+ }, {}),
105
109
  },
106
110
  file: test.location?.file,
107
111
  });
@@ -81,6 +81,7 @@ class WebdriverReporter extends WDIOReporter {
81
81
  .map(el => Buffer.from(el.result.value, 'base64'));
82
82
 
83
83
  await this.client.addTestRun(state, {
84
+ rid: test.uid || '',
84
85
  manuallyAttachedArtifacts: test.artifacts,
85
86
  error,
86
87
  logs: test.logs,
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 fs from 'fs';
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 fileContent = fs.readFileSync(debugFile, 'utf-8');
321
- const lines = fileContent.trim().split('\n').filter(Boolean);
322
-
323
- if (lines.length === 0) {
324
- console.log(APP_PREFIX, ' Debug file is empty');
325
- return process.exit(1);
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
- console.log(APP_PREFIX, '🚀 Publishing to run...');
383
- await client.createRun(runParams);
384
-
385
- // Send each test result - let client.js handle the data mapping and formatting
386
- for (const [index, test] of allTests.entries()) {
387
- try {
388
- await client.addTestRun(test.status, test);
389
- } catch (err) {
390
- console.warn(APP_PREFIX, `⚠️ Failed to send test ${index + 1}: ${err.message}`);
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
- console.error(err.stack);
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/client.js CHANGED
@@ -10,7 +10,7 @@ import { glob } from 'glob';
10
10
  import path, { sep } from 'path';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import { S3Uploader } from './uploader.js';
13
- import { formatStep, storeRunId } from './utils/utils.js';
13
+ import { formatStep, storeRunId, validateSuiteId } from './utils/utils.js';
14
14
  import { filesize as prettyBytes } from 'filesize';
15
15
 
16
16
  const debug = createDebugMessages('@testomatio/reporter:client');
@@ -245,6 +245,10 @@ class Client {
245
245
 
246
246
  const artifacts = (await Promise.all(uploadedFiles)).filter(n => !!n);
247
247
 
248
+ const workspaceDir = process.env.TESTOMATIO_WORKDIR || process.cwd();
249
+ const relativeFile = file ? path.relative(workspaceDir, file) : file;
250
+ const rootSuiteId = validateSuiteId(process.env.TESTOMATIO_SUITE);
251
+
248
252
  const data = {
249
253
  rid,
250
254
  files,
@@ -252,7 +256,7 @@ class Client {
252
256
  status,
253
257
  stack: fullLogs,
254
258
  example,
255
- file,
259
+ file: relativeFile,
256
260
  code,
257
261
  title,
258
262
  suite_title,
@@ -262,6 +266,7 @@ class Client {
262
266
  run_time: typeof time === 'number' ? time : parseFloat(time),
263
267
  artifacts,
264
268
  meta,
269
+ ...(rootSuiteId && { root_suite_id: rootSuiteId }),
265
270
  };
266
271
 
267
272
  // debug('Adding test run...', data);
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({ actions: 'finishRun', params });
112
+ this.logToFile({ action: 'finishRun', params });
113
113
  console.log(APP_PREFIX, '🪲 Debug Saved to', this.logFilePath);
114
114
  }
115
115
 
@@ -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
- if (!this.batch.isEnabled) this.#uploadSingleTest(data);
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
- if (!this.batch.intervalFunction) this.#batchUpload();
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
- // sometimes artifacts uploading started before createRun function completion
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}`;
@@ -40,12 +40,24 @@ const getTestomatIdFromTestTitle = testTitle => {
40
40
  const parseSuite = suiteTitle => {
41
41
  const captures = suiteTitle.match(/@S[\w\d]{8}/);
42
42
  if (captures) {
43
- return captures[1];
43
+ return captures[0];
44
44
  }
45
45
 
46
46
  return null;
47
47
  };
48
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
+
49
61
  const ansiRegExp = () => {
50
62
  const pattern = [
51
63
  '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
@@ -64,16 +76,25 @@ const isValidUrl = s => {
64
76
  }
65
77
  };
66
78
 
67
- const fileMatchRegex = /file:(\/\/?[^:\s]+?\.(png|avi|webm|jpg|html|txt))/gi;
79
+ const fileMatchRegex = /file:(\/+(?:[A-Za-z]:[\\/]|\/)?[^\s]*?\.(png|avi|webm|jpg|html|txt))/gi;
68
80
 
69
- const fetchFilesFromStackTrace = (stack = '') => {
81
+ const fetchFilesFromStackTrace = (stack = '', checkExists = true) => {
70
82
  const files = Array.from(stack.matchAll(fileMatchRegex))
71
83
  .map(f => f[1].trim())
72
- .map(f => (f.startsWith('//') ? f.substring(1) : 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
+ });
73
93
 
74
94
  debug('Found files in stack trace: ', files);
75
95
 
76
96
  return files.filter(f => {
97
+ if (!checkExists) return true;
77
98
  const isFile = fs.existsSync(f);
78
99
  if (!isFile) debug('File %s could not be found and uploaded as artifact', f);
79
100
  return isFile;
@@ -115,6 +136,7 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
115
136
  };
116
137
 
117
138
  export const TEST_ID_REGEX = /@T([\w\d]{8})/;
139
+ export const SUITE_ID_REGEX = /@S([\w\d]{8})/;
118
140
 
119
141
  const fetchIdFromCode = (code, opts = {}) => {
120
142
  const comments = code
@@ -390,4 +412,5 @@ export {
390
412
  specificTestInfo,
391
413
  storeRunId,
392
414
  testRunnerHelper,
415
+ validateSuiteId,
393
416
  };