diff --git a/tests/appium_tests.py b/tests/appium_tests.py index d04d493..5cbf002 100644 --- a/tests/appium_tests.py +++ b/tests/appium_tests.py @@ -1,6 +1,7 @@ +import time + import pytest -from tests.screens.landing import LandingScreen from testui.support import logger from testui.support.appium_driver import NewDriver from testui.support.testui_driver import TestUIDriver @@ -20,9 +21,17 @@ def selenium_driver(self): driver.quit() @pytest.mark.signup - def test_sign_up_flow(self, selenium_driver: TestUIDriver): + def test_screenshot_methods(self, selenium_driver: TestUIDriver): logger.log_test_name("T92701: Create an account") - selenium_driver.navigate_to("https://google.com") - landing_page = LandingScreen(selenium_driver) - landing_page.i_am_in_mobile_landing_screen() + selenium_driver.get_dimensions() + selenium_driver.navigate_to( + "https://github.com/testdevlab/Py-TestUI#image-recognition" + ) + selenium_driver.start_recording_screen() + time.sleep(1) + selenium_driver.stop_recording_and_compare("./resources/comp.png", fps_reduction=30, keep_image_as="v_image.png") + selenium_driver.find_image_match( + "./resources/comp.png", 0.9, True, image_match="image.png" + ) + selenium_driver.click_by_image("./resources/comp.png") selenium_driver.raise_errors() diff --git a/tests/screens/landing.py b/tests/screens/landing.py index 183845b..4dfb976 100644 --- a/tests/screens/landing.py +++ b/tests/screens/landing.py @@ -21,4 +21,4 @@ def i_am_in_mobile_landing_screen(self): self.__log_in_button_2.wait_until_visible() def i_am_in_google_play_landing_screen(self): - self.__google_play_screen.wait_until_visible().get_text() + self.__google_play_screen.wait_until_visible().screenshot() diff --git a/tests/selenium_tests.py b/tests/selenium_tests.py index b0a15ee..bad63d3 100644 --- a/tests/selenium_tests.py +++ b/tests/selenium_tests.py @@ -12,6 +12,7 @@ class TestStringMethods: def selenium_driver(self): options = Options() options.add_argument("disable-user-media-security") + options.add_argument("headless") driver = ( NewDriver() .set_logger() @@ -23,11 +24,9 @@ def selenium_driver(self): driver.quit() @pytest.mark.signup - def test_sign_up_flow(self, selenium_driver: TestUIDriver): + def test_template_matching(self, selenium_driver: TestUIDriver): logger.log_test_name("T92701: Create an account") - selenium_driver.navigate_to("https://google.com") - landing_page = LandingScreen(selenium_driver) - landing_page.i_am_in_landing_screen() + selenium_driver.get_driver().set_window_size(1000, 1100) selenium_driver.navigate_to( "https://github.com/testdevlab/Py-TestUI#image-recognition" ) @@ -35,3 +34,13 @@ def test_sign_up_flow(self, selenium_driver: TestUIDriver): "resources/comp.png", 0.9, True, image_match="image.png" ) selenium_driver.raise_errors() + + @pytest.mark.signup + def test_get_dimensions(self, selenium_driver: TestUIDriver): + logger.log_test_name("T92701: Create an account") + selenium_driver.get_driver().set_window_size(1000, 1100) + selenium_driver.navigate_to( + "https://github.com/testdevlab/Py-TestUI#image-recognition" + ) + selenium_driver.get_dimensions() + selenium_driver.raise_errors() diff --git a/testui/elements/testui_element.py b/testui/elements/testui_element.py index 49e8883..4a24af8 100644 --- a/testui/elements/testui_element.py +++ b/testui/elements/testui_element.py @@ -6,6 +6,7 @@ from os import path from typing import List +from appium.webdriver.common.appiumby import AppiumBy from appium.webdriver.common.touch_action import TouchAction from appium.webdriver.webelement import WebElement from selenium.webdriver import ActionChains @@ -15,6 +16,7 @@ from testui.support.helpers import error_with_traceback from testui.support.testui_images import Dimensions, ImageRecognition + def testui_error(driver, exception: str) -> None: config = driver.configuration @@ -118,21 +120,24 @@ def __find_by_element(self) -> WebElement: return self.driver.find_element(By.XPATH, self.locator) if self.locator_type == "android_id_match": - return self.driver.find_element_by_android_uiautomator( - f'resourceIdMatches("{self.locator}")' - ) + return self.driver.find_element(AppiumBy.ANDROID_UIAUTOMATOR, + f"resourceIdMatches(\"{self.locator}\")" + ) if self.locator_type == "accessibility": - return self.driver.find_element_by_accessibility_id(self.locator) + return self.driver.find_element( + AppiumBy.ACCESSIBILITY_ID, self.locator) if self.locator_type == "uiautomator": - return self.driver.find_element_by_android_uiautomator(self.locator) + return self.driver.find_element( + AppiumBy.ANDROID_UIAUTOMATOR, self.locator) if self.locator_type == "classChain": - return self.driver.find_element_by_ios_class_chain(self.locator) + return self.driver.find_element( + AppiumBy.IOS_CLASS_CHAIN, self.locator) if self.locator_type == "predicate": - return self.driver.find_element_by_ios_predicate(self.locator) + return self.driver.find_element(AppiumBy.IOS_PREDICATE, self.locator) raise ElementException(f"locator not supported: {self.locator_type}") @@ -157,23 +162,24 @@ def __find_by_collection(self) -> List[WebElement]: return self.driver.find_elements(By.XPATH, self.locator) if self.locator_type == "android_id_match": - return self.driver.find_elements_by_android_uiautomator( - f'resourceIdMatches("{self.locator}")' - ) + return self.driver.find_elements(AppiumBy.ANDROID_UIAUTOMATOR, + f'resourceIdMatches("{self.locator}")' + ) if self.locator_type == "accessibility": - return self.driver.find_elements_by_accessibility_id(self.locator) + return self.driver.find_elements(AppiumBy.ACCESSIBILITY_ID, self.locator) if self.locator_type == "uiautomator": - return self.driver.find_elements_by_android_uiautomator( + return self.driver.find_elements( + AppiumBy.ANDROID_UIAUTOMATOR, self.locator ) if self.locator_type == "classChain": - return self.driver.find_elements_by_ios_class_chain(self.locator) + return self.driver.find_elements(AppiumBy.IOS_CLASS_CHAIN, self.locator) if self.locator_type == "predicate": - return self.driver.find_elements_by_ios_predicate(self.locator) + return self.driver.find_elements(AppiumBy.IOS_PREDICATE, self.locator) raise ElementException(f"locator not supported: {self.locator_type}") @@ -496,26 +502,24 @@ def screenshot(self, image_name="cropped_image.png"): :return: """ self.wait_until_visible() - self.testui_driver.save_screenshot(f"{self.device_name}-crop_image.png") - dimensions = self.dimensions - top_left = self.location - ImageRecognition( - f"testui-{self.device_name}-crop_image.png" - ).crop_original_image( - top_left.x + dimensions.x // 2, - top_left.y + dimensions.y // 2, - dimensions.x, - dimensions.y, - image_name, - ) - - root_dir = self.testui_driver.configuration.screenshot_path - if not root_dir: - root_dir = path.dirname( - path.dirname(path.dirname(path.abspath(__file__))) + try: + self.get_element().screenshot(image_name) + except Exception: + path_img = self.testui_driver.save_screenshot(f"{self.device_name}-crop_image.png") + dimensions = self.dimensions + top_left = self.location + logger.log_debug(f'crop dimensions (x,y,w,h):({top_left.x},{top_left.y},{dimensions.x},{dimensions.y})') + ImageRecognition( + path_img + ).crop_original_image( + (top_left.x + dimensions.x // 2), + (top_left.y + dimensions.y // 2), + dimensions.x, + dimensions.y, + image_name, ) - os.remove(root_dir + f"/testui-{self.device_name}-crop_image.png") + os.remove(path_img) return self @@ -556,13 +560,8 @@ def find_image_match( f"{self.device_name}: The images compared matched. " f"Threshold={threshold}, matched = {precision}" ) - root_dir = ( - os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - ) - + "/" - ) - os.remove(root_dir + self.device_name + ".png") + root_dir = self.testui_driver.configuration.screenshot_path + os.remove(os.path.join(root_dir, self.device_name + ".png")) return self @@ -596,13 +595,8 @@ def is_image_match( return False if found and is_not: return False - root_dir = ( - os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - ) - + "/" - ) - os.remove(root_dir + self.device_name + ".png") + root_dir = self.testui_driver.configuration.screenshot_path + os.remove(os.path.join(root_dir, self.device_name + ".png")) return True diff --git a/testui/support/api_support.py b/testui/support/api_support.py index 7be4c3f..79f329c 100644 --- a/testui/support/api_support.py +++ b/testui/support/api_support.py @@ -1,4 +1,9 @@ import requests +import sys +import platform + +from testui.support import logger +import xml.etree.ElementTree as ET def get_chrome_version(version: str): @@ -8,11 +13,37 @@ def get_chrome_version(version: str): # TODO: it is not pulling the version. I just updated all phones to # latest chrome chrome_version = "" - for text in r.text.split(f"{version}."): - if text.__contains__("/chromedriver_mac64.zip"): - new_chrome_version = ( - f'{version}.{text.split("/chromedriver_mac64.zip")[0]}' - ) - if len(new_chrome_version) < 17: - chrome_version = new_chrome_version + mytree = ET.ElementTree(ET.fromstring(r.text)) + root = mytree.getroot() + for child in root: + for child2 in child: + if not child2.text.__contains__(f"{version}."): + continue + if chrome_name() == "arm64" and \ + child2.text.__contains__("m1") or \ + child2.text.__contains__("mac_arm64"): + chrome_version = child2.text.split("/")[0] + elif child2.text.__contains__(chrome_name()): + chrome_version = child2.text.split("/")[0] + return chrome_version + + +def chrome_name(): + pl = sys.platform + if pl == "linux" or pl == "linux2": + return "/chromedriver_linux64.zip" + elif pl == "darwin": + if os_architecture() == 64: + return "arm64" + else: + return "/chromedriver_mac64.zip" + elif pl == "win32": + return "/chromedriver_win32.zip" + + +def os_architecture(): + if platform.machine().endswith("64"): + return 64 + else: + return 32 diff --git a/testui/support/appium_driver.py b/testui/support/appium_driver.py index d41f810..1e23f72 100644 --- a/testui/support/appium_driver.py +++ b/testui/support/appium_driver.py @@ -102,9 +102,7 @@ def set_app_path(self, path: str): if os.path.isabs(self.__app_path): return self else: - root_dir = os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - ) + root_dir = self.configuration.screenshot_path self.__app_path = os.path.join(root_dir, path) logger.log(self.__app_path) return self @@ -158,6 +156,7 @@ def set_chrome_driver(self, version="") -> "NewDriver": if self.udid is None: self.udid = get_device_udid(0) mobile_version = check_chrome_version(self.udid) + logger.log(f"Installing chromedriver version: {mobile_version}") chrome_driver = chrome.ChromeDriverManager( version=mobile_version ).install() @@ -504,6 +503,7 @@ def get_device_udid(number: int): if len(devices) == 0: raise Exception("There are 0 devices connected to the computer!") if len(devices) > number: + logger.log(f"Setting device: {devices[number].get_serial_no()}") return devices[number].get_serial_no() else: new_number = number - (number // len(devices)) * len(devices) diff --git a/testui/support/testui_driver.py b/testui/support/testui_driver.py index 11c6c92..de8c561 100644 --- a/testui/support/testui_driver.py +++ b/testui/support/testui_driver.py @@ -147,7 +147,7 @@ def find_image_match( image_name = f"{self.device_udid}{current_time}.png" image_path = self.save_screenshot(image_name) found, p = ImageRecognition( - image_path, comparison, threshold, self.device_name + image_path, comparison, threshold, self.device_name, self.configuration.screenshot_path ).compare(image_match) if assertion and not found and not not_found: exception = self.new_error_message( @@ -164,26 +164,27 @@ def click_by_image(self, image: str, threshold=0.9, webview=False): now = datetime.now() current_time = now.strftime("%Y-%m-%d%H%M%S") image_name = f"{self.device_udid}{current_time}.png" - self.save_screenshot(image_name) + im_path = self.save_screenshot(image_name) x, y = get_point_match( - f"testui-{image_name}", f"{image}", threshold, self.device_name + im_path, image, threshold, self.device_name ) ta = TouchAction(self.__appium_driver) if webview: y = y + 120 ta.tap(x=x, y=y).perform() - logger.log(f"{self.device_name}: element with image {image} clicked") - self.__delete_screenshot(image_name) + logger.log(f"{self.device_name}: element with image {image} clicked on point ({x},{y})") + self.__delete_screenshot(im_path) def get_dimensions(self): now = datetime.now() current_time = now.strftime("%Y-%m-%d%H%M%S") image_name = f"{self.device_udid}{current_time}.png" - self.save_screenshot(image_name) + path_s = self.save_screenshot(image_name) dimensions = ImageRecognition( - f"testui-{image_name}" + original=path_s ).image_original_size() - self.__delete_screenshot(image_name) + logger.log(f"Deleting screenshot: {path_s}") + self.__delete_screenshot(path_s) return dimensions def click(self, x, y): @@ -220,10 +221,7 @@ def save_screenshot(self, image_name=""): @classmethod def __delete_screenshot(cls, image_name): - root_dir = os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - ) - os.remove(root_dir + f"/testui-{image_name}") + os.remove(image_name) def get_driver(self) -> WebDriver: driver = self.__appium_driver @@ -277,10 +275,9 @@ def start_recording_screen(self): def stop_recording_screen(self, file_name="testui-video.mp4"): file = self.get_driver().stop_recording_screen() decoded_string = base64.b64decode(file) - root_dir = os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - ) - with open(root_dir + f"/{file_name}", "wb") as wfile: + root_dir = self.configuration.screenshot_path + logger.log(f"Recording stopped in {os.path.join(root_dir, file_name)}") + with open(os.path.join(root_dir, file_name), "wb") as wfile: wfile.write(decoded_string) def stop_recording_and_compare( @@ -299,11 +296,15 @@ def stop_recording_and_compare( root_dir = os.path.dirname( os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ) + if self.__configuration.screenshot_path != "": + root_dir = self.__configuration.screenshot_path found = ImageRecognition( - video_name, comparison, threshold, device_name=self.device_name + video_name, comparison, threshold, + device_name=self.device_name, + path=root_dir ).compare_video(keep_image_as, frame_rate_reduction=fps_reduction) - os.remove(root_dir + f"/{video_name}") + os.remove(os.path.join(root_dir, video_name)) if not found and not not_found: if assertion: raise Exception( diff --git a/testui/support/testui_images.py b/testui/support/testui_images.py index 2b61b28..08ad007 100644 --- a/testui/support/testui_images.py +++ b/testui/support/testui_images.py @@ -10,6 +10,7 @@ found_image = False matched = 0.0 +matching_list = [] def compare_video_image( @@ -19,20 +20,32 @@ def compare_video_image( image_match, frame_rate_reduction=1, max_scale=2.0, + path="" ): - root_dir = os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - ) - cap = cv2.VideoCapture(root_dir + "/" + video) - template = cv2.imread(root_dir + "/" + comparison) + global matching_list + global matched + global found_image + + found_image = False + matched = 0.0 + matching_list = [] + + root_dir = path + logger.log_debug(f'root directory: {root_dir}') + cap = cv2.VideoCapture(os.path.join(root_dir, video)) + template = cv2.imread(os.path.join(root_dir, comparison)) + if template is None: + logger.log_warn(f'trying to compare with an image that doesn\'t exist! {os.path.join(root_dir, comparison)}') + return False, 0.0 i = 0 percentage = 0.0 while cap.isOpened(): # Capture frame-by-frame ret, frame = cap.read() if ret and i % frame_rate_reduction == 0: + logger.log(f"frame evaluation = {i}") found, percentage = __compare( - frame, template, threshold, image_match, root_dir, max_scale + frame, template, threshold, image_match, root_dir, max_scale, 0.1, 50 ) if found: cap.release() @@ -54,14 +67,16 @@ def __compare( root_dir: str, max_scale: float, min_scale=0.1, + divisions=25 ): (tH, tW) = template.shape[:2] # loop over the scales of the image found = None global found_image global matched + global matching_list maxVal = 0.0 - for scale in np.linspace(min_scale, max_scale, 5)[::-1]: + for scale in np.linspace(min_scale, max_scale, divisions)[::-1]: # resize the image according to the scale, and keep track of the ratio # of the resizing. resized = imutils.resize(image, width=int(image.shape[1] * scale)) @@ -80,6 +95,7 @@ def __compare( if found_image: lock.release() return True, matched + matching_list.append(maxVal) lock.release() found = (maxVal, maxLoc, r) if maxVal > threshold: @@ -98,13 +114,14 @@ def __compare( image, (startX, startY), (endX, endY), (0, 0, 255), 2 ) cv2.imwrite(os.path.join(root_dir, image_match), image) + logger.log(os.path.join(root_dir, image_match)) lock.acquire() found_image = True lock.release() matched = maxVal return True, maxVal - matched = maxVal - return False, maxVal + matched = max(matching_list) + return False, matched def compare_images( @@ -114,17 +131,18 @@ def compare_images( image_match="", max_scale=2.0, min_scale=0.3, + path="" ): # Read the images from the file global found_image global matched + global matching_list start = time.time() matched = 0.0 + matching_list = [] found_image = False - root_dir = os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - ) + root_dir = path if not os.path.exists(comparison): comparison = os.path.join(root_dir, comparison) if not os.path.exists(comparison): @@ -221,7 +239,7 @@ def compare_images( break logger.log(f"Image recognition took {time.time() - start}s") - return found_image, matched + return found_image, max(matching_list) def get_point_match( @@ -230,15 +248,12 @@ def get_point_match( _ = device_name # Read the images from the file - root_dir = os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - ) - template = cv2.imread(root_dir + "/" + comparison) + template = cv2.imread(comparison) (tH, tW) = template.shape[:2] - image = cv2.imread(root_dir + "/" + original) + image = cv2.imread(original) found = None # loop over the scales of the image - for scale in np.linspace(0.2, 1.0, 10)[::-1]: + for scale in np.linspace(0.2, 2.0, 30)[::-1]: # resize the image according to the scale, and keep track of the ratio # of the resizing resized = imutils.resize(image, width=int(image.shape[1] * scale)) @@ -271,11 +286,8 @@ def draw_match( method = cv2.TM_CCOEFF_NORMED # Read the images from the file - root_dir = os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - ) - large_image = cv2.imread(root_dir + "/" + comparison) - small_image = cv2.imread(root_dir + "/" + original) + large_image = cv2.imread(comparison) + small_image = cv2.imread(original) logger.log_debug( f'{device_name}: Comparing "{original}" with "{comparison}"' @@ -294,22 +306,24 @@ def draw_match( def size(image_path): - root_dir = os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - ) - img = cv2.imread(root_dir + "/" + image_path) + img = cv2.imread(image_path) height, width, _ = img.shape return width, height class ImageRecognition: def __init__( - self, original: str, comparison="", threshold=0.9, device_name="Device" + self, original: str, comparison="", threshold=0.9, device_name="Device", path="" ): self.__original = original self.__comparison = comparison self.__threshold = threshold self.__device_name = device_name + if path == "": + path = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) + self.__path = path def compare(self, image_match="", max_scale=2.0, min_scale=0.3): _, p1 = compare_images( @@ -319,6 +333,7 @@ def compare(self, image_match="", max_scale=2.0, min_scale=0.3): image_match, max_scale, min_scale, + self.__path ) if self.__threshold > p1: logger.log_debug( @@ -345,6 +360,7 @@ def compare_video( image_match, frame_rate_reduction, max_scale, + self.__path ) if found: logger.log_debug( @@ -380,7 +396,11 @@ def draw_image_match(self): return self def image_original_size(self): - size_image = size(self.__original) + path = self.__original + if self.__path != "": + path = os.path.join(self.__path, self.__original) + logger.log(f"Checking size of image: {path}") + size_image = size(path) logger.log(f"The size of the image is {size_image}") return Dimensions(size_image[0], size_image[1]) @@ -393,18 +413,16 @@ def crop_original_image( self, center_x, center_y, width, height, image_name="cropped_image.png" ): # Read the images from the file - root_dir = os.path.dirname( - os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - ) - img = cv2.imread(root_dir + "/" + self.__original) + path = os.path.join(self.__path, self.__original) + img = cv2.imread(path) y = center_y - height // 2 if y < 0: y *= -1 x = center_x - width // 2 if x < 0: x *= -1 - img_2 = img[y : y + height, x : x + width] - cv2.imwrite(root_dir + "/" + image_name, img_2) + img_2 = img[y:y + height, x:x + width] + cv2.imwrite(image_name, img_2) return self