@piwitests/reporter 0.4.2

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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +234 -0
  3. package/dist/compression.d.ts +5 -0
  4. package/dist/compression.js +39 -0
  5. package/dist/config-wrapper.d.ts +21 -0
  6. package/dist/config-wrapper.js +64 -0
  7. package/dist/config.d.ts +104 -0
  8. package/dist/config.js +127 -0
  9. package/dist/crash-recovery.d.ts +23 -0
  10. package/dist/crash-recovery.js +105 -0
  11. package/dist/file-handler.d.ts +38 -0
  12. package/dist/file-handler.js +166 -0
  13. package/dist/fixtures.d.ts +25 -0
  14. package/dist/fixtures.js +156 -0
  15. package/dist/global-setup-module.d.ts +2 -0
  16. package/dist/global-setup-module.js +4 -0
  17. package/dist/helpers.d.ts +44 -0
  18. package/dist/helpers.js +288 -0
  19. package/dist/http-client.d.ts +42 -0
  20. package/dist/http-client.js +154 -0
  21. package/dist/index.d.ts +8 -0
  22. package/dist/index.js +6 -0
  23. package/dist/logger.d.ts +26 -0
  24. package/dist/logger.js +43 -0
  25. package/dist/metadata-collector.d.ts +32 -0
  26. package/dist/metadata-collector.js +243 -0
  27. package/dist/reporter.d.ts +65 -0
  28. package/dist/reporter.js +341 -0
  29. package/dist/run-submitter.d.ts +66 -0
  30. package/dist/run-submitter.js +184 -0
  31. package/dist/serializer.d.ts +45 -0
  32. package/dist/serializer.js +104 -0
  33. package/dist/skip-classify.d.ts +27 -0
  34. package/dist/skip-classify.js +40 -0
  35. package/dist/step-analyzer.d.ts +97 -0
  36. package/dist/step-analyzer.js +216 -0
  37. package/dist/stream-buffer.d.ts +17 -0
  38. package/dist/stream-buffer.js +102 -0
  39. package/dist/stream-manager.d.ts +74 -0
  40. package/dist/stream-manager.js +338 -0
  41. package/dist/types.d.ts +251 -0
  42. package/dist/types.js +14 -0
  43. package/dist/uploader.d.ts +86 -0
  44. package/dist/uploader.js +191 -0
  45. package/package.json +62 -0
