Skip to content

Commit 8703bbc

Browse files
authored
Merge pull request #341 from roshan04/json_html_report_changes
Json html report changes
2 parents 68ed861 + eea30d5 commit 8703bbc

File tree

2 files changed

+229
-577
lines changed

2 files changed

+229
-577
lines changed

bin/helpers/reporterHTML.js

Lines changed: 55 additions & 262 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,12 @@
11
const fs = require('fs'),
22
path = require('path'),
33
request = require('request'),
4+
unzipper = require('unzipper'),
45
logger = require('./logger').winstonLogger,
56
utils = require("./utils"),
67
Constants = require('./constants'),
78
config = require("./config");
89

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-
9110
let reportGenerator = (bsConfig, buildId, args, rawArgs, buildReportData, cb) => {
9211
let options = {
9312
url: `${config.buildUrl}${buildId}/custom_report`,
@@ -100,6 +19,8 @@ let reportGenerator = (bsConfig, buildId, args, rawArgs, buildReportData, cb) =>
10019
},
10120
};
10221

22+
logger.debug('Started fetching the build json and html reports.');
23+
10324
return request.get(options, async function (err, resp, body) {
10425
let message = null;
10526
let messageType = null;
@@ -117,6 +38,7 @@ let reportGenerator = (bsConfig, buildId, args, rawArgs, buildReportData, cb) =>
11738
utils.sendUsageReport(bsConfig, args, message, messageType, errorCode, buildReportData, rawArgs);
11839
return;
11940
} else {
41+
logger.debug('Received reports data from upstream.');
12042
try {
12143
build = JSON.parse(body);
12244
} catch (error) {
@@ -127,7 +49,6 @@ let reportGenerator = (bsConfig, buildId, args, rawArgs, buildReportData, cb) =>
12749
if (resp.statusCode == 299) {
12850
messageType = Constants.messageTypes.INFO;
12951
errorCode = 'api_deprecated';
130-
13152
if (build) {
13253
message = build.message;
13354
logger.info(message);
@@ -163,209 +84,81 @@ let reportGenerator = (bsConfig, buildId, args, rawArgs, buildReportData, cb) =>
16384
} else {
16485
messageType = Constants.messageTypes.SUCCESS;
16586
message = `Report for build: ${buildId} was successfully created.`;
166-
await renderReportHTML(build);
87+
await generateCypressBuildReport(build);
16788
logger.info(message);
16889
}
90+
logger.debug('Finished fetching the build json and html reports.');
16991
utils.sendUsageReport(bsConfig, args, message, messageType, errorCode, buildReportData, rawArgs);
17092
if (cb){
17193
cb();
17294
}
17395
});
17496
}
17597

176-
async function renderReportHTML(report_data) {
177-
let resultsDir = 'results';
178-
let metaCharSet = `<meta charset="utf-8">`;
179-
let metaViewPort = `<meta name="viewport" content="width=device-width, initial-scale=1"> `;
180-
let pageTitle = `<title> BrowserStack Cypress Report </title>`;
181-
let inlineCss = `<style type="text/css"> ${loadInlineCss()} </style>`;
182-
let head = `<head> ${metaCharSet} ${metaViewPort} ${pageTitle} ${inlineCss} </head>`;
183-
let htmlOpenTag = `<!DOCTYPE HTML><html>`;
184-
let htmlClosetag = `</html>`;
185-
let bodyBuildHeader = createBodyBuildHeader(report_data);
186-
let bodyBuildTable = createBodyBuildTable(report_data);
187-
let bodyReporterContainer = `<div class='report-container'> ${bodyBuildHeader} ${bodyBuildTable} </div>`;
188-
let body = `<body> ${bodyReporterContainer} </body>`;
189-
let html = `${htmlOpenTag} ${head} ${body} ${htmlClosetag}`;
190-
98+
async function generateCypressBuildReport(report_data) {
99+
let resultsDir = path.join('./', 'results');
191100

192101
if (!fs.existsSync(resultsDir)){
102+
logger.debug("Results directory doesn't exists.");
103+
logger.debug("Creating results directory.");
193104
fs.mkdirSync(resultsDir);
194105
}
195-
196-
// Writing the JSON used in creating the HTML file.
197-
let reportData = await cypressReportData(report_data);
198-
fs.writeFileSync(
199-
`${resultsDir}/browserstack-cypress-report.json`,
200-
JSON.stringify(reportData),
201-
() => {
202-
if (err) {
203-
return logger.error(err);
204-
}
205-
logger.info("The JSON file is saved");
206-
}
207-
);
208-
209-
// Writing the HTML file generated from the JSON data.
210-
fs.writeFileSync(`${resultsDir}/browserstack-cypress-report.html`, html, () => {
211-
if(err) {
212-
return logger.error(err);
213-
}
214-
logger.info("The HTML file was saved!");
215-
});
216-
}
217-
218-
async function cypressReportData(report_data) {
219-
specFiles = Object.keys(report_data.rows);
220-
combinationPromises = [];
221-
for (let spec of specFiles) {
222-
let specSessions = report_data.rows[spec]["sessions"];
223-
if (specSessions.length > 0) {
224-
for (let combination of specSessions) {
225-
if(utils.isUndefined(report_data.cypress_version) || report_data.cypress_version < "6"){
226-
combinationPromises.push(generateCypressCombinationSpecReportDataWithoutConfigJson(combination));
227-
}else{
228-
combinationPromises.push(generateCypressCombinationSpecReportDataWithConfigJson(combination));
229-
}
230-
}
231-
}
232-
}
233-
await Promise.all(combinationPromises);
234-
return report_data;
106+
await getReportResponse(resultsDir, 'report.zip', report_data.cypress_custom_report_url);
235107
}
236108

237-
function getConfigJsonResponse(combination) {
109+
function getReportResponse(filePath, fileName, reportJsonUrl) {
110+
let tmpFilePath = path.join(filePath, fileName);
111+
const writer = fs.createWriteStream(tmpFilePath);
112+
logger.debug(`Fetching build reports zip.`)
238113
return new Promise(async (resolve, reject) => {
239-
configJsonResponse = null;
240-
configJsonError = false
241-
request.get(combination.tests.config_json , function(err, resp, body) {
242-
if(err) {
243-
configJsonError = true;
244-
reject([configJsonResponse, configJsonError]);
245-
} else {
246-
if(resp.statusCode != 200) {
247-
configJsonError = true;
248-
reject([configJsonResponse, configJsonError]);
249-
} else {
250-
try {
251-
configJsonResponse = JSON.parse(body);
252-
} catch (err) {
253-
configJsonError = true
254-
reject([configJsonResponse, configJsonError]);
255-
}
256-
}
257-
}
258-
resolve([configJsonResponse, configJsonError]);
259-
});
260-
});
261-
}
114+
request.get(reportJsonUrl).on('response', function(response) {
262115

263-
function getResultsJsonResponse(combination) {
264-
return new Promise(async (resolve, reject) => {
265-
resultsJsonResponse = null
266-
resultsJsonError = false;
267-
request.get(combination.tests.result_json , function(err, resp, body) {
268-
if(err) {
269-
resultsJsonError = true;
270-
reject([resultsJsonResponse, resultsJsonError]);
116+
if(response.statusCode != 200) {
117+
let message = `Received non 200 response while fetching reports, code: ${response.statusCode}`;
118+
reject(message);
271119
} else {
272-
if(resp.statusCode != 200) {
273-
resultsJsonError = true;
274-
reject([resultsJsonResponse, resultsJsonError]);
275-
} else {
276-
try {
277-
resultsJsonResponse = JSON.parse(body);
278-
} catch (err) {
279-
resultsJsonError = true
280-
reject([resultsJsonResponse, resultsJsonError]);
281-
}
282-
}
283-
}
284-
resolve([resultsJsonResponse, resultsJsonError]);
285-
});
286-
});
287-
}
288-
289-
function generateCypressCombinationSpecReportDataWithConfigJson(combination){
290-
return new Promise(async (resolve, reject) => {
291-
try {
292-
let configJsonError, resultsJsonError;
293-
let configJson, resultsJson;
294-
295-
await Promise.all([getConfigJsonResponse(combination), getResultsJsonResponse(combination)]).then(function (successResult) {
296-
[[configJson, configJsonError], [resultsJson, resultsJsonError]] = successResult;
297-
}).catch(function (failureResult) {
298-
[[configJson, configJsonError], [resultsJson, resultsJsonError]] = failureResult;
299-
});
300-
301-
if(resultsJsonError || configJsonError){
302-
resolve();
303-
}
304-
let tests = {};
305-
if(utils.isUndefined(configJson.tests) || utils.isUndefined(resultsJson.tests)){
306-
resolve();
307-
}
308-
configJson.tests.forEach((test) => {
309-
tests[test["clientId"]] = test;
120+
//ensure that the user can call `then()` only when the file has
121+
//been downloaded entirely.
122+
response.pipe(writer);
123+
let error = null;
124+
writer.on('error', err => {
125+
error = err;
126+
writer.close();
127+
reject(err);
128+
process.exitCode = Constants.ERROR_EXIT_CODE;
310129
});
311-
resultsJson.tests.forEach((test) => {
312-
tests[test["clientId"]] = Object.assign(
313-
tests[test["clientId"]],
314-
test
315-
);
316-
});
317-
let sessionTests = [];
318-
Object.keys(tests).forEach((testId) => {
319-
sessionTests.push({
320-
name: tests[testId]["title"].pop(),
321-
status: tests[testId]["state"],
322-
duration: parseFloat(
323-
tests[testId]["attempts"].pop()["wallClockDuration"] / 1000
324-
).toFixed(2),
325-
});
130+
writer.on('close', async () => {
131+
if (!error) {
132+
logger.debug("Unzipping downloaded html and json reports.");
133+
unzipFile(filePath, fileName).then((msg) => {
134+
logger.debug(msg);
135+
fs.unlinkSync(tmpFilePath);
136+
logger.debug("Successfully prepared json and html reports.");
137+
resolve(true);
138+
}).catch((err) =>{
139+
logger.debug(`Unzipping html and json report failed. Error: ${err}`)
140+
reject(true);
141+
});
142+
}
143+
//no need to call the reject here, as it will have been called in the
144+
//'error' stream;
326145
});
327-
combination.tests = sessionTests;
328-
resolve(combination.tests);
329-
} catch (error) {
330-
process.exitCode = Constants.ERROR_EXIT_CODE;
331-
reject(error);
332146
}
333-
})
147+
});
148+
});
334149
}
335150

336-
function generateCypressCombinationSpecReportDataWithoutConfigJson(combination){
337-
return new Promise(async (resolve, reject) => {
338-
try {
339-
let resultsJson ,resultsJsonError;
340-
await getResultsJsonResponse(combination).then(function (successResult) {
341-
[resultsJson, resultsJsonError] = successResult
342-
}).catch( function (failureResult) {
343-
[resultsJson, resultsJsonError] = failureResult
344-
})
345-
if(resultsJsonError || utils.isUndefined(resultsJsonResponse)){
346-
resolve();
347-
}
348-
let sessionTests = [];
349-
if(utils.isUndefined(resultsJson.tests)){
350-
resolve();
351-
}
352-
resultsJson.tests.forEach((test) => {
353-
durationKey = utils.isUndefined(test["attempts"]) ? test : test["attempts"].pop()
354-
sessionTests.push({
355-
name: test["title"].pop(),
356-
status: test["state"],
357-
duration: parseFloat(
358-
durationKey["wallClockDuration"] / 1000
359-
).toFixed(2)
360-
})
361-
});
362-
combination.tests = sessionTests;
363-
resolve(combination.tests);
364-
} catch (error) {
151+
const unzipFile = async (filePath, fileName) => {
152+
return new Promise( async (resolve, reject) => {
153+
await unzipper.Open.file(path.join(filePath, fileName))
154+
.then(d => d.extract({path: filePath, concurrency: 5}))
155+
.catch((err) => {
156+
reject(err);
365157
process.exitCode = Constants.ERROR_EXIT_CODE;
366-
reject(error);
367-
}
368-
})
158+
});
159+
let message = "Unzipped the json and html successfully."
160+
resolve(message);
161+
});
369162
}
370163

371164
exports.reportGenerator = reportGenerator;

0 commit comments

Comments
 (0)