View Javadoc

1   /*
2    * Copyright (c) 2002-2011 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package com.gargoylesoftware.htmlunit;
16  
17  import java.io.BufferedInputStream;
18  import java.io.File;
19  import java.io.FileInputStream;
20  import java.io.FileNotFoundException;
21  import java.io.FileOutputStream;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.ObjectOutputStream;
25  import java.io.Serializable;
26  import java.lang.reflect.Method;
27  import java.lang.reflect.Modifier;
28  import java.net.ConnectException;
29  import java.net.MalformedURLException;
30  import java.net.SocketException;
31  import java.net.URL;
32  import java.net.UnknownHostException;
33  import java.util.ArrayList;
34  import java.util.Arrays;
35  import java.util.HashMap;
36  import java.util.List;
37  import java.util.ListIterator;
38  import java.util.Map;
39  
40  import org.apache.commons.io.FileUtils;
41  import org.apache.commons.io.IOUtils;
42  import org.apache.commons.lang.SerializationUtils;
43  import org.apache.commons.lang.StringUtils;
44  import org.apache.commons.logging.Log;
45  import org.apache.commons.logging.LogFactory;
46  import org.junit.After;
47  import org.junit.Assert;
48  import org.junit.Before;
49  import org.junit.Test;
50  
51  import com.gargoylesoftware.htmlunit.html.HtmlElement;
52  import com.gargoylesoftware.htmlunit.html.HtmlPage;
53  
54  /**
55   * Common superclass for HtmlUnit tests.
56   *
57   * @version $Revision: 6392 $
58   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
59   * @author David D. Kilzer
60   * @author Marc Guillemot
61   * @author Chris Erskine
62   * @author Michael Ottati
63   * @author Daniel Gredler
64   * @author Ahmed Ashour
65   */
66  public abstract class WebTestCase {
67  
68      /** Logging support. */
69      private static final Log LOG = LogFactory.getLog(WebTestCase.class);
70  
71      /** The listener port for the web server. */
72      public static final int PORT = Integer.parseInt(System.getProperty("htmlunit.test.port", "12345"));
73  
74      /** Constant for the URL which is used in the tests. */
75      public static final URL URL_FIRST;
76  
77      /** Constant for the URL which is used in the tests. */
78      public static final URL URL_SECOND;
79  
80      /**
81       * Constant for the URL which is used in the tests.
82       * This URL doesn't use the same host name as {@link #URL_FIRST} and {@link #URL_SECOND}.
83       **/
84      public static final URL URL_THIRD;
85  
86      /**
87       * The content type for JavaScript.
88       */
89      public static final String JAVASCRIPT_MIME_TYPE = "application/javascript";
90  
91      /**
92       * The name of the system property used to determine if files should be generated
93       * or not in {@link #createTestPageForRealBrowserIfNeeded(String,List)}.
94       */
95      public static final String PROPERTY_GENERATE_TESTPAGES
96          = "com.gargoylesoftware.htmlunit.WebTestCase.GenerateTestpages";
97  
98      /** System-specific line separator. */
99      protected static final String LINE_SEPARATOR = System.getProperty("line.separator");
100 
101     private BrowserVersion browserVersion_;
102     private WebClient webClient_;
103     private MockWebConnection mockWebConnection_;
104 
105     private String[] expectedAlerts_;
106 
107     private static final BrowserVersion FLAG_ALL_BROWSERS = new BrowserVersion("", "", "", 0);
108     private static final ThreadLocal<BrowserVersion> generateTest_browserVersion_ = new ThreadLocal<BrowserVersion>();
109     private String generateTest_content_;
110     private List<String> generateTest_expectedAlerts_;
111     private boolean generateTest_notYetImplemented_;
112     private String generateTest_testName_;
113     private int nbJSThreadsBeforeTest_;
114 
115     static {
116         try {
117             URL_FIRST = new URL("http://localhost:" + PORT + "/");
118             URL_SECOND = new URL("http://localhost:" + PORT + "/second/");
119             URL_THIRD = new URL("http://127.0.0.1:" + PORT + "/third/");
120         }
121         catch (final MalformedURLException e) {
122             // This is theoretically impossible.
123             throw new IllegalStateException("Unable to create URL constants");
124         }
125     }
126 
127     /**
128      * Constructor.
129      */
130     protected WebTestCase() {
131         generateTest_browserVersion_.remove();
132     }
133 
134     /**
135      * Load a page with the specified HTML using the default browser version.
136      * @param html the HTML to use
137      * @return the new page
138      * @throws Exception if something goes wrong
139      */
140     public final HtmlPage loadPage(final String html) throws Exception {
141         return loadPage(html, null);
142     }
143 
144     /**
145      * Load a page with the specified HTML and collect alerts into the list.
146      * @param browserVersion the browser version to use
147      * @param html the HTML to use
148      * @param collectedAlerts the list to hold the alerts
149      * @return the new page
150      * @throws Exception if something goes wrong
151      */
152     public final HtmlPage loadPage(final BrowserVersion browserVersion,
153             final String html, final List<String> collectedAlerts) throws Exception {
154         if (generateTest_browserVersion_.get() == null) {
155             generateTest_browserVersion_.set(browserVersion);
156         }
157         return loadPage(browserVersion, html, collectedAlerts, getDefaultUrl());
158     }
159 
160     /**
161      * User the default browser version to load a page with the specified HTML
162      * and collect alerts into the list.
163      * @param html the HTML to use
164      * @param collectedAlerts the list to hold the alerts
165      * @return the new page
166      * @throws Exception if something goes wrong
167      */
168     public final HtmlPage loadPage(final String html, final List<String> collectedAlerts) throws Exception {
169         generateTest_browserVersion_.set(FLAG_ALL_BROWSERS);
170         final BrowserVersion version = (browserVersion_ != null) ? browserVersion_ : BrowserVersion.getDefault();
171         return loadPage(version, html, collectedAlerts, getDefaultUrl());
172     }
173 
174     /**
175      * Loads an external URL, accounting for the fact that the remote server may be down or the
176      * machine running the tests may not be connected to the internet.
177      * @param url the URL to load
178      * @return the loaded page, or <tt>null</tt> if there were connectivity issues
179      * @throws Exception if an error occurs
180      */
181     protected static final HtmlPage loadUrl(final String url) throws Exception {
182         try {
183             final WebClient client = new WebClient();
184             client.setUseInsecureSSL(true);
185             return client.getPage(url);
186         }
187         catch (final ConnectException e) {
188             // The remote server is probably down.
189             System.out.println("Connection could not be made to " + url);
190             return null;
191         }
192         catch (final SocketException e) {
193             // The local machine may not be online.
194             System.out.println("Connection could not be made to " + url);
195             return null;
196         }
197         catch (final UnknownHostException e) {
198             // The local machine may not be online.
199             System.out.println("Connection could not be made to " + url);
200             return null;
201         }
202     }
203 
204     /**
205      * Loads a page with the specified HTML and collect alerts into the list.
206      * @param html the HTML to use
207      * @param collectedAlerts the list to hold the alerts
208      * @param url the URL that will use as the document host for this page
209      * @return the new page
210      * @throws Exception if something goes wrong
211      */
212     protected final HtmlPage loadPage(final String html, final List<String> collectedAlerts,
213             final URL url) throws Exception {
214 
215         return loadPage(BrowserVersion.getDefault(), html, collectedAlerts, url);
216     }
217 
218     /**
219      * Load a page with the specified HTML and collect alerts into the list.
220      * @param browserVersion the browser version to use
221      * @param html the HTML to use
222      * @param collectedAlerts the list to hold the alerts
223      * @param url the URL that will use as the document host for this page
224      * @return the new page
225      * @throws Exception if something goes wrong
226      */
227     protected final HtmlPage loadPage(final BrowserVersion browserVersion,
228             final String html, final List<String> collectedAlerts, final URL url) throws Exception {
229 
230         if (webClient_ == null) {
231             webClient_ = new WebClient(browserVersion);
232         }
233         return loadPage(webClient_, html, collectedAlerts, url);
234     }
235 
236     /**
237      * Load a page with the specified HTML and collect alerts into the list.
238      * @param client the WebClient to use (webConnection and alertHandler will be configured on it)
239      * @param html the HTML to use
240      * @param collectedAlerts the list to hold the alerts
241      * @param url the URL that will use as the document host for this page
242      * @return the new page
243      * @throws Exception if something goes wrong
244      */
245     protected static final HtmlPage loadPage(final WebClient client,
246             final String html, final List<String> collectedAlerts, final URL url) throws Exception {
247 
248         if (collectedAlerts != null) {
249             client.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
250         }
251 
252         final MockWebConnection webConnection = new MockWebConnection();
253         webConnection.setDefaultResponse(html);
254         client.setWebConnection(webConnection);
255 
256         return client.getPage(url);
257     }
258 
259     /**
260      * Load a page with the specified HTML and collect alerts into the list.
261      * @param client the WebClient to use (webConnection and alertHandler will be configured on it)
262      * @param html the HTML to use
263      * @param collectedAlerts the list to hold the alerts
264      * @return the new page
265      * @throws Exception if something goes wrong
266      */
267     protected final HtmlPage loadPage(final WebClient client,
268             final String html, final List<String> collectedAlerts) throws Exception {
269 
270         return loadPage(client, html, collectedAlerts, getDefaultUrl());
271     }
272 
273     /**
274      * Assert that the specified object is null.
275      * @param object the object to check
276      */
277     public static void assertNull(final Object object) {
278         Assert.assertNull("Expected null but found [" + object + "]", object);
279     }
280 
281     /**
282      * Facility to test external form of urls. Comparing external form of URLs is
283      * really faster than URL.equals() as the host doesn't need to be resolved.
284      * @param expectedUrl the expected URL
285      * @param actualUrl the URL to test
286      */
287     protected void assertEquals(final URL expectedUrl, final URL actualUrl) {
288         Assert.assertEquals(expectedUrl.toExternalForm(), actualUrl.toExternalForm());
289     }
290 
291     /**
292      * Asserts the two objects are equal.
293      * @param expected the expected object
294      * @param actual the object to test
295      */
296     protected void assertEquals(final Object expected, final Object actual) {
297         Assert.assertEquals(expected, actual);
298     }
299 
300     /**
301      * Asserts the two ints are equal.
302      * @param expected the expected int
303      * @param actual the int to test
304      */
305     protected void assertEquals(final int expected, final int actual) {
306         Assert.assertEquals(expected, actual);
307     }
308 
309     /**
310      * Asserts the two boolean are equal.
311      * @param expected the expected boolean
312      * @param actual the boolean to test
313      */
314     protected void assertEquals(final boolean expected, final boolean actual) {
315         Assert.assertEquals(Boolean.valueOf(expected), Boolean.valueOf(actual));
316     }
317 
318     /**
319      * Facility to test external form of urls. Comparing external form of URLs is
320      * really faster than URL.equals() as the host doesn't need to be resolved.
321      * @param message the message to display if assertion fails
322      * @param expectedUrl the string representation of the expected URL
323      * @param actualUrl the URL to test
324      */
325     protected void assertEquals(final String message, final URL expectedUrl, final URL actualUrl) {
326         Assert.assertEquals(message, expectedUrl.toExternalForm(), actualUrl.toExternalForm());
327     }
328 
329     /**
330      * Facility to test external form of an URL.
331      * @param expectedUrl the string representation of the expected URL
332      * @param actualUrl the URL to test
333      */
334     protected void assertEquals(final String expectedUrl, final URL actualUrl) {
335         Assert.assertEquals(expectedUrl, actualUrl.toExternalForm());
336     }
337 
338     /**
339      * Facility method to avoid having to create explicitly a list from
340      * a String[] (for example when testing received alerts).
341      * Transforms the String[] to a List before calling
342      * {@link junit.framework.Assert#assertEquals(java.lang.Object, java.lang.Object)}.
343      * @param expected the expected strings
344      * @param actual the collection of strings to test
345      */
346     protected void assertEquals(final String[] expected, final List<String> actual) {
347         assertEquals(null, expected, actual);
348     }
349 
350     /**
351      * Facility method to avoid having to create explicitly a list from
352      * a String[] (for example when testing received alerts).
353      * Transforms the String[] to a List before calling
354      * {@link junit.framework.Assert#assertEquals(java.lang.String, java.lang.Object, java.lang.Object)}.
355      * @param message the message to display if assertion fails
356      * @param expected the expected strings
357      * @param actual the collection of strings to test
358      */
359     protected void assertEquals(final String message, final String[] expected, final List<String> actual) {
360         Assert.assertEquals(message, Arrays.asList(expected).toString(), actual.toString());
361     }
362 
363     /**
364      * Facility to test external form of an URL.
365      * @param message the message to display if assertion fails
366      * @param expectedUrl the string representation of the expected URL
367      * @param actualUrl the URL to test
368      */
369     protected void assertEquals(final String message, final String expectedUrl, final URL actualUrl) {
370         Assert.assertEquals(message, expectedUrl, actualUrl.toExternalForm());
371     }
372 
373     /**
374      * Assert the specified condition is true.
375      * @param condition condition to test
376      */
377     protected void assertTrue(final boolean condition) {
378         Assert.assertTrue(condition);
379     }
380 
381     /**
382      * Assert the specified condition is true.
383      * @param message message to show
384      * @param condition condition to test
385      */
386     protected void assertTrue(final String message, final boolean condition) {
387         Assert.assertTrue(message, condition);
388     }
389 
390     /**
391      * Assert the specified condition is false.
392      * @param condition condition to test
393      */
394     protected void assertFalse(final boolean condition) {
395         Assert.assertFalse(condition);
396     }
397 
398     /**
399      * Returns an input stream for the specified file name. Refer to {@link #getFileObject(String)}
400      * for details on how the file is located.
401      * @param fileName the base file name
402      * @return the input stream
403      * @throws FileNotFoundException if the file cannot be found
404      */
405     public static InputStream getFileAsStream(final String fileName) throws FileNotFoundException {
406         return new BufferedInputStream(new FileInputStream(getFileObject(fileName)));
407     }
408 
409     /**
410      * Returns a File object for the specified file name. This is different from just
411      * <code>new File(fileName)</code> because it will adjust the location of the file
412      * depending on how the code is being executed.
413      *
414      * @param fileName the base filename
415      * @return the new File object
416      * @throws FileNotFoundException if the file doesn't exist
417      */
418     public static File getFileObject(final String fileName) throws FileNotFoundException {
419         final String localizedName = fileName.replace('/', File.separatorChar);
420 
421         File file = new File(localizedName);
422         if (!file.exists()) {
423             file = new File("../../" + localizedName);
424         }
425 
426         if (!file.exists()) {
427             try {
428                 System.out.println("currentDir=" + new File(".").getCanonicalPath());
429             }
430             catch (final IOException e) {
431                 e.printStackTrace();
432             }
433             throw new FileNotFoundException(localizedName);
434         }
435         return file;
436     }
437 
438     /**
439      * Facility method transforming expectedAlerts to a list and calling
440      * {@link #createTestPageForRealBrowserIfNeeded(String, List)}.
441      * @param content the content of the HTML page
442      * @param expectedAlerts the expected alerts
443      * @throws IOException if writing file fails
444      */
445     protected void createTestPageForRealBrowserIfNeeded(final String content, final String[] expectedAlerts)
446         throws IOException {
447         createTestPageForRealBrowserIfNeeded(content, Arrays.asList(expectedAlerts));
448     }
449 
450     /**
451      * Generates an instrumented HTML file in the temporary dir to easily make a manual test in a real browser.
452      * The file is generated only if the system property {@link #PROPERTY_GENERATE_TESTPAGES} is set.
453      * @param content the content of the HTML page
454      * @param expectedAlerts the expected alerts
455      * @throws IOException if writing file fails
456      */
457     protected void createTestPageForRealBrowserIfNeeded(final String content, final List<String> expectedAlerts)
458         throws IOException {
459 
460         // save the information to create a test for WebDriver
461         generateTest_content_ = content;
462         generateTest_expectedAlerts_ = expectedAlerts;
463         final Method testMethod = findRunningJUnitTestMethod();
464         generateTest_testName_ = testMethod.getDeclaringClass().getSimpleName() + "_" + testMethod.getName() + ".html";
465 
466         if (System.getProperty(PROPERTY_GENERATE_TESTPAGES) != null) {
467             // should be optimized....
468 
469             // calls to alert() should be replaced by call to custom function
470             String newContent = StringUtils.replace(content, "alert(", "htmlunitReserved_caughtAlert(");
471 
472             final String instrumentationJS = createInstrumentationScript(expectedAlerts);
473 
474             // first version, we assume that there is a <head> and a </body> or a </frameset>
475             if (newContent.indexOf("<head>") > -1) {
476                 newContent = StringUtils.replaceOnce(newContent, "<head>", "<head>" + instrumentationJS);
477             }
478             else {
479                 newContent = StringUtils.replaceOnce(newContent, "<html>",
480                         "<html>\n<head>\n" + instrumentationJS + "\n</head>\n");
481             }
482             final String endScript = "\n<script>htmlunitReserved_addSummaryAfterOnload();</script>\n";
483             if (newContent.contains("</body>")) {
484                 newContent = StringUtils.replaceOnce(newContent, "</body>",  endScript + "</body>");
485             }
486             else {
487                 LOG.info("No test generated: currently only content with a <head> and a </body> is supported");
488             }
489 
490             final File f = File.createTempFile("TEST" + '_', ".html");
491             FileUtils.writeStringToFile(f, newContent, "ISO-8859-1");
492             LOG.info("Test file written: " + f.getAbsolutePath());
493         }
494         else {
495             if (LOG.isDebugEnabled()) {
496                 LOG.debug("System property \"" + PROPERTY_GENERATE_TESTPAGES
497                     + "\" not set, don't generate test HTML page for real browser");
498             }
499         }
500     }
501 
502     /**
503      * @param expectedAlerts the list of the expected alerts
504      * @return the script to be included at the beginning of the generated HTML file
505      * @throws IOException in case of problem
506      */
507     private String createInstrumentationScript(final List<String> expectedAlerts) throws IOException {
508         // generate the js code
509         final InputStream is = getClass().getClassLoader().getResourceAsStream("alertVerifier.js");
510         final String baseJS = IOUtils.toString(is);
511         IOUtils.closeQuietly(is);
512 
513         final StringBuilder sb = new StringBuilder();
514         sb.append("\n<script type='text/javascript'>\n");
515         sb.append("var htmlunitReserved_tab = [");
516         for (final ListIterator<String> iter = expectedAlerts.listIterator(); iter.hasNext();) {
517             if (iter.hasPrevious()) {
518                 sb.append(", ");
519             }
520             String message = iter.next();
521             message = StringUtils.replace(message, "\\", "\\\\");
522             message = message.replaceAll("\n", "\\\\n").replaceAll("\r", "\\\\r");
523             sb.append("{expected: \"").append(message).append("\"}");
524         }
525         sb.append("];\n\n");
526         sb.append(baseJS);
527         sb.append("</script>\n");
528         return sb.toString();
529     }
530 
531     /**
532      * Convenience method to pull the MockWebConnection out of an HtmlPage created with
533      * the loadPage method.
534      * @param page HtmlPage to get the connection from
535      * @return the MockWebConnection that served this page
536      */
537     protected static final MockWebConnection getMockConnection(final HtmlPage page) {
538         return (MockWebConnection) page.getWebClient().getWebConnection();
539     }
540 
541     /**
542      * Runs the calling JUnit test again and fails only if it already runs.<br/>
543      * This is helpful for tests that don't currently work but should work one day,
544      * when the tested functionality has been implemented.<br/>
545      * The right way to use it is:
546      * <pre>
547      * public void testXXX() {
548      *   if (notYetImplemented()) {
549      *       return;
550      *   }
551      *
552      *   ... the real (now failing) unit test
553      * }
554      * </pre>
555      * @return <tt>false</tt> when not itself already in the call stack
556      */
557     protected boolean notYetImplemented() {
558         generateTest_notYetImplemented_ = true;
559         if (notYetImplementedFlag.get() != null) {
560             return false;
561         }
562         notYetImplementedFlag.set(Boolean.TRUE);
563 
564         final Method testMethod = findRunningJUnitTestMethod();
565         try {
566             LOG.info("Running " + testMethod.getName() + " as not yet implemented");
567             testMethod.invoke(this, (Object[]) new Class[] {});
568             Assert.fail(testMethod.getName() + " is marked as not implemented but already works");
569         }
570         catch (final Exception e) {
571             LOG.info(testMethod.getName() + " fails which is normal as it is not yet implemented");
572             // method execution failed, it is really "not yet implemented"
573         }
574         finally {
575             notYetImplementedFlag.set(null);
576         }
577 
578         return true;
579     }
580 
581     /**
582      * Finds from the call stack the active running JUnit test case
583      * @return the test case method
584      * @throws RuntimeException if no method could be found
585      */
586     private Method findRunningJUnitTestMethod() {
587         final Class< ? > cl = getClass();
588         final Class< ? >[] args = new Class[] {};
589 
590         // search the initial junit test
591         final Throwable t = new Exception();
592         for (int i = t.getStackTrace().length - 1; i >= 0; i--) {
593             final StackTraceElement element = t.getStackTrace()[i];
594             if (element.getClassName().equals(cl.getName())) {
595                 try {
596                     final Method m = cl.getMethod(element.getMethodName(), args);
597                     if (isPublicTestMethod(m)) {
598                         return m;
599                     }
600                 }
601                 catch (final Exception e) {
602                     // can't access, ignore it
603                 }
604             }
605         }
606 
607         throw new RuntimeException("No JUnit test case method found in call stack");
608     }
609 
610     /**
611      * From Junit. Test if the method is a junit test.
612      * @param method the method
613      * @return <code>true</code> if this is a junit test
614      */
615     private boolean isPublicTestMethod(final Method method) {
616         return method.getParameterTypes().length == 0
617             && (method.getName().startsWith("test") || method.getAnnotation(Test.class) != null)
618             && method.getReturnType() == Void.TYPE
619             && Modifier.isPublic(method.getModifiers());
620     }
621 
622     private static final ThreadLocal<Boolean> notYetImplementedFlag = new ThreadLocal<Boolean>();
623 
624     /**
625      * Load the specified resource for the supported browsers and tests
626      * that the generated log corresponds to the expected one for this browser.
627      *
628      * @param fileName the resource name which resides in /resources folder and
629      *        belongs to the same package as the test class.
630      *
631      * @throws Exception if the test fails
632      */
633     protected void testHTMLFile(final String fileName) throws Exception {
634         final String resourcePath = getClass().getPackage().getName().replace('.', '/') + '/' + fileName;
635         final URL url = getClass().getClassLoader().getResource(resourcePath);
636 
637         final String browserKey = getBrowserVersion().getNickname().substring(0, 2);
638 
639         final WebClient client = getWebClient();
640 
641         final HtmlPage page = client.getPage(url);
642         final HtmlElement want = page.getHtmlElementById(browserKey);
643 
644         final HtmlElement got = page.getHtmlElementById("log");
645 
646         final List<String> expected = readChildElementsText(want);
647         final List<String> actual = readChildElementsText(got);
648 
649         Assert.assertEquals(expected, actual);
650     }
651 
652     private List<String> readChildElementsText(final HtmlElement elt) {
653         final List<String> list = new ArrayList<String>();
654         for (final HtmlElement child : elt.getChildElements()) {
655             list.add(child.asText());
656         }
657         return list;
658     }
659 
660     void setBrowserVersion(final BrowserVersion browserVersion) {
661         browserVersion_ = browserVersion;
662     }
663 
664     /**
665      * Returns the WebClient instance for the current test with the current {@link BrowserVersion}.
666      * @return a WebClient with the current {@link BrowserVersion}
667      */
668     protected WebClient createNewWebClient() {
669         return new WebClient(getBrowserVersion());
670     }
671 
672     /**
673      * Returns the WebClient instance for the current test with the current {@link BrowserVersion}.
674      * @return a WebClient with the current {@link BrowserVersion}
675      */
676     protected final WebClient getWebClient() {
677         if (webClient_ == null) {
678             webClient_ = createNewWebClient();
679         }
680         return webClient_;
681     }
682 
683     /**
684      * Returns the WebClient instance for the current test with the current {@link BrowserVersion}.
685      * @return a WebClient with the current {@link BrowserVersion}
686      */
687     protected final WebClient getWebClientWithMockWebConnection() {
688         if (webClient_ == null) {
689             webClient_ = createNewWebClient();
690             webClient_.setWebConnection(getMockWebConnection());
691         }
692         return webClient_;
693     }
694 
695     /**
696      * Returns the mock WebConnection instance for the current test.
697      * @return the mock WebConnection instance for the current test
698      */
699     protected MockWebConnection getMockWebConnection() {
700         if (mockWebConnection_ == null) {
701             mockWebConnection_ = new MockWebConnection();
702         }
703         return mockWebConnection_;
704     }
705 
706     /**
707      * Sets the mock WebConnection instance for the current test.
708      * @param connection the connection to use
709      */
710     protected void setMockWebConnection(final MockWebConnection connection) {
711         mockWebConnection_ = connection;
712     }
713 
714     /**
715      * Returns the current {@link BrowserVersion}.
716      * @return current {@link BrowserVersion}
717      */
718     protected final BrowserVersion getBrowserVersion() {
719         if (browserVersion_ == null) {
720             throw new IllegalStateException("You must annotate the test class with '@RunWith(BrowserRunner.class)'");
721         }
722         return browserVersion_;
723     }
724 
725     /**
726      * Sets the expected alerts.
727      * @param expectedAlerts the expected alerts
728      */
729     protected void setExpectedAlerts(final String... expectedAlerts) {
730         expectedAlerts_ = expectedAlerts;
731     }
732 
733     /**
734      * Returns the expected alerts.
735      * @return the expected alerts
736      */
737     protected String[] getExpectedAlerts() {
738         return expectedAlerts_;
739     }
740 
741     /**
742      * Defines the provided HTML as the response of the MockWebConnection for {@link #getDefaultUrl()}
743      * and loads the page with this URL using the current browser version; finally, asserts that the
744      * alerts equal the expected alerts (in which "§§URL§§" has been expanded to the default URL).
745      * @param html the HTML to use
746      * @return the new page
747      * @throws Exception if something goes wrong
748      */
749     protected final HtmlPage loadPageWithAlerts(final String html) throws Exception {
750         return loadPageWithAlerts(html, getDefaultUrl(), -1);
751     }
752 
753     /**
754      * Defines the provided HTML as the response of the MockWebConnection for {@link #getDefaultUrl()}
755      * and loads the page with this URL using the current browser version; finally, asserts the alerts
756      * equal the expected alerts.
757      * @param html the HTML to use
758      * @param url the URL from which the provided HTML code should be delivered
759      * @param waitForJS the milliseconds to wait for background JS tasks to complete. Ignored if -1.
760      * @return the new page
761      * @throws Exception if something goes wrong
762      */
763     protected final HtmlPage loadPageWithAlerts(final String html, final URL url, final int waitForJS)
764         throws Exception {
765         if (expectedAlerts_ == null) {
766             throw new IllegalStateException("You must annotate the test class with '@RunWith(BrowserRunner.class)'");
767         }
768 
769         // expand variables in expected alerts
770         expandExpectedAlertsVariables(url);
771 
772         createTestPageForRealBrowserIfNeeded(html, expectedAlerts_);
773 
774         final WebClient client = getWebClientWithMockWebConnection();
775         final List<String> collectedAlerts = new ArrayList<String>();
776         client.setAlertHandler(new CollectingAlertHandler(collectedAlerts));
777 
778         final MockWebConnection webConnection = getMockWebConnection();
779         webConnection.setResponse(url, html);
780 
781         final HtmlPage page = client.getPage(url);
782         if (waitForJS > 0) {
783             assertEquals(0, client.waitForBackgroundJavaScriptStartingBefore(waitForJS));
784         }
785         assertEquals(expectedAlerts_, collectedAlerts);
786         return page;
787     }
788 
789     /**
790      * Expand "§§URL§§" to the provided URL in the expected alerts.
791      * @param url the url to expand
792      */
793     protected void expandExpectedAlertsVariables(final URL url) {
794         if (expectedAlerts_ == null) {
795             throw new IllegalStateException("You must annotate the test class with '@RunWith(BrowserRunner.class)'");
796         }
797         for (int i = 0; i < expectedAlerts_.length; ++i) {
798             expectedAlerts_[i] = expectedAlerts_[i].replaceAll("§§URL§§", url.toExternalForm());
799         }
800     }
801 
802     /**
803      * A generics-friendly version of {@link SerializationUtils#clone(Serializable)}.
804      * @param <T> the type of the object being cloned
805      * @param object the object being cloned
806      * @return a clone of the specified object
807      */
808     @SuppressWarnings("unchecked")
809     protected <T extends Serializable> T clone(final T object) {
810         return (T) SerializationUtils.clone(object);
811     }
812 
813     /**
814      * Gets the default URL used for the tests.
815      * @return the url
816      */
817     protected static URL getDefaultUrl() {
818         return URL_FIRST;
819     }
820 
821     /**
822      * Generates an HTML file that can be loaded and understood as a test.
823      * @throws IOException in case of problem
824      */
825     @After
826     public void generateTestForWebDriver() throws IOException {
827         if (generateTest_content_ != null && !generateTest_notYetImplemented_) {
828             final File targetDir = new File("target/generated_tests");
829             targetDir.mkdirs();
830 
831             final File outFile = new File(targetDir, generateTest_testName_);
832 
833             // replace alert(x) by a storage in window's scope
834             // Convert to string here due to: http://code.google.com/p/webdriver/issues/detail?id=209
835             final String newContent = StringUtils.replace(generateTest_content_, "alert(",
836                 "(function(t){var x = window.__huCatchedAlerts; x = x ? x : []; "
837                 + "window.__huCatchedAlerts = x; x.push(String(t))})(");
838 
839             FileUtils.writeStringToFile(outFile, newContent);
840 
841             // write the expected alerts
842             final String suffix;
843             BrowserVersion browser = generateTest_browserVersion_.get();
844             if (browser == null) {
845                 browser = getBrowserVersion();
846             }
847             if (browser == FLAG_ALL_BROWSERS) {
848                 suffix = ".expected";
849             }
850             else {
851                 suffix = "." + browser.getNickname() + ".expected";
852             }
853 
854             final File expectedLog = new File(outFile.getParentFile(), outFile.getName() + suffix);
855 
856             final FileOutputStream fos = new FileOutputStream(expectedLog);
857             final ObjectOutputStream oos = new ObjectOutputStream(fos);
858             oos.writeObject(generateTest_expectedAlerts_);
859             oos.close();
860         }
861     }
862 
863     /**
864      * Reads the number of JS threads remaining from unit tests run before the current one.
865      * Ideally it should be always 0.
866      */
867     @Before
868     public void readJSThreadsBeforeTest() {
869         nbJSThreadsBeforeTest_ = getJavaScriptThreads().size();
870     }
871 
872     /**
873      * Cleanup after a test.
874      */
875     @After
876     public void releaseResources() {
877         if (webClient_ != null) {
878             webClient_.closeAllWindows();
879         }
880         webClient_ = null;
881         mockWebConnection_ = null;
882 
883         final List<Thread> jsThreads = getJavaScriptThreads();
884         // collect stack traces
885         // caution: the threads may terminate after the threads have been returned by getJavaScriptThreads()
886         // and before stack traces are retrieved
887         if (jsThreads.size() > nbJSThreadsBeforeTest_) {
888             final Map<String, StackTraceElement[]> stackTraces = new HashMap<String, StackTraceElement[]>();
889             for (final Thread t : jsThreads) {
890                 final StackTraceElement elts[] = t.getStackTrace();
891                 if (elts != null) {
892                     stackTraces.put(t.getName(), elts);
893                 }
894             }
895 
896             if (!stackTraces.isEmpty()) {
897                 System.err.println("JS threads still running:");
898                 for (final Map.Entry<String, StackTraceElement[]> entry : stackTraces.entrySet()) {
899                     System.err.println("Thread: " + entry.getKey());
900                     final StackTraceElement elts[] = entry.getValue();
901                     for (final StackTraceElement elt : elts) {
902                         System.err.println(elt);
903                     }
904                 }
905                 throw new RuntimeException("JS threads are still running: " + jsThreads.size());
906             }
907         }
908     }
909 
910     /**
911      * Gets the active JavaScript threads.
912      * @return the threads
913      */
914     protected List<Thread> getJavaScriptThreads() {
915         final Thread[] threads = new Thread[Thread.activeCount() + 10];
916         Thread.enumerate(threads);
917         final List<Thread> jsThreads = new ArrayList<Thread>();
918         for (final Thread t : threads) {
919             if (t != null && t.getName().startsWith("JS executor for")) {
920                 jsThreads.add(t);
921             }
922         }
923 
924         return jsThreads;
925     }
926 }