Skip to content

Commit 591e030

Browse files
authored
Merge pull request #86 from browserstack/custom-reporter
Custom reporter
2 parents 8e341d9 + f0cf99f commit 591e030

File tree

10 files changed

+758
-3
lines changed

10 files changed

+758
-3
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,5 @@ tests.zip
66
package-lock.json
77
.nyc_output/
88
.env.*
9+
log/*.log
10+
results/*

bin/commands/generateReport.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use strict';
2+
3+
const logger = require("../helpers/logger").winstonLogger,
4+
Constants = require("../helpers/constants"),
5+
utils = require("../helpers/utils"),
6+
reporterHTML = require('../helpers/reporterHTML');
7+
8+
9+
module.exports = function generateReport(args) {
10+
let bsConfigPath = utils.getConfigPath(args.cf);
11+
let reportGenerator = reporterHTML.reportGenerator;
12+
13+
return utils.validateBstackJson(bsConfigPath).then(function (bsConfig) {
14+
// setting setDefaults to {} if not present and set via env variables or via args.
15+
utils.setDefaults(bsConfig, args);
16+
17+
// accept the username from command line if provided
18+
utils.setUsername(bsConfig, args);
19+
20+
// accept the access key from command line if provided
21+
utils.setAccessKey(bsConfig, args);
22+
23+
utils.setUsageReportingFlag(bsConfig, args.disableUsageReporting);
24+
25+
// set cypress config filename
26+
utils.setCypressConfigFilename(bsConfig, args);
27+
28+
let messageType = Constants.messageTypes.INFO;
29+
let errorCode = null;
30+
let buildId = args._[1];
31+
32+
reportGenerator(bsConfig, buildId, args);
33+
utils.sendUsageReport(bsConfig, args, 'generate-report called', messageType, errorCode);
34+
}).catch(function (err) {
35+
logger.error(err);
36+
utils.setUsageReportingFlag(null, args.disableUsageReporting);
37+
utils.sendUsageReport(null, args, err.message, Constants.messageTypes.ERROR, utils.getErrorCodeFromErr(err));
38+
});
39+
};

bin/commands/runs.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ const archiver = require("../helpers/archiver"),
88
Constants = require("../helpers/constants"),
99
utils = require("../helpers/utils"),
1010
fileHelpers = require("../helpers/fileHelpers"),
11-
syncRunner = require("../helpers/syncRunner");
11+
syncRunner = require("../helpers/syncRunner"),
12+
reportGenerator = require('../helpers/reporterHTML').reportGenerator;
1213

1314
module.exports = function run(args) {
1415
let bsConfigPath = utils.getConfigPath(args.cf);
@@ -73,8 +74,11 @@ module.exports = function run(args) {
7374
}
7475
if (args.sync) {
7576
syncRunner.pollBuildStatus(bsConfig, data).then((exitCode) => {
76-
utils.sendUsageReport(bsConfig, args, `${message}\n${dashboardLink}`, Constants.messageTypes.SUCCESS, null);
77-
utils.handleSyncExit(exitCode, data.dashboard_url)
77+
// Generate custom report!
78+
reportGenerator(bsConfig, data.build_id, args, function(){
79+
utils.sendUsageReport(bsConfig, args, `${message}\n${dashboardLink}`, Constants.messageTypes.SUCCESS, null);
80+
utils.handleSyncExit(exitCode, data.dashboard_url);
81+
});
7882
});
7983
}
8084

bin/helpers/constants.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const syncCLI = {
1010

1111
const userMessages = {
1212
BUILD_FAILED: "Build creation failed.",
13+
BUILD_GENERATE_REPORT_FAILED: "Generating report for the build <build-id> failed.",
1314
BUILD_CREATED: "Build created",
1415
BUILD_INFO_FAILED: "Failed to get build info.",
1516
BUILD_STOP_FAILED: "Failed to stop build.",
@@ -98,6 +99,9 @@ const cliMessages = {
9899
ACCESS_KEY: "Your BrowserStack access key",
99100
NO_NPM_WARNING: "No NPM warning if npm_dependencies is empty",
100101
},
102+
GENERATE_REPORT: {
103+
INFO: "Generates the build report"
104+
},
101105
};
102106

103107
const messageTypes = {

bin/helpers/reporterHTML.js

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
const fs = require('fs'),
2+
path = require('path'),
3+
request = require('request'),
4+
logger = require('./logger').winstonLogger,
5+
utils = require("./utils"),
6+
Constants = require('./constants'),
7+
config = require("./config");
8+
9+
let templatesDir = path.join(__dirname, '../', 'templates');
10+
11+
function loadInlineCss() {
12+
return loadFile(path.join(templatesDir, 'assets', 'browserstack-cypress-report.css'));
13+
}
14+
15+
function loadFile(fileName) {
16+
return fs.readFileSync(fileName, 'utf8');
17+
}
18+
19+
function createBodyBuildHeader(report_data){
20+
let projectNameSpan = `<span class='project-name'> ${report_data.project_name} </span>`;
21+
let buildNameSpan = `<span class='build-name'> ${report_data.build_name} </span>`;
22+
let buildMeta = `<div class='build-meta'> ${buildNameSpan} ${projectNameSpan} </div>`;
23+
let buildLink = `<div class='build-link'> <a href='${report_data.build_url}' rel='noreferrer noopener' target='_blank'> View on BrowserStack </a> </div>`;
24+
let buildHeader = `<div class='build-header'> ${buildMeta} ${buildLink} </div>`;
25+
return buildHeader;
26+
}
27+
28+
function createBodyBuildTable(report_data) {
29+
let specs = Object.keys(report_data.rows),
30+
specRow = '',
31+
specSessions = '',
32+
sessionBlocks = '',
33+
specData,
34+
specNameSpan,
35+
specPathSpan,
36+
specStats,
37+
specStatsSpan,
38+
specMeta,
39+
sessionStatus,
40+
sessionClass,
41+
sessionStatusIcon,
42+
sessionLink;
43+
44+
specs.forEach((specName) => {
45+
specData = report_data.rows[specName];
46+
47+
specNameSpan = `<span class='spec-name'> ${specName} </span>`;
48+
specPathSpan = `<span class='spec-path'> ${specData.path} </span>`;
49+
50+
specStats = buildSpecStats(specData.meta);
51+
specStatsSpan = `<span class='spec-stats ${specStats.cssClass}'> ${specStats.label} </span>`;
52+
53+
specMeta = `<div class='spec-meta'> ${specNameSpan} ${specPathSpan} ${specStatsSpan} </div>`;
54+
sessionBlocks = '';
55+
specData.sessions.forEach((specSession) => {
56+
57+
sessionStatus = specSession.status;
58+
sessionClass = sessionStatus === 'passed' ? 'session-passed' : 'session-failed';
59+
sessionStatusIcon = sessionStatus === 'passed' ? "&#10004; " : "&#x2717; ";
60+
61+
sessionLink = `<a href="${specSession.link}" rel="noreferrer noopener" target="_blank"> ${sessionStatusIcon} ${specSession.name} </a>`;
62+
63+
sessionDetail = `<div class="session-detail ${sessionClass}"> ${sessionLink} </div>`;
64+
sessionBlocks = `${sessionBlocks} ${sessionDetail}`;
65+
});
66+
specSessions = `<div class='spec-sessions'> ${sessionBlocks} </div>`;
67+
specRow = `${specRow} <div class='spec-row'> ${specMeta} ${specSessions} </div>`;
68+
});
69+
70+
71+
return `<div class='build-table'> ${specRow} </div>`;
72+
}
73+
74+
function buildSpecStats(specMeta) {
75+
let failedSpecs = specMeta.failed,
76+
passedSpecs = specMeta.passed,
77+
totalSpecs = specMeta.total,
78+
specStats = {};
79+
80+
if (failedSpecs) {
81+
specStats.label = `${failedSpecs}/${totalSpecs} FAILED`;
82+
specStats.cssClass = 'spec-stats-failed';
83+
} else {
84+
specStats.label = `${passedSpecs}/${totalSpecs} PASSED`;
85+
specStats.cssClass = 'spec-stats-passed';
86+
}
87+
88+
return specStats;
89+
}
90+
91+
let reportGenerator = (bsConfig, buildId, args, cb) => {
92+
let options = {
93+
url: `${config.buildUrl}${buildId}/custom_report`,
94+
auth: {
95+
user: bsConfig.auth.username,
96+
password: bsConfig.auth.access_key,
97+
},
98+
headers: {
99+
'User-Agent': utils.getUserAgent(),
100+
},
101+
};
102+
103+
return request.get(options, function (err, resp, body) {
104+
let message = null;
105+
let messageType = null;
106+
let errorCode = null;
107+
let build;
108+
109+
if (err) {
110+
message = Constants.userMessages.BUILD_INFO_FAILED;
111+
messageType = Constants.messageTypes.ERROR;
112+
errorCode = 'api_failed_build_info';
113+
114+
logger.info(message);
115+
} else {
116+
try {
117+
build = JSON.parse(body);
118+
} catch (error) {
119+
build = null;
120+
}
121+
}
122+
123+
if (resp.statusCode == 299) {
124+
messageType = Constants.messageTypes.INFO;
125+
errorCode = 'api_deprecated';
126+
127+
if (build) {
128+
message = build.message;
129+
logger.info(message);
130+
} else {
131+
message = Constants.userMessages.API_DEPRECATED;
132+
logger.info(message);
133+
}
134+
} else if (resp.statusCode != 200) {
135+
messageType = Constants.messageTypes.ERROR;
136+
errorCode = 'api_failed_build_generate_report';
137+
138+
if (build) {
139+
message = `${
140+
Constants.userMessages.BUILD_GENERATE_REPORT_FAILED.replace('<build-id>', buildId)
141+
} with error: \n${JSON.stringify(build, null, 2)}`;
142+
logger.error(message);
143+
if (build.message === 'Unauthorized') errorCode = 'api_auth_failed';
144+
} else {
145+
message = Constants.userMessages.BUILD_GENERATE_REPORT_FAILED.replace('<build-id>', buildId);
146+
logger.error(message);
147+
}
148+
} else {
149+
messageType = Constants.messageTypes.SUCCESS;
150+
message = `Report for build: ${buildId} was successfully created.`;
151+
renderReportHTML(build);
152+
logger.info(message);
153+
}
154+
utils.sendUsageReport(bsConfig, args, message, messageType, errorCode);
155+
if (cb){
156+
cb();
157+
}
158+
});
159+
}
160+
161+
function renderReportHTML(report_data) {
162+
let resultsDir = 'results';
163+
let metaCharSet = `<meta charset="utf-8">`;
164+
let metaViewPort = `<meta name="viewport" content="width=device-width, initial-scale=1"> `;
165+
let pageTitle = `<title> Browserstack Cypress Report </title>`;
166+
let inlineCss = `<style type="text/css"> ${loadInlineCss()} </style>`;
167+
let head = `<head> ${metaCharSet} ${metaViewPort} ${pageTitle} ${inlineCss} </head>`;
168+
let htmlOpenTag = `<!DOCTYPE HTML><html>`;
169+
let htmlClosetag = `</html>`;
170+
let bodyBuildHeader = createBodyBuildHeader(report_data);
171+
let bodyBuildTable = createBodyBuildTable(report_data);
172+
let bodyReporterContainer = `<div class='report-container'> ${bodyBuildHeader} ${bodyBuildTable} </div>`;
173+
let body = `<body> ${bodyReporterContainer} </body>`;
174+
let html = `${htmlOpenTag} ${head} ${body} ${htmlClosetag}`;
175+
176+
177+
if (!fs.existsSync(resultsDir)){
178+
fs.mkdirSync(resultsDir);
179+
}
180+
181+
// Writing the JSON used in creating the HTML file.
182+
fs.writeFileSync(`${resultsDir}/browserstack-cypress-report.json`, JSON.stringify(report_data), () => {
183+
if(err) {
184+
return logger.error(err);
185+
}
186+
logger.info("The JSON file is saved");
187+
});
188+
189+
// Writing the HTML file generated from the JSON data.
190+
fs.writeFileSync(`${resultsDir}/browserstack-cypress-report.html`, html, () => {
191+
if(err) {
192+
return logger.error(err);
193+
}
194+
logger.info("The HTML file was saved!");
195+
});
196+
}
197+
198+
exports.reportGenerator = reportGenerator;

bin/runner.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,46 @@ var argv = yargs
209209
return require('./commands/runs')(argv);
210210
}
211211
})
212+
.command('generate-report', Constants.cliMessages.GENERATE_REPORT.INFO, function(yargs) {
213+
argv = yargs
214+
.usage('usage: $0 generate-report <buildId>')
215+
.demand(1, Constants.cliMessages.BUILD.DEMAND)
216+
.options({
217+
'cf': {
218+
alias: 'config-file',
219+
describe: Constants.cliMessages.BUILD.DESC,
220+
default: 'browserstack.json',
221+
type: 'string',
222+
nargs: 1,
223+
demand: true,
224+
demand: Constants.cliMessages.BUILD.CONFIG_DEMAND
225+
},
226+
'disable-usage-reporting': {
227+
default: undefined,
228+
description: Constants.cliMessages.COMMON.DISABLE_USAGE_REPORTING,
229+
type: "boolean"
230+
},
231+
'u': {
232+
alias: 'username',
233+
describe: Constants.cliMessages.COMMON.USERNAME,
234+
type: "string",
235+
default: undefined
236+
},
237+
'k': {
238+
alias: 'key',
239+
describe: Constants.cliMessages.COMMON.ACCESS_KEY,
240+
type: "string",
241+
default: undefined
242+
},
243+
})
244+
.help('help')
245+
.wrap(null)
246+
.argv
247+
if (checkCommands(yargs, argv, 1)) {
248+
logger.info(Constants.cliMessages.BUILD.INFO_MESSAGE + argv._[1]);
249+
return require('./commands/generateReport')(argv);
250+
}
251+
})
212252
.help('help')
213253
.wrap(null)
214254
.argv

0 commit comments

Comments
 (0)