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
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()
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()
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')
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()
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)