All files / webdriver/src monad.js

98.31% Statements 58/59
100% Branches 31/31
83.33% Functions 10/12
100% Lines 58/58

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179          2x                     23x 23x   23x 23x   23x 23x           23x 23x           23x 4x 21x 16x     5x 5x             23x         23x   23x 23x         23x 22x     23x 11x     23x 3x     3x       2x 1x 1x           2x   1x                         23x 4x     4x 2x       1x 1x           1x   2x 1x 1x 1x   1x       23x                   23x 2x 2x         2x         2x           2x 2x 2x     2x             23x 414x 12x 12x       23x    
import { EventEmitter } from 'events'
import logger from '@wdio/logger'
 
import { commandCallStructure, overwriteElementCommands } from './utils'
 
const SCOPE_TYPES = {
    'browser': /* istanbul ignore next */ function Browser () {},
    'element': /* istanbul ignore next */ function Element () {}
}
 
export default function WebDriver (options, modifier, propertiesObject = {}) {
    /**
     * In order to allow named scopes for elements we have to propagate that
     * info within the `propertiesObject` object. This doesn't have any functional
     * advantages just provides better description of objects when debugging them
     */
    const scopeType = SCOPE_TYPES[propertiesObject.scope] || SCOPE_TYPES['browser']
    delete propertiesObject.scope
 
    const prototype = Object.create(scopeType.prototype)
    const log = logger('webdriver')
 
    const eventHandler = new EventEmitter()
    const EVENTHANDLER_FUNCTIONS = Object.getPrototypeOf(eventHandler)
 
    /**
     * WebDriver monad
     */
    function unit (sessionId, commandWrapper) {
        propertiesObject.commandList = { value: Object.keys(propertiesObject) }
        propertiesObject.options = { value: options }
 
        /**
         * allow to wrap commands if necessary
         * e.g. in wdio-cli to make them synchronous
         */
        if (typeof commandWrapper === 'function') {
            for (const [commandName, { value }] of Object.entries(propertiesObject)) {
                if (typeof value !== 'function') {
                    continue
                }
 
                propertiesObject[commandName].value = commandWrapper(commandName, value)
                propertiesObject[commandName].configurable = true
            }
        }
 
        /**
         * overwrite native element commands with user defined
         */
        overwriteElementCommands.call(this, propertiesObject)
 
        /**
         * assign propertiesObject to itself so the client can be recreated
         */
        propertiesObject['__propertiesObject__'] = { value: propertiesObject }
 
        let client = Object.create(prototype, propertiesObject)
        client.sessionId = sessionId
 
        /**
         * register capabilities only to browser scope
         */
        if (scopeType.name === 'Browser') {
            client.capabilities = options.capabilities
        }
 
        if (typeof modifier === 'function') {
            client = modifier(client, options)
        }
 
        client.addCommand = function (name, func, attachToElement = false, proto, instances) {
            const customCommand = typeof commandWrapper === 'function'
                ? commandWrapper(name, func)
                : func
            if (attachToElement) {
                /**
                 * add command to every multiremote instance
                 */
                if (instances) {
                    Object.values(instances).forEach(instance => {
                        instance.__propertiesObject__[name] = {
                            value: customCommand
                        }
                    })
                }
 
                this.__propertiesObject__[name] = { value: customCommand }
            } else {
                unit.lift(name, customCommand, proto)
            }
        }
 
        /**
         * overwriteCommand
         * @param  {String}   name              command name to be overwritten
         * @param  {Function} func              function to replace original command with;
         *                                      takes original function as first argument.
         * @param  {boolean=} attachToElement   overwrite browser command (false) or element command (true)
         * @param  {Object=}  proto             prototype to add function to (optional)
         * @param  {Object=}  instances         multiremote instances
         */
        client.overwriteCommand = function (name, func, attachToElement = false, proto, instances) {
            let customCommand = typeof commandWrapper === 'function'
                ? commandWrapper(name, func)
                : func
            if (attachToElement) {
                if (instances) {
                    /**
                     * add command to every multiremote instance
                     */
                    Object.values(instances).forEach(instance => {
                        instance.__propertiesObject__.__elementOverrides__.value[name] = customCommand
                    })
                } else {
                    /**
                     * regular mode
                     */
                    this.__propertiesObject__.__elementOverrides__.value[name] = customCommand
                }
            } else if (client[name]) {
                const origCommand = client[name]
                delete client[name]
                unit.lift(name, customCommand, proto, (...args) => origCommand.apply(this, args))
            } else {
                throw new Error('overwriteCommand: no command to be overwritten: ' + name)
            }
        }
 
        return client
    }
 
    /**
     * Enhance monad prototype with function
     * @param  {String}   name          name of function to attach to prototype
     * @param  {Function} func          function to be added to prototype
     * @param  {Object}   proto         prototype to add function to (optional)
     * @param  {Function} origCommand   original command to be passed to custom command as first argument
     */
    unit.lift = function (name, func, proto, origCommand) {
        (proto || prototype)[name] = function next (...args) {
            log.info('COMMAND', commandCallStructure(name, args))
 
            /**
             * set name of function for better error stack
             */
            Object.defineProperty(func, 'name', {
                value: name,
                writable: false,
            })
 
            const result = func.apply(this, origCommand ? [origCommand, ...args] : args)
 
            /**
             * always transform result into promise as we don't know whether or not
             * the user is running tests with wdio-sync or not
             */
            Promise.resolve(result).then((res) => {
                log.info('RESULT', res)
                this.emit('result', { name, result: res })
            }).catch(() => {})
 
            return result
        }
    }
 
    /**
     * register event emitter
     */
    for (let eventCommand in EVENTHANDLER_FUNCTIONS) {
        prototype[eventCommand] = function (...args) {
            eventHandler[eventCommand](...args)
            return this
        }
    }
 
    return unit
}