class QQWebSocketDebugUrlFetcher(object):
    DEFAULT_LOCAL_FORWARD_PORT = 9222

    def __init__(self, device, localForwardPort=None):
        self._device = device
        self._localForwardPort = localForwardPort if localForwardPort is not None \
            else QQWebSocketDebugUrlFetcher.DEFAULT_LOCAL_FORWARD_PORT
        self._webSocketDebugUrl = None
        self.type = ''

        self.pageUrlDict = bidict.bidict()
        self.logger = Log().getLogger()

    def fetchWebSocketDebugUrl(self, refetch=False):
        # 如果没有获取或者需要重新获取debug url,那么获取一次,否则直接返回
        if self._webSocketDebugUrl is None or refetch:
            self._fetchInner()

        return self._webSocketDebugUrl

    def getDevice(self):
        return self._device

    def getForwardPort(self):
        return self._localForwardPort

    def _fetchInner(self):

        self.logger.debug('')

        pid = QQWebSocketDebugUrlFetcher._fetchQQToolsProcessPid(
            device=self._device)
        QQWebSocketDebugUrlFetcher._forwardLocalPort(self._localForwardPort,
                                                     pid,
                                                     device=self._device)

        # 获取本地http://localhost:{重定向端口}/json返回的json数据,提取里面的webSocketDebuggerUrl字段值
        self._webSocketDebugUrl = self._fetchWebSocketDebugUrl(
            self._localForwardPort)

    @staticmethod
    def _fetchQQToolsProcessPid(device=None):

        osName = OS.getPlatform()
        cmd = _ADB_FIND_QQ_STR_CMD[osName]

        stdout = QQWebSocketDebugUrlFetcher._getProcessInfo(cmd, device)

        QQProcessInfoLine = None
        for processInfo in stdout.split("\r\r\n"):
            if "com.tencent.mobileqq:tool" in processInfo:
                QQProcessInfoLine = processInfo
                break

        if QQProcessInfoLine is None:
            errorMsg = ErrorMsgManager().errorCodeToString(
                ERROR_CODE_NOT_FOUND_WEIXIN_TOOLS_PROCESS)
            raise RuntimeError(errorMsg)

        QQProcessInfo = QQProcessInfoLine.split()
        return QQWebSocketDebugUrlFetcher._handlePhoneCompat(QQProcessInfo)

    @staticmethod
    def _getProcessInfo(cmd, device):
        nums = 0
        retry = True
        stdout = ''
        while (retry and nums < 8):
            try:
                stdout, stdError = runCommand(
                    AdbHelper.specifyDeviceOnCmd(cmd, device))
                retry = False
            except:
                nums = nums + 1
                time.sleep(1)
                Log().getLogger().debug('open port mapping ---> retry ' +
                                        str(nums))
        return stdout

    @staticmethod
    def _handlePhoneCompat(processInfo):

        # 这里建立ps命令返回结果行字段数和pid字段index的映射
        pidIndexMap = {
            9: 1,  # 三星的手机是9元祖,第二列为pid index
            5: 0,  # 华为的手机是5元祖,第一列为pid index
            10: 5,  # 当微信出现异常时可能出现两个tools,那么选择第二个。
            18: 10
        }

        fieldCount = len(processInfo)
        pidIndex = pidIndexMap.get(fieldCount, -1)

        if pidIndex >= 0:
            return int(processInfo[pidIndex])
        else:
            raise RuntimeError(ErrorMsgManager().errorCodeToString(
                ERROR_CODE_NOT_ENABLE_DEBUG_MODE))

    @staticmethod
    def _forwardLocalPort(localPort, pid, device=None):
        cmd = "adb forward tcp:%s localabstract:webview_devtools_remote_%s" % (
            localPort, pid)
        runCommand(AdbHelper.specifyDeviceOnCmd(cmd, device))

    def _fetchWebSocketDebugUrl(self, localPort):
        self.logger.debug('')
        import json
        import urllib2
        errorMsg = None
        resultUrl = None

        localUrl = "http://localhost:%s/json" % localPort

        # 去掉代理
        urllib2.getproxies = lambda: {}

        try:
            nums = 0
            while None == resultUrl and nums < 8:
                response = urllib2.urlopen(localUrl)
                responseJson = json.loads(response.read())
                if len(responseJson) != 0:
                    resultUrl = self._handleAndReturnWebSocketDebugUrl(
                        responseJson)
                    self.logger.debug('websocket --> ' + resultUrl)
                    return resultUrl
                else:
                    nums = nums + 1
                    Log().getLogger().debug('retry fetch num ---> ' +
                                            str(nums))
                    time.sleep(1)

        except Exception:
            errorMsg = ErrorMsgManager().errorCodeToString(
                ERROR_CODE_CONFIG_PROXY)
        if not errorMsg:
            errorMsg = ErrorMsgManager().errorCodeToString(
                ERROR_CODE_NOT_ENTER_H5)
        raise RuntimeError(errorMsg)

    # 按顺序记录打开页面,key为打开页面的顺序,value为打开页面的websocketUrl。
    def _handleAndReturnWebSocketDebugUrl(self, responseJson):
        import json
        responseLenth = 0
        # 只记录有效页面的数量
        for i in range(len(responseJson)):
            response = responseJson[i]
            if response['type'] == u'page' and json.loads(
                    response['description'])['empty'] == False:
                responseLenth = responseLenth + 1

        pageUrlDictLength = len(self.pageUrlDict)
        if pageUrlDictLength == 0:
            self.logger.debug('pageUrlDictLength == 0:')
            for i in range(responseLenth):
                response = responseJson[i]
                if response['type'] == u'page' and json.loads(
                        response['description'])['empty'] == False:
                    self.pageUrlDict.update({
                        pageUrlDictLength + 1:
                        response['webSocketDebuggerUrl']
                    })
                    return response['webSocketDebuggerUrl']
        elif responseLenth > pageUrlDictLength:
            self.logger.debug('responseLenth > pageUrlDictLength')
            for i in range(responseLenth):
                response = responseJson[i]
                if response['type'] == u'page' and json.loads(
                        response['description'])['empty'] == False:
                    if self.pageUrlDict.inv.get(
                            response['webSocketDebuggerUrl']) is None:
                        self.pageUrlDict.update({
                            pageUrlDictLength + 1:
                            response['webSocketDebuggerUrl']
                        })
                        return response['webSocketDebuggerUrl']
        elif responseLenth < pageUrlDictLength:
            self.logger.debug('responseLenth < pageUrlDictLength')
            sencondLastPageUrl = self.pageUrlDict.get(pageUrlDictLength - 1)
            del self.pageUrlDict[pageUrlDictLength]
            return sencondLastPageUrl
        else:
            self.logger.debug('responseLenth == pageUrlDictLength')
            lastPageUrl = self.pageUrlDict.get(pageUrlDictLength)
            return lastPageUrl

    def needSwitchNextPage(self):
        import json
        import urllib2
        localUrl = "http://localhost:9223/json"

        # 去掉代理
        urllib2.getproxies = lambda: {}

        response = urllib2.urlopen(localUrl)
        responseJson = json.loads(response.read())
        return len(responseJson) != len(self.pageUrlDict)

    def getType(self):
        return self.type
