app.web.ApplicationITCase.java Source code

Java tutorial

Introduction

Here is the source code for app.web.ApplicationITCase.java

Source

/*
 * Copyright (C) 2013 Pascal Mazars
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU 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 app.web;

import static app.web.ApplicationITCase.ClickEventResult.*;
import static app.web.ApplicationITCase.ClickEventResult.EventType.*;
import com.thoughtworks.selenium.DefaultSelenium;
import com.thoughtworks.selenium.SeleneseTestBase;
import com.thoughtworks.selenium.Selenium;
import com.thoughtworks.selenium.SeleniumException;
import org.apache.log4j.Logger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.util.StringUtils;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Crawler-like Selenium test that recurses through the application starting from the homepage and visiting all pages available from there
 * and interacting with all the elements a user can interact with.
 */
public class ApplicationITCase extends SeleneseTestBase {

    public static final String APPLICATION_HOMEPAGE = "http://localhost:8080/index.html"; //FIXME: read from web.xml
    public static final String DEFAULT_TIMEOUT = "28000"; //FIXME: reduce to a more sensible value, default 30s
    private static final Logger log = Logger.getLogger(ApplicationITCase.class);

    @Before
    public void setUp() throws Exception {
        selenium = new DefaultSelenium("localhost", 4444, "*chrome", "http://localhost:8080/");
        selenium.start();
        selenium.setTimeout(DEFAULT_TIMEOUT);
        //        selenium.setSpeed("2000");
    }

    @Test
    public void testApplication() {
        //goToHomepage();
        selenium.open(APPLICATION_HOMEPAGE);
        Stack<URL> locationStack = new Stack<URL>();
        locationStack.push(toLocationPath(APPLICATION_HOMEPAGE));
        testPageRecursively(locationStack);
    }

    /**
     * We should not test external links though.
     * @param locationStack to find your way back
     */
    public void testPageRecursively(final Stack<URL> locationStack) {
        log.info(showPadding(locationStack.size()) + "Testing page " + selenium.getLocation());
        testHtmlSourceRecursively("//body", locationStack);
    }

    public void testHtmlSourceRecursively(final String xpathPrefix, final Stack<URL> locationStack) {
        //clickAllJSLinks(); includes clickAllAjaxLinks();
        new ClickVerifyClick("(" + xpathPrefix + ")//*[@onclick]", locationStack).run();

        //clickAllNonJSLinks();
        new ClickVerifyClick("(" + xpathPrefix + ")//a[@href and not(@onclick)] | " + " (" + xpathPrefix
                + ")//form//input[@type='submit' and not(@onclick)]", locationStack, true).run();
    }

    public abstract class AsbtractClickCallback {
        String xpath;
        boolean waitForLoad;
        // int i; improvement? getLocator()?

        protected AsbtractClickCallback(String xpath) {
            this(xpath, false);
        }

        protected AsbtractClickCallback(String xpath, boolean waitForLoad) {
            this.xpath = xpath;
            this.waitForLoad = waitForLoad;
        }

        public abstract void executeAfterClick(ClickEventResult result);

        public boolean isWaitForLoad() {
            return waitForLoad;
        }

        public void run(int padding) {
            String currentLocation = selenium.getLocation();
            int count = selenium.getXpathCount(xpath).intValue();
            for (int i = 1; i <= count; i++) {
                String locator = "xpath=(" + xpath + ")[" + i + "]";
                if (selenium.isVisible(locator)) {
                    log.info(showPadding(padding) + "Clicking '" + getFailSafeText(locator) + "'...");
                    ClickEventResult result = clickAndWaitIfNecessary(locator, waitForLoad);
                    log.info(showPadding(padding) + "Result was " + result);
                    executeAfterClick(result);
                    selenium.open(currentLocation); // Refresh page
                } else {
                    log.info(showPadding(padding) + "There was an invisible element we just did not triggger "
                            + locator);
                }
            }
        }
    }

    public class ClickVerifyClick extends AsbtractClickCallback {
        Stack<URL> locationStack;

        public ClickVerifyClick(String xpath, Stack<URL> locationStack) {
            super(xpath);
            this.locationStack = locationStack;
        }

        public ClickVerifyClick(String xpath, Stack<URL> locationStack, boolean waitForLoad) {
            super(xpath, waitForLoad);
            this.locationStack = locationStack;
        }

