View Javadoc

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