Exemplo n.º 2
0
class WxDriver(object):
    def initDriver(self):
        if self._hasInit:
            return

        self._urlFetcher = WxWebSocketDebugUrlFetcher(device=self._device)
        url = self._urlFetcher.fetchWebSocketDebugUrl()

        dirPath = os.path.split(os.path.realpath(__file__))[0]
        PLUG_SRC = os.path.join(dirPath, 'apk', 'inputPlug.apk')
        if not AdbHelper.hasApkInstalled(packageName='com.tencent.fat.wxinputplug'):
            AdbHelper.installApk(PLUG_SRC, device=self._device, installOverride=True)
            self.logger.info('install ---> ' + PLUG_SRC)
        self._vmShutdownHandler.registerToVM()
        UncaughtExceptionHandler.init()
        UncaughtExceptionHandler.registerUncaughtExceptionCallback(self._onUncaughtException)

        self._selectDevice()

        self._webSocketDataTransfer = WebSocketDataTransfer(url=url)
        self._networkHandler = ShortLiveWebSocket(self._webSocketDataTransfer, self._executor, self)
        self._networkHandler.connect()
        self._executor.put(self._networkHandler.receive)

        self.initPageDisplayData()
        self._hasInit = True

    # 为了输入中文要先安装一个插件,./wx/apk/inputPlug.apk
    def __init__(self, d,device=None):
        self._device = device
        self._urlFetcher = None
        self._webSocketDataTransfer = None
        self._vmShutdownHandler = VMShutDownHandler()
        self._networkHandler = None
        self._executor = SingleThreadExecutor()
        self.logger = Log().getLogger()

        self._pageOperator = WxPageOperator()

        self._hasInit = False

        self.d = d
        self.html = None

    def getDriverType(self):
        return constant.WXDRIVER

    def initPageDisplayData(self):
        driverInfo = self.d.info
        displayDp = driverInfo.get('displaySizeDpY')
        displayPx = driverInfo.get('displayHeight')
        self.scale = (displayPx - 0.5) / displayDp
        windowHeight = self.getWindowHeight()
        self.appTitleHeight = displayDp - windowHeight

    def changeDp2Px(self, xDp, yDp):
        xPx, yPx = self._pageOperator.changeDp2Px(xDp, yDp, self.scale, self.appTitleHeight)
        return xPx, yPx

    def needSwitchNextPage(self):
        return self._urlFetcher.needSwitchNextPage()

    def _selectDevice(self):
        devicesList = AdbHelper.listDevices(ignoreUnconnectedDevices=True)
        # 假如没有指定设备,那么判断当前机器是否连接1个设备
        if self._device is None:
            devicesCount = len(devicesList)
            errorMsg = None

            if devicesCount <= 0:
                errorMsg = ErrorMsgManager().errorCodeToString(ERROR_CODE_DEVICE_NOT_CONNECT)

            elif devicesCount == 1:
                self._device = devicesList[0]

            else:
                errorMsg = ErrorMsgManager().errorCodeToString(ERROR_CODE_MULTIPLE_DEVICE)

            if errorMsg is not None:
                raise RuntimeError(errorMsg)

    def switchToNextPage(self):
        """
        需要重新获取当前页面的websocket的url
        """
        self._networkHandler.disconnect()
        url = self._urlFetcher.fetchWebSocketDebugUrl(refetch=True)
        self.logger.debug('url -> ' + url)
        self._webSocketDataTransfer.setUrl(url)
        self._networkHandler.connect()
        self.wait(WAIT_REFLESH_05_SECOND)

    def returnLastPage(self):
        self.wait(WAIT_REFLESH_05_SECOND)
        self.logger.info('')
        self._networkHandler.disconnect()
        self.switchToNextPage()
        self.wait(WAIT_REFLESH_05_SECOND)
        self._networkHandler.disconnect()
        self.d.press('back')
        self.wait(WAIT_REFLESH_05_SECOND)
        self.switchToNextPage()

    def getDocument(self):
        """
        获得getHtml中需要的nodeId
        在调用getHtml之前必须先调用这个方法
        """
        sendStr = self._pageOperator.getDocument()
        return self._networkHandler.send(sendStr)

    def getHtml(self, nodeId=1):
        """
        获得指定nodeId的Html代码。在一条websocket连接中,方法只能够执行一次。
        :param nodeId: getDocument方法返回的nodeId,当为1时,返回整个body的代码
        """
        self.logger.info('')
        self.switchToNextPage()
        self.getDocument()
        sendStr = self._pageOperator.getHtml(nodeId)
        self.html = self._networkHandler.send(sendStr).getResponse()[0]['result']['outerHTML']
        return self.html

    def isElementExist(self, xpath):
        self.logger.info('xpath ---> ' + xpath)
        getExistCmd = self._pageOperator.isElementExist(xpath)
        response = self._networkHandler.send(getExistCmd).getResponse()
        self.logger.debug(response)
        resultValueDict = response[0]
        resultType = resultValueDict['result']['result']['subtype']
        num = 0
        while resultType == 'null' and num < 3:
            self.wait(WAIT_REFLESH_3_SECOND)
            self.switchToNextPage()
            getExistCmd = self._pageOperator.isElementExist(xpath)
            resultValueDict = self._networkHandler.send(getExistCmd).getResponse()[0]
            resultType = resultValueDict['result']['result']['subtype']
            num = num + 1
        self.logger.debug('isElementExist Response --> ' + str(resultValueDict))
        return resultType != 'null'

    def getElementTextByXpath(self, xpath):
        time.sleep(WAIT_REFLESH_1_SECOND)
        self.logger.info('xpath ---> ' + xpath)
        self.switchToNextPage()
        if self.isElementExist(xpath):
            getTextCmd = self._pageOperator.getElementTextByXpath(xpath)
            resultValueDict = self._networkHandler.send(getTextCmd).getResponse()[0]
            resultValue = resultValueDict['result']['result']['value'].encode("utf-8")
        else:
            resultValue = None
        return resultValue

    def getElementSrcByXpath(self, xpath):
        time.sleep(WAIT_REFLESH_1_SECOND)
        self.logger.info('xpath ---> ' + xpath)
        self.switchToNextPage()
        if self.isElementExist(xpath):
            getSrcCmd = self._pageOperator.getElementSrcByXpath(xpath)
            resultValueDict = self._networkHandler.send(getSrcCmd).getResponse()[0]
            resultValue = resultValueDict['result']['result']['value'].encode("utf-8")
        else:
            resultValue = None
        return resultValue

    def getElementByXpath(self, xpath):
        """
        :param:目标的xpath
        :return:返回lxml包装过的element对象,可以使用lxml语法获得对象的信息
        例如:可以使用element.get("attrs")来拿到属性的数据
        可以用element.text拿到它的文字
        当element中含有列表时,使用for循环读取每一个item
        """
        time.sleep(WAIT_REFLESH_1_SECOND)
        self.logger.info('xpath ---> ' + xpath)
        htmlData = self.getHtml()
        if htmlData is not None:
            html = etree.HTML(htmlData)
            elementList = html.xpath(xpath)
            if len(elementList) != 0:
                return elementList[0]
            else:
                self.logger.info('找不到xpath为: ' + xpath + ' 的控件')
                return ''
        else:
            self.logger.info('获取到的html为空')
            return ''

    def textElementByXpath(self, xpath, text, needClick=False):
        """
        :param needClick:如果为true,会先对控件进行一次点击,以获得焦点

        输入前会先保存当前默认的输入法
        然后将输入法切换到adb插件
        输入中文,输入后讲输入法转换回默认输入发
        needClick代表是否需要先对xpath的控件进行一次点击
        """
        self.logger.info('xpath ---> ' + xpath + ' text ---> ' + text)
        if needClick:
            self.clickElementByXpath(xpath, byUiAutomator=True)

        if self.isElementExist(xpath):
            ADB_SHELL = 'adb shell '
            self.logger.debug('textElementByXpath xpath exist')

            if self._device:
                ADB_SHELL = ADB_SHELL.replace("adb", "adb -s %s" % self._device)
            GET_DEFAULT_INPUT_METHOD = ADB_SHELL + 'settings get secure default_input_method'
            SET_INPUT_METHOD = ADB_SHELL + 'ime set {0}'
            CHINESE_INPUT_METHOD = 'com.tencent.fat.wxinputplug/.XCXIME'
            INPUT_TEXT = ADB_SHELL + "am broadcast -a INPUT_TEXT --es TEXT '{0}'"

            DEFAULT_INPUT_METHOD = commandHelper.runCommand(GET_DEFAULT_INPUT_METHOD)[0].replace("\r\n", " ")
            osName = OS.getPlatform()
            INPUT_TEXT_CMD = {
                "Darwin": INPUT_TEXT.format(text),
                "Windows": INPUT_TEXT.format(text).decode('utf-8').replace(u'\xa0', u' ').encode('GBK'),
                "Linux": INPUT_TEXT.format(text)
            }

            self.logger.debug(SET_INPUT_METHOD.format(CHINESE_INPUT_METHOD))
            commandHelper.runCommand(SET_INPUT_METHOD.format(CHINESE_INPUT_METHOD))
            self.wait(WAIT_REFLESH_05_SECOND)
            self.logger.debug(INPUT_TEXT_CMD.get(osName))
            commandHelper.runCommand(INPUT_TEXT_CMD.get(osName))
            self.logger.debug(SET_INPUT_METHOD.format(DEFAULT_INPUT_METHOD))
            commandHelper.runCommand(SET_INPUT_METHOD.format(DEFAULT_INPUT_METHOD))
            self.wait(WAIT_REFLESH_05_SECOND)

    def clickElementByXpath(self, xpath, visibleItemXpath=None, byUiAutomator=False):
        """
        默认滑动点为屏幕的中心,且边距为整个屏幕。当有container时,传入container中任意一个当前可见item的xpath,之后会将目标滑到该可见item的位置
        :param xpath: 要滑动到屏幕中控件的xpath
        :param visibleItemXpath: container中当前可见的一个xpath
        :return:
        """
        self.logger.info('xpath ---> ' + xpath)
        if self.isElementExist(xpath):
            self.scrollToElementByXpath(xpath, visibleItemXpath)

            sendStr = self._pageOperator.getElementRect(xpath)
            self._networkHandler.send(sendStr)

            x = self._getRelativeDirectionValue('x')
            y = self._getRelativeDirectionValue('y')
            self.logger.debug('clickElementByXpath x:' + str(x) + '  y:' + str(y))

            if not byUiAutomator:
                clickCommand = self._pageOperator.clickElementByXpath(x, y)
                return self._networkHandler.send(clickCommand)
            else:
                xPx, yPx = self.changeDp2Px(x, y)
                self.d.click(xPx, yPx)

    def clickFirstElementByText(self, text, visibleItemXpath=None, byUiAutomator=False):
        """
        通过text来搜索,点击第一个text相符合的控件。参数同clickElementByXpath()
        """
        self.clickElementByXpath('.//*[text()="' + text + '"]', visibleItemXpath, byUiAutomator)

    def getElementCoordinateByXpath(self, elementXpath):
        '''
        获得Element的坐标
        :param elementXpath:待获取坐标的元素的xpath
        :return:element相对于整个屏幕的x、y坐标,单位为px
        '''
        self.logger.info(
            'elementXpathXpath ---> ' + elementXpath)
        if self.isElementExist(elementXpath):
            sendStr = self._pageOperator.getElementRect(elementXpath)
            self._networkHandler.send(sendStr)
            x = self._getRelativeDirectionValue('x')
            y = self._getRelativeDirectionValue('y')
            xPx, yPx = self.changeDp2Px(x, y)
            return xPx, yPx

    def scrollToElementByXpath(self, xpath, visibleItemXpath=None, speed=600):
        """
        默认滑动点为屏幕的中心,且边距为整个屏幕。当有container时,传入container中任意一个当前可见item的xpath,之后会将目标滑到该可见item的位置
        :param xpath: 要滑动到屏幕中控件的xpath
        :param visibleItemXpath: container中当前可见的一个xpath
        :return:
        """
        self.logger.info('xpath ---> ' + xpath)
        sendStr = self._pageOperator.getElementRect(xpath)
        self._networkHandler.send(sendStr)

        top = self._getRelativeDirectionValue('topp')
        bottom = self._getRelativeDirectionValue('bottom')
        left = self._getRelativeDirectionValue('left')
        right = self._getRelativeDirectionValue('right')
        self.logger.debug('scrollToElementByXpath -> top:bottom:left:right = ' + str(top) + " :" + str(bottom) \
                          + " :" + str(left) + " :" + str(right))

        if visibleItemXpath is None:
            endTop = 0
            endLeft = 0
            endBottom = self.getWindowHeight()
            endRight = self.getWindowWidth()
        else:
            sendStr = self._pageOperator.getElementRect(visibleItemXpath)
            self._networkHandler.send(sendStr)

            endTop = self._getRelativeDirectionValue('topp')
            endBottom = self._getRelativeDirectionValue('bottom')
            endLeft = self._getRelativeDirectionValue('left')
            endRight = self._getRelativeDirectionValue('right')
            self.logger.debug(
                'scrollToElementByXpath -> toendTop:endBottom:endLeft:endRight = ' + str(endTop) + " :" + str(
                    endBottom) \
                + " :" + str(endLeft) + " :" + str(endRight))

        '''
        竖直方向的滑动
        '''
        if endBottom < bottom:
            scrollYDistance = endBottom - bottom
        elif top < 0:
            scrollYDistance = -(top - endTop)
        else:
            scrollYDistance = 0

        if scrollYDistance < 0:
            self.scrollWindow(int((endLeft + endRight) / 2), int((endTop + endBottom) / 2), 0, scrollYDistance - 80,
                              speed)
        elif scrollYDistance > 0:
            self.scrollWindow(int((endLeft + endRight) / 2), int((endTop + endBottom) / 2), 0, scrollYDistance + 80,
                              speed)
        else:
            self.logger.debug('y方向不需要滑动')
        '''
        水平方向的滑动
        '''
        if right > endRight:
            scrollXDistance = endRight - right
        elif left < 0:
            scrollXDistance = -(left - endLeft)
        else:
            scrollXDistance = 0

        if scrollXDistance != 0:
            self.scrollWindow(int((endLeft + endRight) / 2), int((endTop + endBottom) / 2), scrollXDistance, 0,
                              speed)
        else:
            self.logger.debug('x方向不需要滑动')

    def scrollWindow(self, x, y, xDistance, yDistance, speed=800):
        self.logger.info('')
        sendStr = self._pageOperator.scrollWindow(x, y, xDistance, yDistance, speed)
        return self._networkHandler.send(sendStr)

    def _getRelativeDirectionValue(self, directionKey='topp'):
        '''
        获取相关的方向数据参数值
        :param directionKey: key值
        :return:
        '''
        directionCommand = self._pageOperator.getJSValue(directionKey)
        directionResponse = self._networkHandler.send(directionCommand).getResponse()[0]
        directionValue = directionResponse['result']['result']['value']
        return directionValue

    '''
    性能数据
    '''

    def getMemoryInfo(self):
        '''
        获得小程序进程占用内存信息
        :return: 小程序进程占用内存信息
        '''
        self.logger.info('')
        ADB_SHELL = 'adb shell '
        if self._device:
            ADB_SHELL = ADB_SHELL.replace("adb", "adb -s %s" % self._device)
        GET_MEMORY_INFO = ADB_SHELL + 'dumpsys meminfo com.tencent.mm:appbrand0'
        return commandHelper.runCommand(GET_MEMORY_INFO)

    def getCPUInfo(self):
        '''
        获得小程序进程占用CPU信息
        :return: 小程序进程占用CPU信息
        '''
        self.logger.info('')
        ADB_SHELL = 'adb shell '
        if self._device:
            ADB_SHELL = ADB_SHELL.replace("adb", "adb -s %s" % self._device)
        GET_CPU_INFO = ADB_SHELL + ' top -n 1 | grep com.tencent.mm:appbrand0'
        return commandHelper.runCommand(GET_CPU_INFO)

    def getPageHeight(self):
        getPageHeightCmd = self._pageOperator.getPageHeight()
        resultValueDict = self._networkHandler.send(getPageHeightCmd).getResponse()[0]
        resultValue = resultValueDict['result']['result']['value']
        return resultValue

    def getWindowWidth(self):
        '''
        :return:手机屏幕的宽度
        '''
        getWindowWidthCmd = self._pageOperator.getWindowWidth()
        resultValueDict = self._networkHandler.send(getWindowWidthCmd).getResponse()[0]
        resultValue = resultValueDict['result']['result']['value']
        return resultValue

    def getWindowHeight(self):
        '''
        :return:手机屏幕的高度
        '''
        getWindowHeightCmd = self._pageOperator.getWindowHeight()
        resultValueDict = self._networkHandler.send(getWindowHeightCmd).getResponse()[0]
        resultValue = resultValueDict['result']['result']['value']
        return resultValue

    def wait(self, seconds):
        time.sleep(seconds)

    def addShutDownHook(self, func, *args, **kwargs):
        '''
        添加当程序正常关闭时的操作,可以进行一些环境清理操作等
        '''
        self._vmShutdownHandler.registerVMShutDownCallback(func, *args, **kwargs)

    def _onUncaughtException(self, exctype, value, tb):
        self.close()

    def close(self):
        if self._networkHandler is not None:
            self._networkHandler.quit()

        if self._executor is not None:
            self._executor.shutDown()

        UncaughtExceptionHandler.removeHook()

    def setDebugLogMode(self):
        '''
        开启debug的日志模式
        '''
        Log().setDebugLogMode()
