@piwitests/reporter 0.11.0 → 0.13.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.
- package/dist/global-setup-module.d.ts +2 -1
- package/dist/global-setup-module.js +432 -3
- package/dist/index.d.ts +204 -8
- package/dist/index.js +4724 -23
- package/dist/internal/capture/attachments.d.ts +9 -3
- package/dist/internal/capture/attachments.js +46 -20
- package/dist/internal/capture/capture-fixtures.d.ts +110 -7
- package/dist/internal/capture/capture-fixtures.js +2227 -639
- package/dist/internal/capture/inspect-on-failure.d.ts +50 -0
- package/dist/internal/capture/inspect-on-failure.js +66 -0
- package/dist/internal/capture/locator-healing.d.ts +33 -131
- package/dist/internal/capture/locator-healing.js +602 -711
- package/dist/internal/capture/pick-on-failure.d.ts +198 -0
- package/dist/internal/capture/pick-on-failure.js +1203 -0
- package/package.json +9 -4
- package/dist/internal/collect/error-text.d.ts +0 -18
- package/dist/internal/collect/error-text.js +0 -79
- package/dist/internal/collect/metadata-collector.d.ts +0 -32
- package/dist/internal/collect/metadata-collector.js +0 -246
- package/dist/internal/collect/skip-classify.d.ts +0 -27
- package/dist/internal/collect/skip-classify.js +0 -40
- package/dist/internal/collect/step-analyzer.d.ts +0 -103
- package/dist/internal/collect/step-analyzer.js +0 -221
- package/dist/internal/config/env.d.ts +0 -45
- package/dist/internal/config/env.js +0 -153
- package/dist/internal/files/compression.d.ts +0 -5
- package/dist/internal/files/compression.js +0 -69
- package/dist/internal/files/file-handler.d.ts +0 -38
- package/dist/internal/files/file-handler.js +0 -198
- package/dist/internal/streaming/crash-recovery.d.ts +0 -23
- package/dist/internal/streaming/crash-recovery.js +0 -106
- package/dist/internal/streaming/stream-buffer.d.ts +0 -17
- package/dist/internal/streaming/stream-buffer.js +0 -102
- package/dist/internal/streaming/stream-manager.d.ts +0 -88
- package/dist/internal/streaming/stream-manager.js +0 -395
- package/dist/internal/submit/run-submitter.d.ts +0 -67
- package/dist/internal/submit/run-submitter.js +0 -190
- package/dist/internal/submit/serializer.d.ts +0 -45
- package/dist/internal/submit/serializer.js +0 -107
- package/dist/internal/submit/uploader.d.ts +0 -89
- package/dist/internal/submit/uploader.js +0 -226
- package/dist/internal/support/ci.d.ts +0 -2
- package/dist/internal/support/ci.js +0 -32
- package/dist/internal/support/cli-filters.d.ts +0 -1
- package/dist/internal/support/cli-filters.js +0 -51
- package/dist/internal/support/errors.d.ts +0 -8
- package/dist/internal/support/errors.js +0 -15
- package/dist/internal/support/instance-id.d.ts +0 -4
- package/dist/internal/support/instance-id.js +0 -48
- package/dist/internal/support/limiter.d.ts +0 -2
- package/dist/internal/support/limiter.js +0 -27
- package/dist/internal/support/logger.d.ts +0 -26
- package/dist/internal/support/logger.js +0 -43
- package/dist/internal/support/reporter-version.d.ts +0 -2
- package/dist/internal/support/reporter-version.js +0 -54
- package/dist/internal/support/setup-file.d.ts +0 -13
- package/dist/internal/support/setup-file.js +0 -61
- package/dist/internal/support/source-snippet.d.ts +0 -12
- package/dist/internal/support/source-snippet.js +0 -97
- package/dist/internal/support/worker-index.d.ts +0 -7
- package/dist/internal/support/worker-index.js +0 -14
- package/dist/internal/transport/http-client.d.ts +0 -52
- package/dist/internal/transport/http-client.js +0 -201
- package/dist/public/config-wrapper.d.ts +0 -21
- package/dist/public/config-wrapper.js +0 -64
- package/dist/public/global-setup.d.ts +0 -13
- package/dist/public/global-setup.js +0 -146
- package/dist/public/options.d.ts +0 -78
- package/dist/public/options.js +0 -2
- package/dist/public/reporter.d.ts +0 -68
- package/dist/public/reporter.js +0 -376
- package/dist/types/collected.d.ts +0 -96
- package/dist/types/collected.js +0 -10
- package/dist/types/wire.d.ts +0 -174
- package/dist/types/wire.js +0 -14
- package/dist/types.d.ts +0 -11
- package/dist/types.js +0 -27
|
@@ -1,395 +0,0 @@
|
|
|
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
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.StreamManager = void 0;
|
|
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");
|
|
43
|
-
/**
|
|
44
|
-
* Manages the streaming protocol: queues events (begin / complete), flushes
|
|
45
|
-
* them in batches, schedules retries on failure, and handles per-test-case
|
|
46
|
-
* live file uploads.
|
|
47
|
-
*/
|
|
48
|
-
class StreamManager {
|
|
49
|
-
/** Whether the streaming session is active */
|
|
50
|
-
get enabled() {
|
|
51
|
-
return this._enabled;
|
|
52
|
-
}
|
|
53
|
-
/** Server-assigned run ID (`null` until the stream opens) */
|
|
54
|
-
get runId() {
|
|
55
|
-
return this._runId;
|
|
56
|
-
}
|
|
57
|
-
/** Stream authentication token (`null` until the stream opens) */
|
|
58
|
-
get token() {
|
|
59
|
-
return this._token;
|
|
60
|
-
}
|
|
61
|
-
/** Resolved auth string (API key or session cookie) used in stream requests */
|
|
62
|
-
get auth() {
|
|
63
|
-
return this._auth;
|
|
64
|
-
}
|
|
65
|
-
/** Promise that resolves when the stream has been fully initialised */
|
|
66
|
-
get startPromise() {
|
|
67
|
-
return this._startPromise;
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* @param httpClient HTTP client for server communication.
|
|
71
|
-
* @param streamBuffer On-disk buffer for crash-safe event persistence.
|
|
72
|
-
* @param recovery Crash-recovery handler for uploading stale payloads on startup.
|
|
73
|
-
* @param uploader Uploader for per-test-case file uploads.
|
|
74
|
-
* @param fileHandler File-discovery helper for finding traces and attachments.
|
|
75
|
-
* @param options Piwi Dashboard reporter options.
|
|
76
|
-
* @param logger Prefixed logger.
|
|
77
|
-
*/
|
|
78
|
-
constructor(httpClient, streamBuffer, recovery, uploader, fileHandler, options, logger = new logger_js_1.Logger()) {
|
|
79
|
-
this.httpClient = httpClient;
|
|
80
|
-
this.streamBuffer = streamBuffer;
|
|
81
|
-
this.recovery = recovery;
|
|
82
|
-
this.uploader = uploader;
|
|
83
|
-
this.fileHandler = fileHandler;
|
|
84
|
-
this.options = options;
|
|
85
|
-
this.logger = logger;
|
|
86
|
-
this.pendingEvents = [];
|
|
87
|
-
this.pendingBeginEvents = [];
|
|
88
|
-
this.flushTimer = null;
|
|
89
|
-
this.flushPromises = [];
|
|
90
|
-
this.liveUploadPromises = [];
|
|
91
|
-
this.limitLiveUpload = (0, limiter_js_1.createLimiter)(2);
|
|
92
|
-
this.retryCount = 0;
|
|
93
|
-
this.retryTimer = null;
|
|
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;
|
|
105
|
-
/** Tracks cases whose files have already been uploaded live, so `uploadRemaining` can skip them. */
|
|
106
|
-
this.uploadedCaseFiles = new WeakSet();
|
|
107
|
-
this._enabled = false;
|
|
108
|
-
this._runId = null;
|
|
109
|
-
this._token = null;
|
|
110
|
-
this._auth = null;
|
|
111
|
-
this._startPromise = null;
|
|
112
|
-
}
|
|
113
|
-
/** Begin the streaming session after `onBegin` fires. Non-blocking — the actual handshake runs asynchronously. */
|
|
114
|
-
start(startTime, metadata, instanceId, playwrightVersion, reporterVersion, shardInfo, isFullRun, filterDetails) {
|
|
115
|
-
this._startPromise = this._doStart(startTime, metadata, instanceId, playwrightVersion, reporterVersion, shardInfo, isFullRun, filterDetails);
|
|
116
|
-
}
|
|
117
|
-
async _doStart(startTime, metadata, instanceId, playwrightVersion, reporterVersion, shardInfo, isFullRun, filterDetails) {
|
|
118
|
-
const setupInfo = (0, setup_file_js_1.readSetupInfo)(this.options.projectName);
|
|
119
|
-
try {
|
|
120
|
-
this._auth = await this.httpClient.resolveAuth(this.options);
|
|
121
|
-
await this.recovery.tryUpload(this.httpClient, this._auth);
|
|
122
|
-
let response;
|
|
123
|
-
const shardIndex = shardInfo?.current;
|
|
124
|
-
const shardTotal = shardInfo?.total;
|
|
125
|
-
if (setupInfo) {
|
|
126
|
-
try {
|
|
127
|
-
response = await this.httpClient.postJSON(`/api/test-runs/${setupInfo.runId}/begin`, {
|
|
128
|
-
setupToken: setupInfo.setupToken,
|
|
129
|
-
totalTests: 0,
|
|
130
|
-
metadata,
|
|
131
|
-
playwrightVersion,
|
|
132
|
-
reporterVersion,
|
|
133
|
-
shardIndex,
|
|
134
|
-
shardTotal,
|
|
135
|
-
isFullRun,
|
|
136
|
-
filterDetails,
|
|
137
|
-
}, this._auth);
|
|
138
|
-
}
|
|
139
|
-
catch (error) {
|
|
140
|
-
// Stale setupInfo — the run was cancelled (e.g. by crash recovery submit).
|
|
141
|
-
// Fall back to creating a fresh run instead of silently disabling streaming.
|
|
142
|
-
if (!(error instanceof http_client_js_1.HttpError && error.status === 409))
|
|
143
|
-
throw error;
|
|
144
|
-
this.logger.debug(`Setup info expired, creating fresh run...`);
|
|
145
|
-
response = await this.httpClient.postJSON('/api/test-runs/start', {
|
|
146
|
-
projectName: this.options.projectName,
|
|
147
|
-
projectDescription: this.options.projectDescription,
|
|
148
|
-
startTime,
|
|
149
|
-
environment: this.options.environment || null,
|
|
150
|
-
label: this.options.label || null,
|
|
151
|
-
metadata,
|
|
152
|
-
instanceId,
|
|
153
|
-
playwrightVersion,
|
|
154
|
-
reporterVersion,
|
|
155
|
-
shardIndex,
|
|
156
|
-
shardTotal,
|
|
157
|
-
isFullRun,
|
|
158
|
-
filterDetails,
|
|
159
|
-
}, this._auth);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
response = await this.httpClient.postJSON('/api/test-runs/start', {
|
|
164
|
-
projectName: this.options.projectName,
|
|
165
|
-
projectDescription: this.options.projectDescription,
|
|
166
|
-
startTime,
|
|
167
|
-
environment: this.options.environment || null,
|
|
168
|
-
label: this.options.label || null,
|
|
169
|
-
metadata,
|
|
170
|
-
instanceId,
|
|
171
|
-
playwrightVersion,
|
|
172
|
-
reporterVersion,
|
|
173
|
-
shardIndex,
|
|
174
|
-
shardTotal,
|
|
175
|
-
isFullRun,
|
|
176
|
-
filterDetails,
|
|
177
|
-
}, this._auth);
|
|
178
|
-
}
|
|
179
|
-
if (response?.runId && response?.streamToken) {
|
|
180
|
-
this._runId = response.runId;
|
|
181
|
-
this._token = response.streamToken;
|
|
182
|
-
this._enabled = true;
|
|
183
|
-
this.logger.info(`Streaming enabled. Run ID: ${response.runId}`);
|
|
184
|
-
this.lastActivityAt = Date.now();
|
|
185
|
-
this.scheduleHeartbeat();
|
|
186
|
-
if (this.pendingBeginEvents.length > 0) {
|
|
187
|
-
this.pendingEvents = [...this.pendingBeginEvents, ...this.pendingEvents];
|
|
188
|
-
this.pendingBeginEvents = [];
|
|
189
|
-
this.flush();
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
catch (error) {
|
|
194
|
-
this.logger.debug(`Streaming not available: ${(0, errors_js_1.errorMessage)(error)}. Will use batch mode.`);
|
|
195
|
-
this._enabled = false;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
// Queues a begin event; held in a pre-start buffer until the stream is open,
|
|
199
|
-
// then prepended to the main queue so it arrives before the matching complete event.
|
|
200
|
-
/** Queue a test-case `begin` event. Held in a pre-start buffer if the stream is not yet open, then prepended so it arrives before the matching `complete` event. */
|
|
201
|
-
queueBeginEvent(event) {
|
|
202
|
-
if (this._enabled && this._runId) {
|
|
203
|
-
this.queueEvent(event);
|
|
204
|
-
}
|
|
205
|
-
else {
|
|
206
|
-
this.pendingBeginEvents.push(event);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
/** Queue a test-case event. Triggers an immediate flush when the batch size is reached, otherwise schedules a timer-based flush. */
|
|
210
|
-
queueEvent(event) {
|
|
211
|
-
this.pendingEvents.push(event);
|
|
212
|
-
if (this.pendingEvents.length >= this.options.streamingBatchSize) {
|
|
213
|
-
this.flush();
|
|
214
|
-
}
|
|
215
|
-
else if (!this.flushTimer) {
|
|
216
|
-
this.flushTimer = setTimeout(() => this.flush(), this.options.streamingBatchDelay);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
/** 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). */
|
|
220
|
-
flush() {
|
|
221
|
-
if (this.flushTimer) {
|
|
222
|
-
clearTimeout(this.flushTimer);
|
|
223
|
-
this.flushTimer = null;
|
|
224
|
-
}
|
|
225
|
-
if (this.pendingEvents.length === 0 || !this._enabled || !this._runId)
|
|
226
|
-
return null;
|
|
227
|
-
const events = this.pendingEvents.splice(0);
|
|
228
|
-
// Never rejects: failed events are re-queued so the retry timer or the
|
|
229
|
-
// end-of-run drain can resend them (the server deduplicates).
|
|
230
|
-
const promise = this.httpClient
|
|
231
|
-
.postJSON(`/api/test-runs/${this._runId}/events`, { streamToken: this._token, testCases: events }, this._auth)
|
|
232
|
-
.then(() => {
|
|
233
|
-
this.retryCount = 0;
|
|
234
|
-
this.lastActivityAt = Date.now();
|
|
235
|
-
return true;
|
|
236
|
-
}, () => {
|
|
237
|
-
this.pendingEvents = events.concat(this.pendingEvents);
|
|
238
|
-
this.scheduleRetry();
|
|
239
|
-
return false;
|
|
240
|
-
});
|
|
241
|
-
this.flushPromises.push(promise);
|
|
242
|
-
return promise;
|
|
243
|
-
}
|
|
244
|
-
scheduleRetry() {
|
|
245
|
-
if (this.retryTimer)
|
|
246
|
-
return;
|
|
247
|
-
this.retryCount++;
|
|
248
|
-
const delay = Math.min(1000 * Math.pow(2, this.retryCount - 1), this.maxRetryDelay);
|
|
249
|
-
this.logger.debug(`Will retry streaming flush in ${delay}ms (attempt ${this.retryCount})`);
|
|
250
|
-
this.retryTimer = setTimeout(() => {
|
|
251
|
-
this.retryTimer = null;
|
|
252
|
-
const buffered = this.streamBuffer.load();
|
|
253
|
-
if (buffered.length > 0) {
|
|
254
|
-
this.streamBuffer.clear();
|
|
255
|
-
this.pendingEvents = buffered.concat(this.pendingEvents);
|
|
256
|
-
}
|
|
257
|
-
if (this.pendingEvents.length > 0)
|
|
258
|
-
this.flush();
|
|
259
|
-
}, delay);
|
|
260
|
-
}
|
|
261
|
-
// Schedule the next idle heartbeat. Self-rescheduling; cleared by stopHeartbeat.
|
|
262
|
-
scheduleHeartbeat() {
|
|
263
|
-
if (this.heartbeatStopped || this.heartbeatTimer)
|
|
264
|
-
return;
|
|
265
|
-
this.heartbeatTimer = setTimeout(() => {
|
|
266
|
-
this.heartbeatTimer = null;
|
|
267
|
-
void this.sendHeartbeat();
|
|
268
|
-
}, this.heartbeatInterval);
|
|
269
|
-
}
|
|
270
|
-
// Ping the server only when the run has actually been idle. Real event traffic
|
|
271
|
-
// already bumps the server's activity timestamp, so a recent flush (or pending
|
|
272
|
-
// events about to flush) makes the ping redundant — skip it and reschedule.
|
|
273
|
-
async sendHeartbeat() {
|
|
274
|
-
if (this.heartbeatStopped || !this._enabled || !this._runId || !this._token)
|
|
275
|
-
return;
|
|
276
|
-
const idleFor = Date.now() - this.lastActivityAt;
|
|
277
|
-
if (idleFor < this.heartbeatInterval || this.pendingEvents.length > 0) {
|
|
278
|
-
this.scheduleHeartbeat();
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
try {
|
|
282
|
-
await this.httpClient.postJSON(`/api/test-runs/${this._runId}/heartbeat`, { streamToken: this._token }, this._auth);
|
|
283
|
-
this.lastActivityAt = Date.now();
|
|
284
|
-
}
|
|
285
|
-
catch (error) {
|
|
286
|
-
this.logger.debug(`Heartbeat failed: ${(0, errors_js_1.errorMessage)(error)}`);
|
|
287
|
-
}
|
|
288
|
-
this.scheduleHeartbeat();
|
|
289
|
-
}
|
|
290
|
-
/** Stop the idle heartbeat permanently (called when the run is wrapping up). */
|
|
291
|
-
stopHeartbeat() {
|
|
292
|
-
this.heartbeatStopped = true;
|
|
293
|
-
if (this.heartbeatTimer) {
|
|
294
|
-
clearTimeout(this.heartbeatTimer);
|
|
295
|
-
this.heartbeatTimer = null;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
/** Drain all pending and buffered events before the run finishes. Retries up to 10 times with exponential back-off. */
|
|
299
|
-
async drain() {
|
|
300
|
-
this.stopHeartbeat();
|
|
301
|
-
if (!this._enabled) {
|
|
302
|
-
this.pendingEvents = [];
|
|
303
|
-
this.flushPromises = [];
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
const MAX_ATTEMPTS = 10;
|
|
307
|
-
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
308
|
-
if (this._enabled && this.pendingEvents.length > 0)
|
|
309
|
-
this.flush();
|
|
310
|
-
if (this.flushPromises.length > 0) {
|
|
311
|
-
await Promise.allSettled(this.flushPromises);
|
|
312
|
-
this.flushPromises = [];
|
|
313
|
-
}
|
|
314
|
-
if (this.pendingEvents.length === 0) {
|
|
315
|
-
const buffered = this.streamBuffer.load();
|
|
316
|
-
if (buffered.length > 0) {
|
|
317
|
-
this.pendingEvents = buffered;
|
|
318
|
-
this.streamBuffer.clear();
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
this.logger.debugError(`${this.pendingEvents.length} events pending, retrying (attempt ${attempt + 1}/${MAX_ATTEMPTS})...`);
|
|
324
|
-
await new Promise((resolve) => setTimeout(resolve, Math.min(1000 * Math.pow(2, attempt), 10000)));
|
|
325
|
-
}
|
|
326
|
-
if (this.pendingEvents.length > 0) {
|
|
327
|
-
this.streamBuffer.append(this.pendingEvents);
|
|
328
|
-
this.pendingEvents = [];
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
/** 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. */
|
|
332
|
-
scheduleLiveUpload(tc) {
|
|
333
|
-
const hasTrace = !!this.options.uploadTraces && this.fileHandler.findTraceFiles(tc).some((p) => fs.existsSync(p));
|
|
334
|
-
const hasAttachments = this.fileHandler.findAllAttachments(tc).length > 0;
|
|
335
|
-
if (!hasTrace && !hasAttachments)
|
|
336
|
-
return;
|
|
337
|
-
const promise = (async () => {
|
|
338
|
-
if (this._startPromise)
|
|
339
|
-
await this._startPromise;
|
|
340
|
-
if (!this._enabled || !this._runId || !this._token)
|
|
341
|
-
return;
|
|
342
|
-
// The complete event must reach the server before it can link files.
|
|
343
|
-
// queueEvent may have already triggered a batch-size flush that took all
|
|
344
|
-
// pending events, making our flush() return null. Wait for any in-flight
|
|
345
|
-
// flush promises first, then try to flush remaining events.
|
|
346
|
-
if (this.flushPromises.length > 0) {
|
|
347
|
-
await Promise.allSettled(this.flushPromises);
|
|
348
|
-
}
|
|
349
|
-
const flush = this.flush();
|
|
350
|
-
if (flush)
|
|
351
|
-
await flush;
|
|
352
|
-
// Retry on 404: the events batch carrying this case may still be in flight
|
|
353
|
-
const delays = [0, 1000, 3000];
|
|
354
|
-
for (let attempt = 0; attempt < delays.length; attempt++) {
|
|
355
|
-
if (delays[attempt])
|
|
356
|
-
await new Promise((resolve) => setTimeout(resolve, delays[attempt]));
|
|
357
|
-
try {
|
|
358
|
-
await this.limitLiveUpload(() => this.uploader.uploadCaseFiles(this.options.projectName, this._runId, this._token, tc, this.options.uploadTraces, this._auth));
|
|
359
|
-
this.uploadedCaseFiles.add(tc);
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
catch (error) {
|
|
363
|
-
const retryable = error instanceof http_client_js_1.HttpError && error.status === 404;
|
|
364
|
-
if (!retryable || attempt === delays.length - 1) {
|
|
365
|
-
this.logger.debug(`Live file upload failed for "${tc.title}": ${(0, errors_js_1.errorMessage)(error)}`);
|
|
366
|
-
return; // The end-of-run pass retries cases that failed here
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
})();
|
|
371
|
-
this.liveUploadPromises.push(promise);
|
|
372
|
-
}
|
|
373
|
-
/** Wait for all live uploads to settle, then upload files for any test cases that weren't uploaded live */
|
|
374
|
-
async uploadRemaining(testCases) {
|
|
375
|
-
if (this.liveUploadPromises.length > 0) {
|
|
376
|
-
await Promise.allSettled(this.liveUploadPromises);
|
|
377
|
-
this.liveUploadPromises = [];
|
|
378
|
-
}
|
|
379
|
-
if (!this._enabled || !this._runId || !this._token)
|
|
380
|
-
return;
|
|
381
|
-
for (const tc of testCases) {
|
|
382
|
-
if (this.uploadedCaseFiles.has(tc))
|
|
383
|
-
continue;
|
|
384
|
-
try {
|
|
385
|
-
const uploaded = await this.uploader.uploadCaseFiles(this.options.projectName, this._runId, this._token, tc, this.options.uploadTraces, this._auth);
|
|
386
|
-
if (uploaded)
|
|
387
|
-
this.uploadedCaseFiles.add(tc);
|
|
388
|
-
}
|
|
389
|
-
catch (error) {
|
|
390
|
-
this.logger.warn(`Failed to upload files for "${tc.title}": ${(0, errors_js_1.errorMessage)(error)}`);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
exports.StreamManager = StreamManager;
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import type { FullResult } from '@playwright/test/reporter';
|
|
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
|
-
/**
|
|
10
|
-
* Snapshot of everything the reporter has collected by `onEnd`, handed off to
|
|
11
|
-
* the `RunSubmitter` so the reporter itself stays a thin collect-and-hand-off
|
|
12
|
-
* shell.
|
|
13
|
-
*/
|
|
14
|
-
export interface CollectedRun {
|
|
15
|
-
options: PiwiDashboardOptions;
|
|
16
|
-
testCases: CollectedTestCase[];
|
|
17
|
-
startTime: string | null;
|
|
18
|
-
playwrightVersion: string | null;
|
|
19
|
-
reporterVersion: string | null;
|
|
20
|
-
totalTests: number;
|
|
21
|
-
passedTests: number;
|
|
22
|
-
failedTests: number;
|
|
23
|
-
skippedTests: number;
|
|
24
|
-
timedOutTests: number;
|
|
25
|
-
didNotRunTests: number;
|
|
26
|
-
metadata: Record<string, any>;
|
|
27
|
-
instanceId: string;
|
|
28
|
-
shardInfo: ShardInfo | null;
|
|
29
|
-
setupSteps: SetupStep[];
|
|
30
|
-
isFullRun: boolean;
|
|
31
|
-
filterDetails: FilterDetails | null;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Owns the three-tier submit/fallback ladder:
|
|
35
|
-
*
|
|
36
|
-
* 1. finalize the streaming run (`/finish`) when streaming is active,
|
|
37
|
-
* 2. fall back to multipart upload (`/upload`) when there are reports or
|
|
38
|
-
* traces to attach,
|
|
39
|
-
* 3. fall back to plain JSON (`/submit`) as the last resort, persisting a
|
|
40
|
-
* recovery payload on total failure.
|
|
41
|
-
*
|
|
42
|
-
* The order and logging are identical to the pre-extraction reporter — this is
|
|
43
|
-
* a move, not a redesign.
|
|
44
|
-
*/
|
|
45
|
-
export declare class RunSubmitter {
|
|
46
|
-
private readonly httpClient;
|
|
47
|
-
private readonly uploader;
|
|
48
|
-
private readonly recovery;
|
|
49
|
-
private readonly streamManager;
|
|
50
|
-
private readonly logger;
|
|
51
|
-
/**
|
|
52
|
-
* @param httpClient HTTP client for auth resolution and the finish call.
|
|
53
|
-
* @param uploader Upload strategies (multipart + JSON).
|
|
54
|
-
* @param recovery Crash-recovery persistence.
|
|
55
|
-
* @param streamManager Streaming session (may be `null` when streaming is disabled).
|
|
56
|
-
* @param logger Prefixed logger.
|
|
57
|
-
*/
|
|
58
|
-
constructor(httpClient: HttpClient, uploader: Uploader, recovery: CrashRecovery, streamManager: StreamManager | null, logger?: Logger);
|
|
59
|
-
/** Run the fallback ladder for a completed test run. */
|
|
60
|
-
submit(run: CollectedRun, result: FullResult): Promise<void>;
|
|
61
|
-
private hasReports;
|
|
62
|
-
private reportOptions;
|
|
63
|
-
private buildRunPayload;
|
|
64
|
-
private tryFinishStreaming;
|
|
65
|
-
private tryUploadWithFiles;
|
|
66
|
-
private tryUploadJSON;
|
|
67
|
-
}
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.RunSubmitter = void 0;
|
|
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");
|
|
8
|
-
const serializer_js_1 = require("./serializer.js");
|
|
9
|
-
/**
|
|
10
|
-
* Owns the three-tier submit/fallback ladder:
|
|
11
|
-
*
|
|
12
|
-
* 1. finalize the streaming run (`/finish`) when streaming is active,
|
|
13
|
-
* 2. fall back to multipart upload (`/upload`) when there are reports or
|
|
14
|
-
* traces to attach,
|
|
15
|
-
* 3. fall back to plain JSON (`/submit`) as the last resort, persisting a
|
|
16
|
-
* recovery payload on total failure.
|
|
17
|
-
*
|
|
18
|
-
* The order and logging are identical to the pre-extraction reporter — this is
|
|
19
|
-
* a move, not a redesign.
|
|
20
|
-
*/
|
|
21
|
-
class RunSubmitter {
|
|
22
|
-
/**
|
|
23
|
-
* @param httpClient HTTP client for auth resolution and the finish call.
|
|
24
|
-
* @param uploader Upload strategies (multipart + JSON).
|
|
25
|
-
* @param recovery Crash-recovery persistence.
|
|
26
|
-
* @param streamManager Streaming session (may be `null` when streaming is disabled).
|
|
27
|
-
* @param logger Prefixed logger.
|
|
28
|
-
*/
|
|
29
|
-
constructor(httpClient, uploader, recovery, streamManager, logger = new logger_js_1.Logger()) {
|
|
30
|
-
this.httpClient = httpClient;
|
|
31
|
-
this.uploader = uploader;
|
|
32
|
-
this.recovery = recovery;
|
|
33
|
-
this.streamManager = streamManager;
|
|
34
|
-
this.logger = logger;
|
|
35
|
-
}
|
|
36
|
-
/** Run the fallback ladder for a completed test run. */
|
|
37
|
-
async submit(run, result) {
|
|
38
|
-
const endTime = new Date().toISOString();
|
|
39
|
-
const duration = new Date(endTime).getTime() - new Date(run.startTime).getTime();
|
|
40
|
-
const overallStatus = (0, serializer_js_1.resolveOverallStatus)(result, {
|
|
41
|
-
failedTests: run.failedTests,
|
|
42
|
-
timedOutTests: run.timedOutTests,
|
|
43
|
-
totalTests: run.totalTests,
|
|
44
|
-
});
|
|
45
|
-
this.logger.info(`Test run completed. Status: ${overallStatus} (Playwright result.status: ${result?.status || 'undefined'})`);
|
|
46
|
-
this.logger.info(`Total: ${run.totalTests}, Passed: ${run.passedTests}, Failed: ${run.failedTests}, Skipped: ${run.skippedTests}, TimedOut: ${run.timedOutTests}, DidNotRun: ${run.didNotRunTests}`);
|
|
47
|
-
if (run.options.collectPerformanceMetrics) {
|
|
48
|
-
run.metadata.performance = (0, step_analyzer_js_1.computePerformanceSummary)(run.testCases);
|
|
49
|
-
}
|
|
50
|
-
const sm = this.streamManager;
|
|
51
|
-
if (sm?.startPromise)
|
|
52
|
-
await sm.startPromise;
|
|
53
|
-
await sm?.drain();
|
|
54
|
-
let auth;
|
|
55
|
-
try {
|
|
56
|
-
auth = sm?.auth ?? (await this.httpClient.resolveAuth(run.options));
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
this.logger.error(`Authentication failed: ${(0, errors_js_1.errorMessage)(error)}`);
|
|
60
|
-
throw error;
|
|
61
|
-
}
|
|
62
|
-
if (sm?.enabled && sm?.runId != null) {
|
|
63
|
-
if (await this.tryFinishStreaming(run, overallStatus, duration, auth))
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
if (this.hasReports(run) || run.options.uploadTraces) {
|
|
67
|
-
if (await this.tryUploadWithFiles(run, overallStatus, duration, auth))
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
await this.tryUploadJSON(run, overallStatus, duration, auth);
|
|
71
|
-
}
|
|
72
|
-
hasReports(run) {
|
|
73
|
-
return !!run.options.uploadReport || (run.options.reports?.length ?? 0) > 0;
|
|
74
|
-
}
|
|
75
|
-
reportOptions(run) {
|
|
76
|
-
return {
|
|
77
|
-
uploadTraces: run.options.uploadTraces,
|
|
78
|
-
uploadReport: run.options.uploadReport,
|
|
79
|
-
reports: run.options.reports,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
buildRunPayload(run, status, duration) {
|
|
83
|
-
return {
|
|
84
|
-
projectName: run.options.projectName,
|
|
85
|
-
projectDescription: run.options.projectDescription,
|
|
86
|
-
status,
|
|
87
|
-
startTime: run.startTime,
|
|
88
|
-
duration,
|
|
89
|
-
totalTests: run.totalTests,
|
|
90
|
-
passedTests: run.passedTests,
|
|
91
|
-
failedTests: run.failedTests,
|
|
92
|
-
timedOutTests: run.timedOutTests,
|
|
93
|
-
skippedTests: run.skippedTests,
|
|
94
|
-
didNotRunTests: run.didNotRunTests,
|
|
95
|
-
environment: run.options.environment,
|
|
96
|
-
label: run.options.label || null,
|
|
97
|
-
metadata: run.metadata,
|
|
98
|
-
instanceId: run.instanceId,
|
|
99
|
-
playwrightVersion: run.playwrightVersion ?? undefined,
|
|
100
|
-
reporterVersion: run.reporterVersion ?? undefined,
|
|
101
|
-
testCases: run.testCases,
|
|
102
|
-
shardIndex: run.shardInfo?.current,
|
|
103
|
-
shardTotal: run.shardInfo?.total,
|
|
104
|
-
isFullRun: run.isFullRun,
|
|
105
|
-
filterDetails: run.filterDetails,
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
async tryFinishStreaming(run, overallStatus, duration, auth) {
|
|
109
|
-
const sm = this.streamManager;
|
|
110
|
-
try {
|
|
111
|
-
const flakyTests = run.testCases.filter((tc) => tc.status === 'passed' && (tc.retries || 0) > 0).length;
|
|
112
|
-
const durations = run.testCases.filter((tc) => tc.duration != null).map((tc) => tc.duration);
|
|
113
|
-
await sm.uploadRemaining(run.testCases);
|
|
114
|
-
const finishBody = {
|
|
115
|
-
streamToken: sm.token,
|
|
116
|
-
status: overallStatus,
|
|
117
|
-
duration,
|
|
118
|
-
totalTests: run.totalTests,
|
|
119
|
-
passedTests: run.passedTests,
|
|
120
|
-
failedTests: run.failedTests,
|
|
121
|
-
timedOutTests: run.timedOutTests,
|
|
122
|
-
skippedTests: run.skippedTests,
|
|
123
|
-
didNotRunTests: run.didNotRunTests,
|
|
124
|
-
flakyTests,
|
|
125
|
-
durations,
|
|
126
|
-
label: run.options.label || null,
|
|
127
|
-
metadata: run.metadata,
|
|
128
|
-
hasPendingUploads: this.hasReports(run),
|
|
129
|
-
playwrightVersion: run.playwrightVersion ?? undefined,
|
|
130
|
-
reporterVersion: run.reporterVersion ?? undefined,
|
|
131
|
-
setupSteps: run.setupSteps.length > 0 ? run.setupSteps : undefined,
|
|
132
|
-
isFullRun: run.isFullRun,
|
|
133
|
-
filterDetails: run.filterDetails ?? null,
|
|
134
|
-
};
|
|
135
|
-
if (run.shardInfo) {
|
|
136
|
-
finishBody.shardIndex = run.shardInfo.current;
|
|
137
|
-
finishBody.shardTotal = run.shardInfo.total;
|
|
138
|
-
}
|
|
139
|
-
await this.httpClient.postJSON(`/api/test-runs/${sm.runId}/finish`, finishBody, auth);
|
|
140
|
-
this.logger.info(`Successfully finalized streaming run #${sm.runId}`);
|
|
141
|
-
this.recovery.clear();
|
|
142
|
-
if (this.hasReports(run)) {
|
|
143
|
-
try {
|
|
144
|
-
await this.uploader.uploadReportsForStreamingRun(run.options.projectName, sm.runId, this.reportOptions(run), run.startTime, auth);
|
|
145
|
-
}
|
|
146
|
-
catch (error) {
|
|
147
|
-
this.logger.warn(`Failed to upload reports for streaming run: ${(0, errors_js_1.errorMessage)(error)}`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return true;
|
|
151
|
-
}
|
|
152
|
-
catch (error) {
|
|
153
|
-
this.logger.warn(`Failed to finalize streaming run: ${(0, errors_js_1.errorMessage)(error)}`);
|
|
154
|
-
this.logger.info('Falling back to batch upload...');
|
|
155
|
-
return false;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
async tryUploadWithFiles(run, overallStatus, duration, auth) {
|
|
159
|
-
try {
|
|
160
|
-
await this.uploader.uploadWithFiles(this.buildRunPayload(run, overallStatus, duration), this.reportOptions(run), auth);
|
|
161
|
-
this.recovery.clear();
|
|
162
|
-
return true;
|
|
163
|
-
}
|
|
164
|
-
catch (error) {
|
|
165
|
-
if (error instanceof http_client_js_1.HttpError && error.status === 401 && !auth)
|
|
166
|
-
throw error;
|
|
167
|
-
this.logger.warn(`Failed to upload with files: ${(0, errors_js_1.errorMessage)(error)}`);
|
|
168
|
-
this.logger.info('Falling back to JSON upload...');
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
async tryUploadJSON(run, overallStatus, duration, auth) {
|
|
173
|
-
const payload = this.buildRunPayload(run, overallStatus, duration);
|
|
174
|
-
try {
|
|
175
|
-
await this.uploader.uploadJSON(payload, auth);
|
|
176
|
-
this.recovery.clear();
|
|
177
|
-
}
|
|
178
|
-
catch (error) {
|
|
179
|
-
// If the server returned 401 and no auth was configured, this is a
|
|
180
|
-
// configuration error — throw so the caller knows it's fatal.
|
|
181
|
-
if (error instanceof http_client_js_1.HttpError && error.status === 401 && !auth)
|
|
182
|
-
throw error;
|
|
183
|
-
this.logger.error(`All upload methods failed: ${(0, errors_js_1.errorMessage)(error)}`);
|
|
184
|
-
// Save the wire-serialized form so the recovery file matches the
|
|
185
|
-
// original submit payload (no raw attachments / internal fields).
|
|
186
|
-
this.recovery.save((0, serializer_js_1.serializeRun)(payload, { includeTestCases: true }));
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
exports.RunSubmitter = RunSubmitter;
|