app.web.Application2ITCase.java Source code

Java tutorial

Introduction

Here is the source code for app.web.Application2ITCase.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 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 Application2ITCase 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<Action> actionStack = new Stack<Action>();
        actionStack.push(Action.open(APPLICATION_HOMEPAGE));
        testPageRecursively(actionStack);
    }

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

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

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

    public static class Action {
        public enum ActionType {
            NO_CHANGE, OPEN, CLICK_NEW_PAGE, CLICK_SAME_PAGE, CLICK_AJAX, CLICK_DOM_UPDATE
        }

        protected ActionType type;
        protected String target; // where you click on
        protected String[] value = new String[0]; // url or locator(s)

        //        public Action(ActionType type) {
        //            this(type, null, null);
        //        }

        //        public Action(ActionType type, String target) {
        //            this(type, target, null);
        //        }

        private Action(ActionType type, String target, String... value) {
            this.type = type;
            this.value = value;
            this.target = target;
        }

        public ActionType getType() {
            return type;
        }

        public String getValue() {
            return value[0];
        }

        public String[] getValues() {
            return value;
        }

        public String getTarget() {
            return target;
        }

        public String toString() {
            switch (type) {
            case OPEN:
                return new StringBuilder(type.toString()).append(" ").append(value[0]).toString();
            default:
                return new StringBuilder(type.toString()).append(" ")
                        .append(value != null ? Arrays.toString(value) : null).toString();
            }
        }

        public static Action open(String url) {
            return new Action(ActionType.OPEN, null, url);
        }

        public static Action noChange(String target) {
            return new Action(ActionType.NO_CHANGE, target);
        }

        public static Action navigationSamePage(String target) {
            return new Action(ActionType.CLICK_SAME_PAGE, target);
        }

        public static Action navigationNewPage(String target, String value) {
            return new Action(ActionType.CLICK_NEW_PAGE, target, value);
        }

        public static Action domUpdate(String target, String... value) {
            return new Action(ActionType.CLICK_DOM_UPDATE, target, value);
        }

        public static Action ajaxUpdate(String target, String value) {
            return new Action(ActionType.CLICK_AJAX, target, value);
        }
    }

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

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

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

        public abstract void executeAfterClick(Action result);

        public boolean isWaitForLoad() {
            return waitForLoad;
        }

        public void run(int padding) {
            String currentLocation = selenium.getLocation(); // rename testStartLocation
            int count = selenium.getXpathCount(xpath).intValue();
            log.info("There are " + count + " links to visit. Let's go!");
            for (int i = 1; i <= count; i++) {
                String locator = "xpath=(" + xpath + ")[" + i + "]";
                if (selenium.isVisible(locator)) {
                    log.info(showPadding(padding) + "Clicking '" + getFailSafeText(locator) + "'...");
                    Action result = clickAndWaitIfNecessary(locator, waitForLoad);
                    log.info(showPadding(padding) + "Result was " + result);
                    executeAfterClick(result);
                } else {
                    log.info(showPadding(padding) + "There was an invisible element we just did not triggger "
                            + locator);
                }
            }
        }
    }

    public class ClickVerifyClick extends AbstractClickCallback {
        Stack<Action> actionStack;

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

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

        @Override
        public void executeAfterClick(Action result) {
            verifyFalse(selenium.getTitle().contains("Error"));
            URL currentLocation = toLocationPath(selenium.getLocation());
            if ((result.getType() == Action.ActionType.CLICK_DOM_UPDATE
                    || result.getType() == Action.ActionType.CLICK_AJAX) && result.getValues() != null) {
                actionStack.push(result);
                for (String partialLocator : result.getValues()) {
                    testHtmlSourceRecursively(partialLocator, actionStack);
                    //FIXME: refresh by undoing last action
                }
            } else {
                if (!toLocationStack(actionStack).contains(currentLocation)) {
                    actionStack.push(result);
                    // recursion starts here
                    testPageRecursively(actionStack);
                } else {
                    actionStack.push(result);
                    log.info("We have already visited this page: we are not going to test it again.");
                }
            }
            restoreToPreviousAction(actionStack);
        }

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

    public void restoreToPreviousAction(Stack<Action> actionStack) {
        if (actionStack.size() == 1) {
            return; // No previous state
        }
        log.info("Restore " + actionStack.get(0).getValue());
        selenium.open(actionStack.get(0).getValue());
        actionStack.pop(); // remove last action
        for (int i = 1; i < actionStack.size(); i++) {
            Action action = actionStack.get(i);
            if (action.getType() == Action.ActionType.OPEN) {
            } else {
                log.info("Restore click " + action.getTarget());
                clickAndWaitIfNecessary(action.getTarget(), action.getType() != Action.ActionType.CLICK_DOM_UPDATE);
            }
        }
    }

    public Stack<URL> toLocationStack(Stack<Action> actionStack) {
        Stack<URL> locationStack = new Stack<URL>();
        for (Action action : actionStack) {
            if (action.getType() == Action.ActionType.OPEN
                    || action.getType() == Action.ActionType.CLICK_NEW_PAGE) {
                locationStack.push(toLocationPath(action.getValue()));
            }
        }
        return locationStack;
    }

    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 Action 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) ? Action.navigationSamePage(locator)
                    : Action.navigationNewPage(locator, 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 Action.domUpdate(locator,
                            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 Action.ajaxUpdate(locator, "//*[@id='" + ajaxContentId + "']");
                }
            } else {
                return Action.noChange(locator);
            }
        }
    }

    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.
    }
}