testbeats 2.2.7 → 2.2.9

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.
@@ -0,0 +1,316 @@
1
+ const request = require('phin-retry');
2
+ const { getPercentage, truncate, getPrettyDuration } = require('../helpers/helper');
3
+ const extension_manager = require('../extensions');
4
+ const { HOOK, STATUS } = require('../helpers/constants');
5
+ const logger = require('../utils/logger');
6
+
7
+ const PerformanceTestResult = require('performance-results-parser/src/models/PerformanceTestResult');
8
+ const { getValidMetrics, getMetricValuesText } = require('../helpers/performance');
9
+ const { BaseTarget } = require('./base.target');
10
+
11
+ const STATUSES = {
12
+ GOOD: '✅',
13
+ WARNING: '⚠️',
14
+ DANGER: '❌'
15
+ }
16
+
17
+ const DEFAULT_INPUTS = {
18
+ token: undefined,
19
+ comment_title: undefined,
20
+ update_comment: false,
21
+ owner: undefined,
22
+ repo: undefined,
23
+ pull_number: undefined,
24
+ title: undefined,
25
+ title_suffix: undefined,
26
+ title_link: undefined,
27
+ include_suites: true,
28
+ include_failure_details: false,
29
+ only_failures: false,
30
+ max_suites: 10,
31
+ duration: 'long',
32
+ publish: 'test-summary'
33
+ };
34
+
35
+ const default_options = {
36
+ condition: STATUS.PASS_OR_FAIL
37
+ };
38
+
39
+ class GitHubTarget extends BaseTarget {
40
+ constructor({ target }) {
41
+ super({ target });
42
+ }
43
+
44
+ async run({ result, target }) {
45
+ this.result = result;
46
+ this.setTargetInputs(target);
47
+ const payload = this.getMainPayload();
48
+ if (result instanceof PerformanceTestResult) {
49
+ await this.setPerformancePayload({ result, target, payload });
50
+ } else {
51
+ await this.setFunctionalPayload({ result, target, payload });
52
+ }
53
+ const message = this.getMarkdownMessage({ result, target, payload });
54
+ logger.info(`🔔 Publishing results to GitHub PR...`);
55
+ return await this.publishToGitHub({ target, message });
56
+ }
57
+
58
+ async setFunctionalPayload({ result, target, payload }) {
59
+ await extension_manager.run({ result, target, payload, hook: HOOK.START });
60
+ this.setMainContent({ result, target, payload });
61
+ await extension_manager.run({ result, target, payload, hook: HOOK.AFTER_SUMMARY });
62
+ this.setSuiteContent({ result, target, payload });
63
+ await extension_manager.run({ result, target, payload, hook: HOOK.END });
64
+ }
65
+
66
+ setTargetInputs(target) {
67
+ target.inputs = Object.assign({}, DEFAULT_INPUTS, target.inputs);
68
+ if (target.inputs.publish === 'test-summary-slim') {
69
+ target.inputs.include_suites = false;
70
+ }
71
+ if (target.inputs.publish === 'failure-details') {
72
+ target.inputs.include_failure_details = true;
73
+ }
74
+ }
75
+
76
+ getMainPayload() {
77
+ return {
78
+ content: []
79
+ };
80
+ }
81
+
82
+ setMainContent({ result, target, payload }) {
83
+ const titleText = this.getTitleText(result, target);
84
+ const resultText = this.getResultText(result);
85
+ const durationText = getPrettyDuration(result.duration, target.inputs.duration);
86
+
87
+ let content = `## ${titleText}\n\n`;
88
+ content += `**Results**: ${resultText}\n`;
89
+ content += `**Duration**: ${durationText}\n\n`;
90
+
91
+ payload.content.push(content);
92
+ }
93
+
94
+ getTitleText(result, target) {
95
+ let text = target.inputs.title ? target.inputs.title : result.name;
96
+ if (target.inputs.title_suffix) {
97
+ text = `${text} ${target.inputs.title_suffix}`;
98
+ }
99
+ if (target.inputs.title_link) {
100
+ text = `[${text}](${target.inputs.title_link})`;
101
+ }
102
+
103
+ const status = result.status !== 'PASS' ? STATUSES.DANGER : STATUSES.GOOD;
104
+ return `${status} ${text}`;
105
+ }
106
+
107
+ getResultText(result) {
108
+ const percentage = getPercentage(result.passed, result.total);
109
+ return `${result.passed} / ${result.total} Passed (${percentage}%)`;
110
+ }
111
+
112
+ setSuiteContent({ result, target, payload }) {
113
+ let suite_count = 0;
114
+ if (target.inputs.include_suites) {
115
+ for (let i = 0; i < result.suites.length && suite_count < target.inputs.max_suites; i++) {
116
+ const suite = result.suites[i];
117
+ if (target.inputs.only_failures && suite.status !== 'FAIL') {
118
+ continue;
119
+ }
120
+
121
+ // if suites length eq to 1 then main content will include suite summary
122
+ if (result.suites.length > 1) {
123
+ payload.content.push(this.getSuiteSummary({ target, suite }));
124
+ suite_count += 1;
125
+ }
126
+
127
+ if (target.inputs.include_failure_details) {
128
+ // Only attach failure details if there were failures
129
+ if (suite.failed > 0) {
130
+ payload.content.push(this.getFailureDetails(suite));
131
+ }
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ getSuiteSummary({ target, suite }) {
138
+ const text = this.getSuiteSummaryText(target, suite);
139
+ return `### ${suite.name}\n${text}\n\n`;
140
+ }
141
+
142
+ getFailureDetails(suite) {
143
+ let content = `<details>\n<summary>❌ Failed Tests</summary>\n\n`;
144
+ const cases = suite.cases;
145
+ for (let i = 0; i < cases.length; i++) {
146
+ const test_case = cases[i];
147
+ if (test_case.status === 'FAIL') {
148
+ content += `**Test**: ${test_case.name}\n`;
149
+ content += `**Error**: \n\`\`\`\n${truncate(test_case.failure ?? 'N/A', 500)}\n\`\`\`\n\n`;
150
+ }
151
+ }
152
+ content += `</details>\n\n`;
153
+ return content;
154
+ }
155
+
156
+ getMarkdownMessage({ result, target, payload }) {
157
+ return payload.content.join('');
158
+ }
159
+
160
+ async publishToGitHub({ target, message }) {
161
+ const { url, repo, owner, pull_number } = this.extractGitHubInfo(target);
162
+ const token = target.inputs.token || process.env.GITHUB_TOKEN;
163
+
164
+ if (!token) {
165
+ throw new Error('GitHub token is required. Set GITHUB_TOKEN environment variable or provide token in target inputs.');
166
+ }
167
+
168
+ if (!pull_number) {
169
+ throw new Error('Pull request number not found. This target only works in GitHub Actions triggered by pull requests.');
170
+ }
171
+
172
+ const comment_body = target.inputs.comment_title ?
173
+ `${target.inputs.comment_title}\n\n${message}` :
174
+ message;
175
+
176
+ const headers = {
177
+ 'Authorization': `token ${token}`,
178
+ 'Accept': 'application/vnd.github.v3+json',
179
+ 'User-Agent': 'testbeats'
180
+ };
181
+
182
+ if (target.inputs.update_comment) {
183
+ // Try to find existing comment and update it
184
+ const existingComment = await findExistingComment({ owner, repo, pull_number, token, comment_title: target.inputs.comment_title });
185
+ if (existingComment) {
186
+ return request.patch({
187
+ url: `${url}/repos/${owner}/${repo}/issues/comments/${existingComment.id}`,
188
+ headers,
189
+ body: { body: comment_body }
190
+ });
191
+ }
192
+ }
193
+
194
+ // Create new comment
195
+ return request.post({
196
+ url: `${url}/repos/${owner}/${repo}/issues/${pull_number}/comments`,
197
+ headers,
198
+ body: { body: comment_body }
199
+ });
200
+ }
201
+
202
+ async findExistingComment({ owner, repo, pull_number, token, comment_title }) {
203
+ if (!comment_title) return null;
204
+
205
+ try {
206
+ const url = `https://api.github.com/repos/${owner}/${repo}/issues/${pull_number}/comments`;
207
+ const headers = {
208
+ 'Authorization': `token ${token}`,
209
+ 'Accept': 'application/vnd.github.v3+json',
210
+ 'User-Agent': 'testbeats'
211
+ };
212
+
213
+ const response = await request.get({ url, headers });
214
+ const comments = JSON.parse(response.body);
215
+
216
+ return comments.find(comment => comment.body.includes(comment_title));
217
+ } catch (error) {
218
+ logger.warn('Failed to find existing comment:', error.message);
219
+ return null;
220
+ }
221
+ }
222
+
223
+ extractGitHubInfo(target) {
224
+ let url = target.inputs.url;
225
+ let owner = target.inputs.owner;
226
+ let repo = target.inputs.repo;
227
+ let pull_number = target.inputs.pull_number;
228
+
229
+ if (!owner || !repo) {
230
+ const repository = process.env.GITHUB_REPOSITORY;
231
+ const ref = process.env.GITHUB_REF;
232
+ [owner, repo] = repository.split('/');
233
+ if (ref && ref.includes('refs/pull/')) {
234
+ pull_number = ref.replace('refs/pull/', '').replace('/merge', '');
235
+ }
236
+ }
237
+
238
+ if (!url) {
239
+ url = `https://api.github.com`;
240
+ }
241
+
242
+ return {
243
+ url,
244
+ owner,
245
+ repo,
246
+ pull_number
247
+ };
248
+ }
249
+
250
+ async setPerformancePayload({ result, target, payload }) {
251
+ await extension_manager.run({ result, target, payload, hook: HOOK.START });
252
+ await this.setPerformanceMainContent({ result, target, payload });
253
+ await extension_manager.run({ result, target, payload, hook: HOOK.AFTER_SUMMARY });
254
+ await this.setTransactionContent({ result, target, payload });
255
+ await extension_manager.run({ result, target, payload, hook: HOOK.END });
256
+ }
257
+
258
+ async setPerformanceMainContent({ result, target, payload }) {
259
+ const titleText = this.getTitleText(result, target);
260
+ let content = `## ${titleText}\n\n`;
261
+
262
+ const metrics = getValidMetrics(result.metrics);
263
+ if (metrics.length > 0) {
264
+ content += `**Performance Metrics**:\n`;
265
+ content += getMetricValuesText(metrics);
266
+ content += '\n\n';
267
+ }
268
+
269
+ content += `**Duration**: ${getPrettyDuration(result.duration, target.inputs.duration)}\n\n`;
270
+ payload.content.push(content);
271
+ }
272
+
273
+ async setTransactionContent({ result, target, payload }) {
274
+ let transaction_count = 0;
275
+ if (target.inputs.include_suites) {
276
+ for (let i = 0; i < result.transactions.length && transaction_count < target.inputs.max_suites; i++) {
277
+ const transaction = result.transactions[i];
278
+ if (target.inputs.only_failures && transaction.status !== 'FAIL') {
279
+ continue;
280
+ }
281
+
282
+ payload.content.push(getTransactionSummary({ target, transaction }));
283
+ transaction_count += 1;
284
+ }
285
+ }
286
+ }
287
+
288
+ getTransactionSummary({ target, transaction }) {
289
+ let content = `### ${transaction.name}\n`;
290
+ content += `**Status**: ${transaction.status === 'PASS' ? STATUSES.GOOD : STATUSES.DANGER}\n`;
291
+ content += `**Duration**: ${getPrettyDuration(transaction.duration, target.inputs.duration)}\n`;
292
+
293
+ if (transaction.metrics && transaction.metrics.length > 0) {
294
+ const metrics = getValidMetrics(transaction.metrics);
295
+ if (metrics.length > 0) {
296
+ content += `**Metrics**: ${getMetricValuesText(metrics)}\n`;
297
+ }
298
+ }
299
+
300
+ content += '\n';
301
+ return content;
302
+ }
303
+
304
+ async handleErrors({ target, errors }) {
305
+ logger.error('GitHub target errors:', errors);
306
+ }
307
+ }
308
+
309
+ module.exports = {
310
+ // name: 'GitHub',
311
+ // run,
312
+ // handleErrors,
313
+ // default_inputs,
314
+ // default_options,
315
+ GitHubTarget
316
+ };
@@ -1,7 +1,8 @@
1
1
  const teams = require('./teams');
2
2
  const slack = require('./slack');
3
3
  const chat = require('./chat');
4
- const github = require('./github');
4
+ const { GitHubTarget} = require('./github.target');
5
+ const { GitHubOutputTarget } = require('./github-output.target');
5
6
  const { CustomTarget } = require('./custom.target');
6
7
  const { DelayTarget } = require('./delay.target');
7
8
  const { HttpTarget } = require('./http.target');
@@ -18,7 +19,9 @@ function getTargetRunner(target) {
18
19
  case TARGET.CHAT:
19
20
  return chat;
20
21
  case TARGET.GITHUB:
21
- return github;
22
+ return new GitHubTarget({ target });
23
+ case TARGET.GITHUB_OUTPUT:
24
+ return new GitHubOutputTarget({ target });
22
25
  case TARGET.CUSTOM:
23
26
  return new CustomTarget({ target });
24
27
  case TARGET.DELAY:
@@ -1,13 +1,15 @@
1
1
  const request = require('phin-retry');
2
2
  const { getPercentage, truncate, getPrettyDuration } = require('../helpers/helper');
3
3
  const extension_manager = require('../extensions');
4
- const { HOOK, STATUS, TARGET } = require('../helpers/constants');
4
+ const { HOOK, STATUS } = require('../helpers/constants');
5
5
  const logger = require('../utils/logger');
6
+ const ctx = require('../utils/context.utils');
6
7
 
7
8
  const PerformanceTestResult = require('performance-results-parser/src/models/PerformanceTestResult');
8
9
  const { getValidMetrics, getMetricValuesText } = require('../helpers/performance');
9
10
  const TestResult = require('test-results-parser/src/models/TestResult');
10
- const { getPlatform } = require('../platforms');
11
+ const { BaseTarget } = require('./base.target');
12
+
11
13
 
12
14
  const SLACK_BASE_URL = 'https://slack.com';
13
15
 
@@ -33,7 +35,7 @@ async function run({ result, target }) {
33
35
  }
34
36
  const message = getRootPayload({ result, target, payload });
35
37
  logger.info(`🔔 Publishing results to Slack...`);
36
- return publish({ inputs: target.inputs, message });
38
+ return publish({ target, message });
37
39
  }
38
40
 
39
41
  async function setFunctionalPayload({ result, target, payload }) {
@@ -73,7 +75,7 @@ function setMainBlock({ result, target, payload }) {
73
75
  });
74
76
  }
75
77
 
76
- function getTitleText(result, target, {allowTitleLink = true} = {}) {
78
+ function getTitleText(result, target, { allowTitleLink = true } = {}) {
77
79
  let text = target.inputs.title ? target.inputs.title : result.name;
78
80
  if (target.inputs.title_suffix) {
79
81
  text = `${text} ${target.inputs.title_suffix}`;
@@ -121,8 +123,8 @@ function setSuiteBlock({ result, target, payload }) {
121
123
  }
122
124
 
123
125
  function getSuiteSummary({ target, suite }) {
124
- const platform = getPlatform(TARGET.SLACK);
125
- const text = platform.getSuiteSummaryText(target, suite);
126
+ const tg = new SlackTarget({ target });
127
+ const text = tg.getSuiteSummaryText(target, suite);
126
128
  return {
127
129
  "type": "section",
128
130
  "text": {
@@ -172,7 +174,7 @@ function getRootPayload({ result, target, payload }) {
172
174
  }
173
175
  }
174
176
 
175
- const fallback_text = `${getTitleText(result, target, {allowTitleLink: false})}\nResults: ${getResultText(result)}`;
177
+ const fallback_text = `${getTitleText(result, target, { allowTitleLink: false })}\nResults: ${getResultText(result)}`;
176
178
 
177
179
  if (target.inputs.message_format === 'blocks') {
178
180
  return {
@@ -324,18 +326,29 @@ async function handleErrors({ target, errors }) {
324
326
  });
325
327
  }
326
328
 
327
- async function publish({ inputs, message}) {
328
- const { url, token, channels } = inputs;
329
+ async function publish({ target, message }) {
330
+ const { url, token, channels } = target.inputs;
329
331
  if (token) {
330
- for (let i = 0; i < channels.length; i++) {
331
- message.channel = channels[i];
332
- return request.post({
332
+ for (const channel of channels) {
333
+ message.channel = channel;
334
+ const response = await request.post({
333
335
  url: url ? url : `${SLACK_BASE_URL}/api/chat.postMessage`,
334
336
  headers: {
337
+ 'Content-Type': 'application/json',
335
338
  'Authorization': `Bearer ${token}`
336
339
  },
337
340
  body: message
338
341
  });
342
+ ctx.stores.push({
343
+ target,
344
+ response,
345
+ });
346
+ if (response && response.ok) {
347
+ logger.info(`✔ Published to Slack channel - ${channel}`);
348
+ } else {
349
+ logger.error(`✖ Failed to publish to Slack channel - ${channel}`);
350
+ logger.error(response);
351
+ }
339
352
  }
340
353
 
341
354
  } else {
@@ -346,6 +359,13 @@ async function publish({ inputs, message}) {
346
359
  }
347
360
  }
348
361
 
362
+
363
+ class SlackTarget extends BaseTarget {
364
+ constructor({ target }) {
365
+ super({ target });
366
+ }
367
+ }
368
+
349
369
  module.exports = {
350
370
  run,
351
371
  handleErrors,
@@ -2,12 +2,12 @@ const request = require('phin-retry');
2
2
  const { getPercentage, truncate, getPrettyDuration } = require('../helpers/helper');
3
3
  const { getValidMetrics, getMetricValuesText } = require('../helpers/performance');
4
4
  const extension_manager = require('../extensions');
5
- const { HOOK, STATUS, TARGET } = require('../helpers/constants');
5
+ const { HOOK, STATUS } = require('../helpers/constants');
6
6
  const logger = require('../utils/logger');
7
7
 
8
8
  const TestResult = require('test-results-parser/src/models/TestResult');
9
9
  const PerformanceTestResult = require('performance-results-parser/src/models/PerformanceTestResult');
10
- const { getPlatform } = require('../platforms');
10
+ const { BaseTarget } = require('./base.target');
11
11
 
12
12
  /**
13
13
  * @param {object} param0
@@ -145,11 +145,11 @@ function setSuiteBlock({ result, target, payload }) {
145
145
  }
146
146
 
147
147
  function getSuiteSummary({ suite, target }) {
148
-
149
- const platform = getPlatform(TARGET.TEAMS);
150
- const suite_title = platform.getSuiteTitle(suite);
151
- const suite_results = platform.getSuiteResults(suite);
152
- const duration = platform.getSuiteDuration(target, suite);
148
+ const tg = new TeamsTarget({ target });
149
+ // const platform = getPlatform(TARGET.TEAMS);
150
+ const suite_title = tg.getSuiteTitle(suite);
151
+ const suite_results = tg.getSuiteResults(suite);
152
+ const duration = tg.getSuiteDuration(target, suite);
153
153
 
154
154
  const blocks = [
155
155
  {
@@ -174,7 +174,7 @@ function getSuiteSummary({ suite, target }) {
174
174
  }
175
175
  ];
176
176
 
177
- const suite_metadata_text = platform.getSuiteMetaDataText(suite);
177
+ const suite_metadata_text = tg.getSuiteMetaDataText(suite);
178
178
  if (suite_metadata_text) {
179
179
  blocks.push({
180
180
  "type": "TextBlock",
@@ -348,6 +348,12 @@ async function handleErrors({ target, errors }) {
348
348
  });
349
349
  }
350
350
 
351
+ class TeamsTarget extends BaseTarget {
352
+ constructor({ target }) {
353
+ super({ target });
354
+ }
355
+ }
356
+
351
357
  module.exports = {
352
358
  run,
353
359
  handleErrors,
@@ -0,0 +1,5 @@
1
+ const stores = [];
2
+
3
+ module.exports = {
4
+ stores,
5
+ }