From 6a55dabff734f570b66ce417a50a37001a389507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Gonz=C3=A1lez=20Alonso?= Date: Wed, 8 Mar 2017 15:47:31 +0100 Subject: [PATCH 1/2] Add wait_until_clickable method to page elements --- CHANGELOG.rst | 2 ++ docs/visual_testing.rst | 2 +- toolium/pageelements/button_page_element.py | 13 ++++++-- toolium/pageelements/page_element.py | 16 ++++++++++ .../test/pageelements/test_page_element.py | 20 +++++++++++++ toolium/utils.py | 30 +++++++++++++++++-- 6 files changed, 78 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 337a9169..da409f30 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,8 @@ v1.2.3 - Save *geckodriver.log* file in output folder - Fix MagickEngine name error when using an old version of needle +- Add *wait_until_clickable* method to Utils and PageElement classes to search for an element and wait until it is + clickable v1.2.2 ------ diff --git a/docs/visual_testing.rst b/docs/visual_testing.rst index ffaf1ef1..e044ac1b 100644 --- a/docs/visual_testing.rst +++ b/docs/visual_testing.rst @@ -14,7 +14,7 @@ To achieve this goal, this tool follows these steps: - It takes a screenshot of the current web page or mobile screen (a full screenshot or a single element). - Then, if exists a previous capture with the same name, it compares both of them and shows their differences. - If not, it saves the screenshot to be used as a base in later executions. Probably these images might be reviewed -manually to assure that can be used as expected result. + manually to assure that can be used as expected result. As web pages and mobile applications look different when using different browsers, mobile devices, resolutions, etc. Toolium provides you with a way of storing different base images, called baselines, through a configuration property diff --git a/toolium/pageelements/button_page_element.py b/toolium/pageelements/button_page_element.py index a11529b3..f91db776 100644 --- a/toolium/pageelements/button_page_element.py +++ b/toolium/pageelements/button_page_element.py @@ -16,6 +16,7 @@ limitations under the License. """ +from selenium.common.exceptions import StaleElementReferenceException from toolium.pageelements.page_element import PageElement @@ -26,12 +27,20 @@ def text(self): :returns: element text value """ - return self.web_element.text + try: + return self.web_element.text + except StaleElementReferenceException: + # Retry if element has changed + return self.web_element.text def click(self): """Click the element :returns: page element instance """ - self.web_element.click() + try: + self.wait_until_clickable().web_element.click() + except StaleElementReferenceException: + # Retry if element has changed + self.web_element.click() return self diff --git a/toolium/pageelements/page_element.py b/toolium/pageelements/page_element.py index 16d113e4..207b5f2a 100644 --- a/toolium/pageelements/page_element.py +++ b/toolium/pageelements/page_element.py @@ -126,6 +126,22 @@ def wait_until_not_visible(self, timeout=10): raise exception return self + def wait_until_clickable(self, timeout=10): + """Search element and wait until it is clickable + + :param timeout: max time to wait + :returns: page element instance + """ + try: + self.utils.wait_until_element_clickable(self, timeout) + except TimeoutException as exception: + parent_msg = " and parent locator '{}'".format(self.parent) if self.parent else '' + msg = "Page element of type '%s' with locator %s%s not found or is not clickable after %s seconds" + self.logger.error(msg, type(self).__name__, self.locator, parent_msg, timeout) + exception.msg += "\n {}".format(msg % (type(self).__name__, self.locator, parent_msg, timeout)) + raise exception + return self + def assert_screenshot(self, filename, threshold=0, exclude_elements=[], force=False): """Assert that a screenshot of the element is the same as a screenshot on disk, within a given threshold. diff --git a/toolium/test/pageelements/test_page_element.py b/toolium/test/pageelements/test_page_element.py index 88822624..4d820a8b 100644 --- a/toolium/test/pageelements/test_page_element.py +++ b/toolium/test/pageelements/test_page_element.py @@ -237,6 +237,26 @@ def test_wait_until_not_visible_exception(driver_wrapper): "visible after 10 seconds" in str(excinfo.value) +def test_wait_until_clickable(driver_wrapper): + driver_wrapper.utils.wait_until_element_clickable = mock.MagicMock(return_value=mock_element) + + page_element = RegisterPageObject(driver_wrapper).username + element = page_element.wait_until_clickable() + + assert element == page_element + + +def test_wait_until_clickable_exception(driver_wrapper): + driver_wrapper.utils.wait_until_element_clickable = mock.MagicMock() + driver_wrapper.utils.wait_until_element_clickable.side_effect = TimeoutException('Unknown') + + page_element = RegisterPageObject(driver_wrapper).username + with pytest.raises(TimeoutException) as excinfo: + page_element.wait_until_clickable() + assert "Page element of type 'PageElement' with locator ('xpath', '//input[0]') not found or is not " \ + "clickable after 10 seconds" in str(excinfo.value) + + @mock.patch('toolium.visual_test.VisualTest.__init__', return_value=None) @mock.patch('toolium.visual_test.VisualTest.assert_screenshot') def test_assert_screenshot(visual_assert_screenshot, visual_init, driver_wrapper): diff --git a/toolium/utils.py b/toolium/utils.py index 6c3a1380..8698b121 100644 --- a/toolium/utils.py +++ b/toolium/utils.py @@ -140,7 +140,8 @@ def _expected_condition_find_element(self, element): return web_element 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 + """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: the web element if it is visible or False @@ -153,7 +154,8 @@ def _expected_condition_find_element_visible(self, element): return False 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 + """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 @@ -187,6 +189,20 @@ def _expected_condition_find_first_element(self, elements): pass return element_found + 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 + + :param element: PageElement or element locator as a tuple (locator_type, locator_value) to be found + :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) + try: + return web_element if web_element and web_element.is_enabled() else False + except StaleElementReferenceException: + return False + def _wait_until(self, condition_method, condition_input, timeout=10): """ Common method to wait until condition met @@ -252,6 +268,16 @@ def wait_until_first_element_is_found(self, elements, timeout=10): exception.msg += "\n {}".format(msg % timeout) raise exception + def wait_until_element_clickable(self, element, timeout=10): + """Search element and wait until it is clickable + + :param element: PageElement or element locator as a tuple (locator_type, locator_value) to be found + :param timeout: max time to wait + :returns: the web element if it is clickable or False + :rtype: selenium.webdriver.remote.webelement.WebElement or appium.webdriver.webelement.WebElement + """ + return self._wait_until(self._expected_condition_find_element_clickable, element, timeout) + def get_remote_node(self): """Return the remote node that it's executing the actual test session From 6b43cce2038f14efc735652c5acbeed7bfb00dbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Gonz=C3=A1lez=20Alonso?= Date: Fri, 10 Mar 2017 14:46:04 +0100 Subject: [PATCH 2/2] Release Toolium 1.2.3 --- CHANGELOG.rst | 2 +- VERSION | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index da409f30..355d22e7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,7 +4,7 @@ Toolium Changelog v1.2.3 ------ -*In development* +*Release date: 2017-03-10* - Save *geckodriver.log* file in output folder - Fix MagickEngine name error when using an old version of needle diff --git a/VERSION b/VERSION index b4da54e6..0495c4a8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.3-dev +1.2.3