1
1
import path from "path" ;
2
- import pixelmatch from "pixelmatch" ;
3
2
import fs from "fs" ;
4
- import { PNG , PNGWithMetadata } from "pngjs" ;
5
- import { FILE_SUFFIX , IMAGE_SNAPSHOT_PREFIX , TASK } from "./constants" ;
3
+ import { IMAGE_SNAPSHOT_PREFIX } from "./constants" ;
6
4
import moveFile from "move-file" ;
7
- import sharp from "sharp" ;
8
- import sanitize from "sanitize-filename" ;
5
+ import { initTaskHooks } from "./tasks" ;
9
6
10
7
type NotFalsy < T > = T extends false | null | undefined ? never : T ;
11
8
12
- type CompareImagesCfg = {
13
- scaleFactor : number ;
14
- title : string ;
15
- imgNew : string ;
16
- imgOld : string ;
17
- updateImages : boolean ;
18
- maxDiffThreshold : number ;
19
- diffConfig : Parameters < typeof pixelmatch > [ 5 ] ;
20
- } & Parameters < typeof pixelmatch > [ 5 ] ;
21
-
22
- const round = ( n : number ) => Math . ceil ( n * 1000 ) / 1000 ;
23
-
24
- const createImageResizer = ( width : number , height : number ) => ( source : PNG ) => {
25
- const resized = new PNG ( { width, height, fill : true } ) ;
26
- PNG . bitblt ( source , resized , 0 , 0 , source . width , source . height , 0 , 0 ) ;
27
- return resized ;
28
- } ;
29
-
30
- const inArea = ( x : number , y : number , height : number , width : number ) =>
31
- y > height || x > width ;
32
-
33
- const fillSizeDifference = ( width : number , height : number ) => ( image : PNG ) => {
34
- for ( let y = 0 ; y < image . height ; y ++ ) {
35
- for ( let x = 0 ; x < image . width ; x ++ ) {
36
- if ( inArea ( x , y , height , width ) ) {
37
- const idx = ( image . width * y + x ) << 2 ;
38
- image . data [ idx ] = 0 ;
39
- image . data [ idx + 1 ] = 0 ;
40
- image . data [ idx + 2 ] = 0 ;
41
- image . data [ idx + 3 ] = 64 ;
42
- }
43
- }
44
- }
45
- return image ;
46
- } ;
47
-
48
- const importAndScaleImage = async ( cfg : {
49
- scaleFactor : number ;
50
- path : string ;
51
- } ) => {
52
- const imgBuffer = fs . readFileSync ( cfg . path ) ;
53
- const rawImgNew = PNG . sync . read ( imgBuffer ) ;
54
- if ( cfg . scaleFactor === 1 ) return rawImgNew ;
55
-
56
- const newImageWidth = Math . ceil ( rawImgNew . width * cfg . scaleFactor ) ;
57
- const newImageHeight = Math . ceil ( rawImgNew . height * cfg . scaleFactor ) ;
58
- await sharp ( imgBuffer ) . resize ( newImageWidth , newImageHeight ) . toFile ( cfg . path ) ;
59
-
60
- return PNG . sync . read ( fs . readFileSync ( cfg . path ) ) ;
61
- } ;
62
-
63
- const alignImagesToSameSize = (
64
- firstImage : PNGWithMetadata ,
65
- secondImage : PNGWithMetadata
66
- ) => {
67
- const firstImageWidth = firstImage . width ;
68
- const firstImageHeight = firstImage . height ;
69
- const secondImageWidth = secondImage . width ;
70
- const secondImageHeight = secondImage . height ;
71
-
72
- const resizeToSameSize = createImageResizer (
73
- Math . max ( firstImageWidth , secondImageWidth ) ,
74
- Math . max ( firstImageHeight , secondImageHeight )
75
- ) ;
76
-
77
- const resizedFirst = resizeToSameSize ( firstImage ) ;
78
- const resizedSecond = resizeToSameSize ( secondImage ) ;
79
-
80
- return [
81
- fillSizeDifference ( firstImageWidth , firstImageHeight ) ( resizedFirst ) ,
82
- fillSizeDifference ( secondImageWidth , secondImageHeight ) ( resizedSecond ) ,
83
- ] ;
84
- } ;
85
-
86
9
const getConfigVariableOrThrow = < K extends keyof Cypress . PluginConfigOptions > (
87
10
config : Cypress . PluginConfigOptions ,
88
11
name : K
@@ -108,124 +31,20 @@ const initForceDeviceScaleFactor = (on: Cypress.PluginEvents) => {
108
31
} ) ;
109
32
} ;
110
33
111
- const initTaskHooks = ( on : Cypress . PluginEvents ) => {
112
- on ( "task" , {
113
- [ TASK . getScreenshotPath ] ( { title, imagesDir, specPath } ) {
114
- return path . join (
115
- IMAGE_SNAPSHOT_PREFIX ,
116
- path . dirname ( specPath ) ,
117
- ...imagesDir . split ( "/" ) ,
118
- `${ sanitize ( title ) } ${ FILE_SUFFIX . actual } .png`
119
- ) ;
120
- } ,
121
- [ TASK . doesFileExist ] ( { path } ) {
122
- return fs . existsSync ( path ) ;
123
- } ,
124
- [ TASK . approveImage ] ( { img } ) {
125
- const oldImg = img . replace ( FILE_SUFFIX . actual , "" ) ;
126
- if ( fs . existsSync ( oldImg ) ) fs . unlinkSync ( oldImg ) ;
127
-
128
- const diffImg = img . replace ( FILE_SUFFIX . actual , FILE_SUFFIX . diff ) ;
129
- if ( fs . existsSync ( diffImg ) ) fs . unlinkSync ( diffImg ) ;
130
-
131
- if ( fs . existsSync ( img ) ) moveFile . sync ( img , oldImg ) ;
132
-
133
- return null ;
134
- } ,
135
- async [ TASK . compareImages ] ( cfg : CompareImagesCfg ) : Promise < null | {
136
- error ?: boolean ;
137
- message ?: string ;
138
- imgDiff ?: number ;
139
- maxDiffThreshold ?: number ;
140
- } > {
141
- const messages = [ ] as string [ ] ;
142
- let imgDiff : number | undefined ;
143
- let error = false ;
144
-
145
- if ( fs . existsSync ( cfg . imgOld ) && ! cfg . updateImages ) {
146
- const rawImgNew = await importAndScaleImage ( {
147
- scaleFactor : cfg . scaleFactor ,
148
- path : cfg . imgNew ,
149
- } ) ;
150
- const rawImgOld = PNG . sync . read ( fs . readFileSync ( cfg . imgOld ) ) ;
151
- const isImgSizeDifferent =
152
- rawImgNew . height !== rawImgOld . height ||
153
- rawImgNew . width !== rawImgOld . width ;
154
-
155
- const [ imgNew , imgOld ] = isImgSizeDifferent
156
- ? alignImagesToSameSize ( rawImgNew , rawImgOld )
157
- : [ rawImgNew , rawImgOld ] ;
158
-
159
- const { width, height } = imgNew ;
160
- const diff = new PNG ( { width, height } ) ;
161
- const diffConfig = Object . assign ( { includeAA : true } , cfg . diffConfig ) ;
162
-
163
- const diffPixels = pixelmatch (
164
- imgNew . data ,
165
- imgOld . data ,
166
- diff . data ,
167
- width ,
168
- height ,
169
- diffConfig
170
- ) ;
171
- imgDiff = diffPixels / ( width * height ) ;
172
-
173
- if ( isImgSizeDifferent ) {
174
- messages . push (
175
- `Warning: Images size mismatch - new screenshot is ${ rawImgNew . width } px by ${ rawImgNew . height } px while old one is ${ rawImgOld . width } px by ${ rawImgOld . height } (width x height).`
176
- ) ;
177
- }
178
-
179
- if ( imgDiff > cfg . maxDiffThreshold ) {
180
- messages . unshift (
181
- `Image diff factor (${ round (
182
- imgDiff
183
- ) } ) is bigger than maximum threshold option ${
184
- cfg . maxDiffThreshold
185
- } .`
186
- ) ;
187
- error = true ;
188
- }
189
-
190
- if ( error ) {
191
- fs . writeFileSync (
192
- cfg . imgNew . replace ( FILE_SUFFIX . actual , FILE_SUFFIX . diff ) ,
193
- PNG . sync . write ( diff )
194
- ) ;
195
- return {
196
- error,
197
- message : messages . join ( "\n" ) ,
198
- imgDiff,
199
- maxDiffThreshold : cfg . maxDiffThreshold ,
200
- } ;
201
- } else {
202
- // don't overwrite file if it's the same (imgDiff < cfg.maxDiffThreshold && !isImgSizeDifferent)
203
- fs . unlinkSync ( cfg . imgNew ) ;
204
- }
205
- } else {
206
- // there is no "old screenshot" or screenshots should be immediately updated
207
- imgDiff = 0 ;
208
- moveFile . sync ( cfg . imgNew , cfg . imgOld ) ;
209
- }
210
-
211
- if ( typeof imgDiff !== "undefined" ) {
212
- messages . unshift (
213
- `Image diff (${ round (
214
- imgDiff
215
- ) } %) is within boundaries of maximum threshold option ${
216
- cfg . maxDiffThreshold
217
- } .`
218
- ) ;
219
- return {
220
- message : messages . join ( "\n" ) ,
221
- imgDiff,
222
- maxDiffThreshold : cfg . maxDiffThreshold ,
223
- } ;
224
- }
34
+ const removeScreenshotsDirectory = (
35
+ screenshotsFolder : string ,
36
+ onSuccess : ( ) => void ,
37
+ onError : ( e : Error ) => void
38
+ ) => {
39
+ fs . rm (
40
+ path . join ( screenshotsFolder , IMAGE_SNAPSHOT_PREFIX ) ,
41
+ { recursive : true , force : true } ,
42
+ ( err ) => {
43
+ if ( err ) return onError ( err ) ;
225
44
226
- return null ;
227
- } ,
228
- } ) ;
45
+ onSuccess ( ) ;
46
+ }
47
+ ) ;
229
48
} ;
230
49
231
50
const initAfterScreenshotHook = (
@@ -249,17 +68,13 @@ const initAfterScreenshotHook = (
249
68
) ;
250
69
251
70
void moveFile ( details . path , newAbsolutePath )
252
- . then ( ( ) => {
253
- fs . rm (
254
- path . join ( screenshotsFolder , IMAGE_SNAPSHOT_PREFIX ) ,
255
- { recursive : true , force : true } ,
256
- ( err ) => {
257
- if ( err ) return reject ( err ) ;
258
-
259
- resolve ( { path : newAbsolutePath } ) ;
260
- }
261
- ) ;
262
- } )
71
+ . then ( ( ) =>
72
+ removeScreenshotsDirectory (
73
+ screenshotsFolder ,
74
+ ( ) => resolve ( { path : newAbsolutePath } ) ,
75
+ reject
76
+ )
77
+ )
263
78
. catch ( reject ) ;
264
79
} ) ;
265
80
} ) ;
0 commit comments