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