@supatest/cypress-reporter 0.0.1

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.js ADDED
@@ -0,0 +1,884 @@
1
+ // src/index.ts
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+
6
+ // ../reporter-core/dist/index.js
7
+ import { simpleGit } from "simple-git";
8
+ import fs2 from "fs";
9
+ import { createHash } from "crypto";
10
+ import fs3 from "fs";
11
+ var defaultRetryConfig = {
12
+ maxAttempts: 3,
13
+ baseDelayMs: 1e3,
14
+ maxDelayMs: 1e4,
15
+ retryOnStatusCodes: [408, 429, 500, 502, 503, 504]
16
+ };
17
+ function sleep(ms) {
18
+ return new Promise((resolve) => setTimeout(resolve, ms));
19
+ }
20
+ function shouldRetry(error, config) {
21
+ if (error instanceof Error && "statusCode" in error) {
22
+ const statusCode = error.statusCode;
23
+ return config.retryOnStatusCodes.includes(statusCode);
24
+ }
25
+ if (error instanceof Error) {
26
+ const networkErrors = ["ECONNRESET", "ETIMEDOUT", "ECONNREFUSED", "ENOTFOUND"];
27
+ return networkErrors.some((e) => error.message.includes(e));
28
+ }
29
+ return false;
30
+ }
31
+ async function withRetry(fn, config = defaultRetryConfig) {
32
+ let lastError;
33
+ for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
34
+ try {
35
+ return await fn();
36
+ } catch (error) {
37
+ lastError = error;
38
+ if (attempt === config.maxAttempts) {
39
+ throw error;
40
+ }
41
+ if (!shouldRetry(error, config)) {
42
+ throw error;
43
+ }
44
+ const delay = Math.min(
45
+ config.baseDelayMs * Math.pow(2, attempt - 1),
46
+ config.maxDelayMs
47
+ );
48
+ await sleep(delay);
49
+ }
50
+ }
51
+ throw lastError;
52
+ }
53
+ var SupatestApiClient = class {
54
+ options;
55
+ retryConfig;
56
+ constructor(options) {
57
+ this.options = options;
58
+ this.retryConfig = {
59
+ ...defaultRetryConfig,
60
+ maxAttempts: options.retryAttempts
61
+ };
62
+ }
63
+ async createRun(data) {
64
+ if (this.options.dryRun) {
65
+ this.logPayload("POST /v1/runs", data);
66
+ return {
67
+ runId: `mock_run_${Date.now()}`,
68
+ status: "running"
69
+ };
70
+ }
71
+ return this.request("POST", "/v1/runs", data);
72
+ }
73
+ async submitTest(runId, data) {
74
+ if (this.options.dryRun) {
75
+ this.logPayload(`POST /v1/runs/${runId}/tests`, data);
76
+ return;
77
+ }
78
+ await this.request("POST", `/v1/runs/${runId}/tests`, data);
79
+ }
80
+ async signAttachments(runId, data) {
81
+ if (this.options.dryRun) {
82
+ this.logPayload(`POST /v1/runs/${runId}/attachments/sign`, data);
83
+ return {
84
+ uploads: data.attachments.map((att, i) => ({
85
+ attachmentId: `mock_att_${i}_${Date.now()}`,
86
+ signedUrl: `https://mock-s3.example.com/uploads/${att.filename}`,
87
+ expiresAt: new Date(Date.now() + 15 * 60 * 1e3).toISOString()
88
+ }))
89
+ };
90
+ }
91
+ return this.request(
92
+ "POST",
93
+ `/v1/runs/${runId}/attachments/sign`,
94
+ data
95
+ );
96
+ }
97
+ async completeRun(runId, data) {
98
+ if (this.options.dryRun) {
99
+ this.logPayload(`POST /v1/runs/${runId}/complete`, data);
100
+ return;
101
+ }
102
+ await this.request("POST", `/v1/runs/${runId}/complete`, data);
103
+ }
104
+ async request(method, path2, body) {
105
+ const url = `${this.options.apiUrl}${path2}`;
106
+ return withRetry(async () => {
107
+ const controller = new AbortController();
108
+ const timeoutId = setTimeout(
109
+ () => controller.abort(),
110
+ this.options.timeoutMs
111
+ );
112
+ try {
113
+ const response = await fetch(url, {
114
+ method,
115
+ headers: {
116
+ "Content-Type": "application/json",
117
+ Authorization: `Bearer ${this.options.apiKey}`
118
+ },
119
+ body: body ? JSON.stringify(body) : void 0,
120
+ signal: controller.signal
121
+ });
122
+ if (!response.ok) {
123
+ const error = new Error(
124
+ `API request failed: ${response.status} ${response.statusText}`
125
+ );
126
+ error.statusCode = response.status;
127
+ throw error;
128
+ }
129
+ const text = await response.text();
130
+ return text ? JSON.parse(text) : {};
131
+ } finally {
132
+ clearTimeout(timeoutId);
133
+ }
134
+ }, this.retryConfig);
135
+ }
136
+ logPayload(endpoint, data) {
137
+ console.log(`
138
+ [supatest][dry-run] ${endpoint}`);
139
+ console.log(JSON.stringify(data, null, 2));
140
+ }
141
+ };
142
+ var ErrorCollector = class {
143
+ errors = [];
144
+ recordError(category, message, context) {
145
+ const error = {
146
+ category,
147
+ message,
148
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
149
+ testId: context?.testId,
150
+ testTitle: context?.testTitle,
151
+ attachmentName: context?.attachmentName,
152
+ filePath: context?.filePath,
153
+ originalError: context?.error instanceof Error ? context.error.stack : void 0
154
+ };
155
+ this.errors.push(error);
156
+ }
157
+ getSummary() {
158
+ const byCategory = {
159
+ RUN_CREATE: 0,
160
+ TEST_SUBMISSION: 0,
161
+ ATTACHMENT_SIGN: 0,
162
+ ATTACHMENT_UPLOAD: 0,
163
+ FILE_READ: 0,
164
+ RUN_COMPLETE: 0
165
+ };
166
+ for (const error of this.errors) {
167
+ byCategory[error.category]++;
168
+ }
169
+ return {
170
+ totalErrors: this.errors.length,
171
+ byCategory,
172
+ errors: this.errors
173
+ };
174
+ }
175
+ hasErrors() {
176
+ return this.errors.length > 0;
177
+ }
178
+ formatSummary() {
179
+ if (this.errors.length === 0) {
180
+ return "";
181
+ }
182
+ const summary = this.getSummary();
183
+ const lines = [
184
+ "",
185
+ "\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",
186
+ "\u26A0\uFE0F Supatest Reporter - Error Summary",
187
+ "\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",
188
+ `Total Errors: ${summary.totalErrors}`,
189
+ ""
190
+ ];
191
+ for (const [category, count] of Object.entries(summary.byCategory)) {
192
+ if (count === 0) continue;
193
+ const icon = this.getCategoryIcon(category);
194
+ const label = this.getCategoryLabel(category);
195
+ lines.push(`${icon} ${label}: ${count}`);
196
+ const categoryErrors = this.errors.filter((e) => e.category === category).slice(0, 3);
197
+ for (const error of categoryErrors) {
198
+ lines.push(` \u2022 ${this.formatError(error)}`);
199
+ }
200
+ const remaining = count - categoryErrors.length;
201
+ if (remaining > 0) {
202
+ lines.push(` ... and ${remaining} more`);
203
+ }
204
+ lines.push("");
205
+ }
206
+ lines.push(
207
+ "\u2139\uFE0F Note: Test execution was not interrupted. Some results may not be visible in the dashboard."
208
+ );
209
+ lines.push(
210
+ "\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"
211
+ );
212
+ return lines.join("\n");
213
+ }
214
+ getCategoryIcon(category) {
215
+ const icons = {
216
+ RUN_CREATE: "\u{1F680}",
217
+ TEST_SUBMISSION: "\u{1F4DD}",
218
+ ATTACHMENT_SIGN: "\u{1F510}",
219
+ ATTACHMENT_UPLOAD: "\u{1F4CE}",
220
+ FILE_READ: "\u{1F4C1}",
221
+ RUN_COMPLETE: "\u{1F3C1}"
222
+ };
223
+ return icons[category];
224
+ }
225
+ getCategoryLabel(category) {
226
+ const labels = {
227
+ RUN_CREATE: "Run Initialization",
228
+ TEST_SUBMISSION: "Test Result Submission",
229
+ ATTACHMENT_SIGN: "Attachment Signing",
230
+ ATTACHMENT_UPLOAD: "Attachment Upload",
231
+ FILE_READ: "File Access",
232
+ RUN_COMPLETE: "Run Completion"
233
+ };
234
+ return labels[category];
235
+ }
236
+ formatError(error) {
237
+ const parts = [];
238
+ if (error.testTitle) {
239
+ parts.push(`Test: "${error.testTitle}"`);
240
+ }
241
+ if (error.attachmentName) {
242
+ parts.push(`Attachment: "${error.attachmentName}"`);
243
+ }
244
+ if (error.filePath) {
245
+ parts.push(`File: ${error.filePath}`);
246
+ }
247
+ parts.push(error.message);
248
+ return parts.join(" - ");
249
+ }
250
+ };
251
+ async function getLocalGitInfo(rootDir) {
252
+ try {
253
+ const git = simpleGit(rootDir || process.cwd());
254
+ const isRepo = await git.checkIsRepo();
255
+ if (!isRepo) {
256
+ return {};
257
+ }
258
+ const [branchResult, commitResult, status, remotes] = await Promise.all([
259
+ git.revparse(["--abbrev-ref", "HEAD"]).catch(() => void 0),
260
+ git.revparse(["HEAD"]).catch(() => void 0),
261
+ git.status().catch(() => void 0),
262
+ git.getRemotes(true).catch(() => [])
263
+ ]);
264
+ const branch = typeof branchResult === "string" ? branchResult.trim() : void 0;
265
+ const commit = typeof commitResult === "string" ? commitResult.trim() : void 0;
266
+ let commitMessage;
267
+ try {
268
+ const logOutput = await git.raw(["log", "-1", "--format=%s"]);
269
+ commitMessage = typeof logOutput === "string" ? logOutput.trim() : void 0;
270
+ } catch {
271
+ }
272
+ let author;
273
+ let authorEmail;
274
+ try {
275
+ const authorConfig = await git.getConfig("user.name").catch(() => void 0);
276
+ const emailConfig = await git.getConfig("user.email").catch(() => void 0);
277
+ author = authorConfig && typeof authorConfig.value === "string" ? authorConfig.value.trim() : void 0;
278
+ authorEmail = emailConfig && typeof emailConfig.value === "string" ? emailConfig.value.trim() : void 0;
279
+ } catch {
280
+ }
281
+ let tag;
282
+ try {
283
+ const tagResult = await git.raw(["describe", "--tags", "--exact-match"]).catch(() => void 0);
284
+ tag = typeof tagResult === "string" ? tagResult.trim() : void 0;
285
+ } catch {
286
+ }
287
+ let repo;
288
+ const remote = remotes && remotes.length > 0 ? remotes[0] : null;
289
+ if (remote && remote.refs && remote.refs.fetch) {
290
+ repo = remote.refs.fetch;
291
+ }
292
+ const dirty = status ? status.modified.length > 0 || status.created.length > 0 || status.deleted.length > 0 : false;
293
+ return {
294
+ branch,
295
+ commit,
296
+ commitMessage,
297
+ repo,
298
+ author,
299
+ authorEmail,
300
+ tag,
301
+ dirty
302
+ };
303
+ } catch (error) {
304
+ return {};
305
+ }
306
+ }
307
+ var AttachmentUploader = class {
308
+ options;
309
+ constructor(options) {
310
+ this.options = options;
311
+ }
312
+ async upload(signedUrl, filePath, contentType) {
313
+ if (this.options.dryRun) {
314
+ try {
315
+ const stats = fs2.statSync(filePath);
316
+ console.log(
317
+ `[supatest][dry-run] Would upload ${filePath} (${stats.size} bytes) to S3`
318
+ );
319
+ } catch (error) {
320
+ const message = error instanceof Error ? error.message : String(error);
321
+ console.warn(
322
+ `[supatest][dry-run] Cannot access file ${filePath}: ${message}`
323
+ );
324
+ }
325
+ return;
326
+ }
327
+ let fileBuffer;
328
+ try {
329
+ fileBuffer = await fs2.promises.readFile(filePath);
330
+ } catch (error) {
331
+ const message = error instanceof Error ? error.message : String(error);
332
+ throw new Error(`Failed to read file ${filePath}: ${message}`);
333
+ }
334
+ await withRetry(async () => {
335
+ const controller = new AbortController();
336
+ const timeoutId = setTimeout(
337
+ () => controller.abort(),
338
+ this.options.timeoutMs
339
+ );
340
+ try {
341
+ const response = await fetch(signedUrl, {
342
+ method: "PUT",
343
+ headers: {
344
+ "Content-Type": contentType,
345
+ "Content-Length": String(fileBuffer.length)
346
+ },
347
+ body: new Uint8Array(fileBuffer),
348
+ signal: controller.signal
349
+ });
350
+ if (!response.ok) {
351
+ const error = new Error(
352
+ `S3 upload failed: ${response.status} ${response.statusText}`
353
+ );
354
+ error.statusCode = response.status;
355
+ throw error;
356
+ }
357
+ } finally {
358
+ clearTimeout(timeoutId);
359
+ }
360
+ }, defaultRetryConfig);
361
+ }
362
+ async uploadBatch(items, signedUploads) {
363
+ const pLimit = (await import("p-limit")).default;
364
+ const limit = pLimit(this.options.maxConcurrent);
365
+ const results = await Promise.allSettled(
366
+ items.map(
367
+ (item, index) => limit(async () => {
368
+ try {
369
+ await this.upload(item.signedUrl, item.filePath, item.contentType);
370
+ return {
371
+ success: true,
372
+ attachmentId: signedUploads[index]?.attachmentId
373
+ };
374
+ } catch (error) {
375
+ const message = error instanceof Error ? error.message : String(error);
376
+ return {
377
+ success: false,
378
+ attachmentId: signedUploads[index]?.attachmentId,
379
+ error: `${item.filePath}: ${message}`
380
+ };
381
+ }
382
+ })
383
+ )
384
+ );
385
+ return results.map((result, index) => {
386
+ if (result.status === "fulfilled") {
387
+ return result.value;
388
+ }
389
+ const message = result.reason instanceof Error ? result.reason.message : String(result.reason);
390
+ return {
391
+ success: false,
392
+ attachmentId: signedUploads[index]?.attachmentId,
393
+ error: message
394
+ };
395
+ });
396
+ }
397
+ };
398
+ function hashKey(value) {
399
+ return createHash("sha256").update(value).digest("hex").slice(0, 12);
400
+ }
401
+ function getFileSize(filePath) {
402
+ try {
403
+ return fs3.statSync(filePath).size;
404
+ } catch {
405
+ return 0;
406
+ }
407
+ }
408
+ function getErrorMessage(error) {
409
+ if (error instanceof Error) return error.message;
410
+ return String(error);
411
+ }
412
+ function logInfo(message) {
413
+ console.log(`[supatest] ${message}`);
414
+ }
415
+ function logWarn(message) {
416
+ console.warn(`[supatest] ${message}`);
417
+ }
418
+ function getCIInfo() {
419
+ if (process.env.GITHUB_ACTIONS) {
420
+ return {
421
+ provider: "github-actions",
422
+ runId: process.env.GITHUB_RUN_ID,
423
+ jobId: process.env.GITHUB_JOB,
424
+ jobUrl: `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`,
425
+ buildNumber: process.env.GITHUB_RUN_NUMBER,
426
+ branch: process.env.GITHUB_REF_NAME,
427
+ pullRequest: process.env.GITHUB_EVENT_NAME === "pull_request" ? {
428
+ number: process.env.GITHUB_PR_NUMBER ?? "",
429
+ url: `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/pull/${process.env.GITHUB_PR_NUMBER}`
430
+ } : void 0
431
+ };
432
+ }
433
+ if (process.env.GITLAB_CI) {
434
+ return {
435
+ provider: "gitlab-ci",
436
+ runId: process.env.CI_PIPELINE_ID,
437
+ jobId: process.env.CI_JOB_ID,
438
+ jobUrl: process.env.CI_JOB_URL,
439
+ buildNumber: process.env.CI_PIPELINE_IID,
440
+ branch: process.env.CI_COMMIT_BRANCH,
441
+ pullRequest: process.env.CI_MERGE_REQUEST_IID ? {
442
+ number: process.env.CI_MERGE_REQUEST_IID,
443
+ url: process.env.CI_MERGE_REQUEST_PROJECT_URL + "/-/merge_requests/" + process.env.CI_MERGE_REQUEST_IID,
444
+ title: process.env.CI_MERGE_REQUEST_TITLE
445
+ } : void 0
446
+ };
447
+ }
448
+ if (process.env.JENKINS_URL) {
449
+ return {
450
+ provider: "jenkins",
451
+ runId: process.env.BUILD_ID,
452
+ jobUrl: process.env.BUILD_URL,
453
+ buildNumber: process.env.BUILD_NUMBER,
454
+ branch: process.env.GIT_BRANCH
455
+ };
456
+ }
457
+ if (process.env.CIRCLECI) {
458
+ return {
459
+ provider: "circleci",
460
+ runId: process.env.CIRCLE_WORKFLOW_ID,
461
+ jobId: process.env.CIRCLE_JOB,
462
+ jobUrl: process.env.CIRCLE_BUILD_URL,
463
+ buildNumber: process.env.CIRCLE_BUILD_NUM,
464
+ branch: process.env.CIRCLE_BRANCH,
465
+ pullRequest: process.env.CIRCLE_PULL_REQUEST ? {
466
+ number: process.env.CIRCLE_PR_NUMBER ?? "",
467
+ url: process.env.CIRCLE_PULL_REQUEST
468
+ } : void 0
469
+ };
470
+ }
471
+ if (process.env.TRAVIS) {
472
+ return {
473
+ provider: "travis",
474
+ runId: process.env.TRAVIS_BUILD_ID,
475
+ jobId: process.env.TRAVIS_JOB_ID,
476
+ jobUrl: process.env.TRAVIS_JOB_WEB_URL,
477
+ buildNumber: process.env.TRAVIS_BUILD_NUMBER,
478
+ branch: process.env.TRAVIS_BRANCH,
479
+ pullRequest: process.env.TRAVIS_PULL_REQUEST !== "false" ? {
480
+ number: process.env.TRAVIS_PULL_REQUEST ?? "",
481
+ url: `https://github.com/${process.env.TRAVIS_REPO_SLUG}/pull/${process.env.TRAVIS_PULL_REQUEST}`
482
+ } : void 0
483
+ };
484
+ }
485
+ if (process.env.BUILDKITE) {
486
+ return {
487
+ provider: "buildkite",
488
+ runId: process.env.BUILDKITE_BUILD_ID,
489
+ jobId: process.env.BUILDKITE_JOB_ID,
490
+ jobUrl: process.env.BUILDKITE_BUILD_URL,
491
+ buildNumber: process.env.BUILDKITE_BUILD_NUMBER,
492
+ branch: process.env.BUILDKITE_BRANCH
493
+ };
494
+ }
495
+ if (process.env.AZURE_PIPELINES || process.env.TF_BUILD) {
496
+ return {
497
+ provider: "azure-pipelines",
498
+ runId: process.env.BUILD_BUILDID,
499
+ jobUrl: `${process.env.SYSTEM_COLLECTIONURI}${process.env.SYSTEM_TEAMPROJECT}/_build/results?buildId=${process.env.BUILD_BUILDID}`,
500
+ buildNumber: process.env.BUILD_BUILDNUMBER,
501
+ branch: process.env.BUILD_SOURCEBRANCH
502
+ };
503
+ }
504
+ return void 0;
505
+ }
506
+
507
+ // src/index.ts
508
+ var DEFAULT_API_URL = "https://code-api.supatest.ai";
509
+ var SupatestCypressReporter = class {
510
+ options;
511
+ client;
512
+ uploader;
513
+ errorCollector = new ErrorCollector();
514
+ runId;
515
+ uploadQueue = [];
516
+ startedAt;
517
+ firstTestStartTime;
518
+ firstFailureTime;
519
+ testsProcessed = /* @__PURE__ */ new Map();
520
+ disabled = false;
521
+ rootDir;
522
+ stepIdCounter = 0;
523
+ cypressVersion;
524
+ browserName;
525
+ browserVersion;
526
+ constructor(options = {}) {
527
+ this.options = {
528
+ projectId: options.projectId || process.env.SUPATEST_PROJECT_ID || "",
529
+ apiKey: options.apiKey || process.env.SUPATEST_API_KEY || "",
530
+ apiUrl: options.apiUrl || process.env.SUPATEST_API_URL || DEFAULT_API_URL,
531
+ uploadAssets: options.uploadAssets ?? true,
532
+ maxConcurrentUploads: options.maxConcurrentUploads ?? 5,
533
+ retryAttempts: options.retryAttempts ?? 3,
534
+ timeoutMs: options.timeoutMs ?? 3e4,
535
+ dryRun: options.dryRun ?? process.env.SUPATEST_DRY_RUN === "true"
536
+ };
537
+ }
538
+ async onBeforeRun(details) {
539
+ this.rootDir = details.config.projectRoot;
540
+ this.cypressVersion = details.config.version;
541
+ this.browserName = details.browser.name;
542
+ this.browserVersion = details.browser.version;
543
+ if (!this.options.projectId || !this.options.apiKey) {
544
+ if (!this.options.dryRun) {
545
+ logWarn("Missing projectId or apiKey. Set SUPATEST_PROJECT_ID and SUPATEST_API_KEY.");
546
+ this.disabled = true;
547
+ return;
548
+ }
549
+ }
550
+ this.client = new SupatestApiClient({
551
+ apiKey: this.options.apiKey,
552
+ apiUrl: this.options.apiUrl,
553
+ timeoutMs: this.options.timeoutMs,
554
+ retryAttempts: this.options.retryAttempts,
555
+ dryRun: this.options.dryRun
556
+ });
557
+ this.uploader = new AttachmentUploader({
558
+ maxConcurrent: this.options.maxConcurrentUploads,
559
+ timeoutMs: this.options.timeoutMs,
560
+ dryRun: this.options.dryRun
561
+ });
562
+ this.startedAt = (/* @__PURE__ */ new Date()).toISOString();
563
+ try {
564
+ const runRequest = {
565
+ projectId: this.options.projectId,
566
+ startedAt: this.startedAt,
567
+ cypress: {
568
+ version: this.cypressVersion || "unknown",
569
+ browser: this.browserName || "electron"
570
+ },
571
+ projects: [{
572
+ name: this.browserName || "electron",
573
+ browserName: this.browserName
574
+ }],
575
+ testStats: {
576
+ totalFiles: details.specs.length,
577
+ totalTests: 0,
578
+ // Unknown at this point
579
+ totalProjects: 1
580
+ },
581
+ environment: this.getEnvironmentInfo(),
582
+ git: await this.getGitInfo(),
583
+ rootDir: this.rootDir
584
+ };
585
+ const response = await this.client.createRun(runRequest);
586
+ this.runId = response.runId;
587
+ logInfo(`Run ${this.runId} started (${details.specs.length} spec files)`);
588
+ } catch (error) {
589
+ this.errorCollector.recordError("RUN_CREATE", getErrorMessage(error), { error });
590
+ this.disabled = true;
591
+ }
592
+ }
593
+ async onAfterSpec(spec, results) {
594
+ if (this.disabled || !this.runId) return;
595
+ for (const test of results.tests) {
596
+ await this.processTestResult(spec, test, results);
597
+ }
598
+ }
599
+ async onAfterRun(results) {
600
+ if (this.disabled || !this.runId) {
601
+ if (this.errorCollector.hasErrors()) {
602
+ console.log(this.errorCollector.formatSummary());
603
+ }
604
+ return;
605
+ }
606
+ await Promise.allSettled(this.uploadQueue);
607
+ const summary = {
608
+ total: results.totalTests,
609
+ passed: results.totalPassed,
610
+ failed: results.totalFailed,
611
+ flaky: 0,
612
+ skipped: results.totalSkipped + results.totalPending,
613
+ timedOut: 0,
614
+ interrupted: 0,
615
+ durationMs: results.totalDuration
616
+ };
617
+ try {
618
+ await this.client.completeRun(this.runId, {
619
+ status: results.status === "finished" ? "complete" : "errored",
620
+ endedAt: results.endedTestsAt,
621
+ summary,
622
+ timing: {
623
+ totalDurationMs: results.totalDuration,
624
+ timeToFirstTest: this.firstTestStartTime ? this.firstTestStartTime - new Date(this.startedAt).getTime() : void 0,
625
+ timeToFirstFailure: this.firstFailureTime ? this.firstFailureTime - new Date(this.startedAt).getTime() : void 0
626
+ }
627
+ });
628
+ logInfo(`Run ${this.runId} completed`);
629
+ } catch (error) {
630
+ this.errorCollector.recordError("RUN_COMPLETE", getErrorMessage(error), { error });
631
+ }
632
+ if (this.errorCollector.hasErrors()) {
633
+ console.log(this.errorCollector.formatSummary());
634
+ }
635
+ }
636
+ async processTestResult(spec, test, specResults) {
637
+ const testId = this.getTestId(spec.relative, test.title);
638
+ const status = this.mapCypressStatus(test.state);
639
+ if (!this.firstTestStartTime && test.attempts[0]) {
640
+ this.firstTestStartTime = new Date(test.attempts[0].wallClockStartedAt).getTime();
641
+ }
642
+ if (!this.firstFailureTime && status === "failed") {
643
+ this.firstFailureTime = Date.now();
644
+ }
645
+ const lastAttempt = test.attempts[test.attempts.length - 1];
646
+ const resultEntry = {
647
+ resultId: hashKey(`${testId}:${test.attempts.length - 1}`),
648
+ retry: test.attempts.length - 1,
649
+ status,
650
+ startTime: lastAttempt?.wallClockStartedAt,
651
+ durationMs: lastAttempt?.wallClockDuration || 0,
652
+ errors: this.extractErrors(test),
653
+ steps: [],
654
+ // Cypress doesn't expose steps like Playwright
655
+ attachments: this.buildAttachmentMeta(test, specResults)
656
+ };
657
+ const tags = this.extractTags(test.title.join(" "));
658
+ const metadata = this.parseTestMetadata(tags);
659
+ const payload = {
660
+ testId,
661
+ cypressId: testId,
662
+ file: spec.relative,
663
+ location: { file: spec.relative, line: 0, column: 0 },
664
+ title: test.title[test.title.length - 1],
665
+ titlePath: test.title,
666
+ tags,
667
+ annotations: [],
668
+ expectedStatus: "passed",
669
+ outcome: this.getOutcome(test),
670
+ timeout: 0,
671
+ retries: test.attempts.length - 1,
672
+ status,
673
+ durationMs: test.attempts.reduce((sum, a) => sum + (a.wallClockDuration || 0), 0),
674
+ retryCount: test.attempts.length - 1,
675
+ results: [resultEntry],
676
+ projectName: this.browserName || "electron",
677
+ metadata
678
+ };
679
+ try {
680
+ await this.client.submitTest(this.runId, payload);
681
+ if (this.options.uploadAssets) {
682
+ await this.uploadAttachments(testId, resultEntry.resultId, specResults, test);
683
+ }
684
+ } catch (error) {
685
+ this.errorCollector.recordError("TEST_SUBMISSION", getErrorMessage(error), {
686
+ testId,
687
+ testTitle: test.title.join(" > "),
688
+ error
689
+ });
690
+ }
691
+ }
692
+ getTestId(file, titlePath) {
693
+ const fullTitle = titlePath.join(" ");
694
+ const idMatch = fullTitle.match(/@id:([^\s@]+)/);
695
+ if (idMatch) return idMatch[1];
696
+ return hashKey(`${file}::${titlePath.join("::")}`);
697
+ }
698
+ mapCypressStatus(state) {
699
+ switch (state) {
700
+ case "passed":
701
+ return "passed";
702
+ case "failed":
703
+ return "failed";
704
+ case "pending":
705
+ case "skipped":
706
+ return "skipped";
707
+ default:
708
+ return "failed";
709
+ }
710
+ }
711
+ getOutcome(test) {
712
+ if (test.state === "skipped" || test.state === "pending") return "skipped";
713
+ if (test.state === "passed") {
714
+ if (test.attempts.length > 1 && test.attempts.some((a) => a.state === "failed")) {
715
+ return "flaky";
716
+ }
717
+ return "expected";
718
+ }
719
+ return "unexpected";
720
+ }
721
+ extractErrors(test) {
722
+ const errors = [];
723
+ for (const attempt of test.attempts) {
724
+ if (attempt.error) {
725
+ errors.push({
726
+ message: attempt.error.message,
727
+ stack: attempt.error.stack
728
+ });
729
+ }
730
+ }
731
+ if (test.displayError) {
732
+ errors.push({ message: test.displayError });
733
+ }
734
+ return errors;
735
+ }
736
+ extractTags(title) {
737
+ const tagRegex = /@([a-zA-Z][a-zA-Z0-9_:-]*)/g;
738
+ const tags = [];
739
+ let match;
740
+ while ((match = tagRegex.exec(title)) !== null) {
741
+ tags.push(`@${match[1]}`);
742
+ }
743
+ return tags;
744
+ }
745
+ parseTestMetadata(tags) {
746
+ const metadata = {
747
+ isSlow: false,
748
+ isFlakyTagged: false,
749
+ customMetadata: {}
750
+ };
751
+ for (const tag of tags) {
752
+ const lower = tag.toLowerCase();
753
+ if (lower === "@slow") metadata.isSlow = true;
754
+ if (lower === "@flaky") metadata.isFlakyTagged = true;
755
+ const ownerMatch = tag.match(/@owner:([^\s@]+)/i);
756
+ if (ownerMatch) metadata.owner = ownerMatch[1];
757
+ const priorityMatch = tag.match(/@priority:(critical|high|medium|low)/i);
758
+ if (priorityMatch) metadata.priority = priorityMatch[1].toLowerCase();
759
+ const featureMatch = tag.match(/@feature:([^\s@]+)/i);
760
+ if (featureMatch) metadata.feature = featureMatch[1];
761
+ const typeMatch = tag.match(/@test_type:(smoke|e2e|regression|integration|unit)/i);
762
+ if (typeMatch) metadata.testType = typeMatch[1].toLowerCase();
763
+ }
764
+ return metadata;
765
+ }
766
+ buildAttachmentMeta(test, specResults) {
767
+ const attachments = [];
768
+ for (const screenshot of specResults.screenshots) {
769
+ if (screenshot.path) {
770
+ attachments.push({
771
+ name: screenshot.name || "screenshot",
772
+ filename: path.basename(screenshot.path),
773
+ contentType: "image/png",
774
+ sizeBytes: getFileSize(screenshot.path),
775
+ kind: "screenshot"
776
+ });
777
+ }
778
+ }
779
+ if (specResults.video) {
780
+ attachments.push({
781
+ name: "video",
782
+ filename: path.basename(specResults.video),
783
+ contentType: "video/mp4",
784
+ sizeBytes: getFileSize(specResults.video),
785
+ kind: "video"
786
+ });
787
+ }
788
+ return attachments;
789
+ }
790
+ async uploadAttachments(testId, testResultId, specResults, test) {
791
+ const attachments = [];
792
+ for (const screenshot of specResults.screenshots) {
793
+ if (screenshot.path && fs.existsSync(screenshot.path)) {
794
+ attachments.push({
795
+ path: screenshot.path,
796
+ meta: {
797
+ name: screenshot.name || "screenshot",
798
+ filename: path.basename(screenshot.path),
799
+ contentType: "image/png",
800
+ sizeBytes: getFileSize(screenshot.path),
801
+ kind: "screenshot"
802
+ }
803
+ });
804
+ }
805
+ }
806
+ if (specResults.video && fs.existsSync(specResults.video)) {
807
+ attachments.push({
808
+ path: specResults.video,
809
+ meta: {
810
+ name: "video",
811
+ filename: path.basename(specResults.video),
812
+ contentType: "video/mp4",
813
+ sizeBytes: getFileSize(specResults.video),
814
+ kind: "video"
815
+ }
816
+ });
817
+ }
818
+ if (attachments.length === 0) return;
819
+ try {
820
+ const { uploads } = await this.client.signAttachments(this.runId, {
821
+ testResultId,
822
+ attachments: attachments.map((a) => a.meta)
823
+ });
824
+ const uploadItems = uploads.map((u, i) => ({
825
+ signedUrl: u.signedUrl,
826
+ filePath: attachments[i].path,
827
+ contentType: attachments[i].meta.contentType
828
+ }));
829
+ await this.uploader.uploadBatch(uploadItems, uploads);
830
+ } catch (error) {
831
+ this.errorCollector.recordError("ATTACHMENT_SIGN", getErrorMessage(error), { error });
832
+ }
833
+ }
834
+ getEnvironmentInfo() {
835
+ return {
836
+ os: {
837
+ platform: os.platform(),
838
+ release: os.release(),
839
+ arch: os.arch()
840
+ },
841
+ node: {
842
+ version: process.version
843
+ },
844
+ machine: {
845
+ cpus: os.cpus().length,
846
+ memory: os.totalmem(),
847
+ hostname: os.hostname()
848
+ },
849
+ cypress: {
850
+ version: this.cypressVersion || "unknown"
851
+ },
852
+ ci: getCIInfo()
853
+ };
854
+ }
855
+ async getGitInfo() {
856
+ const ciGitInfo = {
857
+ branch: process.env.GITHUB_REF_NAME ?? process.env.CI_COMMIT_BRANCH ?? process.env.GIT_BRANCH,
858
+ commit: process.env.GITHUB_SHA ?? process.env.CI_COMMIT_SHA ?? process.env.GIT_COMMIT,
859
+ repo: process.env.GITHUB_REPOSITORY ?? process.env.CI_PROJECT_PATH,
860
+ author: process.env.GITHUB_ACTOR ?? process.env.GITLAB_USER_NAME
861
+ };
862
+ if (ciGitInfo.branch || ciGitInfo.commit) {
863
+ return ciGitInfo;
864
+ }
865
+ return await getLocalGitInfo(this.rootDir);
866
+ }
867
+ };
868
+ function supatestPlugin(on, config, options = {}) {
869
+ const reporter = new SupatestCypressReporter(options);
870
+ on("before:run", async (details) => {
871
+ await reporter.onBeforeRun(details);
872
+ });
873
+ on("after:spec", async (spec, results) => {
874
+ await reporter.onAfterSpec(spec, results);
875
+ });
876
+ on("after:run", async (results) => {
877
+ await reporter.onAfterRun(results);
878
+ });
879
+ }
880
+ export {
881
+ ErrorCollector,
882
+ supatestPlugin as default
883
+ };
884
+ //# sourceMappingURL=index.js.map