@@ -3,7 +3,12 @@ const fs = require('fs');
33const path = require ( 'path' )
44const formData = require ( 'form-data' ) ;
55const { JSDOM } = require ( "jsdom" ) ;
6+ const Table = require ( 'cli-table3' ) ;
67var { constants } = require ( './constants' ) ;
8+ const { getLastCommit } = require ( './git' ) ;
9+
10+ var INTERVAL = 2000
11+ const MAX_INTERVAL = 512000
712
813async function sendDoM ( storybookUrl , stories , storybookConfig , options ) {
914 const createBrowser = require ( 'browserless' )
@@ -20,16 +25,24 @@ async function sendDoM(storybookUrl, stories, storybookConfig, options) {
2025 const browserless = await browser . createContext ( )
2126 const html = await browserless . html ( storyInfo . url )
2227
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' ) ) {
2536 let image = new URL ( element . getAttribute ( 'src' ) , storybookUrl ) . href ;
2637 let format = path . extname ( image ) . replace ( / ^ ./ , '' ) ;
2738 format = format === 'svg' ? 'svg+xml' : format
2839 let imageAsBase64 = await getBase64 ( image ) ;
2940 element . setAttribute ( 'src' , 'data:image/' + format + ';base64,' + imageAsBase64 ) ;
3041 }
42+ await serializeCSSOM ( dom , clone ) ;
43+
3144 try {
32- fs . writeFileSync ( 'doms/' + storyId + '.html' , dom . serialize ( ) ) ;
45+ fs . writeFileSync ( 'doms/' + storyId + '.html' , clone . serialize ( ) ) ;
3346 } catch ( err ) {
3447 console . error ( err ) ;
3548 }
@@ -38,41 +51,120 @@ async function sendDoM(storybookUrl, stories, storybookConfig, options) {
3851 }
3952 await browser . close ( )
4053
41- // Send html files to the renderer API
54+ // Create form
55+ // let commit = await getLastCommit();
4256 const form = new formData ( ) ;
4357 for ( const [ storyId , storyInfo ] of Object . entries ( stories ) ) {
4458 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' ) ;
4660 }
4761 form . append ( 'resolution' , storybookConfig . resolutions ) ;
4862 form . append ( 'browser' , storybookConfig . browsers ) ;
4963 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 , {
5271 headers : {
5372 ...form . getHeaders ( )
5473 }
5574 } )
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 ) ;
5878 } )
5979 . 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+ }
6785 } ) ;
68-
86+
6987 fs . rm ( 'doms' , { recursive : true } , ( err ) => {
7088 if ( err ) {
7189 return console . error ( err ) ;
7290 }
7391 } ) ;
7492} ;
7593
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+
76168function getBase64 ( url ) {
77169 return axios . get ( url , {
78170 responseType : "text" ,
@@ -85,4 +177,19 @@ function getBase64(url) {
85177 } ) ;
86178}
87179
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+
88195module . exports = { sendDoM } ;
0 commit comments