@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 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
- "accessToken": "00000000-0000-0000-0000-00000000",
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: TestType.TEST,
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
- if (result.attachments.length > 0) {
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 result.attachments) {
192
- message += `- ${attachment.name} (${attachment.contentType})\n`;
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 result.attachments) {
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
- let content;
257
- if (attachment.body) {
258
- content = attachment.body;
259
- }
260
- else if (attachment.path) {
261
- content = yield (0, utils_1.getBytes)(attachment.path);
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
- else {
264
- throw new Error("Attachment must have either body or path defined.");
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
  }
@@ -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.5",
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.49.0",
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.7"
47
+ "@orangebeard-io/javascript-client": "^2.0.8"
53
48
  }
54
49
  }