From e790f79f4de0779090761844b86532330fe93916 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Mon, 11 Jun 2018 18:15:29 -0700 Subject: [PATCH 1/4] add find by image commands and tests --- .pytest_cache/v/cache/lastfailed | 1 + .pytest_cache/v/cache/nodeids | 5 ++ appium/webdriver/common/mobileby.py | 1 + appium/webdriver/imagelement.py | 51 ++++++++++++ appium/webdriver/webdriver.py | 53 ++++++++++++- .../android/desired_capabilities.py | 9 ++- .../android/find_by_image_failure.png | Bin 0 -> 25258 bytes .../android/find_by_image_success.png | Bin 0 -> 7309 bytes .../functional/android/find_by_image_tests.py | 75 ++++++++++++++++++ 9 files changed, 189 insertions(+), 6 deletions(-) create mode 100644 .pytest_cache/v/cache/lastfailed create mode 100644 .pytest_cache/v/cache/nodeids create mode 100644 appium/webdriver/imagelement.py create mode 100644 test/functional/android/find_by_image_failure.png create mode 100644 test/functional/android/find_by_image_success.png create mode 100644 test/functional/android/find_by_image_tests.py diff --git a/.pytest_cache/v/cache/lastfailed b/.pytest_cache/v/cache/lastfailed new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/.pytest_cache/v/cache/lastfailed @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/.pytest_cache/v/cache/nodeids b/.pytest_cache/v/cache/nodeids new file mode 100644 index 000000000..c3c0a4903 --- /dev/null +++ b/.pytest_cache/v/cache/nodeids @@ -0,0 +1,5 @@ +[ + "test/functional/android/find_by_image_tests.py::FindByImageTests::test_find_based_on_image_template", + "test/functional/android/find_by_image_tests.py::FindByImageTests::test_find_multiple_elements_by_image_just_returns_one", + "test/functional/android/find_by_image_tests.py::FindByImageTests::test_find_throws_no_such_element" +] \ No newline at end of file diff --git a/appium/webdriver/common/mobileby.py b/appium/webdriver/common/mobileby.py index be0b68eb9..3e77c51eb 100644 --- a/appium/webdriver/common/mobileby.py +++ b/appium/webdriver/common/mobileby.py @@ -21,3 +21,4 @@ class MobileBy(By): IOS_CLASS_CHAIN = '-ios class chain' ANDROID_UIAUTOMATOR = '-android uiautomator' ACCESSIBILITY_ID = 'accessibility id' + IMAGE = '-image' diff --git a/appium/webdriver/imagelement.py b/appium/webdriver/imagelement.py new file mode 100644 index 000000000..252599e1d --- /dev/null +++ b/appium/webdriver/imagelement.py @@ -0,0 +1,51 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math + + +class ImageElement: + + def __init__(self, driver, x, y, width, height): + self.driver = driver + self.center_x = math.floor(x + width / 2) + self.center_y = math.floor(y + height / 2) + self.x = x + self.y = y + self.width = width + self.height = height + + def click(self): + """ + Clicks in the middle of an image bounds + """ + return self.driver.tap([(self.center_x, self.center_y)]) + + @property + def size(self): + return {'width': self.width, 'height': self.height} + + @property + def location(self): + return {'x': self.x, 'y': self.y} + + @property + def rect(self): + return { + 'width': self.width, + 'height': self.height, + 'x': self.x, + 'y': self.y + } + + def is_displayed(self): + return True diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 577674f5a..857172ae1 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -18,6 +18,7 @@ from .errorhandler import MobileErrorHandler from .switch_to import MobileSwitchTo from .webelement import WebElement as MobileWebElement +from .imagelement import ImageElement from appium.webdriver.clipboard_content_type import ClipboardContentType from appium.webdriver.common.mobileby import MobileBy @@ -26,7 +27,8 @@ from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait -from selenium.common.exceptions import TimeoutException, InvalidArgumentException +from selenium.common.exceptions import (TimeoutException, + InvalidArgumentException, NoSuchElementException) from selenium.webdriver.remote.command import Command as RemoteCommand @@ -84,7 +86,9 @@ def _make_w3c_caps(caps): always_match['moz:firefoxOptions'] = new_opts return {'firstMatch': [{}], 'alwaysMatch': always_match} + class WebDriver(webdriver.Remote): + def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub', desired_capabilities=None, browser_profile=None, proxy=None, keep_alive=False): @@ -102,6 +106,7 @@ def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub', By.IOS_CLASS_CHAIN = MobileBy.IOS_CLASS_CHAIN By.ANDROID_UIAUTOMATOR = MobileBy.ANDROID_UIAUTOMATOR By.ACCESSIBILITY_ID = MobileBy.ACCESSIBILITY_ID + By.IMAGE = MobileBy.IMAGE def start_session(self, capabilities, browser_profile=None): """ @@ -206,6 +211,9 @@ def find_element(self, by=By.ID, value=None): # elif by == By.NAME: # by = By.CSS_SELECTOR # value = '[name="%s"]' % value + if by == By.IMAGE: + return self.find_element_by_image(value) + return self.execute(RemoteCommand.FIND_ELEMENT, { 'using': by, 'value': value})['value'] @@ -235,6 +243,10 @@ def find_elements(self, by=By.ID, value=None): # Return empty list if driver returns null # See https://github.com/SeleniumHQ/selenium/issues/4555 + + if by == By.IMAGE: + return self.find_elements_by_image(value) + return self.execute(RemoteCommand.FIND_ELEMENTS, { 'using': by, 'value': value})['value'] or [] @@ -327,6 +339,43 @@ def find_elements_by_android_uiautomator(self, uia_string): """ return self.find_elements(by=By.ANDROID_UIAUTOMATOR, value=uia_string) + def find_element_by_image(self, png_img_path): + """Finds a portion of a screenshot by an image. + Uses driver.find_image_occurrence under the hood. + + :Args: + - png_img_path - a string corresponding to the path of a PNG image + """ + screenshot = self.get_screenshot_as_base64() + with open(png_img_path, 'rb') as png_file: + b64_data = base64.encodestring(png_file.read()) + b64_data = b64_data.replace("\n", "") + try: + res = self.find_image_occurrence(screenshot, b64_data) + except Exception as e: + if 'Cannot find any occurrences' in str(e): + raise NoSuchElementException(e) + else: + raise + rect = res['rect'] + return ImageElement(self, rect['x'], rect['y'], rect['width'], + rect['height']) + + def find_elements_by_image(self, png_img_path): + """Finds a portion of a screenshot by an image. + Uses driver.find_image_occurrence under the hood. Note that this will + only ever return at most one element + + :Args: + - png_img_path - a string corresponding to the path of a PNG image + """ + els = [] + try: + els.append(self.find_element_by_image(png_img_path)) + except Exception: + pass + return els + def find_element_by_accessibility_id(self, id): """Finds an element by accessibility id. @@ -1419,4 +1468,4 @@ def _addCommands(self): self.command_executor._commands[Command.GET_CLIPBOARD] = \ ('POST', '/session/$sessionId/appium/device/get_clipboard') self.command_executor._commands[Command.COMPARE_IMAGES] = \ - ('POST', '/session/$sessionId/appium/compare_images') \ No newline at end of file + ('POST', '/session/$sessionId/appium/compare_images') diff --git a/test/functional/android/desired_capabilities.py b/test/functional/android/desired_capabilities.py index b96bfbb08..84cb594fd 100644 --- a/test/functional/android/desired_capabilities.py +++ b/test/functional/android/desired_capabilities.py @@ -14,16 +14,17 @@ import os + # Returns abs path relative to this file and not cwd -PATH = lambda p: os.path.abspath( - os.path.join(os.path.dirname(__file__), p) -) +def PATH(p): + return os.path.abspath( + os.path.join(os.path.dirname(__file__), p) + ) def get_desired_capabilities(app): desired_caps = { 'platformName': 'Android', - 'platformVersion': '4.2', 'deviceName': 'Android Emulator', 'app': PATH('../../apps/' + app), 'newCommandTimeout': 240 diff --git a/test/functional/android/find_by_image_failure.png b/test/functional/android/find_by_image_failure.png new file mode 100644 index 0000000000000000000000000000000000000000..5ea155cc48228ab46a40a4b37708a6cb9efa92f5 GIT binary patch literal 25258 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 QHz<%)6g1@P{|#$ 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 Date: Mon, 11 Jun 2018 18:18:19 -0700 Subject: [PATCH 2/4] remove and ignore pytest cache files --- .gitignore | 1 + .pytest_cache/v/cache/lastfailed | 1 - .pytest_cache/v/cache/nodeids | 5 ----- 3 files changed, 1 insertion(+), 6 deletions(-) delete mode 100644 .pytest_cache/v/cache/lastfailed delete mode 100644 .pytest_cache/v/cache/nodeids diff --git a/.gitignore b/.gitignore index 0a63fe763..d30dfda65 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ dist .cache __pycache__ .idea +.pytest_cache diff --git a/.pytest_cache/v/cache/lastfailed b/.pytest_cache/v/cache/lastfailed deleted file mode 100644 index 9e26dfeeb..000000000 --- a/.pytest_cache/v/cache/lastfailed +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/.pytest_cache/v/cache/nodeids b/.pytest_cache/v/cache/nodeids deleted file mode 100644 index c3c0a4903..000000000 --- a/.pytest_cache/v/cache/nodeids +++ /dev/null @@ -1,5 +0,0 @@ -[ - "test/functional/android/find_by_image_tests.py::FindByImageTests::test_find_based_on_image_template", - "test/functional/android/find_by_image_tests.py::FindByImageTests::test_find_multiple_elements_by_image_just_returns_one", - "test/functional/android/find_by_image_tests.py::FindByImageTests::test_find_throws_no_such_element" -] \ No newline at end of file From 757799e32a5fe0e8c65af2f524aace8638db3ca8 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Mon, 11 Jun 2018 22:52:53 -0700 Subject: [PATCH 3/4] address review comments --- appium/webdriver/webdriver.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 857172ae1..1baee39e3 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -27,14 +27,16 @@ from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait -from selenium.common.exceptions import (TimeoutException, - InvalidArgumentException, NoSuchElementException) +from selenium.common.exceptions import (TimeoutException, + WebDriverException, InvalidArgumentException, NoSuchElementException) from selenium.webdriver.remote.command import Command as RemoteCommand import base64 import copy +DEFAULT_MATCH_THRESHOLD = 0.5 + # From remote/webdriver.py _W3C_CAPABILITY_NAMES = frozenset([ 'acceptInsecureCerts', @@ -339,7 +341,8 @@ def find_elements_by_android_uiautomator(self, uia_string): """ return self.find_elements(by=By.ANDROID_UIAUTOMATOR, value=uia_string) - def find_element_by_image(self, png_img_path): + def find_element_by_image(self, png_img_path, + match_threshold=DEFAULT_MATCH_THRESHOLD): """Finds a portion of a screenshot by an image. Uses driver.find_image_occurrence under the hood. @@ -349,19 +352,19 @@ def find_element_by_image(self, png_img_path): screenshot = self.get_screenshot_as_base64() with open(png_img_path, 'rb') as png_file: b64_data = base64.encodestring(png_file.read()) - b64_data = b64_data.replace("\n", "") try: - res = self.find_image_occurrence(screenshot, b64_data) - except Exception as e: + res = self.find_image_occurrence(screenshot, b64_data, + threshold=match_threshold) + except WebDriverException as e: if 'Cannot find any occurrences' in str(e): raise NoSuchElementException(e) - else: - raise + raise rect = res['rect'] return ImageElement(self, rect['x'], rect['y'], rect['width'], rect['height']) - def find_elements_by_image(self, png_img_path): + def find_elements_by_image(self, png_img_path, + match_threshold=DEFAULT_MATCH_THRESHOLD): """Finds a portion of a screenshot by an image. Uses driver.find_image_occurrence under the hood. Note that this will only ever return at most one element @@ -371,8 +374,8 @@ def find_elements_by_image(self, png_img_path): """ els = [] try: - els.append(self.find_element_by_image(png_img_path)) - except Exception: + els.append(self.find_element_by_image(png_img_path, match_threshold)) + except NoSuchElementException: pass return els From 9b0fea42e4c958d37252836709985e7c2aec29d3 Mon Sep 17 00:00:00 2001 From: Jonathan Lipps Date: Mon, 11 Jun 2018 23:02:08 -0700 Subject: [PATCH 4/4] fix docstrings --- appium/webdriver/webdriver.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/appium/webdriver/webdriver.py b/appium/webdriver/webdriver.py index 1baee39e3..291301a36 100644 --- a/appium/webdriver/webdriver.py +++ b/appium/webdriver/webdriver.py @@ -348,6 +348,10 @@ def find_element_by_image(self, png_img_path, :Args: - png_img_path - a string corresponding to the path of a PNG image + - match_threshold - a double between 0 and 1 below which matches will + be rejected as element not found + + :return: an ImageElement object """ screenshot = self.get_screenshot_as_base64() with open(png_img_path, 'rb') as png_file: @@ -371,6 +375,10 @@ def find_elements_by_image(self, png_img_path, :Args: - png_img_path - a string corresponding to the path of a PNG image + - match_threshold - a double between 0 and 1 below which matches will + be rejected as element not found + + :return: possibly-empty list of ImageElements """ els = [] try: