11import { mkdir , readFile , writeFile } from 'node:fs/promises'
22import path from 'node:path'
33
4+ import { PNG } from 'pngjs'
5+
46import { composite } from './composite.js'
57import { diff as runDiff } from './diff.js'
68import { screenshot } from './microlink.js'
79
810const DEFAULT_THRESHOLD = 0.001
911const DEFAULT_WARNING_THRESHOLD = 0.02
1012const DEFAULT_PIXEL_THRESHOLD = 0.1
11- const DEFAULT_VIEWPORT = { width : 1280 , height : 800 , deviceScaleFactor : 2 }
13+ const DEFAULT_SCREENSHOT_ATTEMPTS = 3
1214const DEFAULT_ROUTES = [ '/' ]
1315
1416const noop = ( ) => { }
@@ -62,6 +64,53 @@ const slugifyRoute = route => {
6264 )
6365}
6466
67+ const imageSize = buffer => {
68+ const { width, height } = PNG . sync . read ( buffer )
69+ return { width, height }
70+ }
71+
72+ const sameSize = ( base , head ) =>
73+ base . width === head . width && base . height === head . height
74+
75+ const capturePair = async ( { baseUrl, headUrl, route, log, attempts, opts } ) => {
76+ let lastBaseSize
77+ let lastHeadSize
78+
79+ for ( let attempt = 1 ; attempt <= attempts ; attempt ++ ) {
80+ const fetchStart = Date . now ( )
81+ const [ baseBuffer , headBuffer ] = await Promise . all ( [
82+ screenshot ( baseUrl , {
83+ ...opts ,
84+ log : msg => log ( `[${ route } ] ${ msg } ` )
85+ } ) ,
86+ screenshot ( headUrl , {
87+ ...opts ,
88+ log : msg => log ( `[${ route } ] ${ msg } ` )
89+ } )
90+ ] )
91+
92+ lastBaseSize = imageSize ( baseBuffer )
93+ lastHeadSize = imageSize ( headBuffer )
94+ log (
95+ `[${ route } ] both screenshots ready in ${
96+ Date . now ( ) - fetchStart
97+ } ms · base ${ lastBaseSize . width } x${ lastBaseSize . height } · head ${
98+ lastHeadSize . width
99+ } x${ lastHeadSize . height } `
100+ )
101+
102+ if ( sameSize ( lastBaseSize , lastHeadSize ) ) return { baseBuffer, headBuffer }
103+ if ( attempt < attempts )
104+ log (
105+ `[${ route } ] screenshot dimensions mismatch, retrying (${ attempt } /${ attempts } )`
106+ )
107+ }
108+
109+ throw new Error (
110+ `Screenshot dimensions mismatch for ${ route } : base ${ lastBaseSize . width } x${ lastBaseSize . height } , head ${ lastHeadSize . width } x${ lastHeadSize . height } `
111+ )
112+ }
113+
65114const runRoute = async ( {
66115 route,
67116 base,
@@ -71,6 +120,7 @@ const runRoute = async ({
71120 pixelThreshold,
72121 outDir,
73122 log,
123+ screenshotAttempts = DEFAULT_SCREENSHOT_ATTEMPTS ,
74124 ...screenshotOpts
75125} ) => {
76126 const baseUrl = joinUrl ( base , route )
@@ -79,18 +129,14 @@ const runRoute = async ({
79129 log ( `[${ route } ] base: ${ baseUrl } ` )
80130 log ( `[${ route } ] head: ${ headUrl } ` )
81131
82- const fetchStart = Date . now ( )
83- const [ baseBuffer , headBuffer ] = await Promise . all ( [
84- screenshot ( baseUrl , {
85- ...screenshotOpts ,
86- log : msg => log ( `[${ route } ] ${ msg } ` )
87- } ) ,
88- screenshot ( headUrl , {
89- ...screenshotOpts ,
90- log : msg => log ( `[${ route } ] ${ msg } ` )
91- } )
92- ] )
93- log ( `[${ route } ] both screenshots ready in ${ Date . now ( ) - fetchStart } ms` )
132+ const { baseBuffer, headBuffer } = await capturePair ( {
133+ baseUrl,
134+ headUrl,
135+ route,
136+ log,
137+ attempts : screenshotAttempts ,
138+ opts : screenshotOpts
139+ } )
94140
95141 const diffStart = Date . now ( )
96142 const {
@@ -168,11 +214,7 @@ export const run = async ({
168214 if ( ! Array . isArray ( routes ) || routes . length === 0 )
169215 throw new Error ( 'routes must be a non-empty array' )
170216
171- const mql = {
172- apiKey,
173- ...mqlOpts ,
174- viewport : { ...DEFAULT_VIEWPORT , ...mqlOpts . viewport }
175- }
217+ const mql = { apiKey, force : true , ...mqlOpts }
176218 const resolvedThreshold = await resolveThreshold ( { flag : threshold , cwd } )
177219 const resolvedWarningThreshold = await resolveWarningThreshold ( { flag : warningThreshold , cwd } )
178220 log (
0 commit comments