From 26e9e9f2d2e5e5b570e9ef829d1433775dfbb63f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Gonz=C3=A1lez=20Alonso?= Date: Thu, 16 May 2024 18:22:36 +0200 Subject: [PATCH] feat: reuse toolium driver and pageobjects for playwright tests --- requirements.txt | 1 + toolium/behave/environment.py | 24 +++------- toolium/driver_wrapper.py | 22 ++++++++- toolium/pageelements/button_page_element.py | 12 +++-- .../pageelements/input_text_page_element.py | 7 +++ toolium/pageelements/page_element.py | 39 ++++++++++++---- toolium/pageelements/text_page_element.py | 7 +++ toolium/pageobjects/common_object.py | 8 ++++ toolium/pageobjects/playwright_page_object.py | 32 ------------- toolium/utils/driver_wait_utils.py | 46 +++++++++++-------- 10 files changed, 118 insertions(+), 80 deletions(-) delete mode 100644 toolium/pageobjects/playwright_page_object.py diff --git a/requirements.txt b/requirements.txt index 32034f00..e5c2f177 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ screeninfo~=0.8 lxml~=5.1 Faker~=18.3 phonenumbers~=8.13 +universalasync~=0.3 diff --git a/toolium/behave/environment.py b/toolium/behave/environment.py index 8236aa3f..24174d0d 100644 --- a/toolium/behave/environment.py +++ b/toolium/behave/environment.py @@ -291,21 +291,9 @@ def start_driver(context, no_driver): :param no_driver: True if this is an api test and driver should not be started """ if context.toolium_config.get_optional('Driver', 'web_library') == 'playwright': - start_playwright(context) - else: - create_and_configure_wrapper(context) - if not no_driver: - connect_wrapper(context) - - -def start_playwright(context): - """Start playwright with configured values - - :param context: behave context - """ - use_or_create_async_context(context) - loop = context.async_context.loop - context.playwright = loop.run_until_complete(async_playwright().start()) - # TODO: select browser from config - context.browser = loop.run_until_complete(context.playwright.chromium.launch(headless=False)) - context.page = loop.run_until_complete(context.browser.new_page()) + # Activate behave async context to execute playwright + use_or_create_async_context(context) + context.driver_wrapper.async_loop = context.async_context.loop + create_and_configure_wrapper(context) + if not no_driver: + connect_wrapper(context) diff --git a/toolium/driver_wrapper.py b/toolium/driver_wrapper.py index ffa9a940..ba6c3841 100644 --- a/toolium/driver_wrapper.py +++ b/toolium/driver_wrapper.py @@ -20,6 +20,8 @@ import os import screeninfo +from behave.api.async_step import use_or_create_async_context +from playwright.async_api import async_playwright from toolium.config_driver import ConfigDriver from toolium.config_parser import ExtendedConfigParser @@ -54,6 +56,7 @@ class DriverWrapper(object): remote_node = None #: remote grid node remote_node_video_enabled = False #: True if the remote grid node has the video recorder enabled logger = None #: logger instance + async_loop = None #: async loop for playwright tests # Configuration and output files config_properties_filenames = None #: configuration filenames separated by commas @@ -204,11 +207,16 @@ def configure(self, tc_config_files, is_selenium_test=True, behave_properties=No def connect(self): """Set up the selenium driver and connect to the server - :returns: selenium driver + :returns: selenium or playwright driver """ if not self.config.get('Driver', 'type') or self.config.get('Driver', 'type') in ['api', 'no_driver']: return None + if self.async_loop: + # Connect playwright driver + self.driver = self.connect_playwright(self.async_loop) + return self.driver + self.driver = ConfigDriver(self.config, self.utils).create_driver() # Save session id and remote node to download video after the test execution @@ -239,6 +247,18 @@ def connect(self): return self.driver + def connect_playwright(self, async_loop): + """Set up the playwright page + + :returns: playwright page + """ + # TODO: should playwright and browser be saved in driver_wrapper? + playwright = async_loop.run_until_complete(async_playwright().start()) + # TODO: select browser from config + browser = async_loop.run_until_complete(playwright.chromium.launch(headless=False)) + page = async_loop.run_until_complete(browser.new_page()) + return page + def resize_window(self): """Resize and move browser window""" if self.is_maximizable(): diff --git a/toolium/pageelements/button_page_element.py b/toolium/pageelements/button_page_element.py index 86a3b1fa..b6375994 100644 --- a/toolium/pageelements/button_page_element.py +++ b/toolium/pageelements/button_page_element.py @@ -17,6 +17,8 @@ """ from selenium.common.exceptions import StaleElementReferenceException +from universalasync import async_to_sync_wraps + from toolium.pageelements.page_element import PageElement @@ -33,14 +35,18 @@ def text(self): # Retry if element has changed return self.web_element.text - def click(self): + @async_to_sync_wraps + async def click(self): """Click the element :returns: page element instance """ + if self.is_playwright: + await (await self.web_element).click() + return self try: - self.wait_until_clickable().web_element.click() + (await self.wait_until_clickable().web_element).click() except StaleElementReferenceException: # Retry if element has changed - self.web_element.click() + (await self.web_element).click() return self diff --git a/toolium/pageelements/input_text_page_element.py b/toolium/pageelements/input_text_page_element.py index 90f56e01..81ca8698 100644 --- a/toolium/pageelements/input_text_page_element.py +++ b/toolium/pageelements/input_text_page_element.py @@ -49,6 +49,13 @@ def text(self, value): else: self.web_element.send_keys(value) + async def set_async_text(self, value): + """Set value on the element + + :param value: value to be set + """ + await (await self.web_element).fill(value) + def clear(self): """Clear the element value diff --git a/toolium/pageelements/page_element.py b/toolium/pageelements/page_element.py index 6e9531e0..7479b775 100644 --- a/toolium/pageelements/page_element.py +++ b/toolium/pageelements/page_element.py @@ -18,6 +18,7 @@ from selenium.common.exceptions import NoSuchElementException, TimeoutException from selenium.webdriver.common.by import By +from universalasync import async_to_sync_wraps from toolium.driver_wrapper import DriverWrappersPool from toolium.pageobjects.common_object import CommonObject @@ -74,15 +75,16 @@ def reset_object(self, driver_wrapper=None): self.driver_wrapper = driver_wrapper self._web_element = None + @async_to_sync_wraps @property - def web_element(self): + async def web_element(self): """Find WebElement using element locator :returns: web element object :rtype: selenium.webdriver.remote.webelement.WebElement or appium.webdriver.webelement.WebElement """ try: - self._find_web_element() + await self._find_web_element() except NoSuchElementException as exception: parent_msg = f" and parent locator {self.parent_locator_str()}" if self.parent else '' msg = "Page element of type '%s' with locator %s%s not found" @@ -91,7 +93,8 @@ def web_element(self): raise exception return self._web_element - def _find_web_element(self): + @async_to_sync_wraps + async def _find_web_element(self): """Find WebElement using element locator and save it in _web_element attribute""" if not self._web_element or not self.driver_wrapper.config.getboolean_optional('Driver', 'save_web_element'): # check context for mobile webviews @@ -113,9 +116,12 @@ def _find_web_element(self): else: # Element will be searched from parent element or from driver base = self.utils.get_web_element(self.parent) if self.parent else self.driver - # Find elements and get the correct index or find a single element - self._web_element = base.find_elements(*self.locator)[self.order] if self.order \ - else base.find_element(*self.locator) + if self.is_playwright: + self._web_element = self.driver.locator(self.playwright_locator) + else: + # Find elements and get the correct index or find a single element + self._web_element = base.find_elements(*self.locator)[self.order] if self.order \ + else base.find_element(*self.locator) def parent_locator_str(self): """Return string with locator tuple for parent element @@ -130,6 +136,22 @@ def parent_locator_str(self): parent_locator = '[WebElement without locator]' return parent_locator + @property + def playwright_locator(self): + """Return playwright locator converted from toolium/selenium locator + + :returns: playwright locator + """ + # TODO: Implement playwright locator conversion + if self.locator[0] == By.ID: + prefix = '#' + elif self.locator[0] == By.XPATH: + prefix = 'xpath=' + else: + raise ValueError(f'Locator type not supported to be converted to playwright: {self.locator[0]}') + playwright_locator = f'{prefix}{self.locator[1]}' + return playwright_locator + def scroll_element_into_view(self): """Scroll element into view @@ -140,7 +162,8 @@ def scroll_element_into_view(self): self.driver.execute_script('window.scrollTo({0}, {1})'.format(x, y)) return self - def is_present(self): + @async_to_sync_wraps + async def is_present(self): """Find element and return True if it is present :returns: True if element is located @@ -148,7 +171,7 @@ def is_present(self): try: # Use _find_web_element() instead of web_element to avoid logging error message self._web_element = None - self._find_web_element() + await self._find_web_element() return True except NoSuchElementException: return False diff --git a/toolium/pageelements/text_page_element.py b/toolium/pageelements/text_page_element.py index cc0dcc93..d68ce07e 100644 --- a/toolium/pageelements/text_page_element.py +++ b/toolium/pageelements/text_page_element.py @@ -27,3 +27,10 @@ def text(self): :returns: the text of the element """ return self.web_element.text + + async def async_text(self): + """Get the text of the element + + :returns: the text of the element + """ + return await (await self.web_element).text_content() diff --git a/toolium/pageobjects/common_object.py b/toolium/pageobjects/common_object.py index 03690834..a9b90854 100644 --- a/toolium/pageobjects/common_object.py +++ b/toolium/pageobjects/common_object.py @@ -65,6 +65,14 @@ def utils(self): """ return self.driver_wrapper.utils + @property + def is_playwright(self): + """Check if the driver is a playwright driver + + :returns: True if the driver is a playwright driver + """ + return self.config.get_optional('Driver', 'web_library') == 'playwright' + def _switch_to_new_context(self, context): """ Change to a new context if its different than the current one""" if self.driver.context != context: diff --git a/toolium/pageobjects/playwright_page_object.py b/toolium/pageobjects/playwright_page_object.py deleted file mode 100644 index d4e33ea6..00000000 --- a/toolium/pageobjects/playwright_page_object.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Copyright 2024 Telefónica Innovación Digital, S.L. -This file is part of Toolium. - -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. -""" - -from playwright.async_api import Page -from toolium.pageobjects.page_object import PageObject - - -class PlaywrightPageObject(PageObject): - """Class to represent a playwright web page""" - - def __init__(self, page: Page): - """Initialize page object properties - - :param page: playwright page instance - """ - self.page = page - super(PlaywrightPageObject, self).__init__() diff --git a/toolium/utils/driver_wait_utils.py b/toolium/utils/driver_wait_utils.py index 87f2aa06..649fd9ad 100644 --- a/toolium/utils/driver_wait_utils.py +++ b/toolium/utils/driver_wait_utils.py @@ -20,6 +20,7 @@ import time from selenium.common.exceptions import NoSuchElementException, TimeoutException, StaleElementReferenceException from selenium.webdriver.support.ui import WebDriverWait +from universalasync import async_to_sync_wraps class WaitUtils(object): @@ -50,7 +51,8 @@ def get_explicitly_wait(self): """ return int(self.driver_wrapper.config.get_optional('Driver', 'explicitly_wait', '10')) - def _expected_condition_find_element(self, element): + @async_to_sync_wraps + async def _expected_condition_find_element(self, element): """Tries to find the element, but does not thrown an exception if the element is not found :param element: PageElement or element locator as a tuple (locator_type, locator_value) to be found @@ -63,7 +65,7 @@ def _expected_condition_find_element(self, element): if isinstance(element, PageElement): # Use _find_web_element() instead of web_element to avoid logging error message element._web_element = None - element._find_web_element() + await element._find_web_element() web_element = element._web_element elif isinstance(element, tuple): web_element = self.driver_wrapper.driver.find_element(*element) @@ -71,7 +73,8 @@ def _expected_condition_find_element(self, element): pass return web_element - def _expected_condition_find_element_visible(self, element): + @async_to_sync_wraps + async def _expected_condition_find_element_visible(self, element): """Tries to find the element and checks that it is visible, but does not thrown an exception if the element is not found @@ -79,26 +82,28 @@ def _expected_condition_find_element_visible(self, element): :returns: the web element if it is visible or False :rtype: selenium.webdriver.remote.webelement.WebElement or appium.webdriver.webelement.WebElement """ - web_element = self._expected_condition_find_element(element) + web_element = await self._expected_condition_find_element(element) try: return web_element if web_element and web_element.is_displayed() else False except StaleElementReferenceException: return False - def _expected_condition_find_element_not_visible(self, element): + @async_to_sync_wraps + async def _expected_condition_find_element_not_visible(self, element): """Tries to find the element and checks that it is visible, but does not thrown an exception if the element is not found :param element: PageElement or element locator as a tuple (locator_type, locator_value) to be found :returns: True if the web element is not found or it is not visible """ - web_element = self._expected_condition_find_element(element) + web_element = await self._expected_condition_find_element(element) try: return True if not web_element or not web_element.is_displayed() else False except StaleElementReferenceException: return False - def _expected_condition_find_first_element(self, elements): + @async_to_sync_wraps + async def _expected_condition_find_first_element(self, elements): """Try to find sequentially the elements of the list and return the first element found :param elements: list of PageElements or element locators as a tuple (locator_type, locator_value) to be found @@ -112,7 +117,7 @@ def _expected_condition_find_first_element(self, elements): try: if isinstance(element, PageElement): element._web_element = None - element._find_web_element() + await element._find_web_element() else: self.driver_wrapper.driver.find_element(*element) element_found = element @@ -121,7 +126,8 @@ def _expected_condition_find_first_element(self, elements): pass return element_found - def _expected_condition_find_element_clickable(self, element): + @async_to_sync_wraps + async def _expected_condition_find_element_clickable(self, element): """Tries to find the element and checks that it is clickable, but does not thrown an exception if the element is not found @@ -129,13 +135,14 @@ def _expected_condition_find_element_clickable(self, element): :returns: the web element if it is clickable or False :rtype: selenium.webdriver.remote.webelement.WebElement or appium.webdriver.webelement.WebElement """ - web_element = self._expected_condition_find_element_visible(element) + web_element = await self._expected_condition_find_element_visible(element) try: return web_element if web_element and web_element.is_enabled() else False except StaleElementReferenceException: return False - def _expected_condition_find_element_stopped(self, element_times): + @async_to_sync_wraps + async def _expected_condition_find_element_stopped(self, element_times): """Tries to find the element and checks that it has stopped moving, but does not thrown an exception if the element is not found @@ -147,14 +154,15 @@ def _expected_condition_find_element_stopped(self, element_times): :rtype: selenium.webdriver.remote.webelement.WebElement or appium.webdriver.webelement.WebElement """ element, times = element_times - web_element = self._expected_condition_find_element(element) + web_element = await self._expected_condition_find_element(element) try: locations_list = [tuple(web_element.location.values()) for i in range(int(times)) if not time.sleep(0.001)] return web_element if set(locations_list) == set(locations_list[-1:]) else False except StaleElementReferenceException: return False - def _expected_condition_find_element_containing_text(self, element_text_pair): + @async_to_sync_wraps + async def _expected_condition_find_element_containing_text(self, element_text_pair): """Tries to find the element and checks that it contains the specified text, but does not thrown an exception if the element is not found @@ -165,13 +173,14 @@ def _expected_condition_find_element_containing_text(self, element_text_pair): :rtype: selenium.webdriver.remote.webelement.WebElement or appium.webdriver.webelement.WebElement """ element, text = element_text_pair - web_element = self._expected_condition_find_element(element) + web_element = await self._expected_condition_find_element(element) try: return web_element if web_element and text in web_element.text else False except StaleElementReferenceException: return False - def _expected_condition_find_element_not_containing_text(self, element_text_pair): + @async_to_sync_wraps + async def _expected_condition_find_element_not_containing_text(self, element_text_pair): """Tries to find the element and checks that it does not contain the specified text, but does not thrown an exception if the element is found @@ -182,13 +191,14 @@ def _expected_condition_find_element_not_containing_text(self, element_text_pair :rtype: selenium.webdriver.remote.webelement.WebElement or appium.webdriver.webelement.WebElement """ element, text = element_text_pair - web_element = self._expected_condition_find_element(element) + web_element = await self._expected_condition_find_element(element) try: return web_element if web_element and text not in web_element.text else False except StaleElementReferenceException: return False - def _expected_condition_value_in_element_attribute(self, element_attribute_value): + @async_to_sync_wraps + async def _expected_condition_value_in_element_attribute(self, element_attribute_value): """Tries to find the element and checks that it contains the requested attribute with the expected value, but does not thrown an exception if the element is not found @@ -200,7 +210,7 @@ def _expected_condition_value_in_element_attribute(self, element_attribute_value :rtype: selenium.webdriver.remote.webelement.WebElement or appium.webdriver.webelement.WebElement """ element, attribute, value = element_attribute_value - web_element = self._expected_condition_find_element(element) + web_element = await self._expected_condition_find_element(element) try: return web_element if web_element and web_element.get_attribute(attribute) == value else False except StaleElementReferenceException: