8
8
9
9
/* eslint-disable @typescript-eslint/no-namespace */
10
10
import {
11
+ PlainRegion ,
12
+ ResolvedVisualRegion ,
11
13
SauceVisualViewport ,
12
14
ScreenshotMetadata ,
13
15
VisualCheckOptions ,
14
16
VisualRegion ,
15
- VisualRegionWithRatio ,
16
17
} from './types' ;
17
18
18
19
declare global {
@@ -39,7 +40,7 @@ declare global {
39
40
const visualLog = ( msg : string , level : 'info' | 'warn' | 'error' = 'error' ) =>
40
41
cy . task ( 'visual-log' , { level, msg } ) ;
41
42
42
- export function isRegion ( elem : any ) : elem is VisualRegion {
43
+ export function isRegion < T extends object > ( elem : T ) : elem is PlainRegion & T {
43
44
if ( 'x' in elem && 'y' in elem && 'width' in elem && 'height' in elem ) {
44
45
return true ;
45
46
}
@@ -51,14 +52,42 @@ export function isChainable(elem: any): elem is Cypress.Chainable {
51
52
return 'chainerId' in elem ;
52
53
}
53
54
54
- /**
55
- * Note: Even if looks like promises, it is not. Cypress makes it run in a consistent and deterministic way.
56
- * As a result, item.then() will be resolved before the cy.screenshot() and cy.task() is executed.
57
- * That makes us be sure that ignoredRegion is populated correctly before the metadata being sent back to
58
- * Cypress main process.
59
- *
60
- * https://docs.cypress.io/guides/core-concepts/introduction-to-cypress#You-cannot-race-or-run-multiple-commands-at-the-same-time
61
- */
55
+ export function intoElement < R extends Omit < object , 'unknown>' > > (
56
+ region : VisualRegion < R > ,
57
+ ) : R {
58
+ return 'element' in region ? region . element : region ;
59
+ }
60
+
61
+ export function getElementDimensions ( elem : HTMLElement ) : Cypress . Dimensions {
62
+ const rect = elem . getBoundingClientRect ( ) ;
63
+ return {
64
+ x : Math . floor ( rect . left ) ,
65
+ y : Math . floor ( rect . top ) ,
66
+ width : Math . floor ( rect . width ) ,
67
+ height : Math . floor ( rect . height ) ,
68
+ } ;
69
+ }
70
+
71
+ export function resolveChainables (
72
+ item : PlainRegion | Cypress . Chainable < HTMLElement [ ] > ,
73
+ ) : Promise < PlainRegion [ ] | null > {
74
+ return new Promise ( ( resolve ) => {
75
+ if ( isChainable ( item ) ) {
76
+ item . then ( ( $el : HTMLElement [ ] ) => {
77
+ const regions : PlainRegion [ ] = [ ] ;
78
+ for ( const elem of $el ) {
79
+ regions . push ( getElementDimensions ( elem ) ) ;
80
+ }
81
+ resolve ( regions ) ;
82
+ } ) ;
83
+ } else if ( isRegion ( item ) ) {
84
+ resolve ( [ item ] ) ;
85
+ } else {
86
+ resolve ( null ) ;
87
+ }
88
+ } ) ;
89
+ }
90
+
62
91
const sauceVisualCheckCommand = (
63
92
screenshotName : string ,
64
93
options ?: VisualCheckOptions ,
@@ -83,16 +112,6 @@ const sauceVisualCheckCommand = (
83
112
viewport . height = win . innerHeight ;
84
113
} ) ;
85
114
86
- const getElementDimensions = ( elem : HTMLElement ) => {
87
- const rect = elem . getBoundingClientRect ( ) ;
88
- return {
89
- x : Math . floor ( rect . left ) ,
90
- y : Math . floor ( rect . top ) ,
91
- width : Math . floor ( rect . width ) ,
92
- height : Math . floor ( rect . height ) ,
93
- } satisfies Cypress . Dimensions ;
94
- } ;
95
-
96
115
if ( clipSelector ) {
97
116
cy . get ( clipSelector ) . then ( ( elem ) => {
98
117
const firstMatch = elem . get ( ) . find ( ( item ) => item ) ;
@@ -106,33 +125,40 @@ const sauceVisualCheckCommand = (
106
125
}
107
126
108
127
/* Remap ignore area */
109
- const providedIgnoredRegions = options ?. ignoredRegions ?? [ ] ;
110
- const ignoredRegions : VisualRegionWithRatio [ ] = [ ] ;
111
- for ( const idx in providedIgnoredRegions ) {
112
- const item = providedIgnoredRegions [ idx ] ;
113
- if ( isRegion ( item ) ) {
114
- ignoredRegions . push ( {
115
- applyScalingRatio : false ,
116
- ...item ,
117
- } ) ;
118
- continue ;
119
- }
120
-
121
- if ( isChainable ( item ) ) {
122
- item . then ( ( $el : HTMLElement [ ] ) => {
123
- for ( const elem of $el ) {
124
- const rect = getElementDimensions ( elem ) ;
125
- ignoredRegions . push ( {
126
- applyScalingRatio : true ,
127
- ...rect ,
128
- } ) ;
129
- }
130
- } ) ;
131
- continue ;
128
+ const visualRegions : VisualRegion [ ] = [
129
+ ...( options ?. ignoredRegions ?? [ ] ) . map ( ( r ) => ( {
130
+ enableOnly : [ ] ,
131
+ element : r ,
132
+ } ) ) ,
133
+ ...( options ?. regions ?? [ ] ) ,
134
+ ] ;
135
+
136
+ const regionsPromise : Promise < ResolvedVisualRegion [ ] > = ( async ( ) => {
137
+ const result = [ ] ;
138
+ for ( const idx in visualRegions ) {
139
+ const visualRegion = visualRegions [ idx ] ;
140
+
141
+ const resolvedElements : PlainRegion [ ] | null = await resolveChainables (
142
+ intoElement ( visualRegion ) ,
143
+ ) ;
144
+ if ( resolvedElements === null )
145
+ throw new Error ( `ignoreRegion[${ idx } ] has an unknown type` ) ;
146
+
147
+ const applyScalingRatio = ! isRegion ( visualRegion . element ) ;
148
+
149
+ for ( const region of resolvedElements ) {
150
+ result . push ( {
151
+ ...visualRegion ,
152
+ element : region ,
153
+ applyScalingRatio,
154
+ } satisfies ResolvedVisualRegion ) ;
155
+ }
132
156
}
133
-
134
- throw new Error ( `ignoreRegion[${ idx } ] has an unknown type` ) ;
135
- }
157
+ return result ;
158
+ } ) ( ) . catch ( ( e ) => {
159
+ visualLog ( `sauce-visual: ${ e } ` ) ;
160
+ return [ ] ;
161
+ } ) ;
136
162
137
163
const id = randomId ( ) ;
138
164
cy . get < Cypress . Dimensions | undefined > ( '@clipToBounds' ) . then (
@@ -167,17 +193,20 @@ const sauceVisualCheckCommand = (
167
193
}
168
194
} ;
169
195
170
- cy . task ( 'visual-register-screenshot' , {
171
- id : `sauce-visual-${ id } ` ,
172
- name : screenshotName ,
173
- suiteName : Cypress . currentTest . titlePath . slice ( 0 , - 1 ) . join ( ' ' ) ,
174
- testName : Cypress . currentTest . title ,
175
- ignoredRegions,
176
- diffingMethod : options ?. diffingMethod ,
177
- devicePixelRatio : win . devicePixelRatio ,
178
- viewport : realViewport ,
179
- dom : getDom ( ) ?? undefined ,
180
- } satisfies ScreenshotMetadata ) ;
196
+ regionsPromise . then ( ( regions ) => {
197
+ cy . task ( 'visual-register-screenshot' , {
198
+ id : `sauce-visual-${ id } ` ,
199
+ name : screenshotName ,
200
+ suiteName : Cypress . currentTest . titlePath . slice ( 0 , - 1 ) . join ( ' ' ) ,
201
+ testName : Cypress . currentTest . title ,
202
+ regions,
203
+ diffingMethod : options ?. diffingMethod ,
204
+ diffingOptions : options ?. diffingOptions ,
205
+ devicePixelRatio : win . devicePixelRatio ,
206
+ viewport : realViewport ,
207
+ dom : getDom ( ) ?? undefined ,
208
+ } satisfies ScreenshotMetadata ) ;
209
+ } ) ;
181
210
} ) ;
182
211
} ) ;
183
212
} ;
0 commit comments