Ok want to write a testmonkey for our web-application. This monkey should log in and randomly start entering values and clicking webelements.
So far, I have been able to log in, find the webelements (filter them) and then select one at random and Click it. I run into the StaleElementReferenceException after a random number of clicks though. All the solutions for this I was able to find suggest I wait for the element, or I find it again. Unfortunately I do not have a locator or path for the element, I only have the element itself.
My code looks like this:
public void Start()
{
List<IWebElement> elements = _seleniumAdapter.AllElementsOnPage();
Debug.WriteLine("Elementcount: " + elements.Count);
IWebElement element = elements[_random.Next(elements.Count)];
Debug.WriteLine(element.TagName + "," + element.Text);
element.Click();
if (element.GetAttribute("type").Contains("text"))
{
Debug.WriteLine("text!");
element.SendKeys(randomLetter());
}
Start();
}
Is there any way I can get around the StaleElementReference? AllElementsOnPage finds the elements By.CssSelector("*") and then filters the list before returning it, so that I only have clickable elements left.
Elements in our app do not have an id most of the time, so I can not use that. Nor have I been able to find any attribute that I can use as a unique selector.
I am stumped.
Store the index which _random.Next has generated for you in a variable. For Example i.
Then make changes in your code to catch the StaleElementReferenceException. In Catch block you can again populate your list and find the element with the same index which _random.Next has generated for you.
Try to click it again in the catch block. If it is still cannot be clicked , catch it again in the second catch block and log the attributes of this element and move on to next elements. Afterwards you can inspect that what was actually wrong with this element. See the the changed code below.
public void Start()
{
List<IWebElement> elements = _seleniumAdapter.AllElementsOnPage();
Debug.WriteLine("Elementcount: " + elements.Count);
int i = _random.Next(elements.Count);
IWebElement element = elements[i];
Debug.WriteLine(element.TagName + "," + element.Text);
try
{
element.Click();
}
catch(StaleElementReferenceException ex)
{
elements = _seleniumAdapter.AllElementsOnPage();
element = elements[i];
element.Click()
}
catch(Exception ex)
{
Debug.WriteLine(ex.Message);
element = null;
}
if(element != null)
{
if (element.GetAttribute("type").Contains("text"))
{
Debug.WriteLine("text!");
element.SendKeys(randomLetter());
}
Start();
}
}
This approach is performance intensive, but it will get things done . I hope.
Related
I am trying to populate a panel with a prefab when the player has an upgrade available however I am getting an error with my current code and in the console i am getting the following error
InvalidOperationException: Collection was modified; enumeration operation may not execute.
The following code block is causing this error
GameObject currentTable = upgradeTable[upgradeTableLevel - 1];
if(GameManager.Instance.AvailableUpgrades != null){
foreach(Upgrade upg in GameManager.Instance.AvailableUpgrades){
GameObject go = Instantiate(upgradePrefab,currentTable.transform);
go.GetComponentInChildren<Text>().text = upg.ToString() + "(level: " + GameManager.Instance.upgrades[upg] + ")";
go.GetComponentInChildren<Button>().GetComponentInChildren<Text>().text = "Buy for " + GameManager.Instance.GetNextCost(GameManager.Instance.upgrades[upg]).ToString() + " units";
GameManager.Instance.DisplayedUpgrades.Add(upg);
GameManager.Instance.AvailableUpgrades.Remove(upg);
}
}
This code is also spawning multiple of the prefab
The goal behavior was for the List "AvailableUpgrades" to be populated with upgrades when they are available which is done with this piece of code
public void UpdateAvailableUpgrades(){
foreach (KeyValuePair<Upgrade,int> cur in upgrades) {
if (units >= GetNextCost(cur.Value)) {
if (!AvailableUpgrades.Contains (cur.Key) && !DisplayedUpgrades.Contains(cur.Key)) {
AvailableUpgrades.Add (cur.Key);
}
}
}
}
Once an upgrade is available this code block is called
#region UpgradeTable
int upgradeTableLevel = GameManager.Instance.FetchUpgradeLevel(Upgrade.UpgradeTable);
upgradeTable[upgradeTableLevel - 1].SetActive(true);
foreach(GameObject go in upgradeTable){
if(go != upgradeTable[upgradeTableLevel - 1]){
go.SetActive(false);
}
}
//TODO
//Add a way to find the currently active table and add new upgrades prefab there
//TEMP SOLUTION
GameObject currentTable = upgradeTable[upgradeTableLevel - 1];
if(GameManager.Instance.AvailableUpgrades != null){
foreach(Upgrade upg in GameManager.Instance.AvailableUpgrades){
GameObject go = Instantiate(upgradePrefab,currentTable.transform);
go.GetComponentInChildren<Text>().text = upg.ToString() + "(level: " + GameManager.Instance.upgrades[upg] + ")";
go.GetComponentInChildren<Button>().GetComponentInChildren<Text>().text = "Buy for " + GameManager.Instance.GetNextCost(GameManager.Instance.upgrades[upg]).ToString() + " units";
GameManager.Instance.DisplayedUpgrades.Add(upg);
GameManager.Instance.AvailableUpgrades.Remove(upg);
}
}
#endregion
Whats happening here is im getting the table level(needed for grabbing the currently active table and other unrelated stuff) once we get down to //temp solution I am grabbing the currently active table. Then checking to see if there are any upgrades in the AvailableUgrades if so I loop through the list to instantiate a new upgrade prefab for each upgrade in the list at the end of this list the upgrade is removed from AvailableUpgrades and added to DisplayedUpgrades I added 2 list for handling this to solve the issue of it constantly spawning new prefabs but it is still spawning them
So how do I fix my script to provide the expected behavior?
The error explains clearly, the collection you are enumerating here;
foreach(Upgrade upg in GameManager.Instance.AvailableUpgrades){
...
Is then modified here, which is still inside the loop (enumeration);
GameManager.Instance.AvailableUpgrades.Remove(upg);
}
You simply need to instead maintain a list of upgrades to remove, and then remove them after the loop is finished.
So make a new list of upgrades, change the .Remove to add it to this new list instead [upgradesToRemove.Add(upg)], and then after the loop is finished you can remove all the upgrades in the new list from the GameManager.Instance.AvailableUpgrades, since it is no longer being enumerated when you are outside the loop. Ez.
As mentioned by Milney, foreach loops do not allow you to alter the enumeration being iterated for safety reasons. It is usually a good practice to avoid this sort of operation anyway since it can easily lead to nasty bugs. If you wish to do it anyway (as I have done it myself a few times), you can do it with for loops instead of foreach:
for (int i = 0; i < GameManager.Instance.AvailableUpgrades.Count; i++) {
// Do you thing
if (/*some possible condition*/) {
GameManager.Instance.DisplayedUpgrades.Add(/*some new Object*/);
}
if (/*another possible condition*/) {
GameManager.Instance.AvailableUpgrades.RemoveAt(i);
i--; // Since i is pointing to current object that was just removed
// Otherwise you'd skip one object
}
}
I repeat. This kind of code can easily lead to hard-to-track bugs and is also hard to understand for newcomers in your project so recommend you try to avoid messing with the enumeration during the loop when possible.
Edit: Or... since I noticed your code does not check for conditions to remove each object you could simply call GameManager.Instance.AvailableUpgrades.Clear() after the loop to remove all objects.
I have to click a certain button on a page. However, when I retrieve all of the elements that have a particular class name. All of the retrieved elements throw a stale reference exception when I try to perform each one or click. I can not double click on any of them. It find's the right elements but throws the exception for all of them. The commented out code is where I actually am trying to select and click the appropriate button. I attached a picture of the form. Note that the pages are changed each time a button is clicked or performed. The Select Upload BOM button is what you need to pay particular attention to.
Website
// Switch to correct frame
IWebElement editorFrame = driver.FindElement(By.ClassName("frame-banner"));
driver.SwitchTo().Frame(editorFrame);
var action = new OpenQA.Selenium.Interactions.Actions(driver);
// Select Project File
IList<IWebElement> projectFileButtonList= driver.FindElements(By.ClassName("data-cell"));
foreach (var button in projectFileButtonList)
{
if (button.Text == "BOM_scrub")
{
// Found Project File now select it
action.DoubleClick(button);
action.Perform();
break;
}
}
// Select Upload BOM Button
IList<IWebElement> uploadBomBtn = driver.FindElements(By.ClassName("se-custom-main-button"));
foreach (var element in uploadBomBtn )
{
try
{
action.DoubleClick(element);
action.Perform();
}
catch
{
}
/*
if (element.Text == "Upload BOM")
{
int i = 0;
while (i == 0)
{
try
{
action.DoubleClick(element);
action.Perform();
break;
}
catch
{
}
}
}
*/
}
Don't use driver.findElement(-s) with dynamic components.
StaleElementReferenceException occurs, as you're trying to perform an action against element, which has already been detached from DOM.
You have to use explicit waits mechanism (a combination of WebDriverWait + ExpectedConditions), which automatically refreshes element's state, and returns its valid representation, when specified condition is met.
I have to develop a unit test which fails if element is present and passes the test if element is not present.
To be detailed, I have a simple form like name, email, address etc. When I click on the save button, error message is displayed if required fields are empty. If error message is displayed then I have to fail the test and if not displayed then pass the test. Is there any solution?
try
{
//Click on save button
IWebElement save_profile = driver.FindElement(By.XPath("//div[#class='form-group buttons']/div/input"));
save_profile.Click();
//Locate Error Message below the text box
IWebElement FirstNameError = driver.FindElement(By.XPath("//form[#class='default form-horizontal']/fieldset/div[4]/div[2]/span/div"));
//I want to fail the test here if above element is found
}
catch
{
//pass the test if element is not found in try statement
}
I think I am going in the wrong direction but couldn't find the solution. Please advice. Thansk in advance.
Following is the behavior of "findElement"
If the element you are looking exists, it returns the WebElement.
If the element does not exist, it throws Exception and if not handled properly leads troubles.
So use "findElements"(here observe 's' at the end)
Following is the behavior of "findElements"
It returns a list,
If the element exists, the size obviously more than 0,
If the element does not exist the size will be 0. So you can just put a if condition with size
Here is pseudo code in Java
if (driver.findElements(By.XPath("//div[#class='form-group buttons']/div/input")).size()>0)
//print element exists
else
//print element does not exists.
I hope the above helps.
You have two options to do this.
Firstly, by using Assert.assertTrue(boolean value);
try
{
//Click on save button
IWebElement save_profile = driver.FindElement(By.XPath("//div[#class='form-group buttons']/div/input"));
save_profile.Click();
//Locate Error Message below the text box
IWebElement FirstNameError = driver.FindElement(By.XPath("//form[#class='default form-horizontal']/fieldset/div[4]/div[2]/span/div"));
//I want to fail the test here if above element is found
Assert.assertTrue(false);
}
catch
{
//pass the test if element is not found in try statement
Assert.assertTrue(true);
}
Secondly, by using boolean return type if used inside a method.:
boolean isValidated = false;
try
{
//Click on save button
IWebElement save_profile = driver.FindElement(By.XPath("//div[#class='form-group buttons']/div/input"));
save_profile.Click();
//Locate Error Message below the text box
IWebElement FirstNameError = driver.FindElement(By.XPath("//form[#class='default form-horizontal']/fieldset/div[4]/div[2]/span/div"));
//I want to fail the test here if above element is found
isValidated =false;
return isValidated;
}
catch(Exception e)
{
//pass the test if element is not found in try statement
isValidated = true;
return isValidated;
}
If the method returns true, pass the test and vice versa
I am just finishing a test script and accessing a fairly dynamic page. The page in question, has an element appear (generally a radio button or tick-box), which is only present if certain criteria in previous pages are met. So, my test will be accessing this page irrelevant of previous criteria and I want to hit the "continue" element at the bottom of the page whilst handling these elements "IF" they appear. I have a few method sto click elements by ID, and so far have the following code:
// Selects the "Confirm" button
IWebElement radioOption = mWebDriver.FindElement(By.Id("Radio_Button_Id"));
if (radioOption.Displayed)
{
this.ClickElementById("Radio_Button_Id");
// Clicks CONTINUE
this.ClickElementById("CONTINUE");
}
else
{
// Selects CONTINUE
this.ClickElementById("CONTINUE");
}
I am trying in this code to handle that if the radio button appears, select it then select the continue button. Also, if the radio button does not appear, ignore it and select the continue button. Any help with this would be much appreciated.
Try something like this:
//Displayed
public static bool IsElementDisplayed(this IWebDriver driver, By element)
{
IReadOnlyCollection<IWebElement> elements = driver.FindElements(element);
if (elements.Count > 0)
{
return elements.ElementAt(0).Displayed;
}
return false;
}
//Enabled
public static bool IsElementEnabled(this IWebDriver driver, By element)
{
IReadOnlyCollection<IWebElement> elements = driver.FindElements(element);
if (elements.Count > 0)
{
return elements.ElementAt(0).Enabled;
}
return false;
}
You'll not get any exception and then the test can continue.
You said that you were getting NoSuchElementExceptions. radioOption.Displayed tests to see if the element is visible on the page, but it will throw an error if the element doesn't even exist. (An element can be present, but invisible)
To test to see if an element is present, you need to do mWebDriver.FindElements (note the S). This will return a List<WebElement> of all of the elements that match your selector, and if it can't find any, it will return a list of size 0 (and not throw an error).
That way, your if statement will be if (radioOptions.size()!=0), and will check to see if the element exists (not if its visible).
I've also used this as a way to test if the element is present and get a handle on the element if it is present:
namespace SeleniumExtensions
{
public static class WebDriverExtensions
{
public static bool TryFindElement(this IWebDriver driver, By by, out IWebElement element)
{
try
{
element = driver.FindElement(by);
return true;
}
catch (NoSuchElementException)
{
element = null;
return false;
}
}
public static bool IsElementEnabled(this IWebDriver driver, By by)
{
IWebElement element = null;
if (driver.TryFindElement(by, out element))
{
return element.Enabled;
}
return false;
}
}
}
This allows for code like:
using SeleniumExtensions;
// ...
IWebElement element = null;
if (driver.TryFindElement(By.Id("item-01"), out element)
{
// use element
}
else
{
// element is null
}
Or:
if (driver.IsElementEnabled(By.Id("item-01"))
{
// item is enabled
}
I need some help because I keep getting a StaleElementReference when I try to parse a list of a tags to click.
What I have done is on page land I iterate through the page and generate an object List<> with with all the a tags
private List<IWebElement> _pageLinks;
public List<IWebElement> pageLinks
{
get
{
if (_pageLinks == null)
{
_pageLinks = InfoDriver.FindElements(By.TagName("a")).ToList();
}
return _pageLinks;
}
}
Then I want to parse this list, and click each one and then go back to the page it was referenced from.
private static SeleniumInformation si = new SeleniumInformation(ffDriver);
si.pageLinks.ForEach(i =>
{
i.Click();
System.Threading.Thread.Sleep(1000);
ffDriver.Navigate().Back();
});
What happens is that after the first click it goes to the new page and then goes back to the starting page but it can't get the next link. I've tried setting it to a static element, setting a backing field so that it checks to see if there is data there already however it appears that on click the IwebElement looses the list and it doesn't rebuild the list either so I get a StaleElementReference exception not handled and element not found in cache.
Is this a bug in Selenium with the IWebElement class or am I doing something wrong? Any help would be greatly appreciated.
This is the expected behavior. You left the page the element was on. When you navigated back, it is a new page and that element is no longer on it.
To work around this I would suggest passing around Bys instead, if you can. Assuming your anchorlinks all have unique hrefs, you could instead generate a list as follows (java code, but should translate to c#):
private static List<By> getLinks(WebDriver driver)
{
List<By> anchorLinkBys = new ArrayList<By>();
List<WebElement> elements = driver.findElements(By.tagName("a"));
for(WebElement e : elements)
{
anchorLinkBys.add(By.cssSelector("a[href=\"" + e.getAttribute("href") + "\"]"));
//could also use another attribute such as id.
}
return anchorLinkBys;
}
I don't know the makeup of your page so I don't know if it is possible to generate By's dynamically that uniquely identify the elements you want. For example if all the elements have the same parent, you could use the css level 3 selector nth-child(n). Hopefully you get some ideas from the above code.
private void YourTest()
{
IWebDriver browserDriver = new FirefoxDriver();
browserDriver.Navigate().GoToUrl(pageUrl);
int linkCount= browserDriver.FindElements(By.TagName("a")).Count;
for (int i = 0; i <= linkCount-1; i++ )
{
List<IWebElement> linksToClick = browserDriver.FindElements(By.TagName("a")).ToList();
linksToClick[i].Click();
System.Threading.Thread.Sleep(4000);
if(some boolean check)
{
//Do something here for validation
}
browserDriver.Navigate().Back();
}
broswerDriver.Quit();
}