org.xwiki.test.wysiwyg.framework.AbstractWysiwygTestCase.java Source code

Java tutorial

Introduction

Here is the source code for org.xwiki.test.wysiwyg.framework.AbstractWysiwygTestCase.java

Source

/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This 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 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.xwiki.test.wysiwyg.framework;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Arrays;

import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.xwiki.test.selenium.framework.AbstractXWikiTestCase;

import com.thoughtworks.selenium.Wait;

import static org.junit.Assert.*;

/**
 * All XWiki WYSIWYG tests must extend this class.
 * 
 * @version $Id: 7c45b7c27fc3b6043b2e5fa929db21c03dd29bc1 $
 */
public class AbstractWysiwygTestCase extends AbstractXWikiTestCase {
    private static final String WYSIWYG_LOCATOR_FOR_WYSIWYG_TAB = "//div[@role='tab'][@tabIndex=0]/div[.='WYSIWYG']";

    private static final String WYSIWYG_LOCATOR_FOR_SOURCE_TAB = "//div[@role='tab'][@tabIndex=0]/div[.='Source']";

    /**
     * The title of the indent tool bar button. This title is used in XPath locators to access the indent button.
     */
    public static final String TOOLBAR_BUTTON_INDENT_TITLE = "Increase Indent";

    /**
     * The title of the outdent tool bar button. This title is used in XPath locators to access the outdent button.
     */
    public static final String TOOLBAR_BUTTON_OUTDENT_TITLE = "Decrease Indent";

    /**
     * The title of the undo tool bar button.
     */
    public static final String TOOLBAR_BUTTON_UNDO_TITLE = "Undo (Ctrl+Z)";

    /**
     * The title of the redo tool bar button.
     */
    public static final String TOOLBAR_BUTTON_REDO_TITLE = "Redo (Ctrl+Y)";

    /**
     * The locator for the tool bar list box used to change the style of the current selection.
     */
    public static final String TOOLBAR_SELECT_STYLE = "//select[@title=\"Apply Style\"]";

    /**
     * Locates a menu item by its label.
     */
    public static final String MENU_ITEM_BY_LABEL = "//td[contains(@class, 'gwt-MenuItem')]/div[@class = 'gwt-MenuItemLabel' and . = '%s']";

    /**
     * Use this small interval when the operation you are waiting for doesn't execute instantly but pretty fast anyway.
     */
    public static final long SMALL_WAIT_INTERVAL = 50L;

    @Override
    public void setUp() {
        super.setUp();

        login();
        open(this.getClass().getSimpleName(), getTestMethodName(), "edit", "editor=wysiwyg");
        waitForEditorToLoad();
    }

    /**
     * Logs in with the default user for this test case.
     */
    protected void login() {
        // Nothing here. Use the default login in the WYSIWYG test setup.
    }

    /**
     * @return the rich text area element
     */
    protected RichTextAreaElement getRichTextArea() {
        WebDriver driver = getDriver();
        return new RichTextAreaElement(driver, driver.findElement(By.className("gwt-RichTextArea")));
    }

    /**
     * @return the source text area
     */
    protected WebElement getSourceTextArea() {
        return getDriver().findElement(By.className("xPlainTextEditor"));
    }

    /**
     * Sets the content of the rich text area.
     * 
     * @param html the new content of the rich text area
     */
    public void setContent(String html) {
        getRichTextArea().setContent(html);
    }

    /**
     * Resets the content of the rich text area by selecting all the text like CTRL+A and deleting it using Backspace.
     */
    public void resetContent() {
        // We try to mimic as much as possible the user behavior.
        // First, we select all the content.
        selectAllContent();
        // Delete the selected content.
        typeBackspace();
        // We select again all the content. In Firefox, the selection will include the annoying br tag. Further typing
        // will overwrite it. See XWIKI-2732.
        selectAllContent();
    }

    public void selectAllContent() {
        getRichTextArea().sendKeys(Keys.chord(Keys.CONTROL, "a"));
    }

    public void typeText(String text) {
        getRichTextArea().sendKeys(text);
    }

