@piwitests/reporter 0.4.4 → 0.6.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 (75) hide show
  1. package/README.md +37 -26
  2. package/dist/global-setup-module.js +2 -2
  3. package/dist/index.d.ts +15 -8
  4. package/dist/index.js +23 -5
  5. package/dist/internal/capture/attachments.d.ts +19 -0
  6. package/dist/internal/capture/attachments.js +22 -0
  7. package/dist/{fixtures.d.ts → internal/capture/capture-fixtures.d.ts} +6 -4
  8. package/dist/internal/capture/capture-fixtures.js +495 -0
  9. package/dist/internal/capture/locator-healing.d.ts +152 -0
  10. package/dist/internal/capture/locator-healing.js +668 -0
  11. package/dist/internal/collect/error-text.d.ts +18 -0
  12. package/dist/internal/collect/error-text.js +79 -0
  13. package/dist/{metadata-collector.d.ts → internal/collect/metadata-collector.d.ts} +2 -2
  14. package/dist/{metadata-collector.js → internal/collect/metadata-collector.js} +12 -9
  15. package/dist/{skip-classify.d.ts → internal/collect/skip-classify.d.ts} +1 -1
  16. package/dist/{step-analyzer.d.ts → internal/collect/step-analyzer.d.ts} +6 -0
  17. package/dist/{step-analyzer.js → internal/collect/step-analyzer.js} +7 -2
  18. package/dist/internal/config/env.d.ts +45 -0
  19. package/dist/{config.js → internal/config/env.js} +59 -33
  20. package/dist/internal/files/compression.js +69 -0
  21. package/dist/{file-handler.d.ts → internal/files/file-handler.d.ts} +4 -4
  22. package/dist/{file-handler.js → internal/files/file-handler.js} +61 -29
  23. package/dist/{crash-recovery.d.ts → internal/streaming/crash-recovery.d.ts} +5 -5
  24. package/dist/{crash-recovery.js → internal/streaming/crash-recovery.js} +9 -8
  25. package/dist/{stream-buffer.d.ts → internal/streaming/stream-buffer.d.ts} +2 -2
  26. package/dist/{stream-buffer.js → internal/streaming/stream-buffer.js} +5 -5
  27. package/dist/{stream-manager.d.ts → internal/streaming/stream-manager.d.ts} +22 -8
  28. package/dist/{stream-manager.js → internal/streaming/stream-manager.js} +66 -12
  29. package/dist/{run-submitter.d.ts → internal/submit/run-submitter.d.ts} +7 -7
  30. package/dist/{run-submitter.js → internal/submit/run-submitter.js} +13 -9
  31. package/dist/{serializer.d.ts → internal/submit/serializer.d.ts} +1 -1
  32. package/dist/{serializer.js → internal/submit/serializer.js} +2 -0
  33. package/dist/{uploader.d.ts → internal/submit/uploader.d.ts} +9 -8
  34. package/dist/{uploader.js → internal/submit/uploader.js} +45 -10
  35. package/dist/internal/support/ci.d.ts +2 -0
  36. package/dist/internal/support/ci.js +32 -0
  37. package/dist/internal/support/cli-filters.d.ts +1 -0
  38. package/dist/internal/support/cli-filters.js +51 -0
  39. package/dist/internal/support/errors.d.ts +8 -0
  40. package/dist/internal/support/errors.js +15 -0
  41. package/dist/internal/support/instance-id.d.ts +4 -0
  42. package/dist/internal/support/instance-id.js +48 -0
  43. package/dist/internal/support/limiter.d.ts +2 -0
  44. package/dist/internal/support/limiter.js +27 -0
  45. package/dist/internal/support/setup-file.d.ts +13 -0
  46. package/dist/internal/support/setup-file.js +61 -0
  47. package/dist/internal/support/source-snippet.d.ts +12 -0
  48. package/dist/internal/support/source-snippet.js +97 -0
  49. package/dist/internal/support/worker-index.d.ts +7 -0
  50. package/dist/internal/support/worker-index.js +14 -0
  51. package/dist/{http-client.d.ts → internal/transport/http-client.d.ts} +11 -1
  52. package/dist/{http-client.js → internal/transport/http-client.js} +57 -10
  53. package/dist/{config-wrapper.d.ts → public/config-wrapper.d.ts} +1 -1
  54. package/dist/{config-wrapper.js → public/config-wrapper.js} +4 -4
  55. package/dist/public/global-setup.d.ts +13 -0
  56. package/dist/public/global-setup.js +146 -0
  57. package/dist/{config.d.ts → public/options.d.ts} +18 -44
  58. package/dist/public/options.js +2 -0
  59. package/dist/{reporter.d.ts → public/reporter.d.ts} +3 -1
  60. package/dist/{reporter.js → public/reporter.js} +66 -34
  61. package/dist/types/collected.d.ts +96 -0
  62. package/dist/types/collected.js +10 -0
  63. package/dist/types/wire.d.ts +174 -0
  64. package/dist/types/wire.js +14 -0
  65. package/dist/types.d.ts +8 -248
  66. package/dist/types.js +23 -10
  67. package/package.json +1 -6
  68. package/dist/compression.js +0 -39
  69. package/dist/fixtures.js +0 -156
  70. package/dist/helpers.d.ts +0 -44
  71. package/dist/helpers.js +0 -288
  72. /package/dist/{skip-classify.js → internal/collect/skip-classify.js} +0 -0
  73. /package/dist/{compression.d.ts → internal/files/compression.d.ts} +0 -0
  74. /package/dist/{logger.d.ts → internal/support/logger.d.ts} +0 -0
  75. /package/dist/{logger.js → internal/support/logger.js} +0 -0
