@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.
- package/LICENSE +21 -0
- package/README.md +234 -0
- package/dist/compression.d.ts +5 -0
- package/dist/compression.js +39 -0
- package/dist/config-wrapper.d.ts +21 -0
- package/dist/config-wrapper.js +64 -0
- package/dist/config.d.ts +104 -0
- package/dist/config.js +127 -0
- package/dist/crash-recovery.d.ts +23 -0
- package/dist/crash-recovery.js +105 -0
- package/dist/file-handler.d.ts +38 -0
- package/dist/file-handler.js +166 -0
- package/dist/fixtures.d.ts +25 -0
- package/dist/fixtures.js +156 -0
- package/dist/global-setup-module.d.ts +2 -0
- package/dist/global-setup-module.js +4 -0
- package/dist/helpers.d.ts +44 -0
- package/dist/helpers.js +288 -0
- package/dist/http-client.d.ts +42 -0
- package/dist/http-client.js +154 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +6 -0
- package/dist/logger.d.ts +26 -0
- package/dist/logger.js +43 -0
- package/dist/metadata-collector.d.ts +32 -0
- package/dist/metadata-collector.js +243 -0
- package/dist/reporter.d.ts +65 -0
- package/dist/reporter.js +341 -0
- package/dist/run-submitter.d.ts +66 -0
- package/dist/run-submitter.js +184 -0
- package/dist/serializer.d.ts +45 -0
- package/dist/serializer.js +104 -0
- package/dist/skip-classify.d.ts +27 -0
- package/dist/skip-classify.js +40 -0
- package/dist/step-analyzer.d.ts +97 -0
- package/dist/step-analyzer.js +216 -0
- package/dist/stream-buffer.d.ts +17 -0
- package/dist/stream-buffer.js +102 -0
- package/dist/stream-manager.d.ts +74 -0
- package/dist/stream-manager.js +338 -0
- package/dist/types.d.ts +251 -0
- package/dist/types.js +14 -0
- package/dist/uploader.d.ts +86 -0
- package/dist/uploader.js +191 -0
- package/package.json +62 -0
|
@@ -0,0 +1,66 @@
|
|
|
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';
|
|
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
|
+
totalTests: number;
|
|
20
|
+
passedTests: number;
|
|
21
|
+
failedTests: number;
|
|
22
|
+
skippedTests: number;
|
|
23
|
+
timedOutTests: number;
|
|
24
|
+
didNotRunTests: number;
|
|
25
|
+
metadata: Record<string, any>;
|
|
26
|
+
instanceId: string;
|
|
27
|
+
shardInfo: ShardInfo | null;
|
|
28
|
+
setupSteps: SetupStep[];
|
|
29
|
+
isFullRun: boolean;
|
|
30
|
+
filterDetails: FilterDetails | null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Owns the three-tier submit/fallback ladder:
|
|
34
|
+
*
|
|
35
|
+
* 1. finalize the streaming run (`/finish`) when streaming is active,
|
|
36
|
+
* 2. fall back to multipart upload (`/upload`) when there are reports or
|
|
37
|
+
* traces to attach,
|
|
38
|
+
* 3. fall back to plain JSON (`/submit`) as the last resort, persisting a
|
|
39
|
+
* recovery payload on total failure.
|
|
40
|
+
*
|
|
41
|
+
* The order and logging are identical to the pre-extraction reporter — this is
|
|
42
|
+
* a move, not a redesign.
|
|
43
|
+
*/
|
|
44
|
+
export declare class RunSubmitter {
|
|
45
|
+
private readonly httpClient;
|
|
46
|
+
private readonly uploader;
|
|
47
|
+
private readonly recovery;
|
|
48
|
+
private readonly streamManager;
|
|
49
|
+
private readonly logger;
|
|
50
|
+
/**
|
|
51
|
+
* @param httpClient HTTP client for auth resolution and the finish call.
|
|
52
|
+
* @param uploader Upload strategies (multipart + JSON).
|
|
53
|
+
* @param recovery Crash-recovery persistence.
|
|
54
|
+
* @param streamManager Streaming session (may be `null` when streaming is disabled).
|
|
55
|
+
* @param logger Prefixed logger.
|
|
56
|
+
*/
|
|
57
|
+
constructor(httpClient: HttpClient, uploader: Uploader, recovery: CrashRecovery, streamManager: StreamManager | null, logger?: Logger);
|
|
58
|
+
/** Run the fallback ladder for a completed test run. */
|
|
59
|
+
submit(run: CollectedRun, result: FullResult): Promise<void>;
|
|
60
|
+
private hasReports;
|
|
61
|
+
private reportOptions;
|
|
62
|
+
private buildRunPayload;
|
|
63
|
+
private tryFinishStreaming;
|
|
64
|
+
private tryUploadWithFiles;
|
|
65
|
+
private tryUploadJSON;
|
|
66
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RunSubmitter = void 0;
|
|
4
|
+
const logger_js_1 = require("./logger.js");
|
|
5
|
+
const step_analyzer_js_1 = require("./step-analyzer.js");
|
|
6
|
+
const serializer_js_1 = require("./serializer.js");
|
|
7
|
+
/**
|
|
8
|
+
* Owns the three-tier submit/fallback ladder:
|
|
9
|
+
*
|
|
10
|
+
* 1. finalize the streaming run (`/finish`) when streaming is active,
|
|
11
|
+
* 2. fall back to multipart upload (`/upload`) when there are reports or
|
|
12
|
+
* traces to attach,
|
|
13
|
+
* 3. fall back to plain JSON (`/submit`) as the last resort, persisting a
|
|
14
|
+
* recovery payload on total failure.
|
|
15
|
+
*
|
|
16
|
+
* The order and logging are identical to the pre-extraction reporter — this is
|
|
17
|
+
* a move, not a redesign.
|
|
18
|
+
*/
|
|
19
|
+
class RunSubmitter {
|
|
20
|
+
/**
|
|
21
|
+
* @param httpClient HTTP client for auth resolution and the finish call.
|
|
22
|
+
* @param uploader Upload strategies (multipart + JSON).
|
|
23
|
+
* @param recovery Crash-recovery persistence.
|
|
24
|
+
* @param streamManager Streaming session (may be `null` when streaming is disabled).
|
|
25
|
+
* @param logger Prefixed logger.
|
|
26
|
+
*/
|
|
27
|
+
constructor(httpClient, uploader, recovery, streamManager, logger = new logger_js_1.Logger()) {
|
|
28
|
+
this.httpClient = httpClient;
|
|
29
|
+
this.uploader = uploader;
|
|
30
|
+
this.recovery = recovery;
|
|
31
|
+
this.streamManager = streamManager;
|
|
32
|
+
this.logger = logger;
|
|
33
|
+
}
|
|
34
|
+
/** Run the fallback ladder for a completed test run. */
|
|
35
|
+
async submit(run, result) {
|
|
36
|
+
const endTime = new Date().toISOString();
|
|
37
|
+
const duration = new Date(endTime).getTime() - new Date(run.startTime).getTime();
|
|
38
|
+
const overallStatus = (0, serializer_js_1.resolveOverallStatus)(result, {
|
|
39
|
+
failedTests: run.failedTests,
|
|
40
|
+
timedOutTests: run.timedOutTests,
|
|
41
|
+
totalTests: run.totalTests,
|
|
42
|
+
});
|
|
43
|
+
this.logger.info(`Test run completed. Status: ${overallStatus} (Playwright result.status: ${result?.status || 'undefined'})`);
|
|
44
|
+
this.logger.info(`Total: ${run.totalTests}, Passed: ${run.passedTests}, Failed: ${run.failedTests}, Skipped: ${run.skippedTests}, TimedOut: ${run.timedOutTests}, DidNotRun: ${run.didNotRunTests}`);
|
|
45
|
+
if (run.options.collectPerformanceMetrics) {
|
|
46
|
+
run.metadata.performance = (0, step_analyzer_js_1.computePerformanceSummary)(run.testCases);
|
|
47
|
+
}
|
|
48
|
+
const sm = this.streamManager;
|
|
49
|
+
if (sm?.startPromise)
|
|
50
|
+
await sm.startPromise;
|
|
51
|
+
await sm?.drain();
|
|
52
|
+
let auth;
|
|
53
|
+
try {
|
|
54
|
+
auth = sm?.auth ?? (await this.httpClient.resolveAuth(run.options));
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
this.logger.error(`Authentication failed: ${error.message}`);
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
if (sm?.enabled && sm?.runId != null) {
|
|
61
|
+
if (await this.tryFinishStreaming(run, overallStatus, duration, auth))
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (this.hasReports(run) || run.options.uploadTraces) {
|
|
65
|
+
if (await this.tryUploadWithFiles(run, overallStatus, duration, auth))
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
await this.tryUploadJSON(run, overallStatus, duration, auth);
|
|
69
|
+
}
|
|
70
|
+
hasReports(run) {
|
|
71
|
+
return !!run.options.uploadReport || (run.options.reports?.length ?? 0) > 0;
|
|
72
|
+
}
|
|
73
|
+
reportOptions(run) {
|
|
74
|
+
return {
|
|
75
|
+
uploadTraces: run.options.uploadTraces,
|
|
76
|
+
uploadReport: run.options.uploadReport,
|
|
77
|
+
reports: run.options.reports,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
buildRunPayload(run, status, duration) {
|
|
81
|
+
return {
|
|
82
|
+
projectName: run.options.projectName,
|
|
83
|
+
projectDescription: run.options.projectDescription,
|
|
84
|
+
status,
|
|
85
|
+
startTime: run.startTime,
|
|
86
|
+
duration,
|
|
87
|
+
totalTests: run.totalTests,
|
|
88
|
+
passedTests: run.passedTests,
|
|
89
|
+
failedTests: run.failedTests,
|
|
90
|
+
skippedTests: run.skippedTests,
|
|
91
|
+
didNotRunTests: run.didNotRunTests,
|
|
92
|
+
environment: run.options.environment,
|
|
93
|
+
label: run.options.label || null,
|
|
94
|
+
metadata: run.metadata,
|
|
95
|
+
instanceId: run.instanceId,
|
|
96
|
+
playwrightVersion: run.playwrightVersion ?? undefined,
|
|
97
|
+
testCases: run.testCases,
|
|
98
|
+
shardIndex: run.shardInfo?.current,
|
|
99
|
+
shardTotal: run.shardInfo?.total,
|
|
100
|
+
isFullRun: run.isFullRun,
|
|
101
|
+
filterDetails: run.filterDetails,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
async tryFinishStreaming(run, overallStatus, duration, auth) {
|
|
105
|
+
const sm = this.streamManager;
|
|
106
|
+
try {
|
|
107
|
+
const flakyTests = run.testCases.filter((tc) => tc.status === 'passed' && (tc.retries || 0) > 0).length;
|
|
108
|
+
const durations = run.testCases.filter((tc) => tc.duration != null).map((tc) => tc.duration);
|
|
109
|
+
await sm.uploadRemaining(run.testCases);
|
|
110
|
+
const finishBody = {
|
|
111
|
+
streamToken: sm.token,
|
|
112
|
+
status: overallStatus,
|
|
113
|
+
duration,
|
|
114
|
+
totalTests: run.totalTests,
|
|
115
|
+
passedTests: run.passedTests,
|
|
116
|
+
failedTests: run.failedTests,
|
|
117
|
+
skippedTests: run.skippedTests,
|
|
118
|
+
didNotRunTests: run.didNotRunTests,
|
|
119
|
+
flakyTests,
|
|
120
|
+
durations,
|
|
121
|
+
label: run.options.label || null,
|
|
122
|
+
metadata: run.metadata,
|
|
123
|
+
hasPendingUploads: this.hasReports(run),
|
|
124
|
+
playwrightVersion: run.playwrightVersion ?? undefined,
|
|
125
|
+
setupSteps: run.setupSteps.length > 0 ? run.setupSteps : undefined,
|
|
126
|
+
isFullRun: run.isFullRun,
|
|
127
|
+
filterDetails: run.filterDetails ?? null,
|
|
128
|
+
};
|
|
129
|
+
if (run.shardInfo) {
|
|
130
|
+
finishBody.shardIndex = run.shardInfo.current;
|
|
131
|
+
finishBody.shardTotal = run.shardInfo.total;
|
|
132
|
+
}
|
|
133
|
+
await this.httpClient.postJSON(`/api/test-runs/${sm.runId}/finish`, finishBody, auth);
|
|
134
|
+
this.logger.info(`Successfully finalized streaming run #${sm.runId}`);
|
|
135
|
+
this.recovery.clear();
|
|
136
|
+
if (this.hasReports(run)) {
|
|
137
|
+
try {
|
|
138
|
+
await this.uploader.uploadReportsForStreamingRun(run.options.projectName, sm.runId, this.reportOptions(run), run.startTime, auth);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
this.logger.warn(`Failed to upload reports for streaming run: ${error.message}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
this.logger.warn(`Failed to finalize streaming run: ${error.message}`);
|
|
148
|
+
this.logger.info('Falling back to batch upload...');
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
async tryUploadWithFiles(run, overallStatus, duration, auth) {
|
|
153
|
+
try {
|
|
154
|
+
await this.uploader.uploadWithFiles(this.buildRunPayload(run, overallStatus, duration), this.reportOptions(run), auth);
|
|
155
|
+
this.recovery.clear();
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
if (error.message?.includes('401') && !auth)
|
|
160
|
+
throw error;
|
|
161
|
+
this.logger.warn(`Failed to upload with files: ${error.message}`);
|
|
162
|
+
this.logger.info('Falling back to JSON upload...');
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async tryUploadJSON(run, overallStatus, duration, auth) {
|
|
167
|
+
const payload = this.buildRunPayload(run, overallStatus, duration);
|
|
168
|
+
try {
|
|
169
|
+
await this.uploader.uploadJSON(payload, auth);
|
|
170
|
+
this.recovery.clear();
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
// If the server returned 401 and no auth was configured, this is a
|
|
174
|
+
// configuration error — throw so the caller knows it's fatal.
|
|
175
|
+
if (error.message?.includes('401') && !auth)
|
|
176
|
+
throw error;
|
|
177
|
+
this.logger.error(`All upload methods failed: ${error.message}`);
|
|
178
|
+
// Save the wire-serialized form so the recovery file matches the
|
|
179
|
+
// original submit payload (no raw attachments / internal fields).
|
|
180
|
+
this.recovery.save((0, serializer_js_1.serializeRun)(payload, { includeTestCases: true }));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
exports.RunSubmitter = RunSubmitter;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { FullResult } from '@playwright/test/reporter';
|
|
2
|
+
import type { CollectedTestCase, WireTestCase } from './types.js';
|
|
3
|
+
import type { RunPayload } from './uploader.js';
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the overall Piwi run status from Playwright's `FullResult.status`
|
|
6
|
+
* and the accumulated per-case counters.
|
|
7
|
+
*
|
|
8
|
+
* `passed`/`failed`/`timedout`/`interrupted` map directly; when Playwright
|
|
9
|
+
* doesn't report a status, the run is `passed` only when no test failed or
|
|
10
|
+
* timed out (and at least one test ran).
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveOverallStatus(result: FullResult, counters: {
|
|
13
|
+
failedTests: number;
|
|
14
|
+
timedOutTests: number;
|
|
15
|
+
totalTests: number;
|
|
16
|
+
}): string;
|
|
17
|
+
/**
|
|
18
|
+
* Project an internally-collected test-case object into the wire shape sent to
|
|
19
|
+
* the server. Carries the `type` discriminant through so the same mapper works
|
|
20
|
+
* for `begin` and `complete` stream events as well as batch submissions.
|
|
21
|
+
*
|
|
22
|
+
* Quirks preserved from the original in-reporter implementation:
|
|
23
|
+
* - `status`/`duration`/`error`/`retries` pass through unchanged (no `null`
|
|
24
|
+
* default), so a `begin` event yields `undefined` for those fields.
|
|
25
|
+
* - Numeric/array fields use `|| null` (so `0` and `''` collapse to `null`),
|
|
26
|
+
* while `workerIndex`/`shardIndex`/`startedAt`/`suitePath`/`suiteConfig`/
|
|
27
|
+
* `testAnnotations` use `?? null` (so `0` survives). Note: an empty array
|
|
28
|
+
* is truthy, so `steps: []` is preserved as `[]`, not `null`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function toWireTestCase(tc: CollectedTestCase): WireTestCase;
|
|
31
|
+
/** Options for `serializeRun`. */
|
|
32
|
+
export interface SerializeRunOptions {
|
|
33
|
+
/** Include `testCases` (projected to wire shape) in the serialized body. */
|
|
34
|
+
includeTestCases: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Serialize a `RunPayload` into the JSON body sent to `/api/test-runs/submit`
|
|
38
|
+
* (with `includeTestCases: true`) or the `testRun` form field of
|
|
39
|
+
* `/api/test-runs/upload` (with `includeTestCases: false`, since the multipart
|
|
40
|
+
* path appends `testCases` as a separate form field).
|
|
41
|
+
*
|
|
42
|
+
* This is the single source of truth for the run-level field list — adding a
|
|
43
|
+
* field touches this helper plus the `RunPayload` type, not three files.
|
|
44
|
+
*/
|
|
45
|
+
export declare function serializeRun(payload: RunPayload, opts: SerializeRunOptions): Record<string, unknown>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveOverallStatus = resolveOverallStatus;
|
|
4
|
+
exports.toWireTestCase = toWireTestCase;
|
|
5
|
+
exports.serializeRun = serializeRun;
|
|
6
|
+
/**
|
|
7
|
+
* Resolve the overall Piwi run status from Playwright's `FullResult.status`
|
|
8
|
+
* and the accumulated per-case counters.
|
|
9
|
+
*
|
|
10
|
+
* `passed`/`failed`/`timedout`/`interrupted` map directly; when Playwright
|
|
11
|
+
* doesn't report a status, the run is `passed` only when no test failed or
|
|
12
|
+
* timed out (and at least one test ran).
|
|
13
|
+
*/
|
|
14
|
+
function resolveOverallStatus(result, counters) {
|
|
15
|
+
const STATUS_MAP = {
|
|
16
|
+
passed: 'passed',
|
|
17
|
+
failed: 'failed',
|
|
18
|
+
timedout: 'failed',
|
|
19
|
+
interrupted: 'failed',
|
|
20
|
+
};
|
|
21
|
+
if (result?.status)
|
|
22
|
+
return STATUS_MAP[result.status] ?? 'failed';
|
|
23
|
+
if (counters.failedTests === 0 && counters.timedOutTests === 0 && counters.totalTests > 0)
|
|
24
|
+
return 'passed';
|
|
25
|
+
return 'failed';
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Project an internally-collected test-case object into the wire shape sent to
|
|
29
|
+
* the server. Carries the `type` discriminant through so the same mapper works
|
|
30
|
+
* for `begin` and `complete` stream events as well as batch submissions.
|
|
31
|
+
*
|
|
32
|
+
* Quirks preserved from the original in-reporter implementation:
|
|
33
|
+
* - `status`/`duration`/`error`/`retries` pass through unchanged (no `null`
|
|
34
|
+
* default), so a `begin` event yields `undefined` for those fields.
|
|
35
|
+
* - Numeric/array fields use `|| null` (so `0` and `''` collapse to `null`),
|
|
36
|
+
* while `workerIndex`/`shardIndex`/`startedAt`/`suitePath`/`suiteConfig`/
|
|
37
|
+
* `testAnnotations` use `?? null` (so `0` survives). Note: an empty array
|
|
38
|
+
* is truthy, so `steps: []` is preserved as `[]`, not `null`.
|
|
39
|
+
*/
|
|
40
|
+
function toWireTestCase(tc) {
|
|
41
|
+
const { type, ...rest } = tc;
|
|
42
|
+
return {
|
|
43
|
+
type,
|
|
44
|
+
title: rest.title,
|
|
45
|
+
location: rest.location,
|
|
46
|
+
status: rest.status,
|
|
47
|
+
duration: rest.duration,
|
|
48
|
+
error: rest.error,
|
|
49
|
+
retries: rest.retries,
|
|
50
|
+
workerIndex: rest.workerIndex ?? null,
|
|
51
|
+
shardIndex: rest.shardIndex ?? null,
|
|
52
|
+
startedAt: rest.startedAt ?? null,
|
|
53
|
+
steps: rest.performanceMetrics?.steps || null,
|
|
54
|
+
stepEvents: rest.stepEvents || null,
|
|
55
|
+
slowestStep: rest.performanceMetrics?.slowestStep?.title || null,
|
|
56
|
+
slowestStepDuration: rest.performanceMetrics?.slowestStep?.duration || null,
|
|
57
|
+
wastedTimeMs: rest.performanceMetrics?.waitTotalDuration ?? null,
|
|
58
|
+
networkRequests: rest.networkRequests || null,
|
|
59
|
+
webVitals: rest.webVitals || null,
|
|
60
|
+
consoleLogs: rest.consoleLogs || null,
|
|
61
|
+
ariaSnapshot: rest.ariaSnapshot || null,
|
|
62
|
+
testSource: rest.testSource || null,
|
|
63
|
+
browser: rest.browser || null,
|
|
64
|
+
suitePath: rest.suitePath ?? null,
|
|
65
|
+
suiteConfig: rest.suiteConfig ?? null,
|
|
66
|
+
testAnnotations: rest.testAnnotations ?? null,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Serialize a `RunPayload` into the JSON body sent to `/api/test-runs/submit`
|
|
71
|
+
* (with `includeTestCases: true`) or the `testRun` form field of
|
|
72
|
+
* `/api/test-runs/upload` (with `includeTestCases: false`, since the multipart
|
|
73
|
+
* path appends `testCases` as a separate form field).
|
|
74
|
+
*
|
|
75
|
+
* This is the single source of truth for the run-level field list — adding a
|
|
76
|
+
* field touches this helper plus the `RunPayload` type, not three files.
|
|
77
|
+
*/
|
|
78
|
+
function serializeRun(payload, opts) {
|
|
79
|
+
const body = {
|
|
80
|
+
projectName: payload.projectName,
|
|
81
|
+
projectDescription: payload.projectDescription,
|
|
82
|
+
status: payload.status,
|
|
83
|
+
startTime: payload.startTime,
|
|
84
|
+
duration: payload.duration,
|
|
85
|
+
totalTests: payload.totalTests,
|
|
86
|
+
passedTests: payload.passedTests,
|
|
87
|
+
failedTests: payload.failedTests,
|
|
88
|
+
skippedTests: payload.skippedTests,
|
|
89
|
+
didNotRunTests: payload.didNotRunTests ?? 0,
|
|
90
|
+
environment: payload.environment ?? null,
|
|
91
|
+
label: payload.label ?? null,
|
|
92
|
+
metadata: payload.metadata,
|
|
93
|
+
instanceId: payload.instanceId,
|
|
94
|
+
playwrightVersion: payload.playwrightVersion,
|
|
95
|
+
shardIndex: payload.shardIndex,
|
|
96
|
+
shardTotal: payload.shardTotal,
|
|
97
|
+
isFullRun: payload.isFullRun ?? true,
|
|
98
|
+
filterDetails: payload.filterDetails ?? null,
|
|
99
|
+
};
|
|
100
|
+
if (opts.includeTestCases) {
|
|
101
|
+
body.testCases = payload.testCases.map((tc) => toWireTestCase(tc));
|
|
102
|
+
}
|
|
103
|
+
return body;
|
|
104
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { TestAnnotation } from './types.js';
|
|
2
|
+
/** Minimal structural shape for the annotation carriers (avoids importing Playwright types). */
|
|
3
|
+
interface AnnotationCarrier {
|
|
4
|
+
annotations?: ReadonlyArray<{
|
|
5
|
+
type: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
}>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Merge a test's declared annotations (`test.annotations`) with its result-level
|
|
11
|
+
* annotations (`result.annotations`), deduped by `type + description`. Runtime
|
|
12
|
+
* `test.skip('reason')` calls can surface on either side depending on the
|
|
13
|
+
* Playwright version, so both are considered.
|
|
14
|
+
*/
|
|
15
|
+
export declare function mergeAnnotations(test: AnnotationCarrier, result: AnnotationCarrier): TestAnnotation[];
|
|
16
|
+
/**
|
|
17
|
+
* Distinguish an intentional skip from a test that could not run.
|
|
18
|
+
*
|
|
19
|
+
* Playwright reports both as `result.status === 'skipped'`, but an intentional
|
|
20
|
+
* `test.skip()` / `test.fixme()` (static, conditional, or runtime) always
|
|
21
|
+
* carries a `skip`/`fixme` annotation, while a test skipped as a side effect of
|
|
22
|
+
* an earlier failure in a `describe.serial` group carries none. The latter is
|
|
23
|
+
* reclassified to `didnotrun` so the dashboard can tell "deliberately skipped"
|
|
24
|
+
* from "never actually executed". Non-skipped statuses pass through unchanged.
|
|
25
|
+
*/
|
|
26
|
+
export declare function classifyStatus(rawStatus: string, annotations: TestAnnotation[]): string;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mergeAnnotations = mergeAnnotations;
|
|
4
|
+
exports.classifyStatus = classifyStatus;
|
|
5
|
+
/**
|
|
6
|
+
* Merge a test's declared annotations (`test.annotations`) with its result-level
|
|
7
|
+
* annotations (`result.annotations`), deduped by `type + description`. Runtime
|
|
8
|
+
* `test.skip('reason')` calls can surface on either side depending on the
|
|
9
|
+
* Playwright version, so both are considered.
|
|
10
|
+
*/
|
|
11
|
+
function mergeAnnotations(test, result) {
|
|
12
|
+
const out = [];
|
|
13
|
+
const seen = new Set();
|
|
14
|
+
for (const list of [test.annotations, result.annotations]) {
|
|
15
|
+
for (const a of list ?? []) {
|
|
16
|
+
const key = `${a.type}\x00${a.description ?? ''}`;
|
|
17
|
+
if (seen.has(key))
|
|
18
|
+
continue;
|
|
19
|
+
seen.add(key);
|
|
20
|
+
out.push(a.description === undefined ? { type: a.type } : { type: a.type, description: a.description });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Distinguish an intentional skip from a test that could not run.
|
|
27
|
+
*
|
|
28
|
+
* Playwright reports both as `result.status === 'skipped'`, but an intentional
|
|
29
|
+
* `test.skip()` / `test.fixme()` (static, conditional, or runtime) always
|
|
30
|
+
* carries a `skip`/`fixme` annotation, while a test skipped as a side effect of
|
|
31
|
+
* an earlier failure in a `describe.serial` group carries none. The latter is
|
|
32
|
+
* reclassified to `didnotrun` so the dashboard can tell "deliberately skipped"
|
|
33
|
+
* from "never actually executed". Non-skipped statuses pass through unchanged.
|
|
34
|
+
*/
|
|
35
|
+
function classifyStatus(rawStatus, annotations) {
|
|
36
|
+
if (rawStatus !== 'skipped')
|
|
37
|
+
return rawStatus;
|
|
38
|
+
const intentional = annotations.some((a) => a.type === 'skip' || a.type === 'fixme');
|
|
39
|
+
return intentional ? 'skipped' : 'didnotrun';
|
|
40
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Categorise a Playwright step into `navigation`, `action`, `input`,
|
|
3
|
+
* `assertion`, `wait`, `api`, `hook`, or `other`.
|
|
4
|
+
*
|
|
5
|
+
* Supports two title formats:
|
|
6
|
+
* - Legacy api-path titles ("page.goto", "locator.click", "page.waitForTimeout")
|
|
7
|
+
* - Modern human-readable titles introduced in newer Playwright versions
|
|
8
|
+
* ("Navigate to \"{url}\"", "Click", "Wait for timeout", "Wait for load state").
|
|
9
|
+
* Hook/fixture/expect steps are detected via Playwright's own `category`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function categorizeStep(title: string, pwCategory?: string): string;
|
|
12
|
+
/** A single step flattened from the Playwright step tree with its derived category */
|
|
13
|
+
export interface FlatStep {
|
|
14
|
+
title: string;
|
|
15
|
+
duration: number;
|
|
16
|
+
category: string;
|
|
17
|
+
}
|
|
18
|
+
/** Step-event category restricted to the values `extractTestStepEvents` emits. */
|
|
19
|
+
export type StepEventCategory = 'hook' | 'fixture' | 'test.step' | 'expect' | 'wait';
|
|
20
|
+
/** Recursively flatten a nested step tree into a flat list. Uses Playwright's built-in category when available. */
|
|
21
|
+
export declare function flattenSteps(steps: any[]): FlatStep[];
|
|
22
|
+
/** Aggregated step performance data for a single test case */
|
|
23
|
+
export interface StepMetrics {
|
|
24
|
+
/** Flattened step list with categories */
|
|
25
|
+
steps: FlatStep[];
|
|
26
|
+
/** Sum of top-level step durations */
|
|
27
|
+
totalStepDuration: number;
|
|
28
|
+
/** The single slowest step (by duration) */
|
|
29
|
+
slowestStep: {
|
|
30
|
+
title: string;
|
|
31
|
+
duration: number;
|
|
32
|
+
} | null;
|
|
33
|
+
/** How many navigation steps were executed */
|
|
34
|
+
navigationCount: number;
|
|
35
|
+
/** Total wall-clock time spent in navigation steps */
|
|
36
|
+
navigationTotalDuration: number;
|
|
37
|
+
/** Total wall-clock time spent in wait steps (wasted time) */
|
|
38
|
+
waitTotalDuration: number;
|
|
39
|
+
/** Count of wait steps */
|
|
40
|
+
waitCount: number;
|
|
41
|
+
}
|
|
42
|
+
/** Collect step metrics (flat steps, slowest step, navigation stats) from a Playwright step array */
|
|
43
|
+
export declare function collectStepMetrics(steps: any[]): StepMetrics;
|
|
44
|
+
/** Calculate the p-th percentile from a sorted array of numbers */
|
|
45
|
+
export declare function percentile(sortedArr: number[], p: number): number;
|
|
46
|
+
/** Summary performance statistics for a complete test run */
|
|
47
|
+
export interface PerformanceSummary {
|
|
48
|
+
/** Average test-case duration in ms */
|
|
49
|
+
avgTestDuration?: number;
|
|
50
|
+
/** Median (P50) test-case duration in ms */
|
|
51
|
+
p50TestDuration?: number;
|
|
52
|
+
/** P90 test-case duration in ms */
|
|
53
|
+
p90TestDuration?: number;
|
|
54
|
+
/** P95 test-case duration in ms */
|
|
55
|
+
p95TestDuration?: number;
|
|
56
|
+
/** Up to 5 slowest test cases */
|
|
57
|
+
slowestTests?: Array<{
|
|
58
|
+
title: string;
|
|
59
|
+
duration: number;
|
|
60
|
+
}>;
|
|
61
|
+
/** Total time spent in navigation steps across all cases */
|
|
62
|
+
totalNavigationDuration?: number;
|
|
63
|
+
/** Average time per navigation step */
|
|
64
|
+
avgNavigationDuration?: number;
|
|
65
|
+
/** Total time spent in wait steps across all cases */
|
|
66
|
+
totalWastedTimeMs?: number;
|
|
67
|
+
}
|
|
68
|
+
/** Compute run-level performance summary (averages, percentiles, slowest tests) from all test cases */
|
|
69
|
+
export declare function computePerformanceSummary(testCases: any[]): PerformanceSummary;
|
|
70
|
+
/**
|
|
71
|
+
* Extract hook and fixture step events with absolute timings from a Playwright
|
|
72
|
+
* step tree. These are used by the WorkersTimeline to render hook segments.
|
|
73
|
+
*
|
|
74
|
+
* Returns only top-level hook/fixture steps (beforeEach, afterEach, fixture
|
|
75
|
+
* setup/teardown) — their sub-steps are included implicitly in their duration.
|
|
76
|
+
*/
|
|
77
|
+
export declare function extractTestStepEvents(steps: any[], _testStartTime: Date): Array<{
|
|
78
|
+
title: string;
|
|
79
|
+
category: StepEventCategory;
|
|
80
|
+
startedAt: number;
|
|
81
|
+
duration: number;
|
|
82
|
+
status: string;
|
|
83
|
+
location?: string | null;
|
|
84
|
+
}>;
|
|
85
|
+
/**
|
|
86
|
+
* Recursively extract wait-category steps from the Playwright step tree
|
|
87
|
+
* with absolute timings. These are rendered as semi-transparent amber bars
|
|
88
|
+
* on WorkersTimeline to visualize wasted time.
|
|
89
|
+
*/
|
|
90
|
+
export declare function extractWaitEvents(steps: any[], insideWait?: boolean): Array<{
|
|
91
|
+
title: string;
|
|
92
|
+
category: StepEventCategory;
|
|
93
|
+
startedAt: number;
|
|
94
|
+
duration: number;
|
|
95
|
+
status: string;
|
|
96
|
+
location?: string | null;
|
|
97
|
+
}>;
|