    public void typeTextThenEnter(String text) {
        getRichTextArea().sendKeys(text, Keys.RETURN);
    }

    /**
     * Presses the specified key for the given number of times in WYSIWYG rich text editor.
     * 
     * @param key the key to be pressed
     * @param count the number of times to press the specified key
     * @param hold {@code false} if the key should be released after each key press, {@code true} if it should be hold
     *            down and released just at the end
     */
    public void sendKey(Keys key, int count, boolean hold) {
        Keys[] sequence = new Keys[count];
        Arrays.fill(sequence, key);
        if (hold) {
            getRichTextArea().sendKeys(Keys.chord(sequence));
        } else {
            getRichTextArea().sendKeys(sequence);
        }
    }

    public void typeEnter() {
        typeEnter(1);
    }

    public void typeEnter(int nb) {
        sendKey(Keys.RETURN, nb, false);
    }

    public void typeShiftEnter() {
        getRichTextArea().sendKeys(Keys.chord(Keys.SHIFT, Keys.RETURN));
    }

    public void typeControlEnter() {
        getRichTextArea().sendKeys(Keys.chord(Keys.CONTROL, Keys.RETURN));
    }

    public void typeMetaEnter() {
        getRichTextArea().sendKeys(Keys.chord(Keys.META, Keys.RETURN));
    }

    public void typeBackspace() {
        typeBackspace(1);
    }

    public void typeBackspace(int count) {
        typeBackspace(count, false);
    }

    public void typeBackspace(int count, boolean hold) {
        sendKey(Keys.BACK_SPACE, count, hold);
    }

    public void typeLeftArrow() {
        getRichTextArea().sendKeys(Keys.ARROW_LEFT);
    }

    public void typeUpArrow() {
        getRichTextArea().sendKeys(Keys.ARROW_UP);
    }

    public void typeRightArrow() {
        getRichTextArea().sendKeys(Keys.ARROW_RIGHT);
    }

    public void typeDownArrow() {
        getRichTextArea().sendKeys(Keys.ARROW_DOWN);
    }

    public void typeDelete() {
        typeDelete(1);
    }

    public void typeDelete(int count) {
        typeDelete(count, false);
    }

    public void typeDelete(int count, boolean hold) {
        sendKey(Keys.DELETE, count, hold);
    }

    public void typeTab() {
        typeTab(1);
    }

    public void typeTab(int count) {
        sendKey(Keys.TAB, count, false);
    }

    public void typeShiftTab() {
        getRichTextArea().sendKeys(Keys.chord(Keys.SHIFT, Keys.TAB));
    }

    public void typeShiftTab(int count) {
        for (int i = 0; i < count; i++) {
            typeShiftTab();
        }
    }

    public void clickUnorderedListButton() {
        pushToolBarButton("Bullets On/Off");
    }

    public void clickOrderedListButton() {
        pushToolBarButton("Numbering On/Off");
    }

    public void clickIndentButton() {
        pushToolBarButton(TOOLBAR_BUTTON_INDENT_TITLE);
    }

    public boolean isIndentButtonEnabled() {
        return isPushButtonEnabled(TOOLBAR_BUTTON_INDENT_TITLE);
    }

    public void clickOutdentButton() {
        pushToolBarButton(TOOLBAR_BUTTON_OUTDENT_TITLE);
    }

    public boolean isOutdentButtonEnabled() {
        return isPushButtonEnabled(TOOLBAR_BUTTON_OUTDENT_TITLE);
    }

    public void clickBoldButton() {
        pushToolBarButton("Bold (Ctrl+B)");
    }

    public void clickItalicsButton() {
        pushToolBarButton("Italic (Ctrl+I)");
    }

    public void clickUnderlineButton() {
        pushToolBarButton("Underline (Ctrl+U)");
    }

    public void clickStrikethroughButton() {
        pushToolBarButton("Strikethrough");
    }

    public void clickHRButton() {
        pushToolBarButton("Insert Horizontal Ruler");
    }

