/* * Copyright (C) 2011 Laurent Caillette * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation, either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.novelang.rendering.javascript; import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.gargoylesoftware.htmlunit.NicelyResynchronizingAjaxController; import com.gargoylesoftware.htmlunit.Page; import com.gargoylesoftware.htmlunit.StatusHandler; import com.gargoylesoftware.htmlunit.UnexpectedPage; import com.gargoylesoftware.htmlunit.WebClient; import com.gargoylesoftware.htmlunit.WebWindow; import com.gargoylesoftware.htmlunit.html.HtmlCheckBoxInput; import com.gargoylesoftware.htmlunit.html.HtmlElement; import com.gargoylesoftware.htmlunit.html.HtmlForm; import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Ordering; import com.google.common.collect.Sets; import org.apache.commons.io.FilenameUtils; import org.junit.Rule; import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import static org.novelang.ResourcesForTests.TaggedPart; import org.novelang.ResourceTools; import org.novelang.ResourcesForTests; import org.novelang.common.filefixture.ResourceInstaller; import org.novelang.configuration.ConfigurationTools; import org.novelang.daemon.HttpDaemon; import org.novelang.logger.Logger; import org.novelang.logger.LoggerFactory; import org.novelang.outfit.TcpPortBooker; import org.novelang.outfit.loader.ClasspathResourceLoader; import org.novelang.outfit.loader.ResourceLoader; import org.novelang.rendering.RenditionMimeType; import org.novelang.testing.junit.MethodSupport; /** * Tests for Javascript-based interactive behavior. * <p> * Don't get impressed by this warning: * <pre> WARN com.gargoylesoftware.htmlunit.html.HtmlPage - Obsolete content type encountered: 'text/javascript'. </pre> * The <code>text/javascript</code> type <em>is</em> to be * <a href="http://en.wikipedia.org/wiki/Client-side_JavaScript#Environment" >preferred</a>. * * @author Laurent Caillette */ public class TagInteractionTest { @Test public void justLoadPage() throws IOException, InterruptedException { final List< HtmlElement > headers = getCurrentHeaders() ; assertEquals( 24, headers.size() ) ; verifyHidden( headers, ImmutableSet.< String >of() ) ; } @Test public void restrictToTag2() throws IOException, InterruptedException { tag2Checkbox.click() ; webClient.waitForBackgroundJavaScript( AJAX_TIMEOUT_MILLISECONDS ) ; verifyHidden( getCurrentHeaders(), ImmutableSet.< String >of( H0_0, H0_1, H1_0, H1_1, H4, H4_0, H5, H5_1 ) ) ; } @Test public void restrictToTag1() throws IOException, InterruptedException { tag1Checkbox.click() ; LOGGER.info( "Just clicked on the checkbox. Let's see what happens" ) ; webClient.waitForBackgroundJavaScript( AJAX_TIMEOUT_MILLISECONDS ) ; verifyHidden( getCurrentHeaders(), ImmutableSet.< String >of( H0_0, H0_2, H2_0, H2_2, H4, H4_0 ) ) ; } @Test public void revert1() throws IOException, InterruptedException { tag1Checkbox.click() ; webClient.waitForBackgroundJavaScript( AJAX_TIMEOUT_MILLISECONDS ) ; tag1Checkbox.click() ; webClient.waitForBackgroundJavaScript( AJAX_TIMEOUT_MILLISECONDS ) ; verifyHidden( getCurrentHeaders(), ImmutableSet.< String >of() ) ; } @Test public void revert2() throws IOException, InterruptedException { tag2Checkbox.click() ; webClient.waitForBackgroundJavaScript( AJAX_TIMEOUT_MILLISECONDS ) ; tag2Checkbox.click() ; webClient.waitForBackgroundJavaScript( AJAX_TIMEOUT_MILLISECONDS ) ; verifyHidden( getCurrentHeaders(), ImmutableSet.< String >of() ) ; } // ======= // Fixture // ======= private static final Logger LOGGER = LoggerFactory.getLogger( TagInteractionTest.class ) ; static { ResourcesForTests.initialize() ; } private final int httpDaemonPort = TcpPortBooker.THIS.find() ; private static final int AJAX_TIMEOUT_MILLISECONDS = 10000 ; private HttpDaemon httpDaemon ; private WebClient webClient ; private HtmlCheckBoxInput tag1Checkbox; private HtmlCheckBoxInput tag2Checkbox; @Rule public final MethodSupport methodSupport = new MethodSupport() { @Override protected void beforeStatementEvaluation() throws Exception { assertEquals( 24, ALL_HEADERS.size() ) ; resourceInstaller.copyContent( TaggedPart.dir ) ; final ResourceLoader resourceLoader = new ClasspathResourceLoader( "/" + ConfigurationTools.BUNDLED_STYLE_DIR ) ; httpDaemon = new HttpDaemon( ResourceTools.createDaemonConfiguration( httpDaemonPort, methodSupport.getDirectory(), resourceLoader ) ) ; httpDaemon.start() ; setupWebClient() ; } @Override protected void afterStatementEvaluation() throws Exception { httpDaemon.stop() ; } } ; private final ResourceInstaller resourceInstaller = new ResourceInstaller( methodSupport ) ; private void setupWebClient() throws IOException { final List< String > collectedStatusMessages = Collections.synchronizedList( Lists.< String >newArrayList() ) ; webClient = new WebClient(); webClient.setThrowExceptionOnScriptError( true ) ; webClient.setStatusHandler( new StatusHandler() { @Override public void statusMessageChanged( final Page page, final String s ) { collectedStatusMessages.add( s ) ; } } ) ; webClient.setAjaxController( new NicelyResynchronizingAjaxController() ) ; webClient.setRedirectEnabled( true ) ; final Page page = webClient.getPage( "http://localhost:" + httpDaemonPort + "/" + FilenameUtils.getBaseName( TaggedPart.TAGGED.getName() ) + "." + RenditionMimeType.HTML.getFileExtension() ) ; if( ! ( page instanceof HtmlPage ) ) { LOGGER.error( "Got page of type ", page.getClass().getName() ) ; final UnexpectedPage unexpectedPage = ( UnexpectedPage ) page ; LOGGER.error( "Unexpected page!" ) ; LOGGER.error( " Staus message: ", unexpectedPage.getWebResponse().getStatusMessage() ) ; LOGGER.error( " Response headers: ", unexpectedPage.getWebResponse().getResponseHeaders() ) ; LOGGER.error( "Page content:\n", unexpectedPage.getWebResponse().getContentAsString() ) ; fail( "Could not load the page." ) ; } final HtmlPage htmlPage = HtmlPage.class.cast( page ) ; LOGGER.info( "Now the whole page should have finished loading and initializing." ) ; LOGGER.debug( "This is the HTML we got:\n\n", htmlPage.asXml(), "\n" ) ; final HtmlForm tagList = htmlPage.getFormByName( TaggedPart.TAGS_FORM_NAME ) ; tag1Checkbox = tagList.getInputByName( TaggedPart.TAG1 ); tag2Checkbox = tagList.getInputByName( TaggedPart.TAG2 ); } private static List< HtmlElement > extractAllHeaders( final HtmlPage htmlPage ) { final List< HtmlElement > allHeaders = Lists.newArrayList() ; allHeaders.addAll( ( Collection< ? extends HtmlElement > ) htmlPage.getByXPath( "/html/body/div/div/h1" ) ) ; allHeaders.addAll( ( Collection< ? extends HtmlElement > ) htmlPage.getByXPath( "/html/body/div/div/div/h2" ) ) ; return allHeaders ; } private static void verifyHidden( final Iterable< HtmlElement > documentElements, final Set< String > hiddenHeaders ) { final Set< String > errors = Sets.newTreeSet() ; final StringBuffer messageBuffer = new StringBuffer( "\n | header | present | should be |" ) ; for( final String header : ALL_HEADERS ) { final boolean shouldBePresent = ! hiddenHeaders.contains( header ) ; final boolean isPresent = findByTextStart( documentElements, header ) != null ; messageBuffer.append( "\n | " ) ; messageBuffer.append( String.format( "%-6s", header ) ) ; messageBuffer.append( " | " ) ; messageBuffer.append( String.format( "%-7b", isPresent ) ) ; messageBuffer.append( " | " ) ; messageBuffer.append( String.format( "%-9b", shouldBePresent ) ) ; messageBuffer.append( " |" ) ; if( isPresent ) { if( ! shouldBePresent ) { errors.add( String.format( "Header %s should be present", header ) ) ; } } else { if( shouldBePresent ) { errors.add( String.format( "Header %s should not be present", header ) ) ; } } } LOGGER.info( messageBuffer.toString() ) ; if( errors.size() > 0 ) { fail( errors.toString() ) ; } } private static HtmlElement findByTextStart( final Iterable< HtmlElement > htmlElements, final String textStart ) { for( final HtmlElement htmlElement : htmlElements ) { if( cleanTextContent( htmlElement ).startsWith( textStart ) ) { return htmlElement ; } } return null ; } private static final Pattern MEANINGFUL_HEADER_TEXT = Pattern.compile( "(H\\d\\.(?:\\d\\.)?)" ) ; private static String cleanTextContent( final HtmlElement htmlElement ) { final String textContent = htmlElement.getTextContent() ; final Matcher matcher = MEANINGFUL_HEADER_TEXT.matcher( textContent ) ; if( matcher.find() ) { return matcher.group( 1 ) ; } else { throw new IllegalArgumentException( "Could not find meaningful text in '" + textContent + "'" ) ; } } private List< HtmlElement > getCurrentHeaders() { final WebWindow webWindow = webClient.getCurrentWindow() ; final HtmlPage htmlPage = ( HtmlPage ) webWindow.getEnclosedPage() ; return Ordering.from( HTMLELEMENT_COMPARATOR ).sortedCopy( extractAllHeaders( htmlPage ) ) ; } private static final Comparator< HtmlElement > HTMLELEMENT_COMPARATOR = new Comparator< HtmlElement >() { @Override public int compare( final HtmlElement e1, final HtmlElement e2 ) { return cleanTextContent( e1 ).compareTo( cleanTextContent( e2 ) ) ; } } ; private static final String H0 = "H0." ; private static final String H0_0 = "H0.0." ; private static final String H0_1 = "H0.1." ; private static final String H0_2 = "H0.2." ; private static final String H0_3 = "H0.3." ; private static final String H1 = "H1." ; private static final String H1_0 = "H1.0." ; private static final String H1_1 = "H1.1." ; private static final String H1_2 = "H1.2." ; private static final String H1_3 = "H1.3." ; private static final String H2 = "H2." ; private static final String H2_0 = "H2.0." ; private static final String H2_1 = "H2.1." ; private static final String H2_2 = "H2.2." ; private static final String H2_3 = "H2.3." ; private static final String H3 = "H3." ; private static final String H3_0 = "H3.0." ; private static final String H3_1 = "H3.1." ; private static final String H3_2 = "H3.2." ; private static final String H3_3 = "H3.3." ; private static final String H4 = "H4." ; private static final String H4_0 = "H4.0." ; private static final String H5 = "H5." ; private static final String H5_1 = "H5.1." ; private static final Set< String > ALL_HEADERS = ImmutableSet.of( H0, H0_0, H0_1, H0_2, H0_3, H1, H1_0, H1_1, H1_2, H1_3, H2, H2_0, H2_1, H2_2, H2_3, H3, H3_0, H3_1, H3_2, H3_3, H4, H4_0, H5, H5_1 ) ; }