From 4495e8cafe9b94c1e8a314736e9dab128f147fb8 Mon Sep 17 00:00:00 2001 From: Tomas Novacik Date: Thu, 7 Aug 2014 16:02:02 +0200 Subject: [PATCH] Add workaround for StaleElementReferenceException StaleElementReferenceException occurres when accessing web element that was reloaded but looks the same. It happens for example when trying to access form under Project/Data processing/image registry -> register image. It is ussually caused by javascript that is loading the page or by mistake in script logic. This patch wraps the WebDriver and WebElement classes and catches the occurrence of StaleElementReferenceException and tries to find the web element again. Till maximum limit is reached or top element is reached as this suggest that mistake was made by programmer. Closes-Bug: #1353342 Partially implements blueprint: selenium-integration-testing Change-Id: I8e24e952e0fb81bed21b7139870d6d7506509c93 --- .../test/integration_tests/helpers.py | 4 +- .../test/integration_tests/webdriver.py | 160 ++++++++++++++++++ 2 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 openstack_dashboard/test/integration_tests/webdriver.py diff --git a/openstack_dashboard/test/integration_tests/helpers.py b/openstack_dashboard/test/integration_tests/helpers.py index 0bc64fdd6d..065fc6bd22 100644 --- a/openstack_dashboard/test/integration_tests/helpers.py +++ b/openstack_dashboard/test/integration_tests/helpers.py @@ -12,13 +12,13 @@ import os -import selenium from selenium.webdriver.support import ui import testtools import xvfbwrapper from openstack_dashboard.test.integration_tests import config from openstack_dashboard.test.integration_tests.pages import loginpage +from openstack_dashboard.test.integration_tests import webdriver class BaseTestCase(testtools.TestCase): @@ -30,7 +30,7 @@ class BaseTestCase(testtools.TestCase): self.vdisplay = xvfbwrapper.Xvfb(width=1280, height=720) self.vdisplay.start() # Start the Selenium webdriver and setup configuration. - self.driver = selenium.webdriver.Firefox() + self.driver = webdriver.WebDriverWrapper() self.driver.maximize_window() self.conf = config.get_config() self.driver.implicitly_wait(self.conf.dashboard.page_timeout) diff --git a/openstack_dashboard/test/integration_tests/webdriver.py b/openstack_dashboard/test/integration_tests/webdriver.py new file mode 100644 index 0000000000..dbd7e9fb5a --- /dev/null +++ b/openstack_dashboard/test/integration_tests/webdriver.py @@ -0,0 +1,160 @@ +# 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. +import time + +from selenium.common import exceptions +from selenium import webdriver +from selenium.webdriver.common import by +from selenium.webdriver.remote import webelement + + +class ElementNotReloadableException(Exception): + """Raised when reload is not possible.""" + pass + + +class WrapperFindOverride(object): + """Mixin for overriding find_element methods.""" + + def find_element(self, by=by.By.ID, value=None): + web_el = super(WrapperFindOverride, self).find_element(by, value) + return WebElementWrapper(web_el.parent, web_el.id, (by, value), self) + + def find_elements(self, by=by.By.ID, value=None): + web_els = super(WrapperFindOverride, self).find_elements(by, value) + result = [] + for index, web_el in enumerate(web_els): + result.append(WebElementWrapper(web_el.parent, web_el.id, + (by, value), self, index)) + return result + + +class WebElementWrapper(WrapperFindOverride, webelement.WebElement): + """WebElement class wrapper. + + WebElement wrapper that catches the StaleElementReferenceException and + tries to reload the element by sending request to its source element + (element that created actual element) for reload, in case that source + element needs to be reloaded as well, it asks its parent till web driver + is reached. In case driver was reached and did not manage to find the + element it is probable that programmer made a mistake and actual + StaleElementReferenceException is raised. + """ + + STALE_ELEMENT_REFERENCE_WAIT = 0.5 + STALE_ELEMENT_REFERENCE_MAX_TRY = 10 + + def __init__(self, parent, id_, locator, src_element, index=None): + super(WebElementWrapper, self).__init__(parent, id_) + self.locator = locator + self.src_element = src_element + + # StaleElementReferenceException occurrence counter + self.stale_reference_occurrence = 0 + + # storing if web element reload succeed or not + # in case of fail StaleElementReferenceException is raised + self.reload_failed = False + + # in case element was looked up previously via find_elements + # we need his position in the returned list + self.index = index + + # if reloading of some other web element is in progress + # StaleElementReferenceException is not raised within current context + self.web_element_reload = False + + def reload_request(self, locator, index=None): + self.web_element_reload = True + try: + # element was found out via find_elements + if index is not None: + web_els = self.src_element.find_elements(*locator) + web_el = web_els[index] + else: + web_el = self.src_element.find_element(*locator) + except (exceptions.NoSuchElementException, IndexError): + return False + + self.web_element_reload = False + return web_el + + def _reload_element(self): + """Method for starting reload process on current instance.""" + web_el = self.src_element.reload_request(self.locator, self.index) + if not web_el: + return + self._parent = web_el.parent + self._id = web_el.id + + def _execute(self, command, params=None): + """Overriding in order to catch StaleElementReferenceException.""" + result = None + while True: + try: + result = super(WebElementWrapper, self)._execute(command, + params) + break + except exceptions.StaleElementReferenceException: + + # in case we reach the limit STALE_ELEMENT_REFERENCE_MAX_TRY + # it is very probable that it is programmer fault + if self.reload_failed or self.stale_reference_occurrence \ + > self.STALE_ELEMENT_REFERENCE_MAX_TRY: + raise + + # this is either programmer fault + # (bad logic in accessing elements) + # or web page content is been loaded via ajax, let's go with + # the second one and wait STALE_ELEMENT_REFERENCE_WAIT till + # the assumed page content is loaded and try + # to execute the whole process + # STALE_ELEMENT_REFERENCE_MAX_TRY times in case of failures + time.sleep(self.STALE_ELEMENT_REFERENCE_WAIT) + + # try to reload the web element + # if result is false it means that request has gone to the + # driver and he did not find the element -> must be programmer + # fault, because it seems we are on entirely different page + try: + self._reload_element() + except ElementNotReloadableException: + + # In case this element was responsible only for loading + # some other element raise the exception further + if self.web_element_reload: + raise + else: + self.reload_failed = True + + # increment occurrences + self.stale_reference_occurrence += 1 + + # reset counter + self.stale_reference_occurrence = 0 + return result + + +class WebDriverWrapper(WrapperFindOverride, webdriver.Firefox): + """Wrapper for webdriver to return WebElementWrapper on find_element.""" + + def reload_request(self, locator, index): + try: + # element was found out via find_elements + if index is not None: + web_els = self.find_elements(*locator) + web_el = web_els[index] + else: + web_el = self.find_element(*locator) + return web_el + except (exceptions.NoSuchElementException, IndexError): + raise ElementNotReloadableException()