View Javadoc

1   package com.atlassian.pageobjects.elements;
2   
3   import com.atlassian.pageobjects.elements.query.AbstractTimedQuery;
4   import com.atlassian.pageobjects.elements.query.ExpirationHandler;
5   import com.atlassian.pageobjects.elements.query.Poller;
6   import com.atlassian.pageobjects.elements.query.TimedQuery;
7   import com.atlassian.pageobjects.elements.timeout.DefaultTimeouts;
8   import com.atlassian.webdriver.utils.Check;
9   import org.hamcrest.StringDescription;
10  import org.openqa.selenium.*;
11  
12  import javax.annotation.Nonnull;
13  import java.util.List;
14  import java.util.concurrent.TimeUnit;
15  
16  import static com.atlassian.pageobjects.elements.query.Poller.*;
17  import static com.atlassian.pageobjects.elements.util.StringConcat.asString;
18  import static com.google.common.base.Preconditions.checkArgument;
19  import static com.google.common.base.Preconditions.checkNotNull;
20  import static org.hamcrest.Matchers.notNullValue;
21  
22  /**
23   * Creates WebDriveLocatables for different search strategies.
24   *
25   */
26  public class WebDriverLocators
27  {
28      /**
29       * Creates the root of a WebDriverLocatable list, usually the instance of WebDriver.
30       *
31       * @return WebDriverLocatable
32       */
33      public static WebDriverLocatable root()
34      {
35          return new WebDriverRootLocator();
36      }
37  
38      /**
39       * Creates a WebDriverLocatable for a single element in global context.
40       *
41       * @param locator The locator strategy within the parent. It will be applied in the global search context
42       * @return WebDriverLocatable
43       */
44      public static WebDriverLocatable single(By locator)
45      {
46          return new WebDriverSingleLocator(locator, root());
47      }
48  
49      /**
50       * Creates a WebDriverLocatable for a single element nested within another locatable.
51       *
52       * @param locator The locator strategy within the parent
53       * @param parent The parent locatable
54       * @return WebDriverLocatable for a single nested element
55       */
56      public static WebDriverLocatable nested(By locator, WebDriverLocatable parent)
57      {
58          return new WebDriverSingleLocator(locator, parent);
59      }
60  
61      /**
62       * Creates a WebDriverLocatable for an element included in a list initialized with given element.
63       *
64       * @param element WebElement
65       * @param locator The locator strategy within the parent that will produce a list of matches
66       * @param locatorIndex The index within the list of matches to find this element
67       * @param parent The locatable for the parent
68       * @return WebDriverLocatable
69       */
70      public static WebDriverLocatable list(WebElement element, By locator, int locatorIndex, WebDriverLocatable parent)
71      {
72          return new WebDriverListLocator(element,locator, locatorIndex, parent);
73      }
74  
75      public static WebDriverLocatable staticElement(WebElement element)
76      {
77          return new WebDriverStaticLocator(element);
78      }
79  
80      /**
81       * Whether the given WebElement is stale (needs to be relocated)
82       * @param webElement WebElement
83       * @return True if element reference is stale, false otherwise
84       */
85      public static boolean isStale(final WebElement webElement)
86      {
87          try
88          {
89              webElement.getTagName();
90              return false;
91          }
92          catch (StaleElementReferenceException ignored)
93          {
94              return true;
95          }
96      }
97  
98      private static Poller.WaitTimeout withinTimeout(WebDriverLocatable.LocateTimeout timeout)
99      {
100         return timeout.timeout() > 0 ? by(timeout.timeout()) : now();
101     }
102 
103     private static long getTimeout(WebDriverLocatable.LocateTimeout timeout) {
104         return timeout.timeout() > 0 ? timeout.timeout() : DefaultTimeouts.DEFAULT;
105     }
106 
107     private static abstract class DeprecatedApiBridge implements WebDriverLocatable
108     {
109         @Override
110         @Nonnull
111         public final SearchContext waitUntilLocated(@Nonnull WebDriver driver, int timeoutInSeconds)
112                 throws NoSuchElementException
113         {
114             return waitUntilLocated(driver, new LocateTimeout.Builder()
115                     .timeout(timeoutInSeconds, TimeUnit.SECONDS)
116                     .build());
117         }
118 
119         @Override
120         public final boolean isPresent(@Nonnull WebDriver driver, int timeoutInSeconds)
121         {
122             return isPresent(driver, new LocateTimeout.Builder()
123                     .timeout(timeoutInSeconds, TimeUnit.SECONDS)
124                     .build());
125         }
126     }
127 
128 
129     /**
130      * A static locator that will blow if the associated WebElement is stale. Not recommended unless no other option.
131      *
132      */
133     private static class WebDriverStaticLocator extends DeprecatedApiBridge implements WebDriverLocatable
134     {
135         private final WebElement element;
136 
137         public WebDriverStaticLocator(WebElement element)
138         {
139             this.element = element;
140         }
141 
142         @Override
143         public By getLocator()
144         {
145             if (isStale(element))
146             {
147                 return null;
148             }
149             // happy reverse engineering from ID
150             final String id = element.getAttribute("id");
151             if (id != null)
152             {
153                 return By.id(id);
154             }
155             // can't get :(
156             return null;
157         }
158 
159         @Override
160         public WebDriverLocatable getParent()
161         {
162             return root();
163         }
164 
165         @Override
166         @Nonnull
167         public SearchContext waitUntilLocated(@Nonnull WebDriver driver, @Nonnull LocateTimeout timeout)
168                 throws NoSuchElementException
169         {
170             checkNotNull(driver, "driver");
171             checkNotNull(timeout, "timeout");
172             if (isStale(element))
173             {
174                 throw new NoSuchElementException("WebElement got stale");
175             }
176             return element;
177         }
178 
179         @Override
180         public boolean isPresent(@Nonnull WebDriver driver, @Nonnull LocateTimeout timeout)
181         {
182             checkNotNull(driver, "driver");
183             checkNotNull(timeout, "timeout");
184             return !isStale(element);
185         }
186     }
187 
188     private static class WebDriverRootLocator extends DeprecatedApiBridge implements WebDriverLocatable
189     {
190         public By getLocator()
191         {
192             return null;
193         }
194 
195         public WebDriverLocatable getParent()
196         {
197             return null;
198         }
199 
200         @Override
201         @Nonnull
202         public SearchContext waitUntilLocated(@Nonnull WebDriver driver, @Nonnull LocateTimeout timeout)
203         {
204             checkNotNull(driver, "driver");
205             checkNotNull(timeout, "timeout");
206             return driver;
207         }
208 
209         @Override
210         public boolean isPresent(@Nonnull WebDriver driver, @Nonnull LocateTimeout timeout)
211         {
212             checkNotNull(driver, "driver");
213             checkNotNull(timeout, "timeout");
214             return true;
215         }
216     }
217 
218     private static class WebDriverSingleLocator extends DeprecatedApiBridge implements  WebDriverLocatable
219     {
220         private WebElement webElement = null;
221         private boolean webElementLocated = false;
222 
223         private final By locator;
224         private final WebDriverLocatable parent;
225 
226         public WebDriverSingleLocator(By locator, WebDriverLocatable parent)
227         {
228             this.locator = checkNotNull(locator, "locator");
229             this.parent = checkNotNull(parent, "parent");
230         }
231 
232         public By getLocator()
233         {
234             return locator;
235         }
236 
237         public WebDriverLocatable getParent()
238         {
239             return parent;
240         }
241 
242         @Override
243         @Nonnull
244         public SearchContext waitUntilLocated(@Nonnull WebDriver driver, @Nonnull LocateTimeout timeout)
245         {
246             checkNotNull(driver, "driver");
247             checkNotNull(timeout, "timeout");
248             if(!webElementLocated || WebDriverLocators.isStale(webElement))
249             {
250                 try
251                 {
252                     webElement = waitUntil(queryForSingleElement(driver, timeout), notNullValue(WebElement.class),
253                             withinTimeout(timeout));
254                 }
255                 catch(AssertionError notFound)
256                 {
257                     throw new NoSuchElementException(new StringDescription()
258                             .appendText("Unable to locate element by timeout.")
259                             .appendText("\nLocator: ").appendValue(locator)
260                             .appendText("\nTimeout: ").appendValue(timeout.timeout()).appendText("ms.")
261                             .toString());
262                 }
263                 webElementLocated = true;
264             }
265             return webElement;
266         }
267 
268         @Override
269         public boolean isPresent(@Nonnull WebDriver driver, @Nonnull LocateTimeout timeout)
270         {
271             checkNotNull(driver, "driver");
272             checkNotNull(timeout, "timeout");
273             try
274             {
275                 return Check.elementExists(this.locator, parent.waitUntilLocated(driver, timeout));
276             }
277             catch (NoSuchElementException e)
278             {
279                 // parent cannot be located
280                 return false;
281             }
282         }
283 
284         @Override
285         public String toString()
286         {
287             return asString("WebDriverSingleLocator[locator=", locator, "]");
288         }
289 
290         private TimedQuery<WebElement> queryForSingleElement(final WebDriver driver, final LocateTimeout timeout)
291         {
292             return new AbstractTimedQuery<WebElement>(getTimeout(timeout), timeout.pollInterval(), ExpirationHandler.RETURN_NULL)
293             {
294                 @Override
295                 protected boolean shouldReturn(WebElement currentEval)
296                 {
297                     return true;
298                 }
299 
300                 @Override
301                 protected WebElement currentValue() {
302                     // we want the parent to be located and find the element within it
303                     if (parent.isPresent(driver, LocateTimeout.zero(timeout.pollInterval())))
304                     {
305                         try
306                         {
307                             return parent.waitUntilLocated(driver, LocateTimeout.zero(timeout.pollInterval()))
308                                     .findElement(locator);
309                         }
310                         catch (NoSuchElementException e)
311                         {
312                             // element not found within parent
313                             return null;
314                         }
315                     }
316                     else
317                     {
318                         // parent not found
319                         return null;
320                     }
321                 }
322             };
323         }
324 
325     }
326 
327     private static class WebDriverListLocator extends DeprecatedApiBridge implements  WebDriverLocatable
328     {
329         private WebElement webElement = null;
330 
331         private final By locator;
332         private final int locatorIndex;
333         private final WebDriverLocatable parent;
334 
335         public WebDriverListLocator(WebElement element, By locator, int locatorIndex, WebDriverLocatable parent)
336         {
337             checkArgument(locatorIndex >= 0, "locator index is negative:" + locatorIndex);
338             this.webElement = checkNotNull(element);
339             this.locatorIndex = locatorIndex;
340             this.locator = checkNotNull(locator, "locator");
341             this.parent = checkNotNull(parent, "parent");
342         }
343 
344         public By getLocator()
345         {
346             return null;
347         }
348 
349         public WebDriverLocatable getParent()
350         {
351             return parent;
352         }
353 
354         @Override
355         @Nonnull
356         public SearchContext waitUntilLocated(@Nonnull WebDriver driver, @Nonnull LocateTimeout timeout)
357         {
358             checkNotNull(driver, "driver");
359             checkNotNull(timeout, "timeout");
360             if(WebDriverLocators.isStale(webElement))
361             {
362                 try
363                 {
364                     webElement = waitUntil(queryForElementInList(driver, timeout), notNullValue(WebElement.class),
365                             withinTimeout(timeout));
366                 }
367                 catch (AssertionError notLocated)
368                 {
369                     throw new NoSuchElementException(new StringDescription()
370                             .appendText("Unable to locate element in collection.")
371                             .appendText("\nLocator: ").appendValue(locator)
372                             .appendText("\nLocator Index: ").appendValue(locatorIndex)
373                             .toString());
374                 }
375             }
376             return webElement;
377         }
378 
379         @Override
380         public boolean isPresent(@Nonnull WebDriver driver, @Nonnull LocateTimeout timeout)
381         {
382             checkNotNull(driver, "driver");
383             checkNotNull(timeout, "timeout");
384             SearchContext searchContext = parent.waitUntilLocated(driver, timeout);
385             List<WebElement> webElements = searchContext.findElements(this.locator);
386             return locatorIndex <= webElements.size() - 1;
387         }
388 
389         @Override
390         public String toString()
391         {
392             return asString("WebDriverListLocator[locator=", locator, ",index=", locatorIndex, "]");
393         }
394 
395         private TimedQuery<WebElement> queryForElementInList(final WebDriver driver, LocateTimeout timeout)
396         {
397             return new AbstractTimedQuery<WebElement>(getTimeout(timeout), timeout.pollInterval(), ExpirationHandler.RETURN_NULL)
398             {
399                 @Override
400                 protected boolean shouldReturn(WebElement currentEval) {
401                     return true;
402                 }
403 
404                 @Override
405                 protected WebElement currentValue() {
406                     // we want the parent to be located and then the child list to be long enough to contain our index!
407                     if (parent.isPresent(driver, 0)) {
408                         List<WebElement> webElements = parent.waitUntilLocated(driver, 0).findElements(locator);
409                         return locatorIndex < webElements.size() ? webElements.get(locatorIndex) : null;
410                     }
411                     return null;
412                 }
413             };
414         }
415     }
416 }