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.PollingQuery;
6   import com.atlassian.pageobjects.elements.query.TimedQuery;
7   import com.atlassian.webdriver.AtlassianWebDriver;
8   import com.atlassian.webdriver.utils.element.ElementLocated;
9   import org.hamcrest.StringDescription;
10  import org.openqa.selenium.By;
11  import org.openqa.selenium.NoSuchElementException;
12  import org.openqa.selenium.SearchContext;
13  import org.openqa.selenium.StaleElementReferenceException;
14  import org.openqa.selenium.WebElement;
15  import org.openqa.selenium.support.ui.TimeoutException;
16  
17  import java.util.List;
18  import java.util.concurrent.TimeUnit;
19  
20  import static com.atlassian.pageobjects.elements.query.Poller.waitUntil;
21  import static org.hamcrest.Matchers.notNullValue;
22  
23  /**
24   * Creates WebDriveLocatables for different search strategies
25   */
26  public class WebDriverLocators
27  {
28      /**
29       * Creates the root of a WebDriverLocatable list, usually the instance of WebDriver
30       * @return WebDriverLocatable
31       */
32      public static WebDriverLocatable root()
33      {
34          return new WebDriverRootLocator();
35      }
36  
37      /**
38       * Creates a WebDriverLocatable for a single element in global context.
39       *
40       * @param locator The locator strategy within the parent. It will be applied in the global search context
41       * @return WebDriverLocatable
42       */
43      public static WebDriverLocatable single(By locator)
44      {
45          return new WebDriverSingleLocator(locator, root());
46      }
47  
48      /**
49       * Creates a WebDriverLocatable for a single element nested within another locatable.
50       *
51       * @param locator The locator strategy within the parent
52       * @param parent The parent locatable
53       * @return WebDriverLocatable for a single nested element
54       */
55      public static WebDriverLocatable nested(By locator, WebDriverLocatable parent)
56      {
57          return new WebDriverSingleLocator(locator, parent);
58      }
59  
60      /**
61       * Creates a WebDriverLocatable for an element included in a list initialized with given element.
62       *
63       * @param element WebElement
64       * @param locator The locator strategy within the parent that will produce a list of matches
65       * @param locatorIndex The index within the list of matches to find this element
66       * @param parent The locatable for the parent
67       * @return WebDriverLocatable
68       */
69      public static WebDriverLocatable list(WebElement element, By locator, int locatorIndex, WebDriverLocatable parent)
70      {
71          return new WebDriverListLocator(element,locator, locatorIndex, parent);
72      }
73  
74      /**
75       * Whether the given WebElement is stale (needs to be relocated)
76       * @param webElement WebElement
77       * @return True if element reference is stale, false otherwise
78       */
79      public static boolean isStale(final WebElement webElement)
80      {
81          try
82          {
83              webElement.getTagName();
84              return false;
85          }
86          catch (StaleElementReferenceException ignored)
87          {
88              return true;
89          }
90      }
91  
92      private static class WebDriverRootLocator implements WebDriverLocatable
93      {
94          public By getLocator()
95          {
96              return null;
97          }
98  
99          public WebDriverLocatable getParent()
100         {
101             return null;
102         }
103 
104         public SearchContext waitUntilLocated(AtlassianWebDriver driver, int timeoutInSeconds)
105         {
106             return driver;
107         }
108 
109         public boolean isPresent(AtlassianWebDriver driver, int timeoutForParentInSeconds)
110         {
111             return true;
112         }
113     }
114 
115     private static class WebDriverSingleLocator implements  WebDriverLocatable
116     {
117         private WebElement webElement = null;
118         private boolean webElementLocated = false;
119 
120         private final By locator;
121         private final WebDriverLocatable parent;
122 
123         public WebDriverSingleLocator(By locator, WebDriverLocatable parent)
124         {
125             this.locator = locator;
126             this.parent = parent;
127         }
128 
129         public By getLocator()
130         {
131             return locator;
132         }
133 
134         public WebDriverLocatable getParent()
135         {
136             return parent;
137         }
138 
139         public SearchContext waitUntilLocated(AtlassianWebDriver driver, int timeoutInSeconds)
140         {
141             if(!webElementLocated || WebDriverLocators.isStale(webElement))
142             {
143                 // TODO we might want to rewrite this so that there is one check including existence of parent and
144                 // TODO existence of the locator within parent - this will be more correct from the timeout point of view
145                 // see the list locatable implementation
146 
147                 SearchContext searchContext = parent.waitUntilLocated(driver, timeoutInSeconds);
148 
149                 if(!driver.elementExistsAt(locator, searchContext) && timeoutInSeconds > 0)
150                 {
151                     try
152                     {
153                         driver.waitUntil(new ElementLocated(locator, searchContext), timeoutInSeconds);
154                     }
155                     catch(TimeoutException e)
156                     {
157                         throw new org.openqa.selenium.NoSuchElementException(new StringDescription()
158                                 .appendText("Unable to locate element after timeout.")
159                                 .appendText("\nLocator: ").appendValue(locator)
160                                 .appendText("\nTimeout: ").appendValue(timeoutInSeconds).appendText(" seconds.")
161                                 .toString(), e);
162                     }
163                 }
164                 webElement = searchContext.findElement(locator);
165                 webElementLocated = true;
166             }
167 
168             return webElement;
169         }
170 
171         public boolean isPresent(AtlassianWebDriver driver, int timeoutForParentInSeconds)
172         {
173             return driver.elementExistsAt(this.locator, parent.waitUntilLocated(driver, timeoutForParentInSeconds));
174         }
175 
176     }
177 
178     private static class WebDriverListLocator implements  WebDriverLocatable
179     {
180         private WebElement webElement = null;
181 
182         private final By locator;
183         private final int locatorIndex;
184         private final WebDriverLocatable parent;
185 
186         public WebDriverListLocator(WebElement element, By locator, int locatorIndex, WebDriverLocatable parent)
187         {
188             this.webElement = element;
189             this.locatorIndex = locatorIndex;
190             this.locator = locator;
191             this.parent = parent;
192         }
193 
194         public By getLocator()
195         {
196             return null;
197         }
198 
199         public WebDriverLocatable getParent()
200         {
201             return parent;
202         }
203 
204         public SearchContext waitUntilLocated(final AtlassianWebDriver driver, int timeoutInSeconds)
205         {
206             if(WebDriverLocators.isStale(webElement))
207             {
208                 try
209                 {
210                     webElement = waitUntil(queryForElement(driver, TimeUnit.SECONDS.toMillis(timeoutInSeconds)),
211                             notNullValue(WebElement.class));
212                 }
213                 catch (AssertionError notLocated)
214                 {
215                     throw new NoSuchElementException(new StringDescription()
216                             .appendText("Unable to locate element in collection.")
217                             .appendText("\nLocator: ").appendValue(locator)
218                             .appendText("\nLocator Index: ").appendValue(locatorIndex)
219                             .toString());
220                 }
221             }
222             return webElement;
223         }
224 
225         private TimedQuery<WebElement> queryForElement(final AtlassianWebDriver driver, long timeout) {
226             return new AbstractTimedQuery<WebElement>(timeout, PollingQuery.DEFAULT_INTERVAL, ExpirationHandler.RETURN_NULL)
227             {
228                 @Override
229                 protected boolean shouldReturn(WebElement currentEval) {
230                     return true;
231                 }
232 
233                 @Override
234                 protected WebElement currentValue() {
235                     // we want the parent to be located and then the child list to be long enough to contain our index!
236                     if (parent.isPresent(driver, 0)) {
237                         List<WebElement> webElements = parent.waitUntilLocated(driver, 0).findElements(locator);
238                         return locatorIndex < webElements.size() ? webElements.get(locatorIndex) : null;
239                     }
240                     return null;
241                 }
242             };
243         }
244 
245         public boolean isPresent(AtlassianWebDriver driver, int timeoutForParentInSeconds)
246         {
247             SearchContext searchContext = parent.waitUntilLocated(driver, timeoutForParentInSeconds);
248 
249             List<WebElement> webElements = searchContext.findElements(this.locator);
250             return locatorIndex <= webElements.size() - 1;
251         }
252     }
253 }