import { StorageManagerInterface } from "@crusher-shared/lib/storage/interface" ; import { ActionStatusEnum , IRunnerLogManagerInterface , IRunnerLogStepMeta } from "@crusher-shared/lib/runnerLog/interface" ; import { iAction , iActionResult } from "@crusher-shared/types/action" ; import { Browser , Page } from "playwright" ; import { LogManager } from "./functions/log" ; import { StorageManager } from "./functions/storage" ; import { waitForSelectors } from "./functions/waitForSelectors" ; import { ActionsInTestEnum } from "@crusher-shared/constants/recordedActions" ; import { handlePopup } from "./middlewares/popup" ; import { getBrowserActions , getMainActions , isWebpack , toCrusherSelectorsFormat , uuidv4 , validActionTypeRegex } from "./utils/helper" ; import { IGlobalManager } from "@crusher-shared/lib/globals/interface" ; import * as fs from "fs" ; import * as path from "path" ; import { sleep } from "./functions" ; import { CrusherSdk } from "./sdk/sdk" ; import { ExportsManager } from "./functions/exports" ; import { IExportsManager } from "@crusher-shared/lib/exports/interface" ; type IActionCategory = "PAGE" | "BROWSER" | "ELEMENT" ; export enum ActionCategoryEnum { PAGE = "PAGE" , BROWSER = "BROWSER" , ELEMENT = "ELEMENT" } const TEST_RESULT_KEY = "TEST_RESULT" ; type OmitFirstArg < F > = F extends ( x : any , ... args : infer P ) => infer R ? ( ... args : P ) => R : never ; export type ActionStatusCallbackFn = ( action : iAction , result : iActionResult ) => Promise < number | void | boolean > ; class CrusherRunnerActions { actionHandlers : { [ type ] : any ; } ; logManager : LogManager ; storageManager : StorageManager ; globals : IGlobalManager ; exportsManager : ExportsManager ; sdk : CrusherSdk | null ; constructor ( logManger , storageManager , baseAssetPath , globalManager , exportsManager , sdk = null ) { this . actionHandlers = { } ; this . globals = globalManager ; this . logManager = new LogManager ( logManger ) ; this . storageManager = new StorageManager ( storageManager , baseAssetPath ) ; this . exportsManager = new ExportsManager ( exportsManager ) ; this . sdk = sdk ; if ( ! this . globals . has ( TEST_RESULT_KEY ) ) { this . globals . set ( TEST_RESULT_KEY , [ ] ) ; } this . initActionHandlers ( ) ; } initActionHandlers ( ) { if ( isWebpack ( ) ) { const actionsRequireContext = require . context ( "./actions/" , true , /\.ts$/ ) ; actionsRequireContext . keys ( ) . forEach ( ( fileName ) => { const { name , description , handler } = actionsRequireContext ( fileName ) ; this . registerStepHandler ( name , description , handler ) ; } ) ; } else { const actionsDir = fs . readdirSync ( path . join ( __dirname , "./actions" ) ) ; for ( let actionFilePath of actionsDir ) { const { name , description , handler } = require ( path . join ( __dirname , "./actions" , actionFilePath ) ) ; this . registerStepHandler ( name , description , handler ) ; } } } async handleActionExecutionStatus ( actionType , status , message = "" , meta = { } , actionCallback = null ) { await this . logManager . logStep ( actionType , status , message , meta ) ; if ( actionCallback ) await actionCallback ( { actionType , status , message , meta } ) ; if ( status === ActionStatusEnum . COMPLETED || status === ActionStatusEnum . FAILED ) { this . globals . get ( TEST_RESULT_KEY ) . push ( { actionType , status , message , meta } ) ; } } async _getCurrentScreenshot ( page ) { try { const currentScreenshotBuffer = await page . screenshot ( ) ; const currentScreenshotUrl = await this . storageManager . uploadAsset ( `${ uuidv4 ( ) }.png` , currentScreenshotBuffer ) ; return currentScreenshotUrl ; } catch ( err ) { return null ; } } stepHandlerHOC ( wrappedHandler , action ) { return async ( step , browser , page = null , actionCallback = null , shouldSleepAfterComplete = true , remainingActionsArr = [ ] , shouldLog = true ) : Promise < void > => { let startingScreenshot = null ; let stepResult = null ; if ( shouldLog ) { await this . handleActionExecutionStatus ( action . name , ActionStatusEnum . STARTED , `Performing ${ action . description } now` , { actionName : step . name ? step . name : null , } , actionCallback ) ; try { startingScreenshot = await this . _getCurrentScreenshot ( page ) ; } catch ( ex ) { } } const beforeUrl = page ? await page . url ( ) : null ; try { switch ( action . category ) { case ActionCategoryEnum . PAGE : stepResult = await wrappedHandler ( page , step , this . globals , this . storageManager , this . exportsManager , this . sdk , browser , this . runActions . bind ( this ) ) ; break ; case ActionCategoryEnum . BROWSER : stepResult = await wrappedHandler ( browser , step , this . globals , this . storageManager , this . exportsManager , this . sdk ) ; break ; case ActionCategoryEnum . ELEMENT : const crusherSelector = toCrusherSelectorsFormat ( step . payload . selectors ) ; const elementLocator = page . locator ( crusherSelector . value ) ; stepResult = await wrappedHandler ( elementLocator . first ( ) , null , step , this . globals , this . storageManager , this . exportsManager , this . sdk ) ; break ; default : throw new Error ( "Invalid action category handler" ) ; } if ( shouldSleepAfterComplete ) { await sleep ( 500 ) ; } if ( shouldLog ) { await this . handleActionExecutionStatus ( action . name , ActionStatusEnum . COMPLETED , stepResult && stepResult . customLogMessage ? stepResult . customlogMessage : `Finished performing ${ action . description }` , stepResult ? { ... stepResult , actionName : step . name ? step . name : null , beforeUrl : beforeUrl , afterUrl : page ? await page . url ( ) : null , meta : { ... stepResult , } , } : { actionName : step . name ? step . name : null , beforeUrl : beforeUrl , afterUrl : page ? await page . url ( ) : null , } , actionCallback ) ; } } catch ( err ) { if ( shouldLog ) { let endingScreenshot = null ; try { endingScreenshot = await this . _getCurrentScreenshot ( page ) ; } catch ( ex ) { } await this . handleActionExecutionStatus ( action . name , ActionStatusEnum . FAILED , `Error performing ${ action . description }` , { failedReason : err . messsage , screenshotDuringError : JSON . stringify ( { startingScreenshot , endingScreenshot } ) , actionName : step . name ? step . name : null , beforeUrl : beforeUrl , afterUrl : page ? await page . url ( ) : null , meta : { ... ( err . meta ? err . meta : { } ) , remainingActionsArr : [ ... remainingActionsArr ] , } , } , actionCallback ) ; } if ( ! step . payload . isOptional ) { throw err ; } } } ; } registerStepHandler ( actionType , description , handler ) { const validActionRegexMatches = validActionTypeRegex . exec ( actionType ) ; if ( ! validActionRegexMatches ) throw new Error ( "Invalid format for action type" + actionType ) ; const actionCategory = validActionRegexMatches [ 1 ] as any ; this . actionHandlers [ actionType ] = this . stepHandlerHOC ( handler , { name : actionType , description : description , category : actionCategory } ) ; } async runActions ( actions , browser , page = null , actionCallback = null , shouldLog = true ) { let index = 0 ; const remainingActionsArr = [ ... actions ] ; for ( let action of actions ) { remainingActionsArr . shift ( ) ; if ( ! this . actionHandlers [ action . type ] ) throw new Error ( "No handler for this action type" ) ; await this . actionHandlers [ action . type ] ( action , browser , page , actionCallback ? actionCallback . bind ( this , action ) : null , actions [ index + 1 ] ? ( actions [ index + 1 ] . type !== ActionsInTestEnum . WAIT_FOR_NAVIGATION ? true : false ) : false , remainingActionsArr , shouldLog ) ; index ++ ; } } getStepHandlers ( ) { return this . actionHandlers ; } } export { CrusherRunnerActions , handlePopup , getBrowserActions , getMainActions , CrusherSdk } ;