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