View Javadoc

1   
2   
3   package com.atlassian.selenium;
4   
5   import com.atlassian.selenium.keyboard.KeyEvent;
6   import com.atlassian.selenium.keyboard.KeyEventSequence;
7   import com.atlassian.selenium.visualcomparison.ScreenElement;
8   import com.atlassian.selenium.visualcomparison.utils.ScreenResolution;
9   import com.atlassian.webtest.ui.keys.KeyEventType;
10  import com.google.gson.Gson;
11  import com.google.gson.GsonBuilder;
12  import com.google.gson.reflect.TypeToken;
13  import com.thoughtworks.selenium.DefaultSelenium;
14  
15  import java.io.BufferedReader;
16  import java.io.IOException;
17  import java.io.InputStream;
18  import java.io.InputStreamReader;
19  import java.lang.reflect.Type;
20  import java.util.Collection;
21  import java.util.EnumSet;
22  import java.util.HashMap;
23  import java.util.Map;
24  import java.util.Set;
25  
26  /**
27   * Extends the {@link DefaultSelenium} client to provide a more sensible implementation
28   * as well some extra utility methods such as keypress.
29   */
30  public class SingleBrowserSeleniumClient extends DefaultSelenium implements SeleniumClient
31  {
32  
33      private Browser browser;
34  
35      private Map<String, KeyEventSequence> characterKeySequenceMap = new HashMap<String, KeyEventSequence>();
36  
37      private static final Set<KeyEventType> ALL_EVENTS = EnumSet.allOf(KeyEventType.class);
38  
39      /**
40       * The maximum page load wait time used by Selenium. This value is set with
41       * {@link SeleniumConfiguration#getPageLoadWait()}.
42       */
43      protected final long PAGE_LOAD_WAIT;
44  
45      /**
46       * The maximum wait time for actions that don't require page loads. This value is set with
47       * {@link SeleniumConfiguration#getActionWait()}.
48       */
49      protected final long ACTION_WAIT;
50  
51      public SingleBrowserSeleniumClient(SeleniumConfiguration config)
52      {
53          super(new HtmlDumpingHttpCommandProcessor(config.getServerLocation(), config.getServerPort(), config.getBrowserStartString(), config.getBaseUrl()));
54  
55          this.PAGE_LOAD_WAIT = config.getPageLoadWait();
56          this.ACTION_WAIT = config.getActionWait();
57  
58          browser = Browser.typeOf(config.getBrowserStartString());
59      }
60  
61      public Browser getBrowser()
62      {
63          return browser;
64      }
65  
66      public void seleniumKeyPress(String locator, String key) {
67          super.keyPress(locator, key);
68      }
69  
70      /**
71       * Unlike {@link DefaultSelenium#open}, this opens the provided URL relative to the application context path.
72       * It also waits for the page to load -- a maximum of {@link #PAGE_LOAD_WAIT} before returning.
73       */
74      public void open(String url)
75      {
76          open(url, PAGE_LOAD_WAIT);
77      }
78  
79      /**
80       * Wait for page to load doesn't work the case of non-HTML based resources (like images).
81       * So sometimes you really do want to open a url without waiting.
82       * @param url
83       */
84      public void openNoWait(String url)
85      {
86          super.open(url);
87      }
88  
89      /**
90       * Opens the given URL and waits a maximum of timeoutMillis for the page to load completely.
91       */
92      public void open(String url, long timeoutMillis)
93      {
94          super.open(url);
95          super.waitForPageToLoad(String.valueOf(timeoutMillis));
96      }
97  
98      /**
99       * Overloads {@link #waitForPageToLoad(String)} to take in a long.
100      */
101     public void waitForPageToLoad(long timeoutMillis)
102     {
103         super.waitForPageToLoad(String.valueOf(timeoutMillis));
104     }
105 
106     /**
107      * Waits for the page to load with the default timeout configured in {@link SeleniumConfiguration}.
108      */
109     public void waitForPageToLoad()
110     {
111         waitForPageToLoad(PAGE_LOAD_WAIT);
112     }
113 
114     /**
115      * Executes the given Javascript in the context of the text page and waits for it to evaluate to true
116      * for a maximum of {@link #ACTION_WAIT} milliseconds.
117      * @see #waitForCondition(String, long) if you would like to specify your own timeout.
118      */
119     public void waitForCondition(String javascript)
120     {
121         waitForCondition(javascript, ACTION_WAIT);
122     }
123 
124     /**
125      * Executes the given Javascript in the context of the text page and waits for it to evaluate to true
126      * for a maximum of timeoutMillis.
127      */
128     public void waitForCondition(String javascript, long timeoutMillis)
129     {
130         waitForCondition(javascript, Long.toString(timeoutMillis));
131     }
132 
133     /**
134      * Waits for the page to finish loading ajax calls, and returns if there are no more ajax calls currently running.
135      * The method will check for a maximum of {@link #ACTION_WAIT} milliseconds
136      * @see #waitForAjaxWithJquery(long) if you would like to specify your own timeout.
137      */
138     public void waitForAjaxWithJquery()
139     {
140         waitForAjaxWithJquery(ACTION_WAIT);
141     }
142 
143     /**
144      * Waits for the page to finish loading ajax calls, and returns if there are no more ajax calls currently running.
145      * The method will check for a maximum of timeoutMillis
146      */
147     public void waitForAjaxWithJquery(long timeoutMillis)
148     {
149         if (!hasJquery())
150         {
151             throw new UnsupportedOperationException("This operation requires jQuery.");
152         }
153         waitForCondition("selenium.browserbot.getCurrentWindow().jQuery.active == 0;", Long.toString(timeoutMillis));
154     }
155 
156     /**
157      * Click the element with the given locator and optionally wait for the page to load, using {@link #PAGE_LOAD_WAIT}.
158      *
159      * @param locator the element to click, specified using Selenium selector syntax
160      * @param waitForPageToLoad whether to wait for the page to reload. Don't use this unless the page is completely
161      * reloaded.
162      * @see #click(String, long) if you would like to specify your own timeout.
163      */
164     public void click(String locator, boolean waitForPageToLoad)
165     {
166         super.click(locator);
167         if (waitForPageToLoad)
168             super.waitForPageToLoad(String.valueOf(PAGE_LOAD_WAIT));
169     }
170 
171     /**
172      * Submit the named form locator and optionally wait for the page to load, using {@link #PAGE_LOAD_WAIT}.
173      *
174      * @param form to click, specified using Selenium selector syntax
175      * @param waitForPageToLoad whether to wait for the page to reload. Don't use this unless the page is completely
176      * reloaded.
177      * @see #submit(String, long) if you would like to specify your own timeout.
178      */
179     public void submit(String form, boolean waitForPageToLoad)
180     {
181         super.submit(form);
182         if (waitForPageToLoad)
183             super.waitForPageToLoad(String.valueOf(PAGE_LOAD_WAIT));
184     }
185 
186     /**
187      * Click the element with the given locator and wait for the page to load, for a maximum of timeoutMillis.
188      * <p/>
189      * Do not use this method if the page does not reload.
190      *
191      * @param locator the element to click, specified using Selenium selector syntax
192      * @param timeoutMillis the maximum number of milliseconds to wait for the page to load. Polling takes place
193      * more frequently.
194      * @see #click(String, boolean) if you would like to use the default timeout
195      */
196     public void click(String locator, long timeoutMillis)
197     {
198         super.click(locator);
199         super.waitForPageToLoad(Long.toString(timeoutMillis));
200     }
201 
202     /**
203      * Click the element with the given locator and wait for the ajax call to finish.
204      *
205      * @param locator the element to click, specified using Selenium selector syntax
206      */
207     public void clickAndWaitForAjaxWithJquery(String locator)
208     {
209         super.click(locator);
210         waitForAjaxWithJquery();
211     }
212 
213     /**
214      * Click the element with the given locator and wait for the ajax call to finish.
215      *
216      * @param locator the element to click, specified using Selenium selector syntax
217      * @param timeoutMillis the maximum number of milliseconds to wait for the ajax calls to finish.
218      * @see #clickAndWaitForAjaxWithJquery(String) if you would like to use the default timeout
219      */
220     public void clickAndWaitForAjaxWithJquery(String locator, long timeoutMillis)
221     {
222         super.click(locator);
223         waitForAjaxWithJquery(timeoutMillis);
224     }
225 
226     /**
227      * Submit the given form and wait for the page to load, for a maximum of timeoutMillis.
228      * <p/>
229      * Do not use this method if the page does not reload.
230      *
231      * @param form the form to submit
232      * @param timeoutMillis the maximum number of milliseconds to wait for the page to load. Polling takes place
233      * more frequently.
234      * @see #click(String, boolean) if you would like to use the default timeout
235      */
236     public void submit(String form, long timeoutMillis)
237     {
238         super.submit(form);
239         super.waitForPageToLoad(Long.toString(timeoutMillis));
240     }
241 
242     /**
243      * This will type into a field by sending key down / key press / key up events.
244      * @param locator Uses the Selenium locator syntax
245      * @param key The key to be pressed
246      */
247     public void keyPress(String locator, String key)
248     {
249         super.keyDown(locator, key);
250         super.keyPress(locator, key);
251         super.keyUp(locator, key);
252     }
253 
254     /**
255      * This will type into a field by first blanking it out and then sending key down / key press / key up
256      * events.
257      *
258      * @param locator the Selenium locator
259      * @param string  the string to type
260      * @param reset   Should the field be reset first?
261      */
262     public void typeWithFullKeyEvents(String locator, String string, boolean reset)
263     {
264         new SeleniumKeyHandler(this, locator, ALL_EVENTS, reset).typeWithFullKeyEvents(string);
265     }
266 
267     /**
268      * This will type into a field by first blanking it out and then sending key down / key press / key up
269      * events. This really only calls {@link #typeWithFullKeyEvents(String,String,boolean)})}
270      *
271      * @param locator - the usual Selenium locator
272      * @param string  the string to type into a field
273      */
274     public void typeWithFullKeyEvents(String locator, String string)
275     {
276         typeWithFullKeyEvents(locator, string, true);
277     }
278 
279     public void simulateKeyPressForCharacter(final String locator, final Character character)
280     {
281         simulateKeyPressForCharacter(locator,character, ALL_EVENTS);
282     }
283 
284     public void simulateKeyPressForCharacter(final String locator, final Character character, Collection<KeyEventType> eventsToFire)
285     {
286         simulateKeyPressForIdentifier(locator,character.toString(), eventsToFire);
287     }
288 
289     public void simulateKeyPressForSpecialKey(final String locator, final int keyCode)
290     {
291         simulateKeyPressForSpecialKey(locator,keyCode, ALL_EVENTS);
292     }
293 
294     public void simulateKeyPressForSpecialKey(final String locator, final int keyCode, Collection<KeyEventType> eventsToFire)
295     {
296         simulateKeyPressForIdentifier(locator,String.format("0x%x",keyCode), eventsToFire);
297     }
298 
299     public void generateKeyEvent(final String locator, final KeyEventType eventType, final int keyCode, final int characterCode,
300                                  final boolean shiftKey, final boolean altKey, final boolean controlKey, final boolean metaKey)
301     {
302         getEval("selenium.generateKeyEvent(\""+locator+"\",'"
303                 +eventType.getEventString()+"',"
304                 +Integer.toString(keyCode)+","
305                 +Integer.toString(characterCode)+","
306                 +Boolean.TRUE.toString()+","
307                 +Boolean.toString(shiftKey)+","
308                 +Boolean.toString(altKey)+","
309                 +Boolean.toString(controlKey)+","
310                 +Boolean.toString(metaKey)+")");
311     }
312 
313     private void simulateKeyPressForIdentifier(final String locator, final String identifier, Collection<KeyEventType> eventsToFire)
314     {
315         if (characterKeySequenceMap.containsKey(identifier))
316         {
317             for (KeyEvent ke : characterKeySequenceMap.get(identifier).getKeyEvents())
318             {
319                 if (!ke.getBrowsers().contains(this.browser))
320                 {
321                     continue;
322                 }
323                 if (!eventsToFire.contains(ke.getEventType()))
324                 {
325                     continue;
326                 }
327                 int keyCode = 0;
328                 int characterCode = 0;
329                 if (ke.isToCharacterCode())
330                 {
331                     characterCode = ke.getCode();
332                 }
333                 if (ke.isToKeyCode())
334                 {
335                    keyCode = ke.getCode();
336                 }
337                 generateKeyEvent(locator,ke.getEventType(),keyCode,characterCode, ke.isCtrlKeyDown(),ke.isAltKeyDown(),
338                         ke.isShiftKeyDown(),ke.isMetaKey());
339             }
340         }
341     }
342 
343     private void toggleShiftKey(boolean keyIsPressed)
344     {
345         if (keyIsPressed)
346         {
347             super.shiftKeyDown();
348         }
349         else
350         {
351             super.shiftKeyUp();
352         }
353     }
354 
355     private void toggleAltKey(boolean keyIsPressed)
356     {
357         if (keyIsPressed)
358         {
359             super.altKeyDown();
360         }
361         else
362         {
363             super.altKeyUp();
364         }
365     }
366 
367     private void toggleControlKey(boolean keyIsPressed)
368     {
369         if (keyIsPressed)
370         {
371             super.controlKeyDown();
372         }
373         else
374         {
375             super.controlKeyUp();
376         }
377     }
378 
379     private void toggleMetaKey(boolean keyIsPressed)
380     {
381         if (keyIsPressed)
382         {
383             super.metaKeyDown();
384         }
385         else
386         {
387             super.metaKeyUp();
388         }
389 
390     }
391 
392     public void toggleToKeyCode(boolean toKeyCode)
393     {
394         super.getEval("this.browserbot.toKeyCode = "+toKeyCode+";");
395     }
396 
397     public void toggleToCharacterCode(boolean toCharacterCode)
398     {
399         super.getEval("this.browserbot.toCharacterCode = "+toCharacterCode+";");        
400     }
401 
402     /**
403      * This will select an option from a {@code select} field.
404      *
405      * @param selectName the select field name
406      * @param label the label to select
407      */
408     public void selectOption(String selectName, String label)
409     {
410         // In some browsers (i.e. Safari) the select items have funny padding
411         // so we need to use this funny method to find how the select item is
412         // padded so that it can be matched
413         String[] options = super.getSelectOptions(selectName);
414         int i = 0;
415         for(; i < options.length; i++)
416         {
417             if(options[i].trim().equals(label))
418             {
419                 break;
420             }
421         }
422         if(i < options.length)
423         {
424             super.select(selectName, options[i]);
425         }
426     }
427 
428     /**
429      * This will select an option from a {@code select} field. If the field calls executes an ajax call onchange of
430      * the value, this method will wait for that ajax method to finish.
431      *
432      * @param selectName the select field name
433      * @param label the label to select
434      */
435     public void selectOptionAndWaitForAjaxWithJquery(String selectName, String label)
436     {
437         this.selectOption(selectName, label);
438         this.waitForAjaxWithJquery();
439     }
440 
441     /**
442      * Checks a checkbox given a name and value.
443      */
444     public void check(String name, String value)
445     {
446         check("name=" + name + " value=" + value);
447     }
448 
449     public void clickLinkWithText(String text, boolean waitForPageToLoad)
450     {
451         super.click("link=" + text);
452         if (waitForPageToLoad) waitForPageToLoad();
453     }
454 
455     public void clickButton(String buttonText, boolean waitForPageToLoad)
456     {
457         clickElementWithXpath("//input[@value = '" + buttonText + "']");
458         if (waitForPageToLoad) waitForPageToLoad();
459     }
460 
461     public void clickButtonAndWaitForAjaxWithJquery(String buttonText)
462     {
463         this.clickButton(buttonText, false);
464         waitForAjaxWithJquery();
465     }
466 
467     public void clickButtonWithName(String buttonName, boolean waitForPageToLoad)
468     {
469         clickElementWithXpath("//input[@name = '" + buttonName + "']");
470         if (waitForPageToLoad) waitForPageToLoad();
471     }
472 
473     public void clickButtonWithNameAndWaitForAjaxWithJquery(String buttonName)
474     {
475         this.clickButtonWithName(buttonName, false);
476         waitForAjaxWithJquery();
477     }
478 
479     public void clickElementWithTitle(String title)
480     {
481         super.click("xpath=//*[@title='" + title + "']");
482     }
483 
484     public void clickElementWithTitleAndWaitForAjaxWithJquery(String title)
485     {
486         this.clickElementWithTitle(title);
487         waitForAjaxWithJquery();
488     }
489 
490     public void clickElementWithClass(String className)
491     {
492         super.click("css=." + className);
493     }
494 
495     public void clickElementWithClassAndWaitForAjaxWithJquery(String className)
496     {
497         this.clickElementWithClass(className);
498         waitForAjaxWithJquery();
499     }
500 
501     public void clickElementWithCss(String cssSelector)
502     {
503         super.click("css=" + cssSelector);
504     }
505 
506     public void clickElementWithCssAndWaitForAjaxWithJquery(String cssSelector)
507     {
508         this.clickElementWithCss(cssSelector);
509         waitForAjaxWithJquery();
510     }
511 
512     public void clickElementWithXpath(String xpath)
513     {
514         super.click("xpath=" + xpath);
515     }
516 
517     public void clickElementWithXpathAndWaitForAjaxWithJquery(String xpath)
518     {
519         this.clickElementWithXpath(xpath);
520         waitForAjaxWithJquery();
521     }
522 
523     public void typeInElementWithName(String elementName, String text)
524     {
525         super.type("name=" + elementName, text);
526     }
527 
528     public void typeInElementWithCss(String cssSelector, String text)
529     {
530         super.type("css=" + cssSelector, text);
531     }
532 
533     public boolean hasJquery()
534     {
535         String evalJquery = getEval("selenium.browserbot.getCurrentWindow().jQuery");
536         return evalJquery != null && !"null".equals(evalJquery) && !"undefined".equals(evalJquery);
537     }
538 
539     private void addJqueryLocator() throws IOException
540     {
541         String jqueryImpl = readFile("jquery-1.4.2.min.js");
542         String jqueryLocStrategy = readFile("jquery-locationStrategy.js");
543         //client.setExtensionJs(jqueryImpl);
544         addScript(jqueryImpl, "jquery");
545         addLocationStrategy("jquery", jqueryLocStrategy );
546     }
547 
548     private void addKeyPressJSCorrections() throws IOException
549     {
550         String keyPressCorrectionsImpl = readFile("KeyPressCorrected.js");
551         String generateKeyPressImpl = readFile("generateKeyEvent.js");
552         addScript(keyPressCorrectionsImpl, "KeyPressCorrected");        
553         addScript(generateKeyPressImpl, "generateKeyEvent");
554     }
555 
556     private void loadCharacterKeyEventMap() throws IOException
557     {
558         Gson gson = new GsonBuilder().create();
559         String characterArrayJSON = readFile("charactersKeySequences.json");
560 
561         Type collectionType = new TypeToken<HashMap<String, KeyEventSequence>>(){}.getType();
562         characterKeySequenceMap = gson.fromJson(characterArrayJSON,collectionType);
563         characterKeySequenceMap.put(" ",characterKeySequenceMap.get(String.format("0x%x",java.awt.event.KeyEvent.VK_SPACE)));
564         characterKeySequenceMap.put("\n",characterKeySequenceMap.get(String.format("0x%x",java.awt.event.KeyEvent.VK_ENTER)));
565     }
566 
567     private void addTagNameScripts() throws IOException
568     {
569         String tagNames = readFile("tag-name.js");
570         addScript(tagNames, "seleniumTagNames");
571     }
572 
573     private static String readFile(String file) throws IOException
574     {
575         InputStream stream = SingleBrowserSeleniumClient.class.getClassLoader().getResourceAsStream(file);
576         BufferedReader reader =  new BufferedReader(new InputStreamReader(stream));
577 
578         String line = reader.readLine();
579         StringBuffer contents = new StringBuffer();
580 
581         while(line != null)
582         {
583             contents.append(line).append("\n");
584             line = reader.readLine();
585         }
586 
587         return contents.toString();
588     }
589 
590     public void start()
591     {
592         super.start();
593         try
594         {
595             addJqueryLocator();
596             loadCharacterKeyEventMap();
597             addKeyPressJSCorrections();
598             addTagNameScripts();
599         }
600         catch (IOException ioe)
601         {
602             System.err.println("Unable to load JQuery locator strategy: " + ioe);
603             ioe.printStackTrace(System.err);
604         }
605 
606     }
607 
608     public void evaluate (String command)
609     {
610         execute(command);
611     }
612 
613     /**
614      * Execute a script on the client.
615      * @param command a string of javascript to send to the client.
616      * @param arguments Selenium does not accept additional arguments. These will be ignored.
617      * @return The evaluated result of the script
618      */
619     public Object execute(String command, Object... arguments)
620     {
621         // For compatibility with VisualComparableClient
622         return this.getEval(command);
623     }
624 
625     public void captureEntirePageScreenshot (String filePath)
626     {
627         // For compatibility with VisualComparableClient
628         this.captureEntirePageScreenshot(filePath, "");
629     }
630 
631     public boolean resizeScreen(ScreenResolution resolution, boolean refreshAfterResize)
632     {
633         final String result = this.getEval("selenium.browserbot.getCurrentWindow().resizeTo(" + resolution.width + ", " + resolution.height + ");");
634         if (refreshAfterResize) refreshAndWait();
635         return true;
636     }
637 
638     public void refreshAndWait ()
639     {
640         // For compatibility with VisualComparableClient
641         this.refresh();
642         this.waitForPageToLoad();
643     }
644 
645     /**
646      * Wait for jQuery AJAX calls to return.
647      * @param waitTimeMillis the time to wait for everything to finish.
648      * @return
649      */
650     public boolean waitForJQuery (long waitTimeMillis)
651     {
652         try
653         {
654             this.waitForCondition("selenium.browserbot.getCurrentWindow().jQuery.active == 0;", Long.toString(waitTimeMillis));
655             Thread.sleep(400);
656         }
657         catch (InterruptedException e)
658         {
659             return false;
660         }
661         return true;
662     }
663 
664     @Override
665     public ScreenElement getElementAtPoint(int x, int y)
666     {
667         throw new UnsupportedOperationException("Not implemented");
668     }
669 }