Python Selenium hover action concatenates in memory - python

I am testing a website which has a menu with submenus appearing on hover.
I have created a function to interact with this menu:
def go_to(navbar_item, menu_item):
# find the navbar item
assets_main_menu = driver.find_element(By.ID, navbar_item)
#hover over the navbar item, so the submenu appears
hover.move_to_element(assets_main_menu).perform()
# find the submenu item
xpath = "//*[contains(text(), \'" + menu_item + "\')]"
destination = driver.find_element_by_xpath(xpath)
# hover over the submenu item and clicks
hover.move_to_element(destination).click().perform()
The problem is that i use this function more than once such as:
# action 1
go_to('navbar item1 id', 'submenu item1')
do_something()
# action 2
go_to('navbar item1 id', 'submenu item2')
do something()
# action 3
go_to('navbar item1 id', 'submenu item3')
do_something()
selenium actually repeats the previous steps going through the past menu items like:
ACTUAL OUPTUP
action 1, do something -> action 1, action 2, do something -> action 1, action 2, action 3, do something
Instead my DESIRED OUTPUT would be:
action 1, do something -> action 2, do something -> action 3, do something
I tried unsetting the variables:
navbar_item, menu_item, hover, xpath, destination.
at the end of the function with no luck.
I have also tried to instantiate hover within my function
hover = ActionChains(driver);
but in this last attempt my code stopped working.

When you call an action chain, perform() does not clear out the previous steps. You've only really shared your function so the real culprit is the structure of your code and how python consumes variables.
I note in your function, you pass in two strings but your function knows what driver and hover are. Which sounds like you're using global variables.
To demo your problem I created this simple page for you with a click counter:
<html>
<body>
<button id="button" onclick="document.getElementById('input').value = parseInt(document.getElementById('input').value) + 1">Click me</button>
<input id="input" value="0"></input>
</body>
</html>
It's a flat page that just knocks up a number each time you press the button:
Then, to show you what's happening, i created a similar version of your code:
driver = webdriver.Chrome()
driver.implicitly_wait(10)
driver.get(r"c:\git\test.html")
actions = ActionChains(driver)
def ClickByActions(element):
actions.move_to_element(element).click().perform()
#find the button and click it a few times...
button = driver.find_element_by_id('button')
ClickByActions(button)
ClickByActions(button)
ClickByActions(button)
With this, you expect the end click-count-value to be 3. However, it is 6.
Same as your problem. First call does +1, second call does +1 +1, third call does +1 +1 +1.
Finally! the solution - create action chain in the function with your driver:
def ClickByActions(element):
localActions = ActionChains(driver)
localActions.move_to_element(element).click().perform()
I note in the comments you say you tried this. Can you please try:
not using hover but another name -
pass in driver instead of relying on it being a global variable. For this you would use go_to(navbar_item, menu_item, driver)
Apparently hover.reset_actions() should also work - but this didn't work for me.
If these don't work, please share your site URL so I can try on your actual site or say what the error is and describe what happens.

Related

Python Selenium - element is not attached to the page document - once button is clicked

