@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
package/dist/uploader.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Uploader = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const form_data_1 = __importDefault(require("form-data"));
|
|
10
|
+
const serializer_js_1 = require("./serializer.js");
|
|
11
|
+
/**
|
|
12
|
+
* Handles all upload strategies: plain JSON, multipart (with reports and
|
|
13
|
+
* traces), per-test-case file uploads for streaming runs, and report-only
|
|
14
|
+
* uploads for already-submitted streaming runs.
|
|
15
|
+
*/
|
|
16
|
+
class Uploader {
|
|
17
|
+
/**
|
|
18
|
+
* @param httpClient HTTP client for server communication.
|
|
19
|
+
* @param fileHandler File discovery and compression helper.
|
|
20
|
+
* @param logger Prefixed logger.
|
|
21
|
+
*/
|
|
22
|
+
constructor(httpClient, fileHandler, logger) {
|
|
23
|
+
this.httpClient = httpClient;
|
|
24
|
+
this.fileHandler = fileHandler;
|
|
25
|
+
this.logger = logger;
|
|
26
|
+
}
|
|
27
|
+
/** Submit test results as a plain JSON payload (no file attachments) */
|
|
28
|
+
async uploadJSON(payload, auth) {
|
|
29
|
+
const response = await this.httpClient.postJSON('/api/test-runs/submit', (0, serializer_js_1.serializeRun)(payload, { includeTestCases: true }), auth);
|
|
30
|
+
this.logger.info(`Successfully uploaded test results`);
|
|
31
|
+
if (response.testRunId)
|
|
32
|
+
this.logger.info(`Test Run ID: ${response.testRunId}, Project ID: ${response.projectId}`);
|
|
33
|
+
return response;
|
|
34
|
+
}
|
|
35
|
+
/** Submit test results as a multipart form with trace files and compressed report directories */
|
|
36
|
+
async uploadWithFiles(payload, reportOptions, auth) {
|
|
37
|
+
const form = new form_data_1.default();
|
|
38
|
+
form.append('projectName', payload.projectName);
|
|
39
|
+
form.append('testRun', JSON.stringify((0, serializer_js_1.serializeRun)(payload, { includeTestCases: false })));
|
|
40
|
+
form.append('testCases', JSON.stringify(payload.testCases.map((tc) => (0, serializer_js_1.toWireTestCase)(tc))));
|
|
41
|
+
await this.appendReportsToForm(form, reportOptions.reports, reportOptions.uploadReport);
|
|
42
|
+
await this.appendFilesToForm(form, payload.testCases, reportOptions.uploadTraces);
|
|
43
|
+
const response = await this.httpClient.postFormData('/api/test-runs/upload', form, auth);
|
|
44
|
+
this.logger.info(`Successfully uploaded test results with files`);
|
|
45
|
+
if (response.testRunId)
|
|
46
|
+
this.logger.info(`Test Run ID: ${response.testRunId}, Project ID: ${response.projectId}`);
|
|
47
|
+
if (response.reports) {
|
|
48
|
+
for (const r of response.reports)
|
|
49
|
+
this.logger.info(`${r.label}: ${r.path}`);
|
|
50
|
+
}
|
|
51
|
+
return response;
|
|
52
|
+
}
|
|
53
|
+
/** Upload report files for an already-submitted streaming run */
|
|
54
|
+
async uploadReportsForStreamingRun(projectName, runId, reportOptions, startTime, auth) {
|
|
55
|
+
const form = new form_data_1.default();
|
|
56
|
+
form.append('testRunId', String(runId));
|
|
57
|
+
form.append('projectName', projectName);
|
|
58
|
+
form.append('testRun', JSON.stringify({
|
|
59
|
+
status: 'already-submitted',
|
|
60
|
+
startTime,
|
|
61
|
+
duration: 0,
|
|
62
|
+
totalTests: 0,
|
|
63
|
+
passedTests: 0,
|
|
64
|
+
failedTests: 0,
|
|
65
|
+
skippedTests: 0,
|
|
66
|
+
metadata: {},
|
|
67
|
+
}));
|
|
68
|
+
form.append('testCases', JSON.stringify([]));
|
|
69
|
+
await this.appendReportsToForm(form, reportOptions.reports, reportOptions.uploadReport);
|
|
70
|
+
const response = await this.httpClient.postFormData('/api/test-runs/upload', form, auth);
|
|
71
|
+
this.logger.info(`Successfully uploaded reports for streaming run #${runId}`);
|
|
72
|
+
if (response.reports) {
|
|
73
|
+
for (const r of response.reports)
|
|
74
|
+
this.logger.info(`${r.label}: ${r.path}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Upload one test case's trace and attachments for a streaming run.
|
|
79
|
+
* The matching `complete` event must have been flushed to the server first.
|
|
80
|
+
* Returns `false` when the case has no files to upload.
|
|
81
|
+
*/
|
|
82
|
+
async uploadCaseFiles(projectName, runId, streamToken, testCase, uploadTraces, auth) {
|
|
83
|
+
const attachments = this.fileHandler.findAllAttachments(testCase);
|
|
84
|
+
let traceInfo = null;
|
|
85
|
+
if (uploadTraces) {
|
|
86
|
+
traceInfo = await this.fileHandler.computeSingleTraceHash(testCase);
|
|
87
|
+
}
|
|
88
|
+
if (!traceInfo && attachments.length === 0)
|
|
89
|
+
return false;
|
|
90
|
+
// Skip the trace body when the server already has this blob
|
|
91
|
+
let missingHashes = null;
|
|
92
|
+
if (traceInfo) {
|
|
93
|
+
missingHashes = await this.fileHandler.checkMissingTraces(this.httpClient, projectName, new Map([[0, traceInfo]]), auth);
|
|
94
|
+
}
|
|
95
|
+
const buildForm = (includeTraceFile) => {
|
|
96
|
+
const form = new form_data_1.default();
|
|
97
|
+
form.append('streamToken', streamToken);
|
|
98
|
+
form.append('testCase', JSON.stringify({
|
|
99
|
+
title: testCase.title,
|
|
100
|
+
location: testCase.location,
|
|
101
|
+
retries: testCase.retries ?? 0,
|
|
102
|
+
suitePath: testCase.suitePath ?? null,
|
|
103
|
+
}));
|
|
104
|
+
if (traceInfo) {
|
|
105
|
+
form.append('trace_hash', traceInfo.hash);
|
|
106
|
+
if (includeTraceFile) {
|
|
107
|
+
form.append('trace', fs_1.default.createReadStream(traceInfo.tracePath), {
|
|
108
|
+
filename: path_1.default.basename(traceInfo.tracePath),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (attachments.length > 0) {
|
|
113
|
+
form.append('attach_meta', JSON.stringify(attachments.map((a) => ({ name: a.name, contentType: a.contentType, originalName: a.originalName }))));
|
|
114
|
+
for (const a of attachments) {
|
|
115
|
+
form.append('attach_file', fs_1.default.createReadStream(a.path), { filename: a.originalName });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return form;
|
|
119
|
+
};
|
|
120
|
+
const includeTraceFile = !traceInfo || !missingHashes || missingHashes.has(traceInfo.hash);
|
|
121
|
+
try {
|
|
122
|
+
await this.httpClient.postFormData(`/api/test-runs/${runId}/case-files`, buildForm(includeTraceFile), auth);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
// 422: the server doesn't have the blob after all — resend with the file
|
|
126
|
+
if (traceInfo && !includeTraceFile && error.message?.includes('422')) {
|
|
127
|
+
await this.httpClient.postFormData(`/api/test-runs/${runId}/case-files`, buildForm(true), auth);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
this.logger.debug(`Uploaded files for "${testCase.title}" (trace: ${traceInfo ? 'yes' : 'no'}, attachments: ${attachments.length})`);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
async appendReportsToForm(form, reports, uploadReport) {
|
|
137
|
+
const list = reports ? [...reports] : [];
|
|
138
|
+
if (uploadReport && !list.some((r) => r.type === 'html'))
|
|
139
|
+
list.push({ type: 'html' });
|
|
140
|
+
for (const cfg of list) {
|
|
141
|
+
const defaultDir = this.fileHandler.getDefaultReportDirs()[cfg.type] || cfg.type + '-report';
|
|
142
|
+
const reportDir = cfg.dir
|
|
143
|
+
? this.fileHandler.findReportDirectory(cfg.dir)
|
|
144
|
+
: cfg.type === 'html'
|
|
145
|
+
? this.fileHandler.findHTMLReportDirectory()
|
|
146
|
+
: this.fileHandler.findReportDirectory(defaultDir);
|
|
147
|
+
if (!reportDir) {
|
|
148
|
+
this.logger.debug(`No report directory found for type '${cfg.type}'`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const compressed = await this.fileHandler.compressReportDirectory(reportDir);
|
|
152
|
+
if (compressed) {
|
|
153
|
+
form.append(`report_${cfg.type}`, compressed, { filename: `${cfg.type}-report.gz` });
|
|
154
|
+
if (cfg.label)
|
|
155
|
+
form.append(`report_label_${cfg.type}`, cfg.label);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async appendFilesToForm(form, testCases, uploadTraces) {
|
|
160
|
+
let attachmentCount = 0;
|
|
161
|
+
for (const [i, tc] of testCases.entries()) {
|
|
162
|
+
const attachments = this.fileHandler.findAllAttachments(tc);
|
|
163
|
+
if (attachments.length === 0)
|
|
164
|
+
continue;
|
|
165
|
+
form.append(`attach_meta_${i}`, JSON.stringify(attachments.map((a) => ({
|
|
166
|
+
name: a.name,
|
|
167
|
+
contentType: a.contentType,
|
|
168
|
+
originalName: a.originalName,
|
|
169
|
+
}))));
|
|
170
|
+
for (const a of attachments) {
|
|
171
|
+
form.append(`attach_file_${i}`, fs_1.default.createReadStream(a.path), { filename: a.originalName });
|
|
172
|
+
attachmentCount++;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (attachmentCount > 0)
|
|
176
|
+
this.logger.info(`Uploading ${attachmentCount} non-trace attachments`);
|
|
177
|
+
if (!uploadTraces)
|
|
178
|
+
return;
|
|
179
|
+
let traceCount = 0;
|
|
180
|
+
for (const [i, tc] of testCases.entries()) {
|
|
181
|
+
for (const tp of this.fileHandler.findTraceFiles(tc)) {
|
|
182
|
+
if (fs_1.default.existsSync(tp)) {
|
|
183
|
+
form.append(`trace_${i}`, fs_1.default.createReadStream(tp), { filename: path_1.default.basename(tp) });
|
|
184
|
+
traceCount++;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
this.logger.info(`Found ${traceCount} trace files`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
exports.Uploader = Uploader;
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@piwitests/reporter",
|
|
3
|
+
"version": "0.4.2",
|
|
4
|
+
"description": "Playwright reporter for sending test results to Piwi Dashboard",
|
|
5
|
+
"url": "https://github.com/PiwiTests/platform",
|
|
6
|
+
"homepage": "https://piwitests.github.io",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/PiwiTests/platform"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/PiwiTests/platform/issues"
|
|
13
|
+
},
|
|
14
|
+
"type": "commonjs",
|
|
15
|
+
"main": "dist/index.js",
|
|
16
|
+
"types": "dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js",
|
|
21
|
+
"require": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./fixtures": {
|
|
24
|
+
"types": "./dist/fixtures.d.ts",
|
|
25
|
+
"import": "./dist/fixtures.js",
|
|
26
|
+
"require": "./dist/fixtures.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"playwright",
|
|
31
|
+
"reporter",
|
|
32
|
+
"dashboard",
|
|
33
|
+
"test-results"
|
|
34
|
+
],
|
|
35
|
+
"author": "piwitests",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public",
|
|
39
|
+
"provenance": true
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"reporter:build": "tsc",
|
|
43
|
+
"reporter:dev": "tsc --watch",
|
|
44
|
+
"reporter:format": "oxfmt --config oxfmt.config.mts --write \"src/\"",
|
|
45
|
+
"reporter:format:check": "oxfmt --config oxfmt.config.mts --check \"src/\"",
|
|
46
|
+
"reporter:lint": "oxlint --config oxlint.config.mts .",
|
|
47
|
+
"reporter:lint:fix": "oxlint --config oxlint.config.mts . --fix",
|
|
48
|
+
"reporter:test": "vitest run",
|
|
49
|
+
"reporter:test:watch": "vitest",
|
|
50
|
+
"test": "npm run reporter:test",
|
|
51
|
+
"prepublishOnly": "npm run reporter:build"
|
|
52
|
+
},
|
|
53
|
+
"files": [
|
|
54
|
+
"dist/"
|
|
55
|
+
],
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"@playwright/test": "^1.40.0"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"form-data": "^4.0.0"
|
|
61
|
+
}
|
|
62
|
+
}
|