    public void clickSubscriptButton() {
        pushToolBarButton("Subscript");
    }

    public void clickSuperscriptButton() {
        pushToolBarButton("Superscript");
    }

    public void clickUndoButton() {
        pushToolBarButton(TOOLBAR_BUTTON_UNDO_TITLE);
    }

    public void clickUndoButton(int count) {
        for (int i = 0; i < count; i++) {
            clickUndoButton();
        }
    }

    public void clickRedoButton() {
        pushToolBarButton(TOOLBAR_BUTTON_REDO_TITLE);
    }

    public void clickRedoButton(int count) {
        for (int i = 0; i < count; i++) {
            clickRedoButton();
        }
    }

    public void clickSymbolButton() {
        pushToolBarButton("Insert Custom Character");
    }

    public void clickOfficeImporterButton() {
        pushToolBarButton("Import Office Content");
    }

    public void clickBackToEdit() {
        submit("//input[@type = 'submit' and @value = 'Back To Edit']");
        waitForEditorToLoad();
    }

    public void applyStyle(final String style) {
        // Wait until the given style is not selected (because the tool bar might not be updated).
        new Wait() {
            public boolean until() {
                return getSelenium().isEditable(TOOLBAR_SELECT_STYLE)
                        && (!getSelenium().isSomethingSelected(TOOLBAR_SELECT_STYLE)
                                || !style.equals(getSelenium().getSelectedLabel(TOOLBAR_SELECT_STYLE)));
            }
        }.wait("The specified style, '" + style + "', is already applied!");
        getSelenium().select(TOOLBAR_SELECT_STYLE, style);
    }

    /**
     * Waits for the specified style to be detected.
     * 
     * @param style the expected style
     */
    public void waitForStyleDetected(final String style) {
        new Wait() {
            public boolean until() {
                return getSelenium().isSomethingSelected(TOOLBAR_SELECT_STYLE)
                        && style.equals(getSelenium().getSelectedLabel(TOOLBAR_SELECT_STYLE));
            }
        }.wait("The specified style, '" + style + "', wasn't detected!");
    }

    public void applyStylePlainText() {
        applyStyle("Plain text");
    }

    public void applyStyleTitle1() {
        applyStyle("Title 1");
    }

    public void applyStyleTitle2() {
        applyStyle("Title 2");
    }

    public void applyStyleTitle3() {
        applyStyle("Title 3");
    }

    public void applyStyleTitle4() {
        applyStyle("Title 4");
    }

    public void applyStyleTitle5() {
        applyStyle("Title 5");
    }

    public void applyStyleTitle6() {
        applyStyle("Title 6");
    }

    public void pushButton(String locator) {
        // Can't use : selenium.click(locator);
        // A GWT PushButton is not a standard HTML <input type="submit" ...> or a <button ...>
        // rather it is a styled button constructed from DIV and other HTML tags.
        // Source :
        // http://www.blackpepper.co.uk/black-pepper-blog/Simulating-clicks-on-GWT-push-buttons-with-Selenium-RC.html
        getSelenium().mouseOver(locator);
        getSelenium().mouseDown(locator);
        getSelenium().mouseUp(locator);
        getSelenium().mouseOut(locator);
    }

    /**
     * Pushes the tool bar button with the specified title.
     * 
     * @param title the title of the tool bar button to be pushed
     */
    public void pushToolBarButton(String title) {
        pushButton("//div[@title='" + title + "']");
    }

    public void clickButtonWithText(String buttonText) {
        getSelenium().click("//button[. = \"" + buttonText + "\"]");
    }

    /**
     * Clicks on the menu item with the specified label.
     * 
     * @param menuLabel a {@link String} representing the label of a menu item
     */
    public void clickMenu(String menuLabel) {
        String selector = String.format(MENU_ITEM_BY_LABEL, menuLabel);
        // We select the menu item first.
        getSelenium().mouseOver(selector);
        // And then we click on it.
        getSelenium().click(selector);
    }

