001: /*
002: * Copyright (c) 2002-2008 Gargoyle Software Inc. All rights reserved.
003: *
004: * Redistribution and use in source and binary forms, with or without
005: * modification, are permitted provided that the following conditions are met:
006: *
007: * 1. Redistributions of source code must retain the above copyright notice,
008: * this list of conditions and the following disclaimer.
009: * 2. Redistributions in binary form must reproduce the above copyright notice,
010: * this list of conditions and the following disclaimer in the documentation
011: * and/or other materials provided with the distribution.
012: * 3. The end-user documentation included with the redistribution, if any, must
013: * include the following acknowledgment:
014: *
015: * "This product includes software developed by Gargoyle Software Inc.
016: * (http://www.GargoyleSoftware.com/)."
017: *
018: * Alternately, this acknowledgment may appear in the software itself, if
019: * and wherever such third-party acknowledgments normally appear.
020: * 4. The name "Gargoyle Software" must not be used to endorse or promote
021: * products derived from this software without prior written permission.
022: * For written permission, please contact info@GargoyleSoftware.com.
023: * 5. Products derived from this software may not be called "HtmlUnit", nor may
024: * "HtmlUnit" appear in their name, without prior written permission of
025: * Gargoyle Software Inc.
026: *
027: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
028: * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
029: * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GARGOYLE
030: * SOFTWARE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
031: * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
032: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
033: * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
034: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
035: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
036: * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
037: */
038: package com.gargoylesoftware.htmlunit;
039:
040: import java.io.BufferedInputStream;
041: import java.io.File;
042: import java.io.FileInputStream;
043: import java.io.FileNotFoundException;
044: import java.io.IOException;
045: import java.io.InputStream;
046: import java.lang.reflect.Method;
047: import java.lang.reflect.Modifier;
048: import java.net.ConnectException;
049: import java.net.MalformedURLException;
050: import java.net.SocketException;
051: import java.net.URL;
052: import java.net.UnknownHostException;
053: import java.util.ArrayList;
054: import java.util.Arrays;
055: import java.util.HashMap;
056: import java.util.Iterator;
057: import java.util.List;
058: import java.util.ListIterator;
059: import java.util.Map;
060:
061: import junit.framework.AssertionFailedError;
062:
063: import org.apache.commons.io.FileUtils;
064: import org.apache.commons.io.IOUtils;
065: import org.apache.commons.lang.StringUtils;
066: import org.apache.commons.logging.Log;
067: import org.apache.commons.logging.LogFactory;
068:
069: import com.gargoylesoftware.base.testing.BaseTestCase;
070: import com.gargoylesoftware.htmlunit.html.HtmlElement;
071: import com.gargoylesoftware.htmlunit.html.HtmlPage;
072:
073: /**
074: * Common superclass for HtmlUnit tests
075: *
076: * @version $Revision: 2132 $
077: * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
078: * @author David D. Kilzer
079: * @author Marc Guillemot
080: * @author Chris Erskine
081: * @author Michael Ottati
082: * @author Daniel Gredler
083: * @author Ahmed Ashour
084: */
085: public abstract class WebTestCase extends BaseTestCase {
086: /** Constant for the url http://first which is used in the tests. */
087: public static final URL URL_FIRST;
088:
089: /** Constant for the url http://second which is used in the tests. */
090: public static final URL URL_SECOND;
091:
092: /** Constant for the url http://third which is used in the tests. */
093: public static final URL URL_THIRD;
094:
095: /** Constant for the url http://www.gargoylesoftware.com which is used in the tests. */
096: public static final URL URL_GARGOYLE;
097:
098: /**
099: * The name of the system property used to determine if files should be generated
100: * or not in {@link #createTestPageForRealBrowserIfNeeded(String,List)}
101: */
102: public static final String PROPERTY_GENERATE_TESTPAGES = "com.gargoylesoftware.htmlunit.WebTestCase.GenerateTestpages";
103:
104: static {
105: try {
106: URL_FIRST = new URL("http://first");
107: URL_SECOND = new URL("http://second");
108: URL_THIRD = new URL("http://third");
109: URL_GARGOYLE = new URL("http://www.gargoylesoftware.com/");
110: } catch (final MalformedURLException e) {
111: // This is theoretically impossible.
112: throw new IllegalStateException(
113: "Unable to create url constants");
114: }
115: }
116:
117: /**
118: * Create an instance.
119: * @param name The name of the test.
120: */
121: public WebTestCase(final String name) {
122: super (name);
123: }
124:
125: /**
126: * Load a page with the specified html using the default browser version.
127: * @param html The html to use.
128: * @return The new page.
129: * @throws Exception if something goes wrong.
130: */
131: protected static final HtmlPage loadPage(final String html)
132: throws Exception {
133: return loadPage(html, null);
134: }
135:
136: /**
137: * Load a page with the specified html and collect alerts into the list.
138: * @param browserVersion the browser version to use
139: * @param html The HTML to use.
140: * @param collectedAlerts The list to hold the alerts.
141: * @return The new page.
142: * @throws Exception If something goes wrong.
143: */
144: protected static final HtmlPage loadPage(
145: final BrowserVersion browserVersion, final String html,
146: final List collectedAlerts) throws Exception {
147: return loadPage(browserVersion, html, collectedAlerts,
148: URL_GARGOYLE);
149: }
150:
151: /**
152: * User the default browser version to load a page with the specified html
153: * and collect alerts into the list.
154: * @param html The HTML to use.
155: * @param collectedAlerts The list to hold the alerts.
156: * @return The new page.
157: * @throws Exception If something goes wrong.
158: */
159: protected static final HtmlPage loadPage(final String html,
160: final List collectedAlerts) throws Exception {
161: return loadPage(BrowserVersion.getDefault(), html,
162: collectedAlerts, URL_GARGOYLE);
163: }
164:
165: /**
166: * Loads an external URL, accounting for the fact that the remote server may be down or the
167: * machine running the tests may not be connected to the internet.
168: * @param url the URL to load
169: * @return the loaded page, or <tt>null</tt> if there were connectivity issues
170: * @throws Exception if an error occurs
171: */
172: protected static final HtmlPage loadUrl(final String url)
173: throws Exception {
174: try {
175: final WebClient client = new WebClient();
176: client.setUseInsecureSSL(true);
177: return (HtmlPage) client.getPage(url);
178: } catch (final ConnectException e) {
179: // The remote server is probably down.
180: System.out
181: .println("Connection could not be made to " + url);
182: return null;
183: } catch (final SocketException e) {
184: // The local machine may not be online.
185: System.out
186: .println("Connection could not be made to " + url);
187: return null;
188: } catch (final UnknownHostException e) {
189: // The local machine may not be online.
190: System.out
191: .println("Connection could not be made to " + url);
192: return null;
193: }
194: }
195:
196: /**
197: * Return the log that is being used for all testing objects
198: * @return The log.
199: */
200: protected final Log getLog() {
201: return LogFactory.getLog(getClass());
202: }
203:
204: /**
205: * Load 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 static final HtmlPage loadPage(final String html,
213: final List collectedAlerts, final URL url) throws Exception {
214:
215: return loadPage(BrowserVersion.getDefault(), html,
216: collectedAlerts, url);
217: }
218:
219: /**
220: * Load a page with the specified html and collect alerts into the list.
221: * @param browserVersion the browser version to use
222: * @param html The HTML to use.
223: * @param collectedAlerts The list to hold the alerts.
224: * @param url The URL that will use as the document host for this page
225: * @return The new page.
226: * @throws Exception If something goes wrong.
227: */
228: protected static final HtmlPage loadPage(
229: final BrowserVersion browserVersion, final String html,
230: final List collectedAlerts, final URL url) throws Exception {
231:
232: final WebClient client = new WebClient(browserVersion);
233: if (collectedAlerts != null) {
234: client.setAlertHandler(new CollectingAlertHandler(
235: collectedAlerts));
236: }
237:
238: final MockWebConnection webConnection = new MockWebConnection(
239: client);
240: webConnection.setDefaultResponse(html);
241: client.setWebConnection(webConnection);
242:
243: final HtmlPage page = (HtmlPage) client.getPage(url);
244: return page;
245: }
246:
247: /**
248: * Assert that the specified object is null.
249: * @param object The object to check.
250: */
251: public static void assertNull(final Object object) {
252: if (object != null) {
253: throw new AssertionFailedError("Expected null but found ["
254: + object + "]");
255: }
256: }
257:
258: /**
259: * Facility to test external form of urls. Comparing external form of urls is
260: * really faster than URL.equals() as the host doesn't need to be resolved.
261: * @param expectedUrl the expected url
262: * @param actualUrl the url to test
263: */
264: protected void assertEquals(final URL expectedUrl,
265: final URL actualUrl) {
266: assertEquals(expectedUrl.toExternalForm(), actualUrl
267: .toExternalForm());
268: }
269:
270: /**
271: * Facility to test external form of urls. Comparing external form of urls is
272: * really faster than URL.equals() as the host doesn't need to be resolved.
273: * @param message the message to display if assertion fails
274: * @param expectedUrl the string representation of the expected url
275: * @param actualUrl the url to test
276: */
277: protected void assertEquals(final String message,
278: final URL expectedUrl, final URL actualUrl) {
279: assertEquals(message, expectedUrl.toExternalForm(), actualUrl
280: .toExternalForm());
281: }
282:
283: /**
284: * Facility to test external form of an url.
285: * @param expectedUrl the string representation of the expected url
286: * @param actualUrl the url to test
287: */
288: protected void assertEquals(final String expectedUrl,
289: final URL actualUrl) {
290: assertEquals(expectedUrl, actualUrl.toExternalForm());
291: }
292:
293: /**
294: * Facility method to avoid having to create explicitly a list from
295: * a String[] (for example when testing received alerts).
296: * Transforms the String[] to a List before calling
297: * {@link junit.framework.Assert#assertEquals(java.lang.Object, java.lang.Object)}.
298: * @param expected the expected strings
299: * @param actual the collection of strings to test
300: */
301: protected void assertEquals(final String[] expected,
302: final List actual) {
303: assertEquals(Arrays.asList(expected), actual);
304: }
305:
306: /**
307: * Facility method to avoid having to create explicitly a list from
308: * a String[] (for example when testing received alerts).
309: * Transforms the String[] to a List before calling
310: * {@link junit.framework.Assert#assertEquals(java.lang.String, java.lang.Object, java.lang.Object)}.
311: * @param message the message to display if assertion fails
312: * @param expected the expected strings
313: * @param actual the collection of strings to test
314: */
315: protected void assertEquals(final String message,
316: final String[] expected, final List actual) {
317: assertEquals(message, Arrays.asList(expected), actual);
318: }
319:
320: /**
321: * Facility to test external form of an url.
322: * @param message the message to display if assertion fails
323: * @param expectedUrl the string representation of the expected url
324: * @param actualUrl the url to test
325: */
326: protected void assertEquals(final String message,
327: final String expectedUrl, final URL actualUrl) {
328: assertEquals(message, expectedUrl, actualUrl.toExternalForm());
329: }
330:
331: /**
332: * Return an input stream for the specified file name. Refer to {@link #getFileObject(String)}
333: * for details on how the file is located.
334: * @param fileName The base file name.
335: * @return The input stream.
336: * @throws FileNotFoundException If the file cannot be found.
337: */
338: public static InputStream getFileAsStream(final String fileName)
339: throws FileNotFoundException {
340: return new BufferedInputStream(new FileInputStream(
341: getFileObject(fileName)));
342: }
343:
344: /**
345: * Return a File object for the specified file name. This is different from just
346: * <code>new File(fileName)</code> because it will adjust the location of the file
347: * depending on how the code is being executed.
348: *
349: * @param fileName The base filename.
350: * @return The new File object.
351: * @throws FileNotFoundException if !file.exists()
352: */
353: public static File getFileObject(final String fileName)
354: throws FileNotFoundException {
355: final String localizedName = fileName.replace('/',
356: File.separatorChar);
357:
358: File file = new File(localizedName);
359: if (!file.exists()) {
360: file = new File("../../" + localizedName);
361: }
362:
363: if (!file.exists()) {
364: try {
365: System.out.println("currentDir="
366: + new File(".").getCanonicalPath());
367: } catch (final IOException e) {
368: e.printStackTrace();
369: }
370: throw new FileNotFoundException(localizedName);
371: }
372: return file;
373: }
374:
375: /**
376: * Facility method transforming expectedAlerts to a list and calling
377: * {@link #createTestPageForRealBrowserIfNeeded(String, List)}
378: * @param content the content of the html page
379: * @param expectedAlerts the expected alerts
380: * @throws IOException if writing file fails
381: */
382: protected void createTestPageForRealBrowserIfNeeded(
383: final String content, final String[] expectedAlerts)
384: throws IOException {
385: createTestPageForRealBrowserIfNeeded(content, Arrays
386: .asList(expectedAlerts));
387: }
388:
389: /**
390: * Generates an instrumented html file in the temporary dir to easily make a manual test in a real browser.
391: * The file is generated only if the system property {@link #PROPERTY_GENERATE_TESTPAGES} is set.
392: * @param content the content of the html page
393: * @param expectedAlerts the expected alerts
394: * @throws IOException if writing file fails
395: */
396: protected void createTestPageForRealBrowserIfNeeded(
397: final String content, final List expectedAlerts)
398: throws IOException {
399: final Log log = LogFactory.getLog(WebTestCase.class);
400: if (System.getProperty(PROPERTY_GENERATE_TESTPAGES) != null) {
401: // should be optimized....
402:
403: // calls to alert() should be replaced by call to custom function
404: String newContent = StringUtils.replace(content, "alert(",
405: "htmlunitReserved_caughtAlert(");
406:
407: final String instrumentationJS = createInstrumentationScript(expectedAlerts);
408:
409: // first version, we assume that there is a <head> and a </body> or a </frameset>
410: if (newContent.indexOf("<head>") > -1) {
411: newContent = StringUtils.replaceOnce(newContent,
412: "<head>", "<head>" + instrumentationJS);
413: } else {
414: newContent = StringUtils.replaceOnce(newContent,
415: "<html>", "<html>\n<head>\n"
416: + instrumentationJS + "\n</head>\n");
417: }
418: final String endScript = "\n<script>htmlunitReserved_addSummaryAfterOnload();</script>\n";
419: if (newContent.indexOf("</body>") != -1) {
420: newContent = StringUtils.replaceOnce(newContent,
421: "</body>", endScript + "</body>");
422: } else {
423: throw new RuntimeException(
424: "Currently only content with a <head> and a </body> is supported");
425: }
426:
427: final String testName = this .getName();
428: final File f = File.createTempFile(testName + '_', ".html");
429: FileUtils.writeStringToFile(f, newContent, "ISO-8859-1");
430: log.info("Test file written: " + f.getAbsolutePath());
431: } else {
432: log
433: .debug("System property \""
434: + PROPERTY_GENERATE_TESTPAGES
435: + "\" not set, don't generate test html page for real browser");
436: }
437: }
438:
439: /**
440: * @param expectedAlerts the list of the expected alerts
441: * @return the script to be included at the beginning of the generated html file
442: * @throws IOException in case of problem
443: */
444: private String createInstrumentationScript(final List expectedAlerts)
445: throws IOException {
446: // generate the js code
447: final InputStream is = getClass().getClassLoader()
448: .getResourceAsStream("alertVerifier.js");
449: final String baseJS = IOUtils.toString(is);
450: IOUtils.closeQuietly(is);
451:
452: final StringBuffer sb = new StringBuffer();
453: sb.append("\n<script type='text/javascript'>\n");
454: sb.append("var htmlunitReserved_tab = [");
455: for (final ListIterator iter = expectedAlerts.listIterator(); iter
456: .hasNext();) {
457: if (iter.hasPrevious()) {
458: sb.append(", ");
459: }
460: final String message = (String) iter.next();
461: sb.append("{expected: \"").append(message).append("\"}");
462: }
463: sb.append("];\n\n");
464: sb.append(baseJS);
465: sb.append("</script>\n");
466: return sb.toString();
467: }
468:
469: /**
470: * Convenience method to pull the MockWebConnection out of an HtmlPage created with
471: * the loadPage method.
472: * @param page HtmlPage to get the connection from
473: * @return the MockWebConnection that served this page
474: */
475: protected static final MockWebConnection getMockConnection(
476: final HtmlPage page) {
477: return (MockWebConnection) page.getWebClient()
478: .getWebConnection();
479: }
480:
481: /**
482: * Runs the calling JUnit test again and fails only if it already runs.<br/>
483: * This is helpful for tests that don't currently work but should work one day,
484: * when the tested functionality has been implemented.<br/>
485: * The right way to use it is:
486: * <pre>
487: * public void testXXX() {
488: * if (notYetImplemented()) {
489: * return;
490: * }
491: *
492: * ... the real (now failing) unit test
493: * }
494: * </pre>
495: * @return <false> when not itself already in the call stack
496: */
497: protected boolean notYetImplemented() {
498: if (notYetImplementedFlag.get() != null) {
499: return false;
500: }
501: notYetImplementedFlag.set(Boolean.TRUE);
502:
503: final Method testMethod = findRunningJUnitTestMethod();
504: try {
505: getLog().info(
506: "Running " + testMethod.getName()
507: + " as not yet implemented");
508: testMethod.invoke(this , new Class[] {});
509: fail(testMethod.getName()
510: + " is marked as not implemented but already works");
511: } catch (final Exception e) {
512: getLog()
513: .info(
514: testMethod.getName()
515: + " fails which is normal as it is not yet implemented");
516: // method execution failed, it is really "not yet implemented"
517: } finally {
518: notYetImplementedFlag.set(null);
519: }
520:
521: return true;
522: }
523:
524: /**
525: * Finds from the call stack the active running JUnit test case
526: * @return the test case method
527: * @throws RuntimeException if no method could be found.
528: */
529: private Method findRunningJUnitTestMethod() {
530: final Class cl = getClass();
531: final Class[] args = new Class[] {};
532:
533: // search the initial junit test
534: final Throwable t = new Exception();
535: for (int i = t.getStackTrace().length - 1; i >= 0; i--) {
536: final StackTraceElement element = t.getStackTrace()[i];
537: if (element.getClassName().equals(cl.getName())) {
538: try {
539: final Method m = cl.getMethod(element
540: .getMethodName(), args);
541: if (isPublicTestMethod(m)) {
542: return m;
543: }
544: } catch (final Exception e) {
545: // can't acces, ignore it
546: }
547: }
548: }
549:
550: throw new RuntimeException(
551: "No JUnit test case method found in call stack");
552: }
553:
554: /**
555: * From Junit. Test if the method is a junit test.
556: * @param method the method
557: * @return <code>true</code> if this is a junit test.
558: */
559: private boolean isPublicTestMethod(final Method method) {
560: final String name = method.getName();
561: final Class[] parameters = method.getParameterTypes();
562: final Class returnType = method.getReturnType();
563:
564: return parameters.length == 0 && name.startsWith("test")
565: && returnType.equals(Void.TYPE)
566: && Modifier.isPublic(method.getModifiers());
567: }
568:
569: private static final ThreadLocal notYetImplementedFlag = new ThreadLocal();
570:
571: /**
572: * Load the specified resource for the supported browsers and tests
573: * that the generated log corresponds to the expected one for this browser.
574: *
575: * @param fileName the resource name which resides in /resources folder and
576: * belongs to the same package as the test class.
577: *
578: * @throws Exception if the test fails
579: */
580: protected void testHTMLFile(final String fileName) throws Exception {
581: final String resourcePath = getClass().getPackage().getName()
582: .replace('.', '/')
583: + '/' + fileName;
584: final URL url = getClass().getClassLoader().getResource(
585: resourcePath);
586:
587: final Map testedBrowser = new HashMap();
588: testedBrowser.put("FIREFOX_2", BrowserVersion.FIREFOX_2);
589: testedBrowser.put("INTERNET_EXPLORER_6_0",
590: BrowserVersion.INTERNET_EXPLORER_6_0);
591:
592: for (final Iterator iter = testedBrowser.entrySet().iterator(); iter
593: .hasNext();) {
594: final Map.Entry entry = (Map.Entry) iter.next();
595: final String browserKey = (String) entry.getKey();
596: final BrowserVersion browserVersion = (BrowserVersion) entry
597: .getValue();
598:
599: final WebClient client = new WebClient(browserVersion);
600:
601: final HtmlPage page = (HtmlPage) client.getPage(url);
602: final HtmlElement want = page
603: .getHtmlElementById(browserKey);
604:
605: final HtmlElement got = page.getHtmlElementById("log");
606:
607: final List expected = readChildElementsText(want);
608: final List actual = readChildElementsText(got);
609:
610: assertEquals(expected, actual);
611: }
612: }
613:
614: private List readChildElementsText(final HtmlElement elt) {
615: final List list = new ArrayList();
616: for (final Iterator iter = elt.getChildElementsIterator(); iter
617: .hasNext();) {
618: final HtmlElement child = (HtmlElement) iter.next();
619: list.add(child.asText());
620: }
621: return list;
622: }
623: }
|