@@ -3,7 +3,12 @@ const fs = require('fs');
3
3
const path = require ( 'path' )
4
4
const formData = require ( 'form-data' ) ;
5
5
const { JSDOM } = require ( "jsdom" ) ;
6
+ const Table = require ( 'cli-table3' ) ;
6
7
var { constants } = require ( './constants' ) ;
8
+ const { getLastCommit } = require ( './git' ) ;
9
+
10
+ var INTERVAL = 2000
11
+ const MAX_INTERVAL = 512000
7
12
8
13
async function sendDoM ( storybookUrl , stories , storybookConfig , options ) {
9
14
const createBrowser = require ( 'browserless' )
@@ -20,16 +25,24 @@ async function sendDoM(storybookUrl, stories, storybookConfig, options) {
20
25
const browserless = await browser . createContext ( )
21
26
const html = await browserless . html ( storyInfo . url )
22
27
23
- dom = new JSDOM ( html ) ;
24
- for ( element of dom . window . document . querySelectorAll ( 'img' ) ) {
28
+ dom = new JSDOM ( html , {
29
+ url : storybookUrl ,
30
+ resources : 'usable'
31
+ } ) ;
32
+ clone = new JSDOM ( html ) ;
33
+
34
+ // Serialize DOM
35
+ for ( element of clone . window . document . querySelectorAll ( 'img' ) ) {
25
36
let image = new URL ( element . getAttribute ( 'src' ) , storybookUrl ) . href ;
26
37
let format = path . extname ( image ) . replace ( / ^ ./ , '' ) ;
27
38
format = format === 'svg' ? 'svg+xml' : format
28
39
let imageAsBase64 = await getBase64 ( image ) ;
29
40
element . setAttribute ( 'src' , 'data:image/' + format + ';base64,' + imageAsBase64 ) ;
30
41
}
42
+ await serializeCSSOM ( dom , clone ) ;
43
+
31
44
try {
32
- fs . writeFileSync ( 'doms/' + storyId + '.html' , dom . serialize ( ) ) ;
45
+ fs . writeFileSync ( 'doms/' + storyId + '.html' , clone . serialize ( ) ) ;
33
46
} catch ( err ) {
34
47
console . error ( err ) ;
35
48
}
@@ -38,41 +51,120 @@ async function sendDoM(storybookUrl, stories, storybookConfig, options) {
38
51
}
39
52
await browser . close ( )
40
53
41
- // Send html files to the renderer API
54
+ // Create form
55
+ // let commit = await getLastCommit();
42
56
const form = new formData ( ) ;
43
57
for ( const [ storyId , storyInfo ] of Object . entries ( stories ) ) {
44
58
const file = fs . readFileSync ( 'doms/' + storyId + '.html' ) ;
45
- form . append ( 'html ' , file , storyInfo . kind + ': ' + storyInfo . name + '.html' ) ;
59
+ form . append ( 'files ' , file , storyInfo . kind + ': ' + storyInfo . name + '.html' ) ;
46
60
}
47
61
form . append ( 'resolution' , storybookConfig . resolutions ) ;
48
62
form . append ( 'browser' , storybookConfig . browsers ) ;
49
63
form . append ( 'projectToken' , process . env . PROJECT_TOKEN ) ;
50
- form . append ( 'buildName' , options . buildname ) ;
51
- axios . post ( constants [ constants . env ] . RENDER_API_URL , form , {
64
+ // form.append('branch', commit.branch);
65
+ // form.append('commitId', commit.shortHash);
66
+ // form.append('commitAuthor', commit.author.name);
67
+ // form.append('commitMessage', commit.subject);
68
+
69
+ // Send DOM to render API
70
+ await axios . post ( constants [ options . env ] . RENDER_API_URL , form , {
52
71
headers : {
53
72
...form . getHeaders ( )
54
73
}
55
74
} )
56
- . then ( function ( response ) {
57
- console . log ( '[smartui] Build successful' )
75
+ . then ( async function ( response ) {
76
+ console . log ( '[smartui] Build in progress...' ) ;
77
+ await shortPolling ( response . data . buildId , 0 , options ) ;
58
78
} )
59
79
. catch ( function ( error ) {
60
- fs . rm ( 'doms' , { recursive : true } , ( err ) => {
61
- if ( err ) {
62
- return console . error ( err ) ;
63
- }
64
- } ) ;
65
- console . log ( '[smartui] Build failed: Error: ' , error . message ) ;
66
- process . exit ( 0 ) ;
80
+ if ( error . response ) {
81
+ console . log ( '[smartui] Build failed: Error: ' , error . response . data . message ) ;
82
+ } else {
83
+ console . log ( '[smartui] Build failed: Error: ' , error . message ) ;
84
+ }
67
85
} ) ;
68
-
86
+
69
87
fs . rm ( 'doms' , { recursive : true } , ( err ) => {
70
88
if ( err ) {
71
89
return console . error ( err ) ;
72
90
}
73
91
} ) ;
74
92
} ;
75
93
94
+ async function shortPolling ( buildId , retries = 0 , options ) {
95
+ await axios . get ( new URL ( '?buildId=' + buildId , constants [ options . env ] . BUILD_STATUS_URL ) . href , {
96
+ headers : {
97
+ projectToken : process . env . PROJECT_TOKEN
98
+ } } )
99
+ . then ( function ( response ) {
100
+ if ( response . data ) {
101
+ if ( response . data . buildStatus === 'completed' ) {
102
+ console . log ( '[smartui] Build successful\n' ) ;
103
+ console . log ( '[smartui] Build details:\n' ,
104
+ 'Build URL: ' , response . data . buildURL , '\n' ,
105
+ 'Build Name: ' , response . data . buildName , '\n' ,
106
+ 'Total Screenshots: ' , response . data . totalScreenshots , '\n' ,
107
+ 'Approved: ' , response . data . buildResults . approved , '\n' ,
108
+ 'Changes found: ' , response . data . buildResults . changesFound , '\n'
109
+ ) ;
110
+
111
+ if ( response . data . screenshots && response . data . screenshots . length > 0 ) {
112
+ import ( 'chalk' ) . then ( ( chalk ) => {
113
+ const table = new Table ( {
114
+ head : [
115
+ { content : chalk . default . white ( 'Story' ) , hAlign : 'center' } ,
116
+ { content : chalk . default . white ( 'Mis-match %' ) , hAlign : 'center' } ,
117
+ ]
118
+ } ) ;
119
+ response . data . screenshots . forEach ( screenshot => {
120
+ let mismatch = screenshot . mismatchPercentage
121
+ table . push ( [
122
+ chalk . default . yellow ( screenshot . storyName ) ,
123
+ mismatch > 0 ? chalk . default . red ( mismatch ) : chalk . default . green ( mismatch )
124
+ ] )
125
+ } ) ;
126
+ console . log ( table . toString ( ) ) ;
127
+ } )
128
+ } else {
129
+ if ( response . data . baseline ) {
130
+ console . log ( 'No comparisons run. This is a baseline build.' ) ;
131
+ } else {
132
+ console . log ( 'No comparisons run. No screenshot in the current build has the corresponding screenshot in baseline build.' ) ;
133
+ }
134
+ }
135
+ return ;
136
+ } else {
137
+ if ( response . data . screenshots && response . data . screenshots . length > 0 ) {
138
+ // TODO: show Screenshots processed current/total
139
+ console . log ( '[smartui] Screenshots compared: ' , response . data . screenshots . length )
140
+ }
141
+ }
142
+ }
143
+
144
+ // Double the INTERVAL, up to the maximum INTERVAL of 512 secs (so ~15 mins in total)
145
+ INTERVAL = Math . min ( INTERVAL * 2 , MAX_INTERVAL ) ;
146
+ if ( INTERVAL == MAX_INTERVAL ) {
147
+ console . log ( '[smartui] Please check the build status on LambdaTest SmartUI.' ) ;
148
+ return ;
149
+ }
150
+
151
+ setTimeout ( function ( ) {
152
+ shortPolling ( buildId , 0 , options )
153
+ } , INTERVAL ) ;
154
+ } )
155
+ . catch ( function ( error ) {
156
+ if ( retries >= 3 ) {
157
+ console . log ( '[smartui] Error: Failed getting build status.' , error . message ) ;
158
+ console . log ( '[smartui] Please check the build status on LambdaTest SmartUI.' ) ;
159
+ return ;
160
+ }
161
+
162
+ setTimeout ( function ( ) {
163
+ shortPolling ( buildId , retries + 1 , options ) ;
164
+ } , 2000 ) ;
165
+ } ) ;
166
+ } ;
167
+
76
168
function getBase64 ( url ) {
77
169
return axios . get ( url , {
78
170
responseType : "text" ,
@@ -85,4 +177,19 @@ function getBase64(url) {
85
177
} ) ;
86
178
}
87
179
180
+ async function serializeCSSOM ( dom , clone ) {
181
+ return new Promise ( resolve => {
182
+ dom . window . addEventListener ( "load" , ( ) => {
183
+ for ( let styleSheet of dom . window . document . styleSheets ) {
184
+ let style = clone . window . document . createElement ( 'style' ) ;
185
+ style . type = 'text/css' ;
186
+ style . innerHTML = Array . from ( styleSheet . cssRules )
187
+ . map ( cssRule => cssRule . cssText ) . join ( '\n' ) ;
188
+ clone . window . document . head . appendChild ( style ) ;
189
+ }
190
+ resolve ( ) ;
191
+ } ) ;
192
+ } ) ;
193
+ }
194
+
88
195
module . exports = { sendDoM } ;
0 commit comments