Exemplo n.º 3
0
class QQDriver(object):
    def initDriver(self):
        if self._hasInit:
            return

        self._urlFetcher = QQWebSocketDebugUrlFetcher(device=self._device)
        url = self._urlFetcher.fetchWebSocketDebugUrl()
        self._vmShutdownHandler.registerToVM()
        UncaughtExceptionHandler.init()
        UncaughtExceptionHandler.registerUncaughtExceptionCallback(
            self._onUncaughtException)

        self._selectDevice()

        self._webSocketDataTransfer = WebSocketDataTransfer(url=url)
        self._networkHandler = ShortLiveWebSocket(self._webSocketDataTransfer,
                                                  self._executor, self)
        self._networkHandler.connect()
        self._executor.put(self._networkHandler.receive)

        self.initPageDisplayData()
        self.logger = Log().getLogger()
        self._hasInit = True

    def __init__(self, device=None):
        self._device = device
        self._urlFetcher = None
        self._webSocketDataTransfer = None
        self._vmShutdownHandler = VMShutDownHandler()
        self._networkHandler = None
        self._executor = SingleThreadExecutor()

        self._pageOperator = H5PageOperator()

        self._hasInit = False

        self.d = uiautomator.Device(self._device)
        self.html = None

    def getDriverType(self):
        return constant.QQDRIVER

    def initPageDisplayData(self):
        driverInfo = self.d.info
        displayDp = driverInfo.get('displaySizeDpY')
        displayPx = driverInfo.get('displayHeight')
        self.scale = (displayPx - 0.5) / displayDp
        windowHeight = self.getWindowHeight()
        self.appTitleHeight = displayDp - windowHeight

    def changeDp2Px(self, xDp, yDp):
        xPx, yPx = self._pageOperator.changeDp2Px(xDp, yDp, self.scale,
                                                  self.appTitleHeight)
        return xPx, yPx

    def needSwitchNextPage(self):
        return self._urlFetcher.needSwitchNextPage()

    def _selectDevice(self):
        devicesList = AdbHelper.listDevices(ignoreUnconnectedDevices=True)
        # 假如没有指定设备,那么判断当前机器是否连接1个设备
        if self._device is None:
            devicesCount = len(devicesList)
            errorMsg = None

            if devicesCount <= 0:
                errorMsg = ErrorMsgManager().errorCodeToString(
                    ERROR_CODE_DEVICE_NOT_CONNECT)

            elif devicesCount == 1:
                self._device = devicesList[0]

            else:
                errorMsg = ErrorMsgManager().errorCodeToString(
                    ERROR_CODE_MULTIPLE_DEVICE)

            if errorMsg is not None:
                raise RuntimeError(errorMsg)

    def switchToNextPage(self):
        """
        需要重新获取当前页面的websocket的url
        """
        self._networkHandler.disconnect()
        url = self._urlFetcher.fetchWebSocketDebugUrl(refetch=True)
        self.logger.debug('url -> ' + url)
        self._webSocketDataTransfer.setUrl(url)
        self._networkHandler.connect()
        self.wait(WAIT_REFLESH_05_SECOND)

    def returnLastPage(self):
        self.logger.info('')
        self.wait(WAIT_REFLESH_1_SECOND)
        self._networkHandler.disconnect()
        self.switchToNextPage()
        self._networkHandler.disconnect()
        self.d.press('back')
        self.wait(WAIT_REFLESH_1_SECOND)
        self.switchToNextPage()

    def getDocument(self):
        """
        获得getHtml中需要的nodeId
        在调用getHtml之前必须先调用这个方法
        """
        sendStr = self._pageOperator.getDocument()
        return self._networkHandler.send(sendStr)

    def getHtml(self, nodeId=1):
        """
        获得指定nodeId的Html代码。在一条websocket连接中,方法只能够执行一次。
        :param nodeId: getDocument方法返回的nodeId,当为1时,返回整个body的代码
        """
        self.logger.info('')
        self.switchToNextPage()
        self.getDocument()
        sendStr = self._pageOperator.getHtml(nodeId)
        self.html = self._networkHandler.send(
            sendStr).getResponse()[0]['result']['outerHTML']
        return self.html

    def isElementExist(self, xpath):
        self.logger.info('xpath ---> ' + xpath)
        getExistCmd = self._pageOperator.isElementExist(xpath)
        resultValueDict = self._networkHandler.send(
            getExistCmd).getResponse()[0]
        resultType = resultValueDict['result']['result']['subtype']
        if resultType == 'null':
            self.wait(WAIT_REFLESH_3_SECOND)
            self.switchToNextPage()
            getExistCmd = self._pageOperator.isElementExist(xpath)
            resultValueDict = self._networkHandler.send(
                getExistCmd).getResponse()[0]
            resultType = resultValueDict['result']['result']['subtype']
        self.logger.debug('isElementExist Response --> ' +
                          str(resultValueDict))
        return resultType != 'null'

    def getElementTextByXpath(self, xpath):
        time.sleep(WAIT_REFLESH_1_SECOND)
        self.logger.info('xpath ---> ' + xpath)
        self.switchToNextPage()
        if self.isElementExist(xpath):
            getTextCmd = self._pageOperator.getElementTextByXpath(xpath)
            resultValueDict = self._networkHandler.send(
                getTextCmd).getResponse()[0]
            resultValue = resultValueDict['result']['result']['value'].encode(
                "utf-8")
        else:
            resultValue = None
        return resultValue

    def getElementSrcByXpath(self, xpath):
        time.sleep(WAIT_REFLESH_1_SECOND)
        self.logger.info('xpath ---> ' + xpath)
        self.switchToNextPage()
        if self.isElementExist(xpath):
            getSrcCmd = self._pageOperator.getElementSrcByXpath(xpath)
            resultValueDict = self._networkHandler.send(
                getSrcCmd).getResponse()[0]
            resultValue = resultValueDict['result']['result']['value'].encode(
                "utf-8")
        else:
            resultValue = None
        return resultValue

    def getElementByXpath(self, xpath):
        """
        :param:目标的xpath
        :return:返回lxml包装过的element对象,可以使用lxml语法获得对象的信息
        例如:可以使用element.get("attrs")来拿到属性的数据
        可以用element.text拿到它的文字
        当element中含有列表时,使用for循环读取每一个item
        """
        time.sleep(WAIT_REFLESH_1_SECOND)
        self.logger.info('xpath ---> ' + xpath)
        htmlData = self.getHtml()
        if htmlData is not None:
            html = etree.HTML(htmlData)
            elementList = html.xpath(xpath)
            if len(elementList) != 0:
                return elementList[0]
            else:
                self.logger.info('找不到xpath为: ' + xpath + ' 的控件')
                return ''
        else:
            self.logger.info('获取到的html为空')
            return ''

    def textElementByXpath(self, xpath, text):
        """
               先获取输入框的焦点, 再使用Chrome debug协议的输入api,再输入文字内容
               :param xpath:输入框的xpath
               :parm text:要输入的文字
               """
        self.logger.info('xpath ---> ' + xpath + ' text ---> ' + text)
        self.focusElementByXpath(xpath)
        sendStrList = self._pageOperator.textElementByXpath(text)

        for command in sendStrList:
            self._networkHandler.send(command)
        self.wait(WAIT_REFLESH_05_SECOND)

    def clickElementByXpath(self,
                            xpath,
                            visibleItemXpath=None,
                            byUiAutomator=False):
        """
        默认滑动点为屏幕的中心,且边距为整个屏幕。当有container时,传入container中任意一个当前可见item的xpath,之后会将目标滑到该可见item的位置
        :param xpath: 要滑动到屏幕中控件的xpath
        :param visibleItemXpath: container中当前可见的一个xpath
        :return:
        """
        self.logger.info('xpath ---> ' + xpath)
        self.scrollToElementByXpath(xpath, visibleItemXpath)

        sendStr = self._pageOperator.getElementRect(xpath)
        self._networkHandler.send(sendStr)

        x = self._getRelativeDirectionValue('x')
        y = self._getRelativeDirectionValue('y')
        self.logger.debug('clickElementByXpath x:' + str(x) + '  y:' + str(y))

        if not byUiAutomator:
            clickCommand = self._pageOperator.clickElementByXpath(x, y)
            return self._networkHandler.send(clickCommand)
        else:
            xPx, yPx = self.changeDp2Px(x, y)
            self.d.click(xPx, yPx)

    def clickFirstElementByText(self,
                                text,
                                visibleItemXpath=None,
                                byUiAutomator=False):
        """
        通过text来搜索,点击第一个text相符合的控件。参数同clickElementByXpath()
        """
        self.clickElementByXpath('.//*[text()="' + text + '"]',
                                 visibleItemXpath, byUiAutomator)

    def focusElementByXpath(self, xpath):
        """
        调用目标的focus()方法。
        :param xpath:目标的xpath
        """
        executeCmd = self._pageOperator.focusElementByXpath(xpath)
        self._networkHandler.send(executeCmd)

    def getElementCoordinateByXpath(self, elementXpath):
        '''
        获得Element的坐标
        :param elementXpath:待获取坐标的元素的xpath
        :return:element相对于整个屏幕的x、y坐标,单位为px
        '''
        self.logger.info('elementXpathXpath ---> ' + elementXpath)

        sendStr = self._pageOperator.getElementRect(elementXpath)
        self._networkHandler.send(sendStr)
        x = self._getRelativeDirectionValue('x')
        y = self._getRelativeDirectionValue('y')
        xPx, yPx = self.changeDp2Px(x, y)
        return xPx, yPx

    def scrollToElementByXpath(self, xpath, visibleItemXpath=None, speed=600):
        """
        默认滑动点为屏幕的中心,且边距为整个屏幕。当有container时,传入container中任意一个当前可见item的xpath,之后会将目标滑到该可见item的位置
        :param xpath: 要滑动到屏幕中控件的xpath
        :param visibleItemXpath: container中当前可见的一个xpath
        :return:
        """
        self.logger.info('xpath ---> ' + xpath)
        sendStr = self._pageOperator.getElementRect(xpath)
        self._networkHandler.send(sendStr)

        top = self._getRelativeDirectionValue('topp')
        bottom = self._getRelativeDirectionValue('bottom')
        left = self._getRelativeDirectionValue('left')
        right = self._getRelativeDirectionValue('right')
        self.logger.debug('scrollToElementByXpath -> top:bottom:left:right = ' + str(top) + " :" + str(bottom) \
                          + " :" + str(left) + " :" + str(right))

        if visibleItemXpath is None:
            endTop = 0
            endLeft = 0
            endBottom = self.getWindowHeight()
            endRight = self.getWindowWidth()
        else:
            sendStr = self._pageOperator.getElementRect(visibleItemXpath)
            self._networkHandler.send(sendStr)

            endTop = self._getRelativeDirectionValue('topp')
            endBottom = self._getRelativeDirectionValue('bottom')
            endLeft = self._getRelativeDirectionValue('left')
            endRight = self._getRelativeDirectionValue('right')
            self.logger.debug(
                'scrollToElementByXpath -> toendTop:endBottom:endLeft:endRight = ' + str(endTop) + " :" + str(
                    endBottom) \
                + " :" + str(endLeft) + " :" + str(endRight))
        '''
        竖直方向的滑动
        '''
        if endBottom < bottom:
            scrollYDistance = endBottom - bottom
        elif top < 0:
            scrollYDistance = -(top - endTop)
        else:
            scrollYDistance = 0

        if scrollYDistance != 0:
            self.scrollWindow(int((endLeft + endRight) / 2),
                              int((endTop + endBottom) / 2), 0,
                              scrollYDistance, speed)
        else:
            self.logger.debug('y方向不需要滑动')
        '''
        水平方向的滑动
        '''
        if right > endRight:
            scrollXDistance = endRight - right
        elif left < 0:
            scrollXDistance = -(left - endLeft)
        else:
            scrollXDistance = 0

        if scrollXDistance != 0:
            self.scrollWindow(int((endLeft + endRight) / 2),
                              int((endTop + endBottom) / 2), scrollXDistance,
                              0, speed)
        else:
            self.logger.debug('x方向不需要滑动')

    def scrollWindow(self, x, y, xDistance, yDistance, speed=800):
        self.logger.info('')
        sendStr = self._pageOperator.scrollWindow(x, y, xDistance, yDistance,
                                                  speed)
        return self._networkHandler.send(sendStr)

    def _getRelativeDirectionValue(self, directionKey='topp'):
        '''
        获取相关的方向数据参数值
        :param directionKey: key值
        :return:
        '''
        directionCommand = self._pageOperator.getJSValue(directionKey)
        directionResponse = self._networkHandler.send(
            directionCommand).getResponse()[0]
        directionValue = directionResponse['result']['result']['value']
        return directionValue

    '''
    性能数据
    '''

    def getMemoryInfo(self):
        '''
        获得小程序进程占用内存信息
        :return: 小程序进程占用内存信息
        '''
        self.logger.info('')
        ADB_SHELL = 'adb shell '
        if self._device:
            ADB_SHELL = ADB_SHELL.replace("adb", "adb -s %s" % self._device)
        GET_MEMORY_INFO = ADB_SHELL + 'dumpsys meminfo com.tencent.mm:appbrand0'
        return commandHelper.runCommand(GET_MEMORY_INFO)

    def getCPUInfo(self):
        '''
        获得小程序进程占用CPU信息
        :return: 小程序进程占用CPU信息
        '''
        self.logger.info('')
        ADB_SHELL = 'adb shell '
        if self._device:
            ADB_SHELL = ADB_SHELL.replace("adb", "adb -s %s" % self._device)
        GET_CPU_INFO = ADB_SHELL + ' top -n 1 | grep com.tencent.mm:appbrand0'
        return commandHelper.runCommand(GET_CPU_INFO)

    def getPageHeight(self):
        getPageHeightCmd = self._pageOperator.getPageHeight()
        resultValueDict = self._networkHandler.send(
            getPageHeightCmd).getResponse()[0]
        resultValue = resultValueDict['result']['result']['value']
        return resultValue

    def getWindowWidth(self):
        '''
        :return:手机屏幕的宽度
        '''
        getWindowWidthCmd = self._pageOperator.getWindowWidth()
        resultValueDict = self._networkHandler.send(
            getWindowWidthCmd).getResponse()[0]
        resultValue = resultValueDict['result']['result']['value']
        return resultValue

    def getWindowHeight(self):
        '''
        :return:手机屏幕的高度
        '''
        getWindowHeightCmd = self._pageOperator.getWindowHeight()
        resultValueDict = self._networkHandler.send(
            getWindowHeightCmd).getResponse()[0]
        resultValue = resultValueDict['result']['result']['value']
        return resultValue

    def wait(self, seconds):
        time.sleep(seconds)

    def addShutDownHook(self, func, *args, **kwargs):
        '''
        添加当程序正常关闭时的操作,可以进行一些环境清理操作等
        '''
        self._vmShutdownHandler.registerVMShutDownCallback(
            func, *args, **kwargs)

    def _onUncaughtException(self, exctype, value, tb):
        self.close()

    def close(self):
        if self._networkHandler is not None:
            self._networkHandler.quit()

        if self._executor is not None:
            self._executor.shutDown()

        UncaughtExceptionHandler.removeHook()

    def setDebugLogMode(self):
        '''
        开启debug的日志模式
        '''
        Log().setDebugLogMode()