    /**
     * Waits for the specified menu to be present.
     * 
     * @param menuLabel the menu label
     */
    public void waitForMenu(String menuLabel) {
        waitForElement(String.format(MENU_ITEM_BY_LABEL, menuLabel));
    }

    /**
     * Closes the menu containing the specified menu item by pressing the escape key.
     * 
     * @param menuLabel a menu item from the menu to be closed
     */
    public void closeMenuContaining(String menuLabel) {
        getSelenium().typeKeys(String.format(MENU_ITEM_BY_LABEL, menuLabel), "\\27");
    }

    /**
     * Switch the WYSIWYG editor by clicking on the "WYSIWYG" tab item and waits for the rich text area to be
     * initialized.
     */
    public void switchToWysiwyg() {
        switchToWysiwyg(true);
    }

    /**
     * Switch the WYSIWYG editor by clicking on the "WYSIWYG" tab item.
     * 
     * @param wait {@code true} to wait for the rich text area to be initialized, {@code false} otherwise
     */
    public void switchToWysiwyg(boolean wait) {
        ensureElementIsNotCoveredByFloatingMenu(By.xpath(WYSIWYG_LOCATOR_FOR_WYSIWYG_TAB));
        getSelenium().click(WYSIWYG_LOCATOR_FOR_WYSIWYG_TAB);
        if (wait) {
            final String enabledToolBarButtonXPath = "//div[contains(@class, 'gwt-ToggleButton') and not(contains(@class, '-disabled'))]";
            getDriver().waitUntilCondition(new ExpectedCondition<Boolean>() {
                @Override
                public Boolean apply(WebDriver input) {
                    // When switching between tabs, it sometimes takes longer for toggle buttons to become enabled. We
                    // need to wait for that before we can properly use the WYSIWYG.
                    return !getSourceTextArea().isEnabled()
                            && getDriver().hasElementWithoutWaiting(By.xpath(enabledToolBarButtonXPath));
                }
            });
        }
    }

    /**
     * Switch the Source editor by clicking on the "Source" tab item and waits for the plain text area to be
     * initialized.
     */
    public void switchToSource() {
        switchToSource(true);
    }

    /**
     * Switch the Source editor by clicking on the "Source" tab item.
     * 
     * @param wait {@code true} to wait for the plain text area to be initialized, {@code false} otherwise
     */
    public void switchToSource(boolean wait) {
        ensureElementIsNotCoveredByFloatingMenu(By.xpath(WYSIWYG_LOCATOR_FOR_SOURCE_TAB));
        getSelenium().click(WYSIWYG_LOCATOR_FOR_SOURCE_TAB);
        if (wait) {
            getDriver().waitUntilCondition(new ExpectedCondition<Boolean>() {
                @Override
                public Boolean apply(WebDriver input) {
                    return getDriver().findElementWithoutWaiting(By.className("xPlainTextEditor")).isEnabled();
                }
            });
            // Focus the source text area.
            getSourceTextArea().sendKeys("");
        }
    }

    /**
     * Types the specified text in the input specified by its title.
     * 
     * @param inputTitle the {@code title} attribute of the {@code} input element to type in
     * @param text the text to type in the input
     */
    public void typeInInput(String inputTitle, String text) {
        getSelenium().type("//input[@title=\"" + inputTitle + "\"]", text);
    }

    /**
     * @param inputTitle the title of the input whose value to return.
     * @return the value of an input specified by its title.
     */
    public String getInputValue(String inputTitle) {
        return getSelenium().getValue("//input[@title=\"" + inputTitle + "\"]");
    }

    public boolean isPushButtonEnabled(String pushButtonTitle) {
        return getDriver().hasElementWithoutWaiting(
                By.xpath("//div[@title='" + pushButtonTitle + "' and @class='gwt-PushButton gwt-PushButton-up']"));
    }

    /**
     * @param toggleButtonTitle the tool tip of a toggle button from the WYSIWYG tool bar
     * @return {@code true} if the specified toggle button is enabled, {@code false} otherwise
     */
    public boolean isToggleButtonEnabled(String toggleButtonTitle) {
        return getDriver().hasElementWithoutWaiting(By.xpath("//div[@title='" + toggleButtonTitle
                + "' and contains(@class, 'gwt-ToggleButton') and not(contains(@class, '-disabled'))]"));
    }

