content: /**
   * 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!),
       *     "&lt;span wicket:id=\"label\"&gt;replaced by Wicket&lt;/span&gt;"
       * );</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">
       * &lt;!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"&gt;
       * &lt;html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"&gt;
       * &lt;head&gt;
       *   &lt;title&gt;untitled&lt;/title&gt;
       * &lt;/head&gt;
       * &lt;body&gt;
       * &lt;span wicket:id=&quot;label&quot;&gt;Hello, world!&lt;/span&gt;
       * &lt;/body&gt;
       * &lt;/html&gt;</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!),
       *     "&lt;span wicket:id=\"label\"&gt;replaced by Wicket&lt;/span&gt;"
       * );</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">
       * &lt;!DOCTYPE html&gt;
       * &lt;html&gt;
       * &lt;head&gt;
       *   &lt;title&gt;untitled&lt;/title&gt;
       * &lt;/head&gt;
       * &lt;body&gt;
       * &lt;span wicket:id=&quot;label&quot;&gt;Hello, world!&lt;/span&gt;
       * &lt;/body&gt;
       * &lt;/html&gt;</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;
      }
  }
  