Exemplo n.º 4
0
class ShortLiveWebSocket(DataTransferCallback):
    def __init__(self, webSocketDataTransfer, executor, driver):
        super(ShortLiveWebSocket, self).__init__()
        self._webSocketDataTransfer = webSocketDataTransfer
        self._executor = executor
        self._webSocketDataTransfer.registerCallback(self)

        self._readWriteSyncEvent = threading.Event()
        self._connectSyncEvent = threading.Event()
        self._id = AtomicInteger()
        self._currentRequest = None

        self._quit = False

        self._retryEvent = threading.Event()
        self.driver = driver
        self.logger = Log().getLogger()

    def setUrl(self, url):
        self._webSocketDataTransfer.setUrl(url)

    def isConnected(self):
        return self._webSocketDataTransfer.isConnected()

    def connect(self):
        self._webSocketDataTransfer.connect()
        self._connectSyncEvent.set()

    def disconnect(self):
        self._webSocketDataTransfer.disconnect()
        self._connectSyncEvent.clear()

    def receive(self):
        while not self.isQuit():
            try:
                self._waitForConnectOrThrow()
                self._webSocketDataTransfer.receive()

            except WebSocketConnectionClosedException:
                if self.isQuit():
                    print('already quit')
                else:
                    pass
            except Exception:
                pass

    def send(self, data, timeout=int(1 * 60)):
        self._waitForConnectOrThrow()
        # 微信小程序和QQ公粽号都需要切换页面
        if self.driver.getDriverType(
        ) == WXDRIVER or self.driver.getDriverType == QQDRIVER:
            # 只有点击事件才会导致页面的切换
            if 'x=Math.round((left+right)/2)' in data:
                time.sleep(WAIT_REFLESH_1_SECOND)
            if self.driver.needSwitchNextPage():
                self.driver.switchToNextPage()

        self._currentRequest = _NetWorkRequset(self._id.getAndIncrement(),
                                               data)
        currentRequestToJsonStr = self._currentRequest.toSendJsonString()
        self.logger.debug(' ---> ' + currentRequestToJsonStr)

        # scroll操作需要滑动到位置才会有返回,如果是scroll操作则等待,防止超时退出
        if 'synthesizeScrollGesture' in data:
            self._webSocketDataTransfer.send(currentRequestToJsonStr)
            self._retryEvent.wait(WAIT_REFLESH_40_SECOND)
        else:
            for num in range(0, SEND_DATA_ATTEMPT_TOTAL_NUM):
                self.logger.debug(" ---> attempt num: " + str(num))

                if num != 3 and num != 5:
                    self._webSocketDataTransfer.send(currentRequestToJsonStr)
                    self._retryEvent.wait(3)
                else:
                    self.driver.switchToNextPage()
                    time.sleep(WAIT_REFLESH_2_SECOND)
                    self.logger.debug('switch when request:  ' +
                                      currentRequestToJsonStr)
                    self._webSocketDataTransfer.send(currentRequestToJsonStr)
                    self._retryEvent.wait(WAIT_REFLESH_2_SECOND)

                if self._readWriteSyncEvent.isSet():
                    break

        self._readWriteSyncEvent.wait(timeout=timeout)
        self._readWriteSyncEvent.clear()
        self._retryEvent.clear()

        self._checkReturnOrThrow(self._currentRequest)

        return self._currentRequest

    def quit(self):
        self._quit = True
        self.disconnect()

    def isQuit(self):
        return self._quit

    def _checkReturnOrThrow(self, netWorkRequest):
        needThrown = (netWorkRequest.getResponse() is None
                      and netWorkRequest.getException() is None)
        if needThrown:
            errorMsg = ErrorMsgManager().errorCodeToString(
                ERROR_CODE_BAD_REQUEST)
            raise RuntimeError("%s, request data {%s}" %
                               (errorMsg, netWorkRequest.getRequestData()))

    def _waitForConnectOrThrow(self):
        if self._webSocketDataTransfer.isConnected():
            return

        self._connectSyncEvent.wait(WAIT_REFLESH_60_SECOND)

        if not self._webSocketDataTransfer.isConnected():
            errorMsg = ErrorMsgManager().errorCodeToString(
                ERROR_CODE_CONNECT_CLOSED)
            raise RuntimeError(
                "connect %s timeout, %s" %
                (self._webSocketDataTransfer.getUrl(), errorMsg))

    def onMessage(self, message):
        if self._currentRequest is None:
            raise RuntimeError("current request is None")
        if message is not None:
            self._currentRequest.addResponse(message)
            # 只有当response带有id,所有response才接收完
            if json.loads(message).get('id') is not None:
                self._retryEvent.set()
                self._readWriteSyncEvent.set()
            self.logger.debug(' ---> ' + message)
        else:
            self.logger.debug(' --->' + 'message is None')