    /**
     * Waits for the specified push button to have the specified state i.e. enabled or disabled.
     * 
     * @param pushButtonTitle identifies the button to wait for
     * @param enabled {@code true} to wait for the specified button to become enabled, {@code false} to wait for it to
     *            become disabled
     */
    public void waitForPushButton(final String pushButtonTitle, final boolean enabled) {
        new Wait() {
            public boolean until() {
                return enabled == isPushButtonEnabled(pushButtonTitle);
            }
        }.wait(pushButtonTitle + " button is not " + (enabled ? "enabled" : "disabled") + "!");
    }

    /**
     * Waits for the specified toggle button be enabled or disabled, based on the given state parameter.
     * 
     * @param toggleButtonTitle identifies the button to wait for
     * @param enabled {@code true} to wait for the specified toggle button to become enabled, {@code false} to wait for
     *            it to become disabled
     */
    public void waitForToggleButton(final String toggleButtonTitle, final boolean enabled) {
        new Wait() {
            public boolean until() {
                return enabled == isToggleButtonEnabled(toggleButtonTitle);
            }
        }.wait(toggleButtonTitle + " button is not " + (enabled ? "enabled" : "disabled") + "!");
    }

    /**
     * Waits until the specified toggle button has the given state. This method is useful to wait until a toggle button
     * from the tool bar is updated.
     * 
     * @param toggleButtonTitle the tool tip of a toggle button
     * @param down {@code true} to wait until the specified toggle button is down, {@code false} to wait until it is up
     */
    public void waitForToggleButtonState(final String toggleButtonTitle, final boolean down) {
        new Wait() {
            public boolean until() {
                return down == isToggleButtonDown(toggleButtonTitle);
            }
        }.wait("The state of the '" + toggleButtonTitle + "' toggle button didn't change!");
    }

    /**
     * @param toggleButtonTitle the tool tip of a toggle button
     * @return {@code true} if the specified toggle button is down, {@code false} otherwise
     */
    public boolean isToggleButtonDown(String toggleButtonTitle) {
        return getDriver().hasElementWithoutWaiting(By.xpath(
                "//div[@title='" + toggleButtonTitle + "' and @class='gwt-ToggleButton gwt-ToggleButton-down']"));
    }

    /**
     * Checks if a menu item is enabled or disabled. Menu items have {@code gwt-MenuItem} CSS class. Disabled menu items
     * have and additional {@code gwt-MenuItem-disabled} CSS class.
     * 
     * @param menuLabel a {@link String} representing the label of a menu item
     * @return {@code true} if the menu with the specified label is enabled, {@code false} otherwise
     */
    public boolean isMenuEnabled(String menuLabel) {
        return getDriver().hasElementWithoutWaiting(
                By.xpath("//td[contains(@class, 'gwt-MenuItem') and not(contains(@class, 'gwt-MenuItem-disabled'))]"
                        + "/div[@class = 'gwt-MenuItemLabel' and . = '" + menuLabel + "']"));
    }

    /**
     * Asserts that the rich text area has the expected inner HTML.
     * 
     * @param expectedHTML the expected inner HTML of the rich text area
     */
    public void assertContent(String expectedHTML) {
        assertEquals(expectedHTML, getRichTextArea().getContent());
    }

    /**
     * Places the caret in the specified container, at the specified offset.
     * 
     * @param containerJSLocator the JavaScript code used to access the container node
     * @param offset the offset within the container node
     */
    public void moveCaret(String containerJSLocator, int offset) {
        StringBuilder script = new StringBuilder();
        script.append("var range = document.createRange();\n");
        script.append("range.setStart(");
        script.append(containerJSLocator);
        script.append(", ");
        script.append(offset);
        script.append(");\n");
        script.append("range.collapse(true);\n");
        script.append("window.getSelection().removeAllRanges();\n");
        script.append("window.getSelection().addRange(range);");
        getRichTextArea().executeScript(script.toString());
        triggerToolbarUpdate();
    }

