11const 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' ? "✔ " : "✗ " ;
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-
9110let 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
371164exports . reportGenerator = reportGenerator ;
0 commit comments