playwright-slack-report-burak 1.7.0 → 2.0.1

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.
@@ -6,38 +6,118 @@
6
6
  /* eslint-disable class-methods-use-this */
7
7
  /* eslint-disable no-param-reassign */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const axios = require('axios');
12
+ const { exec } = require('child_process');
13
+
9
14
  class ResultsParser {
10
15
  result;
11
16
  separateFlakyTests;
17
+ totalShardCount = process.env.CIRCLE_NODE_TOTAL || 1;
18
+ shardIndex = process.env.CIRCLE_NODE_INDEX || 0;
12
19
  constructor(options = { separateFlakyTests: false }) {
13
20
  this.result = [];
14
21
  this.separateFlakyTests = options.separateFlakyTests;
15
22
  }
16
23
  async getParsedResults() {
24
+ const summary = {
25
+ total: 0,
26
+ passed: 0,
27
+ failed: 0,
28
+ flaky: 0,
29
+ skipped: 0,
30
+ failures: [],
31
+ tests: [],
32
+ };
17
33
  const failures = await this.getFailures();
18
34
  const flakes = await this.getFlakes();
19
35
  let passes = await this.getPasses();
20
36
  /*if (this.separateFlakyTests) {
21
37
  passes = this.doSeparateFlakyTests(passes, flakes);
22
38
  }*/
23
- const summary = {
24
- passed: passes.length,
25
- failed: failures.length,
26
- flaky: (this.separateFlakyTests && flakes.length > 0) ?
27
- flakes.length : 0,
39
+
40
+ // Define the directory to store node summaries
41
+ const summariesDir = path.join(process.env.CIRCLE_WORKING_DIRECTORY || './', 'playwright-report');
42
+
43
+ if (!fs.existsSync(summariesDir)) {
44
+ fs.mkdirSync(summariesDir, { recursive: true });
45
+ }
46
+
47
+ // Determine the current node index
48
+ const currentNodeIndex = this.shardIndex;
49
+
50
+ // Define the file for the current node's summary
51
+ const nodeSummaryFile = path.join(summariesDir, `node_summary_${currentNodeIndex}.json`);
52
+
53
+ const totalTestCasesForNode = this.result.reduce((acc, suite) => acc + suite.testSuite.tests.length, 0);
54
+
55
+ // Initialize or read existing summary from file
56
+ let nodeSummary = {
57
+ total: totalTestCasesForNode,
58
+ passed: 0,
59
+ failed: 0,
60
+ flaky: 0,
28
61
  skipped: 0,
29
- failures,
62
+ failures: [],
30
63
  tests: [],
31
64
  };
65
+
66
+ // Create the file if it doesn't exist
67
+ if (!fs.existsSync(nodeSummaryFile)) {
68
+ fs.writeFileSync(nodeSummaryFile, JSON.stringify(nodeSummary, null, 2));
69
+ } else {
70
+ nodeSummary = JSON.parse(fs.readFileSync(nodeSummaryFile, 'utf-8'));
71
+ }
72
+
73
+ // Update the node summary with new test results
32
74
  for (const suite of this.result) {
33
- summary.tests = summary.tests.concat(suite.testSuite.tests);
75
+ nodeSummary.tests = nodeSummary.tests.concat(suite.testSuite.tests);
34
76
  for (const test of suite.testSuite.tests) {
35
77
  if (test.status === 'skipped') {
36
- summary.skipped += 1;
78
+ nodeSummary.skipped += 1;
79
+ }
80
+ if (test.status === 'failed' || test.status === 'timedOut') {
81
+ nodeSummary.failed += 1;
82
+ }
83
+ if (this.separateFlakyTests && test.status === 'passed' && test.retry > 0) {
84
+ nodeSummary.flaky += 1;
85
+ }
86
+ if (test.status === 'passed' && (!this.separateFlakyTests || test.retry === 0)) {
87
+ nodeSummary.passed += 1;
88
+ }
89
+ }
90
+ }
91
+
92
+ // Write the updated summary back to the file for the current node
93
+ fs.writeFileSync(nodeSummaryFile, JSON.stringify(nodeSummary, null, 2));
94
+
95
+ if (this.shardIndex === 0 && this.totalShardCount > 1) {
96
+ await this.fetchAllArtifacts();
97
+ }
98
+
99
+ if (this.allNodeSummaryFilesExist() && this.allBlobZipsExist()) {
100
+ // Merge all node summaries into the final summary
101
+ for (let i = 0; i < this.totalShardCount; i++) {
102
+ const nodeSummaryFile = path.join(summariesDir, `node_summary_${i}.json`);
103
+ const fileToRead = nodeSummaryFile;
104
+
105
+ if (fs.existsSync(fileToRead)) {
106
+ const nodeSummary = JSON.parse(fs.readFileSync(fileToRead, 'utf-8'));
107
+ summary.total += nodeSummary.total;
108
+ summary.passed += nodeSummary.passed;
109
+ summary.failed += nodeSummary.failed;
110
+ summary.flaky += nodeSummary.flaky;
111
+ summary.skipped += nodeSummary.skipped;
112
+ summary.failures = summary.failures.concat(nodeSummary.failures);
113
+ summary.tests = summary.tests.concat(nodeSummary.tests);
37
114
  }
38
115
  }
116
+ this.mergeReports();
117
+ return summary;
118
+ } else {
119
+ return { summary, sendResults: 'off' };
39
120
  }
40
- return summary;
41
121
  }
42
122
  async getFailures() {
43
123
  const failures = [];
@@ -179,5 +259,81 @@ class ResultsParser {
179
259
  }
180
260
  return [..._passes.values()];
181
261
  }*/
262
+ allNodeSummaryFilesExist() {
263
+ const summariesDir = path.join(process.env.CIRCLE_WORKING_DIRECTORY || './', 'playwright-report');
264
+
265
+ for (let i = 0; i < this.totalShardCount; i++) {
266
+ const nodeSummaryFile = path.join(summariesDir, `node_summary_${i}.json`);
267
+ if (!fs.existsSync(nodeSummaryFile)) {
268
+ return false;
269
+ }
270
+ }
271
+ return true;
272
+ }
273
+
274
+ allBlobZipsExist() {
275
+ const summariesDir = path.join(process.env.CIRCLE_WORKING_DIRECTORY || './', 'playwright-report');
276
+
277
+ for (let i = 0; i < this.totalShardCount; i++) {
278
+ const blobZipFile = path.join(summariesDir, `blob-report-node-${i}.zip`);
279
+ if (!fs.existsSync(blobZipFile)) {
280
+ return false;
281
+ }
282
+ }
283
+ return true;
284
+ }
285
+
286
+ async fetchAllArtifacts() {
287
+ const summariesDir = path.join(process.env.CIRCLE_WORKING_DIRECTORY || './', 'playwright-report');
288
+ while (!this.allNodeSummaryFilesExist() || !this.allBlobZipsExist()) {
289
+ console.log('Waiting for all blob zips to exist...');
290
+ if (!fs.existsSync(summariesDir)) {
291
+ fs.mkdirSync(summariesDir, { recursive: true });
292
+ }
293
+ for (let i = 1; i < this.totalShardCount; i++) {
294
+ await this.fetchArtifact(i, `node_summary_${i}.json`, summariesDir);
295
+ await this.fetchArtifact(i, `blob-report-node-${i}.zip`, summariesDir);
296
+ }
297
+ }
298
+ }
299
+
300
+ async fetchArtifact(i, file, summariesDir) {
301
+ const circleciToken = process.env.safetywingtest_CIRCLECI_API_TOKEN;
302
+ const circleciJobId = process.env.CIRCLE_WORKFLOW_JOB_ID || 'cb4e5f50-16cc-4dd1-a494-8c3094d91ceb';
303
+ const circleciApiUrl = `https://output.circle-artifacts.com/output/job/${circleciJobId}/artifacts/${i}/html-report/${file}`;
304
+ const filePath = path.join(summariesDir, file);
305
+
306
+ while (true) {
307
+ try {
308
+ const response = await axios.get(circleciApiUrl, {
309
+ headers: {
310
+ 'Circle-Token': circleciToken,
311
+ }
312
+ });
313
+
314
+ fs.writeFileSync(filePath, response.data);
315
+ console.log(`Successfully fetched file ${file} from shard ${i}`);
316
+ break; // Exit the loop if the file is fetched successfully
317
+ } catch (error) {
318
+ if (error.response && error.response.status === 404) {
319
+ console.warn(`File ${file} not found at ${circleciApiUrl}. Retrying in 10 seconds...`);
320
+ await new Promise(resolve => setTimeout(resolve, 10000)); // Wait for 10 seconds
321
+ } else {
322
+ console.error(`Failed to fetch file ${file} from shard ${i}!`, error);
323
+ break; // Exit the loop on other errors
324
+ }
325
+ }
326
+ }
327
+ }
328
+ mergeReports() {
329
+ try {
330
+ // Execute the command to merge reports
331
+ exec('npx playwright merge-reports --reporter html playwright-report', { stdio: 'inherit' });
332
+ console.log('Reports merged successfully.');
333
+ } catch (error) {
334
+ // Log a warning instead of throwing an error
335
+ console.warn('Warning: Failed to merge reports. This may not affect the overall process.', error.message);
336
+ }
337
+ }
182
338
  }
183
- exports.default = ResultsParser;
339
+ exports.default = ResultsParser;
package/package.json CHANGED
@@ -30,10 +30,7 @@
30
30
  "lint-fix": "npx eslint . --ext .ts --fix"
31
31
  },
