Skip to content

Wait for proper non-stale element reference in WebDriverWait #1447

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed

Conversation

anders-kiaer
Copy link
Contributor

@anders-kiaer anders-kiaer commented Oct 28, 2020

Resolves #1164.

My experience is that the stale reference appears more frequently in multi-page apps using dcc.Location / dcc.Link, compared with dcc.Tabs. When it happens, it is difficult to debug, as it happens a bit by random (even in CI context, with same initial setup, multiple test runs on a Dash app can sometimes go fine, sometimes fail with StaleElementReferenceException).

This PR ensures that the WebDriverWait waits until a proper non-stale reference exists to the element, inspired by link to StackOverflow post provided in #1164. By default selenium.webdriver.support.wait.WebDriverWait only ignores NoSuchElementException.

Contributor Checklist

  • I have broken down my PR scope into the following TODO tasks
    • Add StaleElementReferenceException to list of ignored selenium errors while waiting on element.
  • I have run the tests locally and they passed. (refer to testing section in contributing)
  • I have added tests, or extended existing tests, to cover any new features or bugs fixed in this PR

optionals

  • I have added entry in the CHANGELOG.md

@anders-kiaer anders-kiaer force-pushed the staleelementreferenceexception branch 2 times, most recently from 81bc417 to 72f986f Compare October 28, 2020 14:34
@anders-kiaer anders-kiaer force-pushed the staleelementreferenceexception branch from 72f986f to 2cdf27f Compare October 28, 2020 14:35
@anders-kiaer anders-kiaer marked this pull request as ready for review October 28, 2020 14:44
@anders-kiaer anders-kiaer marked this pull request as draft October 28, 2020 19:57
@alexcjohnson
Copy link
Collaborator

@anders-kiaer this looks great, thanks! Does the Draft indicate you have more work in mind for the PR? I don't think it necessitates any new tests, since it isn't intended to add new functionality, just make the tests themselves more robust.

The test failure is likely not a problem, just a residual from the tests not running initially. We needed to adjust settings for fork PRs, after changing how we access docker images on CircleCI. If there's no more you intend to run here, an empty commit should resolve the failure.

@anders-kiaer
Copy link
Contributor Author

anders-kiaer commented Oct 29, 2020

Thanks for the feedback @alexcjohnson.

Yes, I changed it to draft since I saw there was a chance I would probably want to change the CHANGELOG.md entry slightly to be more clear, and maybe consider if the PR was needed after all (when I looked more in detail in dash.testing).

My current understanding (might be wrong) is that e.g.

WebDriverWait(driver, timeout, ignored_exceptions=ignored_exceptions).until(some_condition)

will never fail on StaleElementReferenceException within until() if included in ignored_exceptions, but if the user does something like

el = WebDriverWait(driver, timeout, ignored_exceptions=ignored_exceptions).until(some_condition)
el.click()

during the click() method there is no such guarantee anymore (i.e. the ignored_exceptions is used while running until()). That is perhaps obvious for experienced selenium users, but if correct could perhaps be stated clearly in the CHANGELOG.md entry (if so, I will double check if current unstanding is correct first).

The .until(some_condition) methods where ignoring StaleElementReferenceException are relevant should be methods where you find an element, and do something with it (e.g. extract text, style or click on it etc.). However, I see that those classes in Dash already have try / except on their own level, e.g.

class text_to_equal(object):
def __init__(self, selector, text):
self.selector = selector
self.text = text
def __call__(self, driver):
try:
elem = driver.find_element_by_css_selector(self.selector)
logger.debug("text to equal {%s} => expected %s", elem.text, self.text)
return (
str(elem.text) == self.text
or str(elem.get_attribute("value")) == self.text
)
except WebDriverException:
return False
and that they typically catch the base class exception WebDriverException which a broad range of other exceptions inherit from.

I guess it is a matter of preference, if you want to have common exceptions to ignore within WebDriverWait initialization, and/or treat them on the level of the different functions with try / except. And then how broad you want to catch there (WebDriverException is quite broad by looking at the exception list in selenium documentation, might be that if some of those errors should occur you would prefer the actual error to be raised and not transformed into TimeOutException as I guess would be the current behaviour?)


As a side note, the use case that hit us randomly originally was dash_duo.wait_for_element(...).click() raising StaleElementReferenceException (then probably happening within the click() action - since dash_duo.wait_for_element does not do anything with the element after it is found besides returning it). In order to not have everyone deal with try/except StaleElementReferenceException on click actions, maybe we (as a separate issue) could add something like dash_duo.wait_click_when_clickable to accompany wait_for_text_to_equal, wait_for_contains_text...?

@alexcjohnson
Copy link
Collaborator

If we can reduce spurious failures by ignoring more exceptions, without impacting real errors that users will care about, then absolutely let's do it! I'm not aware of any other exceptions in the WebDriverException group arising during these waits, but I haven't really looked into it.

I wouldn't be opposed to a new method - perhaps dash_duo.wait_for_and_click(selector) - if that's the cleanest from a user standpoint. I'm still a bit unclear where the problem is coming from though. Is it that the element renders, so the WebDriverWait returns, but then before the click is executed another callback returns, which causes the element to be deleted and recreated thereby invalidating the original reference?

If so perhaps we should make dash_duo._wait_for_callbacks() an official part of the API (ie remove the leading _)? This one is a little tricky as it looks at the internal state of the Dash renderer, via its redux store, to determine whether there are still callbacks in progress. But we do use it internally and have maintained backward compatibility of that method when we've changed those internals (so you can use a newer version of dash.testing against an app that's running an older version of dash - not a problem for dash_duo but if you use dash_br to access an already-running app this could come up)

@anders-kiaer
Copy link
Contributor Author

anders-kiaer commented May 9, 2021

Sorry for late response on this - have not been working on the Dash based project for some months 🍼

I'm still a bit unclear where the problem is coming from though. Is it that the element renders, so the WebDriverWait returns, but then before the click is executed another callback returns, which causes the element to be deleted and recreated thereby invalidating the original reference?

This is Mozilla's explanation of StaleElementReferenceException. I.e. you have a reference to an element (using e.g. wait_for_element), and then before you e.g. click() on it, it is removed from DOM. I guess there are multiple things that can cause a StaleElementReferenceException in the stack, everything from another callback returns to an internal JavaScript React rerender.

I tested adding your suggestion @alexcjohnson with dash_duo._wait_for_callbacks() prior to the line el.click() randomly giving problems with StaleElementReferenceException. The frequency of observed test failures with stale reference dropped immediately in our large Dash app from ~1/3 to 0/7. 🎉

If making _wait_for_callbacks() an official part of the API is still on the table, I think it sounds like a good idea. Another use case of it could perhaps be when someone has a large Dash app with callbacks that take more time than the wait_timeout - the test code can properly wait for callbacks to finish (instead of the less elegant solution of increasing general wait_timeout?).

@gvwilson gvwilson self-assigned this Jul 18, 2024
@gvwilson gvwilson removed their assignment Aug 2, 2024
@gvwilson gvwilson added P2 considered for next cycle fix fixes something broken community community contribution labels Aug 13, 2024
@gvwilson
Copy link
Contributor

thanks again for this @anders-kiaer - closing as stale (both Dash and Selenium have moved on).

@gvwilson gvwilson closed this Aug 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
community community contribution fix fixes something broken P2 considered for next cycle
Projects
None yet
Development

Successfully merging this pull request may close these issues.

handle StaleElementReferenceException
3 participants