@testomatio/reporter 2.9.3-beta.4-allure-links → 2.9.4

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.
@@ -681,8 +681,9 @@
681
681
 
682
682
  .test-group-header {
683
683
  padding: 20px;
684
- background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-hover) 100%);
685
- color: white;
684
+ background: var(--gray-50);
685
+ color: var(--gray-800);
686
+ border-left: 4px solid var(--primary-color);
686
687
  cursor: pointer;
687
688
  display: flex;
688
689
  justify-content: space-between;
@@ -691,7 +692,37 @@
691
692
  }
692
693
 
693
694
  .test-group-header:hover {
694
- filter: brightness(1.1);
695
+ background: var(--gray-100);
696
+ }
697
+
698
+ .test-group-header.has-failures {
699
+ background: #fef2f2;
700
+ color: var(--gray-900);
701
+ border-left-color: var(--danger-color);
702
+ }
703
+
704
+ .test-group-header.has-failures:hover {
705
+ background: #fee2e2;
706
+ }
707
+
708
+ .test-group-header.has-skips {
709
+ background: #fffbeb;
710
+ color: var(--gray-900);
711
+ border-left-color: var(--warning-color);
712
+ }
713
+
714
+ .test-group-header.has-skips:hover {
715
+ background: #fef3c7;
716
+ }
717
+
718
+ .test-group-header.all-passed {
719
+ background: #ecfdf5;
720
+ color: var(--gray-900);
721
+ border-left-color: var(--success-color);
722
+ }
723
+
724
+ .test-group-header.all-passed:hover {
725
+ background: #d1fae5;
695
726
  }
696
727
 
697
728
  .test-group-title {
@@ -707,28 +738,56 @@
707
738
  }
708
739
 
709
740
  .test-group-count {
710
- background: rgba(255, 255, 255, 0.2);
741
+ background: white;
742
+ color: var(--gray-600);
711
743
  padding: 6px 16px;
712
744
  border-radius: 20px;
713
745
  font-size: 14px;
714
746
  font-weight: 600;
747
+ border: 1px solid var(--gray-200);
715
748
  }
716
749
 
717
750
  .test-group-stats {
718
751
  display: flex;
719
- gap: 20px;
752
+ gap: 8px;
720
753
  font-size: 14px;
721
754
  margin-right: 10px;
755
+ flex-wrap: wrap;
756
+ justify-content: flex-end;
722
757
  }
723
758
 
724
759
  .test-group-stat {
725
760
  display: flex;
726
761
  align-items: center;
727
762
  gap: 6px;
763
+ padding: 6px 10px;
764
+ border-radius: 999px;
765
+ font-weight: 700;
766
+ background: white;
767
+ border: 1px solid currentColor;
728
768
  }
729
769
 
