/**
* Copyright 2014 55 Minutes (http://www.55minutes.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package fiftyfive.wicket.test;
import java.io.IOException;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.xpath.XPathExpressionException;
import fiftyfive.util.XPathHelper;
import org.apache.wicket.Component;
import org.apache.wicket.markup.html.WebPage;
import org.apache.wicket.protocol.http.mock.MockHttpServletRequest;
import org.apache.wicket.protocol.http.mock.MockHttpSession;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.util.tester.WicketTester;
import org.apache.wicket.util.tester.WicketTesterHelper;
import org.htmlcleaner.CleanerProperties;
import org.htmlcleaner.DomSerializer;
import org.htmlcleaner.HtmlCleaner;
import org.junit.Assert;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* Helper functions and assertions for easier testing of Wicket pages
* and components. Care has been taken to ensure that these helpers
* automatically work with both XHTML and HTML5 documents. In some cases
* there are separate methods to handle the two cases.
*
* @see #startComponentWithHtml startComponentWithHtml()
* @see #startComponentWithXHtml startComponentWithXHtml()
*/
public abstract class WicketTestUtils
{
/**
* Parses the most recently rendered Wicket page into an XML Document
* object. Uses <a href="http://htmlcleaner.sourceforge.net/">HtmlCleaner</a>
* under the hood to parse even the sloppiest HTML. You can then use
* things like xpath expressions against the document.
*
* @return The root Node of the resulting DOM
*/
public static Node markupAsDOM(WicketTester tester) throws ParserConfigurationException
{
CleanerProperties props = new CleanerProperties();
props.setNamespacesAware(false);
HtmlCleaner cleaner = new HtmlCleaner(props);
return new DomSerializer(props, true).createDOM(cleaner.clean(document(tester)));
}
/**
* Asserts that an XPath expression can be found in the most recently
* rendered Wicket page. Uses
* <a href="http://htmlcleaner.sourceforge.net/">HtmlCleaner</a>
* under the hood to parse even the sloppiest HTML into XML that can be
* queryied by XPath. It is therefore possible that the XPath assertion
* will pass, even though {@link #assertValidMarkup(WicketTester) assertValidMarkup()}
* fails.
*/
public static void assertXPath(WicketTester wt, String expr)
throws IOException, SAXException, ParserConfigurationException, TransformerException,
XPathExpressionException
{
if(matchCount(wt, expr) == 0)
{
Assert.fail(String.format(
"XPath expression [%s] could not be found in document:%n%s",
expr,
document(wt)
));
}
}
/**
* Asserts that exactly {@code count} number of instances of a given
* XPath expression can be found in the most recently
* rendered Wicket page. Uses
* <a href="http://htmlcleaner.sourceforge.net/">HtmlCleaner</a>
* under the hood to parse even the sloppiest HTML into XML that can be
* queryied by XPath. It is therefore possible that the XPath assertion
* will pass, even though {@link #assertValidMarkup(WicketTester) assertValidMarkup()}
* fails.
*/
public static void assertXPath(int count, WicketTester wt, String expr)
throws IOException, SAXException, ParserConfigurationException, TransformerException,
XPathExpressionException
{
// First make sure the expression exists at all
if(count > 0)
{
assertXPath(wt, expr);
}
// Then do a more exact check
final int matches = matchCount(wt, expr);
if(matches != count)
{
String s = 1 == count ? "" : "s";
Assert.fail(String.format(
"Expected %d occurance%s of XPath expression [%s], but " +
"found %d in document:%n%s",
count,
s,
expr,
matches,
document(wt)
));
}
}
/**
* Assert that the last rendered page has a content-type of text/html
* and is valid markup. Will autodetect whether the document is HTML5 or
* XHTML and use the appropriate validator. An HTML5 document must start
* with {@code <!DOCTYPE html>}, anything else is assumed to be XHTML.
*
* @param tester A WicketTester object that has just rendered a page.
*/
public static void assertValidMarkup(WicketTester tester)
throws IOException
{
assertValidMarkup(tester, -1);
}
/**
* Assert that the last rendered page has a content-type of text/html
* and is valid markup. Will autodetect whether the document is HTML5 or
* XHTML and use the appropriate validator. An HTML5 document must start
* with {@code <!DOCTYPE html>}, anything else is assumed to be XHTML.
*
* @param tester A WicketTester object that has just rendered a page.
* @param linesContext The number of lines of context to include around
* each validation error.
*/
public static void assertValidMarkup(WicketTester tester, int linesContext)
throws IOException
{
String type = tester.getLastResponse().getContentType();
Assert.assertNotNull(
"Content type of rendered Wicket page cannot be null", type
);
Assert.assertTrue(
"Content type of rendered Wicket page must be text/html",
type.equals("text/html") ||
type.startsWith("text/html;")
);
String document = document(tester);
AbstractDocumentValidator validator = validator(document);
if(linesContext >= 0)
{
validator.setNumLinesContext(linesContext);
}
validator.parse(document);
if(!validator.isValid())
{
Assert.fail(String.format(
"Invalid HTML:%n%s",
WicketTesterHelper.asLined(validator.getErrors())
));
}
}
/**
* Renders a component using a snippet of XHTML 1.0 Strict markup. Example:
* <pre class="example">
* startComponentWithXHtml(
* tester,
* new Label("label", "Hello, world!),
* "<span wicket:id=\"label\">replaced by Wicket</span>"
* );</pre>
* <p>
* This method will place the component in a simple XHTML Page and render
* it using the normal WicketTester request/response. In the above example,
* the rendered output will be:
* <pre class="example">
* <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
* <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
* <head>
* <title>untitled</title>
* </head>
* <body>
* <span wicket:id="label">Hello, world!</span>
* </body>
* </html></pre>
* <p>
* You can then use helper method like
* {@link #assertXPath(WicketTester,String) assertXPath} or
* {@link WicketTester#assertContains(String)}
* to verify the component rendered as expected.
*/
public static void startComponentWithXHtml(WicketTester tester,
Component c,
final String markup)
{
startComponentWithXHtml(tester, null, c, markup);
}
/**
* A variation of
* {@link #startComponentWithXHtml(WicketTester,Component,String) startComponentWithXHtml()}
* that accepts {@link PageParameters}. These page parameters are passed to
* the page that wraps the component under test.
*
* @since 2.0.4
*/
public static void startComponentWithXHtml(WicketTester tester,
PageParameters parameters,
Component c,
final String markup)
{
WebPage page = new PageWithInlineMarkup(String.format(
"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" " +
"\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">%n" +
"<html xmlns=\"http://www.w3.org/1999/xhtml\" " +
"xml:lang=\"en\" lang=\"en\">%n" +
"<head>%n <title>untitled</title>%n</head>%n" +
"<body>%n%s%n</body>%n</html>",
markup),
parameters
);
page.add(c);
tester.startPage(page);
}
/**
* Renders a component using a snippet of HTML5 markup. Example:
* <pre class="example">
* startComponentWithHtml(
* tester,
* new Label("label", "Hello, world!),
* "<span wicket:id=\"label\">replaced by Wicket</span>"
* );</pre>
* <p>
* This method will place the component in a simple HTML5 Page and render
* it using the normal WicketTester request/response. In the above example,
* the rendered output will be:
* <pre class="example">
* <!DOCTYPE html>
* <html>
* <head>
* <title>untitled</title>
* </head>
* <body>
* <span wicket:id="label">Hello, world!</span>
* </body>
* </html></pre>
* <p>
* You can then use helper method like
* {@link #assertXPath(WicketTester,String) assertXPath} or
* {@link WicketTester#assertContains(String)}
* to verify the component rendered as expected.
*/
public static void startComponentWithHtml(WicketTester tester,
Component c,
final String markup)
{
startComponentWithHtml(tester, null, c, markup);
}
/**
* A variation of
* {@link #startComponentWithHtml(WicketTester,Component,String) startComponentWithHtml()}
* that accepts {@link PageParameters}. These page parameters are passed to
* the page that wraps the component under test.
*
* @since 2.0.4
*/
public static void startComponentWithHtml(WicketTester tester,
PageParameters parameters,
Component c,
final String markup)
{
WebPage page = new PageWithInlineMarkup(String.format(
"<!DOCTYPE html>%n" +
"<html>%n<head>%n <title>untitled</title>%n</head>%n" +
"<body>%n%s%n</body>%n</html>",
markup),
parameters
);
page.add(c);
tester.startPage(page);
}
/**
* Download the requested resource and assert that the binary contents of that
* resource match the provided byte array.
*
* @param tester The WicketTester that was used to render the page being tested
* @param resourceUri A path to a resource to download, like {@code wicket/resource/...}
* (note the lack of a leading slash)
* @param expectedBytes The expected binary contents of that resource
*
* @since 3.2
*/
public static void assertDownloadEquals(WicketTester tester,
String resourceUri,
byte[] expectedBytes)
{
MockHttpSession session = new MockHttpSession(tester.getApplication().getServletContext());
MockHttpServletRequest request = new MockHttpServletRequest(
tester.getApplication(),
session,
tester.getApplication().getServletContext());
request.setURL(resourceUri);
tester.processRequest(request);
byte[] actual = tester.getLastResponse().getBinaryContent();
Assert.assertArrayEquals(expectedBytes, actual);
}
/**
* Returns the most recently rendered page as a String, as provided by
* the WicketTester.
*/
private static String document(WicketTester tester)
{
String doc = tester.getLastResponseAsString();
Assert.assertNotNull(
"HTTP body of rendered Wicket page cannot be null", doc
);
return doc;
}
/**
* Creates either an Html5Validator or XHtmlValidator based on whether the
* specific document appears to be HTML5 or not. It does this by looking
* for the exact string {@code <!DOCTYPE html>} before the opening
* {@code <html>} element. Documents with this doctype are assumed to be
* HTML5. Otherwise the document is assumed to be some flavor of XHTML.
*/
private static AbstractDocumentValidator validator(String document)
{
AbstractDocumentValidator validator;
// Create a validator based on whether or not the document appears to
// be HTML5. If not HTML5, assume some flavor of XHTML.
int doctype = document.indexOf("<!DOCTYPE html>");
if(doctype >= 0 && doctype < document.indexOf("<html"))
{
validator = new Html5Validator();
}
else
{
validator = new XHtmlValidator();
}
return validator;
}
/**
* Counts the number of nodes that are matched by the given xpath
* expression. The expression is evaluated against the most recently
* rendered page in the WicketTester.
*/
private static int matchCount(WicketTester tester, String xPathExpr)
throws IOException, SAXException, ParserConfigurationException, TransformerException,
XPathExpressionException
{
NodeList nl = new XPathHelper(markupAsDOM(tester)).findNodes(xPathExpr);
return nl != null ? nl.getLength() : 0;
}
}