@orangebeard-io/playwright-orangebeard-reporter 1.0.5 → 1.0.8
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/README.md +1 -1
- package/dist/reporter/OrangebeardReporter.js +155 -30
- package/dist/reporter/utils.js +26 -0
- package/package.json +4 -9
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ Create orangebeard.json (in your test projects's folder (or above))
|
|
|
45
45
|
```JSON
|
|
46
46
|
{
|
|
47
47
|
"endpoint": "https://XXX.orangebeard.app",
|
|
48
|
-
"
|
|
48
|
+
"token": "00000000-0000-0000-0000-00000000",
|
|
49
49
|
"project": "my_project_name",
|
|
50
50
|
"testset": "My Test Set Name",
|
|
51
51
|
"description": "A run from playwright",
|
|
@@ -48,10 +48,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
48
48
|
exports.OrangebeardReporter = void 0;
|
|
49
49
|
const utils_1 = require("./utils");
|
|
50
50
|
const OrangebeardAsyncV3Client_1 = __importDefault(require("@orangebeard-io/javascript-client/dist/client/OrangebeardAsyncV3Client"));
|
|
51
|
-
const StartTest_1 = require("@orangebeard-io/javascript-client/dist/client/models/StartTest");
|
|
52
51
|
const Log_1 = require("@orangebeard-io/javascript-client/dist/client/models/Log");
|
|
53
52
|
const FinishStep_1 = require("@orangebeard-io/javascript-client/dist/client/models/FinishStep");
|
|
54
|
-
var TestType = StartTest_1.StartTest.TestType;
|
|
55
53
|
var LogFormat = Log_1.Log.LogFormat;
|
|
56
54
|
var LogLevel = Log_1.Log.LogLevel;
|
|
57
55
|
var Status = FinishStep_1.FinishStep.Status;
|
|
@@ -62,6 +60,7 @@ class OrangebeardReporter {
|
|
|
62
60
|
this.tests = new Map(); //testId, uuid
|
|
63
61
|
this.steps = new Map(); //testId_stepPath, uuid
|
|
64
62
|
this.promises = [];
|
|
63
|
+
this.processedStepAttachments = new Map(); // testId -> set of attachment keys already uploaded on step level
|
|
65
64
|
this.client = new OrangebeardAsyncV3Client_1.default();
|
|
66
65
|
this.config = this.client.config;
|
|
67
66
|
}
|
|
@@ -135,6 +134,34 @@ class OrangebeardReporter {
|
|
|
135
134
|
onStepEnd(test, _result, step) {
|
|
136
135
|
const testUUID = this.tests.get(test.id);
|
|
137
136
|
const stepUUID = this.steps.get(test.id + "|" + step.titlePath());
|
|
137
|
+
// Handle step-level attachments (similar to test-level attachments in onTestEnd)
|
|
138
|
+
if (step.attachments && step.attachments.length > 0 && stepUUID) {
|
|
139
|
+
let message = "";
|
|
140
|
+
for (const attachment of step.attachments) {
|
|
141
|
+
message += `- ${attachment.name} (${attachment.contentType})\\n`;
|
|
142
|
+
}
|
|
143
|
+
const attachmentsLogUUID = this.client.log({
|
|
144
|
+
logFormat: LogFormat.MARKDOWN,
|
|
145
|
+
logLevel: LogLevel.INFO,
|
|
146
|
+
logTime: (0, utils_1.getTime)(),
|
|
147
|
+
message: message,
|
|
148
|
+
testRunUUID: this.testRunId,
|
|
149
|
+
testUUID: testUUID,
|
|
150
|
+
stepUUID: stepUUID,
|
|
151
|
+
});
|
|
152
|
+
for (const attachment of step.attachments) {
|
|
153
|
+
// Track that this attachment has already been uploaded on the step level,
|
|
154
|
+
// so we can skip it when handling test-level attachments in onTestEnd.
|
|
155
|
+
const key = (0, utils_1.getAttachmentKey)(attachment);
|
|
156
|
+
let processedForTest = this.processedStepAttachments.get(test.id);
|
|
157
|
+
if (!processedForTest) {
|
|
158
|
+
processedForTest = new Set();
|
|
159
|
+
this.processedStepAttachments.set(test.id, processedForTest);
|
|
160
|
+
}
|
|
161
|
+
processedForTest.add(key);
|
|
162
|
+
this.promises.push(this.logAttachment(attachment, testUUID, attachmentsLogUUID));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
138
165
|
if (step.error) {
|
|
139
166
|
const message = step.error.message;
|
|
140
167
|
this.client.log({
|
|
@@ -165,15 +192,59 @@ class OrangebeardReporter {
|
|
|
165
192
|
});
|
|
166
193
|
this.steps.delete(test.id + "|" + step.titlePath());
|
|
167
194
|
}
|
|
168
|
-
onTestBegin(test) {
|
|
195
|
+
onTestBegin(test, result) {
|
|
196
|
+
var _a;
|
|
169
197
|
//check suite
|
|
170
198
|
const suiteUUID = this.getOrStartSuite(test.parent.titlePath());
|
|
171
199
|
const attributes = [];
|
|
200
|
+
// Tags -> attributes without key
|
|
172
201
|
for (const tag of test.tags) {
|
|
173
202
|
attributes.push({ value: tag });
|
|
174
203
|
}
|
|
204
|
+
// Annotations -> structured attributes
|
|
205
|
+
for (const annotation of test.annotations) {
|
|
206
|
+
const description = (_a = annotation.description) === null || _a === void 0 ? void 0 : _a.trim();
|
|
207
|
+
switch (annotation.type) {
|
|
208
|
+
case 'issue':
|
|
209
|
+
case 'bug':
|
|
210
|
+
if (description) {
|
|
211
|
+
attributes.push({ key: 'Issue', value: description });
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
case 'tag':
|
|
215
|
+
if (description) {
|
|
216
|
+
attributes.push({ value: description });
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
219
|
+
case 'skip':
|
|
220
|
+
case 'slow':
|
|
221
|
+
case 'fixme':
|
|
222
|
+
case 'fail': {
|
|
223
|
+
const value = description && description.length > 0 ? description : annotation.type;
|
|
224
|
+
attributes.push({ key: 'PlaywrightAnnotation', value: value });
|
|
225
|
+
if (annotation.type === 'fail') {
|
|
226
|
+
// Mark tests annotated as expected-to-fail
|
|
227
|
+
attributes.push({ key: 'ExpectedStatus', value: 'failed' });
|
|
228
|
+
attributes.push({ key: 'ExpectedToFail', value: 'true' });
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
default:
|
|
233
|
+
if (description && description.length > 0) {
|
|
234
|
+
attributes.push({ key: `annotation:${annotation.type}`, value: description });
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
attributes.push({ value: `annotation:${annotation.type}` });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// If this is a retry attempt (retry index > 0), add a Retry attribute
|
|
242
|
+
if (typeof (result === null || result === void 0 ? void 0 : result.retry) === 'number' && result.retry > 0) {
|
|
243
|
+
attributes.push({ key: 'Retry', value: result.retry.toString() });
|
|
244
|
+
}
|
|
245
|
+
const testType = (0, utils_1.determineTestType)(test.parent.titlePath().join('>'));
|
|
175
246
|
const testUUID = this.client.startTest({
|
|
176
|
-
testType:
|
|
247
|
+
testType: testType,
|
|
177
248
|
testRunUUID: this.testRunId,
|
|
178
249
|
suiteUUID: suiteUUID,
|
|
179
250
|
testName: test.title,
|
|
@@ -186,10 +257,15 @@ class OrangebeardReporter {
|
|
|
186
257
|
onTestEnd(test, result) {
|
|
187
258
|
return __awaiter(this, void 0, void 0, function* () {
|
|
188
259
|
const testUUID = this.tests.get(test.id);
|
|
189
|
-
|
|
260
|
+
// Filter out attachments that were already handled at step level to avoid duplicates.
|
|
261
|
+
const processedForTest = this.processedStepAttachments.get(test.id);
|
|
262
|
+
const remainingAttachments = processedForTest
|
|
263
|
+
? result.attachments.filter((attachment) => !processedForTest.has((0, utils_1.getAttachmentKey)(attachment)))
|
|
264
|
+
: result.attachments;
|
|
265
|
+
if (remainingAttachments.length > 0) {
|
|
190
266
|
let message = "";
|
|
191
|
-
for (const attachment of
|
|
192
|
-
message += `- ${attachment.name} (${attachment.contentType})
|
|
267
|
+
for (const attachment of remainingAttachments) {
|
|
268
|
+
message += `- ${attachment.name} (${attachment.contentType})\\n`;
|
|
193
269
|
}
|
|
194
270
|
const attachmentsLogUUID = this.client.log({
|
|
195
271
|
logFormat: LogFormat.MARKDOWN,
|
|
@@ -199,12 +275,52 @@ class OrangebeardReporter {
|
|
|
199
275
|
testRunUUID: this.testRunId,
|
|
200
276
|
testUUID: testUUID
|
|
201
277
|
});
|
|
202
|
-
for (const attachment of
|
|
278
|
+
for (const attachment of remainingAttachments) {
|
|
203
279
|
this.promises.push(this.logAttachment(attachment, testUUID, attachmentsLogUUID));
|
|
204
280
|
}
|
|
205
281
|
}
|
|
282
|
+
// Log test-level errors that are not tied to specific steps (e.g. hooks/fixtures)
|
|
283
|
+
const errors = result.errors && result.errors.length > 0
|
|
284
|
+
? result.errors
|
|
285
|
+
: (result.error ? [result.error] : []);
|
|
286
|
+
if (errors && errors.length > 0) {
|
|
287
|
+
let errorMessage = '';
|
|
288
|
+
for (let index = 0; index < errors.length; index += 1) {
|
|
289
|
+
const err = errors[index];
|
|
290
|
+
if (index > 0) {
|
|
291
|
+
errorMessage += '\n\n';
|
|
292
|
+
}
|
|
293
|
+
if (err.message) {
|
|
294
|
+
errorMessage += `**Error:** ${(0, utils_1.ansiToMarkdown)(err.message)}\n`;
|
|
295
|
+
}
|
|
296
|
+
if (err.stack) {
|
|
297
|
+
errorMessage += `\`\`\`\n${(0, utils_1.removeAnsi)(err.stack)}\n\`\`\``;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (errorMessage.length > 0) {
|
|
301
|
+
this.client.log({
|
|
302
|
+
logFormat: LogFormat.MARKDOWN,
|
|
303
|
+
logLevel: LogLevel.ERROR,
|
|
304
|
+
logTime: (0, utils_1.getTime)(),
|
|
305
|
+
message: errorMessage,
|
|
306
|
+
testRunUUID: this.testRunId,
|
|
307
|
+
testUUID: testUUID,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
206
311
|
//determine status
|
|
207
312
|
const status = utils_1.testStatusMap[result.status];
|
|
313
|
+
// If the test passed after one or more retries, mark it as flaky in the logs
|
|
314
|
+
if (typeof result.retry === 'number' && result.retry > 0 && status === Status.PASSED) {
|
|
315
|
+
this.client.log({
|
|
316
|
+
logFormat: LogFormat.PLAIN_TEXT,
|
|
317
|
+
logLevel: LogLevel.INFO,
|
|
318
|
+
logTime: (0, utils_1.getTime)(),
|
|
319
|
+
message: `Test passed after ${result.retry} retr${result.retry === 1 ? 'y' : 'ies'}`,
|
|
320
|
+
testRunUUID: this.testRunId,
|
|
321
|
+
testUUID: testUUID,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
208
324
|
//finish test
|
|
209
325
|
this.client.finishTest(testUUID, {
|
|
210
326
|
testRunUUID: this.testRunId,
|
|
@@ -212,6 +328,7 @@ class OrangebeardReporter {
|
|
|
212
328
|
endTime: (0, utils_1.getTime)()
|
|
213
329
|
});
|
|
214
330
|
this.tests.delete(test.id);
|
|
331
|
+
this.processedStepAttachments.delete(test.id);
|
|
215
332
|
});
|
|
216
333
|
}
|
|
217
334
|
printsToStdio() {
|
|
@@ -253,30 +370,38 @@ class OrangebeardReporter {
|
|
|
253
370
|
}
|
|
254
371
|
logAttachment(attachment, testUUID, logUUID) {
|
|
255
372
|
return __awaiter(this, void 0, void 0, function* () {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
373
|
+
try {
|
|
374
|
+
let content;
|
|
375
|
+
if (attachment.body) {
|
|
376
|
+
content = attachment.body;
|
|
377
|
+
}
|
|
378
|
+
else if (attachment.path) {
|
|
379
|
+
content = yield (0, utils_1.getBytes)(attachment.path);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
throw new Error("Attachment must have either body or path defined.");
|
|
383
|
+
}
|
|
384
|
+
const orangebeardAttachment = {
|
|
385
|
+
file: {
|
|
386
|
+
name: path.basename(attachment.path),
|
|
387
|
+
content: content,
|
|
388
|
+
contentType: attachment.contentType,
|
|
389
|
+
},
|
|
390
|
+
metaData: {
|
|
391
|
+
testRunUUID: this.testRunId,
|
|
392
|
+
testUUID: testUUID,
|
|
393
|
+
logUUID: logUUID,
|
|
394
|
+
attachmentTime: (0, utils_1.getTime)()
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
yield this.client.sendAttachment(orangebeardAttachment);
|
|
262
398
|
}
|
|
263
|
-
|
|
264
|
-
|
|
399
|
+
catch (err) {
|
|
400
|
+
// Avoid failing the entire test run due to a single attachment failure.
|
|
401
|
+
// Log to stderr so issues are visible during test execution.
|
|
402
|
+
// eslint-disable-next-line no-console
|
|
403
|
+
console.error('Error sending attachment to Orangebeard:', err);
|
|
265
404
|
}
|
|
266
|
-
const orangebeardAttachment = {
|
|
267
|
-
file: {
|
|
268
|
-
name: path.basename(attachment.path),
|
|
269
|
-
content: content,
|
|
270
|
-
contentType: attachment.contentType,
|
|
271
|
-
},
|
|
272
|
-
metaData: {
|
|
273
|
-
testRunUUID: this.testRunId,
|
|
274
|
-
testUUID: testUUID,
|
|
275
|
-
logUUID: logUUID,
|
|
276
|
-
attachmentTime: (0, utils_1.getTime)()
|
|
277
|
-
},
|
|
278
|
-
};
|
|
279
|
-
this.client.sendAttachment(orangebeardAttachment);
|
|
280
405
|
});
|
|
281
406
|
}
|
|
282
407
|
}
|
package/dist/reporter/utils.js
CHANGED
|
@@ -47,11 +47,15 @@ exports.getTime = getTime;
|
|
|
47
47
|
exports.removeAnsi = removeAnsi;
|
|
48
48
|
exports.ansiToMarkdown = ansiToMarkdown;
|
|
49
49
|
exports.getCodeSnippet = getCodeSnippet;
|
|
50
|
+
exports.getAttachmentKey = getAttachmentKey;
|
|
51
|
+
exports.determineTestType = determineTestType;
|
|
50
52
|
const core_1 = require("@js-joda/core");
|
|
51
53
|
const FinishTest_1 = require("@orangebeard-io/javascript-client/dist/client/models/FinishTest");
|
|
52
54
|
const fs = __importStar(require("node:fs"));
|
|
53
55
|
const util_1 = require("util");
|
|
54
56
|
var Status = FinishTest_1.FinishTest.Status;
|
|
57
|
+
const StartTest_1 = require("@orangebeard-io/javascript-client/dist/client/models/StartTest");
|
|
58
|
+
var TestType = StartTest_1.StartTest.TestType;
|
|
55
59
|
const stat = (0, util_1.promisify)(fs.stat);
|
|
56
60
|
const access = (0, util_1.promisify)(fs.access);
|
|
57
61
|
function getTime() {
|
|
@@ -175,3 +179,25 @@ const getBytes = (filePath) => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
175
179
|
}
|
|
176
180
|
});
|
|
177
181
|
exports.getBytes = getBytes;
|
|
182
|
+
function getAttachmentKey(attachment) {
|
|
183
|
+
var _a, _b;
|
|
184
|
+
const size = attachment.body ? attachment.body.byteLength : undefined;
|
|
185
|
+
const pathOrSize = (_b = (_a = attachment.path) !== null && _a !== void 0 ? _a : size) !== null && _b !== void 0 ? _b : 'no-path-no-size';
|
|
186
|
+
return `${attachment.name}|${attachment.contentType}|${pathOrSize}`;
|
|
187
|
+
}
|
|
188
|
+
function determineTestType(parentTitlePath) {
|
|
189
|
+
const lower = parentTitlePath.toLowerCase();
|
|
190
|
+
if (lower.includes('beforeall') || lower.includes('before all')) {
|
|
191
|
+
return TestType.BEFORE;
|
|
192
|
+
}
|
|
193
|
+
if (lower.includes('afterall') || lower.includes('after all')) {
|
|
194
|
+
return TestType.AFTER;
|
|
195
|
+
}
|
|
196
|
+
if (lower.includes('setup')) {
|
|
197
|
+
return TestType.BEFORE;
|
|
198
|
+
}
|
|
199
|
+
if (lower.includes('teardown')) {
|
|
200
|
+
return TestType.AFTER;
|
|
201
|
+
}
|
|
202
|
+
return TestType.TEST;
|
|
203
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orangebeard-io/playwright-orangebeard-reporter",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "A Playwright reporter to report to Orangebeard.io",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -13,10 +13,7 @@
|
|
|
13
13
|
"format:md": "prettier --write README.md",
|
|
14
14
|
"format": "npm run format:js && npm run format:md",
|
|
15
15
|
"test": "jest --unhandled-rejections=none --config ./jest.config.js",
|
|
16
|
-
"test:coverage": "jest --unhandled-rejections=none --coverage"
|
|
17
|
-
"get-version": "echo $npm_package_version",
|
|
18
|
-
"update-version": "release-it --ci --no-git --no-npm.publish",
|
|
19
|
-
"create-changelog": "auto-changelog --template changelog-template.hbs --starting-version v$npm_package_version"
|
|
16
|
+
"test:coverage": "jest --unhandled-rejections=none --coverage"
|
|
20
17
|
},
|
|
21
18
|
"repository": {
|
|
22
19
|
"type": "git",
|
|
@@ -35,20 +32,18 @@
|
|
|
35
32
|
},
|
|
36
33
|
"homepage": "https://github.com/orangebeard-io/playwright-listener#readme",
|
|
37
34
|
"devDependencies": {
|
|
38
|
-
"@playwright/test": "^1.
|
|
35
|
+
"@playwright/test": "^1.57.0",
|
|
39
36
|
"@types/node": "^22.10.9",
|
|
40
37
|
"@typescript-eslint/parser": "^8.15.0",
|
|
41
|
-
"auto-changelog": "^2.5.0",
|
|
42
38
|
"eslint": "^9.15.0",
|
|
43
39
|
"eslint-config-prettier": "^9.1.0",
|
|
44
40
|
"eslint-plugin-prettier": "^5.2.1",
|
|
45
41
|
"prettier": "^3.3.3",
|
|
46
|
-
"release-it": "^17.10.0",
|
|
47
42
|
"rimraf": "^6.0.1",
|
|
48
43
|
"typescript": "^5.6.3"
|
|
49
44
|
},
|
|
50
45
|
"dependencies": {
|
|
51
46
|
"@js-joda/core": "^5.6.3",
|
|
52
|
-
"@orangebeard-io/javascript-client": "^2.0.
|
|
47
|
+
"@orangebeard-io/javascript-client": "^2.0.8"
|
|
53
48
|
}
|
|
54
49
|
}
|