        @Override
        public void executeAfterClick(ClickEventResult result) {
            verifyFalse(selenium.getTitle().contains("Error"));
            URL currentLocation = toLocationPath(selenium.getLocation());
            if (result.getEventType() == DOM_UPDATE || result.getEventType() == AJAX_UPDATE) {
                for (String partialLocator : result.getLocators()) {
                    testHtmlSourceRecursively(partialLocator, locationStack);
                    //refresh by undoing last action
                }
            } else {
                if (!locationStack.contains(currentLocation)) {
                    locationStack.push(currentLocation);
                    // recursion starts here
                    testPageRecursively(locationStack);
                    // Restore location
                    selenium.open(locationStack.pop().toString());
                } else {
                    log.info("We are not going to visit the link because it would create an infinite loop.");
                }
            }
        }

        public void run() {
            super.run(locationStack.size());
        }
    }

    private String showPadding(int padding) {
        StringBuilder strBld = new StringBuilder(padding > 1 ? "|-" : "");
        for (int i = 1; i < padding; i++) {
            strBld.append("-");
        }
        return strBld.toString();
    }

    public static int PAGE_LOAD_MAX_WAIT = 2000;

    private static boolean[] getVisibilityArray(Selenium selenium) {
        boolean[] visibilityArray = new boolean[selenium.getXpathCount("//*").intValue()];
        for (int i = 0; i < visibilityArray.length; i++) {
            // array is 0-zero, xpath is 1-based
            visibilityArray[i] = selenium.isVisible("xpath=(//*)[" + (i + 1) + "]");
        }
        return visibilityArray;
    }

    public List<Integer> compareVisibilityArray(boolean[] arr1, boolean[] arr2) {
        assertEquals(arr1.length, arr2.length);
        List<Integer> results = new ArrayList<Integer>();
        for (int i = 0; i < arr1.length; i++) {
            if (!arr1[i] && arr2[i]) {
                results.add(i);
                break; // Return only the first mismatch for now
            }
        }
        return results;
    }

    /**
     * Compares before and after ajax call.
     * @param previousHtmlSource html before ajax
     * @param currentHtmlSource html after ajax (supposedly containing extra html code)
     * @return the id of the parent which had ajax content appended to.
     */
    private static String compareHtmlSourceAfterAjaxLoad(String previousHtmlSource, String currentHtmlSource) {
        String[] previousSplit = previousHtmlSource.split("(?<=>)");
        String[] currentSplit = currentHtmlSource.split("(?<=>)");
        int i = 0;
        while (previousSplit.length > i && (currentSplit[i].equals(previousSplit[i])
                || (i == 0 || extractIdFromTag(currentSplit[i - 1]) == null))) {
            // continue incrementing if no mismatch in html code OR previous tag does not have an id attribute.
            i++;
        }
        return extractIdFromTag(currentSplit[i - 1]);
    }

    public static final Pattern ID_ATTR_EXTRACTION_PATTERN = Pattern.compile("\\sid=\"(.*?)\"");

    private static String extractIdFromTag(String htmlTag) {
        Matcher m = ID_ATTR_EXTRACTION_PATTERN.matcher(htmlTag);
        String id = null;
        while (m.find()) {
            id = m.group(1);
        }
        return id;
    }

    public ClickEventResult clickAndWaitIfNecessary(String locator, boolean forceWaitForLoad) {
        String previousHtmlSource = selenium.getHtmlSource();
        String previousLocation = selenium.getLocation();
        boolean[] previousVisibilityArray = getVisibilityArray(selenium);
        selenium.click(locator);
        if (selenium.isConfirmationPresent()) {
            // consume confirmation or else next text will fail
            selenium.getConfirmation();
        } else if (selenium.isAlertPresent()) {
            // consume alert or else next text will fail
            selenium.getAlert();
        }
        boolean isNewPageLoaded = waitForLoadIfNecessary(forceWaitForLoad);
        if (isNewPageLoaded) {
            String currentLocation = selenium.getLocation();
            return currentLocation.equals(previousLocation) ? navigationSamePage()
                    : navigationNewPage(currentLocation);
        } else {
            String currentHtmlSource = selenium.getHtmlSource();
            if (!currentHtmlSource.equals(previousHtmlSource)) {
                // Compare the number of tags
                if (StringUtils.countOccurrencesOf(currentHtmlSource, "<") == StringUtils
                        .countOccurrencesOf(previousHtmlSource, "<")) {
                    List<String> xpathList = new ArrayList<String>();
                    for (int xpathIndex : compareVisibilityArray(previousVisibilityArray,
                            getVisibilityArray(selenium))) {
                        xpathList.add("(//*)[" + (xpathIndex + 1) + "]"); //array 0-based, xpath 1-based
                    }
                    //                    if (!xpathList.isEmpty())
                    //                        log.info("Test newly visible content"); // '" + getFailSafeText(xpathList.get(0)) + "'");
                    return domUpdate(xpathList.size() > 0 ? xpathList.toArray(new String[xpathList.size()]) : null);
                } else {
                    String ajaxContentId = compareHtmlSourceAfterAjaxLoad(previousHtmlSource, currentHtmlSource);
                    //                    log.info("Test new ajax content under id " + ajaxContentId);
                    return ajaxUpdate("//*[@id='" + ajaxContentId + "']");
                }
            } else {
                return noChange();
            }
        }
    }