@@ -0,0 +1,338 @@
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("fs"));
38
+ const logger_js_1 = require("./logger.js");
39
+ const helpers_js_1 = require("./helpers.js");
40
+ /**
41
+ * Manages the streaming protocol: queues events (begin / complete), flushes
42
+ * them in batches, schedules retries on failure, and handles per-test-case
43
+ * live file uploads.
44
+ */
45
+ class StreamManager {
46
+ /** Whether the streaming session is active */
47
+ get enabled() {
48
+ return this._enabled;
49
+ }
50
+ /** Server-assigned run ID (`null` until the stream opens) */
51
+ get runId() {
52
+ return this._runId;
53
+ }
54
+ /** Stream authentication token (`null` until the stream opens) */
55
+ get token() {
56
+ return this._token;
57
+ }
58
+ /** Resolved auth string (API key or session cookie) used in stream requests */
59
+ get auth() {
60
+ return this._auth;
61
+ }
62
+ /** Promise that resolves when the stream has been fully initialised */
63
+ get startPromise() {
64
+ return this._startPromise;
65
+ }
66
+ /**
67
+ * @param httpClient HTTP client for server communication.
68
+ * @param streamBuffer On-disk buffer for crash-safe event persistence.
69
+ * @param recovery Crash-recovery handler for uploading stale payloads on startup.
70
+ * @param uploader Uploader for per-test-case file uploads.
71
+ * @param fileHandler File-discovery helper for finding traces and attachments.
72
+ * @param options Piwi Dashboard reporter options.
73
+ * @param logger Prefixed logger.
74
+ */
75
+ constructor(httpClient, streamBuffer, recovery, uploader, fileHandler, options, logger = new logger_js_1.Logger()) {
76
+ this.httpClient = httpClient;
77
+ this.streamBuffer = streamBuffer;
78
+ this.recovery = recovery;
79
+ this.uploader = uploader;
80
+ this.fileHandler = fileHandler;
81
+ this.options = options;
82
+ this.logger = logger;
83
+ this.pendingEvents = [];
84
+ this.pendingBeginEvents = [];
85
+ this.flushTimer = null;
86
+ this.flushPromises = [];
87
+ this.liveUploadPromises = [];
88
+ this.limitLiveUpload = (0, helpers_js_1.createLimiter)(2);
89
+ this.retryCount = 0;
90
+ this.retryTimer = null;
91
+ this.maxRetryDelay = 30000;
92
+ /** Tracks cases whose files have already been uploaded live, so `uploadRemaining` can skip them. */
93
+ this.uploadedCaseFiles = new WeakSet();
94
+ this._enabled = false;
95
+ this._runId = null;
96
+ this._token = null;
97
+ this._auth = null;
98
+ this._startPromise = null;
99
+ }
100
+ /** Begin the streaming session after `onBegin` fires. Non-blocking — the actual handshake runs asynchronously. */
101
+ start(startTime, metadata, instanceId, playwrightVersion, shardInfo, isFullRun, filterDetails) {
102
+ this._startPromise = this._doStart(startTime, metadata, instanceId, playwrightVersion, shardInfo, isFullRun, filterDetails);
103
+ }
104
+ async _doStart(startTime, metadata, instanceId, playwrightVersion, shardInfo, isFullRun, filterDetails) {
105
+ const setupInfo = (0, helpers_js_1.readSetupInfo)(this.options.projectName);
106
+ try {
107
+ this._auth = await this.httpClient.resolveAuth(this.options);
108
+ await this.recovery.tryUpload(this.httpClient, this._auth);
109
+ let response;
110
+ const shardIndex = shardInfo?.current;
111
+ const shardTotal = shardInfo?.total;
112
+ if (setupInfo) {
113
+ try {
114
+ response = await this.httpClient.postJSON(`/api/test-runs/${setupInfo.runId}/begin`, {
115
+ setupToken: setupInfo.setupToken,
116
+ totalTests: 0,
117
+ metadata,
118
+ playwrightVersion,
119
+ shardIndex,
120
+ shardTotal,
121
+ isFullRun,
122
+ filterDetails,
123
+ }, this._auth);
124
+ }
125
+ catch (beginError) {
126
+ // Stale setupInfo — the run was cancelled (e.g. by crash recovery submit).
127
+ // Fall back to creating a fresh run instead of silently disabling streaming.
128
+ if (!beginError.message?.includes('409'))
129
+ throw beginError;
130
+ this.logger.debug(`Setup info expired, creating fresh run...`);
131
+ response = await this.httpClient.postJSON('/api/test-runs/start', {
132
+ projectName: this.options.projectName,
133
+ projectDescription: this.options.projectDescription,
134
+ startTime,
135
+ environment: this.options.environment || null,
136
+ label: this.options.label || null,
137
+ metadata,
138
+ instanceId,
139
+ playwrightVersion,
140
+ shardIndex,
141
+ shardTotal,
142
+ isFullRun,
143
+ filterDetails,
144
+ }, this._auth);
145
+ }
146
+ }
147
+ else {
148
+ response = await this.httpClient.postJSON('/api/test-runs/start', {
149
+ projectName: this.options.projectName,
150
+ projectDescription: this.options.projectDescription,
151
+ startTime,
152
+ environment: this.options.environment || null,
153
+ label: this.options.label || null,
154
+ metadata,
155
+ instanceId,
156
+ playwrightVersion,
157
+ shardIndex,
158
+ shardTotal,
159
+ isFullRun,
160
+ filterDetails,
161
+ }, this._auth);
162
+ }
163
+ if (response?.runId && response?.streamToken) {
164
+ this._runId = response.runId;
165
+ this._token = response.streamToken;
166
+ this._enabled = true;
167
+ this.logger.info(`Streaming enabled. Run ID: ${response.runId}`);
168
+ if (this.pendingBeginEvents.length > 0) {
169
+ this.pendingEvents = [...this.pendingBeginEvents, ...this.pendingEvents];
170
+ this.pendingBeginEvents = [];
171
+ this.flush();
172
+ }
173
+ }
174
+ }
175
+ catch (error) {
176
+ this.logger.debug(`Streaming not available: ${error.message}. Will use batch mode.`);
177
+ this._enabled = false;
178
+ }
179
+ }
180
+ // Queues a begin event; held in a pre-start buffer until the stream is open,
181
+ // then prepended to the main queue so it arrives before the matching complete event.
182
+ /** 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. */
183
+ queueBeginEvent(event) {
184
+ if (this._enabled && this._runId) {
185
+ this.queueEvent(event);
186
+ }
187
+ else {
188
+ this.pendingBeginEvents.push(event);
189
+ }
190
+ }
191
+ /** Queue a test-case event. Triggers an immediate flush when the batch size is reached, otherwise schedules a timer-based flush. */
192
+ queueEvent(event) {
193
+ this.pendingEvents.push(event);
194
+ if (this.pendingEvents.length >= this.options.streamingBatchSize) {
195
+ this.flush();
196
+ }
197
+ else if (!this.flushTimer) {
198
+ this.flushTimer = setTimeout(() => this.flush(), this.options.streamingBatchDelay);
199
+ }
200
+ }
201
+ /** 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). */
202
+ flush() {
203
+ if (this.flushTimer) {
204
+ clearTimeout(this.flushTimer);
205
+ this.flushTimer = null;
206
+ }
207
+ if (this.pendingEvents.length === 0 || !this._enabled || !this._runId)
208
+ return null;
209
+ const events = this.pendingEvents.splice(0);
210
+ // Never rejects: failed events are re-queued so the retry timer or the
211
+ // end-of-run drain can resend them (the server deduplicates).
212
+ const promise = this.httpClient
213
+ .postJSON(`/api/test-runs/${this._runId}/events`, { streamToken: this._token, testCases: events }, this._auth)
214
+ .then(() => {
215
+ this.retryCount = 0;
216
+ return true;
217
+ }, () => {
218
+ this.pendingEvents = events.concat(this.pendingEvents);
219
+ this.scheduleRetry();
220
+ return false;
221
+ });
222
+ this.flushPromises.push(promise);
223
+ return promise;
224
+ }
225
+ scheduleRetry() {
226
+ if (this.retryTimer)
227
+ return;
228
+ this.retryCount++;
229
+ const delay = Math.min(1000 * Math.pow(2, this.retryCount - 1), this.maxRetryDelay);
230
+ this.logger.debug(`Will retry streaming flush in ${delay}ms (attempt ${this.retryCount})`);
231
+ this.retryTimer = setTimeout(() => {
232
+ this.retryTimer = null;
233
+ const buffered = this.streamBuffer.load();
234
+ if (buffered.length > 0) {
235
+ this.streamBuffer.clear();
236
+ this.pendingEvents = buffered.concat(this.pendingEvents);
237
+ }
238
+ if (this.pendingEvents.length > 0)
239
+ this.flush();
240
+ }, delay);
241
+ }
242
+ /** Drain all pending and buffered events before the run finishes. Retries up to 10 times with exponential back-off. */
243
+ async drain() {
244
+ if (!this._enabled) {
245
+ this.pendingEvents = [];
246
+ this.flushPromises = [];
247
+ return;
248
+ }
249
+ const MAX_ATTEMPTS = 10;
250
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
251
+ if (this._enabled && this.pendingEvents.length > 0)
252
+ this.flush();
253
+ if (this.flushPromises.length > 0) {
254
+ await Promise.allSettled(this.flushPromises);
255
+ this.flushPromises = [];
256
+ }
257
+ if (this.pendingEvents.length === 0) {
258
+ const buffered = this.streamBuffer.load();
259
+ if (buffered.length > 0) {
260
+ this.pendingEvents = buffered;
261
+ this.streamBuffer.clear();
262
+ continue;
263
+ }
264
+ return;
265
+ }
266
+ this.logger.debugError(`${this.pendingEvents.length} events pending, retrying (attempt ${attempt + 1}/${MAX_ATTEMPTS})...`);
267
+ await new Promise((resolve) => setTimeout(resolve, Math.min(1000 * Math.pow(2, attempt), 10000)));
268
+ }
269
+ if (this.pendingEvents.length > 0) {
270
+ this.streamBuffer.append(this.pendingEvents);
271
+ this.pendingEvents = [];
272
+ }
273
+ }
274
+ /** 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. */
275
+ scheduleLiveUpload(tc) {
276
+ const hasTrace = !!this.options.uploadTraces && this.fileHandler.findTraceFiles(tc).some((p) => fs.existsSync(p));
277
+ const hasAttachments = this.fileHandler.findAllAttachments(tc).length > 0;
278
+ if (!hasTrace && !hasAttachments)
279
+ return;
280
+ const promise = (async () => {
281
+ if (this._startPromise)
282
+ await this._startPromise;
283
+ if (!this._enabled || !this._runId || !this._token)
284
+ return;
285
+ // The complete event must reach the server before it can link files.
286
+ // queueEvent may have already triggered a batch-size flush that took all
287
+ // pending events, making our flush() return null. Wait for any in-flight
288
+ // flush promises first, then try to flush remaining events.
289
+ if (this.flushPromises.length > 0) {
290
+ await Promise.allSettled(this.flushPromises);
291
+ }
292
+ const flush = this.flush();
293
+ if (flush)
294
+ await flush;
295
+ // Retry on 404: the events batch carrying this case may still be in flight
296
+ const delays = [0, 1000, 3000];
297
+ for (let attempt = 0; attempt < delays.length; attempt++) {
298
+ if (delays[attempt])
299
+ await new Promise((resolve) => setTimeout(resolve, delays[attempt]));
300
+ try {
301
+ await this.limitLiveUpload(() => this.uploader.uploadCaseFiles(this.options.projectName, this._runId, this._token, tc, this.options.uploadTraces, this._auth));
302
+ this.uploadedCaseFiles.add(tc);
303
+ return;
304
+ }
305
+ catch (error) {
306
+ const retryable = error.message?.includes('404');
307
+ if (!retryable || attempt === delays.length - 1) {
308
+ this.logger.debug(`Live file upload failed for "${tc.title}": ${error.message}`);
309
+ return; // The end-of-run pass retries cases that failed here
310
+ }
311
+ }
312
+ }
313
+ })();
314
+ this.liveUploadPromises.push(promise);
315
+ }
316
+ /** Wait for all live uploads to settle, then upload files for any test cases that weren't uploaded live */
317
+ async uploadRemaining(testCases) {
318
+ if (this.liveUploadPromises.length > 0) {
319
+ await Promise.allSettled(this.liveUploadPromises);
320
+ this.liveUploadPromises = [];
321
+ }
322
+ if (!this._enabled || !this._runId || !this._token)
323
+ return;
324
+ for (const tc of testCases) {
325
+ if (this.uploadedCaseFiles.has(tc))
326
+ continue;
327
+ try {
328
+ const uploaded = await this.uploader.uploadCaseFiles(this.options.projectName, this._runId, this._token, tc, this.options.uploadTraces, this._auth);
329
+ if (uploaded)
330
+ this.uploadedCaseFiles.add(tc);
331
+ }
332
+ catch (error) {
333
+ this.logger.warn(`Failed to upload files for "${tc.title}": ${error.message}`);
334
+ }
335
+ }
336
+ }
337
+ }
338
+ exports.StreamManager = StreamManager;
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Reporter-local domain model.
3
+ *
4
+ * These interfaces describe the data that flows *between* the reporter's
5
+ * classes (`FileHandler`, `Uploader`, `StreamManager`, `PiwiDashboardReporter`).
6
+ * They intentionally mirror — but do not import — the wire types in
7
+ * `application/shared/types.ts`. Importing that module would leak the monorepo
8
+ * path into the published `.d.ts` files; the constraint forbids the import,
9
+ * not the types. Keep these structurally compatible with
10
+ * `TestCasePayload` / `StreamEventPayload` / `TestRunFinishPayload` when
11
+ * evolving them.
12
+ */
13
+ export interface SuiteConfigEntry {
14
+ mode: 'parallel' | 'serial' | 'default';
15
+ annotations: Array<{
16
+ type: string;
17
+ description?: string;
18
+ }>;
19
+ }
20
+ export interface TestAnnotation {
21
+ type: string;
22
+ description?: string;
23
+ }
24
+ /**
25
+ * Filter that narrowed a run to a subset of tests, recorded when `isFullRun`
26
+ * is false. Mirrors `FilterDetails` in `application/shared/types.ts`.
27
+ */
28
+ export interface FilterDetails {
29
+ /** A non-default `--grep` pattern (Playwright's default `.*` is excluded). */
30
+ grep?: string;
31
+ /** A `--grep-invert` pattern. */
32
+ grepInvert?: string;
33
+ /** Positional file/path filters from the CLI invocation (e.g. ["tests/login.spec.ts"]). */
34
+ files?: string[];
35
+ }
36
+ export interface BrowserConfig {
37
+ projectName?: string;
38
+ browserName?: string | null;
39
+ channel?: string | null;
40
+ viewport?: {
41
+ width: number;
42
+ height: number;
43
+ } | null;
44
+ deviceScaleFactor?: number | null;
45
+ isMobile?: boolean | null;
46
+ hasTouch?: boolean | null;
47
+ locale?: string | null;
48
+ timezoneId?: string | null;
49
+ geolocation?: {
50
+ longitude: number;
51
+ latitude: number;
52
+ accuracy?: number;
53
+ } | null;
54
+ colorScheme?: string | null;
55
+ reducedMotion?: string | null;
56
+ forcedColors?: string | null;
57
+ offline?: boolean | null;
58
+ bypassCSP?: boolean | null;
59
+ javaScriptEnabled?: boolean | null;
60
+ serviceWorkers?: string | null;
61
+ userAgent?: string | null;
62
+ }
63
+ /**
64
+ * A raw Playwright attachment as exposed on `TestResult.attachments`.
65
+ * Carried verbatim on `CollectedTestCase.attachments` so `FileHandler` can
66
+ * resolve trace/attachment paths; never sent to the server.
67
+ */
68
+ export interface RawAttachment {
69
+ name: string;
70
+ path?: string;
71
+ contentType?: string;
72
+ body?: Buffer;
73
+ originalName?: string;
74
+ }
75
+ /** Performance metrics collected from `result.steps` by `step-analyzer`. */
76
+ export interface CollectedPerformanceMetrics {
77
+ steps: Array<{
78
+ title: string;
79
+ duration: number;
80
+ category: string;
81
+ }>;
82
+ totalStepDuration: number;
83
+ slowestStep: {
84
+ title: string;
85
+ duration: number;
86
+ } | null;
87
+ navigationCount: number;
88
+ navigationTotalDuration: number;
89
+ waitTotalDuration: number;
90
+ waitCount: number;
91
+ }
92
+ /** A hook/fixture step event with absolute timings (for the workers timeline). */
93
+ export interface TestStepEvent {
94
+ title: string;
95
+ category: 'hook' | 'fixture' | 'test.step' | 'expect' | 'wait';
96
+ startedAt: number;
97
+ duration: number;
98
+ status: string;
99
+ location?: string | null;
100
+ }
101
+ /**
102
+ * What `onTestEnd` accumulates per test case. Mixes three concerns that the
103
+ * reporter must keep together during a run:
104
+ * - **wire fields** (`title`, `status`, `duration`, …) that `toWireTestCase`
105
+ * projects onto `WireTestCase` before sending,
106
+ * - **collection-only state** (`attachments`, `performanceMetrics`,
107
+ * `stepEvents`) consumed by `FileHandler` and the run-level summary,
108
+ * - the `type` discriminant so the same collected object can be queued as a
109
+ * stream event.
110
+ *
111
+ * Upload bookkeeping (`_filesUploaded`) is deliberately NOT on this object —
112
+ * `StreamManager` tracks it in a side `Set` so the data model stays clean.
113
+ */
114
+ export interface CollectedTestCase {
115
+ /** Stream-event discriminant: `'begin'` or `'complete'`. Omitted for batch-only runs. */
116
+ type?: 'begin' | 'complete';
117
+ title: string;
118
+ location: string;
119
+ status?: string;
120
+ duration?: number;
121
+ error?: string | null;
122
+ retries?: number;
123
+ workerIndex?: number | null;
124
+ shardIndex?: number | null;
125
+ startedAt?: number | null;
126
+ /** Raw Playwright attachments — never sent on the wire. */
127
+ attachments?: RawAttachment[];
128
+ browser?: BrowserConfig | null;
129
+ suitePath?: string[] | null;
130
+ suiteConfig?: SuiteConfigEntry[] | null;
131
+ testAnnotations?: TestAnnotation[] | null;
132
+ /** Source snippet around the failing line (failed/timedOut only). */
133
+ testSource?: string;
134
+ /** Step metrics from `collectStepMetrics`. Consumed by the run summary + `toWireTestCase`. */
135
+ performanceMetrics?: CollectedPerformanceMetrics;
136
+ stepEvents?: TestStepEvent[];
137
+ /** Parsed from `piwi-dashboard-network` attachments by `FileHandler`. */
138
+ networkRequests?: unknown;
139
+ /** Parsed from `piwi-dashboard-web-vitals` attachments. */
140
+ webVitals?: unknown;
141
+ /** Parsed from `piwi-dashboard-console` attachments. */
142
+ consoleLogs?: unknown;
143
+ /** Parsed from `piwi-dashboard-aria-snapshot` attachment. */
144
+ ariaSnapshot?: string;
145
+ }
146
+ /**
147
+ * The per-case wire shape that `toWireTestCase` produces and the server
148
+ * receives. Structurally compatible with `TestCasePayload` and the per-event
149
+ * `StreamEventPayload`.
150
+ */
151
+ export interface WireTestCase {
152
+ type?: 'begin' | 'complete' | 'step-begin' | 'step-end';
153
+ title: string;
154
+ location: string;
155
+ status?: string;
156
+ duration?: number;
157
+ error?: string | null;
158
+ retries?: number;
159
+ workerIndex?: number | null;
160
+ shardIndex?: number | null;
161
+ startedAt?: number | null;
162
+ steps?: unknown;
163
+ stepEvents?: TestStepEvent[] | null;
164
+ slowestStep?: string | null;
165
+ slowestStepDuration?: number | null;
166
+ wastedTimeMs?: number | null;
167
+ networkRequests?: unknown;
168
+ webVitals?: unknown;
169
+ consoleLogs?: unknown;
170
+ ariaSnapshot?: unknown;
171
+ testSource?: string | null;
172
+ browser?: BrowserConfig | null;
173
+ suitePath?: string[] | null;
174
+ suiteConfig?: SuiteConfigEntry[] | null;
175
+ testAnnotations?: TestAnnotation[] | null;
176
+ /** Step-event discriminant (only for `step-begin`/`step-end` events). */
177
+ stepCategory?: string | null;
178
+ parentTitle?: string | null;
179
+ }
180
+ export interface BeginStreamEvent {
181
+ type: 'begin';
182
+ title: string;
183
+ location: string;
184
+ workerIndex: number | null;
185
+ shardIndex: number | null;
186
+ browser?: BrowserConfig | null;
187
+ suitePath?: string[] | null;
188
+ suiteConfig?: SuiteConfigEntry[] | null;
189
+ }
190
+ export interface CompleteStreamEvent {
191
+ type: 'complete';
192
+ title: string;
193
+ location: string;
194
+ status: string;
195
+ duration: number;
196
+ error: string | null;
197
+ retries: number;
198
+ workerIndex: number | null;
199
+ shardIndex: number | null;
200
+ startedAt: number | null;
201
+ browser?: BrowserConfig | null;
202
+ suitePath?: string[] | null;
203
+ suiteConfig?: SuiteConfigEntry[] | null;
204
+ testAnnotations?: TestAnnotation[] | null;
205
+ steps?: unknown;
206
+ stepEvents?: TestStepEvent[] | null;
207
+ slowestStep?: string | null;
208
+ slowestStepDuration?: number | null;
209
+ networkRequests?: unknown;
210
+ webVitals?: unknown;
211
+ consoleLogs?: unknown;
212
+ ariaSnapshot?: unknown;
213
+ testSource?: string | null;
214
+ }
215
+ export interface StepBeginStreamEvent {
216
+ type: 'step-begin';
217
+ title: string;
218
+ location: string;
219
+ stepCategory: 'hook' | 'fixture';
220
+ parentTitle: string | null;
221
+ workerIndex: number | null;
222
+ startedAt: number | null;
223
+ }
224
+ export interface StepEndStreamEvent {
225
+ type: 'step-end';
226
+ title: string;
227
+ location: string;
228
+ status: string;
229
+ duration: number;
230
+ stepCategory: 'hook' | 'fixture';
231
+ parentTitle: string | null;
232
+ workerIndex: number | null;
233
+ startedAt: number | null;
234
+ }
235
+ /** Discriminated union of events queued to `StreamManager` and persisted by `StreamBuffer`. */
236
+ export type StreamEvent = BeginStreamEvent | CompleteStreamEvent | StepBeginStreamEvent | StepEndStreamEvent;
237
+ export interface SetupStep {
238
+ title: string;
239
+ category: string;
240
+ startedAt: number;
241
+ duration: number;
242
+ status: string;
243
+ location?: string | null;
244
+ workerIndex?: number | null;
245
+ }
246
+ /** Hash + size of a single trace file, used for dedup against the server. */
247
+ export interface TraceHashInfo {
248
+ tracePath: string;
249
+ hash: string;
250
+ size: number;
251
+ }
package/dist/types.js ADDED
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ /**
3
+ * Reporter-local domain model.
4
+ *
5
+ * These interfaces describe the data that flows *between* the reporter's
6
+ * classes (`FileHandler`, `Uploader`, `StreamManager`, `PiwiDashboardReporter`).
7
+ * They intentionally mirror — but do not import — the wire types in
8
+ * `application/shared/types.ts`. Importing that module would leak the monorepo
9
+ * path into the published `.d.ts` files; the constraint forbids the import,
10
+ * not the types. Keep these structurally compatible with
11
+ * `TestCasePayload` / `StreamEventPayload` / `TestRunFinishPayload` when
12
+ * evolving them.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,86 @@
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';
5
+ /** Payload for a batch test-run submission */
6
+ export interface RunPayload {
7
+ /** Name of the project the run belongs to */
8
+ projectName: string;
9
+ /** Optional project description */
10
+ projectDescription?: string;
11
+ /** Overall run status: `"passed"` or `"failed"` */
12
+ status: string;
13
+ /** ISO-8601 timestamp when the run started */
14
+ startTime: string | null;
15
+ /** Total wall-clock duration of the run in ms */
16
+ duration: number;
17
+ totalTests: number;
18
+ passedTests: number;
19
+ failedTests: number;
20
+ skippedTests: number;
21
+ /** Tests that never executed (cut short by `maxFailures` or a serial-group failure) */
22
+ didNotRunTests?: number;
23
+ /** Deployment environment label (e.g. `"staging"`, `"production"`) */
24
+ environment?: string;
25
+ /** Optional display label for the test run (e.g. "v2.3.1 release") */
26
+ label?: string | null;
27
+ /** Arbitrary metadata collected from the environment, CI, and Playwright config */
28
+ metadata: Record<string, any>;
29
+ /** Unique instance identifier for deduplication */
30
+ instanceId: string;
31
+ /** Test case results in their collected form (with attachments). Projected to the wire shape by `serializeRun`/`toWireTestCase` at the JSON boundary. */
32
+ testCases: CollectedTestCase[];
33
+ /** Playwright framework version used for this run */
34
+ playwrightVersion?: string;
35
+ /** 1-based shard index (e.g. 1, 2, 3) */
36
+ shardIndex?: number;
37
+ /** Total number of shards (e.g. 3) */
38
+ shardTotal?: number;
39
+ /** Whether this run represents the full test suite (true) or a filtered subset (false) */
40
+ isFullRun?: boolean;
41
+ /** Filter details when isFullRun is false */
42
+ filterDetails?: FilterDetails | null;
43
+ }
44
+ /** Options controlling which report files and traces to upload */
45
+ export interface ReportOptions {
46
+ /** Upload trace files. Defaults to `true`. */
47
+ uploadTraces?: boolean;
48
+ /** Upload the Playwright HTML report. Defaults to `true`. */
49
+ uploadReport?: boolean;
50
+ /** Additional report types to discover and upload */
51
+ reports?: Array<{
52
+ type: string;
53
+ dir?: string;
54
+ label?: string;
55
+ }>;
56
+ }
57
+ /**
58
+ * Handles all upload strategies: plain JSON, multipart (with reports and
59
+ * traces), per-test-case file uploads for streaming runs, and report-only
60
+ * uploads for already-submitted streaming runs.
61
+ */
62
+ export declare class Uploader {
63
+ private httpClient;
64
+ private fileHandler;
65
+ private logger;
66
+ /**
67
+ * @param httpClient HTTP client for server communication.
68
+ * @param fileHandler File discovery and compression helper.
69
+ * @param logger Prefixed logger.
70
+ */
71
+ constructor(httpClient: HttpClient, fileHandler: FileHandler, logger: Logger);
72
+ /** Submit test results as a plain JSON payload (no file attachments) */
73
+ uploadJSON(payload: RunPayload, auth: string | null): Promise<any>;
74
+ /** Submit test results as a multipart form with trace files and compressed report directories */
75
+ uploadWithFiles(payload: RunPayload, reportOptions: ReportOptions, auth: string | null): Promise<any>;
76
+ /** Upload report files for an already-submitted streaming run */
77
+ uploadReportsForStreamingRun(projectName: string, runId: number, reportOptions: ReportOptions, startTime: string | null, auth: string | null): Promise<void>;
78
+ /**
79
+ * Upload one test case's trace and attachments for a streaming run.
80
+ * The matching `complete` event must have been flushed to the server first.
81
+ * Returns `false` when the case has no files to upload.
82
+ */
83
+ uploadCaseFiles(projectName: string, runId: number, streamToken: string, testCase: CollectedTestCase, uploadTraces: boolean | undefined, auth: string | null): Promise<boolean>;
84
+ private appendReportsToForm;
85
+ private appendFilesToForm;
86
+ }