View Javadoc

1   package com.atlassian.webdriver.testing.rule;
2   
3   import com.atlassian.webdriver.browsers.WebDriverBrowserAutoInstall;
4   import com.atlassian.webdriver.debug.DefaultJavaScriptErrorRetriever;
5   import com.atlassian.webdriver.debug.JavaScriptErrorInfo;
6   import com.atlassian.webdriver.debug.JavaScriptErrorRetriever;
7   import com.google.common.annotations.VisibleForTesting;
8   import com.google.common.base.Joiner;
9   import com.google.common.base.Supplier;
10  import com.google.common.base.Suppliers;
11  import com.google.common.collect.ImmutableList;
12  import com.google.common.collect.ImmutableSet;
13  import org.junit.rules.TestWatcher;
14  import org.junit.runner.Description;
15  import org.openqa.selenium.WebDriver;
16  import org.slf4j.Logger;
17  import org.slf4j.LoggerFactory;
18  
19  import javax.inject.Inject;
20  import java.util.List;
21  import java.util.Set;
22  
23  import static com.google.common.base.Preconditions.checkNotNull;
24  
25  /**
26   * Rule to log Javascript console error messages.
27   *
28   * At present, the logging only works in Firefox, since we use
29   * a Firefox extension to collect the console output.
30   *
31   * @since 2.3
32   */
33  public class JavaScriptErrorsRule extends TestWatcher
34  {
35      private static final Logger DEFAULT_LOGGER = LoggerFactory.getLogger(JavaScriptErrorsRule.class);
36  
37      private final Logger logger;
38      private final Supplier<? extends WebDriver> webDriver;
39      private final ImmutableSet<String> errorsToIgnore;
40      private final boolean failOnJavaScriptErrors;
41      private final JavaScriptErrorRetriever errorRetriever;
42  
43      public JavaScriptErrorsRule(Supplier<? extends WebDriver> webDriver)
44      {
45          this(webDriver, DEFAULT_LOGGER);
46      }
47  
48      public JavaScriptErrorsRule(Supplier<? extends WebDriver> webDriver, Logger logger)
49      {
50          this(new DefaultJavaScriptErrorRetriever(webDriver), webDriver, logger, ImmutableSet.<String>of(), false);
51      }
52  
53      @Inject
54      public JavaScriptErrorsRule(WebDriver webDriver)
55      {
56          this(Suppliers.ofInstance(checkNotNull(webDriver, "webDriver")), DEFAULT_LOGGER);
57      }
58  
59      public JavaScriptErrorsRule(WebDriver webDriver, Logger logger)
60      {
61          this(Suppliers.ofInstance(checkNotNull(webDriver, "webDriver")));
62      }
63  
64      public JavaScriptErrorsRule()
65      {
66          this(WebDriverBrowserAutoInstall.driverSupplier());
67      }
68  
69      protected JavaScriptErrorsRule(JavaScriptErrorRetriever errorRetriever,
70              Supplier<? extends WebDriver> webDriver,
71              Logger logger,
72              Set<String> errorsToIgnore,
73              boolean failOnJavaScriptErrors)
74      {
75          this.errorRetriever = checkNotNull(errorRetriever, "errorRetriever");
76          this.webDriver = checkNotNull(webDriver, "webDriver");
77          this.logger = checkNotNull(logger, "logger");
78          this.errorsToIgnore = ImmutableSet.copyOf(checkNotNull(errorsToIgnore, "errorsToIgnore"));
79          this.failOnJavaScriptErrors = failOnJavaScriptErrors;
80      }
81  
82      /**
83       * Returns a copy of this rule with a different underlying {@link ErrorRetriever}.
84       * You may wish to override the ErrorRetriever in order to do custom post-processing of error messages.
85       * @param errorRetriever  an implementation of {@link ErrorRetriever}
86       * @return  a new JavaScriptErrorsRule based on the current instance
87       */
88      public JavaScriptErrorsRule errorRetriever(JavaScriptErrorRetriever errorRetriever)
89      {
90          return new JavaScriptErrorsRule(errorRetriever, this.webDriver, this.logger, this.errorsToIgnore,
91                  this.failOnJavaScriptErrors);
92      }
93  
94      /**
95       * Returns a copy of this rule with a specific set of error messages to ignore.  Any error whose
96       * message exactly matches one of the strings in this set will not be logged.
97       * @param errorsToIgnore  a set of error messages to ignore
98       * @return  a new JavaScriptErrorsRule based on the current instance
99       */
100     public JavaScriptErrorsRule errorsToIgnore(Set<String> errorsToIgnore)
101     {
102         return new JavaScriptErrorsRule(this.errorRetriever, this.webDriver, this.logger, errorsToIgnore,
103                 this.failOnJavaScriptErrors);
104     }
105 
106     /**
107      * Returns a copy of this rule that writes to a different logger.
108      * @param logger  a {@link Logger}
109      * @return  a new JavaScriptErrorsRule based on the current instance
110      */
111     public JavaScriptErrorsRule logger(Logger logger)
112     {
113         return new JavaScriptErrorsRule(this.errorRetriever, this.webDriver, logger, this.errorsToIgnore,
114                 this.failOnJavaScriptErrors);
115     }
116 
117     /**
118      * Returns a copy of this rule, specifying whether it should force a test failure when Javascript errors are found.
119      * This property is false by default.
120      * @param failOnJavaScriptErrors  true if Javascript errors should always cause a test to fail
121      * @return  a new JavaScriptErrorsRule based on the current instance
122      */
123     public JavaScriptErrorsRule failOnJavaScriptErrors(boolean failOnJavaScriptErrors)
124     {
125         return new JavaScriptErrorsRule(this.errorRetriever, this.webDriver, this.logger, this.errorsToIgnore,
126                 failOnJavaScriptErrors);
127     }
128 
129     @Override
130     @VisibleForTesting
131     public void finished(final Description description)
132     {
133         if (errorRetriever.isErrorRetrievalSupported())
134         {
135             List<String> errors = getErrors();
136             if (errors.isEmpty())
137             {
138                 // Use INFO level if there were no errors, so you can filter these lines out if desired in your logger configuration
139                 logger.info("----- Test '{}' finished with 0 JS errors. ", description.getMethodName());
140             }
141             else
142             {
143                 // Use WARN level if there were errors
144                 logger.warn("----- Test '{}' finished with {} JS error(s). ", description.getMethodName(), getErrors().size());
145                 logger.warn("----- START CONSOLE OUTPUT DUMP\n\n{}\n", getConsoleOutput(errors));
146                 logger.warn("----- END CONSOLE OUTPUT DUMP");
147                 if (failOnJavaScriptErrors)
148                 {
149                     throw new RuntimeException("Test failed due to javascript errors being detected: \n" + getConsoleOutput());
150                 }
151             }
152         }
153         else
154         {
155             logger.info("Unable to provide console output. Console output is currently only supported on Firefox.");
156         }
157     }
158 
159     @VisibleForTesting
160     public String getConsoleOutput()
161     {
162         return getConsoleOutput(getErrors());
163     }
164 
165     private String getConsoleOutput(List<String> errors)
166     {
167         return Joiner.on("\n").join(errors);   
168     }
169 
170     /**
171      * Get the console output from the browser.
172      *
173      * @return The result of invoking {@link JavaScriptError#toString} via a List.
174      */
175     @VisibleForTesting
176     protected List<String> getErrors()
177     {
178         final ImmutableList.Builder<String> ret = ImmutableList.builder();
179         for (JavaScriptErrorInfo error : errorRetriever.getErrors())
180         {
181             if (errorsToIgnore.contains(error.getMessage()))
182             {
183                 logger.debug("Ignoring JS error: {}", error);
184             }
185             else
186             {
187                 ret.add(error.getDescription());
188             }
189         }
190         return ret.build();
191     }
192 }