When I click on an item inside a dropdown list, the item is correctly clicked but the error: "stale element is not attached to the page document" is raised.
I guess this error is raised because of an element I use as changed inside the DOM or has been refreshed, but I didn't find any solutions for now. Here's my code:
exam_question_type = self.driver.find_element(By.XPATH, f"//*[name()='svg'][#class='css-19bqh2r']")
exam_question_type.click()
WebDriverWait(self.driver).until(ec.visibility_of_element_located((By.CSS_SELECTOR, 'class=" css-b02ejv-menu"'))
select_question_type = self.driver.find_elements(By.CSS_SELECTOR, 'class="CustomOptionMenu_button__2GnMd"')
for type in select_question_type:
if type.text == "QCM grid":
try:
ActionChains(self.driver).click(type).perform()
except StaleElementReferenceException:
pass
What i'm doing wrong?
Since by clicking on elements inside the dropdown list the page is changing and elements are refreshed you have to grab select_question_type elements again after the refreshing. Something like this:
exam_question_type = self.driver.find_element(By.XPATH, f"//*[name()='svg'][#class='css-19bqh2r']")
exam_question_type.click()
WebDriverWait(self.driver).until(ec.visibility_of_element_located((By.CSS_SELECTOR, 'class=" css-b02ejv-menu"'))
select_question_type = self.driver.find_elements(By.CSS_SELECTOR, 'class="CustomOptionMenu_button__2GnMd"')
for index, type_el in enumerate(select_question_type):
type_el = select_question_type[index]
if type_el.text == "QCM grid":
ActionChains(self.driver).click(type_el).perform()
WebDriverWait(self.driver).until(ec.visibility_of_element_located((By.CSS_SELECTOR, 'class=" css-b02ejv-menu"'))
select_question_type = self.driver.find_elements(By.CSS_SELECTOR, 'class="CustomOptionMenu_button__2GnMd"')
Also, I've changed the element name from type to type_el since type is a keyword that should not be used for naming of variables.

Selenium Hover over element gives me an Error AFTER is goes past the code?

I have the following code:
for button in buttons:
ActionChains(driver).move_to_element(button).perform()
time.sleep(2)
button.click()
time.sleep(2)
try:
wait_button.until(EC.presence_of_element_located((By.XPATH,'//div/h2')))
time.sleep(2)
name = driver.find_element_by_xpath('//div/h2').text
except:
wait_button.until(EC.presence_of_element_located((By.XPATH,'//span[#id="chat-header-title"]')))
time.sleep(2)
name = driver.find_element_by_xpath('//span[#id="chat-header-title"]').text
def pull_ul() -> list:
chat_frame = driver.find_element_by_xpath("//iframe[starts-with(#id, 'experience-container-')]")
driver.switch_to.frame(chat_frame)
wait_button.until(EC.presence_of_element_located((By.XPATH,'//ul[#aria-label="Chat content"]')))
the_ul =driver.find_element(By.XPATH,'//ul[#aria-label="Chat content"]')
new_lis =the_ul.find_elements(By.TAG_NAME,'li')
return new_lis
def pull_ul_again() -> list:
the_ul =driver.find_element(By.XPATH,'//ul[#aria-label="Chat content"]')
new_lis_2 =the_ul.find_elements(By.TAG_NAME,'li')
return new_lis_2
lis = pull_ul()
print(f"Archiving Chat with {name} ...\n")
print("this is len lis: ",len(lis), "for " + name)
And here is what the terminal shows:
As you can see, the code actually does run past the line that threw up the error, how is this possible? Also, why would this be happening , I successfully ran the code multiple times and suddenly it starts throwing the follwing error?
The loop that you're doing,
for button in buttons:
ActionChains(driver).move_to_element(button).perform()
...
is causing your StaleElementReferenceException because inside that you have driver.switch_to.frame(chat_frame), but you never switch back to default content or to the parent frame, which means that your buttons won't exist in that level, and so Selenium throws StaleElementReferenceException as a result. That could also happen if you navigate away from the page (which might also be happening depending on the button clicks).
If you ever switch frames or leave pages, you have to re-find elements in order to interact with them.

A way around the StaleElementReferenceException after opening and closing a new tab?

So I have this project: It is a website with multiple WebElements on it. I find those WebElements by their class name (they all have the same one obviously) and then iterate through them to click on each of them. After clicking on them I have to click on another button "next". Some of them then open a website in a new tab (others don't). I then immediately close the newly opened tab and try to iterate through the next element when I get the StaleElementReferenceException.
Don't get me wrong here, I know what a StaleElementReferenceException is, I just don't know why it occurs. The DOM of the initial website doesn't seem to change and more importantly: The WebElement I'm trying to reach in the next iteration is still known so I can print it out, but not click on it.
I have tried working around this issue by creating a new class CustomElement to permanently "save" the found WebElements to be able to reach them after the DOM has changed but that also doesn't seem to be working.
Whatever here's some code for you guys:
def myMethod():
driver.get("https://initialwebsite.com")
time.sleep(1)
scrollToBottom() #custom Method to scroll to the bottom of the website to make sure I find all webelemnts
ways = driver.find_elements_by_class_name("sc-dYzWWc")
waysCounter = 1
for way in ways:
# print("clicking: " + str(way)) ##this will get executed even if there was a new tab opened in the previous iteration....
driver.execute_script("arguments[0].click();", way)
# print("clicked: " + str(way)) ##...but this won't get executed
try:
text = driver.find_element_by_xpath("/html/body/div[1]/div/div[2]/div/div[1]/div/div[7]/div[2]/div[" + str(entryWaysCounter) + "]/div[1]/div/div/div[1]").text
except:
waysCounter += 1
text = driver.find_element_by_xpath("/html/body/div[1]/div/div[2]/div/div[1]/div/div[7]/div[2]/div[" + str(entryWaysCounter) + "]/div[1]/div/div/div[1]").text
methode = None
#big bunch of if and else statements to give methode a specific number based on what text reads
print(methode)
weiterButton = driver.find_element_by_xpath(
"/html/body/div[1]/div/div[2]/div/div[1]/div/div[7]/div[2]/div[" + str(
entryWaysCounter) + "]/div[2]/div/div/div/div/div/div[2]/button[2]")
try:
driver.execute_script("arguments[0].click();", weiterButton)
except:
pass
if (methode == 19):
time.sleep(0.2)
try:
driver.switch_to.window(driver.window_handles[1])
driver.close()
time.sleep(0.5)
driver.switch_to.window(driver.window_handles[0])
time.sleep(0.5)
except:
pass
waysCounter += 1
time.sleep(0.5)
And for those who are curious here's the workaround class I set up:
class CustomElement:
def __init__(self, text, id, location):
self.text = text
self.id = id
self.location = location
def __str__(self):
return str(str(self.text) + " \t" + str(self.id) + " \t" + str(self.location))
def storeWebElements(seleniumElements):
result = []
for elem in seleniumElements:
result.append(CustomElement(elem.text, elem.id, elem.location))
return result
I tried then working with the id and "re-finding" the WebElement ("way") by id but apparently the id that gets saved is a completely different id.
So what can I say I really tried my best, searched nearly every forum but didn't come up with a good soluation, I really hope you got one for me :)
Thanks!
Are you crawling links? If so then you want to save the destination, not the element.
Otherwise you could force the link to open in a new window (perhaps like https://stackoverflow.com/a/19152396/1387701), switch to that wind, parse the page, close the page and still have the original window open.

Combine multiple Selenium waits using "OR"?

My selenium code checks for a completed subroutine to be done by waiting on the site's title to change which worked perfectly. Code looks like this:
waitUntilDone = WebDriverWait(session, 15).until(EC.title_contains(somestring))
However, this can fail sometimes since the site's landing page changes after manual website visits. The server remembers where you left off. This forces me to check for an alternate condition (website title = "somestring2).
Here is what I came up with so far (also works as far as I can tell):
try:
waitUntilDone = WebDriverWait(session, 15).until(EC.title_contains(somestring)) # the old condition
except:
try:
waitUntilDone = WebDriverWait(session, 15).until(EC.title_contains(somestring2)) # the new other condition which is also valid
except:
print "oh crap" # we should never reach this point
Either one of these conditions is always true. I don't know which one thou.
Is there any way to include an "OR" inside these waits or make the try/except block look nicer?
Looks like selenium will let you do this by creating your own class. Check out the documentation here: http://selenium-python.readthedocs.io/waits.html
Here's a quick example for your case. Note the key is to have a method named __call__ in your class that defines the check you want. Selenium will call that function every 500 milliseconds until it returns True or some not null value.
class title_is_either(object):
def __init__(self, locator, string1, string2):
self.locator = locator
self.string1 = string1
self.string2 = string2
def __call__(self, driver):
element = driver.find_element(*self.locator) # Finding the referenced element
title = element.text
if self.string1 in title or self.string2 in title
return element
else:
return False
# Wait until an element with id='ID-of-title' contains text from one of your two strings
somestring = "Title 1"
somestring2 = "Title 2"
wait = WebDriverWait(driver, 10)
element = wait.until(title_is_either((By.ID, 'ID-of-title'), somestring, somestring2))

Looking and Performing action through xpath variables in Selenium - Python

I have the following page layout:
<div class="x1">
<button aria-expanded="false" class="button" data-toggle="dropdown" type="button">Actions<span class="caret"></span></button>
<ul class="dropdown-menu" role="menu">
<li>
Start
</li>
<li>
<a data-target="delete" data-toggle="modal">Delete</a>
</li>
<li>
Authenticate
</li>
</ul>
</div>
Each one of these represent a button ("Action"), which when clicked will present a dropdown menu between Start, Delete, and Authenticate.
I'm trying to automate my page test from the backend using Selenium and Python.
Here what I have so far:
def scan_page(action):
scan_url = urlparse.urljoin(self.base_url, "foo")
self.driver.get(scan_url)
elements = self.driver.find_elements_by_xpath("//div[#class='x1']")
# loop over all the elements found by xpath
for element in elements:
element.click()
ul = element.find_element_by_class_name("dropdown-menu")
if action == "scan":
if "Start" in ul.text:
# start the action
button = element.find_element_by_xpath("//button[#data-toggle='dropdown']")
button.click()
elif action == "delete":
if "Delete" in ul.text:
self.log.info("Deleting...")
elif action == "auth":
if "auth" in ul.text:
self.log.info("Authenticating...")
else:
continue
The function is given an action, which define what is to be done. If "start" then loops over all the elements and click the Start option. However, it doesn't look like it's going over all the buttons defined in the page, and only the first button is being started. So, for some reason, the Delete option is somehow selected.
What am I doing wrong?
Update:
I've added a JS Fiddle so people can play with that:
http://jsfiddle.net/58rs3kuk/1/
I found and fixed a few issues. I think this should work or at least get you on the right track.
1) You were clicking the container DIV which isn't necessary. It probably doens't hurt, it just doesn't do anything productive.
2) You were clicking BUTTON only if action = "scan". You want to click BUTTON for all actions so those lines need to move up before the IF starts. So... you will 1. click the BUTTON, 2. determine what action was requested, and 3. perform that action.
3) You were grabbing the UL to get the text but the text you want is in the LI (but is not needed after the changes I made).
4) I changed the names of some of the variables to make them more identifiable, elements is now buttonContainers and element is now buttonContainer. I would suggest you follow this practice because it will help you (and others) debug your code more easily.
5) Since we know the order of the actions in each button and they are all the same, we don't have to look for the text in the link... we can just click them by index: 0 for Start, 1 for Delete, 2 for Authenticate.
6) Added a warning in the logs if an invalid action is passed. You should probably change that to an exception or whatever is appropriate in your scripts.
7) Fixed the indent of the code you posted
def scan_page(action):
scan_url = urlparse.urljoin(self.base_url, "foo")
self.driver.get(scan_url)
buttonContainers = self.driver.find_elements_by_xpath("//div[#class='x1']")
# loop over all the buttonContainers
for buttonContainer in buttonContainers:
buttonContainer.find_element_by_tag_name("button").click()
lis = buttonContainer.find_elements_by_tag_name("li")
if action == "start":
lis[0].click() # because we know the order of the actions and they are the same in each button, we can just click on them by index.
self.log.info("Starting...")
elif action == "delete":
lis[1].click()
self.log.info("Deleting...")
elif action == "auth":
lis[2].click()
self.log.info("Authenticating...")
else:
self.log.info("WARNING: " + action + " is not a valid action.")
continue

Categories

Resources