@supatest/playwright-reporter 0.0.2 → 0.0.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.
package/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ ErrorCollector: () => ErrorCollector,
33
34
  default: () => SupatestPlaywrightReporter
34
35
  });
35
36
  module.exports = __toCommonJS(index_exports);
@@ -173,6 +174,117 @@ var SupatestApiClient = class {
173
174
  }
174
175
  };
175
176
 
177
+ // src/error-collector.ts
178
+ var ErrorCollector = class {
179
+ errors = [];
180
+ recordError(category, message, context) {
181
+ const error = {
182
+ category,
183
+ message,
184
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
185
+ testId: context?.testId,
186
+ testTitle: context?.testTitle,
187
+ attachmentName: context?.attachmentName,
188
+ filePath: context?.filePath,
189
+ originalError: context?.error instanceof Error ? context.error.stack : void 0
190
+ };
191
+ this.errors.push(error);
192
+ }
193
+ getSummary() {
194
+ const byCategory = {
195
+ RUN_CREATE: 0,
196
+ TEST_SUBMISSION: 0,
197
+ ATTACHMENT_SIGN: 0,
198
+ ATTACHMENT_UPLOAD: 0,
199
+ FILE_READ: 0,
200
+ RUN_COMPLETE: 0
201
+ };
202
+ for (const error of this.errors) {
203
+ byCategory[error.category]++;
204
+ }
205
+ return {
206
+ totalErrors: this.errors.length,
207
+ byCategory,
208
+ errors: this.errors
209
+ };
210
+ }
211
+ hasErrors() {
212
+ return this.errors.length > 0;
213
+ }
214
+ formatSummary() {
215
+ if (this.errors.length === 0) {
216
+ return "";
217
+ }
218
+ const summary = this.getSummary();
219
+ const lines = [
220
+ "",
221
+ "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
222
+ "\u26A0\uFE0F Supatest Reporter - Error Summary",
223
+ "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501",
224
+ `Total Errors: ${summary.totalErrors}`,
225
+ ""
226
+ ];
227
+ for (const [category, count] of Object.entries(summary.byCategory)) {
228
+ if (count === 0) continue;
229
+ const icon = this.getCategoryIcon(category);
230
+ const label = this.getCategoryLabel(category);
231
+ lines.push(`${icon} ${label}: ${count}`);
232
+ const categoryErrors = this.errors.filter((e) => e.category === category).slice(0, 3);
233
+ for (const error of categoryErrors) {
234
+ lines.push(` \u2022 ${this.formatError(error)}`);
235
+ }
236
+ const remaining = count - categoryErrors.length;
237
+ if (remaining > 0) {
238
+ lines.push(` ... and ${remaining} more`);
239
+ }
240
+ lines.push("");
241
+ }
242
+ lines.push(
243
+ "\u2139\uFE0F Note: Test execution was not interrupted. Some results may not be visible in the dashboard."
244
+ );
245
+ lines.push(
246
+ "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501"
247
+ );
248
+ return lines.join("\n");
249
+ }
250
+ getCategoryIcon(category) {
251
+ const icons = {
252
+ RUN_CREATE: "\u{1F680}",
253
+ TEST_SUBMISSION: "\u{1F4DD}",
254
+ ATTACHMENT_SIGN: "\u{1F510}",
255
+ ATTACHMENT_UPLOAD: "\u{1F4CE}",
256
+ FILE_READ: "\u{1F4C1}",
257
+ RUN_COMPLETE: "\u{1F3C1}"
258
+ };
259
+ return icons[category];
260
+ }
261
+ getCategoryLabel(category) {
262
+ const labels = {
263
+ RUN_CREATE: "Run Initialization",
264
+ TEST_SUBMISSION: "Test Result Submission",
265
+ ATTACHMENT_SIGN: "Attachment Signing",
266
+ ATTACHMENT_UPLOAD: "Attachment Upload",
267
+ FILE_READ: "File Access",
268
+ RUN_COMPLETE: "Run Completion"
269
+ };
270
+ return labels[category];
271
+ }
272
+ formatError(error) {
273
+ const parts = [];
274
+ if (error.testTitle) {
275
+ parts.push(`Test: "${error.testTitle}"`);
276
+ }
277
+ if (error.attachmentName) {
278
+ parts.push(`Attachment: "${error.attachmentName}"`);
279
+ }
280
+ if (error.filePath) {
281
+ parts.push(`File: ${error.filePath}`);
282
+ }
283
+ parts.push(error.message);
284
+ return parts.join(" - ");
285
+ }
286
+ };
287
+
176
288
  // src/git-utils.ts
177
289
  var import_simple_git = require("simple-git");
178
290
  async function getLocalGitInfo(rootDir) {
@@ -241,13 +353,26 @@ var AttachmentUploader = class {
241
353
  }
242
354
  async upload(signedUrl, filePath, contentType) {
243
355
  if (this.options.dryRun) {
244
- const stats = import_node_fs.default.statSync(filePath);
245
- console.log(
246
- `[supatest][dry-run] Would upload ${filePath} (${stats.size} bytes) to S3`
247
- );
356
+ try {
357
+ const stats = import_node_fs.default.statSync(filePath);
358
+ console.log(
359
+ `[supatest][dry-run] Would upload ${filePath} (${stats.size} bytes) to S3`
360
+ );
361
+ } catch (error) {
362
+ const message = error instanceof Error ? error.message : String(error);
363
+ console.warn(
364
+ `[supatest][dry-run] Cannot access file ${filePath}: ${message}`
365
+ );
366
+ }
248
367
  return;
249
368
  }
250
- const fileBuffer = await import_node_fs.default.promises.readFile(filePath);
369
+ let fileBuffer;
370
+ try {
371
+ fileBuffer = await import_node_fs.default.promises.readFile(filePath);
372
+ } catch (error) {
373
+ const message = error instanceof Error ? error.message : String(error);
374
+ throw new Error(`Failed to read file ${filePath}: ${message}`);
375
+ }
251
376
  await withRetry(async () => {
252
377
  const controller = new AbortController();
253
378
  const timeoutId = setTimeout(
@@ -261,7 +386,7 @@ var AttachmentUploader = class {
261
386
  "Content-Type": contentType,
262
387
  "Content-Length": String(fileBuffer.length)
263
388
  },
264
- body: fileBuffer,
389
+ body: new Uint8Array(fileBuffer),
265
390
  signal: controller.signal
266
391
  });
267
392
  if (!response.ok) {
@@ -282,11 +407,20 @@ var AttachmentUploader = class {
282
407
  const results = await Promise.allSettled(
283
408
  items.map(
284
409
  (item, index) => limit(async () => {
285
- await this.upload(item.signedUrl, item.filePath, item.contentType);
286
- return {
287
- success: true,
288
- attachmentId: signedUploads[index]?.attachmentId
289
- };
410
+ try {
411
+ await this.upload(item.signedUrl, item.filePath, item.contentType);
412
+ return {
413
+ success: true,
414
+ attachmentId: signedUploads[index]?.attachmentId
415
+ };
416
+ } catch (error) {
417
+ const message = error instanceof Error ? error.message : String(error);
418
+ return {
419
+ success: false,
420
+ attachmentId: signedUploads[index]?.attachmentId,
421
+ error: `${item.filePath}: ${message}`
422
+ };
423
+ }
290
424
  })
291
425
  )
292
426
  );
@@ -294,17 +428,18 @@ var AttachmentUploader = class {
294
428
  if (result.status === "fulfilled") {
295
429
  return result.value;
296
430
  }
431
+ const message = result.reason instanceof Error ? result.reason.message : String(result.reason);
297
432
  return {
298
433
  success: false,
299
434
  attachmentId: signedUploads[index]?.attachmentId,
300
- error: result.reason instanceof Error ? result.reason.message : String(result.reason)
435
+ error: message
301
436
  };
302
437
  });
303
438
  }
304
439
  };
305
440
 
306
441
  // src/index.ts
307
- var DEFAULT_API_URL = "https://api.supatest.dev";
442
+ var DEFAULT_API_URL = "https://code-api.supatest.ai";
308
443
  var DEFAULT_MAX_CONCURRENT_UPLOADS = 5;
309
444
  var DEFAULT_RETRY_ATTEMPTS = 3;
310
445
  var DEFAULT_TIMEOUT_MS = 3e4;
@@ -314,6 +449,7 @@ var SupatestPlaywrightReporter = class {
314
449
  options;
315
450
  client;
316
451
  uploader;
452
+ errorCollector = new ErrorCollector();
317
453
  runId;
318
454
  uploadQueue = [];
319
455
  uploadLimit;
@@ -414,7 +550,8 @@ var SupatestPlaywrightReporter = class {
414
550
  `Run ${this.runId} started (${allTests.length} tests across ${testFiles.size} files, ${projects.length} projects)`
415
551
  );
416
552
  } catch (error) {
417
- this.logWarn(`Failed to create run: ${this.getErrorMessage(error)}`);
553
+ const errorMsg = this.getErrorMessage(error);
554
+ this.errorCollector.recordError("RUN_CREATE", errorMsg, { error });
418
555
  this.disabled = true;
419
556
  }
420
557
  }
@@ -433,15 +570,14 @@ var SupatestPlaywrightReporter = class {
433
570
  }
434
571
  async onEnd(result) {
435
572
  if (this.disabled || !this.runId) {
573
+ if (this.disabled && this.errorCollector.hasErrors()) {
574
+ console.log(this.errorCollector.formatSummary());
575
+ }
436
576
  this.logInfo("Reporter disabled, skipping onEnd");
437
577
  return;
438
578
  }
439
579
  this.logInfo(`Waiting for ${this.uploadQueue.length} pending uploads...`);
440
- const results = await Promise.allSettled(this.uploadQueue);
441
- const failures = results.filter((r) => r.status === "rejected");
442
- if (failures.length > 0) {
443
- this.logWarn(`${failures.length} uploads failed`);
444
- }
580
+ await Promise.allSettled(this.uploadQueue);
445
581
  const summary = this.computeSummary(result);
446
582
  const endedAt = (/* @__PURE__ */ new Date()).toISOString();
447
583
  const startTime = this.startedAt ? new Date(this.startedAt).getTime() : 0;
@@ -458,12 +594,16 @@ var SupatestPlaywrightReporter = class {
458
594
  });
459
595
  this.logInfo(`Run ${this.runId} completed: ${result.status}`);
460
596
  } catch (error) {
461
- this.logWarn(`Failed to complete run: ${this.getErrorMessage(error)}`);
597
+ const errorMsg = this.getErrorMessage(error);
598
+ this.errorCollector.recordError("RUN_COMPLETE", errorMsg, { error });
599
+ }
600
+ if (this.errorCollector.hasErrors()) {
601
+ console.log(this.errorCollector.formatSummary());
462
602
  }
