1
1
const fs = require ( 'fs' ) ,
2
2
path = require ( 'path' ) ,
3
3
request = require ( 'request' ) ,
4
+ unzipper = require ( 'unzipper' ) ,
4
5
logger = require ( './logger' ) . winstonLogger ,
5
6
utils = require ( "./utils" ) ,
6
7
Constants = require ( './constants' ) ,
7
8
config = require ( "./config" ) ;
8
9
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
-
91
10
let reportGenerator = ( bsConfig , buildId , args , rawArgs , buildReportData , cb ) => {
92
11
let options = {
93
12
url : `${ config . buildUrl } ${ buildId } /custom_report` ,
@@ -100,6 +19,8 @@ let reportGenerator = (bsConfig, buildId, args, rawArgs, buildReportData, cb) =>
100
19
} ,
101
20
} ;
102
21
22
+ logger . debug ( 'Started fetching the build json and html reports.' ) ;
23
+
103
24
return request . get ( options , async function ( err , resp , body ) {
104
25
let message = null ;
105
26
let messageType = null ;
@@ -117,6 +38,7 @@ let reportGenerator = (bsConfig, buildId, args, rawArgs, buildReportData, cb) =>
117
38
utils . sendUsageReport ( bsConfig , args , message , messageType , errorCode , buildReportData , rawArgs ) ;
118
39
return ;
119
40
} else {
41
+ logger . debug ( 'Received reports data from upstream.' ) ;
120
42
try {
121
43
build = JSON . parse ( body ) ;
122
44
} catch ( error ) {
@@ -127,7 +49,6 @@ let reportGenerator = (bsConfig, buildId, args, rawArgs, buildReportData, cb) =>
127
49
if ( resp . statusCode == 299 ) {
128
50
messageType = Constants . messageTypes . INFO ;
129
51
errorCode = 'api_deprecated' ;
130
-
131
52
if ( build ) {
132
53
message = build . message ;
133
54
logger . info ( message ) ;
@@ -163,209 +84,81 @@ let reportGenerator = (bsConfig, buildId, args, rawArgs, buildReportData, cb) =>
163
84
} else {
164
85
messageType = Constants . messageTypes . SUCCESS ;
165
86
message = `Report for build: ${ buildId } was successfully created.` ;
166
- await renderReportHTML ( build ) ;
87
+ await generateCypressBuildReport ( build ) ;
167
88
logger . info ( message ) ;
168
89
}
90
+ logger . debug ( 'Finished fetching the build json and html reports.' ) ;
169
91
utils . sendUsageReport ( bsConfig , args , message , messageType , errorCode , buildReportData , rawArgs ) ;
170
92
if ( cb ) {
171
93
cb ( ) ;
172
94
}
173
95
} ) ;
174
96
}
175
97
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' ) ;
191
100
192
101
if ( ! fs . existsSync ( resultsDir ) ) {
102
+ logger . debug ( "Results directory doesn't exists." ) ;
103
+ logger . debug ( "Creating results directory." ) ;
193
104
fs . mkdirSync ( resultsDir ) ;
194
105
}
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 ) ;
235
107
}
236
108
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.` )
238
113
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 ) {
262
115
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 ) ;
271
119
} 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 ;
310
129
} ) ;
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 ;
326
145
} ) ;
327
- combination . tests = sessionTests ;
328
- resolve ( combination . tests ) ;
329
- } catch ( error ) {
330
- process . exitCode = Constants . ERROR_EXIT_CODE ;
331
- reject ( error ) ;
332
146
}
333
- } )
147
+ } ) ;
148
+ } ) ;
334
149
}
335
150
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 ) ;
365
157
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
+ } ) ;
369
162
}
370
163
371
164
exports . reportGenerator = reportGenerator ;
0 commit comments