View Javadoc

1   package com.atlassian.webdriver.utils.by;
2   
3   import com.atlassian.webdriver.utils.JavaScriptUtils;
4   import com.google.common.base.Preconditions;
5   import org.apache.commons.lang.StringUtils;
6   import org.apache.commons.lang.Validate;
7   import org.openqa.selenium.By;
8   import org.openqa.selenium.NoSuchElementException;
9   import org.openqa.selenium.SearchContext;
10  import org.openqa.selenium.WebDriver;
11  import org.openqa.selenium.WebElement;
12  
13  import java.util.ArrayList;
14  import java.util.List;
15  import java.util.regex.Matcher;
16  import java.util.regex.Pattern;
17  
18  import static com.atlassian.webdriver.LifecycleAwareWebDriverGrid.getDriver;
19  
20  /**
21   * Provides an extension to By so that jQuery selectors can be used. By calling the ByJquery.$
22   * method will ensure that jQuery get's loaded into the page. It is namespaced away within the
23   * javascript so that it doens't override another version of jQuery on the page. This allows the
24   * ByJquery locator to be dependent on it's own version of jQuery.
25   * <p/>
26   * same usages of ByJquery include: <code> ByJquery.$("div.className li");
27   * ByJQuery.$("('div.className li a').parent('div')"); </code> It accepts simple searches like the
28   * first example that don't need to be wrapped in brackets, but if you want to call another jQuery
29   * method like <em>.parent</em> the first selector needs to be wrapped in brakcets.
30   *
31   * @since 2.0
32   */
33  public abstract class ByJquery extends By
34  {
35  
36      // TODO: fix this so that can extract the simple selection out and run that and continue chaining
37      // eg. $("#someid .class") -> By.id("someid"), By.className(class)
38      private final Pattern SIMPLE_SELECTOR_PATTERN = Pattern.compile("^([#]|[.]|[a-zA-Z])[\\w-]+(\\s[^~]*)?$");
39      private final Pattern ID_SELECTOR = Pattern.compile("^#(\\S+)(\\s?.*)$");
40      private final Pattern CLASSNAME_SELECTOR = Pattern.compile("^[.](\\S+)(\\s?.*)$");
41      private final Pattern TAGNAME_SELECTOR = Pattern.compile("^([A-Za-z]\\w+)(\\s?.*)$");
42  
43      private enum SelectorType {
44          FIND,
45          PARENT,
46          PARENTS,
47          SIBLINGS,
48          CHILDREN,
49          PREV,
50          CLOSEST,
51          FILTER;
52      }
53  
54      private class Selector {
55  
56          private final String selector;
57          private final SelectorType type;
58  
59          public Selector(String selector, SelectorType type) {
60              this.selector = selector;
61              this.type = type;
62          }
63  
64          public String getSelector()
65          {
66              return selector;
67          }
68  
69          public SelectorType getType()
70          {
71              return type;
72          }
73      }
74  
75      private final List<Selector> selectors;
76  
77      private ByJquery(String selector, SelectorType type)
78      {
79          this.selectors = new ArrayList<Selector>();
80          this.selectors.add(new Selector(selector, type));
81      }
82  
83      private ByJquery() {
84          this.selectors = new ArrayList<Selector>();
85      }
86  
87      private ByJquery(String selector, SelectorType type, List<Selector> selectors)
88      {
89          this.selectors = selectors;
90          this.selectors.add(new Selector(selector, type));
91      }
92  
93      List<WebElement> findElementsWithSelectors(SearchContext context)
94      {
95          List<WebElement> elements = new ArrayList<WebElement>();
96  
97          for (Selector selector : selectors)
98          {
99  
100             String selectorStr = fixSelector(selector.getSelector());
101 
102             Object[] args = null;
103 
104             switch (selector.type)
105             {
106                 case FIND:
107                     if (elements.isEmpty())
108                     {
109                         elements = $findElements(selector.getSelector(), context);
110                     }
111                     else {
112                         elements = $findElements(selector.getSelector(), elements);
113                     }
114                     break;
115                 case PARENT:
116                 case SIBLINGS:
117                 case PARENTS:
118                 case CHILDREN:
119                 case PREV:
120                 case CLOSEST:
121                     args = new Object[]{"ATLWD.$(context)." + selector.type.name().toLowerCase() + "(" + selectorStr + ")", elements };
122                     elements = JavaScriptUtils.execute("return ATLWD.byJquery.execute(arguments[0],arguments[1])", getDriver(), args);
123                     break;
124                 case FILTER:
125                     args = new Object[]{"ATLWD.$(context)." + selector.type.name().toLowerCase() + "(" + selector.getSelector() + ")", elements };
126                     elements = JavaScriptUtils.execute("return ATLWD.byJquery.execute(arguments[0],arguments[1])", getDriver(), args);
127                     break;
128 
129                 default:
130                     throw new UnsupportedOperationException("Unknown selector type: " + selector.type);
131             }
132 
133         }
134 
135         return elements;
136     }
137 
138     private String fixSelector(String selector)
139     {
140         if (selector == null)
141         {
142             return "";
143         }
144         else
145         {
146             return "'" + selector.replaceAll("'", "\"") + "'";
147         }
148 
149         //return selector == null ? "" : "'" + selector + "'";
150     }
151 
152     private boolean isSimpleSelector(String selector)
153     {
154         return SIMPLE_SELECTOR_PATTERN.matcher(selector).matches();
155     }
156 
157     private List<WebElement> executeMatcher(Matcher matcher, SearchContext context, By by)
158     {
159         if (matcher.matches())
160             {
161                 if (StringUtils.isEmpty(matcher.group(2)))
162                 {
163                     return context.findElements(by);
164                 }
165                 else
166                 {
167                     return $findElements(matcher.group(2), context.findElements(by));
168                 }
169             }
170 
171             throw new IllegalStateException("ID Selector failed match");
172     }
173 
174     private List<WebElement> executeIdMatcher(Matcher matcher, SearchContext context)
175     {
176         if (matcher.find()) {
177             return executeMatcher(matcher, context, By.id(matcher.group(1)));
178         }
179 
180         throw new IllegalArgumentException("Invalid matcher.");
181     }
182 
183     private List<WebElement> executeClassNameMatcher(Matcher matcher, SearchContext context)
184     {
185         if (matcher.find())
186         {
187             return executeMatcher(matcher, context, By.className(matcher.group(1)));
188         }
189 
190         throw new IllegalArgumentException("Invalid matcher.");
191     }
192 
193     private List<WebElement> executeTagNameMatcher(Matcher matcher, SearchContext context)
194     {
195         if (matcher.find())
196         {
197             return executeMatcher(matcher, context, By.tagName(matcher.group(1)));
198         }
199 
200         throw new IllegalArgumentException("Invalid matcher.");
201     }
202 
203     private List<WebElement> executeSimpleSelector(String selector, SearchContext context)
204     {
205         if (selector.startsWith("#"))
206         {
207             Matcher matcher = ID_SELECTOR.matcher(selector);
208             return executeIdMatcher(matcher, context);
209         }
210         else if (selector.startsWith("."))
211         {
212             Matcher matcher = CLASSNAME_SELECTOR.matcher(selector);
213             return executeClassNameMatcher(matcher, context);
214         }
215         else
216         {
217             Matcher matcher = TAGNAME_SELECTOR.matcher(selector);
218             return executeTagNameMatcher(matcher, context);
219         }
220     }
221 
222     List<WebElement> $findElements(final String selector, final SearchContext context)
223     {
224         if (isSimpleSelector(selector))
225         {
226             return executeSimpleSelector(selector, context);
227         }
228 
229         String fixedSelector = fixSelector(selector);
230 
231         if (context instanceof WebElement)
232         {
233             Object[] args = { "ATLWD.$(context).find(" + fixedSelector + ")", (WebElement) context };
234             return JavaScriptUtils.execute("return ATLWD.byJquery.execute(arguments[0],arguments[1])", getDriver(), args);
235         }
236         else
237         {
238             Object[] args = { "ATLWD.$(document).find(" + fixedSelector  + ")"};
239             return JavaScriptUtils.execute("return ATLWD.byJquery.execute(arguments[0])", getDriver(), args);
240         }
241     }
242 
243     List<WebElement> $findElements(final String selector, List<WebElement> els)
244     {
245         // Chrome Bug(http://code.google.com/p/selenium/issues/detail?id=934):
246         // if pass in elements in the args object then chrome fails to run the selector properly.
247         // So intead have to iterate over the elements and run the selector one at a time.
248         List<WebElement> newElements = new ArrayList<WebElement>();
249         for (WebElement element : els)
250         {
251             Object[] args = { "ATLWD.$(context).find(" + fixSelector(selector) + ")", element };
252             List<WebElement> temp = JavaScriptUtils.execute("return ATLWD.byJquery.execute(arguments[0],arguments[1])", getDriver(), args);
253             newElements.addAll(temp);
254         }
255 
256         return newElements;
257         //Object[] args = { "ATLWD.byJquery.$(context).find('" + selector + "')", els };
258         //return JavaScriptUtils.execute("return ATLWD.byJquery.execute(arguments[0],arguments[1])", driver, args);
259     }
260 
261     public static ByJquery $(final WebElement element)
262     {
263         loadJqueryLocator(getDriver());
264 
265         return new ByJquery() {
266             @Override
267             public List<WebElement> findElements(final SearchContext context)
268             {
269                 return findElementsWithSelectors(element);
270             }
271 
272             @Override
273             public WebElement findElement(final SearchContext context)
274             {
275                 List<WebElement> els = findElementsWithSelectors(element);
276                 if(!els.isEmpty())
277                 {
278                     return els.get(0);
279                 }
280                 else
281                 {
282                     throw new NoSuchElementException("Element not found by jQuery.");
283                 }
284             }
285 
286         };
287     }
288 
289     public static ByJquery $(final String selector)
290     {
291         if (selector == null)
292         {
293             throw new IllegalArgumentException("The jquery selector cannot be null.");
294         }
295         loadJqueryLocator(getDriver());
296 
297         return new ByJquery(selector, SelectorType.FIND)
298         {
299             @Override
300             public List<WebElement> findElements(final SearchContext context)
301             {
302                 return findElementsWithSelectors(context);
303             }
304 
305             @Override
306             public WebElement findElement(final SearchContext context)
307             {
308                 List<WebElement> els = findElementsWithSelectors(context);
309                 if(!els.isEmpty())
310                 {
311                     return els.get(0);
312                 }
313                 else
314                 {
315                     throw new NoSuchElementException(this.toString());
316                 }
317             }
318 
319             @Override
320             public String toString()
321             {
322                 return "jQuery selector: " + selector;
323             }
324         };
325 
326     }
327 
328     private void addSelector(Selector selector)
329     {
330         this.selectors.add(selector);
331     }
332 
333     public ByJquery find(String selector) {
334         Validate.notNull(selector, "The find selector cannot be null");
335         addSelector(new Selector(selector, SelectorType.FIND));
336 
337         return this;
338     }
339 
340     public ByJquery parent()
341     {
342         return parent(null);
343     }
344 
345     public ByJquery parent(String selector)
346     {
347 
348         addSelector(new Selector(selector, SelectorType.PARENT));
349 
350         return this;
351     }
352 
353     public ByJquery parents()
354     {
355         return parents(null);
356     }
357 
358     public ByJquery parents(String selector)
359     {
360 
361         addSelector(new Selector(selector, SelectorType.PARENTS));
362 
363         return this;
364     }
365 
366     public ByJquery siblings()
367     {
368         return siblings(null);
369     }
370 
371     public ByJquery siblings(String selector)
372     {
373 
374         addSelector(new Selector(selector, SelectorType.SIBLINGS));
375 
376         return this;
377     }
378 
379     public ByJquery children()
380     {
381         return children(null);
382     }
383 
384     public ByJquery children(String selector)
385     {
386         addSelector(new Selector(selector, SelectorType.CHILDREN));
387 
388         return this;
389     }
390 
391     public ByJquery prev() {
392         return prev(null);
393     }
394 
395     public ByJquery prev(String selector)
396     {
397         addSelector(new Selector(selector, SelectorType.PREV));
398 
399         return this;
400     }
401 
402     public ByJquery closest(String selector)
403     {
404         addSelector(new Selector(selector, SelectorType.CLOSEST));
405         return this;
406     }
407 
408     /**
409      * This allows filtering results based on a selector or a function.
410      * elements are currently not supported.
411      * @param selector can represent a selector expression or a function
412      * @return
413      */
414     // TODO(jwilson): SELENIUM-173 Fix up filter method.
415     public ByJquery filter(String selector)
416     {
417         addSelector(new Selector(selector, SelectorType.FILTER));
418         return this;
419     }
420 
421     /**
422      * Find a single element. Override this method if necessary.
423      *
424      * @param context A context to use to find the element
425      * @return The WebElement that matches the selector
426      */
427     public WebElement findElement(SearchContext context)
428     {
429         List<WebElement> allElements = findElements(context);
430         if (allElements == null || allElements.size() == 0)
431         {
432             throw new NoSuchElementException("Cannot locate an element using " + toString());
433         }
434         return allElements.get(0);
435     }
436 
437     private static void loadJqueryLocator(WebDriver driver)
438     {
439         JavaScriptUtils.loadScript("js/byjquery/byJquery.js", driver);
440     }
441 
442     @Override
443     public boolean equals(Object o)
444     {
445         if (this == o)
446         {
447             return true;
448         }
449         if (o == null || getClass() != o.getClass())
450         {
451             return false;
452         }
453 
454         By by = (By) o;
455 
456         return toString().equals(by.toString());
457     }
458 
459     @Override
460     public int hashCode()
461     {
462         return toString().hashCode();
463     }
464 
465 }