463
603
  }
464
604
  async processTestResult(test, result) {
465
605
  const testId = this.getTestId(test);
466
- const resultId = this.getResultId(testId, result.retry);
606
+ const resultId = this.getResultId(testId, result.retry, result.workerIndex, result.parallelIndex);
467
607
  try {
468
608
  const payload = this.buildTestPayload(test, result);
469
609
  await this.client.submitTest(this.runId, payload);
@@ -478,10 +618,12 @@ var SupatestPlaywrightReporter = class {
478
618
  retries: (existing?.retries ?? 0) + (result.retry > 0 ? 1 : 0)
479
619
  });
480
620
  } catch (error) {
481
- this.logWarn(
482
- `Failed to submit test ${test.title}: ${this.getErrorMessage(error)}`
483
- );
484
- throw error;
621
+ const errorMsg = this.getErrorMessage(error);
622
+ this.errorCollector.recordError("TEST_SUBMISSION", errorMsg, {
623
+ testId,
624
+ testTitle: test.title,
625
+ error
626
+ });
485
627
  }
486
628
  }
487
629
  buildTestPayload(test, result) {
@@ -489,7 +631,7 @@ var SupatestPlaywrightReporter = class {
489
631
  const tags = test.tags ?? [];
490
632
  const annotations = this.serializeAnnotations(test.annotations ?? []);
491
633
  const resultEntry = {
492
- resultId: this.getResultId(testId, result.retry),
634
+ resultId: this.getResultId(testId, result.retry, result.workerIndex, result.parallelIndex),
493
635
  retry: result.retry,
494
636
  status: result.status,
495
637
  startTime: result.startTime?.toISOString?.() ?? void 0,
@@ -606,7 +748,24 @@ var SupatestPlaywrightReporter = class {
606
748
  async uploadAttachments(result, testResultId) {
607
749
  const attachments = (result.attachments ?? []).filter((a) => a.path);
608
750
  if (attachments.length === 0) return;
609
- const meta = attachments.map((a) => ({
751
+ const validAttachments = [];
752
+ for (const attachment of attachments) {
753
+ try {
754
+ await import_node_fs2.default.promises.access(attachment.path, import_node_fs2.default.constants.R_OK);
755
+ validAttachments.push(attachment);
756
+ } catch (error) {
757
+ const errorMsg = `File not accessible: ${attachment.path}`;
758
+ this.errorCollector.recordError("FILE_READ", errorMsg, {
759
+ attachmentName: attachment.name,
760
+ filePath: attachment.path,
761
+ error
762
+ });
763
+ }
764
+ }
765
+ if (validAttachments.length === 0) {
766
+ return;
767
+ }
768
+ const meta = validAttachments.map((a) => ({
610
769
  name: a.name,
611
770
  filename: import_node_path.default.basename(a.path),
612
771
  contentType: a.contentType,
@@ -620,18 +779,30 @@ var SupatestPlaywrightReporter = class {
620
779
  });
621
780
  const uploadItems = uploads.map((u, i) => ({
622
781
  signedUrl: u.signedUrl,
623
- filePath: attachments[i].path,
624
- contentType: attachments[i].contentType
782
+ filePath: validAttachments[i].path,
783
+ contentType: validAttachments[i].contentType
625
784
  }));
626
785
  const results = await this.uploader.uploadBatch(uploadItems, uploads);
627
- const failedCount = results.filter((r) => !r.success).length;
628
- if (failedCount > 0) {
629
- this.logWarn(`${failedCount} attachment uploads failed`);
786
+ const failures = results.filter((r) => !r.success);
787
+ if (failures.length > 0) {
788
+ failures.forEach((failure) => {
789
+ const attachment = validAttachments.find(
790
+ (_, i) => uploads[i]?.attachmentId === failure.attachmentId
791
+ );
792
+ this.errorCollector.recordError(
793
+ "ATTACHMENT_UPLOAD",
794
+ failure.error || "Upload failed",
795
+ {
796
+ attachmentName: attachment?.name,
797
+ filePath: attachment?.path,
798
+ error: failure.error
799
+ }
800
+ );
801
+ });
630
802
  }
631
803
  } catch (error) {
632
- this.logWarn(
633
- `Failed to upload attachments: ${this.getErrorMessage(error)}`
634
- );
804
+ const errorMsg = this.getErrorMessage(error);
805
+ this.errorCollector.recordError("ATTACHMENT_SIGN", errorMsg, { error });
635
806
  }
636
807
  }
637
808
  buildProjectConfigs(config) {
@@ -733,11 +904,13 @@ var SupatestPlaywrightReporter = class {
733
904
  }
734
905
  }
735
906
  getTestId(test) {
736
- const key = `${test.location.file}:${test.location.line}:${test.title}`;
907
+ const projectName = test.parent?.project()?.name ?? "unknown";
908
+ const key = `${test.location.file}:${test.location.line}:${test.title}:${projectName}`;
737
909
  return this.hashKey(key);
738
910
  }
739
- getResultId(testId, retry) {
740
- return this.hashKey(`${testId}:${retry}`);
911
+ getResultId(testId, retry, workerIndex, parallelIndex) {
912
+ const key = parallelIndex !== void 0 ? `${testId}:${retry}:${parallelIndex}` : workerIndex !== void 0 ? `${testId}:${retry}:${workerIndex}` : `${testId}:${retry}`;
913
+ return this.hashKey(key);
741
914
  }
742
915
  hashKey(value) {
743
916
  return (0, import_node_crypto.createHash)("sha256").update(value).digest("hex").slice(0, 12);
@@ -897,4 +1070,8 @@ var SupatestPlaywrightReporter = class {
897
1070
  console.warn(`[supatest] ${message}`);
898
1071
  }
899
1072
  };
1073
+ // Annotate the CommonJS export names for ESM import in node:
1074
+ 0 && (module.exports = {
1075
+ ErrorCollector
1076
+ });
900
1077
  //# sourceMappingURL=index.cjs.map