@@ -2,28 +2,33 @@ import { Before, QuickPickleWorld, QuickPickleWorldInterface } from 'quickpickle
22import type { BrowserPage , Locator , UserEvent , ScreenshotOptions } from '@vitest/browser/context'
33import { defaultsDeep } from 'lodash-es'
44import type { TestContext } from 'vitest' ;
5- import type { InfoConstructor } from 'quickpickle/dist/world' ;
5+ import { ScreenshotComparisonOptions , VisualConfigSetting , VisualWorld , VisualWorldInterface , type InfoConstructor } from 'quickpickle' ;
6+ import { commands } from '@vitest/browser/context' ;
7+ import { Buffer } from 'buffer'
8+
69
710/// <reference types="@vitest/browser/providers/playwright" />
811
9- export type VitestWorldConfig = {
12+ export interface VitestWorldConfigSetting extends VisualConfigSetting {
1013 componentDir ?: string ;
11- screenshotDir ?: string ;
12- screenshotOptions ?: Partial < ScreenshotOptions > ;
1314}
1415
15- export const defaultVitestWorldConfig :VitestWorldConfig = {
16+ export const defaultVitestWorldConfig :VitestWorldConfigSetting = {
1617 componentDir : '' , // directory in which components are kept, relative to project root
1718 screenshotDir : 'screenshots' , // directory in which to save screenshots, relative to project root (default: "screenshots")
18- screenshotOptions : { } , // options for the default screenshot comparisons
19+ screenshotOpts : { // options for the default screenshot comparisons
20+ threshold : 0.1 ,
21+ alpha : 0.6 ,
22+ maxDiffPercentage : .01 ,
23+ } ,
1924}
2025
2126export type ActionsInterface = {
2227 clicks : any [ ] ;
2328 doubleClicks : any [ ] ;
2429}
2530
26- export type VitestBrowserWorldInterface = QuickPickleWorldInterface & {
31+ export interface VitestBrowserWorldInterface extends VisualWorldInterface {
2732 /**
2833 * The `render` function must be provided by the World Constructor
2934 * and must be tailored for the framework being used. It should render
@@ -49,7 +54,7 @@ export type VitestBrowserWorldInterface = QuickPickleWorldInterface & {
4954 userEvent : UserEvent ;
5055}
5156
52- export class VitestBrowserWorld extends QuickPickleWorld implements VitestBrowserWorldInterface {
57+ export class VitestBrowserWorld extends VisualWorld implements VitestBrowserWorldInterface {
5358
5459 actions :ActionsInterface = {
5560 clicks : [ ] ,
@@ -59,7 +64,7 @@ export class VitestBrowserWorld extends QuickPickleWorld implements VitestBrowse
5964 userEvent ! : UserEvent ;
6065 async render ( name :string | any , props ?:any , renderOptions ?:any ) { } ;
6166 async cleanup ( ) { } ;
62- private _page :Locator | null = null ;
67+ _page ! :Locator | null ;
6368
6469 constructor ( context :TestContext , info :InfoConstructor ) {
6570 info = defaultsDeep ( info || { } , { config : { worldConfig : defaultVitestWorldConfig } } )
@@ -75,22 +80,6 @@ export class VitestBrowserWorld extends QuickPickleWorld implements VitestBrowse
7580 this . userEvent = browserContext . userEvent ;
7681 }
7782
78- get screenshotDir ( ) {
79- return this . sanitizePath ( this . worldConfig . screenshotDir )
80- }
81-
82- get screenshotFilename ( ) {
83- return `${ this . toString ( ) . replace ( / ^ .+ ?F e a t u r e : / , '' ) . replace ( ' ' + this . info . step , '' ) } .png`
84- }
85-
86- async screenshot ( options :{ name ?:string , locator ?:Locator } = { } ) {
87- let locator = options . locator || this . page
88- let path = options . name
89- ? this . fullPath ( `${ this . screenshotDir } /${ options . name } ${ ( this . info . explodedIdx ? ` (${ this . info . tags . join ( ',' ) } )` : '' ) } .png` . replace ( / \. p n g \. p n g $ / i, '.png' ) )
90- : this . fullPath ( `${ this . screenshotDir } /${ this . screenshotFilename } ` )
91- await locator . screenshot ( { path } )
92- }
93-
9483 get page ( ) :Locator {
9584 if ( ! this . _page ) throw new Error ( 'You must render a component before running tests.' )
9685 return this . _page
@@ -220,9 +209,75 @@ export class VitestBrowserWorld extends QuickPickleWorld implements VitestBrowse
220209 if ( toBePresent === ( matchingElements . length === 0 ) ) throw new Error ( `The${ toBeVisible ? '' : ' hidden' } element "${ locator } " was unexpectedly ${ toBePresent ? 'not present' : 'present' } .` )
221210 }
222211
212+ async screenshot ( opts ?:{
213+ bufferOnly ?:boolean
214+ name ?:string
215+ locator ?:Locator
216+ } ) :Promise < any > {
217+ let path
218+ if ( ! opts ?. bufferOnly ) path = this . getScreenshotPath ( opts ?. name )
219+ let locator = opts ?. locator ?? this . page
220+ return locator . screenshot ( { path } )
221+ }
222+
223+ async expectScreenshotMatch ( locator :Locator , filename ?:string , opts :ScreenshotComparisonOptions = { } ) {
224+
225+ const filepath = this . getScreenshotPath ( filename )
226+ let expectedImg :string
227+
228+ /**
229+ * Load existing screenshot, or save it if it doesn't yet exist
230+ */
231+ try {
232+ expectedImg = await commands . readFile ( this . getScreenshotPath ( filename ) , 'base64' )
233+ }
234+ catch ( e ) {
235+ // If the screenshot doesn't exist, save it and pass the test
236+ await locator . screenshot ( )
237+ console . warn ( `new visual regression test: ${ this . screenshotDir } /${ this . screenshotFilename } ` )
238+ return
239+ }
240+
241+ let expected = Buffer . from ( expectedImg , 'base64' )
242+
243+ /**
244+ * Get the screenshot
245+ */
246+ // type does not include the "save" option in the docs: see https://vitest.dev/guide/browser/locators#screenshot
247+ let screenshotOptions = { save :false , base64 :true } as ScreenshotOptions
248+ let actualImg = await locator . screenshot ( screenshotOptions ) as string | { base64 :string }
249+ let actual = Buffer . from ( typeof actualImg === 'string' ? actualImg : actualImg . base64 , 'base64' )
250+
251+ /**
252+ * Compare the two screenshots
253+ */
254+ let screenshotOpts = defaultsDeep ( opts , this . worldConfig . screenshotOpts )
255+ let matchResult = null
256+ try {
257+ matchResult = await this . screenshotDiff ( actual , expected , screenshotOpts )
258+ }
259+ catch ( e ) { }
260+ console . log ( {
261+ pass : matchResult ?. pass ,
262+ diffPercentage : matchResult ?. diffPercentage ,
263+ filename,
264+ locator,
265+ } . toString ( ) )
266+
267+ if ( ! matchResult ?. pass ) {
268+ await commands . writeFile ( `${ filepath } .actual.png` , actual . toString ( 'base64' ) , 'base64' ) ;
269+ if ( matchResult ?. diff ) await commands . writeFile ( `${ filepath } .diff.png` , matchResult . diff . toString ( 'base64' ) , 'base64' ) ;
270+ throw new Error ( `Screenshot does not match the snapshot.
271+ Diff percentage: ${ matchResult ?. diffPercentage ?. toFixed ( 2 ) ?? '100' } %
272+ Max allowed: ${ screenshotOpts . maxDiffPercentage } %
273+ Diffs at: ${ filepath } .(diff|actual).png` )
274+ }
275+ }
276+
223277 /**
224278 * Waits for a certain amount of time
225279 * @param ms number
280+ * @deprecated use `wait` method instead
226281 */
227282 async waitForTimeout ( ms :number ) {
228283 await new Promise ( r => setTimeout ( r , ms ) )
0 commit comments