Reputation: 1054
I have a relatively simple method to wait until an element exists and is displayed. The method handles the situation where more than a single element is returned for the given By (usually we only expect one of them to be displayed but in any case the method will return the first displayed element found).
The issue I'm having is that when there is no matching element on the page (at all), it is taking magnitudes of time more* than the TimeSpan specified, and I can't figure why.
*I just tested with a 30s timeout and it took a little over 5m
code:
/// <summary>
/// Returns the (first) element that is displayed when multiple elements are found on page for the same by
/// </summary>
public static IWebElement FindDisplayedElement(By by, int secondsToWait = 30)
{
WebDriver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(secondsToWait);
// Wait for an element to exist and also displayed
IWebElement element = null;
bool success = SpinWait.SpinUntil(() =>
{
var collection = WebDriver.FindElements(by);
if (collection.Count <= 0)
return false;
element = collection.ToList().FirstOrDefault(x => x.Displayed == true);
return element != null;
}
, TimeSpan.FromSeconds(secondsToWait));
if (success)
return element;
// if element still not found
throw new NoSuchElementException("Could not find visible element with by: " + by.ToString());
}
You would call it with something like this:
[Test]
public void FindDisplayedElement()
{
webDriver.Navigate().GoToUrl("https://stackoverflow.com/questions");
var nonExistenetElementBy = By.CssSelector("#custom-header99");
FindDisplayedElement(nonExistenetElementBy , 10);
}
If you run the test (with 10s timeout) you will find it takes about 100 seconds to actually exit.
It looks like it might have something to do with the mix of the inherit wait built into WebDriver.FindElements() wrapped inside a SpinWait.WaitUntil().
Would like to hear what you guys think about this conundrum.
Cheers!
Upvotes: 3
Views: 1360
Reputation: 1054
Doing some further testing I found out that reducing the WebDriver Implicit Wait Timeout to a low number (e.g. 100ms) fixes the issue. This corresponds to the explanation @Evk provided to why using SpinUntil doesn't work.
I've changed the function to use WebDriverWait instead (as shown in this answer to a different question) and it now works correctly. This removed the need to use the implicit wait timeout at all.
/// <summary>
/// Returns the (first) element that is displayed when multiple elements are found on page for the same by
/// </summary>
/// <exception cref="NoSuchElementException">Thrown when either an element is not found or none of the found elements is displayed</exception>
public static IWebElement FindDisplayedElement(By by, int secondsToWait = DEFAULT_WAIT)
{
var wait = new WebDriverWait(WebDriver, TimeSpan.FromSeconds(secondsToWait));
try
{
return wait.Until(condition =>
{
return WebDriver.FindElements(by).ToList().FirstOrDefault(x => x.Displayed == true);
});
}
catch (WebDriverTimeoutException ex)
{
throw new NoSuchElementException("Could not find visible element with by: " + by.ToString(), ex);
}
}
Upvotes: 0
Reputation: 101453
That's because SpinWait.WaitUntil
is implemented rougly as follows:
public static bool SpinUntil(Func<bool> condition, TimeSpan timeout) {
int millisecondsTimeout = (int) timeout.TotalMilliseconds;
long num = 0;
if (millisecondsTimeout != 0 && millisecondsTimeout != -1)
num = Environment.TickCount;
SpinWait spinWait = new SpinWait();
while (!condition())
{
if (millisecondsTimeout == 0)
return false;
spinWait.SpinOnce();
// HERE
if (millisecondsTimeout != -1 && spinWait.NextSpinWillYield && millisecondsTimeout <= (Environment.TickCount - num))
return false;
}
return true;
}
Note condition under "HERE" comment above. It only checks whether timeout has expired IF spinWait.NextSpinWillYield
returns true. What that means is: if next spin will result in context switch and timeout is expired - give up and return. But otherwise - keep spinning without even checking a timeout.
NextSpinWillYield
result depends on number of previous spins. Basically this construct spins X amount of times (10 I believe), then starts to yield (give up current thread time slice to other threads).
In your case, condition inside SpinUntil
take VERY long time to evaluate, which is completely against design of SpinWait - it expects condition evaluation take no time at all (and where SpinWait is actually applicable - it's true). Say one evaluation of condition takes 5 seconds in your case. Then, even if timeout is 1 second - it will spin 10 times first (50 seconds total) before even checking the timeout. That's because SpinWait is not designed for the thing you are trying to use it for. From documentation:
System.Threading.SpinWait is a lightweight synchronization type that you can use in low-level scenarios to avoid the expensive context switches and kernel transitions that are required for kernel events. On multicore computers, when a resource is not expected to be held for long periods of time, it can be more efficient for a waiting thread to spin in user mode for a few dozen or a few hundred cycles, and then retry to acquire the resource. If the resource is available after spinning, then you have saved several thousand cycles. If the resource is still not available, then you have spent only a few cycles and can still enter a kernel-based wait. This spinning-then-waiting combination is sometimes referred to as a two-phase wait operation.
None of which is applicable to your situation, in my opinion. Another part of documentation states "SpinWait is not generally useful for ordinary applications".
In this case, with such a long condition evaluation time - you can just run it in a loop without additional waiting or spinning, and manually check if timeout has expired on each iteration.
Upvotes: 2