11import { Snapshot , Context , ProcessedSnapshot } from "../types.js" ;
2- import { chromium , Locator } from "@playwright/test"
2+ import { scrollToBottomAndBackToTop } from "./utils.js"
3+ import { chromium , Locator , selectors } from "@playwright/test"
34
5+ const MAX_RESOURCE_SIZE = 5 * ( 1024 ** 2 ) ; // 5MB
6+ var ALLOWED_RESOURCES = [ 'document' , 'stylesheet' , 'image' , 'media' , 'font' , 'other' ] ;
7+ const ALLOWED_STATUSES = [ 200 , 201 ] ;
48const MIN_VIEWPORT_HEIGHT = 1080 ;
59
610export default async ( snapshot : Snapshot , ctx : Context ) : Promise < Record < string , any > > => {
7- // Process snapshot options
11+ ctx . log . debug ( `Processing snapshot ${ snapshot . name } ` ) ;
12+
13+ if ( ! ctx . browser ) ctx . browser = await chromium . launch ( { headless : true } ) ;
14+ const context = await ctx . browser . newContext ( )
15+ const page = await context . newPage ( ) ;
16+ let cache : Record < string , any > = { } ;
17+
18+ // Use route to intercept network requests and discover resources
19+ await page . route ( '**/*' , async ( route , request ) => {
20+ const requestUrl = request . url ( )
21+ const snapshotHostname = new URL ( snapshot . url ) . hostname ;
22+ const requestHostname = new URL ( requestUrl ) . hostname ;
23+
24+ try {
25+ const response = await page . request . fetch ( request ) ;
26+ const body = await response . body ( ) ;
27+
28+ if ( ctx . webConfig . enableJavaScript ) ALLOWED_RESOURCES . push ( 'script' ) ;
29+ if ( ! body ) {
30+ ctx . log . debug ( `Handling request ${ requestUrl } \n - skipping no response` ) ;
31+ } else if ( ! body . length ) {
32+ ctx . log . debug ( `Handling request ${ requestUrl } \n - skipping empty response` ) ;
33+ } else if ( requestUrl === snapshot . url ) {
34+ ctx . log . debug ( `Handling request ${ requestUrl } \n - skipping root resource` ) ;
35+ } else if ( requestHostname !== snapshotHostname ) {
36+ ctx . log . debug ( `Handling request ${ requestUrl } \n - skipping remote resource` ) ;
37+ } else if ( cache [ requestUrl ] ) {
38+ ctx . log . debug ( `Handling request ${ requestUrl } \n - skipping already cached resource` ) ;
39+ } else if ( body . length > MAX_RESOURCE_SIZE ) {
40+ ctx . log . debug ( `Handling request ${ requestUrl } \n - skipping resource larger than 5MB` ) ;
41+ } else if ( ! ALLOWED_STATUSES . includes ( response . status ( ) ) ) {
42+ ctx . log . debug ( `Handling request ${ requestUrl } \n - skipping disallowed status [${ response . status ( ) } ]` ) ;
43+ } else if ( ! ctx . webConfig . enableJavaScript && ! ALLOWED_RESOURCES . includes ( request . resourceType ( ) ) ) {
44+ ctx . log . debug ( `Handling request ${ requestUrl } \n - skipping disallowed resource type [${ request . resourceType ( ) } ]` ) ;
45+ } else {
46+ ctx . log . debug ( `Handling request ${ requestUrl } \n - content-type ${ response . headers ( ) [ 'content-type' ] } ` ) ;
47+ cache [ requestUrl ] = {
48+ body : body . toString ( 'base64' ) ,
49+ type : response . headers ( ) [ 'content-type' ]
50+ }
51+ }
52+
53+ // Continue the request with the fetched response
54+ route . fulfill ( {
55+ status : response . status ( ) ,
56+ headers : response . headers ( ) ,
57+ body : body ,
58+ } ) ;
59+ } catch ( error ) {
60+ ctx . log . debug ( `Handling request ${ requestUrl } - aborted` ) ;
61+ route . abort ( ) ;
62+ }
63+ } ) ;
64+
865 let options = snapshot . options ;
966 let optionWarnings : Set < string > = new Set ( ) ;
1067 let processedOptions : Record < string , any > = { } ;
11- if ( options && Object . keys ( options ) . length !== 0 ) {
12- ctx . log . debug ( `Processing options: ${ JSON . stringify ( options ) } ` ) ;
68+ let selectors : Array < string > = [ ] ;
69+ let ignoreOrSelectDOM : string ;
70+ let ignoreOrSelectBoxes : string ;
71+ if ( options && Object . keys ( options ) . length ) {
72+ ctx . log . debug ( `Snapshot options: ${ JSON . stringify ( options ) } ` ) ;
1373
14- if ( ( options . ignoreDOM && Object . keys ( options . ignoreDOM ) . length !== 0 ) || ( options . selectDOM && Object . keys ( options . selectDOM ) . length !== 0 ) ) {
15- if ( ! ctx . browser ) ctx . browser = await chromium . launch ( { headless : true } ) ;
16-
17- let ignoreOrSelectDOM : string ;
18- let ignoreOrSelectBoxes : string ;
19- if ( options . ignoreDOM && Object . keys ( options . ignoreDOM ) . length !== 0 ) {
74+ if ( ( options . ignoreDOM && Object . keys ( options . ignoreDOM ) . length ) || ( options . selectDOM && Object . keys ( options . selectDOM ) . length ) ) {
75+ if ( options . ignoreDOM && Object . keys ( options . ignoreDOM ) . length ) {
2076 processedOptions . ignoreBoxes = { } ;
2177 ignoreOrSelectDOM = 'ignoreDOM' ;
2278 ignoreOrSelectBoxes = 'ignoreBoxes' ;
@@ -26,7 +82,6 @@ export default async (snapshot: Snapshot, ctx: Context): Promise<Record<string,
2682 ignoreOrSelectBoxes = 'selectBoxes' ;
2783 }
2884
29- let selectors : Array < string > = [ ] ;
3085 for ( const [ key , value ] of Object . entries ( options [ ignoreOrSelectDOM ] ) ) {
3186 switch ( key ) {
3287 case 'id' :
@@ -42,46 +97,62 @@ export default async (snapshot: Snapshot, ctx: Context): Promise<Record<string,
4297 selectors . push ( ...value ) ;
4398 break ;
4499 }
45- }
46-
47- for ( const vp of ctx . webConfig . viewports ) {
48- const page = await ctx . browser . newPage ( { viewport : { width : vp . width , height : vp . height || MIN_VIEWPORT_HEIGHT } } ) ;
49- await page . setContent ( snapshot . dom . html ) ;
100+ }
101+ }
102+ }
50103
51- let viewport : string = `${ vp . width } ${ vp . height ? 'x' + vp . height : '' } ` ;
52- if ( ! Array . isArray ( processedOptions [ ignoreOrSelectBoxes ] [ viewport ] ) ) processedOptions [ ignoreOrSelectBoxes ] [ viewport ] = [ ]
104+ // process for every viewport
105+ let navigated : boolean = false ;
106+ for ( const viewport of ctx . webConfig . viewports ) {
107+ await page . setViewportSize ( { width : viewport . width , height : viewport . height || MIN_VIEWPORT_HEIGHT } ) ;
108+ ctx . log . debug ( `Page resized to ${ viewport . width } x${ viewport . height || MIN_VIEWPORT_HEIGHT } ` ) ;
109+ if ( ! navigated ) {
110+ await page . goto ( snapshot . url ) ;
111+ navigated = true ;
112+ ctx . log . debug ( `Navigated to ${ snapshot . url } ` ) ;
113+ }
114+ if ( ! viewport . height ) await page . evaluate ( scrollToBottomAndBackToTop ) ;
115+ await page . waitForLoadState ( 'networkidle' ) ;
116+ ctx . log . debug ( 'Network idle 500ms' ) ;
53117
54- let locators : Array < Locator > = [ ] ;
55- let boxes : Array < Record < string , number > > = [ ] ;
56- for ( const selector of selectors ) {
57- let l = await page . locator ( selector ) . all ( )
58- if ( l . length === 0 ) {
59- optionWarnings . add ( `For snapshot ${ snapshot . name } , no element found for selector ${ selector } ` ) ;
60- continue ;
61- }
62- locators . push ( ...l ) ;
63- }
64- for ( const locator of locators ) {
65- let bb = await locator . boundingBox ( ) ;
66- if ( bb ) boxes . push ( {
67- left : bb . x ,
68- top : bb . y ,
69- right : bb . x + bb . width ,
70- bottom : bb . y + bb . height
71- } ) ;
118+ // find bounding boxes for elements
119+ if ( selectors . length ) {
120+ let viewportString : string = `${ viewport . width } ${ viewport . height ? 'x' + viewport . height : '' } ` ;
121+ if ( ! Array . isArray ( processedOptions [ ignoreOrSelectBoxes ] [ viewportString ] ) ) processedOptions [ ignoreOrSelectBoxes ] [ viewportString ] = [ ]
122+
123+ let locators : Array < Locator > = [ ] ;
124+ let boxes : Array < Record < string , number > > = [ ] ;
125+ for ( const selector of selectors ) {
126+ let l = await page . locator ( selector ) . all ( )
127+ if ( l . length === 0 ) {
128+ optionWarnings . add ( `For snapshot ${ snapshot . name } , no element found for selector ${ selector } ` ) ;
129+ continue ;
72130 }
73-
74- processedOptions [ ignoreOrSelectBoxes ] [ viewport ] . push ( ...boxes ) ;
75- await page . close ( ) ;
131+ locators . push ( ...l ) ;
132+ }
133+ for ( const locator of locators ) {
134+ let bb = await locator . boundingBox ( ) ;
135+ if ( bb ) boxes . push ( {
136+ left : bb . x ,
137+ top : bb . y ,
138+ right : bb . x + bb . width ,
139+ bottom : bb . y + bb . height
140+ } ) ;
76141 }
142+
143+ processedOptions [ ignoreOrSelectBoxes ] [ viewportString ] . push ( ...boxes ) ;
77144 }
78145 }
79146
147+ await page . close ( ) ;
148+ await context . close ( ) ;
149+
80150 return {
81151 processedSnapshot : {
82152 name : snapshot . name ,
83153 url : snapshot . url ,
84154 dom : Buffer . from ( snapshot . dom . html ) . toString ( 'base64' ) ,
155+ resources : cache ,
85156 options : processedOptions
86157 } ,
87158 warnings : [ ...optionWarnings , ...snapshot . dom . warnings ]
0 commit comments