    /**
     * Selects the content between the specified points in the DOM tree.
     * 
     * @param startContainerJSLocator the node containing the start of the selection
     * @param startOffset the offset within the start container where the selection starts
     * @param endContainerJSLocator the node containing the end of the selection
     * @param endOffset the offset within the end container where the selection ends
     */
    public void select(String startContainerJSLocator, int startOffset, String endContainerJSLocator,
            int endOffset) {
        StringBuilder script = new StringBuilder();
        script.append("var range = document.createRange();\n");
        script.append("range.setStart(");
        script.append(startContainerJSLocator);
        script.append(", ");
        script.append(startOffset);
        script.append(");\n");
        script.append("range.setEnd(");
        script.append(endContainerJSLocator);
        script.append(", ");
        script.append(endOffset);
        script.append(");\n");
        script.append("window.getSelection().removeAllRanges();\n");
        script.append("window.getSelection().addRange(range);\n");
        getRichTextArea().executeScript(script.toString());
        triggerToolbarUpdate();
    }

    /**
     * Selects the specified DOM node.
     * 
     * @param jsLocator a JavaScript locator for the node to be selected
     */
    public void selectNode(String jsLocator) {
        StringBuilder script = new StringBuilder();
        script.append("var range = document.createRange();\n");
        script.append("range.selectNode(");
        script.append(jsLocator);
        script.append(");\n");
        script.append("window.getSelection().removeAllRanges();\n");
        script.append("window.getSelection().addRange(range);\n");
        getRichTextArea().executeScript(script.toString());
        triggerToolbarUpdate();
    }

    /**
     * Selects the contents of the specified DOM node.
     * 
     * @param jsLocator a JavaScript locator for the node whose content are to be selected
     */
    public void selectNodeContents(String jsLocator) {
        StringBuilder script = new StringBuilder();
        script.append("var range = document.createRange();\n");
        script.append("range.selectNodeContents(");
        script.append(jsLocator);
        script.append(");\n");
        script.append("window.getSelection().removeAllRanges();\n");
        script.append("window.getSelection().addRange(range);\n");
        getRichTextArea().executeScript(script.toString());
        triggerToolbarUpdate();
    }

    /**
     * Triggers the wysiwyg toolbar update by typing a key. To be used after programatically setting the selection (with
     * {@link AbstractWysiwygTestCase#select(String, int, String, int)} or
     * {@link AbstractWysiwygTestCase#moveCaret(String, int)}): it will not influence the selection but it will cause
     * the toolbar to update according to the new selection.
     */
    protected void triggerToolbarUpdate() {
        getRichTextArea().sendKeys(Keys.SHIFT);
    }

    /**
     * Wait for a WYSIWYG dialog to close. The test checks for a {@code div} element with {@code xDialogBox} value of
     * {@code class} to not be present.
     */
    public void waitForDialogToClose() {
        getDriver().waitUntilElementDisappears(By.className("xDialogBox"));
    }

    /**
     * Waits until a WYSIWYG modal dialog is fully loaded. While loading, the body of the dialog has the {@code loading}
     * CSS class besides the {@code xDialogBody} one.
     */
    public void waitForDialogToLoad() {
        getDriver().waitUntilElementIsVisible(
                By.xpath("//div[contains(@class, 'xDialogBody') and not(contains(@class, 'loading'))]"));
    }

    /**
     * Close the dialog by clicking the close icon in the top right.
     */
    public void closeDialog() {
        getSelenium().click("//img[contains(@class, \"gwt-Image\") and contains(@class, \"xDialogCloseIcon\")]");
        waitForDialogToClose();
    }

    /**
     * Waits until the WYSIWYG editor detects the bold style on the current selection. The bold style is detected when
     * the associated tool bar button is updated. The update is delayed to increase the typing speed.
     */
    public void waitForBoldDetected(boolean down) {
        waitForToggleButtonState("Bold (Ctrl+B)", down);
    }

