wdio-test-ledger-service 9.0.2 → 9.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +285 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wdio-test-ledger-service",
3
- "version": "9.0.2",
3
+ "version": "9.2.0",
4
4
  "description": "WebdriverIO server to send reporter data to testledger.dev",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/index.js CHANGED
@@ -5,6 +5,18 @@ import { SevereServiceError } from 'webdriverio';
5
5
 
6
6
  const api_url = `https://app-api.testledger.dev`;
7
7
 
8
+ // MIME type mapping for artifacts
9
+ const MIME_TYPES = {
10
+ '.png' : 'image/png',
11
+ '.jpg' : 'image/jpeg',
12
+ '.jpeg' : 'image/jpeg',
13
+ '.gif' : 'image/gif',
14
+ '.webp' : 'image/webp',
15
+ '.webm' : 'video/webm',
16
+ '.mp4' : 'video/mp4',
17
+ '.mov' : 'video/quicktime'
18
+ };
19
+
8
20
  class TestLedgerLauncher {
9
21
  constructor(options) {
10
22
  this.options = options;
@@ -20,6 +32,11 @@ class TestLedgerLauncher {
20
32
  if(!this.options.apiToken) {
21
33
  throw new SevereServiceError(`No apiToken specified`)
22
34
  }
35
+
36
+ // Artifact upload options
37
+ this.upload_artifacts = this.options.upload_artifacts || false;
38
+ this.screenshot_dir = this.options.screenshot_dir || null;
39
+ this.video_dir = this.options.video_dir || null;
23
40
  }
24
41
 
25
42
  onPrepare() {
@@ -34,8 +51,15 @@ class TestLedgerLauncher {
34
51
  const data = this.buildData(config);
35
52
 
36
53
  try {
37
- const tmp = await this.post(data);
54
+ const response = await this.post(data);
55
+ const result = await response.json();
56
+
38
57
  fs.writeFileSync(`${this.options.reporterOutputDir}/trio-onComplete-post.txt`, `onComplete-post`, { encoding : `utf-8` });
58
+
59
+ // Upload artifacts if enabled
60
+ if(this.upload_artifacts && result.status === 'success') {
61
+ await this.upload_all_artifacts(data, result);
62
+ }
39
63
  }
40
64
  catch(e) {
41
65
  fs.writeFileSync(`${this.options.reporterOutputDir}/trio-post-error.txt`, e.message, { encoding : `utf-8` });
@@ -200,6 +224,266 @@ class TestLedgerLauncher {
200
224
  this.options.apiToken,
201
225
  ].join(`:`));
202
226
  }
227
+
228
+ /**
229
+ * Upload all artifacts (screenshots and videos) after test run is posted
230
+ */
231
+ async upload_all_artifacts(data, run_result) {
232
+ const artifacts = this.collect_artifacts(data, run_result);
233
+
234
+ if(artifacts.length === 0) {
235
+ fs.writeFileSync(`${this.options.reporterOutputDir}/trio-no-artifacts.txt`, `No artifacts found to upload`, { encoding : `utf-8` });
236
+ return;
237
+ }
238
+
239
+ fs.writeFileSync(`${this.options.reporterOutputDir}/trio-artifacts-found.txt`, `Found ${artifacts.length} artifacts`, { encoding : `utf-8` });
240
+
241
+ try {
242
+ // Request presigned URLs
243
+ const presigned_response = await this.request_presigned_urls(artifacts);
244
+
245
+ if(!presigned_response.uploads || presigned_response.uploads.length === 0) {
246
+ fs.writeFileSync(`${this.options.reporterOutputDir}/trio-presigned-empty.txt`, `No presigned URLs returned`, { encoding : `utf-8` });
247
+ return;
248
+ }
249
+
250
+ // Upload each artifact to S3
251
+ const upload_results = await this.upload_to_s3(presigned_response.uploads, artifacts);
252
+
253
+ // Confirm successful uploads
254
+ const confirmed_ids = upload_results
255
+ .filter(r => r.success)
256
+ .map(r => r.artifact_id);
257
+
258
+ if(confirmed_ids.length > 0) {
259
+ await this.confirm_uploads(confirmed_ids);
260
+ }
261
+
262
+ fs.writeFileSync(`${this.options.reporterOutputDir}/trio-artifacts-complete.txt`, `Uploaded ${confirmed_ids.length}/${artifacts.length} artifacts`, { encoding : `utf-8` });
263
+ }
264
+ catch(e) {
265
+ fs.writeFileSync(`${this.options.reporterOutputDir}/trio-artifacts-error.txt`, e.message, { encoding : `utf-8` });
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Collect all artifact files and match them to suites/tests
271
+ */
272
+ collect_artifacts(data, run_result) {
273
+ const artifacts = [];
274
+
275
+ // Build lookup maps from run result
276
+ const suite_map = {};
277
+ for(const suite of run_result.suites) {
278
+ suite_map[suite.suite_key] = suite.id;
279
+ }
280
+
281
+ const test_map = {};
282
+ for(const test of run_result.tests) {
283
+ test_map[test.suite_test_key] = {
284
+ id : test.id,
285
+ test_run_suite_id : test.test_run_suite_id
286
+ };
287
+ }
288
+
289
+ // Collect screenshots
290
+ if(this.screenshot_dir && fs.existsSync(this.screenshot_dir)) {
291
+ const screenshot_files = this.find_files(this.screenshot_dir, ['.png', '.jpg', '.jpeg', '.gif', '.webp']);
292
+
293
+ for(const file_path of screenshot_files) {
294
+ const filename = path.basename(file_path);
295
+ const matched_suite = this.match_file_to_suite(filename, data.suites, suite_map);
296
+
297
+ if(matched_suite) {
298
+ artifacts.push({
299
+ type : 'screenshot',
300
+ filename : filename,
301
+ path : file_path,
302
+ mime_type : MIME_TYPES[path.extname(file_path).toLowerCase()] || 'image/png',
303
+ file_size : fs.statSync(file_path).size,
304
+ test_run_suite_id : matched_suite.suite_id,
305
+ test_run_suite_test_id : matched_suite.test_id || null
306
+ });
307
+ }
308
+ }
309
+ }
310
+
311
+ // Collect videos
312
+ if(this.video_dir && fs.existsSync(this.video_dir)) {
313
+ const video_files = this.find_files(this.video_dir, ['.webm', '.mp4', '.mov']);
314
+
315
+ for(const file_path of video_files) {
316
+ const filename = path.basename(file_path);
317
+ const matched_suite = this.match_file_to_suite(filename, data.suites, suite_map);
318
+
319
+ if(matched_suite) {
320
+ artifacts.push({
321
+ type : 'video',
322
+ filename : filename,
323
+ path : file_path,
324
+ mime_type : MIME_TYPES[path.extname(file_path).toLowerCase()] || 'video/webm',
325
+ file_size : fs.statSync(file_path).size,
326
+ test_run_suite_id : matched_suite.suite_id,
327
+ test_run_suite_test_id : matched_suite.test_id || null
328
+ });
329
+ }
330
+ }
331
+ }
332
+
333
+ return artifacts;
334
+ }
335
+
336
+ /**
337
+ * Find all files with given extensions in a directory (recursive)
338
+ */
339
+ find_files(dir, extensions) {
340
+ const files = [];
341
+
342
+ const items = fs.readdirSync(dir, { withFileTypes: true });
343
+ for(const item of items) {
344
+ const full_path = path.join(dir, item.name);
345
+
346
+ if(item.isDirectory()) {
347
+ files.push(...this.find_files(full_path, extensions));
348
+ }
349
+ else if(extensions.includes(path.extname(item.name).toLowerCase())) {
350
+ files.push(full_path);
351
+ }
352
+ }
353
+
354
+ return files;
355
+ }
356
+
357
+ /**
358
+ * Match an artifact filename to a suite based on spec file name
359
+ */
360
+ match_file_to_suite(filename, suites, suite_map) {
361
+ const lower_filename = filename.toLowerCase();
362
+
363
+ for(const suite of suites) {
364
+ // Extract spec file name without extension
365
+ const spec_base = path.basename(suite.spec_file, path.extname(suite.spec_file)).toLowerCase();
366
+
367
+ // Check if the artifact filename contains the spec name
368
+ if(lower_filename.includes(spec_base)) {
369
+ const suite_key = `${suite.title}:${suite.spec_file}:${suite.capabilities}`;
370
+ const suite_id = suite_map[suite_key];
371
+
372
+ if(suite_id) {
373
+ return {
374
+ suite_id : suite_id,
375
+ test_id : null // Could enhance to match specific tests
376
+ };
377
+ }
378
+ }
379
+ }
380
+
381
+ // If no match found, return the first suite as fallback
382
+ if(suites.length > 0) {
383
+ const suite = suites[0];
384
+ const suite_key = `${suite.title}:${suite.spec_file}:${suite.capabilities}`;
385
+ const suite_id = suite_map[suite_key];
386
+
387
+ if(suite_id) {
388
+ return {
389
+ suite_id : suite_id,
390
+ test_id : null
391
+ };
392
+ }
393
+ }
394
+
395
+ return null;
396
+ }
397
+
398
+ /**
399
+ * Request presigned URLs from Test Ledger API
400
+ */
401
+ async request_presigned_urls(artifacts) {
402
+ const payload = {
403
+ artifacts: artifacts.map(a => ({
404
+ test_run_suite_test_id : a.test_run_suite_test_id,
405
+ test_run_suite_id : a.test_run_suite_id,
406
+ artifact_type : a.type,
407
+ filename : a.filename,
408
+ mime_type : a.mime_type,
409
+ file_size : a.file_size
410
+ }))
411
+ };
412
+
413
+ const response = await fetch(`${this.getApiUrl()}/artifacts/presigned-upload`, {
414
+ method : 'POST',
415
+ headers : {
416
+ 'Content-Type' : 'application/json',
417
+ 'Authorization' : `Basic ${this.getAuthToken()}`
418
+ },
419
+ body : JSON.stringify(payload)
420
+ });
421
+
422
+ return response.json();
423
+ }
424
+
425
+ /**
426
+ * Upload artifacts to S3 using presigned URLs
427
+ */
428
+ async upload_to_s3(uploads, artifacts) {
429
+ const results = [];
430
+
431
+ for(let i = 0; i < uploads.length; i++) {
432
+ const upload = uploads[i];
433
+ const artifact = artifacts[i];
434
+
435
+ try {
436
+ const file_buffer = fs.readFileSync(artifact.path);
437
+
438
+ const response = await fetch(upload.presigned_url, {
439
+ method : 'PUT',
440
+ headers : {
441
+ 'Content-Type' : artifact.mime_type
442
+ },
443
+ body : file_buffer
444
+ });
445
+
446
+ if(response.ok) {
447
+ results.push({
448
+ artifact_id : upload.artifact_id,
449
+ success : true
450
+ });
451
+ }
452
+ else {
453
+ results.push({
454
+ artifact_id : upload.artifact_id,
455
+ success : false,
456
+ error : `HTTP ${response.status}`
457
+ });
458
+ }
459
+ }
460
+ catch(e) {
461
+ results.push({
462
+ artifact_id : upload.artifact_id,
463
+ success : false,
464
+ error : e.message
465
+ });
466
+ }
467
+ }
468
+
469
+ return results;
470
+ }
471
+
472
+ /**
473
+ * Confirm successful uploads with Test Ledger API
474
+ */
475
+ async confirm_uploads(artifact_ids) {
476
+ const response = await fetch(`${this.getApiUrl()}/artifacts/confirm`, {
477
+ method : 'POST',
478
+ headers : {
479
+ 'Content-Type' : 'application/json',
480
+ 'Authorization' : `Basic ${this.getAuthToken()}`
481
+ },
482
+ body : JSON.stringify({ artifact_ids: artifact_ids })
483
+ });
484
+
485
+ return response.json();
486
+ }
203
487
  }
204
488
 
205
489
  export default class TestReporterService {};