import callsites from "callsites" ; import chalk from "chalk" ; import { existsSync } from "fs" ; import { toMatchImageSnapshot } from "jest-image-snapshot" ; import { dirname , join , sep } from "path" ; import { debugLogger } from "../logger" ; import { fetch } from "../network/fetch" ; import { Viewport } from "../screenshot-renderer/api" ; import { getScreenshotPrefix , SCREENSHOT_MODE , SCREENSHOT_SERVER_URL , } from "../screenshot-server/config" ; import { ReactComponentServer } from "./ReactComponentServer" ; const logDebug = debugLogger ( "ReactScreenshotTest" ) ; /**
 * ReactScreenshotTest is a builder for screenshot tests.
 *
 * Example usage:
 * ```
 * describe("screenshots", () => {
 *   ReactScreenshotTest.create("MyComponent")
 *       .viewports(VIEWPORTS)
 *       .shoot("with title", <MyComponent title="Hello, World!" />)
 *       .shoot("without title", <MyComponent title={null} />)
 *       .run();
 * });
 * ```
 */ export class ReactScreenshotTest { private readonly _viewports : { [ name ] : Viewport ; } = { } ; private readonly _shots : { [ name ] : React . ReactNode ; } = { } ; private readonly _remoteStylesheetUrls : string [ ] = [ ] ; private readonly _staticPaths : Record < string , string > = { } ; private ran = false ; /**
     * Creates a screenshot test.
     */ static create ( componentName ) { return new this ( componentName ) ; } private constructor ( private readonly componentName ) { setImmediate ( ( ) => { if ( ! this . ran ) { throw new Error ( "Please call .run()" ) ; } } ) ; } /**
     * Adds a set of viewports to the screenshot test.
     */ viewports ( viewports ) { for ( const [ name , viewport ] of Object . entries ( viewports ) ) { this . viewport ( name , viewport ) ; } return this ; } /**
     * Adds a single viewport to the screenshot test.
     */ viewport ( viewportName , viewport ) { if ( this . ran ) { throw new Error ( "Cannot add a viewport after running." ) ; } if ( this . _viewports [ viewportName ] ) { throw new Error ( `Viewport "${ viewportName }" is declared more than once` ) ; } this . _viewports [ viewportName ] = viewport ; return this ; } /**
     * Adds a specific shot of a component to the screenshot test.
     */ shoot ( shotName , component ) { if ( this . ran ) { throw new Error ( "Cannot add a shot after running." ) ; } if ( this . _shots [ shotName ] ) { throw new Error ( `Shot "${ shotName }" is declared more than once` ) ; } this . _shots [ shotName ] = component ; return this ; } remoteStylesheet ( stylesheetUrl ) { this . _remoteStylesheetUrls . push ( stylesheetUrl ) ; return this ; } static ( mappedPath , dirOrFilePath ) { if ( ! mappedPath . startsWith ( "/" ) ) { throw new Error ( "Directory mapping path must start with /" ) ; } if ( ! existsSync ( dirOrFilePath ) ) { throw new Error ( `Could not find path "${ dirOrFilePath }". Consider using path.resolve() to get an absolute path.` ) ; } if ( this . _staticPaths [ mappedPath ] ) { throw new Error ( "Cannot map multiple directories to the same path" ) ; } this . _staticPaths [ mappedPath ] = dirOrFilePath ; return this ; } /**
     * Runs the actual test (delegating to Jest).
     */ run ( ) { if ( this . ran ) { throw new Error ( "Cannot run more than once." ) ; } this . ran = true ; if ( Object . keys ( this . _viewports ) . length === 0 ) { throw new Error ( "Please define viewports with .viewport()" ) ; } if ( Object . keys ( this . _shots ) . length === 0 ) { throw new Error ( "Please define shots with .shoot()" ) ; } const componentServer = new ReactComponentServer ( this . _staticPaths ) ; expect . extend ( { toMatchImageSnapshot } ) ; beforeAll ( async ( ) => { await componentServer . start ( ) ; } ) ; afterAll ( async ( ) => { await componentServer . stop ( ) ; } ) ; const testFilename = callsites ( ) [ 1 ] . getFileName ( ) ! ; const snapshotsDir = dirname ( testFilename ) ; const prefix = getScreenshotPrefix ( ) ; let subdirectory = "" ; let filenamePrefix = "" ; if ( prefix . indexOf ( sep ) > - 1 ) { [ subdirectory , filenamePrefix ] = prefix . split ( sep , 2 ) ; } else { filenamePrefix = prefix ; } describe ( this . componentName , ( ) => { for ( const [ viewportName , viewport ] of Object . entries ( this . _viewports ) ) { describe ( viewportName , ( ) => { for ( const [ shotName , shot ] of Object . entries ( this . _shots ) ) { it ( shotName , async ( ) => { const name = `${ this . componentName } - ${ viewportName } - ${ shotName }` ; logDebug ( `Requesting component server to generate screenshot: ${ name }` ) ; const screenshot = await componentServer . serve ( { name , reactNode : shot , remoteStylesheetUrls : this . _remoteStylesheetUrls , } , async ( port , path ) => { const url = SCREENSHOT_MODE === "docker" && process . platform !== "linux" ? `http://host.docker.internal:${ port }${ path }` : `http://localhost:${ port }${ path }` ; return this . render ( name , url , viewport ) ; } ) ; logDebug ( `Screenshot generated.` ) ; if ( screenshot ) { logDebug ( `Comparing screenshot.` ) ; expect ( screenshot ) . toMatchImageSnapshot ( { customSnapshotsDir : join ( snapshotsDir , "__screenshots__" , this . componentName , subdirectory ) , customSnapshotIdentifier : `${ filenamePrefix }${ viewportName } - ${ shotName }` , } ) ; logDebug ( `Screenshot compared.` ) ; } else { logDebug ( `Skipping screenshot matching.` ) ; } } ) ; } } ) ; } } ) ; } private async render ( name , url , viewport ) { let response ; try { logDebug ( `Initiating request to screenshot server at ${ SCREENSHOT_SERVER_URL }.` ) ; response = await fetch ( `${ SCREENSHOT_SERVER_URL }/render` , "POST" , { name , url , viewport , } ) ; } catch ( e ) { console . error ( chalk . red ( `Unable to reach screenshot server. Please make sure that your Jest configuration contains the following:

{
  "globalSetup": "react-screenshot-test/global-setup",
  "globalTeardown": "react-screenshot-test/global-teardown"
}
` ) ) ; throw e ; } logDebug ( `Response received with status code ${ response . status }.` ) ; if ( response . status === 204 ) { return null ; } if ( response . status !== 200 ) { console . error ( chalk . red ( `Screenshot server failed to render (status ${ response . status }).

Error: ${ response . body . toString ( "utf8" ) }
` ) ) ; throw new Error ( `Received response ${ response . status } from screenshot server.` ) ; } return response . body ; } }