Exemplo n.º 5
0
class H5Driver(object):
    def __init__(self, d, device=None):
        self._device = device
        self._urlFetcher = None
        self._webSocketDataTransfer = None
        self._vmShutdownHandler = VMShutDownHandler()
        self._networkHandler = None

        self._pageOperator = H5PageOperator()
        self.d = d
        self._hasInit = False

        self.html = None
        self.bodyNode = None

    def initDriver(self):
        if self._hasInit:
            return
        self._executor = SingleThreadExecutor()
        self._vmShutdownHandler.registerToVM()
        UncaughtExceptionHandler.init()
        UncaughtExceptionHandler.registerUncaughtExceptionCallback(
            self._onUncaughtException)

        self._selectDevice()
        self._urlFetcher = H5WebSocketDebugUrlFetcher(device=self._device)
        url = self._urlFetcher.fetchWebSocketDebugUrl()

        self._webSocketDataTransfer = WebSocketDataTransfer(url=url)
        self._networkHandler = ShortLiveWebSocket(self._webSocketDataTransfer,
                                                  self._executor, self)
        self._networkHandler.connect()

        self._executor.put(self._networkHandler.receive)
        self.logger = Log().getLogger()
        self.initPageDisplayData()
        self._hasInit = True
        self.logger.info('url ----> ' + url)

    def _selectDevice(self):
        devicesList = AdbHelper.listDevices(ignoreUnconnectedDevices=True)
        # 假如没有指定设备,那么判断当前机器是否连接1个设备
        if self._device is None:
            devicesCount = len(devicesList)
            errorMsg = None

            if devicesCount <= 0:
                errorMsg = ErrorMsgManager().errorCodeToString(
                    ERROR_CODE_DEVICE_NOT_CONNECT)

            elif devicesCount == 1:
                self._device = devicesList[0]

            else:
                errorMsg = ErrorMsgManager().errorCodeToString(
                    ERROR_CODE_MULTIPLE_DEVICE)

            if errorMsg is not None:
                raise RuntimeError(errorMsg)

    def wait(self, seconds=1):
        time.sleep(seconds)

    def _onUncaughtException(self, exctype, value, tb):
        self.close()

    def close(self):
        if self._networkHandler is not None:
            self._networkHandler.quit()

        if self._executor is not None:
            self._executor.shutDown()

        self._hasInit = False

        UncaughtExceptionHandler.removeHook()

    def addShutDownHook(self, func, *args, **kwargs):
        self._vmShutdownHandler.registerVMShutDownCallback(
            func, *args, **kwargs)

    def getDriverType(self):
        return constant.H5DRIVER

    def switchToNextPage(self):
        """
        把之前缓存的html置为none
        再重新连接websocket
        """
        self.logger.debug('')
        self.html = None
        self._networkHandler.disconnect()
        self._networkHandler.connect()

    def initPageDisplayData(self):
        driverInfo = self.d.info
        displayDp = driverInfo.get('displaySizeDpY')
        displayPx = driverInfo.get('displayHeight')
        self.scale = (displayPx - 0.5) / displayDp
        windowHeight = self.getWindowHeight()
        self.appTitleHeight = displayDp - windowHeight

    def changeDp2Px(self, xDp, yDp):
        xPx, yPx = self._pageOperator.changeDp2Px(xDp, yDp, self.scale,
                                                  self.appTitleHeight)
        return xPx, yPx

    def clickElementByXpath(self,
                            xpath,
                            visibleItemXpath=None,
                            duration=50,
                            tapCount=1):
        """
        默认滑动点为屏幕的中心,且边距为整个屏幕。当有container时,传入container中任意一个当前可见item的xpath,之后会将目标滑到该可见item的位置
        :param xpath: 要滑动到屏幕中控件的xpath
        :param visibleItemXpath: container中当前可见的一个xpath
        :return:
        """
        self.logger.info('xpath ---> ' + xpath)
        # 防止websocket未失效但页面已经开始跳转
        self.wait(WAIT_REFLESH_05_SECOND)

        if self.isElementExist(xpath):
            self.scrollToElementByXpath(xpath, visibleItemXpath)

            sendStr = self._pageOperator.getElementRect(xpath)
            self._networkHandler.send(sendStr)

            x = self._getRelativeDirectionValue("x")
            y = self._getRelativeDirectionValue("y")

            self.logger.debug('clickElementByXpath --> x:' + str(x) + '   y:' +
                              str(y))
            clickCommand = self._pageOperator.clickElementByXpath(
                x, y, duration, tapCount)
            return self._networkHandler.send(clickCommand)
        else:
            raise TypeError('未找到控件')

    def clickFirstElementByText(self,
                                text,
                                visibleItemXpath=None,
                                duration=50,
                                tapCount=1):
        """
        通过text来搜索,点击第一个text相符合的控件。参数同clickElementByXpath()
        """
        self.clickElementByXpath('.//*[text()="' + text + '"]',
                                 visibleItemXpath, duration, tapCount)

    def longClickElementByXpath(self,
                                xpath,
                                visibleItemXpath=None,
                                duration=2000,
                                tapCount=1):
        self.clickElementByXpath(xpath, visibleItemXpath, duration, tapCount)

    def repeatedClickElementByXpath(self,
                                    xpath,
                                    visibleItemXpath=None,
                                    duration=50,
                                    tapCount=2):
        self.clickElementByXpath(xpath, visibleItemXpath, duration, tapCount)

    def scrollToElementByXpath(self, xpath, visibleItemXpath=None, speed=400):
        """
        滑动屏幕,使指定xpath的控件可见
        默认滑动点为屏幕的中心,且边距为整个屏幕。当有container时,传入container中任意一个当前可见item的xpath,之后会将目标滑到该可见item的位置
        :param xpath: 要滑动到屏幕中控件的xpath
        :param visibleItemXpath: container中当前可见的一个xpath
        """
        self.logger.info('xpath ---> ' + xpath)
        sendStr = self._pageOperator.getElementRect(xpath)
        self._networkHandler.send(sendStr)
        top = self._getRelativeDirectionValue("topp")
        bottom = self._getRelativeDirectionValue("bottom")
        left = self._getRelativeDirectionValue("left")
        right = self._getRelativeDirectionValue("right")

        if visibleItemXpath is None:
            endTop = 0
            endLeft = 0
            endBottom = self.getWindowHeight()
            endRight = self.getWindowWidth()
        else:
            containerSendStr = self._pageOperator.getElementRect(
                visibleItemXpath)
            self._networkHandler.send(containerSendStr)

            endTop = self._getRelativeDirectionValue("topp")
            endBottom = self._getRelativeDirectionValue("bottom")
            endLeft = self._getRelativeDirectionValue("left")
            endRight = self._getRelativeDirectionValue("right")
        '''
        竖直方向的滑动
        '''
        if bottom > endBottom:
            scrollYDistance = endBottom - bottom
        elif top < 0:
            scrollYDistance = -(top - endTop)
        else:
            scrollYDistance = 0

        if scrollYDistance < 0:
            self.scrollWindow(int((endLeft + endRight) / 2),
                              int((endTop + endBottom) / 2), 0,
                              scrollYDistance - 80, speed)
        elif scrollYDistance > 0:
            self.scrollWindow(int((endLeft + endRight) / 2),
                              int((endTop + endBottom) / 2), 0,
                              scrollYDistance + 80, speed)
        else:
            self.logger.debug('y方向不需要滑动')
        '''
        水平方向的滑动
        '''
        if right > endRight:
            scrollXDistance = endRight - right
        elif left < 0:
            scrollXDistance = -(left - endLeft)
        else:
            scrollXDistance = 0

        if scrollXDistance != 0:
            self.scrollWindow(int((endLeft + endRight) / 2),
                              int((endTop + endBottom) / 2), scrollXDistance,
                              0, speed)
        else:
            self.logger.debug('y方向不需要滑动')

    def getElementCoordinateByXpath(self, elementXpath):
        '''
        获得Element的坐标
        :param elementXpath:待获取坐标的元素的xpath
        :return:element相对于整个屏幕的x、y坐标,单位为px
        '''
        self.logger.info('elementXpathXpath ---> ' + elementXpath)
        # 防止websocket未失效但页面已经开始跳转
        self.wait(WAIT_REFLESH_05_SECOND)
        if self.isElementExist(elementXpath):
            sendStr = self._pageOperator.getElementRect(elementXpath)
            self._networkHandler.send(sendStr)
            x = self._getRelativeDirectionValue("x")
            y = self._getRelativeDirectionValue("y")
            x, y = self.changeDp2Px(x, y)
            return x, y
        errorMessage = ErrorMsgManager().errorCodeToString(
            ERROR_CODE_GETCOORDINATE)
        raise RuntimeError(errorMessage)

    def clearInputTextByXpath(self, xpath):
        '''
        清空输入框的文字
        :param xpath:input框的xpath
        '''
        self.logger.info('xpath ---> ' + xpath)
        clearInputTextSendStr = self._pageOperator.clearInputTextByXpath(xpath)
        self._networkHandler.send(clearInputTextSendStr)

    def getWindowHeight(self):
        '''
        :return:手机屏幕的高度
        '''
        getWindowHeightCmd = self._pageOperator.getWindowHeight()
        resultValueDict = self._networkHandler.send(
            getWindowHeightCmd).getResponse()[0]
        resultValue = resultValueDict['result']['result']['value']
        return resultValue

    def getWindowWidth(self):
        '''
        :return:手机屏幕的宽度
        '''
        getWindowWidthCmd = self._pageOperator.getWindowWidth()
        resultValueDict = self._networkHandler.send(
            getWindowWidthCmd).getResponse()[0]
        resultValue = resultValueDict['result']['result']['value']
        return resultValue

    def scrollWindow(self, x, y, xDistance, yDistance, speed=800):
        """
        通过坐标来滑动(屏幕的左上角为(0,0),向下和向右坐标逐渐增大)
        :param x: 滑动的起始X点坐标
        :param y: 滑动的起始Y点坐标
        :param xDistance: X方向滑动的距离
        :param yDistance: Y方向滑动的距离
        :param speed: 滑动的速度
        """
        sendStr = self._pageOperator.scrollWindow(x, y, xDistance, yDistance,
                                                  speed)
        return self._networkHandler.send(sendStr)

    def _getRelativeDirectionValue(self, directionKey='topp', contextId=None):
        '''
        获取相关的方向数据参数值
        :param directionKey: 获取的方向
        :return:
        '''
        directionCommand = self._pageOperator.getJSValue(
            directionKey, contextId)
        directionResponse = self._networkHandler.send(
            directionCommand).getResponse()[0]
        directionValue = directionResponse['result']['result']['value']
        return directionValue

    def textElementByXpath(self, xpath, text):
        """
        先获取输入框的焦点, 再使用Chrome debug协议的输入api,再输入文字内容
        :param xpath:输入框的xpath
        :parm text:要输入的文字
        """
        self.logger.info('xpath ---> ' + xpath + ' text ---> ' + text)
        self.focusElementByXpath(xpath)
        sendStrList = self._pageOperator.textElementByXpath(text)

        for command in sendStrList:
            self._networkHandler.send(command)
        self.wait(WAIT_REFLESH_05_SECOND)

    def focusElementByXpath(self, xpath):
        """
        调用目标的focus()方法。
        :param xpath:目标的xpath
        """
        executeCmd = self._pageOperator.focusElementByXpath(xpath)
        self._networkHandler.send(executeCmd)

    def getHtml(self, nodeId=1):
        """
        获得指定nodeId的Html代码。在一条websocket连接中,方法只能够执行一次。
        :param nodeId: getDocument方法返回的nodeId,当为1时,返回整个body的代码
        """
        self.logger.info('')
        if self.html is not None:
            self.switchToNextPage()

        self.getDocument()

        sendStr = self._pageOperator.getHtml(nodeId)
        self.html = self._networkHandler.send(
            sendStr).getResponse()[0]['result']['outerHTML']
        return self.html

    def getDocument(self):
        """
        获得getHtml中需要的nodeId
        在调用getHtml之前必须先调用这个方法
        """
        sendStr = self._pageOperator.getDocument()
        return self._networkHandler.send(sendStr)

    def closeWindow(self):
        """
        关闭整个h5页面
        """
        sendStr = self._pageOperator.closeWindow()
        return self._networkHandler.send(sendStr)

    def returnLastPage(self):
        """
        返回上一页
        """
        self.logger.info('')
        self.wait(WAIT_REFLESH_05_SECOND)
        sendStr = self._pageOperator.returnLastPage()
        self._networkHandler.send(sendStr)
        self.wait(WAIT_REFLESH_05_SECOND)

    def scrollPickerByXpath(self, xpath):
        """
        滑动选择picker的选项
        要获取四个变量,所以发送了四次消息。
        1.先定位到要点击的item
        2.找到它的parent(即整个picker的content区域),并且获得要点击item的index,和总共item的个数
        3.获得picker的BoundingClientRect。再计算每个item的位置,要滑动的距离
        4.因为picker窗口弹出时,默认选择上一次的item,所以先进行一次滑动到顶端。
        (为了避免滑动时的惯性,需要设置一个speed属性,控制速度)
        :param xpath: 要选择的item
        """
        self.logger.info('xpath ---> ' + xpath)

        sendStr = self._pageOperator.getPickerRect(xpath)
        self._networkHandler.send(sendStr)

        startScrollX = self._getRelativeDirectionValue("start_scroll_x")
        startScrollY = self._getRelativeDirectionValue("start_scroll_y")

        # 获取整个列表的高度
        height = self._getRelativeDirectionValue("height")

        # 获取要滑动的距离
        dex = self._getRelativeDirectionValue("dex")

        scrollWindowCmd = self._pageOperator.scrollWindow(
            startScrollX, startScrollY, startScrollX, height)
        self._networkHandler.send(scrollWindowCmd)

        scrollWIndowWithSpeedCmd = self._pageOperator.scrollWindow(
            startScrollX, startScrollY, startScrollX, -dex, speed=72)
        self._networkHandler.send(scrollWIndowWithSpeedCmd)

    def getPageHeight(self):
        getPageHeightCmd = self._pageOperator.getPageHeight()
        resultValueDict = self._networkHandler.send(
            getPageHeightCmd).getResponse()[0]
        resultValue = resultValueDict['result']['result']['value']
        return resultValue

    def getElementTextByXpath(self, xpath):
        '''
        :param xpath: 目标的xpath
        :return: 获取到的目标text内容
        '''
        self.logger.info('xpath ---> ' + xpath)
        if self.isElementExist(xpath):
            getTextCmd = self._pageOperator.getElementTextByXpath(xpath)
            resultValueDict = self._networkHandler.send(
                getTextCmd).getResponse()[0]
            resultValue = resultValueDict['result']['result']['value'].encode(
                "utf-8")
        else:
            resultValue = None
        return resultValue

    def getElementSrcByXpath(self, xpath):
        """
        :param xpath: 目标的xpath
        :return: 获取到的目标src内容
        """
        self.logger.info('xpath ---> ' + xpath)
        if self.isElementExist(xpath):
            getSrcCmd = self._pageOperator.getElementSrcByXpath(xpath)
            resultValueDict = self._networkHandler.send(
                getSrcCmd).getResponse()[0]
            resultValue = resultValueDict['result']['result']['value'].encode(
                "utf-8")
        else:
            resultValue = None
        return resultValue

    def getElementClassNameByXpath(self, xpath):
        '''
        :param xpath:目标的xpath
        :return: 目标的className
        '''
        self.logger.info('xpath ---> ' + xpath)
        if self.isElementExist(xpath):
            getClassNameCmd = self._pageOperator.getElementClassNameByXpath(
                xpath)
            resultValueDict = self._networkHandler.send(
                getClassNameCmd).getResponse()[0]
            resultValue = resultValueDict['result']['result']['value'].encode(
                "utf-8")
        else:
            resultValue = None
        return resultValue

    def getElementByXpath(self, xpath):
        """
        :param:目标的xpath
        :return:返回lxml包装过的element对象,可以使用lxml语法获得对象的信息
        例如:可以使用element.get("attrs")来拿到属性的数据
        可以用element.text拿到它的文字
        当element中含有列表时,使用for循环读取每一个item
        """
        self.logger.info('xpath ---> ' + xpath)
        html = etree.HTML(self.getHtml())
        return html.xpath(xpath)[0]

    def isElementExist(self, xpath, contextId=None):
        """
        :param xpath: 目标的xpath
        :return: 返回一个boolean,该Element是否存在
        """
        self.logger.info('xpath ---> ' + xpath)
        getExistCmd = self._pageOperator.isElementExist(xpath, contextId)
        resultValueDict = self._networkHandler.send(
            getExistCmd).getResponse()[0]
        resultType = resultValueDict['result']['result']['subtype']
        num = 0
        while resultType == 'null' and num < 3:
            self.wait(WAIT_REFLESH_2_SECOND)
            getExistCmd = self._pageOperator.isElementExist(xpath, contextId)
            resultValueDict = self._networkHandler.send(
                getExistCmd).getResponse()[0]
            resultType = resultValueDict['result']['result']['subtype']
            num = num + 1
        return resultType != 'null'

    def navigateToPage(self, url):
        """
        跳转到指定url,在某些微信版本上不生效
        :param url: 要跳转的url
        """
        self.logger.info('url ---> ' + url)
        navigateCmd = self._pageOperator.navigateToPage(url)
        self._networkHandler.send(navigateCmd)

    def executeScript(self, script):
        """
        手动发送js命令并执行
        :param script:要执行的js指令
        :return: 执行结果
        """
        executeCmd = self._pageOperator.executeScript(script)
        resultValueDict = self._networkHandler.send(
            executeCmd).getResponse()[0]

        return resultValueDict

    def getCurrentPageUrl(self):
        """
        获得当前页面的url
        :return:
        """
        executeCmd = self._pageOperator.getCurrentPageUrl()
        resultValueDict = self._networkHandler.send(
            executeCmd).getResponse()[0]
        resultValue = resultValueDict['result']['result']['value'].encode(
            "utf-8")
        return resultValue

    '''
    针对跨域IFrame进行的操作
    '''

    def _getAllNodeId(self):
        '''
        获得body标签中所有的包含IFrame数据的NodeId
        :return:
        '''
        nodeIdList = []
        nodesList = self.getBodyNode()['params']['nodes']
        for node in nodesList:
            if node.get('contentDocument') is not None:
                nodeIdList.append(node.get('contentDocument').get('nodeId'))
        return nodeIdList

    def getBodyNode(self):
        '''
        :return:获得body中所有node的frameId
        '''
        self.switchToNextPage()
        self.wait(WAIT_REFLESH_2_SECOND)
        self.getDocument()
        executeCmd = self._pageOperator.getBodyNode()
        resultValueDict = self._networkHandler.send(
            executeCmd).getResponse()[0]
        return resultValueDict

    def requestChildNodes(self, nodeId=5):
        '''
        :param nodeId:指定的nodeId
        :return:
        '''
        self.switchToNextPage()
        self.wait(WAIT_REFLESH_2_SECOND)
        self.getDocument()
        executeCmd = self._pageOperator.requestChildNodes(nodeId)
        resultValueDict = self._networkHandler.send(
            executeCmd).getResponse()[0]
        return resultValueDict

    def getIFrameContextId(self):
        '''
        当body标签中只存在一个IFrame调用
        :return:ContextId
        '''
        time.sleep(5)
        frameIdList = self.getAllIFrameNode()
        contextList = self.getAllContext()
        if len(frameIdList) == 1:
            for contextInfo in contextList:
                if contextInfo["frameId"] == frameIdList[0]:
                    return contextInfo["contextId"]

    def getAllContext(self):
        '''
        获得所有的Context 
        :return: 所有的ContextId,域以及FrameId
        '''
        executeCmd = self._pageOperator.getAllContext()
        resultValueDict = self._networkHandler.send(executeCmd).getResponse()
        resultDictList = []
        for dict in resultValueDict:
            if dict.get('result') is None:
                resultDict = {}
                if dict.get('params').get('context') is not None:
                    context = dict.get('params').get('context')
                    origin = context.get('origin')
                    contextId = context.get('id')
                    frameId = context.get('auxData').get('frameId')
                    resultDict['origin'] = origin
                    resultDict['contextId'] = contextId
                    resultDict['frameId'] = frameId
                    resultDictList.append(resultDict)
        return resultDictList

    def getAllIFrameNode(self):
        '''
        获得body标签中所有的IFrameNode
        :return:
        '''
        try:
            iFrameNodeList = []
            nodesList = self.getBodyNode()['params']['nodes']
            for node in nodesList:
                if node['nodeName'] == 'IFRAME':
                    iFrameNodeList.append(node)
            frameIdList = []
            for iFrameNode in iFrameNodeList:
                frameIdList.append(iFrameNode['frameId'])
            return frameIdList
        except:
            errorMessage = ErrorMsgManager().errorCodeToString(
                ERROR_CODE_SETUP_FRAME_PAGE)
            raise RuntimeError(errorMessage)

    def getIFrameNodeId(self):
        '''
        当body标签中只存在一个IFrame调用
        :return:IFrame的NodeId,可以通过它获得html
        '''
        nodeIdList = self._getAllNodeId()
        if len(nodeIdList) == 1:
            return nodeIdList[0]

    def getIFrameElementCoordinateByXpath(self, elementXpath, iFrameXpath,
                                          contextId):
        '''
        获得IFrame中元素的坐标
        :param elementXpath:待获取坐标的元素的xpath
        :param iFrameXpath: 外层iFrame的xpath
        :param contextId: iFrame的ContextId
        :return:element相对于整个屏幕的x、y坐标,单位为px
        '''
        self.logger.info('elementXpathXpath ---> ' + elementXpath +
                         ' iFrameXpath ---> ' + iFrameXpath +
                         ' contextId ---> ' + str(contextId))
        # 防止websocket未失效但页面已经开始跳转
        self.wait(WAIT_REFLESH_05_SECOND)
        if self.isElementExist(iFrameXpath):
            sendStr = self._pageOperator.getElementRect(iFrameXpath)
            self._networkHandler.send(sendStr)
            iframeLeft = self._getRelativeDirectionValue("left")
            iframeTop = self._getRelativeDirectionValue("topp")
            if self.isElementExist(elementXpath, contextId):
                sendStr = self._pageOperator.getElementRect(
                    elementXpath, contextId)
                self._networkHandler.send(sendStr)
                x = self._getRelativeDirectionValue("x", contextId)
                y = self._getRelativeDirectionValue("y", contextId)
                x = iframeLeft + x
                y = iframeTop + y
                x, y = self.changeDp2Px(x, y)
                return x, y
        errorMessage = ErrorMsgManager().errorCodeToString(
            ERROR_CODE_GETCOORDINATE)
        raise RuntimeError(errorMessage)

    def getIFrameHtml(self, nodeId=None):
        '''
        :param nodeId:
        :return: 获得指定nodeId的Html
        '''

        self.logger.info('')

        self.switchToNextPage()

        self.getDocument()

        self.wait(WAIT_REFLESH_05_SECOND)
        sendStr = self._pageOperator.requestChildNodes()
        self._networkHandler.send(sendStr)

        if nodeId is None:
            nodeId = self.getIFrameNodeId()
        sendStr = self._pageOperator.getHtml(nodeId)

        result = self._networkHandler.send(sendStr).getResponse()[0]
        self.html = result.get('result').get('outerHTML')
        return self.html

    def getIFrameElementByXpath(self, xpath, nodeId):
        '''
        :param xpath:element的Xpath
        :param nodeId: element所在页面的nodeId
        :return: lxml的ELement对象
        '''
        self.logger.info('xpath ---> ' + xpath)
        html = etree.HTML(self.getIFrameHtml(nodeId))
        self.logger.info(etree.tostring(html))
        return html.xpath(xpath)[0]

    '''
    性能数据
    '''

    def getMemoryInfo(self):
        '''
        获得H5进程占用内存信息
        :return: H5进程占用内存信息
        '''
        self.logger.info('')
        ADB_SHELL = 'adb shell '
        if self._device:
            ADB_SHELL = ADB_SHELL.replace("adb", "adb -s %s" % self._device)
        GET_MEMORY_INFO = ADB_SHELL + 'dumpsys meminfo com.tencent.mm:tools'
        return commandHelper.runCommand(GET_MEMORY_INFO)

    def getCPUInfo(self):
        '''
        获得H5进程占用CPU信息
        :return: H5进程占用CPU信息
        '''
        self.logger.info('')
        ADB_SHELL = 'adb shell '
        if self._device:
            ADB_SHELL = ADB_SHELL.replace("adb", "adb -s %s" % self._device)
        GET_CPU_INFO = ADB_SHELL + ' top -n 1 | grep com.tencent.mm:tools'
        return commandHelper.runCommand(GET_CPU_INFO)

    def setDebugLogMode(self):
        Log().setDebugLogMode()
