testbeats 2.2.6 → 2.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testbeats",
3
- "version": "2.2.6",
3
+ "version": "2.2.8",
4
4
  "description": "Publish test results to Microsoft Teams, Google Chat, Slack and InfluxDB",
5
5
  "main": "src/index.js",
6
6
  "types": "./src/index.d.ts",
@@ -10,7 +10,7 @@ const logger = require('../utils/logger');
10
10
  const TestAttachment = require('test-results-parser/src/models/TestAttachment');
11
11
 
12
12
  const MAX_ATTACHMENTS_PER_REQUEST = 5;
13
- const MAX_ATTACHMENTS_PER_RUN = 20;
13
+ const MAX_ATTACHMENTS_PER_RUN = 50;
14
14
  const MAX_ATTACHMENT_SIZE = 2 * 1024 * 1024;
15
15
 
16
16
  class BeatsAttachments {
@@ -186,14 +186,14 @@ class Beats {
186
186
  if (process.env.TEST_BEATS_DELAY) {
187
187
  return parseInt(process.env.TEST_BEATS_DELAY);
188
188
  }
189
- return 3000;
189
+ return 5000;
190
190
  }
191
191
 
192
192
  async #setTestRun(text, wait_for = 'smart_analysis_status') {
193
193
  if (this.test_run && this.test_run[wait_for] === PROCESS_STATUS.COMPLETED) {
194
194
  return;
195
195
  }
196
- let retry = 3;
196
+ let retry = 5;
197
197
  while (retry >= 0) {
198
198
  retry = retry - 1;
199
199
  await new Promise(resolve => setTimeout(resolve, this.#getDelay()));
package/src/index.d.ts CHANGED
@@ -10,7 +10,7 @@ export interface ITarget {
10
10
  name: TargetName;
11
11
  enable?: string | boolean;
12
12
  condition?: Condition;
13
- inputs?: SlackInputs | TeamsInputs | ChatInputs | GitHubInputs | ICustomTargetInputs | InfluxDBTargetInputs;
13
+ inputs?: SlackInputs | TeamsInputs | ChatInputs | IGitHubInputs | ICustomTargetInputs | InfluxDBTargetInputs;
14
14
  extensions?: IExtension[];
15
15
  }
16
16
 
@@ -244,7 +244,7 @@ export interface TeamsInputs extends TargetInputs {
244
244
 
245
245
  export interface ChatInputs extends TargetInputs { }
246
246
 
247
- export interface GitHubInputs extends TargetInputs {
247
+ export interface IGitHubInputs extends TargetInputs {
248
248
  token?: string;
249
249
  owner?: string;
250
250
  repo?: string;
@@ -6,7 +6,10 @@ class BasePlatform {
6
6
  * @param {string|number} text
7
7
  */
8
8
  bold(text) {
9
- return `**${text}**`;
9
+ if (text) {
10
+ return `**${text}**`;
11
+ }
12
+ return text;
10
13
  }
11
14
 
12
15
  break() {
@@ -39,117 +42,19 @@ class BasePlatform {
39
42
  return text;
40
43
  }
41
44
 
42
-
43
-
44
- /**
45
- *
46
- * @param {import('..').ITarget} target
47
- * @param {import('test-results-parser').ITestSuite} suite
48
- */
49
- getSuiteSummaryText(target, suite) {
50
- const suite_title = this.getSuiteTitle(suite);
51
- const suite_results_text = this.#getSuiteResultsText(suite);
52
- const duration_text = this.#getSuiteDurationText(target, suite);
53
-
54
- const texts = [
55
- this.bold(suite_title),
56
- this.break(),
57
- this.break(),
58
- suite_results_text,
59
- this.break(),
60
- duration_text,
61
- ];
62
-
63
- const metadata_text = this.getSuiteMetaDataText(suite);
64
-
65
- if (metadata_text) {
66
- texts.push(this.break());
67
- texts.push(this.break());
68
- texts.push(metadata_text);
69
- }
70
-
71
- return texts.join('');
72
- }
73
-
74
- /**
75
- *
76
- * @param {import('test-results-parser').ITestSuite} suite
77
- * @returns {string}
78
- */
79
- getSuiteTitle(suite) {
80
- const emoji = suite.status === 'PASS' ? '✅' : suite.total === suite.skipped ? '⏭️' : '❌';
81
- return `${emoji} ${suite.name}`;
82
- }
83
-
84
45
  /**
85
- *
86
- * @param {import('test-results-parser').ITestSuite} suite
87
- * @returns {string}
88
- */
89
- #getSuiteResultsText(suite) {
90
- const suite_results = this.getSuiteResults(suite);
91
- return `${this.bold('Results')}: ${suite_results}`;
92
- }
93
-
94
- /**
95
- *
96
- * @param {import('test-results-parser').ITestSuite} suite
97
- * @returns {string}
98
- */
99
- getSuiteResults(suite) {
100
- return `${suite.passed} / ${suite.total} Passed (${getPercentage(suite.passed, suite.total)}%)`;
101
- }
102
-
103
- /**
104
- *
105
- * @param {import('..').ITarget} target
106
- * @param {import('test-results-parser').ITestSuite} suite
107
- */
108
- #getSuiteDurationText(target, suite) {
109
- const duration = this.getSuiteDuration(target, suite);
110
- return `${this.bold('Duration')}: ${duration}`
111
- }
112
-
113
- /**
114
- *
115
- * @param {import('..').ITarget} target
116
- * @param {import('test-results-parser').ITestSuite} suite
117
- */
118
- getSuiteDuration(target, suite) {
119
- return getPrettyDuration(suite.duration, target.inputs.duration);
120
- }
121
-
122
- /**
123
- *
124
- * @param {import('test-results-parser').ITestSuite} suite
46
+ * @param {string} text
47
+ * @param {string} url
125
48
  * @returns {string}
126
49
  */
127
- getSuiteMetaDataText(suite) {
128
- if (!suite || !suite.metadata) {
129
- return;
130
- }
131
-
132
- const texts = [];
133
-
134
- // webdriver io
135
- if (suite.metadata.device && typeof suite.metadata.device === 'string') {
136
- texts.push(`${suite.metadata.device}`);
137
- }
138
-
139
- if (suite.metadata.platform && suite.metadata.platform.name && suite.metadata.platform.version) {
140
- texts.push(`${suite.metadata.platform.name} ${suite.metadata.platform.version}`);
50
+ link(text, url) {
51
+ if (url) {
52
+ if (!text) {
53
+ text = url;
54
+ }
55
+ return `[${text}](${url})`;
141
56
  }
142
-
143
- if (suite.metadata.browser && suite.metadata.browser.name && suite.metadata.browser.version) {
144
- texts.push(`${suite.metadata.browser.name} ${suite.metadata.browser.version}`);
145
- }
146
-
147
- // playwright
148
- if (suite.metadata.hostname && typeof suite.metadata.hostname === 'string') {
149
- texts.push(`${suite.metadata.hostname}`);
150
- }
151
-
152
- return texts.join(' • ');
57
+ return url;
153
58
  }
154
59
 
155
60
  /**
@@ -6,7 +6,10 @@ class ChatPlatform extends BasePlatform {
6
6
  * @param {string|number} text
7
7
  */
8
8
  bold(text) {
9
- return `<b>${text}</b>`;
9
+ if (text) {
10
+ return `<b>${text}</b>`;
11
+ }
12
+ return text;
10
13
  }
11
14
 
12
15
  break() {
@@ -6,7 +6,10 @@ class SlackPlatform extends BasePlatform {
6
6
  * @param {string|number} text
7
7
  */
8
8
  bold(text) {
9
- return `*${text}*`;
9
+ if (text) {
10
+ return `*${text}*`;
11
+ }
12
+ return text;
10
13
  }
11
14
 
12
15
  /**
@@ -21,8 +24,21 @@ class SlackPlatform extends BasePlatform {
21
24
  }
22
25
 
23
26
  code(text) {
24
- return `\`\`\`${text}\`\`\``;
27
+ if (text) {
28
+ return `\`\`\`${text}\`\`\``;
29
+ }
30
+ return text;
31
+ }
32
+
33
+ link(text, url) {
34
+ if (url) {
35
+ if (!text) {
36
+ text = url;
37
+ }
38
+ return `<${url}|${text}>`;
39
+ }
40
+ return text;
25
41
  }
26
42
  }
27
43
 
28
- module.exports = { SlackPlatform }
44
+ module.exports = { SlackPlatform }
@@ -1,5 +1,6 @@
1
1
  const { getPlatform } = require('../platforms');
2
2
  const { STATUS } = require('../helpers/constants');
3
+ const { getPercentage, getPrettyDuration } = require('../helpers/helper');
3
4
 
4
5
  class BaseTarget {
5
6
 
@@ -34,12 +35,124 @@ class BaseTarget {
34
35
  * @type {import('../platforms/base.platform').BasePlatform}
35
36
  */
36
37
  this.platform = getPlatform(this.name);
38
+
37
39
  }
38
40
 
39
41
  async run({ result }) {
40
42
  // throw new Error('Not implemented');
41
43
  }
42
44
 
45
+ /**
46
+ *
47
+ * @param {import('..').ITarget} target
48
+ * @param {import('test-results-parser').ITestSuite} suite
49
+ */
50
+ getSuiteSummaryText(target, suite) {
51
+ const suite_title = this.getSuiteTitle(suite);
52
+ const suite_results_text = this.#getSuiteResultsText(suite);
53
+ const duration_text = this.#getSuiteDurationText(target, suite);
54
+
55
+ const texts = [
56
+ this.platform.bold(suite_title),
57
+ this.platform.break(),
58
+ this.platform.break(),
59
+ suite_results_text,
60
+ this.platform.break(),
61
+ duration_text,
62
+ ];
63
+
64
+ const metadata_text = this.getSuiteMetaDataText(suite);
65
+
66
+ if (metadata_text) {
67
+ texts.push(this.platform.break());
68
+ texts.push(this.platform.break());
69
+ texts.push(metadata_text);
70
+ }
71
+
72
+ return texts.join('');
73
+ }
74
+
75
+ /**
76
+ *
77
+ * @param {import('test-results-parser').ITestSuite} suite
78
+ * @returns {string}
79
+ */
80
+ getSuiteTitle(suite) {
81
+ const emoji = suite.status === 'PASS' ? '✅' : suite.total === suite.skipped ? '⏭️' : '❌';
82
+ return `${emoji} ${suite.name}`;
83
+ }
84
+
85
+ /**
86
+ *
87
+ * @param {import('test-results-parser').ITestSuite} suite
88
+ * @returns {string}
89
+ */
90
+ #getSuiteResultsText(suite) {
91
+ const suite_results = this.getSuiteResults(suite);
92
+ return `${this.platform.bold('Results')}: ${suite_results}`;
93
+ }
94
+
95
+ /**
96
+ *
97
+ * @param {import('test-results-parser').ITestSuite} suite
98
+ * @returns {string}
99
+ */
100
+ getSuiteResults(suite) {
101
+ return `${suite.passed} / ${suite.total} Passed (${getPercentage(suite.passed, suite.total)}%)`;
102
+ }
103
+
104
+ /**
105
+ *
106
+ * @param {import('..').ITarget} target
107
+ * @param {import('test-results-parser').ITestSuite} suite
108
+ */
109
+ #getSuiteDurationText(target, suite) {
110
+ const duration = this.getSuiteDuration(target, suite);
111
+ return `${this.platform.bold('Duration')}: ${duration}`
112
+ }
113
+
114
+ /**
115
+ *
116
+ * @param {import('..').ITarget} target
117
+ * @param {import('test-results-parser').ITestSuite} suite
118
+ */
119
+ getSuiteDuration(target, suite) {
120
+ return getPrettyDuration(suite.duration, target.inputs.duration);
121
+ }
122
+
123
+ /**
124
+ *
125
+ * @param {import('test-results-parser').ITestSuite} suite
126
+ * @returns {string}
127
+ */
128
+ getSuiteMetaDataText(suite) {
129
+ if (!suite || !suite.metadata) {
130
+ return;
131
+ }
132
+
133
+ const texts = [];
134
+
135
+ // webdriver io
136
+ if (suite.metadata.device && typeof suite.metadata.device === 'string') {
137
+ texts.push(`${suite.metadata.device}`);
138
+ }
139
+
140
+ if (suite.metadata.platform && suite.metadata.platform.name && suite.metadata.platform.version) {
141
+ texts.push(`${suite.metadata.platform.name} ${suite.metadata.platform.version}`);
142
+ }
143
+
144
+ if (suite.metadata.browser && suite.metadata.browser.name && suite.metadata.browser.version) {
145
+ texts.push(`${suite.metadata.browser.name} ${suite.metadata.browser.version}`);
146
+ }
147
+
148
+ // playwright
149
+ if (suite.metadata.hostname && typeof suite.metadata.hostname === 'string') {
150
+ texts.push(`${suite.metadata.hostname}`);
151
+ }
152
+
153
+ return texts.join(' • ');
154
+ }
155
+
43
156
  }
44
157
 
45
- module.exports = { BaseTarget};
158
+ module.exports = { BaseTarget };
@@ -1,11 +1,11 @@
1
1
  const request = require('phin-retry');
2
2
  const { getTitleText, getResultText, 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 PerformanceTestResult = require('performance-results-parser/src/models/PerformanceTestResult');
6
6
  const { getValidMetrics, getMetricValuesText } = require('../helpers/performance');
7
7
  const logger = require('../utils/logger');
8
- const { getPlatform } = require('../platforms');
8
+ const { BaseTarget } = require('./base.target');
9
9
 
10
10
  async function run({ result, target }) {
11
11
  setTargetInputs(target);
@@ -101,8 +101,9 @@ function setSuiteBlock({ result, target, payload }) {
101
101
  }
102
102
 
103
103
  function getSuiteSummary({ target, suite }) {
104
- const platform = getPlatform(TARGET.CHAT);
105
- const text = platform.getSuiteSummaryText(target, suite);
104
+ const tg = new ChatTarget({ target });
105
+ // const platform = getPlatform(TARGET.CHAT);
106
+ const text = tg.getSuiteSummaryText(target, suite);
106
107
  return text;
107
108
  }
108
109
 
@@ -260,6 +261,12 @@ async function handleErrors({ target, errors }) {
260
261
  });
261
262
  }
262
263
 
264
+ class ChatTarget extends BaseTarget {
265
+ constructor({ target }) {
266
+ super({ target });
267
+ }
268
+ }
269
+
263
270
  module.exports = {
264
271
  run,
265
272
  handleErrors,
@@ -1,5 +1,6 @@
1
1
  const { BaseTarget } = require('./base.target');
2
2
  const path = require('path');
3
+ const ctx = require('../utils/context.utils');
3
4
 
4
5
  const DEFAULT_INPUTS = {};
5
6
 
@@ -18,9 +19,9 @@ class CustomTarget extends BaseTarget {
18
19
  if (typeof this.inputs.load === 'string') {
19
20
  const cwd = process.cwd();
20
21
  const target_runner = require(path.join(cwd, this.inputs.load));
21
- await target_runner.run({ target: this.target, result });
22
+ await target_runner.run({ target: this.target, result, ctx });
22
23
  } else if (typeof this.inputs.load === 'function') {
23
- await this.inputs.load({ target: this.target, result });
24
+ await this.inputs.load({ target: this.target, result, ctx });
24
25
  } else {
25
26
  throw `Invalid 'load' input in custom target - ${this.inputs.load}`;
26
27
  }
@@ -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,7 @@
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
5
  const { CustomTarget } = require('./custom.target');
6
6
  const { DelayTarget } = require('./delay.target');
7
7
  const { HttpTarget } = require('./http.target');
@@ -18,7 +18,7 @@ function getTargetRunner(target) {
18
18
  case TARGET.CHAT:
19
19
  return chat;
20
20
  case TARGET.GITHUB:
21
- return github;
21
+ return new GitHubTarget({ target });
22
22
  case TARGET.CUSTOM:
23
23
  return new CustomTarget({ target });
24
24
  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
+ }
@@ -1,310 +0,0 @@
1
- const request = require('phin-retry');
2
- const { getPercentage, truncate, getPrettyDuration } = require('../helpers/helper');
3
- const extension_manager = require('../extensions');
4
- const { HOOK, STATUS, TARGET } = 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 TestResult = require('test-results-parser/src/models/TestResult');
10
- const { getPlatform } = require('../platforms');
11
-
12
- const STATUSES = {
13
- GOOD: '✅',
14
- WARNING: '⚠️',
15
- DANGER: '❌'
16
- }
17
-
18
- async function run({ result, target }) {
19
- setTargetInputs(target);
20
- const payload = getMainPayload();
21
- if (result instanceof PerformanceTestResult) {
22
- await setPerformancePayload({ result, target, payload });
23
- } else {
24
- await setFunctionalPayload({ result, target, payload });
25
- }
26
- const message = getMarkdownMessage({ result, target, payload });
27
- logger.info(`🔔 Publishing results to GitHub PR...`);
28
- return await publishToGitHub({ target, message });
29
- }
30
-
31
- async function setFunctionalPayload({ result, target, payload }) {
32
- await extension_manager.run({ result, target, payload, hook: HOOK.START });
33
- setMainContent({ result, target, payload });
34
- await extension_manager.run({ result, target, payload, hook: HOOK.AFTER_SUMMARY });
35
- setSuiteContent({ result, target, payload });
36
- await extension_manager.run({ result, target, payload, hook: HOOK.END });
37
- }
38
-
39
- function setTargetInputs(target) {
40
- target.inputs = Object.assign({}, default_inputs, target.inputs);
41
- if (target.inputs.publish === 'test-summary-slim') {
42
- target.inputs.include_suites = false;
43
- }
44
- if (target.inputs.publish === 'failure-details') {
45
- target.inputs.include_failure_details = true;
46
- }
47
- }
48
-
49
- function getMainPayload() {
50
- return {
51
- content: []
52
- };
53
- }
54
-
55
- function setMainContent({ result, target, payload }) {
56
- const titleText = getTitleText(result, target);
57
- const resultText = getResultText(result);
58
- const durationText = getPrettyDuration(result.duration, target.inputs.duration);
59
-
60
- let content = `## ${titleText}\n\n`;
61
- content += `**Results**: ${resultText}\n`;
62
- content += `**Duration**: ${durationText}\n\n`;
63
-
64
- payload.content.push(content);
65
- }
66
-
67
- function getTitleText(result, target) {
68
- let text = target.inputs.title ? target.inputs.title : result.name;
69
- if (target.inputs.title_suffix) {
70
- text = `${text} ${target.inputs.title_suffix}`;
71
- }
72
- if (target.inputs.title_link) {
73
- text = `[${text}](${target.inputs.title_link})`;
74
- }
75
-
76
- const status = result.status !== 'PASS' ? STATUSES.DANGER : STATUSES.GOOD;
77
- return `${status} ${text}`;
78
- }
79
-
80
- function getResultText(result) {
81
- const percentage = getPercentage(result.passed, result.total);
82
- return `${result.passed} / ${result.total} Passed (${percentage}%)`;
83
- }
84
-
85
- function setSuiteContent({ result, target, payload }) {
86
- let suite_count = 0;
87
- if (target.inputs.include_suites) {
88
- for (let i = 0; i < result.suites.length && suite_count < target.inputs.max_suites; i++) {
89
- const suite = result.suites[i];
90
- if (target.inputs.only_failures && suite.status !== 'FAIL') {
91
- continue;
92
- }
93
-
94
- // if suites length eq to 1 then main content will include suite summary
95
- if (result.suites.length > 1) {
96
- payload.content.push(getSuiteSummary({ target, suite }));
97
- suite_count += 1;
98
- }
99
-
100
- if (target.inputs.include_failure_details) {
101
- // Only attach failure details if there were failures
102
- if (suite.failed > 0) {
103
- payload.content.push(getFailureDetails(suite));
104
- }
105
- }
106
- }
107
- }
108
- }
109
-
110
- function getSuiteSummary({ target, suite }) {
111
- const platform = getPlatform(TARGET.GITHUB);
112
- const text = platform.getSuiteSummaryText(target, suite);
113
- return `### ${suite.name}\n${text}\n\n`;
114
- }
115
-
116
- function getFailureDetails(suite) {
117
- let content = `<details>\n<summary>❌ Failed Tests</summary>\n\n`;
118
- const cases = suite.cases;
119
- for (let i = 0; i < cases.length; i++) {
120
- const test_case = cases[i];
121
- if (test_case.status === 'FAIL') {
122
- content += `**Test**: ${test_case.name}\n`;
123
- content += `**Error**: \n\`\`\`\n${truncate(test_case.failure ?? 'N/A', 500)}\n\`\`\`\n\n`;
124
- }
125
- }
126
- content += `</details>\n\n`;
127
- return content;
128
- }
129
-
130
- function getMarkdownMessage({ result, target, payload }) {
131
- return payload.content.join('');
132
- }
133
-
134
- async function publishToGitHub({ target, message }) {
135
- const { url, repo, owner, pull_number } = extractGitHubInfo(target);
136
- const token = target.inputs.token || process.env.GITHUB_TOKEN;
137
-
138
- if (!token) {
139
- throw new Error('GitHub token is required. Set GITHUB_TOKEN environment variable or provide token in target inputs.');
140
- }
141
-
142
- if (!pull_number) {
143
- throw new Error('Pull request number not found. This target only works in GitHub Actions triggered by pull requests.');
144
- }
145
-
146
- const comment_body = target.inputs.comment_title ?
147
- `${target.inputs.comment_title}\n\n${message}` :
148
- message;
149
-
150
- const headers = {
151
- 'Authorization': `token ${token}`,
152
- 'Accept': 'application/vnd.github.v3+json',
153
- 'User-Agent': 'testbeats'
154
- };
155
-
156
- if (target.inputs.update_comment) {
157
- // Try to find existing comment and update it
158
- const existingComment = await findExistingComment({ owner, repo, pull_number, token, comment_title: target.inputs.comment_title });
159
- if (existingComment) {
160
- return request.patch({
161
- url: `${url}/repos/${owner}/${repo}/issues/comments/${existingComment.id}`,
162
- headers,
163
- body: { body: comment_body }
164
- });
165
- }
166
- }
167
-
168
- // Create new comment
169
- return request.post({
170
- url: `${url}/repos/${owner}/${repo}/issues/${pull_number}/comments`,
171
- headers,
172
- body: { body: comment_body }
173
- });
174
- }
175
-
176
- async function findExistingComment({ owner, repo, pull_number, token, comment_title }) {
177
- if (!comment_title) return null;
178
-
179
- try {
180
- const url = `https://api.github.com/repos/${owner}/${repo}/issues/${pull_number}/comments`;
181
- const headers = {
182
- 'Authorization': `token ${token}`,
183
- 'Accept': 'application/vnd.github.v3+json',
184
- 'User-Agent': 'testbeats'
185
- };
186
-
187
- const response = await request.get({ url, headers });
188
- const comments = JSON.parse(response.body);
189
-
190
- return comments.find(comment => comment.body.includes(comment_title));
191
- } catch (error) {
192
- logger.warn('Failed to find existing comment:', error.message);
193
- return null;
194
- }
195
- }
196
-
197
- function extractGitHubInfo(target) {
198
- let url = target.inputs.url;
199
- let owner = target.inputs.owner;
200
- let repo = target.inputs.repo;
201
- let pull_number = target.inputs.pull_number;
202
-
203
- if (!owner || !repo) {
204
- const repository = process.env.GITHUB_REPOSITORY;
205
- const ref = process.env.GITHUB_REF;
206
- [owner, repo] = repository.split('/');
207
- if (ref && ref.includes('refs/pull/')) {
208
- pull_number = ref.replace('refs/pull/', '').replace('/merge', '');
209
- }
210
- }
211
-
212
- if (!url) {
213
- url = `https://api.github.com`;
214
- }
215
-
216
- return {
217
- url,
218
- owner,
219
- repo,
220
- pull_number
221
- };
222
- }
223
-
224
- async function setPerformancePayload({ result, target, payload }) {
225
- await extension_manager.run({ result, target, payload, hook: HOOK.START });
226
- await setPerformanceMainContent({ result, target, payload });
227
- await extension_manager.run({ result, target, payload, hook: HOOK.AFTER_SUMMARY });
228
- await setTransactionContent({ result, target, payload });
229
- await extension_manager.run({ result, target, payload, hook: HOOK.END });
230
- }
231
-
232
- async function setPerformanceMainContent({ result, target, payload }) {
233
- const titleText = getTitleText(result, target);
234
- let content = `## ${titleText}\n\n`;
235
-
236
- const metrics = getValidMetrics(result.metrics);
237
- if (metrics.length > 0) {
238
- content += `**Performance Metrics**:\n`;
239
- content += getMetricValuesText(metrics);
240
- content += '\n\n';
241
- }
242
-
243
- content += `**Duration**: ${getPrettyDuration(result.duration, target.inputs.duration)}\n\n`;
244
- payload.content.push(content);
245
- }
246
-
247
- async function setTransactionContent({ result, target, payload }) {
248
- let transaction_count = 0;
249
- if (target.inputs.include_suites) {
250
- for (let i = 0; i < result.transactions.length && transaction_count < target.inputs.max_suites; i++) {
251
- const transaction = result.transactions[i];
252
- if (target.inputs.only_failures && transaction.status !== 'FAIL') {
253
- continue;
254
- }
255
-
256
- payload.content.push(getTransactionSummary({ target, transaction }));
257
- transaction_count += 1;
258
- }
259
- }
260
- }
261
-
262
- function getTransactionSummary({ target, transaction }) {
263
- let content = `### ${transaction.name}\n`;
264
- content += `**Status**: ${transaction.status === 'PASS' ? STATUSES.GOOD : STATUSES.DANGER}\n`;
265
- content += `**Duration**: ${getPrettyDuration(transaction.duration, target.inputs.duration)}\n`;
266
-
267
- if (transaction.metrics && transaction.metrics.length > 0) {
268
- const metrics = getValidMetrics(transaction.metrics);
269
- if (metrics.length > 0) {
270
- content += `**Metrics**: ${getMetricValuesText(metrics)}\n`;
271
- }
272
- }
273
-
274
- content += '\n';
275
- return content;
276
- }
277
-
278
- async function handleErrors({ target, errors }) {
279
- logger.error('GitHub target errors:', errors);
280
- }
281
-
282
- const default_inputs = {
283
- token: undefined,
284
- comment_title: undefined,
285
- update_comment: false,
286
- owner: undefined,
287
- repo: undefined,
288
- pull_number: undefined,
289
- title: undefined,
290
- title_suffix: undefined,
291
- title_link: undefined,
292
- include_suites: true,
293
- include_failure_details: false,
294
- only_failures: false,
295
- max_suites: 10,
296
- duration: 'long',
297
- publish: 'test-summary'
298
- };
299
-
300
- const default_options = {
301
- condition: STATUS.PASS_OR_FAIL
302
- };
303
-
304
- module.exports = {
305
- name: 'GitHub',
306
- run,
307
- handleErrors,
308
- default_inputs,
309
- default_options
310
- };