32
32
  "name": "playwright-slack-report-burak",
33
- "version": "1.7.0",
34
- "bin": {
35
- "playwright-slack-report": "dist/cli.js"
36
- },
33
+ "version": "2.0.1",
37
34
  "main": "index.js",
38
35
  "types": "dist/index.d.ts",
39
36
  "repository": "git@github.com:ryanrosello-og/playwright-slack-report.git",
package/dist/src/cli.js DELETED
@@ -1,175 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
- if (k2 === undefined) k2 = k;
5
- var desc = Object.getOwnPropertyDescriptor(m, k);
6
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
- desc = { enumerable: true, get: function() { return m[k]; } };
8
- }
9
- Object.defineProperty(o, k2, desc);
10
- }) : (function(o, m, k, k2) {
11
- if (k2 === undefined) k2 = k;
12
- o[k2] = m[k];
13
- }));
14
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
- Object.defineProperty(o, "default", { enumerable: true, value: v });
16
- }) : function(o, v) {
17
- o["default"] = v;
18
- });
19
- var __importStar = (this && this.__importStar) || function (mod) {
20
- if (mod && mod.__esModule) return mod;
21
- var result = {};
22
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
23
- __setModuleDefault(result, mod);
24
- return result;
25
- };
26
- var __importDefault = (this && this.__importDefault) || function (mod) {
27
- return (mod && mod.__esModule) ? mod : { "default": mod };
28
- };
29
- Object.defineProperty(exports, "__esModule", { value: true });
30
- /* eslint-disable no-console */
31
- const commander_1 = require("commander");
32
- const web_api_1 = require("@slack/web-api");
33
- const https_proxy_agent_1 = require("https-proxy-agent");
34
- const webhook_1 = require("@slack/webhook");
35
- const path_1 = __importDefault(require("path"));
36
- const ResultsParser_1 = __importDefault(require("./src/ResultsParser"));
37
- const SlackClient_1 = __importDefault(require("./src/SlackClient"));
38
- const cli_pre_checks_1 = __importDefault(require("./src/cli/cli_pre_checks"));
39
- const SlackWebhookClient_1 = __importDefault(require("./src/SlackWebhookClient"));
40
- const program = new commander_1.Command();
41
- program
42
- .name('playwright-slack-report - cli')
43
- .version('1.0.0')
44
- .description('📦 Send Playwright json results to directly Slack ')
45
- .option('-c, --config <path>', 'Configuration file for the CLI app e.g ./config.json')
46
- .option('-j, --json-results <path>', 'Generated Playwright json results file e.g. ./results.json')
47
- .action(async (options) => {
48
- const preCheckResult = await (0, cli_pre_checks_1.default)(options.jsonResults, options.config);
49
- const config = preCheckResult.config;
50
- if (preCheckResult.status === 'error') {
51
- console.error(`❌ ${preCheckResult.message}`);
52
- process.exit(1);
53
- }
54
- const agent = config.proxy ? new https_proxy_agent_1.HttpsProxyAgent(config.proxy) : undefined;
55
- const resultsParser = new ResultsParser_1.default();
56
- const resultSummary = await resultsParser.parseFromJsonFile(preCheckResult.jsonPath);
57
- if (config.sendUsingBot) {
58
- const slackClient = new SlackClient_1.default(new web_api_1.WebClient(process.env.SLACK_BOT_USER_OAUTH_TOKEN, {
59
- logLevel: config.slackLogLevel,
60
- agent,
61
- }));
62
- const success = await sendResultsUsingBot({
63
- resultSummary,
64
- slackClient,
65
- config,
66
- });
67
- if (!success) {
68
- console.error('❌ Failed to send results to Slack');
69
- process.exit(1);
70
- }
71
- else {
72
- console.log('✅ Results sent to Slack');
73
- process.exit(0);
74
- }
75
- }
76
- });
77
- program.parse();
78
-
79
- async function sendResultsUsingBot({ resultSummary, slackClient, config, }) {
80
- if (config.slackLogLevel === web_api_1.LogLevel.DEBUG) {
81
- console.log({ config });
82
- }
83
- if (resultSummary.failures.length === 0
84
- && config.sendResults === 'on-failure') {
85
- console.log('⏩ Slack CLI reporter - no failures found');
86
- return true;
87
- }
88
- let summaryResults = resultSummary;
89
- const meta = replaceEnvVars(config.meta);
90
- summaryResults = { ...resultSummary, meta };
91
- if (config.sendUsingBot) {
92
- const result = await slackClient.sendMessage({
93
- options: {
94
- channelIds: config.sendUsingBot.channels,
95
- customLayout: await attemptToImportLayout(config.customLayout?.source, config.customLayout?.functionName),
96
- customLayoutAsync: await attemptToImportLayout(config.customLayoutAsync?.source, config.customLayoutAsync?.functionName),
97
- maxNumberOfFailures: config.maxNumberOfFailures,
98
- disableUnfurl: config.disableUnfurl,
99
- summaryResults,
100
- showInThread: config.showInThread,
101
- },
102
- });
103
- if (config.showInThread) {
104
- for (let i = 0; i < result.length; i += 1) {
105
- await slackClient.attachDetailsToThreadRuns({
106
- channelIds: [result[i].channel],
107
- ts: result[i].ts,
108
- summaryResults: resultSummary,
109
- maxNumberOfFailures: config.maxNumberOfFailures,
110
- });
111
- }
112
- if (resultSummary.failures.length > 0 || resultSummary.skipped > 0 || resultSummary.flaky > 0) {
113
- for (let i = 0; i < result.length; i += 1) {
114
- await slackClient.attachDetailsToThreadSuites({
115
- channelIds: [result[i].channel],
116
- ts: result[i].ts,
117
- summaryResults: resultSummary,
118
- maxNumberOfFailures: config.maxNumberOfFailures,
119
- });
120
- }
121
- for (let i = 0; i < result.length; i += 1) {
122
- await slackClient.attachDetailsToThreadCases({
123
- channelIds: [result[i].channel],
124
- ts: result[i].ts,
125
- summaryResults: resultSummary,
126
- maxNumberOfFailures: config.maxNumberOfFailures,
127
- });
128
- }
129
- if (resultSummary.failures.length > 0) {
130
- for (let i = 0; i < result.length; i += 1) {
131
- await slackClient.attachDetailsToThreadReasons({
132
- channelIds: [result[i].channel],
133
- ts: result[i].ts,
134
- summaryResults: resultSummary,
135
- maxNumberOfFailures: config.maxNumberOfFailures,
136
- });
137
- }
138
- }
139
- }
140
- }
141
- return true;
142
- }
143
- throw new Error('sendUsingBot config is not set');
144
- }
145
-
146
- async function attemptToImportLayout(source, functionName) {
147
- if (source && functionName) {
148
- try {
149
- const importPath = path_1.default.resolve(source);
150
- const layout = await Promise.resolve(`${importPath}`).then(s => __importStar(require(s)));
151
- if (layout.default[functionName]) {
152
- return layout.default[functionName];
153
- }
154
- console.error(`Function [${functionName}] was not found in [${source}]`);
155
- }
156
- catch (error) {
157
- console.error(error);
158
- }
159
- }
160
- return undefined;
161
- }
162
-
163
- function replaceEnvVars(originalMeta) {
164
- const newMeta = [];
165
- for (const m of originalMeta) {
166
- let metaValue = m.value;
167
- if (m.value.startsWith('__ENV')) {
168
- const environmentVarName = m.value.replace('__ENV_', '');
169
- metaValue = process.env[environmentVarName] || `❌ Environment variable [${environmentVarName}] was not set.`;
170
- }
171
- newMeta.push({ key: m.key, value: metaValue });
172
- }
173
- return newMeta;
174
- }
175
- //# sourceMappingURL=cli.js.map