From a37d789326db6e320f561249ca5ada9546d53a22 Mon Sep 17 00:00:00 2001 From: Brandon Wolfe Date: Wed, 28 Oct 2020 13:00:27 -0500 Subject: [PATCH] Better _is_element_present plus 'Select Elements From Menu' (#146) * Testing * Testing * Cleanup * cleanup * more tests * more * fix coverage * remove unneeded count increment * new docs, increment version.py * added test * doc update * remove unused import * remove duplicate tests --- docs/APILibraryDocumentation.html | 2 +- docs/DesktopLibraryDocumentation.html | 2 +- docs/GUILibraryDocumentation.html | 2 +- docs/MobileLibraryDocumentation.html | 2 +- docs/SOAPLibraryDocumentation.html | 2 +- samples/DesktopTests.robot | 12 +++-- src/Zoomba/DesktopLibrary.py | 60 +++++++++++++++++++-- test/Desktop/test_desktop.py | 78 ++++++++++++++++++++++++++- version.py | 2 +- 9 files changed, 148 insertions(+), 14 deletions(-) diff --git a/docs/APILibraryDocumentation.html b/docs/APILibraryDocumentation.html index 4416e2ea..70ba9f33 100644 --- a/docs/APILibraryDocumentation.html +++ b/docs/APILibraryDocumentation.html @@ -614,7 +614,7 @@ jQuery.extend({highlight:function(e,t,n,r){if(e.nodeType===3){var i=e.data.match(t);if(i){var s=document.createElement(n||"span");s.className=r||"highlight";var o=e.splitText(i.index);o.splitText(i[0].length);var u=o.cloneNode(true);s.appendChild(u);o.parentNode.replaceChild(s,o);return 1}}else if(e.nodeType===1&&e.childNodes&&!/(script|style)/i.test(e.tagName)&&!(e.tagName===n.toUpperCase()&&e.className===r)){for(var a=0;a diff --git a/docs/DesktopLibraryDocumentation.html b/docs/DesktopLibraryDocumentation.html index 025705a4..a21b2da8 100644 --- a/docs/DesktopLibraryDocumentation.html +++ b/docs/DesktopLibraryDocumentation.html @@ -614,7 +614,7 @@ jQuery.extend({highlight:function(e,t,n,r){if(e.nodeType===3){var i=e.data.match(t);if(i){var s=document.createElement(n||"span");s.className=r||"highlight";var o=e.splitText(i.index);o.splitText(i[0].length);var u=o.cloneNode(true);s.appendChild(u);o.parentNode.replaceChild(s,o);return 1}}else if(e.nodeType===1&&e.childNodes&&!/(script|style)/i.test(e.tagName)&&!(e.tagName===n.toUpperCase()&&e.className===r)){for(var a=0;a diff --git a/docs/GUILibraryDocumentation.html b/docs/GUILibraryDocumentation.html index dd70df2c..03e311a0 100644 --- a/docs/GUILibraryDocumentation.html +++ b/docs/GUILibraryDocumentation.html @@ -614,7 +614,7 @@ jQuery.extend({highlight:function(e,t,n,r){if(e.nodeType===3){var i=e.data.match(t);if(i){var s=document.createElement(n||"span");s.className=r||"highlight";var o=e.splitText(i.index);o.splitText(i[0].length);var u=o.cloneNode(true);s.appendChild(u);o.parentNode.replaceChild(s,o);return 1}}else if(e.nodeType===1&&e.childNodes&&!/(script|style)/i.test(e.tagName)&&!(e.tagName===n.toUpperCase()&&e.className===r)){for(var a=0;a diff --git a/docs/MobileLibraryDocumentation.html b/docs/MobileLibraryDocumentation.html index 3e62a80d..62b19307 100644 --- a/docs/MobileLibraryDocumentation.html +++ b/docs/MobileLibraryDocumentation.html @@ -614,7 +614,7 @@ jQuery.extend({highlight:function(e,t,n,r){if(e.nodeType===3){var i=e.data.match(t);if(i){var s=document.createElement(n||"span");s.className=r||"highlight";var o=e.splitText(i.index);o.splitText(i[0].length);var u=o.cloneNode(true);s.appendChild(u);o.parentNode.replaceChild(s,o);return 1}}else if(e.nodeType===1&&e.childNodes&&!/(script|style)/i.test(e.tagName)&&!(e.tagName===n.toUpperCase()&&e.className===r)){for(var a=0;a diff --git a/docs/SOAPLibraryDocumentation.html b/docs/SOAPLibraryDocumentation.html index ec935a7d..f79cf3b3 100644 --- a/docs/SOAPLibraryDocumentation.html +++ b/docs/SOAPLibraryDocumentation.html @@ -614,7 +614,7 @@ jQuery.extend({highlight:function(e,t,n,r){if(e.nodeType===3){var i=e.data.match(t);if(i){var s=document.createElement(n||"span");s.className=r||"highlight";var o=e.splitText(i.index);o.splitText(i[0].length);var u=o.cloneNode(true);s.appendChild(u);o.parentNode.replaceChild(s,o);return 1}}else if(e.nodeType===1&&e.childNodes&&!/(script|style)/i.test(e.tagName)&&!(e.tagName===n.toUpperCase()&&e.className===r)){for(var a=0;a diff --git a/samples/DesktopTests.robot b/samples/DesktopTests.robot index d102a2cc..9be55bb5 100644 --- a/samples/DesktopTests.robot +++ b/samples/DesktopTests.robot @@ -94,9 +94,11 @@ Send Keys Keyword Test Send Keys 24 \ue025 2 \ue007 Page Should Contain Text 26 -Send Keys with Modifier (Ctrl + v) - Send Keys \ue009 v \ue009 - Wait Until Element Contains accessibility_id=CalculatorResults Display is Invalid input +Send Keys with Modifier (Alt + 2) + Send Keys \ue00A 2 \ue00A + Wait Until Page Contains Element Name=Scientific Calculator mode + Send Keys \ue00A 1 \ue00A + Wait Until Page Contains Element Name=Standard Calculator mode Send Keys To Element Keyword Test Send Keys To Element name=Display is 0 24 \ue025 2 \ue007 @@ -118,6 +120,10 @@ Select Element From Combobox Test Wait Until Page Contains Element accessibility_id=TogglePaneButton Select Element From ComboBox accessibility_id=TogglePaneButton accessibility_id=Standard +Select Elements From Menu Test + Select Elements From Menu name=Two name=Three name=Four + Wait Until Element Contains accessibility_id=CalculatorResults 234 + Switch To Desktop Test Close Application Switch Application Desktop diff --git a/src/Zoomba/DesktopLibrary.py b/src/Zoomba/DesktopLibrary.py index d819318f..a6713b23 100644 --- a/src/Zoomba/DesktopLibrary.py +++ b/src/Zoomba/DesktopLibrary.py @@ -8,7 +8,8 @@ from robot.libraries.BuiltIn import BuiltIn from selenium.common.exceptions import NoSuchElementException from selenium.webdriver.common.action_chains import ActionChains -from time import sleep +from time import sleep, time +from robot import utils try: AppiumCommon = importlib.import_module('Helpers.AppiumCommon', package='Helpers') @@ -121,7 +122,7 @@ def get_keyword_names(self): 'mouse_over_and_context_click_element', 'mouse_over_by_offset', 'drag_and_drop', 'drag_and_drop_by_offset', 'send_keys', 'send_keys_to_element', 'capture_page_screenshot', 'save_appium_screenshot', 'select_element_from_combobox', - 'driver_setup', 'driver_teardown', + 'driver_setup', 'driver_teardown', 'select_elements_from_menu', # External Libraries 'clear_text', 'click_button', 'click_element', 'close_all_applications', 'close_application', 'element_attribute_should_match', 'element_should_be_disabled', @@ -605,6 +606,30 @@ def select_element_from_combobox(self, list_locator, element_locator, skip_to_de self.click_element(element_locator) self.switch_application(original_index) + @keyword("Select Elements From Menu") + def select_elements_from_menu(self, *args): + """Selects N number of elements in the order they are given. This is useful for working + though a nested menu listing of elements. + + On failure this keyword wil attempt to select the elements from the desktop session due to + the nature of some pop-out menus in Windows.""" + count = 0 + try: + for each in args: + self.click_element(each) + count += 1 + except NoSuchElementException: + original_index = self._cache.current_index + self.switch_application('Desktop') + for each in args[count:]: + try: + self.click_element(each) + except NoSuchElementException: + self._wait_until_page_contains_element(each, self.get_appium_timeout()) + self.click_element(each) + count += 1 + self.switch_application(original_index) + # Private def _move_to_element(self, actions, element, x_offset=0, y_offset=0): if x_offset != 0 or y_offset != 0: @@ -654,6 +679,23 @@ def _element_find(self, locator, first_only, *kwargs): return driver.find_elements_by_accessibility_id(criteria) zoomba.fail("Element locator with prefix '" + prefix + "' is not supported") + def _is_element_present(self, locator): + prefix, criteria = self._parse_locator(locator) + driver = self._current_application() + if prefix is None: + if criteria.startswith('//'): + return len(driver.find_elements_by_xpath(criteria)) > 0 + return len(driver.find_elements_by_accessibility_id(criteria)) > 0 + if prefix == 'name': + return len(driver.find_elements_by_name(criteria)) > 0 + if prefix == 'class': + return len(driver.find_elements_by_class_name(criteria)) > 0 + if prefix == 'xpath': + return len(driver.find_elements_by_xpath(criteria)) > 0 + if prefix == 'accessibility_id': + return len(driver.find_elements_by_accessibility_id(criteria)) > 0 + zoomba.fail("Element locator with prefix '" + prefix + "' is not supported") + def _parse_locator(self, locator): prefix = None criteria = locator @@ -672,4 +714,16 @@ def _wait_until_page_contains_element(self, locator, timeout=None, error=None): """Internal version to avoid duplicate screenshots""" if not error: error = "Element '%s' did not appear in " % locator - self._wait_until(timeout, error, self._element_find, locator, True) + self._wait_until(timeout, error, self._is_element_present, locator) + + # Overrides to prevent expensive log_source call + def _wait_until_no_error(self, timeout, wait_func, *args): + timeout = utils.timestr_to_secs(timeout) if timeout is not None else self._timeout_in_secs + max_time = time() + timeout + while True: + timeout_error = wait_func(*args) + if not timeout_error: + return + if time() > max_time: + raise AssertionError(timeout_error) + sleep(0.2) diff --git a/test/Desktop/test_desktop.py b/test/Desktop/test_desktop.py index d59a9c14..2c3b6e1d 100644 --- a/test/Desktop/test_desktop.py +++ b/test/Desktop/test_desktop.py @@ -11,6 +11,11 @@ from webdriverremotemock import WebdriverRemoteMock +def _long_running_function(): + while True: + return 'Error' + + class TestInternal(unittest.TestCase): def test_get_keyword_names_successful(self): DesktopLibrary().get_keyword_names() @@ -318,6 +323,53 @@ def test_element_find_fail(self): self.assertRaisesRegex(AssertionError, "Element locator with prefix 'blockbuster_id' is not supported", DesktopLibrary._element_find, mock_desk, "blockbuster_id=123456789", True, True) + def test_is_element_present_by_name(self): + mock_desk = MagicMock() + mock_desk._parse_locator = MagicMock(return_value=['name', 'Capture']) + DesktopLibrary._is_element_present(mock_desk, "Name='Capture'") + mock_desk._current_application().find_elements_by_name.assert_called_with('Capture') + + def test_is_element_present_by_accessibility_id(self): + mock_desk = MagicMock() + mock_desk._parse_locator = MagicMock(return_value=['accessibility_id', 'Capture']) + DesktopLibrary._is_element_present(mock_desk, "accessibility_id='Capture'") + mock_desk._current_application().find_elements_by_accessibility_id.assert_called_with( + 'Capture') + + def test_is_element_present_by_class_name(self): + mock_desk = MagicMock() + mock_desk._parse_locator = MagicMock(return_value=['class', 'Capture']) + DesktopLibrary._is_element_present(mock_desk, "class='Capture'") + mock_desk._current_application().find_elements_by_class_name.assert_called_with('Capture') + + def test_is_element_present_by_xpath(self): + mock_desk = MagicMock() + mock_desk._parse_locator = MagicMock(return_value=['xpath', 'Capture']) + DesktopLibrary._is_element_present(mock_desk, "xpath=//TreeItem[@Name='Capture']") + mock_desk._current_application().find_elements_by_xpath.assert_called_with('Capture') + + def test_is_element_present_by_default_xpath(self): + mock_desk = MagicMock() + mock_desk._parse_locator = MagicMock(return_value=[None, "//TreeItem[@Name='Capture']"]) + DesktopLibrary._is_element_present(mock_desk, "//TreeItem[@Name='Capture']") + mock_desk._current_application().find_elements_by_xpath.assert_called_with( + "//TreeItem[@Name='Capture']") + + def test_is_element_present_by_default_accessibility_id(self): + mock_desk = MagicMock() + mock_desk._parse_locator = MagicMock(return_value=[None, "Capture"]) + DesktopLibrary._is_element_present(mock_desk, "Capture") + mock_desk._current_application().find_elements_by_accessibility_id.assert_called_with( + 'Capture') + + def test_is_element_present_fail(self): + mock_desk = MagicMock() + mock_desk._parse_locator = MagicMock(return_value=['blockbuster_id', '123456789']) + self.assertRaisesRegex(AssertionError, + "Element locator with prefix 'blockbuster_id' is not supported", + DesktopLibrary._is_element_present, mock_desk, + "blockbuster_id=123456789") + def test_parse_locator_xpath(self): mock_desk = MagicMock() parse = DesktopLibrary._parse_locator(mock_desk, '//test') @@ -462,6 +514,23 @@ def test_select_from_combobox_retry_desktop(self): DesktopLibrary.select_element_from_combobox(mock_desk, 'some_locator', 'another_locator', True) mock_desk.click_element.assert_called_with('another_locator') + def test_select_elements_from_menu_retry_desktop(self): + mock_desk = MagicMock() + mock_desk.click_element = MagicMock(side_effect=[True, NoSuchElementException, True]) + DesktopLibrary.select_elements_from_menu(mock_desk, 'some_locator', 'another_locator') + mock_desk.click_element.assert_called_with('another_locator') + + def test_select_elements_from_menu_retry_desktop_2(self): + mock_desk = MagicMock() + mock_desk.click_element = MagicMock(side_effect=[True, NoSuchElementException, NoSuchElementException, True]) + DesktopLibrary.select_elements_from_menu(mock_desk, 'some_locator', 'another_locator') + mock_desk.click_element.assert_called_with('another_locator') + + def test_select_elements_from_menu_no_desktop(self): + mock_desk = MagicMock() + DesktopLibrary.select_elements_from_menu(mock_desk, 'some_locator', 'another_locator') + mock_desk.click_element.assert_called_with('another_locator') + def test_wait_until_page_contains_private(self): mock_desk = MagicMock() DesktopLibrary._wait_until_page_contains(mock_desk, 'some_text', 5) @@ -471,5 +540,10 @@ def test_wait_until_page_contains_element_private(self): mock_desk = MagicMock() DesktopLibrary._wait_until_page_contains_element(mock_desk, 'some_element', 5) mock_desk._wait_until.assert_called_with(5, "Element 'some_element' did not appear in " - "", unittest.mock.ANY, 'some_element', - True) + "", unittest.mock.ANY, 'some_element') + + def test_wait_until_no_error_timeout(self): + mock_desk = MagicMock() + self.assertRaisesRegex(AssertionError, + 'Error', DesktopLibrary._wait_until_no_error, mock_desk, + 1, _long_running_function) diff --git a/version.py b/version.py index 65fd358a..3c8616f9 100644 --- a/version.py +++ b/version.py @@ -1 +1 @@ -VERSION = "2.5.4" +VERSION = "2.5.5"