    /**
     * Waits until the WYSIWYG editor detects the underline style on the current selection. The underline style is
     * detected when the associated tool bar button is updated. The update is delayed to increase the typing speed.
     */
    public void waitForUnderlineDetected(boolean down) {
        waitForToggleButtonState("Underline (Ctrl+U)", down);
    }

    /**
     * Asserts that the specified error message exists, and the element passed through its XPath locator is marked as in
     * error.
     * 
     * @param errorMessage the expected error message
     * @param fieldXPathLocator the XPath locator of the field which is in error
     */
    public void assertFieldErrorIsPresent(String errorMessage, String fieldXPathLocator) {
        // test that the error field is present through this method because the isVisible stops at first encouter of the
        // matching element and fails if it's not visible. However, multiple matching elements might exist and we're
        // interested in at least one of them visible
        assertTrue(getSelenium()
                .getXpathCount("//*[contains(@class, \"xErrorMsg\") and . = '" + errorMessage + "' and @style='']")
                .intValue() > 0);
        assertTrue(getDriver()
                .hasElementWithoutWaiting(By.xpath(fieldXPathLocator + "[contains(@class, 'xErrorField')]")));
    }

    /**
     * Asserts that the specified error message does not exist and that the field passed through the XPath locator is
     * not in error. Note that this function checks that the passed field is present, but without an error marker.
     * 
     * @param errorMessage the error message
     * @param fieldXPathLocator the XPath locator of the field to check that it's not in error
     */
    public void assertFieldErrorIsNotPresent(String errorMessage, String fieldXPathLocator) {
        assertFalse(
                getSelenium().isVisible("//*[contains(@class, \"xErrorMsg\") and . = \"" + errorMessage + "\"]"));
        assertTrue(getDriver()
                .hasElementWithoutWaiting(By.xpath(fieldXPathLocator + "[not(contains(@class, 'xFieldError'))]")));
    }

    /**
     * Asserts that no error message or field marked as in error is present.
     */
    public void assertFieldErrorIsNotPresent() {
        // no error is visible
        assertFalse(getSelenium().isVisible("//*[contains(@class, \"xErrorMsg\")]"));
        // no field with error markers should be present
        assertFalse(getDriver().hasElementWithoutWaiting(By.className("xFieldError")));
    }

    /**
     * Focuses the rich text area.
     * <p>
     * NOTE: The initial range CAN differ when the browser window is focused from when it isn't! Make sure you place the
     * caret where you want it to be at the beginning of you test and after switching back to WYSIWYG editor.
     */
    protected void focusRichTextArea() {
        getRichTextArea().sendKeys("");
    }

    /**
     * Simulates a blur event on the rich text area. We don't use the blur method because it fails to notify our
     * listeners when the browser window is not focused, preventing us from running the tests in background.
     */
    protected void blurRichTextArea() {
        getDriver().findElement(By.id("xwikidoctitleinput")).sendKeys("");
    }

    /**
     * Inserts a table in place of the current selection or at the caret position, using the default table settings.
     */
    protected void insertTable() {
        openInsertTableDialog();
        getSelenium().click("//button[text()=\"Insert Table\"]");
    }

    /**
     * Opens the insert table dialog.
     */
    protected void openInsertTableDialog() {
        clickMenu("Table");
        clickMenu("Insert Table...");
        waitForDialogToLoad();
    }

    /**
     * @return the text from the source text area
     */
    protected String getSourceText() {
        return getSourceTextArea().getAttribute("value");
    }

    /**
     * Sets the value of the source text area.
     * 
     * @param sourceText the new value for the source text area
     */
    protected void setSourceText(String sourceText) {
        ((JavascriptExecutor) getDriver()).executeScript("arguments[0].value = arguments[1]", getSourceTextArea(),
                sourceText);
    }

    /**
     * Asserts that the source text area has the given value.
     * 
     * @param expectedSourceText the expected value of the source text area
     */
    protected void assertSourceText(String expectedSourceText) {
        assertEquals(expectedSourceText, getSourceText());
    }

