siesta-lite
Version:
Stress-free JavaScript unit testing and functional testing tool, works in NodeJS and browsers
379 lines (262 loc) • 12.5 kB
text/typescript
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)
}
}