Exemplo n.º 6
0
class WxWebSocketDebugUrlFetcher(object):
    DEFAULT_LOCAL_FORWARD_PORT = 9223
    # 根据dict中存储的数量来获得新添加页面的名称
    pageMap = {
        1: 'event',
        2: 'first',
        3: 'second',
        4: 'third',
        5: 'fourth',
        6: 'fifth'
    }

    def __init__(self, device, localForwardPort=None):
        self._device = device
        self._localForwardPort = localForwardPort if localForwardPort is not None \
            else WxWebSocketDebugUrlFetcher.DEFAULT_LOCAL_FORWARD_PORT
        self._webSocketDebugUrl = None
        # 通过dic来管理页面
        self.pageUrlDict = bidict.bidict()
        self.logger = Log().getLogger()
        self.mode = MODE_NORMAL

    def fetchWebSocketDebugUrl(self, refetch=False):
        # 如果没有获取或者需要重新获取debug url,那么获取一次,否则直接返回
        if self._webSocketDebugUrl is None:
            self._fetchInner()
        if refetch:
            self._webSocketDebugUrl = self._fetchWebSocketDebugUrl(
                self._localForwardPort)
        return self._webSocketDebugUrl

    def getDevice(self):
        return self._device

    def getForwardPort(self):
        return self._localForwardPort

    def _fetchInner(self):
        # 先获取微信appbrand进程Pid
        pid = WxWebSocketDebugUrlFetcher._fetchWeixinToolsProcessPid(
            device=self._device)

        # 重定向端口
        WxWebSocketDebugUrlFetcher._forwardLocalPort(self._localForwardPort,
                                                     pid,
                                                     device=self._device)

        # 获取本地http://localhost:{重定向端口}/json返回的json数据,提取里面的webSocketDebuggerUrl字段值
        self._webSocketDebugUrl = self._fetchWebSocketDebugUrl(
            self._localForwardPort)

    @staticmethod
    def _fetchWeixinToolsProcessPid(device=None):
        osName = OS.getPlatform()
        cmd = _ADB_GET_TOP_ACTIVITY_CMD[osName]
        stdout, stdError = runCommand(AdbHelper.specifyDeviceOnCmd(
            cmd, device))
        print "fetchWeixinToolsProcessActivity:\n", stdout
        mmActivity = stdout.split('com.tencent.mm/')
        pid = 0
        for activity in mmActivity:
            if '.plugin.appbrand.ui.AppBrandInToolsUI' in activity:
                # 小米
                strlist = activity.split('pid=')
                pid = strlist[1].split("\r\n")[0]
                break
            elif '.plugin.appbrand.ui.AppBrandUI' in activity:
                # VIVO
                strlist = activity.split('pid=')
                pid = strlist[1].split("\r\n")[0]
                break
        webviewCmd = _ADB_GET_WEBVIEW_TOOLS_CMD[osName] % (pid)
        print "pid:", pid

        if pid == 0:  # 维持原有逻辑
            strlist = stdout.split('pid=')
            pid = strlist[1].split("\r\n")[0]

        # 验证是否启动了小程序webview
        try:
            webStdout, webStdError = runCommand(
                AdbHelper.specifyDeviceOnCmd(webviewCmd, device))
        except:
            errorMsg = ErrorMsgManager().errorCodeToString(
                ERROR_CODE_NOT_ENTER_XCX)
            raise RuntimeError(errorMsg)
        return pid

    @staticmethod
    def _forwardLocalPort(localPort, pid, device=None):
        cmd = "adb forward tcp:%s localabstract:webview_devtools_remote_%s" % (
            localPort, pid)
        runCommand(AdbHelper.specifyDeviceOnCmd(cmd, device))

    def _fetchWebSocketDebugUrl(self, localPort):
        import time
        localUrl = "http://localhost:%s/json" % localPort
        errorMsg = None
        resultUrl = None

        # 去掉代理
        urllib2.getproxies = lambda: {}

        try:
            nums = 0
            while None == resultUrl and nums < 8:
                response = urllib2.urlopen(localUrl)
                responseJson = json.loads(response.read())
                if len(responseJson) != 1:

                    self._cleanJsonData(responseJson)

                    resultUrl = self._handleAndReturnWebSocketDebugUrl(
                        responseJson)
                    self.logger.debug('websocket --> ' + resultUrl)
                    return resultUrl
                else:
                    nums = nums + 1
                    self.logger.debug('retry fetch num ---> ' + str(nums))
                    time.sleep(1)

        except IndexError:
            errorMsg = ErrorMsgManager().errorCodeToString(
                ERROR_CODE_NOT_ENABLE_DEBUG_MODE)
        except LookupError:
            errorMsg = ErrorMsgManager().errorCodeToString(
                ERROR_CODE_NOT_ENTER_XCX)
        except AttributeError:
            errorMsg = ErrorMsgManager().errorCodeToString(
                ERROR_CODE_NOT_GET_XCX_PAGE_INFO)
        except Exception:
            errorMsg = ErrorMsgManager().errorCodeToString(
                ERROR_CODE_CONFIG_PROXY)
        raise RuntimeError(errorMsg)

    # 去掉file://开头的脏数据
    def _cleanJsonData(self, responseJson):
        removeList = []
        for response in responseJson:
            if u'file:///' in response['url']:
                removeList.append(response)
        for i in removeList:
            responseJson.remove(i)

    def _handleAndReturnWebSocketDebugUrl(self, responseJson):
        responseLength = len(responseJson)
        pageUrlDictLength = len(self.pageUrlDict)
        # 如果第一次进入小程序,当前加载的dict为空
        if pageUrlDictLength == 0:
            # 考虑正常的小程序,有两个链接,一个是空的server,一个是首页面。
            if responseLength == 2:
                self.mode = MODE_NORMAL
                return self._initWebSocketUrlWithTwoPage(responseJson)
            # 如果是小程序内嵌H5的情况,有三个链接,其中两个ServiceWeChat的URL,一个H5的URL。
            if responseLength == 3:
                self.mode = MODE_EMBEDDED_H5
                return self._initWebSocketUrlWithThreePage(responseJson)

        # 如果返回的大于当前打开的,说明要开启新的页面
        # 有两种情况,一种是打开正常的小程序页面,启动一个新链接,另一种是打开嵌套H5的小程序页面,会启动两个链接
        elif responseLength > pageUrlDictLength:
            if responseLength - pageUrlDictLength == 1:
                for i in range(responseLength):
                    message = self._getPageFeature(
                        responseJson[i]["webSocketDebuggerUrl"])
                    if self.pageUrlDict.inv.get(message) is None:
                        self.pageUrlDict.update(
                            {self.pageMap.get(pageUrlDictLength + 1): message})
                        return responseJson[i]["webSocketDebuggerUrl"]
            elif responseLength - pageUrlDictLength == 2:
                h5Url = ''
                for i in range(responseLength):
                    message = self._getPageFeature(
                        responseJson[i]["webSocketDebuggerUrl"])
                    if self.pageUrlDict.inv.get(message) is None:
                        self.pageUrlDict.update(
                            {self.pageMap.get(pageUrlDictLength + 1): message})
                        if 'https://servicewechat.com/' not in responseJson[i][
                                "url"]:
                            h5Url = responseJson[i]["webSocketDebuggerUrl"]
                return h5Url
            else:
                raise AttributeError()

        # 如果返回的小于当前打开的,说明要删除一个页面
        # 有两种情况,一种是关闭正常的小程序页面,删除一个链接,另一种是关闭嵌套H5的小程序页面,需要删除两个链接
        elif responseLength < pageUrlDictLength:
            if pageUrlDictLength - responseLength == 1:
                sencondLastPageMessage = self.pageUrlDict.get(
                    self.pageMap.get(pageUrlDictLength - 1))
                shouldReturnUrl = None
                del self.pageUrlDict[self.pageMap.get(pageUrlDictLength)]
                for i in range(responseLength):
                    message = self._getPageFeature(
                        responseJson[i]["webSocketDebuggerUrl"])
                    if message == sencondLastPageMessage:
                        shouldReturnUrl = responseJson[i][
                            "webSocketDebuggerUrl"]
                return shouldReturnUrl
            elif pageUrlDictLength - responseLength == 2:
                sencondLastPageMessage = self.pageUrlDict.get(
                    self.pageMap.get(pageUrlDictLength - 2))
                shouldReturnUrl = None
                del self.pageUrlDict[self.pageMap.get(pageUrlDictLength)]
                del self.pageUrlDict[self.pageMap.get(pageUrlDictLength - 1)]
                for i in range(responseLength):
                    message = self._getPageFeature(
                        responseJson[i]["webSocketDebuggerUrl"])
                    if message == sencondLastPageMessage:
                        shouldReturnUrl = responseJson[i][
                            "webSocketDebuggerUrl"]
                return shouldReturnUrl

        # 如果返回的等于当前打开的,有两种情况。一种是两个URL时,返回最后一个webSocketUrl。一种是当有三个URL,小程序内嵌H5 URL时,返回内嵌的H5 URL。
        elif responseLength == pageUrlDictLength:
            if self.mode == MODE_NORMAL:
                lastPageMessage = self.pageUrlDict.get(
                    self.pageMap.get(pageUrlDictLength))
                for i in range(responseLength):
                    message = self._getPageFeature(
                        responseJson[i]["webSocketDebuggerUrl"])
                    if message == lastPageMessage:
                        return responseJson[i]["webSocketDebuggerUrl"]
                #     如果都为空,则在当前页面进行了跳转,因此要更新dict中的特征
                # 依次连接返回的所有page的websocketurl,找到一个更新后的页面
                for response in responseJson:
                    websocketUrl = response["webSocketDebuggerUrl"]
                    pageFeature = self._getPageFeature(websocketUrl)

                    if self.pageUrlDict.inv.get(pageFeature) is None:
                        self.pageUrlDict.update(
                            {self.pageMap.get(pageUrlDictLength): pageFeature})
                        return websocketUrl
            else:
                return self.pageUrlDict.get(1)

    def _initWebSocketUrlWithTwoPage(self, responseJson):
        eventData = u'[object Text][object HTMLDivElement][object Text]'
        responseLength = 2
        pageUrlDictLength = len(self.pageUrlDict)
        for i in range(responseLength):
            message = self._getPageFeature(
                responseJson[i]["webSocketDebuggerUrl"])
            if message == eventData:
                self.pageUrlDict.update(
                    {self.pageMap.get(pageUrlDictLength + 1): message})
                if i == 0:
                    firstMessage = self._getPageFeature(
                        responseJson[1]["webSocketDebuggerUrl"])
                    self.pageUrlDict.update({
                        self.pageMap.get(pageUrlDictLength + 2):
                        firstMessage
                    })
                    return responseJson[1]["webSocketDebuggerUrl"]
                else:
                    firstMessage = self._getPageFeature(
                        responseJson[0]["webSocketDebuggerUrl"])
                    self.pageUrlDict.update({
                        self.pageMap.get(pageUrlDictLength + 2):
                        firstMessage
                    })
                    return responseJson[0]["webSocketDebuggerUrl"]

    def _initWebSocketUrlWithThreePage(self, responseJson):
        responseWesocketUrlDict = {}
        responseUrlList = []
        serviceUrlList = []
        h5Url = []
        responseLength = 3

        for i in range(responseLength):
            response = responseJson[i]
            url = response['url']
            webSocketUrl = response['webSocketDebuggerUrl']
            responseUrlList.append(url)
            responseWesocketUrlDict[url] = webSocketUrl
        for url in responseUrlList:
            if u"servicewechat" in url:
                serviceUrlList.append(url)
            else:
                h5Url.append(url)
        if len(serviceUrlList) != 2 or len(h5Url) != 1:
            raise AttributeError()
        else:
            url = ''
            for url in serviceUrlList:
                webSocketUrl = responseWesocketUrlDict.get(url)
                print webSocketUrl
                webSocketConn = create_connection(url=webSocketUrl)
                webSocketConn.send(
                    '{"id": 1,"method": "Runtime.evaluate","params": {"expression": "srcs = document.body.childNodes[0].getAttribute(\'src\')"}}'
                )
                results = webSocketConn.recv()
                result = json.loads(results)
                webSocketConn.close()
                if result.get('result').get('result').get('type') == u'string':
                    url = result.get('result').get('result').get('value')
                    break
            if url not in h5Url[0]:
                raise AttributeError()
            else:
                self.pageUrlDict.forceupdate(
                    {1: responseWesocketUrlDict.get(h5Url[0])})
                self.pageUrlDict.forceupdate(
                    {2: responseWesocketUrlDict.get(serviceUrlList[0])})
                self.pageUrlDict.forceupdate(
                    {3: responseWesocketUrlDict.get(serviceUrlList[1])})
                return responseWesocketUrlDict.get(h5Url[0])

    def _getPageFeature(self, url):
        import time
        retry = True
        nums = 0
        message = ''
        while (retry and nums < 8):
            try:
                webSocketConn = create_connection(url=url)

                webSocketConn.send(
                    '{"id": 1,"method": "Runtime.evaluate","params": {"expression": "var allNodeList = [];function getChildNode(node){var nodeList = node.childNodes;for(var i = 0;i < nodeList.length;i++){var childNode = nodeList[i];allNodeList.push(childNode);getChildNode(childNode);}};getChildNode(document.body);"}}'
                )
                webSocketConn.recv()
                webSocketConn.send(
                    '{"id": 2,"method": "Runtime.evaluate","params": {"expression": "allNodeEle=\'\'"}}'
                )
                webSocketConn.recv()
                webSocketConn.send(
                    '{"id": 3,"method": "Runtime.evaluate","params": {"expression": "for(j = 0; j < allNodeList.length; j++) {allNodeEle = allNodeEle+allNodeList[j];}"}}'
                )

                results = webSocketConn.recv()

                self.logger.debug(results)

                result = json.loads(results)
                retry = True if result.get('result').get('wasThrown') is True or \
                                result.get('result').get('result').get('value') == u'' or \
                                result.get('result').get('result').get('type') == u'undefined' else False
                if retry:
                    time.sleep(2)
                    nums = nums + 1
                    webSocketConn.close()
                    Log().getLogger().info('retry getFeatur ---> ' + str(nums))
                    continue
                else:
                    message = result['result']['result']['value']
                    retry = False
                webSocketConn.close()
                return message
            except:
                continue
        if retry or message == '':
            raise AttributeError()

    def needSwitchNextPage(self):
        import json
        import urllib2
        localUrl = "http://localhost:9223/json"

        # 去掉代理
        urllib2.getproxies = lambda: {}

        response = urllib2.urlopen(localUrl)
        responseJson = json.loads(response.read())
        return len(responseJson) != len(self.pageUrlDict)