@@ -1,21 +1,21 @@
1
- import { HttpClient } from './http-client.js';
2
- import { Logger } from './logger.js';
1
+ import type { HttpClient } from '../transport/http-client.js';
2
+ import { Logger } from '../support/logger.js';
3
3
  /**
4
4
  * Persists a test-run payload to disk when all upload strategies fail, so the
5
5
  * data can be retried on the next run.
6
6
  */
7
7
  export declare class CrashRecovery {
8
- private filePath;
9
8
  private readonly logger;
9
+ private readonly filePath;
10
10
  /**
11
11
  * @param projectName Used to derive the temp-file name so recovery data is project-scoped.
12
12
  * @param logger Prefixed logger.
13
13
  */
14
14
  constructor(projectName: string, logger?: Logger);
15
15
  /** Serialise the payload to a temp file for later retry */
16
- save(data: Record<string, any>): void;
16
+ save(data: Record<string, unknown>): void;
17
17
  /** Read the saved payload from disk, or return `null` if none exists */
18
- load(): Record<string, any> | null;
18
+ load(): Record<string, unknown> | null;
19
19
  /** Delete the recovery file from disk */
20
20
  clear(): void;
21
21
  /** Attempt to submit a previously saved payload. Clears the recovery file on success. */
@@ -34,11 +34,12 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.CrashRecovery = void 0;
37
- const path = __importStar(require("path"));
38
- const os = __importStar(require("os"));
39
- const fs = __importStar(require("fs"));
40
- const helpers_js_1 = require("./helpers.js");
41
- const logger_js_1 = require("./logger.js");
37
+ const path = __importStar(require("node:path"));
38
+ const errors_js_1 = require("../support/errors.js");
39
+ const os = __importStar(require("node:os"));
40
+ const fs = __importStar(require("node:fs"));
41
+ const instance_id_js_1 = require("../support/instance-id.js");
42
+ const logger_js_1 = require("../support/logger.js");
42
43
  /**
43
44
  * Persists a test-run payload to disk when all upload strategies fail, so the
44
45
  * data can be retried on the next run.
@@ -50,7 +51,7 @@ class CrashRecovery {
50
51
  */
51
52
  constructor(projectName, logger = new logger_js_1.Logger()) {
52
53
  this.logger = logger;
53
- this.filePath = path.join(os.tmpdir(), `piwi-dashboard-recovery-${(0, helpers_js_1.hashForProject)(projectName)}.json`);
54
+ this.filePath = path.join(os.tmpdir(), `piwi-dashboard-recovery-${(0, instance_id_js_1.hashForProject)(projectName)}.json`);
54
55
  }
55
56
  /** Serialise the payload to a temp file for later retry */
56
57
  save(data) {
@@ -59,7 +60,7 @@ class CrashRecovery {
59
60
  this.logger.info('Saved recovery data for later upload');
60
61
  }
61
62
  catch (error) {
62
- this.logger.error(`Failed to save recovery data: ${error.message}`);
63
+ this.logger.error(`Failed to save recovery data: ${(0, errors_js_1.errorMessage)(error)}`);
63
64
  }
64
65
  }
65
66
  /** Read the saved payload from disk, or return `null` if none exists */
@@ -98,7 +99,7 @@ class CrashRecovery {
98
99
  this.clear();
99
100
  }
100
101
  catch (error) {
101
- this.logger.warn(`Could not upload saved test data: ${error.message}`);
102
+ this.logger.warn(`Could not upload saved test data: ${(0, errors_js_1.errorMessage)(error)}`);
102
103
  }
103
104
  }
104
105
  }
