View Javadoc

1   /*
2   ### Copyright 2017 App-Project, LLC
3   
4         *File:* WebDriverWrapper.java
5   
6      *Purpose:* Provides Selenium Remote WebDriver
7   
8      Licensed under the Apache License, Version 2.0 (the "License");
9      you may not use this file except in compliance with the License.
10     You may obtain a copy of the License at
11  
12       http://www.apache.org/licenses/LICENSE-2.0
13  
14     Unless required by applicable law or agreed to in writing, software
15     distributed under the License is distributed on an "AS IS" BASIS,
16     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17     See the License for the specific language governing permissions and
18     limitations under the License.
19  
20   */
21  package us.johnmeyer.sitetest.credentials;
22  
23  import us.johnmeyer.utilities.UrlWrapper;
24  import org.openqa.selenium.WebDriver;
25  //import org.openqa.selenium.WebDriver.Timeouts;
26  import org.openqa.selenium.By;
27  import org.openqa.selenium.WebElement;
28  import org.openqa.selenium.remote.DesiredCapabilities;
29  import org.openqa.selenium.remote.RemoteWebDriver;
30  import java.util.Formatter;
31  import java.util.List;
32  import static java.time.LocalDateTime.now;
33  
34  
35   /* TECHNICAL NOTE
36   *
37   * <h4>LifeCycle of the WebDriver</h4>
38   *
39   * <p>The webdriver is capable of controlling one device/browser at a time, and
40   * the browser can be directed to a series of pages.  Therefore, to maximize efficiency,
41   * the webDriver would be born at the beginning of the experience path and then terminate
42   * after all the tests had run against it.</p>
43   *
44   * <p>Some tests may do things with the javascript memory load etc, and it may be necessary
45   * to request a fresh test device, however, this is not going to be an initial requirement.</p>
46   *
47   * <p>The optimal approach is to lease the WebDriver to an experience path, and then give the
48   * path three options: no initialize, fresh initialize on begin, initialize on end. The second
49   * option would reinitialize if the current webdriver had been used before.</p>
50   *
51   * <p>For this reason the webdriver would need a flag to indicate prior use.  It may also need
52   * to do things like reinitialize its logs if the logs are to be matched to the experience.</p>
53   *
54   * <p>With this approach a webdriver's use could be maximized if none of the paths require
55   * initialization, and yet it could also be used to flag experience paths that are either
56   * poisoning the browser or else subject to performance scrutiny.</p>
57   *
58   */
59  /**Provides Selenium WebDriver.  Encapsulates driver instantiation, use, and
60   * disposal.
61  */
62  public final class WebDriverWrapper {
63  
64  
65      /* ### ENUMERATIONS ### */
66  
67      /** Enumerates browser options for Browserstack.*/
68      public enum Browser {
69  
70          /**Mozilla Firefox.*/
71          FIREFOX             ("Firefox"),
72  
73          /**Apple Safari.*/
74          SAFARI              ("Safari"),
75  
76          /**Microsoft Internet Explorer.*/
77          INTERNET_EXPLORER   ("IE"),
78  
79          /**Google Chrome.*/
80          CHROME              ("Chrome"),
81  
82          /**Opera Software Opera. */
83          OPERA               ("Opera"),
84  
85          /**Microsoft Edge. */
86          EDGE                ("Edge");
87  
88          /** Contains capability key value passed to WebDriver initialization. */
89          private String key;
90  
91          /** Constructor.
92           *
93           * @param capabilityKeyParam WebDriver capability key to use with this instance.*/
94          Browser(final String capabilityKeyParam) {
95              key = capabilityKeyParam;
96          }
97  
98          /** Accesses key value.
99           *
100          *  @return key value used by Browserstack/Selenium RemoteWebDriver API
101          */
102         public String getKey() {
103             return (key);
104         }
105 
106 
107     }
108 
109     /* ### CONSTANTS ### */
110 
111 
112 
113     /** Message if wrapper is re-instantiated with out quitting previous.*/
114     private static final String REINSTANTIATION_EXCEPTION_CODE = "E001";
115 
116     /** Browserstack capability key. */
117     private static final String BROWSER_CAPABILITY_KEY = "browser";
118 
119     /** Browserstack/Selenium debug flag capability. */
120     private static final String BROWSERSTACK_DEBUG_CAPABILITY_KEY = "browserstack.debug";
121 
122     /** Browserstack/Selenium build name capability. */
123     private static final String BROWSERSTACK_BUILD_NAME_CAPABILITY_KEY = "build";
124 
125     /** Browserstack/Selenium browser 'Desired Capabilities' capability key. */
126     private static final Browser BROWSER_SELECTION = Browser.CHROME;
127 
128     /** Browserstack/Selenium 'Desired Capabilities' debug flag capaibilty key.*/
129     private static final String BROWSER_DEBUG_FLAG_DESIRED_CAPABILITY_KEY_VALUE = "true";
130 
131     /** Browserstack/Selenium 'Desired Capabilities' build name capability key.*/
132     private static final String BROWSERSTACK_BUILD_NAME_DESIRED_CAPABILITY_KEY_VALUE = "defaultBuildName";
133 
134     /* ### STATIC FIELDS ### */
135 
136     /** The previously instantiated wrapper, to prevent reinstatiation without
137      * quit. As this is in a STATIC FIELD it is a global value and represents
138      * only the latest wrapper. Null means it's a new run or the driver was
139      * quit.
140      */
141     private static WebDriverWrapper activeWrapper;
142 
143     /* ### INSTANCE FIELDS ### */
144 
145     /**Build name for Browserstack UI organization.*/
146     private String buildName = null;
147 
148     /**Selenium Webdriver.*/
149     private WebDriver webDriver = null;
150 
151     /**System URL. */
152     private String systemUrl = null;
153 
154     /**Element timeout in seconds. */
155     private String elementTimeout = "Browserstack default";
156 
157     /* ### STATIC METHODS ### */
158 
159 
160     /**
161      * Is wrapper initialized?
162      * @return true if a WebDriverWrapper is initialized and has not been quit
163      */
164     public static boolean isInitialized() {
165         return (getPreviousWrapper() != null);
166     }
167 
168 
169     /**
170      * Browserstack capability key.
171      * @return the BrowserCapabilityKey
172      */
173     public static String getBrowserCapabilityKey() {
174         return BROWSER_CAPABILITY_KEY;
175     }
176 
177     /**
178      * Browserstack/Selenium debug flag capability.
179      * @return the BrowserstackDebugCapabilityKey
180      */
181     public static String getBrowserstackDebugCapabilityKey() {
182         return BROWSERSTACK_DEBUG_CAPABILITY_KEY;
183     }
184 
185     /**
186      * Browserstack/Selenium build name capability.
187      * @return the BrowserstackBuildNameCapabilityKey
188      */
189     public static String getBrowserstackBuildNameCapabilityKey() {
190         return BROWSERSTACK_BUILD_NAME_CAPABILITY_KEY;
191     }
192 
193     /**
194      * Browserstack/Selenium browser 'Desired Capabilities' capability key.
195      * @return the BrowserDesiredCapabilityKey
196      */
197     public static Browser getBrowserSelection() {
198         return BROWSER_SELECTION;
199     }
200 
201     /**
202      * Browserstack/Selenium 'Desired Capabilities' debug flag capaibilty key.
203      * @return the BrowserDebugFlagDesiredCapabilityKey
204      */
205     public static String getBrowserDebugFlagDesiredCapabilityKeyValue() {
206         return BROWSER_DEBUG_FLAG_DESIRED_CAPABILITY_KEY_VALUE;
207     }
208 
209     /**
210      * Browserstack/Selenium 'Desired Capabilities' build name capability key.
211      * @return the BrowserstackBuildNameDesiredCapabilityKey
212      */
213     public static String getBrowserstackBuildNameDesiredCapabilityKeyValue() {
214         return BROWSERSTACK_BUILD_NAME_DESIRED_CAPABILITY_KEY_VALUE;
215 
216     }
217 
218 
219     /** Recycles previous wrapper if possible;
220      * if it matches existing  build name.
221      *
222      * @param buildNameParam Name of build to use to organize tests on Browserstack
223      *
224      * @return Initialized/active Selenium WebDriver wrapper instance
225      */
226     public static WebDriverWrapper newWebDriverWrapper(final String buildNameParam) {
227 
228                                 
229         WebDriverWrapper wrapper = WebDriverWrapper.getPreviousWrapper();
230         WebDriver driver = null;
231 
232         
233         log("newWebDriverWrapper1(" + buildNameParam + ")");
234 
235 
236         if (wrapper != null) {
237 
238             //null driver signals recycle
239             String oldBuildName = wrapper.getBuildName();
240 
241             if (oldBuildName.equals(buildNameParam)) {
242                 log("Recycling wrapper.");
243                return wrapper;
244             } else {
245                 log("Insantiating new wrapper because build name "
246                         + oldBuildName + " != " + buildNameParam);
247             }
248         } else {
249             log("No previous wrapper found.");
250         }
251         return newWebDriverWrapper(buildNameParam, driver);
252 
253     }
254 
255 
256     /** Uses  previous  wrapper if possible;
257      * matches  build name.
258      *
259      * @param webDriverParam driver to wrap
260      *
261      * @param buildNameParam Name of build to use to organize tests on Browserstack
262      *
263      *
264      * @return Initialized/active Selenium WebDriver wrapper instance
265      */
266     public static WebDriverWrapper newWebDriverWrapper(final String buildNameParam,
267                                                        final WebDriver webDriverParam) {
268 
269          WebDriverWrapper webDriverWrapperLocal;
270          boolean doReinitialize = true;
271 
272          log("newWebDriverWrapper2(" + buildNameParam + ", [webDriverParam])");
273 
274          if (buildNameParam == null) {
275 
276              throw new NullPointerException("Submit build name.");
277          }
278 
279 
280          webDriverWrapperLocal = WebDriverWrapper.getPreviousWrapper();
281 
282          if (webDriverWrapperLocal != null
283                  && webDriverParam != null)  {
284 
285              String prevBuildName = webDriverWrapperLocal.getBuildName();
286 
287              if (prevBuildName == null) {
288 
289                  doReinitialize = false;
290 
291              } else if (isInitialized() && !(prevBuildName.equals(buildNameParam))) {
292 
293                         System.out.println("Recycling WebDriverWrapper.");
294                         webDriverWrapperLocal = WebDriverWrapper.getPreviousWrapper();
295                         doReinitialize = false;
296 
297              }
298 
299 
300          }
301 
302          if (doReinitialize) {
303 
304                         log("Initializing new WebDriver.");
305                         WebDriver webDriverLocal =  newWebDriver(buildNameParam);
306                         log("Initializing new wrapper.");
307                         webDriverWrapperLocal = new WebDriverWrapper(webDriverLocal,
308                                                                      buildNameParam);
309 
310          }
311 
312         return webDriverWrapperLocal;
313 
314     }
315 
316 
317     /** Returns previous wrapper unless previous has been de-initialized.
318      *
319      * @return Initialized/active Selenium WebDriver wrapper instance
320      */
321     public static WebDriverWrapper newWebDriverWrapper() {
322 
323         String keyValue
324             = getBrowserstackBuildNameDesiredCapabilityKeyValue();
325 
326         log("newWebDriverWrapper()");
327 
328         if (keyValue == null) {
329             throw new NullPointerException("Received null key value from"
330                 + " getBrowserstackBuildNameDesiredCapabilityKeyValue");
331         }
332 
333         WebDriverWrapper webDriverWrapperLocal
334             = newWebDriverWrapper(keyValue);
335 
336 
337         return webDriverWrapperLocal;
338     }
339 
340     /** Creates webDriver DesiredCapabilities object.  Labels represent K
341       * and values represent V in KV syntax.
342       *
343       * @param browserKeyParam label for browser requested field
344       * @param debugKeyParam label for debug key field
345       * @param buildNameKeyParam label for build name field
346       * @param browserValueParam  value for browser requested
347       * @param debugValueParam value for debug value
348       * @param buildNameValueParam value for build name
349       *
350       * @return configured desired capabilities object
351       */
352     public static DesiredCapabilities
353                   getDesiredCapabilities(final String browserKeyParam,
354                                          final String debugKeyParam,
355                                          final String buildNameKeyParam,
356                                          final Browser browserValueParam,
357                                          final String debugValueParam,
358                                          final String buildNameValueParam) {
359 
360         DesiredCapabilities desiredCapabilities
361                 = new DesiredCapabilities();
362 
363         desiredCapabilities.setCapability(browserKeyParam,
364                                           browserValueParam.getKey());
365 
366         desiredCapabilities
367                 .setCapability(debugKeyParam,
368                                debugValueParam);
369 
370         desiredCapabilities
371                 .setCapability(buildNameKeyParam,
372                                buildNameValueParam);
373 
374         return desiredCapabilities;
375 
376     }
377 
378 
379     /** Logs wrapper activity.
380      *
381      *  @param logEntryParam text to log
382      */
383     public static void log(final String logEntryParam) {
384 
385         String entry;
386 
387         entry = now().toString() + " DriverWrapper "
388             + ": " + logEntryParam;
389 
390         System.out.println(entry);
391 
392     }
393 
394 
395     /** Retrieves previously instantiated wrapper that is still active.
396      *
397      *  @return still-active wrapper registered previously, or null if
398      *          never initialized or if the last one quit.
399      */
400 
401     private static WebDriverWrapper getPreviousWrapper() {
402         return activeWrapper;
403     }
404 
405 
406     /** Stores latest instantiated wrapper.
407      *
408      *  @param wrapperParam reference to wrapper, to be stored
409      */
410     public static void setActiveWrapper(final WebDriverWrapper wrapperParam) {
411 
412         activeWrapper = wrapperParam;
413     }
414 
415     /** Initializes webdriver.
416      *
417      * @param buildNameParam build name to use
418      *
419      *  @return unwrapped Selenium driver
420      */
421     private static WebDriver newWebDriver(final String buildNameParam) {
422 
423 
424 
425         WebDriver webDriverLocal;
426         
427 
428         log("newWebDriver(" + buildNameParam + ")");
429         
430    
431 
432         try {
433 
434             BrowserstackCredential.newBrowserstackCredential();
435 
436         } catch (BrowserstackCredential.UsingRecycledCredentialInstance r) {
437 
438                 System.out.println("Recycling credential: "
439                         + r.toString());
440 
441         } catch (RuntimeException r) {
442 
443                 throw r;
444 
445         } catch (BrowserstackAutomateKeyException e) {
446 
447             throw new RuntimeException(e.getMessage());
448         }
449 
450         DesiredCapabilities desiredCapabilities
451             = getDesiredCapabilities(getBrowserCapabilityKey(),
452                                      getBrowserstackDebugCapabilityKey(),
453                                      getBrowserstackBuildNameCapabilityKey(),
454                                      getBrowserSelection(),
455                                      getBrowserDebugFlagDesiredCapabilityKeyValue(),
456                                      buildNameParam);
457 
458         UrlWrapper urlWrapper = BrowserstackCredential.getBrowserstackAutomateUrl();
459         webDriverLocal
460                 = new RemoteWebDriver(urlWrapper.getUrl(),
461                                       desiredCapabilities);
462 
463         return webDriverLocal;
464 
465     }
466 
467 
468     /* ### CONSTRUCTOR ### */
469 
470     /** Constructs wrapper, stores webDriver reference.
471       *
472       * @param webDriverParam web driver to wrap
473       *
474       * @param buildNameParam name of build to be tracked on Browserstack
475       *
476      */
477     private WebDriverWrapper(final WebDriver webDriverParam,
478                             final String buildNameParam) {
479 
480         webDriver = webDriverParam;
481         WebDriverWrapper.setActiveWrapper(this);
482         this.buildName = buildNameParam;
483         log("Wrapping ["
484                 + webDriverParam.toString()
485                 + "] for build "
486                 + buildNameParam);
487 
488     }
489 
490 
491     /* ### INSTANCE METHODS ### */
492 
493     /** Returns true if buildname matches.
494       *
495       * @param buildNameParam build name to check
496       *
497       * @return true if that build name is used in this wrapper
498       */
499 
500     private boolean isBuildName(final String buildNameParam) {
501 
502         if (buildNameParam == null) {
503             throw new NullPointerException("Pass not a null build name.");
504         }
505 
506         if (buildName == null) {
507             return false;
508         }
509 
510         return (buildName.equals(buildNameParam));
511 
512     }
513 
514     /** Returns buildname.
515      *
516      * @return build name as stored in instance field of this object
517      */
518     public String getBuildName() {
519 
520         if (buildName == null) {
521             throw new NullPointerException("Found null instead of internal buildName value.");
522         }
523 
524         return buildName;
525 
526     }
527 
528 
529 
530     /** Waits so webdriver can load all elements.  Issues of not finding
531       * elements immediately after pageLoad
532       * require this to be added.
533       *
534       * @param secondsParam Time (in seconds) to wait before timing out the element
535       *             search.
536       */
537     public void waitSeconds(final long secondsParam) {
538 
539         String secondsParamString
540             = new Long(secondsParam).toString();
541 
542         String oldTimeout = elementTimeout;
543 
544         java.util.concurrent.TimeUnit unit
545             = java.util.concurrent.TimeUnit.SECONDS;
546 
547         WebDriver webDriverLocal = getWebDriver();
548         webDriverLocal.manage().timeouts().implicitlyWait(secondsParam, unit);
549 
550         elementTimeout = secondsParamString;
551 
552         if (!oldTimeout.equals(elementTimeout)) {
553 
554             log("Element timeout changed from " + oldTimeout
555                     + " to " + secondsParamString
556                     + " seconds.");
557         }
558 
559      }
560 
561      /** Retrieve page source from target.
562      *
563      *  @return test of page source from connected site.
564      */
565     public String getPageSource() {
566         String pageSource = getWebDriver().getPageSource();
567         return pageSource;
568     }
569 
570      /** Retrieve body element from target.
571      *
572      *  @return test of body element from connected site.
573      */
574     public String getBodyText() {
575         WebElement webElement = findElement(By.tagName("body"));
576         return webElement.getText();
577     }
578 
579     /** Checks if *this* wrapper is not active due to quit().
580      *
581      *  @return true if this wrapper was never or is no longer active
582      */
583 
584     public boolean isInactive() {
585 
586         WebDriverWrapper previousWrapper;
587 
588         // check if never initialized
589 
590         if (!WebDriverWrapper.isInitialized()) {
591             return true;
592         }
593 
594 
595         // check if this one is quit
596 
597         previousWrapper = WebDriverWrapper.getPreviousWrapper();
598 
599         if (previousWrapper != null) {
600 
601             if (previousWrapper != this) {
602 
603                 return true;
604             }
605 
606         }
607 
608         return false;
609 
610     }
611 
612     /** Returns object state without any secrets. Returns class name, build name, webDriver
613      *   object ID, and obfuscated tester system URL.
614      *
615      *   @return description of state of this wrapper instance.
616 
617      */
618 
619     public String toString() {
620 
621         String objectId, isPrevious;
622         WebDriver webDriverLocal = getWebDriver();
623 
624         if (getPreviousWrapper() != null) {
625             isPrevious = "true";
626         } else {
627             isPrevious = "false";
628         }
629 
630         if (webDriverLocal != null) {
631             objectId = webDriverLocal.toString();
632         } else {
633             objectId = null;
634         }
635 
636         String format = "# WebDriverWrapper #\n---\n"
637             + "    WebDriver Object: %s\nBuild Name: %s\n"
638             + "    Element timeout: %s\n"
639             + "    Obfuscated Test System URL: %s\n"
640             + "    Previous Wrapper = %s";
641 
642         Formatter formatter = new Formatter();
643 
644         formatter.format(format, objectId, buildName, elementTimeout,
645                 systemUrl, isPrevious);
646 
647         return formatter.toString(); // "WebDriverWrapper";
648     }
649 
650     /** Sends remote browser to specified URL.
651      *
652      *  @param targetUrlString URL to load
653      */
654 
655     public void get(final String targetUrlString) {
656         getWebDriver().get(targetUrlString);
657 
658     }
659 
660     /**Returns an instance of a Selenium html elmement.
661      *
662      * @param byParam Selenium element locator.
663      *
664      * @return A Selenium HTML element instance.
665      */
666 
667     public WebElement findElement(final By byParam) {
668 
669         return getWebDriver().findElement(byParam);
670 
671     }
672 
673 
674 
675     /**Returns list of web page elements, using Selenium.
676      *
677      * @param byParam Selenium element locator.
678      *
679      * @return A Selenium HTML element instance.
680      */
681 
682     public List<WebElement> findElements(final By byParam) {
683 
684         return getWebDriver().findElements(byParam);
685 
686     }
687 
688     /**Quits webdriver and designates this wrapper as inactive.
689      *
690      */
691     public void quit() {
692         getWebDriver().quit();
693         setActiveWrapper(null);
694         log("quit()");
695 
696     }
697 
698 
699     /** Provides access to active WebDriver toString() method. For test purposes.
700      *
701      *  @return toString() output from wrapped WebDriver or {code}null{/code} if none active
702      */
703 
704     public String getWebDriverToString() {
705 
706         if (!isInitialized()) {
707             return null;
708         } else {
709             return webDriver.toString();
710         }
711     }
712 
713 
714 
715     /** Provides direct access to web driver. This is a transitional method, deprecated
716      * at birth.
717      *
718      * @return raw Selenium webDriver
719      *
720      * @throws IllegalStateException if this instance has not been initialized with a driver.
721      */
722 
723     private WebDriver getWebDriver() {
724 
725         if (!isInitialized()) {
726             throw new IllegalStateException("Wrapper not initialized or"
727               + " previous driver not quit.  Check code as init flag is set in constructor.");
728         }
729 
730         return webDriver;
731 
732     }
733 
734 }
735 
736