    public static class ClickEventResult {
        public enum EventType {
            NO_CHANGE, DOM_UPDATE, AJAX_UPDATE, NAVIGATION_SAME_PAGE, NAVIGATION_NEW_PAGE
        }

        private EventType eventType;
        private String[] location; // or locators depending on the event type

        private ClickEventResult(EventType eventType, String... location) {
            this.eventType = eventType;
            this.location = location;
        }

        public EventType getEventType() {
            return eventType;
        }

        public String getLocation() {
            assertEquals(1, location.length);
            return location[0];
        }

        public String[] getLocators() {
            return location != null ? location : new String[0]; // fail-safe method
        }

        public static ClickEventResult noChange() {
            return new ClickEventResult(EventType.NO_CHANGE);
        }

        public static ClickEventResult domUpdate(String... location) {
            return new ClickEventResult(EventType.DOM_UPDATE, location);
        }

        public static ClickEventResult ajaxUpdate(String location) {
            return new ClickEventResult(EventType.DOM_UPDATE, location);
        }

        public static ClickEventResult navigationSamePage() {
            return new ClickEventResult(EventType.NAVIGATION_SAME_PAGE);
        }

        public static ClickEventResult navigationNewPage(String location) {
            return new ClickEventResult(EventType.NAVIGATION_NEW_PAGE, location);
        }

        public String toString() {
            return eventType + (location != null ? Arrays.toString(location) : "");
        }
    }

    public boolean waitForLoadIfNecessary(boolean forceWaitForLoad) {
        if (forceWaitForLoad) {
            selenium.waitForPageToLoad(Integer.toString(PAGE_LOAD_MAX_WAIT));
            return true;
        } else {
            long initTime = System.currentTimeMillis();
            while (System.currentTimeMillis() - initTime < PAGE_LOAD_MAX_WAIT) {
                try {
                    selenium.waitForPageToLoad("10");
                    return true;
                } catch (SeleniumException e) {
                    // We do not want to fail if there was no page reload.
                }
            }
            return false;
        }
    }

    /**
     * Like assertFalse, but fails at the end of the test (during tearDown)
     */
    public void verifyFalse(String message, boolean b) {
        try {
            assertFalse(message, b);
        } catch (Error e) {
            verificationErrors.append(throwableToString(e));
        }
    }

    private static String throwableToString(Throwable t) {
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        t.printStackTrace(pw);
        return sw.toString();
    }

    public static URL toLocationPath(String location) {
        try {
            URL fullUrl = new URL(location);
            // We just throw the query string if any
            return new URL(fullUrl.getProtocol(), fullUrl.getHost(), fullUrl.getPort(), fullUrl.getPath());
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
    }

    public String getFailSafeText(String locator) {
        String text = selenium.getText(locator);
        if (text.length() > 0) {
            return text;
        } else {
            try {
                return selenium.getValue(locator);
            } catch (SeleniumException e) {
                return e.getMessage();
            }
        }
    }

    @Deprecated
    private String getReadyState() {
        return selenium.getEval("selenium.browserbot.getCurrentWindow().document.readyState");
    }

    @Deprecated
    private boolean isAjaxActive() {
        return !selenium.getEval("selenium.browserbot.getCurrentWindow().jQuery.active").equals("0");
    }

    @After
    public void tearDown() throws Exception {
        selenium.stop();
        super.tearDown(); // do not remove as this would swallow all verification errors.
    }
}