import { NodeVM } from 'vm2' ; import Faker from 'faker' ; import nodeAssert from 'assert' ; import { EventEmitter } from 'events' ; import fs from 'fs' ; import { promisify } from 'util' ; const exists = promisify ( fs . exists ) ; import { Step , TestFn , StepOptions , normalizeStepOptions , ConditionFn , StepExtended , StepRecoveryObject , RecoverWith , extractStep , } from './Step' ; import { Browser } from '../interface/IBrowser' ; import Test from './Test' ; import { mustCompileFile } from '../TestScript' ; import { TestScriptError , TestScriptErrorMapper , expect } from '@flood/element-report' ; import { ITestScript } from '../interface/ITestScript' ; import { DEFAULT_SETTINGS , TestSettings } from './Settings' ; import { RuntimeEnvironment } from '../runtime-environment/types' ; import { Until } from '../page/Until' ; import { By } from '../page/By' ; import { BaseLocator } from '../page/Locator' ; import { MouseButtons , Key , userAgents } from '../page/Enums' ; import { Device } from '../page/Device' ; import { TestDataSource , TestDataFactory } from '../test-data/TestData' ; import { BoundTestDataLoaders } from '../test-data/TestDataLoaders' ; import { EvaluatedScriptLike } from './EvaluatedScriptLike' ; import { Hook , HookType , normalizeHookBase } from './StepLifeCycle' ; const debug = require ( 'debug' ) ( 'element:runtime:eval-script' ) ; function createVirtualMachine ( floodElementActual , root ? ) { const vm = new NodeVM ( { console : 'redirect' , sandbox : { } , wrapper : 'commonjs' , sourceExtensions : [ 'js' , 'ts' ] , require : { builtin : [ '*' ] , external : true , context : 'sandbox' , root , mock : { '@flood/element' : floodElementActual , faker : Faker , assert : nodeAssert , } , } , } ) ; vm . sandbox . process . env = process . env ; return vm ; } export class EvaluatedScript implements TestScriptErrorMapper , EvaluatedScriptLike { public steps : Step [ ] ; public hook : Hook ; public recoverySteps : StepRecoveryObject ; public settings : TestSettings ; private vm : NodeVM ; constructor ( public runEnv , public script , lazyEval = false ) { if ( ! lazyEval ) { this . evaluate ( ) ; } } public static async mustCompileFile ( path , runEnv ) { if ( ! ( await exists ( path ) ) ) { throw new Error ( `unable to compile script: no script found at path ${ path }` ) ; } return new EvaluatedScript ( runEnv , await mustCompileFile ( path ) ) ; } public isScriptError ( error ) { return this . script . isScriptError ?. apply ( this , error ) ?? false ; } public maybeLiftError ( error ) { return this . script ?. maybeLiftError ?. apply ( this , error ) ?? false ; } public liftError ( error ) { return this . script . liftError ! ( error ) ; } public filterAndUnMapStack ( stack ) { return this . script ?. filterAndUnMapStack ?. apply ( this , stack ) ; } public bindTest ( test ) { if ( this . vm === undefined ) throw new Error ( 'bindTest: no vm created - script must be evaluated first' ) ; const { reporter } = test ; const eevm = this . vm as any as EventEmitter ; [ 'log' , 'info' , 'error' , 'dir' , 'trace' ] . forEach ( ( key ) => eevm . on ( `console.${ key }` , ( message , ... args ) => reporter . testScriptConsole ( key , message , ... args ) ) ) ; } public async beforeTestRun ( ) { if ( this . testData ) await this . testData . load ( ) ; } private _testDataLoaders : TestDataFactory | undefined ; public get testDataLoaders ( ) : TestDataFactory { if ( this . _testDataLoaders === undefined ) { this . _testDataLoaders = new BoundTestDataLoaders ( this , this . runEnv . workRoot ) ; } return this . _testDataLoaders ; } private _testData : TestDataSource < any > ; public set testData ( testDataSource ) { this . _testData = testDataSource ; this . _testData . setInstanceID ( this . sequence . toString ( ) ) ; } public get testData ( ) : TestDataSource < any > { return this . _testData ; } public get sequence ( ) : number { return this . runEnv . stepEnv ( ) . SEQUENCE ; } public evaluate ( ) { debug ( 'evaluating' ) ; const steps = [ ] ; const recoverySteps = { } ; const hook = { afterAll : [ ] , afterEach : [ ] , beforeAll : [ ] , beforeEach : [ ] , } ; const rawSettings = DEFAULT_SETTINGS ; const ENV = this . runEnv . stepEnv ( ) ; function captureStep ( args ) { let name ; let fn ; let options = { } ; if ( args . length === 3 ) { ; [ name , options , fn ] = args ; options = normalizeStepOptions ( options ) ; } else { ; [ name , fn ] = args ; } console . assert ( typeof name === 'string' , 'Step name must be a string' ) ; if ( steps . find ( ( { name : stepName } ) => name === stepName ) ) { console . warn ( `Duplicate step name: ${ name }, skipping step` ) ; return ; } steps . push ( { fn , name , options } ) ; } const afterAll = ( ... args ) => { const [ fn , waitTimeout ] = args ; const hookBase = normalizeHookBase ( { fn , waitTimeout , type : HookType . afterAll } ) ; hook . afterAll . push ( hookBase ) ; } ; const afterEach = ( ... args ) => { const [ fn , waitTimeout ] = args ; const hookBase = normalizeHookBase ( { fn , waitTimeout , type : HookType . afterEach } ) ; hook . afterEach . push ( hookBase ) ; } ; const beforeAll = ( ... args ) => { const [ fn , waitTimeout ] = args ; const hookBase = normalizeHookBase ( { fn , waitTimeout , type : HookType . beforeAll } ) ; hook . beforeAll . push ( hookBase ) ; } ; const beforeEach = ( ... args ) => { const [ fn , waitTimeout ] = args ; const hookBase = normalizeHookBase ( { fn , waitTimeout , type : HookType . beforeEach } ) ; hook . beforeEach . push ( hookBase ) ; } ; const step = ( ... nameOrOptionsOrFn ) => { const [ name , option , fn ] = extractStep ( nameOrOptionsOrFn ) ; captureStep ( [ name , option , fn ] ) ; } ; step . once = ( ... nameOrOptionsOrFn ) => { const [ name , option , fn ] = extractStep ( nameOrOptionsOrFn ) ; captureStep ( [ name , { ... option , once : true } , fn ] ) ; } ; step . if = async ( conditionFn , ... nameOrOptionsOrFn ) => { const [ name , option , fn ] = extractStep ( nameOrOptionsOrFn ) ; captureStep ( [ name , { ... option , predicate : conditionFn } , fn ] ) ; } ; step . unless = async ( conditionFn , ... nameOrOptionsOrFn ) => { const [ name , option , fn ] = extractStep ( nameOrOptionsOrFn ) ; captureStep ( [ name , { ... option , predicate : ( browser ) => Promise . resolve ( conditionFn ( browser ) ) . then ( ( result ) => ! result ) , } , fn , ] ) ; } ; step . skip = async ( ... nameOrOptionsOrFn ) => { const [ name , option , fn ] = extractStep ( nameOrOptionsOrFn ) ; captureStep ( [ name , { ... option , skip : true } , fn ] ) ; } ; step . repeat = async ( repeatVal , ... nameOrOptionsOrFn ) => { const [ name , option , fn ] = extractStep ( nameOrOptionsOrFn ) ; const repeat = repeatVal < 0 ? 0 : repeatVal ; captureStep ( [ name , { ... option , repeat : { count : repeat , iteration : 0 , } , } , fn , ] ) ; } ; step . while = async ( condition , ... nameOrOptionsOrFn ) => { const [ name , option , fn ] = extractStep ( nameOrOptionsOrFn ) ; captureStep ( [ name , { ... option , stepWhile : { predicate : condition , } , } , fn , ] ) ; } ; step . recovery = async ( ... nameOrOptionsOrFn ) => { const [ name , options , fn ] = extractStep ( nameOrOptionsOrFn ) ; if ( name === 'global' && recoverySteps [ name ] ) throw Error ( 'Global recovery step called with too many times' ) ; recoverySteps [ name ] = { recoveryStep : { name , options , fn } , loopCount : options . tries || 0 , iteration : 0 , } ; } ; const context = { setup : ( setupSettings ) => { Object . assign ( rawSettings , setupSettings ) ; } , ENV , step , afterAll , afterEach , beforeAll , beforeEach , By , Until , BaseLocator , Device , MouseButtons , TestData : this . testDataLoaders , Key , RecoverWith , userAgents , } ; this . vm = createVirtualMachine ( context , this . script . scriptRoot ) ; const result = this . vm . run ( this . script . vmScript ) ; debug ( 'eval %O' , result ) ; const testFn = expect ( result . default , 'Test script must export a default function' ) ; /**
         * Evaluate default function
         */ testFn . apply ( null , [ ] ) ; const scriptSettings = result . settings ; if ( scriptSettings ) { this . settings = { ... scriptSettings } ; } this . settings = { ... this . settings , name : this . script . testName , description : this . script . testDescription , } ; this . steps = steps ; this . recoverySteps = recoverySteps ; this . hook = hook ; debug ( 'settings' , this . settings ) ; debug ( 'steps' , this . steps ) ; return this ; } }