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