UNPKG

siesta-lite

Version:

Stress-free JavaScript unit testing and functional testing tool, works in NodeJS and browsers

379 lines (262 loc) 12.5 kB
import * as types from "../../generic/simulator/Types.js"; import * as commands from "../../generic/simulator/Commands.js" import {ChildEndPoint} from "../channel/websocket/ChildEndPoint.js"; import {Point} from "../../generic/util/Point.js"; declare const Class : Function; declare const Siesta : any; const FormatHelper = Class({ does : Siesta.Util.Role.CanFormatStrings }) const formatStringHelper = new FormatHelper() export class RemoteSimulatorClient extends ChildEndPoint implements types.SimulatorClient { type : string = 'native' syntheticSimulator : types.SimulatorClient syntheticSimulatorClass : Function syntheticSimulatorConfig : any currentTest : any commandTimeout : number = 15000; currentPosition : Point = [ 0, 0 ]; // delta between screen position and browser viewport position - this is set on the harness page level screenDeltaX : number = 0; screenDeltaY : number = 0; // additional offset - set on the individual test level offsetX : number = 0; offsetY : number = 0; onSocketCloseListener : Function lastClickFromTest : number lastClickTime : number lastClickPoint : Point antiClickMergeDelay : number = 500 states : Object = {} constructor (props : Partial<RemoteSimulatorClient>) { super(props) this.states[ types.PointerModifier.Left ] = types.PointerState.Up this.states[ types.PointerModifier.Middle ] = types.PointerState.Up this.states[ types.PointerModifier.Right ] = types.PointerState.Up // aka Object.assign(this, props) for (let i in props) if (props.hasOwnProperty(i)) this[ i ] = props [ i ] } onTestLaunch (test) { this.adjustToElement(test.global.frameElement) this.lastClickTime = null this.lastClickFromTest = null this.lastClickPoint = null this.syntheticSimulatorClass = test.getSimulatorClass() this.syntheticSimulatorConfig = test.simulatorConfig this.currentTest = test } cleanup () { this.syntheticSimulator = null this.syntheticSimulatorConfig = null this.syntheticSimulatorClass = null this.currentTest = null } getSyntheticSimulator () : types.SimulatorClient { if (this.syntheticSimulator) return this.syntheticSimulator this.syntheticSimulator = new (<any>this.syntheticSimulatorClass)(this.syntheticSimulatorConfig || {}) this.syntheticSimulator.onTestLaunch(this.currentTest) return this.syntheticSimulator } setup () : Promise<any> { return Promise.resolve() } onSocketOpen (event : object) { } onSocketClose (event : object) { super.onSocketClose(event) this.onSocketCloseListener && this.onSocketCloseListener(event) } adjustToElement (el : HTMLElement) { if (!el) { this.offsetX = this.offsetY = 0 return } const rect = el.getBoundingClientRect() this.offsetX = rect.left this.offsetY = rect.top } translateX (x : number) { return Math.max(0, x + this.screenDeltaX + this.offsetX) } translateY (y : number) { return Math.max(0, y + this.screenDeltaY + this.offsetY) } // calculate mouse move precision, based on the value for synthetic calculateMouseMovePrecision () : number { var config = this.syntheticSimulatorConfig if (config) { // "speedRun" mode if (config.mouseMovePrecision == 1 && config.pathBatchSize == 30) return 3 return config.mouseMovePrecision } else return null } simulateMouseMove (x : number, y : number, options, params : any) : Promise<any> { // we don't use default value for the argument because it is sometimes provided // as `undefiend` or `null` from the outer code if (!params) params = {} return this.sendRpcCall(<commands.MovePointer>{ type : 'move_pointer', x : this.translateX(x), y : this.translateY(y), moveKind : params.moveKind || "smooth", mouseMovePrecision : params.mouseMovePrecision || this.calculateMouseMovePrecision(), modifierKey : this.optionsToModifierKeys(options) }, 2 * this.commandTimeout).then(result => { this.currentPosition[ 0 ] = x this.currentPosition[ 1 ] = y return result }) } simulateMouseDown (clickInfo, options) : Promise<any> { const cont = this.getAntiClickMergePromise(clickInfo, options) return cont.then(() => { return this.sendRpcCall(<commands.SetPointerState>{ type : 'set_pointer_state', state : types.PointerState.Down, modifier : types.PointerModifier.Left, modifierKey : this.optionsToModifierKeys(options) }, this.commandTimeout).then(result => { this.states[ types.PointerModifier.Left ] = types.PointerState.Down return result }) }) } simulateMouseUp (clickInfo, options) : Promise<any> { const cont = this.getAntiClickMergePromise(clickInfo, options) return cont.then(() => { return this.sendRpcCall(<commands.SetPointerState>{ type : 'set_pointer_state', state : types.PointerState.Up, modifier : types.PointerModifier.Left, modifierKey : this.optionsToModifierKeys(options) }, this.commandTimeout).then(result => { this.states[ types.PointerModifier.Left ] = types.PointerState.Up return result }) }) } // TODO remove the `clickInfo` arg? which was used to apply additional condition, that // action happened at the same point // which seems not to be enough in certain cases and needs to be removed getAntiClickMergePromise (clickInfo, options = <any>{}) : Promise<any> { const clickFrom = options.testUniqueId let cont // this construct is solving "click merge" problem in native simulation, // where 2 consequent clicks in the same point (like "t.click()") // will be treated by browser like double click // that double click will select text in the input for example and something that // does not happen in synthetic simulation (which users are used to) // the heuristic is: // if the click happens less than 1000 ms since the last click // REMOVED: and its on the same point // and its from different test (or different subtest of the same test file) if ( this.lastClickTime && clickFrom != null && (new Date().getTime() - this.lastClickTime) < 1000 && clickFrom != this.lastClickFromTest // && clickInfo.globalXY[ 0 ] == this.lastClickPoint[ 0 ] // && clickInfo.globalXY[ 1 ] == this.lastClickPoint[ 1 ] ) { //console.log("Delaying click") cont = new Promise(resolve => setTimeout(() => resolve(), this.antiClickMergeDelay)) } else { cont = Promise.resolve() } this.lastClickTime = new Date().getTime() this.lastClickFromTest = clickFrom // this.lastClickPoint = clickInfo.globalXY.slice() //console.log("click time ", this.lastClickTime, " click test id ", this.lastClickFromTest, " click point ", this.lastClickPoint) return cont } simulateMouseClick (clickInfo, options = <any>{}) : Promise<any> { const cont = this.getAntiClickMergePromise(clickInfo, options) // not possible to click on <option> (at least easily) since <option> element // has no "getBoundingClientRect" and we don't know where to click // fallback to synthetic for backward compat if (clickInfo.el && clickInfo.el.tagName.toLowerCase() == 'option') { return this.getSyntheticSimulator().simulateMouseClick(clickInfo, options) } else return cont.then(() => { return this.sendRpcCall(<commands.PointerClick>{ type : 'pointer_click', modifier : types.PointerModifier.Left, modifierKey : this.optionsToModifierKeys(options), }, this.commandTimeout) }) } simulateRightClick (clickInfo, options) : Promise<any> { const cont = this.getAntiClickMergePromise(clickInfo, options) return cont.then(() => { return this.sendRpcCall(<commands.PointerClick>{ type : 'pointer_click', modifier : types.PointerModifier.Right, modifierKey : this.optionsToModifierKeys(options) }, this.commandTimeout) }) } simulateDoubleClick (clickInfo, options) : Promise<any> { const cont = this.getAntiClickMergePromise(clickInfo, options) return cont.then(() => { return this.sendRpcCall(<commands.PointerDoubleClick>{ type : 'pointer_double_click', modifier : types.PointerModifier.Left, modifierKey : this.optionsToModifierKeys(options) }, this.commandTimeout) }) } simulateDrag (sourceXY, targetXY, options, dragOnly) : Promise<any> { const cont = this.getAntiClickMergePromise(undefined, options) return cont.then(() => { return this.sendRpcCall(<commands.PointerDrag>{ type : 'pointer_drag', modifier : types.PointerModifier.Left, fromX : this.translateX(sourceXY[ 0 ]), fromY : this.translateY(sourceXY[ 1 ]), toX : this.translateX(targetXY[ 0 ]), toY : this.translateY(targetXY[ 1 ]), dragOnly : dragOnly, modifierKey : this.optionsToModifierKeys(options) }, 2 * this.commandTimeout).then(() => { this.currentPosition[ 0 ] = targetXY[ 0 ] this.currentPosition[ 1 ] = targetXY[ 1 ] this.states[ types.PointerModifier.Left ] = dragOnly ? types.PointerState.Up : types.PointerState.Down }) }) } simulateMouseWheel (clickInfo, options) : Promise<any> { return this.sendRpcCall(<commands.MouseWheel>{ type : 'mouse_wheel', deltaX : options.deltaX || 0, deltaY : options.deltaY || 0, modifierKey : this.optionsToModifierKeys(options) }, this.commandTimeout) } optionsToModifierKeys (options: any) : types.ModifierKey[] { if (!options) return [] let modifiers : types.ModifierKey[] = [] if (options.shiftKey) modifiers.push('SHIFT') if (options.ctrlKey) modifiers.push('CTRL') if (options.altKey) modifiers.push('ALT') // is this correct? if (options.metaKey) modifiers.push('CMD') return modifiers } // proxy method to be available in the subclasses extractKeysAndSpecialKeys (text : string) : string[] { return formatStringHelper.extractKeysAndSpecialKeys(text).map(key => { if (key.length == 1) return key return key.substring(1, key.length - 1) }) } simulateType (text, options, params) : Promise<any> { return this.sendRpcCall(<commands.Type>{ type : 'type', text : this.extractKeysAndSpecialKeys(text), modifierKey : this.optionsToModifierKeys(options) }, this.commandTimeout) } doFullSimulationReset () : Promise<any> { return this.sendRpcCall(<commands.Reset>{ type : 'reset' }, this.commandTimeout) } }