    /**
     * Waits for the WYSIWYG editor to load.
     */
    protected void waitForEditorToLoad() {
        final String sourceTabSelected = "//div[@class = 'gwt-TabBarItem gwt-TabBarItem-selected']/div[. = 'Source']";
        final String richTextAreaLoader = "//div[@class = 'xRichTextEditor']//div[@class = 'loading']";
        getDriver().waitUntilCondition(new ExpectedCondition<Boolean>() {
            @Override
            public Boolean apply(WebDriver input) {
                // Either the source tab is present and selected and the plain text area can be edited or the rich text
                // area is not loading (with or without tabs).
                return (getDriver().hasElementWithoutWaiting(By.xpath(sourceTabSelected))
                        && getSourceTextArea().isEnabled())
                        || (getDriver().hasElementWithoutWaiting(By.className("xRichTextEditor"))
                                && !getDriver().hasElementWithoutWaiting(By.xpath(richTextAreaLoader)));
            }
        });
    }

    /**
     * Switches to full screen editing mode.
     */
    protected void clickEditInFullScreen() {
        getSelenium().click("//img[@title = 'Maximize']");
        waitForElement("//div[@class = 'fullScreenWrapper']");
    }

    /**
     * Exists full screen editing mode.
     */
    protected void clickExitFullScreen() {
        getDriver().findElementByXPath("//input[@value = 'Exit Full Screen']").click();
        getDriver().waitUntilElementDisappears(By.className("fullScreenWrapper"));
    }

    /**
     * Creates a new space with the specified name.
     * 
     * @param spaceName the name of the new space to create
     */
    public void createSpace(String spaceName) {
        getSelenium().runScript("window.scrollTo(0, 0)");
        getSelenium().click("//div[@id='tmCreate']//button[contains(@class, 'dropdown-toggle')]");
        clickLinkWithLocator("tmCreateSpace");
        getSelenium().type("name", spaceName);
        clickLinkWithLocator("//input[@value='Create']");
        clickEditSaveAndView();
    }

    /**
     * Begin creating a page in the specified space, with the specified name.
     * <p>
     * NOTE: We don't use the save action URL because it requires special characters in space and page name to be
     * escaped. We use instead the create action URL.
     * 
     * @param spaceName the name of the space where to create the page
     * @param pageName the name of the page to create
     * @see #createPage(String, String, String)
     */
    public void startCreatePage(String spaceName, String pageName) {
        String queryString = "templateprovider=";
        try {
            queryString += "&space=" + URLEncoder.encode(spaceName, "UTF-8");
            queryString += "&page=" + URLEncoder.encode(pageName, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            // Shouldn't happen.
        }
        getSelenium().open(this.getUrl("Main", "WebHome", "create", queryString));
    }

    /**
     * Creates a page in the specified space, with the specified name.
     * <p>
     * NOTE: We overwrite the method from the base class because it creates the new page using the save action URL which
     * requires special characters in space and page name to be escaped. We use instead the create action URL.
     * 
     * @param spaceName the name of the space where to create the page
     * @param pageName the name of the page to create
     * @param content the content of the new page
     * @see AbstractXWikiTestCase#createPage(String, String, String)
     */
    public void createPage(String spaceName, String pageName, String content) {
        startCreatePage(spaceName, pageName);

        String location = getSelenium().getLocation();
        if (location.endsWith("?xpage=docalreadyexists")) {
            open(location.substring(0, location.length() - 23));
            clickEditPageInWysiwyg();
        }
        waitForEditorToLoad();
        switchToSource();
        setSourceText(content);
        clickEditSaveAndView();
    }

    /**
     * Selects the rich text area frame. Selectors are relative to the edited document after calling this method.
     */
    public void selectRichTextAreaFrame() {
        WebDriver driver = getDriver();
        driver.switchTo().frame(driver.findElement(By.className("gwt-RichTextArea"))).switchTo().activeElement();
    }

    /**
     * Selects the top frame.
     */
    public void selectTopFrame() {
        getSelenium().selectFrame("relative=top");
    }
}