From f847786e786993c98323d4d3cec5bdc368421d09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Esteban=20Ramos?= Date: Thu, 23 Nov 2023 17:40:56 +0100 Subject: [PATCH] NOV-239933: set webview to pagelements (#366) * added webview and automatic context selection * linter fixes * added test and new logic for iframes * fix lint * fix lint * added tests * fix lint * refactored switch to new context * review fix --- toolium/pageelements/page_element.py | 53 ------------ toolium/pageelements/page_elements.py | 25 +++++- toolium/pageobjects/common_object.py | 55 ++++++++++++ .../test/pageelements/test_page_elements.py | 85 +++++++++++++++++++ 4 files changed, 162 insertions(+), 56 deletions(-) diff --git a/toolium/pageelements/page_element.py b/toolium/pageelements/page_element.py index 0d2532ac..6e9531e0 100644 --- a/toolium/pageelements/page_element.py +++ b/toolium/pageelements/page_element.py @@ -32,8 +32,6 @@ class PageElement(CommonObject): or toolium.pageelements.PageElement or (selenium.webdriver.common.by.By or appium.webdriver.common.mobileby.MobileBy, str) """ - native_context = "NATIVE_APP" - webview_context_prefix = "WEBVIEW" def __init__(self, by, value, parent=None, order=None, wait=False, shadowroot=None, webview=False, webview_context_selection_callback=None, webview_csc_args=None): @@ -119,57 +117,6 @@ def _find_web_element(self): self._web_element = base.find_elements(*self.locator)[self.order] if self.order \ else base.find_element(*self.locator) - def _android_automatic_context_selection(self): - """Change context selection depending if the element is a webview for android devices""" - # we choose the appPackage webview context, and select the first window returned by mobile: getContexts - if self.webview: - context = None - window_handle = None - if self.webview_context_selection_callback: - context, window_handle = self.webview_context_selection_callback(*self.webview_csc_args) - else: - app_web_context = "{}_{}".format(PageElement.webview_context_prefix, - self.driver.capabilities['appPackage']) - if app_web_context in self.driver.contexts: - context = app_web_context - if self.driver.context != context: - self.driver.switch_to.context(context) - window_handle = self.driver.window_handles[0] - else: - raise KeyError("WEBVIEW context not found") - - if context: - if self.driver.context != context: - self.driver.switch_to.context(context) - if self.driver.current_window_handle != window_handle: - self.driver.switch_to.window(window_handle) - else: - raise KeyError("WEBVIEW context not found") - else: - if self.driver.context != PageElement.native_context: - self.driver.switch_to.context(PageElement.native_context) - - def _ios_automatic_context_selection(self): - """Change context selection depending if the element is a webview for ios devices""" - # we choose the last webview context returned by mobile: getContexts for the bundleid - if self.webview: - if self.webview_context_selection_callback: - context_id = self.webview_context_selection_callback(*self.webview_csc_args) - else: - contexts = self.driver.execute_script('mobile: getContexts') - context_id = next( - (item['id'] for item in reversed(contexts) if - 'bundleId' in item and item['bundleId'] == self.driver.capabilities['bundleId']), - None) - if context_id: - if self.driver.context != context_id: - self.driver.switch_to.context(context_id) - else: - raise KeyError("WEBVIEW context not found") - else: - if self.driver.context != PageElement.native_context: - self.driver.switch_to.context(PageElement.native_context) - def parent_locator_str(self): """Return string with locator tuple for parent element diff --git a/toolium/pageelements/page_elements.py b/toolium/pageelements/page_elements.py index 4173e5ef..13bcb4b2 100644 --- a/toolium/pageelements/page_elements.py +++ b/toolium/pageelements/page_elements.py @@ -41,7 +41,8 @@ class PageElements(CommonObject): """ page_element_class = PageElement #: class of page elements (PageElement, Button...) - def __init__(self, by, value, parent=None, page_element_class=None, order=None): + def __init__(self, by, value, parent=None, page_element_class=None, order=None, webview=False, + webview_context_selection_callback=None, webview_csc_args=None): """Initialize the PageElements object with the given locator components. If parent is not None, find_elements will be performed over it, instead of @@ -53,6 +54,11 @@ def __init__(self, by, value, parent=None, page_element_class=None, order=None): :param order: index value if the locator returns more than one element :param page_element_class: class of page elements (PageElement, Button...) :param shadowroot: CSS SELECTOR of JS element where shadowroot tag appears + :param webview: True if the element is in a mobile webiew + :param webview_context_selection_callback: method provided to select the desired webview context if + automatic_context_selection is enabled. Must return a tuple (context, window_handle) for android, and a context + for ios + :param webview_csc_args: arguments list for webview_context_selection_callback """ super(PageElements, self).__init__() self.locator = (by, value) #: tuple with locator type and locator value @@ -61,6 +67,10 @@ def __init__(self, by, value, parent=None, page_element_class=None, order=None): self.shadowroot = None #: Not implemented for PageElements yet self.driver_wrapper = DriverWrappersPool.get_default_wrapper() #: driver wrapper instance # update instance element class or use PageElement class + self.webview = webview + self.webview_context_selection_callback = webview_context_selection_callback #: callback for selection of the + # webview context with automatic_context_selection + self.webview_csc_args = webview_csc_args #: arguments list for the context selection callback method if page_element_class: self.page_element_class = page_element_class self._page_elements = [] @@ -90,6 +100,12 @@ def web_elements(self): if self.parent: self._web_elements = self.utils.get_web_element(self.parent).find_elements(*self.locator) else: + # check context for mobile webviews + if self.driver_wrapper.config.getboolean_optional('Driver', 'automatic_context_selection'): + if self.driver_wrapper.is_android_test(): + self._android_automatic_context_selection() + elif self.driver_wrapper.is_ios_test(): + self._ios_automatic_context_selection() self._web_elements = self.driver.find_elements(*self.locator) return self._web_elements @@ -104,8 +120,11 @@ def page_elements(self) -> List[Any]: self._page_elements = [] for order, web_element in enumerate(self.web_elements): # Create multiple PageElement with original locator and order - page_element = self.page_element_class(self.locator[0], self.locator[1], parent=self.parent, - order=order) + page_element =\ + self.page_element_class(self.locator[0], self.locator[1], parent=self.parent, + order=order, webview=self.webview, + webview_context_selection_callback=self.webview_context_selection_callback, + webview_csc_args=self.webview_csc_args) page_element.reset_object(self.driver_wrapper) page_element._web_element = web_element self._page_elements.append(page_element) diff --git a/toolium/pageobjects/common_object.py b/toolium/pageobjects/common_object.py index 05ef9499..03690834 100644 --- a/toolium/pageobjects/common_object.py +++ b/toolium/pageobjects/common_object.py @@ -25,6 +25,8 @@ class CommonObject(object): :type logger: logging.Logger :type driver_wrapper: toolium.driver_wrapper.DriverWrapper """ + native_context = "NATIVE_APP" + webview_context_prefix = "WEBVIEW" def __init__(self): """Initialize common object""" @@ -62,3 +64,56 @@ def utils(self): :returns: utils instance """ return self.driver_wrapper.utils + + def _switch_to_new_context(self, context): + """ Change to a new context if its different than the current one""" + if self.driver.context != context: + self.driver.switch_to.context(context) + + def _android_automatic_context_selection(self): + """Change context selection depending if the element is a webview for android devices""" + # we choose the appPackage webview context, and select the first window returned by mobile: getContexts + if self.webview or (self.parent and self.parent.webview): + context = None + window_handle = None + if self.webview_context_selection_callback: + context, window_handle = self.webview_context_selection_callback(*self.webview_csc_args) + elif self.parent and self.parent.webview_context_selection_callback: + context, window_handle = self.parent.webview_context_selection_callback(*self.parent.webview_csc_args) + else: + app_web_context = "{}_{}".format(CommonObject.webview_context_prefix, + self.driver.capabilities['appPackage']) + if app_web_context in self.driver.contexts: + context = app_web_context + self._switch_to_new_context(context) + window_handle = self.driver.window_handles[0] + else: + raise KeyError("WEBVIEW context not found") + + if context: + self._switch_to_new_context(context) + if self.driver.current_window_handle != window_handle: + self.driver.switch_to.window(window_handle) + else: + raise KeyError("WEBVIEW context not found") + else: + self._switch_to_new_context(CommonObject.native_context) + + def _ios_automatic_context_selection(self): + """Change context selection depending if the element is a webview for ios devices""" + # we choose the last webview context returned by mobile: getContexts for the bundleid + if self.webview: + if self.webview_context_selection_callback: + context_id = self.webview_context_selection_callback(*self.webview_csc_args) + else: + contexts = self.driver.execute_script('mobile: getContexts') + context_id = next( + (item['id'] for item in reversed(contexts) if + 'bundleId' in item and item['bundleId'] == self.driver.capabilities['bundleId']), + None) + if context_id: + self._switch_to_new_context(context_id) + else: + raise KeyError("WEBVIEW context not found") + else: + self._switch_to_new_context(CommonObject.native_context) diff --git a/toolium/test/pageelements/test_page_elements.py b/toolium/test/pageelements/test_page_elements.py index 28a6d078..c53bd575 100644 --- a/toolium/test/pageelements/test_page_elements.py +++ b/toolium/test/pageelements/test_page_elements.py @@ -35,6 +35,12 @@ def init_page_elements(self): self.inputs = PageElements(By.XPATH, '//input') self.links = PageElements(By.XPATH, '//a') self.inputs_with_parent = PageElements(By.XPATH, '//input', parent=(By.ID, 'parent')) + self.inputs_with_webview = PageElements(By.XPATH, '//input', webview=True) + self.inputs_with_webview_callback = \ + PageElements(By.XPATH, '//input', webview_context_selection_callback=lambda a, b: (a, b), + webview_csc_args=['WEBVIEW_fake.other', "CDwindow-0123456789"], webview=True) + self.parent_webview = PageElement(By.XPATH, '//parent', webview=True) + self.inputs_with_webview_parent = PageElements(By.XPATH, '//input', parent=self.parent_webview, webview=True) @pytest.fixture @@ -163,3 +169,82 @@ def test_reset_object(driver_wrapper): assert len(login_page.links._web_elements) == 1 assert login_page.links._page_elements[0]._web_element is not None assert page_element_21._web_element is not None + + +def test_get_page_elements_without_webview(driver_wrapper): + driver_wrapper.driver.find_elements.return_value = child_elements + page_elements = LoginPageObject().inputs.page_elements + + # Check webview attribute is set to false by default in child elements + assert not page_elements[0].webview + assert not page_elements[1].webview + + +def test_get_page_elements_with_webview(driver_wrapper): + driver_wrapper.driver.find_elements.return_value = child_elements + page_elements = LoginPageObject().inputs_with_webview.page_elements + + # Check webview attribute is set to true in child element when a Pagelements element + # is created with the webview attribute + assert page_elements[0].webview + assert page_elements[1].webview + + +def test_get_page_elements_with_context_selection_callback_provided(driver_wrapper): + driver_wrapper.driver.find_elements.return_value = child_elements + page_elements = LoginPageObject().inputs_with_webview_callback.page_elements + + # Check context selection callback provided is set correctly to pageelements + assert page_elements[0].webview_context_selection_callback + assert page_elements[0].webview_csc_args + assert page_elements[1].webview_context_selection_callback + assert page_elements[1].webview_csc_args + + +def test_mobile_automatic_context_selection_switch_to_new_webview_context_in_pagelements_without_parent(driver_wrapper): + driver_wrapper.is_android_test = mock.MagicMock(return_value=True) + driver_wrapper.is_ios_test = mock.MagicMock(return_value=False) + driver_wrapper.config = mock.MagicMock() + driver_wrapper.config.set('Driver', 'automatic_context_selection', 'true') + driver_wrapper.driver.capabilities = {'appPackage': 'test.package.fake'} + driver_wrapper.driver.context = "WEBVIEW_other.fake.context" + driver_wrapper.driver.contexts = ["WEBVIEW_test.package.fake", "WEBVIEW_other.fake.context"] + driver_wrapper.driver.current_window_handle = "0987654321" + driver_wrapper.driver.window_handles = ["1234567890", "0987654321"] + LoginPageObject().inputs_with_webview.page_elements + driver_wrapper.driver.switch_to.context.assert_called_with('WEBVIEW_test.package.fake') + driver_wrapper.driver.switch_to.window.assert_called_once_with('1234567890') + + +def test_mobile_automatic_context_selection_called_in_pagelements_without_parent(driver_wrapper): + PageElements._android_automatic_context_selection = mock.MagicMock() + PageElement._android_automatic_context_selection = mock.MagicMock() + driver_wrapper.is_android_test = mock.MagicMock(return_value=True) + driver_wrapper.is_ios_test = mock.MagicMock(return_value=False) + driver_wrapper.config = mock.MagicMock() + driver_wrapper.config.set('Driver', 'automatic_context_selection', 'true') + + mock_element = mock.MagicMock(spec=WebElement) + mock_element.find_elements.return_value = child_elements + driver_wrapper.driver.find_element.return_value = mock_element + + LoginPageObject().inputs_with_webview.page_elements + PageElement._android_automatic_context_selection.assert_not_called + PageElements._android_automatic_context_selection.assert_called_once() + + +def test_mobile_automatic_context_selection_called_in_pagelements_with_parent(driver_wrapper): + PageElements._android_automatic_context_selection = mock.MagicMock() + PageElement._android_automatic_context_selection = mock.MagicMock() + driver_wrapper.is_android_test = mock.MagicMock(return_value=True) + driver_wrapper.is_ios_test = mock.MagicMock(return_value=False) + driver_wrapper.config = mock.MagicMock() + driver_wrapper.config.set('Driver', 'automatic_context_selection', 'true') + + mock_element = mock.MagicMock(spec=WebElement) + mock_element.find_elements.return_value = child_elements + driver_wrapper.driver.find_element.return_value = mock_element + + LoginPageObject().inputs_with_webview_parent.page_elements + PageElement._android_automatic_context_selection.assert_called_once() + PageElements._android_automatic_context_selection.assert_not_called