730
- .test-group-expand {
770
+ .test-group-stat.passed {
771
+ color: var(--success-color);
772
+ }
773
+
774
+ .test-group-stat.failed {
731
775
  color: white;
776
+ background: var(--danger-color);
777
+ border-color: var(--danger-color);
778
+ box-shadow: 0 1px 3px rgba(239, 68, 68, 0.35);
779
+ }
780
+
781
+ .test-group-stat.skipped {
782
+ color: var(--warning-color);
783
+ }
784
+
785
+ .test-group-stat.todo {
786
+ color: #8b5cf6;
787
+ }
788
+
789
+ .test-group-expand {
790
+ color: var(--gray-500);
732
791
  transition: var(--transition);
733
792
  font-size: 18px;
734
793
  }
@@ -3366,11 +3425,18 @@
3366
3425
  const failedCount = group.tests.filter(t => t.status.toLowerCase() === 'failed').length;
3367
3426
  const skippedCount = group.tests.filter(t => t.status.toLowerCase() === 'skipped').length;
3368
3427
  const todoCount = group.tests.filter(t => t.status.toLowerCase() === 'todo').length;
3428
+ const groupStateClass = failedCount > 0
3429
+ ? 'has-failures'
3430
+ : skippedCount > 0 || todoCount > 0
3431
+ ? 'has-skips'
3432
+ : passedCount > 0
3433
+ ? 'all-passed'
3434
+ : '';
3369
3435
 
3370
3436
  const groupIcon = group.type === 'suite' ? 'layer-group' : 'file-code';
3371
3437
 
3372
3438
  const header = document.createElement('div');
3373
- header.className = 'test-group-header';
3439
+ header.className = ['test-group-header', groupStateClass].filter(Boolean).join(' ');
3374
3440
  header.innerHTML = `
3375
3441
  <div class='test-group-title'>
3376
3442
  <i class='fas fa-${groupIcon}'></i>
@@ -3378,10 +3444,10 @@
3378
3444
  <div class='test-group-count'>${group.tests.length} tests</div>
3379
3445
  </div>
3380
3446
  <div class='test-group-stats'>
3381
- ${passedCount > 0 ? `<div class='test-group-stat' style='color: #10b981;'><i class='fas fa-check-circle'></i> ${passedCount}</div>` : ''}
3382
- ${failedCount > 0 ? `<div class='test-group-stat' style='color: #ef4444;'><i class='fas fa-times-circle'></i> ${failedCount}</div>` : ''}
3383
- ${skippedCount > 0 ? `<div class='test-group-stat' style='color: #f59e0b;'><i class='fas fa-forward'></i> ${skippedCount}</div>` : ''}
3384
- ${todoCount > 0 ? `<div class='test-group-stat' style='color: #8b5cf6;'><i class='fas fa-clock'></i> ${todoCount}</div>` : ''}
3447
+ ${passedCount > 0 ? `<div class='test-group-stat passed'><i class='fas fa-check-circle'></i> ${passedCount}</div>` : ''}
3448
+ ${failedCount > 0 ? `<div class='test-group-stat failed'><i class='fas fa-times-circle'></i> ${failedCount}</div>` : ''}
3449
+ ${skippedCount > 0 ? `<div class='test-group-stat skipped'><i class='fas fa-forward'></i> ${skippedCount}</div>` : ''}
3450
+ ${todoCount > 0 ? `<div class='test-group-stat todo'><i class='fas fa-clock'></i> ${todoCount}</div>` : ''}
3385
3451
  </div>
3386
3452
  <i class='fas fa-chevron-down test-group-expand'></i>
3387
3453
  `;
@@ -64,20 +64,3 @@ export function parsePipeOptions(optionsStr?: string): any;
64
64
  * @returns {string} Empty string if no ids; otherwise the formatted output.
65
65
  */
66
66
  export function formatFilterListIds(ids: string[], format: "grep" | "json" | "newline" | "ids"): string;
67
- /**
68
- * Calculate the approximate size of data in bytes (JSON stringified, UTF-8 encoded length).
69
- * @param {Object} data - Data to measure
70
- * @returns {number} Size in bytes
71
- */
72
- export function getObjectSize(data: any): number;
73
- /**
74
- * Split a tests array into chunks bounded by serialized size.
75
- * Used by the XML and Allure readers so both upload tests with identical batching:
76
- * each chunk becomes a single batch request (manual batch mode), which keeps requests
77
- * under the size limit and guarantees every test is sent exactly once.
78
- *
79
- * @param {Array} tests - Array of tests to split
80
- * @param {number} [maxSizeBytes=1048576] - Maximum serialized size per chunk (default 1MB)
81
- * @returns {Array<Array>} Array of test chunks
82
- */
83
- export function splitTestsIntoChunks(tests: any[], maxSizeBytes?: number): Array<any[]>;
@@ -8,8 +8,6 @@ exports.statusEmoji = statusEmoji;
8
8
  exports.fullName = fullName;
9
9
  exports.parsePipeOptions = parsePipeOptions;
10
10
  exports.formatFilterListIds = formatFilterListIds;
11
- exports.getObjectSize = getObjectSize;
12
- exports.splitTestsIntoChunks = splitTestsIntoChunks;
13
11
  const log_js_1 = require("./log.js");
14
12
  /**
15
13
  * Set S3 credentials from the provided artifacts object.
@@ -164,45 +162,6 @@ function parsePipeOptions(optionsStr) {
164
162
  }
165
163
  return options;
166
164
  }
167
- /**
168
- * Calculate the approximate size of data in bytes (JSON stringified, UTF-8 encoded length).
169
- * @param {Object} data - Data to measure
170
- * @returns {number} Size in bytes
171
- */
172
- function getObjectSize(data) {
173
- const body = JSON.stringify(data);
174
- return new TextEncoder().encode(body).length;
175
- }
176
- /**
177
- * Split a tests array into chunks bounded by serialized size.
178
- * Used by the XML and Allure readers so both upload tests with identical batching:
179
- * each chunk becomes a single batch request (manual batch mode), which keeps requests
180
- * under the size limit and guarantees every test is sent exactly once.
181
- *
182
- * @param {Array} tests - Array of tests to split
183
- * @param {number} [maxSizeBytes=1048576] - Maximum serialized size per chunk (default 1MB)
184
- * @returns {Array<Array>} Array of test chunks
185
- */
186
- function splitTestsIntoChunks(tests, maxSizeBytes = 1 * 1024 * 1024) {
187
- const chunks = [];
188
- let currentChunk = [];
189
- let currentChunkSize = 0;
190
- for (const test of tests) {
191
- const testSize = getObjectSize(test);
192
- const wouldExceedSize = currentChunkSize + testSize > maxSizeBytes;
193
- if (wouldExceedSize && currentChunk.length > 0) {
194
- chunks.push(currentChunk);
195
- currentChunk = [];
196
- currentChunkSize = 0;
197
- }
198
- currentChunk.push(test);
199
- currentChunkSize += testSize;
200
- }
201
- if (currentChunk.length > 0) {
202
- chunks.push(currentChunk);
203
- }
204
- return chunks;
205
- }
206
165
  /**
207
166
  * Format a list of test IDs for `--filter-list` machine-readable output.
208
167
  * Used when the CLI `--format` option is passed,
@@ -239,7 +198,3 @@ module.exports.fullName = fullName;
239
198
  module.exports.parsePipeOptions = parsePipeOptions;
240
199
 
241
200
  module.exports.formatFilterListIds = formatFilterListIds;
242
-
243
- module.exports.getObjectSize = getObjectSize;
244
-
245
- module.exports.splitTestsIntoChunks = splitTestsIntoChunks;
@@ -324,15 +324,6 @@ const fetchSourceCode = (contents, opts = {}) => {
324
324
  if (lineIndex === -1)
325
325
  lineIndex = lines.findIndex(l => l.includes(`${title}(`));
326
326
  }
327
- else if (opts.lang === 'kotlin') {
328
- lineIndex = lines.findIndex(l => l.includes(`fun test${title}`));
329
- if (lineIndex === -1)
330
- lineIndex = lines.findIndex(l => l.includes(`@DisplayName("${title}`));
331
- if (lineIndex === -1)
332
- lineIndex = lines.findIndex(l => l.includes(`fun ${title}`));
333
- if (lineIndex === -1)
334
- lineIndex = lines.findIndex(l => l.includes(`${title}(`));
335
- }
336
327
  else if (opts.lang === 'csharp') {
337
328
  // Find the method declaration line
338
329
  let methodLineIndex = lines.findIndex(l => l.includes(`public void ${title}(`));
@@ -93,6 +93,7 @@ declare class XmlReader {
93
93
  pipes: any;
94
94
  uploadData(): Promise<any[]>;
95
95
  _finishRun(): Promise<any[]>;
96
+ #private;
96
97
  }
97
98
  import { XMLParser } from 'fast-xml-parser';
98
99
  import { S3Uploader } from './uploader.js';
package/lib/xmlReader.js CHANGED
@@ -14,7 +14,6 @@ const url_1 = require("url");
14
14
  const nunit_parser_js_1 = require("./junit-adapter/nunit-parser.js");
15
15
  const utils_js_1 = require("./utils/utils.js");
16
16
  const index_js_1 = require("./pipe/index.js");
17
- const pipe_utils_js_1 = require("./utils/pipe_utils.js");
18
17
  const index_js_2 = __importDefault(require("./junit-adapter/index.js"));
19
18
  const config_js_1 = require("./config.js");
20
19
  const uploader_js_1 = require("./uploader.js");
@@ -475,6 +474,43 @@ class XmlReader {
475
474
  this.uploader.checkEnabled();
476
475
  return run;
477
476
  }
477
+ /**
478
+ * Calculate the approximate size of data in bytes (JSON stringified length)
479
+ * @param {Object} data - Data to measure
480
+ * @returns {number} Size in bytes
481
+ */
482
+ #getObjectSize(data) {
483
+ const body = JSON.stringify(data);
484
+ return new TextEncoder().encode(body).length;
485
+ }
486
+ /**
487
+ * Split tests array into chunks based on data size
488
+ * @param {Array} tests - Array of tests to split
489
+ * @returns {Array<Array>} Array of test chunks
490
+ */
491
+ #splitTestsIntoChunks(tests) {
492
+ const maxSizeBytes = 1 * 1024 * 1024;
493
+ const chunks = [];
494
+ let currentChunk = [];
495
+ let currentChunkSize = 0;
496
+ for (const test of tests) {
497
+ const testSize = this.#getObjectSize(test);
498
+ const wouldExceedSize = currentChunkSize + testSize > maxSizeBytes;
499
+ if (wouldExceedSize) {
500
+ if (currentChunk.length > 0) {
501
+ chunks.push(currentChunk);
502
+ }
503
+ currentChunk = [];
504
+ currentChunkSize = 0;
505
+ }
506
+ currentChunk.push(test);
507
+ currentChunkSize += testSize;
508
+ }
509
+ if (currentChunk.length > 0) {
510
+ chunks.push(currentChunk);
511
+ }
512
+ return chunks;
513
+ }
478
514
  async uploadData() {
479
515
  await this.uploadArtifacts();
480
516
  this.calculateStats();
@@ -495,7 +531,7 @@ class XmlReader {
495
531
  };
496
532
  return Promise.all(this.pipes.map(p => p.finishRun(finishData)));
497
533
  }
498
- const testChunks = (0, pipe_utils_js_1.splitTestsIntoChunks)(this.tests);
534
+ const testChunks = this.#splitTestsIntoChunks(this.tests);
499
535
  const totalChunks = testChunks.length;
500
536
  const totalTests = this.tests.length;
501
537
  debug(`Split ${totalTests} tests into ${totalChunks} chunks (max 1MB per chunk)`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.9.3-beta.4-allure-links",
3
+ "version": "2.9.4",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"
@@ -154,21 +154,40 @@ function CodeceptReporter(config) {
154
154
 
155
155
  // mark as failed all tests inside the failed hook
156
156
  event.dispatcher.on(event.hook.failed, hook => {
157
- if (hook.name !== 'BeforeSuiteHook' && hook.name !== 'BeforeHook') return;
158
- const suite = hook.runnable.parent;
157
+ const error = hook?.ctx?.currentTest?.err || hook?.err;
158
+
159
+ // Handle BeforeSuite and Before hooks: mark all tests in suite as failed
160
+ if (hook.name === 'BeforeSuiteHook' || hook.name === 'BeforeHook') {
161
+ const suite = hook.runnable.parent;
162
+ if (!suite) return;
163
+
164
+ for (const test of suite.tests) {
165
+ reportedTestUids.add(test.uid);
166
+ const reportTestPromise = client.addTestRun('failed', {
167
+ ...stripExampleFromTitle(test.title),
168
+ rid: test.uid,
169
+ test_id: getTestomatIdFromTestTitle(test.title),
170
+ suite_title: stripTagsFromTitle(suite.title),
171
+ error,
172
+ time: hook?.runnable?.duration,
173
+ });
174
+ reportTestPromises.push(reportTestPromise);
175
+ }
176
+ }
159
177
 
160
- if (!suite) return;
178
+ if (hook.name === 'AfterSuiteHook') {
179
+ const suite = hook.runnable?.parent || hook.suite;
180
+ const suiteTitle = suite ? stripTagsFromTitle(suite.title) : 'Unknown Suite';
161
181
 
162
- const error = hook?.ctx?.currentTest?.err;
182
+ const stepHierarchy = buildUnifiedStepHierarchy(null, hookSteps);
163
183
 
164
- for (const test of suite.tests) {
165
- reportedTestUids.add(test.uid);
184
+ // Report the hook failure as a separate test entry
166
185
  const reportTestPromise = client.addTestRun('failed', {
167
- ...stripExampleFromTitle(test.title),
168
- rid: test.uid,
169
- test_id: getTestomatIdFromTestTitle(test.title),
170
- suite_title: stripTagsFromTitle(suite.title),
186
+ title: 'AfterSuite Hook',
187
+ suite_title: suiteTitle,
171
188
  error,
189
+ message: error?.message || 'AfterSuite hook failed',
190
+ steps: stepHierarchy,
172
191
  time: hook?.runnable?.duration,
173
192
  });
174
193
  reportTestPromises.push(reportTestPromise);
@@ -194,8 +213,10 @@ function CodeceptReporter(config) {
194
213
  });
195
214
 
196
215
  event.dispatcher.on(event.all.after, () => {
197
- recorder.add('Finishing run', async () => {
198
- await finalizeRun('all.after');
216
+ setImmediate(() => {
217
+ finalizeRun('all.after').catch(err => {
218
+ debug('Error finalizing run:', err);
219
+ });
199
220
  });
200
221
  });
201
222
 
@@ -278,6 +299,8 @@ async function uploadAttachments(client, attachments, messagePrefix, attachmentT
278
299
  log.info(`Attachments: ${messagePrefix} ${attachments.length} ${attachmentType} ...`);
279
300
  }
280
301
 
302
+ await Promise.all(client.pipes?.map(pipe => pipe.addArtifacts?.(attachments)) || []);
303
+
281
304
  const promises = attachments.map(async attachment => {
282
305
  const { rid, title, path, type } = attachment;
283
306
  const file = { path, type, title };
@@ -48,14 +48,8 @@ class PlaywrightReporter {
48
48
  const { title } = test;
49
49
  const { error, duration } = result;
50
50
  const pwAttachments = (result.attachments || []).filter(a => a.body || a.path);
51
-
52
- const files = pwAttachments
53
- .map(att => ({
54
- path: this.#getArtifactPath(att),
55
- title: att.name || title,
56
- type: att.contentType,
57
- }))
58
- .filter(f => f.path);
51
+ const files = buildArtifactFiles(pwAttachments, attachment => this.#getArtifactPath(attachment), title);
52
+ const artifactsForUpload = processArtifactsForUpload(pwAttachments);
59
53
 
60
54
  const suite_title = test.parent ? test.parent?.title : path.basename(test?.location?.file);
61
55
 
@@ -150,7 +144,7 @@ class PlaywrightReporter {
150
144
  this.uploads.push({
151
145
  rid: `${rid}-${project.name}`,
152
146
  title: test.title,
153
- files: pwAttachments,
147
+ files: artifactsForUpload,
154
148
  file: test.location?.file,
155
149
  });
156
150
  // remove empty uploads
@@ -229,6 +223,25 @@ function checkStatus(status) {
229
223
  );
230
224
  }
231
225
 
226
+ function buildArtifactFiles(attachments, getArtifactPath, title) {
227
+ return attachments
228
+ .filter(isScreenshotArtifact)
229
+ .map(attachment => ({
230
+ path: getArtifactPath(attachment),
231
+ title: attachment.name || title,
232
+ type: attachment.contentType,
233
+ }))
234
+ .filter(file => file.path);
235
+ }
236
+
237
+ function processArtifactsForUpload(attachments) {
238
+ return attachments.filter(attachment => !isScreenshotArtifact(attachment));
239
+ }
240
+
241
+ function isScreenshotArtifact(attachment) {
242
+ return attachment?.contentType === 'image/png' && attachment?.name === 'screenshot';
243
+ }
244
+
232
245
  function appendStep(step, shift = 0) {
233
246
  // nesting too deep, ignore those steps
234
247
  if (shift >= 10) return;
package/src/bin/cli.js CHANGED
@@ -6,7 +6,6 @@ import { glob } from 'glob';
6
6
  import createDebugMessages from 'debug';
7
7
  import TestomatClient from '../client.js';
8
8
  import XmlReader from '../xmlReader.js';
9
- import AllureReader from '../allureReader.js';
10
9
  import { APP_PREFIX, STATUS, DEBUG_FILE, BATCH_MODE } from '../constants.js';
11
10
  import { cleanLatestRunId, getPackageVersion, applyFilter } from '../utils/utils.js';
12
11
  import { config } from '../config.js';
@@ -17,6 +16,8 @@ import dotenv from 'dotenv';
17
16
  import Replay from '../replay.js';
18
17
  import { log } from '../utils/log.js';
19
18
  import { formatFilterListIds } from '../utils/pipe_utils.js';
19
+ import fs from 'fs';
20
+ import path from 'path';
20
21
 
21
22
  const debug = createDebugMessages('@testomatio/reporter:cli');
22
23
  const version = getPackageVersion();
@@ -353,53 +354,61 @@ program
353
354
  });
354
355
 
355
356
  program
356
- .command('allure')
357
- .description('Parse Allure result files and upload to Testomat.io')
358
- .argument('<pattern>', 'Allure result directory pattern')
359
- .option('-d, --dir <dir>', 'Project directory')
360
- .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
361
- .option('--with-package', 'Keep full package path in file names (default: strip package prefix)')
362
- .option(
363
- '--java-tests [path]',
364
- 'Path to test sources; links cases from @TmsLink in source for skipped tests (default: src/test)',
365
- )
366
- .option('--lang <lang>', 'Language used (java, kotlin, ...)')
367
- .action(async (pattern, opts) => {
368
- let javaTests = opts.javaTests;
369
- if (javaTests === true) javaTests = 'src/test';
357
+ .command('upload-artifacts')
358
+ .description('Upload artifacts to Testomat.io')
359
+ .argument('[jsonl-file]', 'Path to JSONL debug file (optional)')
360
+ .option('--force', 'Re-upload artifacts even if they were uploaded before')
361
+ .action(async (jsonlFile, opts) => {
362
+ const apiKey = config.TESTOMATIO;
370
363
 
371
- const runReader = new AllureReader({ withPackage: opts.withPackage, javaTests, lang: opts.lang });
364
+ // JSONL file mode: upload artifacts from debug file
365
+ if (jsonlFile) {
366
+ if (!fs.existsSync(jsonlFile)) {
367
+ log.error(`JSONL file not found: ${jsonlFile}`);
368
+ return process.exit(1);
369
+ }
372
370
 
373
- let timeoutTimer;
374
- if (opts.timelimit) {
375
- timeoutTimer = setTimeout(
376
- () => {
377
- console.log(
378
- `⚠️ Reached timeout of ${opts.timelimit}s. Exiting... (Exit code is 0 to not fail the pipeline)`,
379
- );
380
- process.exit(0);
381
- },
382
- parseInt(opts.timelimit, 10) * 1000,
383
- );
384
- }
371
+ const replay = new Replay({ apiKey });
372
+ const { tests, runId } = replay.parseDebugFile(jsonlFile);
385
373
 
386
- try {
387
- await runReader.parse(pattern);
388
- await runReader.createRun();
389
- await runReader.uploadData();
390
- } catch (err) {
391
- console.log(APP_PREFIX, 'Error uploading Allure results:', err);
392
- }
374
+ if (!runId) {
375
+ log.error('runId not found in JSONL file');
376
+ return process.exit(1);
377
+ }
393
378
 
394
- if (timeoutTimer) clearTimeout(timeoutTimer);
395
- });
379
+ log.info(`Processing ${tests.length} tests from ${jsonlFile}`);
396
380
 
397
- program
398
- .command('upload-artifacts')
399
- .description('Upload artifacts to Testomat.io')
400
- .option('--force', 'Re-upload artifacts even if they were uploaded before')
401
- .action(async opts => {
402
- const apiKey = config.TESTOMATIO;
381
+ const client = new TestomatClient({
382
+ apiKey,
383
+ runId,
384
+ batchMode: BATCH_MODE.DISABLED,
385
+ });
386
+
387
+ await client.createRun();
388
+ client.uploader.checkEnabled();
389
+ client.uploader.disableLogStorage();
390
+
391
+ let uploadedCount = 0;
392
+ let failedCount = 0;
393
+
394
+ for (const test of tests) {
395
+ if (!hasExistingArtifacts(test)) continue;
396
+
397
+ try {
398
+ await client.addTestRun(undefined, { ...test, overwrite: true });
399
+ uploadedCount++;
400
+ } catch (e) {
401
+ log.error(`Failed ${test.test_id || test.rid || test.title}: ${e.message}`);
402
+ failedCount++;
403
+ }
404
+ }
405
+
406
+ log.info(`🗄️ ${uploadedCount} tests with artifacts uploaded`);
407
+ if (failedCount > 0) {
408
+ log.warn(`⚠️ ${failedCount} tests failed to upload artifacts`);
409
+ }
410
+ return;
411
+ }
403
412
 
404
413
  process.env.TESTOMATIO_DISABLE_ARTIFACTS = '';
405
414
  const runId = process.env.TESTOMATIO_RUN || process.env.runId || readLatestRunId();
@@ -543,6 +552,28 @@ program
543
552
  }
544
553
  });
545
554
 
555
+ function hasExistingArtifacts(test) {
556
+ const hasExistingFile = items => {
557
+ for (const item of items || []) {
558
+ const artifactPath = typeof item === 'object' ? item?.path : item;
559
+ if (artifactPath && fs.existsSync(artifactPath)) return true;
560
+ }
561
+ return false;
562
+ };
563
+
564
+ if (hasExistingFile(test.files)) return true;
565
+
566
+ const stack = [...(test.steps || [])];
567
+ while (stack.length) {
568
+ const step = stack.pop();
569
+ if (!step) continue;
570
+ if (hasExistingFile(step.artifacts)) return true;
571
+ if (Array.isArray(step.steps)) stack.push(...step.steps);
572
+ }
573
+
574
+ return false;
575
+ }
576
+
546
577
  program.parse(process.argv);
547
578
 
548
579
  if (!process.argv.slice(2).length) {
package/src/client.js CHANGED
@@ -159,11 +159,12 @@ class Client {
159
159
 
160
160
  const uploadedArtifacts = [];
161
161
  for (const artifact of step.artifacts) {
162
- if (typeof artifact === 'string' && !isHttpUrl(artifact)) {
163
- const filename = generateShortFilename(artifact);
162
+ const artifactPath = typeof artifact === 'object' ? artifact?.path : artifact;
163
+ if (typeof artifactPath === 'string' && !isHttpUrl(artifactPath)) {
164
+ const filename = generateShortFilename(artifactPath);
164
165
  try {
165
166
  const uploadResult = await this.uploader.uploadFileByPath(
166
- artifact,
167
+ artifactPath,
167
168
  [this.runId, testRid, 'steps', filename]
168
169
  );
169
170
  if (uploadResult) {
@@ -4,7 +4,6 @@ import JavaAdapter from './java.js';
4
4
  import PythonAdapter from './python.js';
5
5
  import RubyAdapter from './ruby.js';
6
6
  import CSharpAdapter from './csharp.js';
7
- import KotlinAdapter from './kotlin.js';
8
7
 
9
8
  function AdapterFactory(lang, opts) {
10
9
  if (lang === 'java') {
@@ -22,9 +21,6 @@ function AdapterFactory(lang, opts) {
22
21
  if (lang === 'c#' || lang === 'csharp') {
23
22
  return new CSharpAdapter(opts);
24
23
  }
25
- if (lang === 'kotlin') {
26
- return new KotlinAdapter(opts);
27
- }
28
24
 
29
25
  return new Adapter(opts);
30
26
  }
package/src/pipe/debug.js CHANGED
@@ -111,6 +111,17 @@ export class DebugPipe {
111
111
  log.info(`History: ${this.historyDir}`);
112
112
  }
113
113
 
114
+ /**
115
+ * Logs artifacts data to the debug file.
116
+ * Used for trace.zip, video files and other artifacts uploaded after tests finish.
117
+ * @param {Array} artifacts - Array of artifacts with { rid, title, path, type }
118
+ */
119
+ addArtifacts(artifacts) {
120
+ if (!this.isEnabled || !artifacts?.length) return;
121
+ const logData = { action: 'addArtifacts', artifacts, runId: this.store.runId };
122
+ this.logToFile(logData);
123
+ }
124
+
114
125
  async sync() {
115
126
  this.flushBufferedTests();
116
127
  }