@@ -1,10 +1,10 @@
1
- import type { StreamEvent } from './types.js';
1
+ import type { StreamEvent } from '../../types.js';
2
2
  /**
3
3
  * Persistent JSONL buffer on disk. Events are appended to a temp file so they
4
4
  * survive a crash and can be replayed when the reporter restarts.
5
5
  */
6
6
  export declare class StreamBuffer {
7
- private filePath;
7
+ private readonly filePath;
8
8
  constructor(projectName: string);
9
9
  /** Append one or more events to the on-disk buffer */
10
10
  append(events: StreamEvent[]): void;
@@ -34,17 +34,17 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.StreamBuffer = void 0;
37
- const path = __importStar(require("path"));
38
- const os = __importStar(require("os"));
39
- const fs = __importStar(require("fs"));
40
- const helpers_js_1 = require("./helpers.js");
37
+ const path = __importStar(require("node:path"));
38
+ const os = __importStar(require("node:os"));
39
+ const fs = __importStar(require("node:fs"));
40
+ const instance_id_js_1 = require("../support/instance-id.js");
41
41
  /**
42
42
  * Persistent JSONL buffer on disk. Events are appended to a temp file so they
43
43
  * survive a crash and can be replayed when the reporter restarts.
44
44
  */
45
45
  class StreamBuffer {
46
46
  constructor(projectName) {
47
- this.filePath = path.join(os.tmpdir(), `piwi-dashboard-stream-${(0, helpers_js_1.hashForProject)(projectName)}.jsonl`);
47
+ this.filePath = path.join(os.tmpdir(), `piwi-dashboard-stream-${(0, instance_id_js_1.hashForProject)(projectName)}.jsonl`);
48
48
  }
49
49
  /** Append one or more events to the on-disk buffer */
50
50
  append(events) {
@@ -1,11 +1,11 @@
1
- import type { PiwiDashboardOptions, ShardInfo } from './config.js';
2
- import { HttpClient } from './http-client.js';
3
- import { StreamBuffer } from './stream-buffer.js';
4
- import { CrashRecovery } from './crash-recovery.js';
5
- import { Uploader } from './uploader.js';
6
- import { FileHandler } from './file-handler.js';
7
- import { Logger } from './logger.js';
8
- import type { CollectedTestCase, StreamEvent, FilterDetails } from './types.js';
1
+ import type { PiwiDashboardOptions, ShardInfo } from '../../public/options.js';
2
+ import type { HttpClient } from '../transport/http-client.js';
3
+ import type { StreamBuffer } from './stream-buffer.js';
4
+ import type { CrashRecovery } from './crash-recovery.js';
5
+ import type { Uploader } from '../submit/uploader.js';
6
+ import type { FileHandler } from '../files/file-handler.js';
7
+ import { Logger } from '../support/logger.js';
8
+ import type { CollectedTestCase, StreamEvent, FilterDetails } from '../../types.js';
9
9
  /**
10
10
  * Manages the streaming protocol: queues events (begin / complete), flushes
11
11
  * them in batches, schedules retries on failure, and handles per-test-case
@@ -28,6 +28,16 @@ export declare class StreamManager {
28
28
  private retryCount;
29
29
  private retryTimer;
30
30
  private readonly maxRetryDelay;
31
+ /**
32
+ * Idle heartbeat: while the run is open but no events are flowing (e.g. a single
33
+ * long test, or `beforeAll` setup), a periodic ping keeps the server's activity
34
+ * timestamp fresh so the stale-run reaper can tell a live run from a crashed one.
35
+ * The interval must stay well below the server's stale timeout.
36
+ */
37
+ private heartbeatTimer;
38
+ private heartbeatStopped;
39
+ private lastActivityAt;
40
+ private readonly heartbeatInterval;
31
41
  /** Tracks cases whose files have already been uploaded live, so `uploadRemaining` can skip them. */
32
42
  private readonly uploadedCaseFiles;
33
43
  private _enabled;
@@ -65,6 +75,10 @@ export declare class StreamManager {
65
75
  /** Flush all pending events to the server. Returns a promise that resolves to `true` on success or `false` on failure (events are re-queued for retry). */
66
76
  flush(): Promise<boolean> | null;
67
77
  private scheduleRetry;
78
+ private scheduleHeartbeat;
79
+ private sendHeartbeat;
80
+ /** Stop the idle heartbeat permanently (called when the run is wrapping up). */
81
+ private stopHeartbeat;
68
82
  /** Drain all pending and buffered events before the run finishes. Retries up to 10 times with exponential back-off. */
69
83
  drain(): Promise<void>;
70
84
  /** Schedule a live upload of trace and attachment files for a test case. Skips cases with no files. Concurrency is limited to 2 simultaneous uploads. */
@@ -34,9 +34,12 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.StreamManager = void 0;
37
- const fs = __importStar(require("fs"));
38
- const logger_js_1 = require("./logger.js");
39
- const helpers_js_1 = require("./helpers.js");
37
+ const fs = __importStar(require("node:fs"));
38
+ const errors_js_1 = require("../support/errors.js");
39
+ const http_client_js_1 = require("../transport/http-client.js");
40
+ const logger_js_1 = require("../support/logger.js");
41
+ const limiter_js_1 = require("../support/limiter.js");
42
+ const setup_file_js_1 = require("../support/setup-file.js");
40
43
  /**
41
44
  * Manages the streaming protocol: queues events (begin / complete), flushes
42
45
  * them in batches, schedules retries on failure, and handles per-test-case
@@ -85,10 +88,20 @@ class StreamManager {
85
88
  this.flushTimer = null;
86
89
  this.flushPromises = [];
87
90
  this.liveUploadPromises = [];
88
- this.limitLiveUpload = (0, helpers_js_1.createLimiter)(2);
91
+ this.limitLiveUpload = (0, limiter_js_1.createLimiter)(2);
89
92
  this.retryCount = 0;
90
93
  this.retryTimer = null;
91
94
  this.maxRetryDelay = 30000;
95
+ /**
96
+ * Idle heartbeat: while the run is open but no events are flowing (e.g. a single
97
+ * long test, or `beforeAll` setup), a periodic ping keeps the server's activity
98
+ * timestamp fresh so the stale-run reaper can tell a live run from a crashed one.
99
+ * The interval must stay well below the server's stale timeout.
100
+ */
101
+ this.heartbeatTimer = null;
102
+ this.heartbeatStopped = false;
103
+ this.lastActivityAt = 0;
104
+ this.heartbeatInterval = 15000;
92
105
  /** Tracks cases whose files have already been uploaded live, so `uploadRemaining` can skip them. */
93
106
  this.uploadedCaseFiles = new WeakSet();
94
107
  this._enabled = false;
@@ -102,7 +115,7 @@ class StreamManager {
102
115
  this._startPromise = this._doStart(startTime, metadata, instanceId, playwrightVersion, shardInfo, isFullRun, filterDetails);
103
116
  }
104
117
  async _doStart(startTime, metadata, instanceId, playwrightVersion, shardInfo, isFullRun, filterDetails) {
105
- const setupInfo = (0, helpers_js_1.readSetupInfo)(this.options.projectName);
118
+ const setupInfo = (0, setup_file_js_1.readSetupInfo)(this.options.projectName);
106
119
  try {
107
120
  this._auth = await this.httpClient.resolveAuth(this.options);
108
121
  await this.recovery.tryUpload(this.httpClient, this._auth);
@@ -122,11 +135,11 @@ class StreamManager {
122
135
  filterDetails,
123
136
  }, this._auth);
124
137
  }
125
- catch (beginError) {
138
+ catch (error) {
126
139
  // Stale setupInfo — the run was cancelled (e.g. by crash recovery submit).
127
140
  // Fall back to creating a fresh run instead of silently disabling streaming.
128
- if (!beginError.message?.includes('409'))
129
- throw beginError;
141
+ if (!(error instanceof http_client_js_1.HttpError && error.status === 409))
142
+ throw error;
130
143
  this.logger.debug(`Setup info expired, creating fresh run...`);
131
144
  response = await this.httpClient.postJSON('/api/test-runs/start', {
132
145
  projectName: this.options.projectName,
@@ -165,6 +178,8 @@ class StreamManager {
165
178
  this._token = response.streamToken;
166
179
  this._enabled = true;
167
180
  this.logger.info(`Streaming enabled. Run ID: ${response.runId}`);
181
+ this.lastActivityAt = Date.now();
182
+ this.scheduleHeartbeat();
168
183
  if (this.pendingBeginEvents.length > 0) {
169
184
  this.pendingEvents = [...this.pendingBeginEvents, ...this.pendingEvents];
170
185
  this.pendingBeginEvents = [];
@@ -173,7 +188,7 @@ class StreamManager {
173
188
  }
174
189
  }
175
190
  catch (error) {
176
- this.logger.debug(`Streaming not available: ${error.message}. Will use batch mode.`);
191
+ this.logger.debug(`Streaming not available: ${(0, errors_js_1.errorMessage)(error)}. Will use batch mode.`);
177
192
  this._enabled = false;
178
193
  }
179
194
  }
@@ -213,6 +228,7 @@ class StreamManager {
213
228
  .postJSON(`/api/test-runs/${this._runId}/events`, { streamToken: this._token, testCases: events }, this._auth)
214
229
  .then(() => {
215
230
  this.retryCount = 0;
231
+ this.lastActivityAt = Date.now();
216
232
  return true;
217
233
  }, () => {
218
234
  this.pendingEvents = events.concat(this.pendingEvents);
@@ -239,8 +255,46 @@ class StreamManager {
239
255
  this.flush();
240
256
  }, delay);
241
257
  }
258
+ // Schedule the next idle heartbeat. Self-rescheduling; cleared by stopHeartbeat.
259
+ scheduleHeartbeat() {
260
+ if (this.heartbeatStopped || this.heartbeatTimer)
261
+ return;
262
+ this.heartbeatTimer = setTimeout(() => {
263
+ this.heartbeatTimer = null;
264
+ void this.sendHeartbeat();
265
+ }, this.heartbeatInterval);
266
+ }
267
+ // Ping the server only when the run has actually been idle. Real event traffic
268
+ // already bumps the server's activity timestamp, so a recent flush (or pending
269
+ // events about to flush) makes the ping redundant — skip it and reschedule.
270
+ async sendHeartbeat() {
271
+ if (this.heartbeatStopped || !this._enabled || !this._runId || !this._token)
272
+ return;
273
+ const idleFor = Date.now() - this.lastActivityAt;
274
+ if (idleFor < this.heartbeatInterval || this.pendingEvents.length > 0) {
275
+ this.scheduleHeartbeat();
276
+ return;
277
+ }
278
+ try {
279
+ await this.httpClient.postJSON(`/api/test-runs/${this._runId}/heartbeat`, { streamToken: this._token }, this._auth);
280
+ this.lastActivityAt = Date.now();
281
+ }
282
+ catch (error) {
283
+ this.logger.debug(`Heartbeat failed: ${(0, errors_js_1.errorMessage)(error)}`);
284
+ }
285
+ this.scheduleHeartbeat();
286
+ }
287
+ /** Stop the idle heartbeat permanently (called when the run is wrapping up). */
288
+ stopHeartbeat() {
289
+ this.heartbeatStopped = true;
290
+ if (this.heartbeatTimer) {
291
+ clearTimeout(this.heartbeatTimer);
292
+ this.heartbeatTimer = null;
293
+ }
294
+ }
242
295
  /** Drain all pending and buffered events before the run finishes. Retries up to 10 times with exponential back-off. */
243
296
  async drain() {
297
+ this.stopHeartbeat();
244
298
  if (!this._enabled) {
245
299
  this.pendingEvents = [];
246
300
  this.flushPromises = [];
@@ -303,9 +357,9 @@ class StreamManager {
303
357
  return;
304
358
  }
305
359
  catch (error) {
306
- const retryable = error.message?.includes('404');
360
+ const retryable = error instanceof http_client_js_1.HttpError && error.status === 404;
307
361
  if (!retryable || attempt === delays.length - 1) {
308
- this.logger.debug(`Live file upload failed for "${tc.title}": ${error.message}`);
362
+ this.logger.debug(`Live file upload failed for "${tc.title}": ${(0, errors_js_1.errorMessage)(error)}`);
309
363
  return; // The end-of-run pass retries cases that failed here
310
364
  }
311
365
  }
@@ -330,7 +384,7 @@ class StreamManager {
330
384
  this.uploadedCaseFiles.add(tc);
331
385
  }
332
386
  catch (error) {
333
- this.logger.warn(`Failed to upload files for "${tc.title}": ${error.message}`);
387
+ this.logger.warn(`Failed to upload files for "${tc.title}": ${(0, errors_js_1.errorMessage)(error)}`);
334
388
  }
335
389
  }
336
390
  }
@@ -1,11 +1,11 @@
1
1
  import type { FullResult } from '@playwright/test/reporter';
2
- import type { PiwiDashboardOptions, ShardInfo } from './config.js';
3
- import { HttpClient } from './http-client.js';
4
- import { Uploader } from './uploader.js';
5
- import { CrashRecovery } from './crash-recovery.js';
6
- import { StreamManager } from './stream-manager.js';
7
- import { Logger } from './logger.js';
8
- import type { CollectedTestCase, SetupStep, FilterDetails } from './types.js';
2
+ import type { PiwiDashboardOptions, ShardInfo } from '../../public/options.js';
3
+ import type { HttpClient } from '../transport/http-client.js';
4
+ import type { Uploader } from './uploader.js';
5
+ import type { CrashRecovery } from '../streaming/crash-recovery.js';
6
+ import type { StreamManager } from '../streaming/stream-manager.js';
7
+ import { Logger } from '../support/logger.js';
8
+ import type { CollectedTestCase, SetupStep, FilterDetails } from '../../types.js';
9
9
  /**
10
10
  * Snapshot of everything the reporter has collected by `onEnd`, handed off to
11
11
  * the `RunSubmitter` so the reporter itself stays a thin collect-and-hand-off
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RunSubmitter = void 0;
4
- const logger_js_1 = require("./logger.js");
5
- const step_analyzer_js_1 = require("./step-analyzer.js");
4
+ const errors_js_1 = require("../support/errors.js");
5
+ const http_client_js_1 = require("../transport/http-client.js");
6
+ const logger_js_1 = require("../support/logger.js");
7
+ const step_analyzer_js_1 = require("../collect/step-analyzer.js");
6
8
  const serializer_js_1 = require("./serializer.js");
7
9
  /**
8
10
  * Owns the three-tier submit/fallback ladder:
@@ -54,7 +56,7 @@ class RunSubmitter {
54
56
  auth = sm?.auth ?? (await this.httpClient.resolveAuth(run.options));
55
57
  }
56
58
  catch (error) {
57
- this.logger.error(`Authentication failed: ${error.message}`);
59
+ this.logger.error(`Authentication failed: ${(0, errors_js_1.errorMessage)(error)}`);
58
60
  throw error;
59
61
  }
60
62
  if (sm?.enabled && sm?.runId != null) {
@@ -87,6 +89,7 @@ class RunSubmitter {
87
89
  totalTests: run.totalTests,
88
90
  passedTests: run.passedTests,
89
91
  failedTests: run.failedTests,
92
+ timedOutTests: run.timedOutTests,
90
93
  skippedTests: run.skippedTests,
91
94
  didNotRunTests: run.didNotRunTests,
92
95
  environment: run.options.environment,
@@ -114,6 +117,7 @@ class RunSubmitter {
114
117
  totalTests: run.totalTests,
115
118
  passedTests: run.passedTests,
116
119
  failedTests: run.failedTests,
120
+ timedOutTests: run.timedOutTests,
117
121
  skippedTests: run.skippedTests,
118
122
  didNotRunTests: run.didNotRunTests,
119
123
  flakyTests,
@@ -138,13 +142,13 @@ class RunSubmitter {
138
142
  await this.uploader.uploadReportsForStreamingRun(run.options.projectName, sm.runId, this.reportOptions(run), run.startTime, auth);
139
143
  }
140
144
  catch (error) {
141
- this.logger.warn(`Failed to upload reports for streaming run: ${error.message}`);
145
+ this.logger.warn(`Failed to upload reports for streaming run: ${(0, errors_js_1.errorMessage)(error)}`);
142
146
  }
143
147
  }
144
148
  return true;
145
149
  }
146
150
  catch (error) {
147
- this.logger.warn(`Failed to finalize streaming run: ${error.message}`);
151
+ this.logger.warn(`Failed to finalize streaming run: ${(0, errors_js_1.errorMessage)(error)}`);
148
152
  this.logger.info('Falling back to batch upload...');
149
153
  return false;
150
154
  }
@@ -156,9 +160,9 @@ class RunSubmitter {
156
160
  return true;
157
161
  }
158
162
  catch (error) {
159
- if (error.message?.includes('401') && !auth)
163
+ if (error instanceof http_client_js_1.HttpError && error.status === 401 && !auth)
160
164
  throw error;
161
- this.logger.warn(`Failed to upload with files: ${error.message}`);
165
+ this.logger.warn(`Failed to upload with files: ${(0, errors_js_1.errorMessage)(error)}`);
162
166
  this.logger.info('Falling back to JSON upload...');
163
167
  return false;
164
168
  }
@@ -172,9 +176,9 @@ class RunSubmitter {
172
176
  catch (error) {
173
177
  // If the server returned 401 and no auth was configured, this is a
174
178
  // configuration error — throw so the caller knows it's fatal.
175
- if (error.message?.includes('401') && !auth)
179
+ if (error instanceof http_client_js_1.HttpError && error.status === 401 && !auth)
176
180
  throw error;
177
- this.logger.error(`All upload methods failed: ${error.message}`);
181
+ this.logger.error(`All upload methods failed: ${(0, errors_js_1.errorMessage)(error)}`);
178
182
  // Save the wire-serialized form so the recovery file matches the
179
183
  // original submit payload (no raw attachments / internal fields).
180
184
  this.recovery.save((0, serializer_js_1.serializeRun)(payload, { includeTestCases: true }));
@@ -1,5 +1,5 @@
1
1
  import type { FullResult } from '@playwright/test/reporter';
2
- import type { CollectedTestCase, WireTestCase } from './types.js';
2
+ import type { CollectedTestCase, WireTestCase } from '../../types.js';
3
3
  import type { RunPayload } from './uploader.js';
4
4
  /**
5
5
  * Resolve the overall Piwi run status from Playwright's `FullResult.status`
@@ -64,6 +64,7 @@ function toWireTestCase(tc) {
64
64
  suitePath: rest.suitePath ?? null,
65
65
  suiteConfig: rest.suiteConfig ?? null,
66
66
  testAnnotations: rest.testAnnotations ?? null,
67
+ locatorSnapshots: rest.locatorSnapshots || null,
67
68
  };
68
69
  }
69
70
  /**
@@ -85,6 +86,7 @@ function serializeRun(payload, opts) {
85
86
  totalTests: payload.totalTests,
86
87
  passedTests: payload.passedTests,
87
88
  failedTests: payload.failedTests,
89
+ timedOutTests: payload.timedOutTests ?? 0,
88
90
  skippedTests: payload.skippedTests,
89
91
  didNotRunTests: payload.didNotRunTests ?? 0,
90
92
  environment: payload.environment ?? null,
@@ -1,7 +1,7 @@
1
- import { HttpClient } from './http-client.js';
2
- import { FileHandler } from './file-handler.js';
3
- import { Logger } from './logger.js';
4
- import type { CollectedTestCase, FilterDetails } from './types.js';
1
+ import type { HttpClient } from '../transport/http-client.js';
2
+ import type { FileHandler } from '../files/file-handler.js';
3
+ import { Logger } from '../support/logger.js';
4
+ import type { CollectedTestCase, FilterDetails } from '../../types.js';
5
5
  /** Payload for a batch test-run submission */
6
6
  export interface RunPayload {
7
7
  /** Name of the project the run belongs to */
@@ -17,6 +17,7 @@ export interface RunPayload {
17
17
  totalTests: number;
18
18
  passedTests: number;
19
19
  failedTests: number;
20
+ timedOutTests?: number;
20
21
  skippedTests: number;
21
22
  /** Tests that never executed (cut short by `maxFailures` or a serial-group failure) */
22
23
  didNotRunTests?: number;
@@ -60,15 +61,15 @@ export interface ReportOptions {
60
61
  * uploads for already-submitted streaming runs.
61
62
  */
62
63
  export declare class Uploader {
63
- private httpClient;
64
- private fileHandler;
65
- private logger;
64
+ private readonly httpClient;
65
+ private readonly fileHandler;
66
+ private readonly logger;
66
67
  /**
67
68
  * @param httpClient HTTP client for server communication.
68
69
  * @param fileHandler File discovery and compression helper.
69
70
  * @param logger Prefixed logger.
70
71
  */
71
- constructor(httpClient: HttpClient, fileHandler: FileHandler, logger: Logger);
72
+ constructor(httpClient: HttpClient, fileHandler: FileHandler, logger?: Logger);
72
73
  /** Submit test results as a plain JSON payload (no file attachments) */
73
74
  uploadJSON(payload: RunPayload, auth: string | null): Promise<any>;
74
75
  /** Submit test results as a multipart form with trace files and compressed report directories */
@@ -1,12 +1,47 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
5
38
  Object.defineProperty(exports, "__esModule", { value: true });
6
39
  exports.Uploader = void 0;
7
- const fs_1 = __importDefault(require("fs"));
8
- const path_1 = __importDefault(require("path"));
40
+ const fs = __importStar(require("node:fs"));
41
+ const path = __importStar(require("node:path"));
9
42
  const form_data_1 = __importDefault(require("form-data"));
43
+ const http_client_js_1 = require("../transport/http-client.js");
44
+ const logger_js_1 = require("../support/logger.js");
10
45
  const serializer_js_1 = require("./serializer.js");
11
46
  /**
12
47
  * Handles all upload strategies: plain JSON, multipart (with reports and
@@ -19,7 +54,7 @@ class Uploader {
19
54
  * @param fileHandler File discovery and compression helper.
20
55
  * @param logger Prefixed logger.
21
56
  */
22
- constructor(httpClient, fileHandler, logger) {
57
+ constructor(httpClient, fileHandler, logger = new logger_js_1.Logger()) {
23
58
  this.httpClient = httpClient;
24
59
  this.fileHandler = fileHandler;
25
60
  this.logger = logger;
@@ -104,15 +139,15 @@ class Uploader {
104
139
  if (traceInfo) {
105
140
  form.append('trace_hash', traceInfo.hash);
106
141
  if (includeTraceFile) {
107
- form.append('trace', fs_1.default.createReadStream(traceInfo.tracePath), {
108
- filename: path_1.default.basename(traceInfo.tracePath),
142
+ form.append('trace', fs.createReadStream(traceInfo.tracePath), {
143
+ filename: path.basename(traceInfo.tracePath),
109
144
  });
110
145
  }
111
146
  }
112
147
  if (attachments.length > 0) {
113
148
  form.append('attach_meta', JSON.stringify(attachments.map((a) => ({ name: a.name, contentType: a.contentType, originalName: a.originalName }))));
114
149
  for (const a of attachments) {
115
- form.append('attach_file', fs_1.default.createReadStream(a.path), { filename: a.originalName });
150
+ form.append('attach_file', fs.createReadStream(a.path), { filename: a.originalName });
116
151
  }
117
152
  }
118
153
  return form;
@@ -123,7 +158,7 @@ class Uploader {
123
158
  }
124
159
  catch (error) {
125
160
  // 422: the server doesn't have the blob after all — resend with the file
126
- if (traceInfo && !includeTraceFile && error.message?.includes('422')) {
161
+ if (traceInfo && !includeTraceFile && error instanceof http_client_js_1.HttpError && error.status === 422) {
127
162
  await this.httpClient.postFormData(`/api/test-runs/${runId}/case-files`, buildForm(true), auth);
128
163
  }
129
164
  else {
@@ -168,7 +203,7 @@ class Uploader {
168
203
  originalName: a.originalName,
169
204
  }))));
170
205
  for (const a of attachments) {
171
- form.append(`attach_file_${i}`, fs_1.default.createReadStream(a.path), { filename: a.originalName });
206
+ form.append(`attach_file_${i}`, fs.createReadStream(a.path), { filename: a.originalName });
172
207
  attachmentCount++;
173
208
  }
174
209
  }
@@ -179,8 +214,8 @@ class Uploader {
179
214
  let traceCount = 0;
180
215
  for (const [i, tc] of testCases.entries()) {
181
216
  for (const tp of this.fileHandler.findTraceFiles(tc)) {
182
- if (fs_1.default.existsSync(tp)) {
183
- form.append(`trace_${i}`, fs_1.default.createReadStream(tp), { filename: path_1.default.basename(tp) });
217
+ if (fs.existsSync(tp)) {
218
+ form.append(`trace_${i}`, fs.createReadStream(tp), { filename: path.basename(tp) });
184
219
  traceCount++;
185
220
  }
186
221
  }
@@ -0,0 +1,2 @@
1
+ /** Detect a stable CI run label from well-known environment variables. Returns null outside CI. */
2
+ export declare function detectCiRunLabel(): string | null;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectCiRunLabel = detectCiRunLabel;
4
+ /** Detect a stable CI run label from well-known environment variables. Returns null outside CI. */
5
+ function detectCiRunLabel() {
6
+ const env = process.env;
7
+ if (env.GITHUB_ACTIONS && env.GITHUB_RUN_ID)
8
+ return env.GITHUB_RUN_ID;
9
+ if (env.GITLAB_CI && env.CI_PIPELINE_ID)
10
+ return env.CI_PIPELINE_ID;
11
+ if (env.CIRCLECI && env.CIRCLE_WORKFLOW_ID)
12
+ return env.CIRCLE_WORKFLOW_ID;
13
+ if (env.TRAVIS && env.TRAVIS_BUILD_ID)
14
+ return env.TRAVIS_BUILD_ID;
15
+ if (env.TF_BUILD && env.BUILD_BUILDID)
16
+ return env.BUILD_BUILDID;
17
+ if (env.JENKINS_URL && env.BUILD_ID)
18
+ return env.BUILD_ID;
19
+ if (env.BUILDKITE_BUILD_ID)
20
+ return env.BUILDKITE_BUILD_ID;
21
+ if (env.TEAMCITY_BUILD_ID)
22
+ return env.TEAMCITY_BUILD_ID;
23
+ if (env.BITBUCKET_BUILD_NUMBER)
24
+ return env.BITBUCKET_BUILD_NUMBER;
25
+ if (env.SEMAPHORE_WORKFLOW_ID)
26
+ return env.SEMAPHORE_WORKFLOW_ID;
27
+ if (env.APPVEYOR_BUILD_ID)
28
+ return env.APPVEYOR_BUILD_ID;
29
+ if (env.DRONE_BUILD_NUMBER)
30
+ return env.DRONE_BUILD_NUMBER;
31
+ return null;
32
+ }
@@ -0,0 +1 @@
1
+ export declare function detectCliFileFilters(argv?: string[]): string[];