UNPKG

siesta-lite

Version:

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

457 lines (352 loc) 14.7 kB
import * as point from '../../../generic/util/Point' import * as types from '../../../generic/simulator/Types' import {ServerEndPoint} from "../../channel/websocket/ServerEndPoint.js"; import {Channel, Envelop, Message} from "../../../generic/channel/Types.js"; import {SimulatorCommand} from "../../../generic/simulator/Commands.js"; import * as commands from "../../../generic/simulator/Commands.js"; import {filterPathAccordingToPrecision} from "../../../generic/util/Point.js"; import {delay} from "../../../generic/util/helper/Delay.js"; let robotjs let noRobot = false try { robotjs = require('robotjs') // this can probably be reduced on Linux (or even set to 0) // once https://github.com/octalmage/robotjs/issues/347 gets fixed robotjs.setMouseDelay(process.platform == 'win32' ? 10 : 10) robotjs.setKeyboardDelay(process.platform == 'win32' ? 20 : 20) } catch (e) { noRobot = true } export class SimulatorServerRobotJs extends ServerEndPoint { displayNumber : string mouseMovePrecision : number = 3 // see https://github.com/octalmage/robotjs/issues/347 afterActionDelay : number = 100 setXDisplayName (displayNumber : string) { this.displayNumber = displayNumber robotjs.setXDisplayName(':' + displayNumber + '.0') } connect(channel : Channel) : Promise<any> { if ((process.platform == 'linux' || process.platform == 'freebsd')) { // running on Linux w/o X11 server? // no point to even setup the WebSocket server - trying to call any RobotJS method, like `moveMouse` below // will segfault if (!process.env.DISPLAY || noRobot) return Promise.resolve() if (this.displayNumber) this.setXDisplayName(this.displayNumber) } // move mouse left-top corner robotjs.moveMouse(0, 0) return super.connect(channel) } doDispatchEnvelop (envelop : Envelop) { const cmd = envelop.payload as SimulatorCommand let action switch (cmd.type) { case 'calibrate' : action = this.doCalibrate(<commands.Calibrate>cmd); break case 'move_pointer' : action = this.doMovePointer(<commands.MovePointer>cmd); break case 'set_pointer_state' : action = this.doSetPointerState(<commands.SetPointerState>cmd); break case 'pointer_click' : action = this.doPointerClick(<commands.PointerClick>cmd); break case 'pointer_double_click' : action = this.doPointerDoubleClick(<commands.PointerDoubleClick>cmd); break case 'mouse_wheel' : action = this.doMouseWheel(<commands.MouseWheel>cmd); break case 'pointer_drag' : action = this.doPointerDrag(<commands.PointerDrag>cmd); break case 'type' : action = this.doType(<commands.Type>cmd); break case 'reset' : action = this.doSimulationReset(<commands.Reset>cmd); break case 'set_key_state' : action = this.doSetKeyState(<commands.SetKeyState>cmd); break } action.then(result => this.replyWith(envelop, result), reason => { this.info("Simulator command failed: " + reason) this.replyWith(envelop, reason, true) }) } doCalibrate (command : commands.Calibrate) : Promise<any> { const testPointX = Math.round(command.left + command.width / 2) const testPointY = Math.round(command.top + command.height / 2) robotjs.moveMouse(0, 0) return delay(100).then(() => { robotjs.moveMouse(testPointX, testPointY) return delay(100) }).then(() => { return Promise.resolve({ x : testPointX, y : testPointY }) }) } doMovePointer (command : commands.MovePointer) : Promise<any> { this.doWithModifierKeys(() => { if (command.moveKind == 'instant') robotjs.moveMouse(command.x, command.y) else if (command.moveKind == 'smooth') { this.moveMouseTo(command.x, command.y, command.mouseMovePrecision) } }, command.modifierKey) return Promise.resolve() } doSetKeyState (command : commands.SetKeyState) : Promise<any> { robotjs.keyToggle(command.key, command.state) return delay(this.afterActionDelay) } doSetPointerState (command : commands.SetPointerState) : Promise<any> { this.doWithModifierKeys(() => { robotjs.mouseToggle(command.state, command.modifier) }, command.modifierKey) return delay(this.afterActionDelay) } doPointerClick (command : commands.PointerClick) : Promise<any> { this.doWithModifierKeys(() => { robotjs.mouseClick(command.modifier, false) }, command.modifierKey) return delay(this.afterActionDelay) } doPointerDoubleClick (command : commands.PointerDoubleClick) : Promise<any> { this.doWithModifierKeys(() => { robotjs.mouseClick(command.modifier, true) }, command.modifierKey) return delay(this.afterActionDelay) } // not used currently but might be useful doInBatches (processor : Function, input: any[], batchSize : number, delayAfterBatch : number) : Promise<any> { if (input.length === 0) return Promise.resolve() for (let i = 0; i < batchSize; i++) { if (input.length === 0) break processor(input.shift()) } return delay(delayAfterBatch).then(() => this.doInBatches(processor, input, batchSize, delayAfterBatch)) } doSimulationReset (command : commands.Reset) : Promise<any> { '`1234567890-=qwertyuiop[]\\asdfghjkl;\'zxcvbnm,./ '.split('').concat( Object.keys(SpecialKey).filter( key => isNaN(Number(key)) && !key.match(/^lights_/) // fails on linux && !key.match(/^numpad_/) // fails on linux && !key.match(/^audio_/) // fails on linux && !(process.platform == 'darwin' && (key == 'printscreen' || key == 'insert')) ) ).forEach(key => { // console.log("Key up: " + key) robotjs.keyToggle(key, types.KeyState.Up) }) if (process.platform != 'win32') for (let button in types.PointerModifier) { if (isNaN(Number(button))) { robotjs.mouseToggle(types.PointerState.Up, types.PointerModifier[ button ]) } } robotjs.moveMouse(0, 0) return delay(this.afterActionDelay) } doMouseWheel (command : commands.MouseWheel) : Promise<any> { this.doWithModifierKeys(() => { // https://github.com/octalmage/robotjs/issues/318 // https://github.com/octalmage/robotjs/issues/303 ?? robotjs.scrollMouse(command.deltaX, 0) }, command.modifierKey) return delay(this.afterActionDelay) } doPointerDrag (command : commands.PointerDrag) : Promise<any> { this.doWithModifierKeys(() => { robotjs.moveMouse(command.fromX, command.fromY) robotjs.mouseToggle(types.PointerState.Down, command.modifier) this.moveMouseTo(command.toX, command.toY, 1) if (!command.dragOnly) robotjs.mouseToggle(types.PointerState.Up, command.modifier) }, command.modifierKey) return Promise.resolve() } doWithModifierKeys (action : Function, modifierKeys : types.ModifierKey[] = []) { this.toggleModifierKeys(modifierKeys, types.KeyState.Down) action() this.toggleModifierKeys(modifierKeys, types.KeyState.Up) } toggleModifierKeys (modifierKeys : types.ModifierKey[] = [], state : types.KeyState) { modifierKeys.forEach(modKey => robotjs.keyToggle(this.siestaKeyToSimulatorKey(modKey), state)) } siestaKeyToSimulatorKey (key : string) : string { if (key.length == 1) return key return siestaToRobotJsKeys[ key ] } doType (command : commands.Type) : Promise<any> { const text = command.text for (let i = 0; i < text.length; i++) { this.typeSingleChar(this.siestaKeyToSimulatorKey(text[ i ]), command.modifierKey) } // this setTimeout is required, because for some reason when typing just "delete" in the input field // the callback is called, but the delete has not been processed yet // (the it=`keyPress` method should support special chars like [SMTH]` in keyevents/043_special_keys.t.js // created a ticket for robotjs: https://github.com/octalmage/robotjs/issues/347 but project is very poorly maintained.. return delay(2 * this.afterActionDelay) } // `key` is a robotjs-recognizable key typeSingleChar (key : string, modifierKey : types.ModifierKey[] = []) { if (key.length > 1) // special key - we just tap it // see also comment below this.doWithModifierKeys(() => { robotjs.keyTap(key) }, modifierKey) else { // a single char let needShift = false if (key == '!') { key = "1"; needShift = true } if (key == '@') { key = "2"; needShift = true } if (key == '#') { key = "3"; needShift = true } if (key == '$') { key = "4"; needShift = true } if (key == '%') { key = "5"; needShift = true } if (key == '^') { key = "6"; needShift = true } if (key == '&') { key = "7"; needShift = true } if (key == '*') { key = "8"; needShift = true } if (key == '(') { key = "9"; needShift = true } if (key == ')') { key = "0"; needShift = true } if (key == '_') { key = "-"; needShift = true } if (key == '+') { key = "="; needShift = true } if (key == '{') { key = "["; needShift = true } if (key == '}') { key = "]"; needShift = true } if (key == '|') { key = "\\"; needShift = true } if (key == ':') { key = ";"; needShift = true } if (key == '"') { key = "'"; needShift = true } if (key == '<') { key = ","; needShift = true } if (key == '>') { key = "."; needShift = true } if (key == '?') { key = "/"; needShift = true } // the `keyTap` method of robotjs support modifiers array, but we can't use it, because // lets say for CTRL + A (keyTap('a', [ 'control' ]), the order of events will be: // keydown CTRL / keydown a / keyup CTRL / keyup a // which is obviously wrong, at least for us, we want: // keydown CTRL / keydown a / keyup a / keyup CTRL // so we use our "doWithModifierKeys" wrapper this.doWithModifierKeys(() => { robotjs.keyTap(key) }, needShift || key != key.toLowerCase() ? [ <types.ModifierKey>'SHIFT' ].concat(modifierKey) : modifierKey) } } // Of course, the "moveMouseSmooth" method in RobotJS is a total mess: // https://github.com/octalmage/robotjs/issues/238 // we have to introduce our own moveMouseTo (x : number, y : number, mouseMovePrecision : number) { var currentPos = robotjs.getMousePos() var path = point.getPathBetweenPoints([ currentPos.x, currentPos.y ], [ x, y ]) path = filterPathAccordingToPrecision(path, mouseMovePrecision || this.mouseMovePrecision) for (var i = 0; i < path.length; i++) { var step = path[ i ] robotjs.moveMouse(step[ 0 ], step[ 1 ]) } } } export enum SpecialKey { 'backspace', 'delete', 'enter', 'tab', 'escape', 'up', 'down', 'right', 'left', 'home', 'end', 'pageup', 'pagedown', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12', 'command', 'alt', 'control', 'shift', 'right_shift', 'space', 'printscreen', 'insert', 'audio_mute', 'audio_vol_down', 'audio_vol_up', 'audio_play', 'audio_stop', 'audio_pause', 'audio_prev', 'audio_next', 'audio_rewind', 'audio_forward', 'audio_repeat', 'audio_random', 'numpad_0', 'numpad_1', 'numpad_2', 'numpad_3', 'numpad_4', 'numpad_5', 'numpad_6', 'numpad_7', 'numpad_8', 'numpad_9', 'lights_mon_up', 'lights_mon_down', 'lights_kbd_toggle', 'lights_kbd_up', 'lights_kbd_down' } const siestaToRobotJsKeys = { 'BACKSPACE' : 'backspace', 'TAB' : 'tab', 'RETURN' : 'enter', 'ENTER' : 'enter', //special 'SHIFT' : 'shift', 'CTRL' : 'control', 'ALT' : 'alt', 'CMD' : 'command', // Mac //weird 'PAUSE-BREAK' : null, 'CAPS' : null, 'ESCAPE' : 'escape', 'ESC' : 'escape', 'NUM-LOCK' : null, 'SCROLL-LOCK' : null, 'PRINT' : 'printscreen', // No Mac support //navigation 'PAGE-UP' : 'pageup', 'PAGE-DOWN' : 'pagedown', 'PAGEUP' : 'pageup', 'PAGEDOWN' : 'pagedown', 'END' : 'end', 'HOME' : 'home', 'LEFT' : 'left', 'ARROWLEFT' : 'left', 'UP' : 'up', 'ARROWUP' : 'up', 'RIGHT' : 'right', 'ARROWRIGHT' : 'right', 'DOWN' : 'down', 'ARROWDOWN' : 'down', 'INSERT' : 'insert', // No Mac support 'DELETE' : 'delete', //NORMAL-CHARACTERS, NUMPAD 'NUM0' : 'numpad_0', 'NUM1' : 'numpad_1', 'NUM2' : 'numpad_2', 'NUM3' : 'numpad_3', 'NUM4' : 'numpad_4', 'NUM5' : 'numpad_5', 'NUM6' : 'numpad_6', 'NUM7' : 'numpad_7', 'NUM8' : 'numpad_8', 'NUM9' : 'numpad_9', 'F1' : 'f1', 'F2' : 'f2', 'F3' : 'f3', 'F4' : 'f4', 'F5' : 'f5', 'F6' : 'f6', 'F7' : 'f7', 'F8' : 'f8', 'F9' : 'f9', 'F10' : 'f10', 'F11' : 'f11', 'F12' : 'f12' } // eof robotJSKeys export const SimulatorServer = SimulatorServerRobotJs