All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.loyada.jdollarx.highlevelapi.Inputs Maven / Gradle / Ivy

There is a newer version: 1.5.5
Show newest version
package com.github.loyada.jdollarx.highlevelapi;

import com.github.loyada.jdollarx.ElementProperty;
import com.github.loyada.jdollarx.InBrowser;
import com.github.loyada.jdollarx.Operations.OperationFailedException;
import com.github.loyada.jdollarx.Path;
import com.google.common.base.Strings;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebElement;

import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static com.github.loyada.jdollarx.BasicPath.*;
import static com.github.loyada.jdollarx.ElementProperties.contains;
import static com.github.loyada.jdollarx.ElementProperties.hasAggregatedTextEqualTo;
import static com.github.loyada.jdollarx.ElementProperties.hasId;
import static com.github.loyada.jdollarx.ElementProperties.not;
import static com.github.loyada.jdollarx.HighLevelPaths.hasType;
import static com.github.loyada.jdollarx.NPath.exactly;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

/**
 * High-level API to define and interact with various input elements.
 * High level API's are not optimized. A definition of an element may interact with the browser
 * to understand the structure of the DOM.
 */
public final class Inputs {
    static final int MAX_NUM_OF_TRIES_TO_CLEAR_INPUT = 20;
    public static int wait_time_for_dropdown_to_reach_stable_state_ms = 100;
    private Inputs() {}

    /**
     * A lazy way to find an input based on the label. Mote that unlike
     * It looks for a label element that has an ID. If it finds one, it returns
     * a Path to an input with that ID. Otherwise it returns a Path to an input
     * inside the label element.
     * @param browser the browser
     * @param labelText the label to look for
     * @return a Path to the input, on a best effort basis
     */
    public static Path inputForLabel(InBrowser browser, String labelText) {
        Path myLabel = label.that(hasAggregatedTextEqualTo(labelText.trim()));
        String theId = browser.find(myLabel).getAttribute("for");
        if (isNullOrEmpty(theId)) {
            return input.inside(myLabel);
        }
        return input.that(hasId(theId));
    }

    /**
     * A lazy way to find an input based on the label. Mote that unlike
     * It looks for a label element that has an ID. If it finds one, it returns
     * a Path to an input with that ID. Otherwise it returns a Path to an input
     * inside the label element.
     * @param browser the browser
     * @param labelText the label to look for
     * @param properties optional additional properties
     * @return a Path to the input, on a best effort basis
     */
    public static Path inputForLabel(InBrowser browser, String labelText, ElementProperty... properties) {
        Path myLabel = label.that(hasAggregatedTextEqualTo(labelText.trim()));
        String theId = browser.find(myLabel).getAttribute("for");
        if (isNullOrEmpty(theId)) {
            return input.inside(myLabel).and(properties);
        }
        return input.that(hasId(theId)).and(properties);
    }

    /**
     * A generic, reasonable guess of an input field in a form.
     * @param fieldName - the field before the input
     * @return a Path for the input field
     */
    public static Path genericFormInputAfterField(String fieldName) {
        Path fieldNameEl = element.that(hasAggregatedTextEqualTo(fieldName)).and(not(contains(input)));

        // note: we ensure the ancestor is not too high up in the DOM hierarchy
        Path ancestor = element.afterSibling(fieldNameEl).that(
                contains(exactly(1).occurrencesOf(input)));
        return (input.inside(ancestor)).or(
                input.afterSibling(fieldNameEl))
                .describedBy(String.format("input following field \"%s\"", fieldName));
    }

    /**
     * A generic, reasonable guess of an input field in a form.
     * @param fieldName - the field before the input
     * @param container - definition of a form/section of the form that contains the input
     * @return a Path for the input field
     */
    public static Path genericFormInputAfterField(String fieldName, Path container) {
        Path fieldNameEl = element.that(hasAggregatedTextEqualTo(fieldName)).and(not(contains(input)));

        // note: we ensure the ancestor is not too high up in the DOM hierarchy
        Path ancestor = element.inside(container).afterSibling(fieldNameEl).that(
                contains(exactly(1).occurrencesOf(input)));
        return (input.inside(ancestor)).or(
                        input.afterSibling(fieldNameEl))
                .describedBy(String.format("input following field \"%s\"", fieldName));
    }

    /**
     * A generic, reasonable guess of an input field in a form.
     * @param fieldName - the field before the input
     * @return a Path for the input field
     */
    public static Path genericFormInputBeforeField(String fieldName) {
        Path fieldNameEl = element.that(hasAggregatedTextEqualTo(fieldName));

        // note: we ensure the ancestor is not too high up in the DOM hierarchy
        Path ancestor = element.immediatelyBeforeSibling(fieldNameEl).that(
                contains(exactly(1).occurrencesOf(input)));
        return lastOccurrenceOf((input.inside(ancestor)).or(
                input.immediatelyBeforeSibling(fieldNameEl)))
                .describedBy(String.format("input before field \"%s\"", fieldName));
    }

    /**
     * A generic, reasonable guess of an input field in a form.
     * @param fieldName - the field before the input
     * @param container - definition of a form/section of the form that contains the input
     * @return a Path for the input field
     */
    public static Path genericFormInputBeforeField(String fieldName, Path container) {
        Path fieldNameEl = element.that(hasAggregatedTextEqualTo(fieldName));

        // note: we ensure the ancestor is not too high up in the DOM hierarchy
        Path ancestor = element.inside(container).immediatelyBeforeSibling(fieldNameEl).that(
                contains(exactly(1).occurrencesOf(input)));
        return lastOccurrenceOf((input.inside(ancestor)).or(
                input.immediatelyBeforeSibling(fieldNameEl)))
                .describedBy(String.format("input before field \"%s\"", fieldName));
    }

    /**
     * Input followed by text that does not have its on label element.
     * @param text the text following the input
     * @return a Path to the input element
     */
    public static Path inputFollowedByUnlabeledText(String text) {
        return input.immediatelyBeforeSibling(textNode(text));
    }

    /**
     * Takes an input element and returns such an input of type checkbox.
     * @param inp the input element
     * @return a Path to the input
     */
    public static Path checkboxType(Path inp) {
        return inp.that(hasType("checkbox"));
    }

    /**
     * Takes an input element and returns such an input of type radio.
     * @param inp the input element
     * @return a Path to the input
     */
    public static Path radioType(Path inp) {
        return inp.that(hasType("radio"));
    }

    /**
     * Clear operation on an input element. This enforces successful clear. If it fails, it throws
     * and exception.
     * @param browser the browser
     * @param field the input element
     */
    public static void clearInput(InBrowser browser, Path field) throws OperationFailedException {
        Inputs.clearInputInternal(browser, field, true);
    }

    /**
     * Clear operation on an input element. This enforces successful clear. If it fails, it throws
     * and exception.
     * @param browser the browser
     * @param field the input element
     */
    public static void clearInput(InBrowser browser, WebElement field) throws OperationFailedException {
        Inputs.clearInputInternal(browser, field);
    }

    /**
     * Quickly try to clear input, assuming it is not too long. This is not guaranteed to work.
     * @param browser the browser
     * @param field the input element
     */
    public static void quickTryClearInput(InBrowser browser, Path field) throws OperationFailedException {
        int MAX_LENGTH = 100;
        sendDeletionKeys(browser, MAX_LENGTH, field);
    }

    /**
     * Clear operation on an input element, but does not enforces a complete clear.
     * In other words, it will try to clear as much as it can, and not fail if it can clear it completely.
     * @param browser the browser
     * @param field the input element
     */
    public static void clearInputNonStrict(InBrowser browser, Path field) throws OperationFailedException {
        Inputs.clearInputInternal(browser, field, false);
    }


    private static void sendDeletionKeys(InBrowser browser, int length, Path field) throws OperationFailedException {
        String keysBack = IntStream.range(0, length)
                .mapToObj(i-> Keys.BACK_SPACE)
                .collect(Collectors.joining());
        String keysDel = IntStream.range(0, length)
            .mapToObj(i-> Keys.DELETE)
                .collect(Collectors.joining());
        browser.sendKeys(keysBack+keysDel).to(field);
    }

    private static void sendDeletionKeys(InBrowser browser, int length, WebElement field) throws OperationFailedException {
        String keysBack = IntStream.range(0, length)
                .mapToObj(i-> Keys.BACK_SPACE)
                .collect(Collectors.joining());
        String keysDel = IntStream.range(0, length)
                .mapToObj(i-> Keys.DELETE)
                .collect(Collectors.joining());
        browser.sendKeys(keysBack+keysDel).to(field);
    }

    private static void clearInputInternal(InBrowser browser, Path field, boolean enforce)
            throws OperationFailedException {
        // sometimes clear() works. Try that first.
        String value = browser.find(field).getAttribute("value");
        if (Strings.isNullOrEmpty(value)) {
            return;
        }
        browser.find(field).clear();

        value = browser.find(field).getAttribute("value");
        if (!Strings.isNullOrEmpty(value)) {
            sendDeletionKeys(browser, value.length(), field);
        }

        value = browser.find(field).getAttribute("value");
        if (Strings.isNullOrEmpty(value)) {
            return;
        }
        // previous clears don't work in all cases. If they didn't, use the expensive
        // method of sending one backspace at a time, until we guarantee it is empty
        boolean anythingLeft = true;
        int num_of_tries_left = enforce ? MAX_NUM_OF_TRIES_TO_CLEAR_INPUT : 3;
        int lastLength = Integer.MAX_VALUE;
        while(anythingLeft && num_of_tries_left>0) {
            num_of_tries_left-=1;
            String currentValue = browser.find(field).getAttribute("value");
            if (enforce && (currentValue.length()>lastLength || num_of_tries_left<1))
                throw new OperationFailedException("clearing input does not work. Is this an" +
                        " autocomplete or another custom input?");
            lastLength = currentValue.length();

            if (!Strings.isNullOrEmpty(currentValue)) {
                for (int i = 0; i < currentValue.length(); i++) {
                    browser.sendKeys(Keys.BACK_SPACE).to(field);
                }
                for (int i = 0; i < currentValue.length(); i++) {
                    browser.sendKeys(Keys.DELETE).to(field);
                }
            }
            anythingLeft = currentValue.length() > 0;
        }
    }

    private static void clearInputInternal(InBrowser browser, WebElement field)
            throws OperationFailedException {
        // sometimes clear() works. Try that first.
        String value = field.getAttribute("value");
        if (Strings.isNullOrEmpty(value)) {
            return;
        }
        field.clear();

        value = field.getAttribute("value");
        if (!Strings.isNullOrEmpty(value)) {
            sendDeletionKeys(browser, value.length(), field);
        }

        value = field.getAttribute("value");
        if (Strings.isNullOrEmpty(value)) {
            return;
        }
        // previous clears don't work in all cases. If they didn't, use the expensive
        // method of sending one backspace at a time, until we guarantee it is empty
        boolean anythingLeft = true;
        int num_of_tries_left = MAX_NUM_OF_TRIES_TO_CLEAR_INPUT;
        int lastLength = Integer.MAX_VALUE;
        while(anythingLeft && num_of_tries_left>0) {
            num_of_tries_left-=1;
            String currentValue = field.getAttribute("value");
            if (currentValue.length()>lastLength || num_of_tries_left<1)
                throw new OperationFailedException("clearing input does not work. Is this an" +
                        " autocomplete or another custom input?");
            lastLength = currentValue.length();

            if (!Strings.isNullOrEmpty(currentValue)) {
                for (int i = 0; i < currentValue.length(); i++) {
                    browser.sendKeys(Keys.BACK_SPACE).to(field);
                }
                for (int i = 0; i < currentValue.length(); i++) {
                    browser.sendKeys(Keys.DELETE).to(field);
                }
            }
            anythingLeft = currentValue.length() > 0;
        }
    }



    /**
     * Perform a selection of an option in a select element.
     * It expects to find the label element with the given text before the select element
     * @param browser the browser
     * @param labelText The text of the select label
     * @param option The option text
     * @return the Path of the select element
     */
    public static Path selectInFieldWithLabel(InBrowser browser, String labelText, String option) {
        Path selector = select.after(label.withText(labelText));
        browser.clickOn(selector);
        browser.getSelect(selector).selectByVisibleText(option);
        return selector;
    }

    /**
     * Change input value: clear it and then enter another text in it
     * @param browser the browser
     * @param field Path to the input field
     * @param text the text to enter in the input field
     * @throws OperationFailedException failed to perform the operation
     */
    public static void changeInputValue(InBrowser browser, Path field, String text)
            throws OperationFailedException {
        browser.clickOn(field);
        clearInput(browser, field);
        browser.sendKeys(text).to(field);
    }

    /**
     * Change input value: clear it and then enter another text in it. This assumes the input element is not
     * replaced by another element, as with that assumption, it is supposed to be slightly faster.
     * @param browser the browser
     * @param field Path to the input field
     * @param text the text to enter in the input field
     * @throws OperationFailedException failed to perform the operation
     */
    public static void changeInputValueAssumingElementIsNotReplaced(InBrowser browser, Path field, String text)
            throws OperationFailedException {
        WebElement el = browser.clickOn(field);
        clearInput(browser, el);
        browser.sendKeys(text).to(el);
    }

    /**
     * Change input value: Try to clear it first  and then enter another text in it.
     * Clearing the input focuses on speed and is not guaranteed.
     * @param browser the browser
     * @param field Path to the input field
     * @param text the text to enter in the input field
     * @throws OperationFailedException failed to perform the operation
     */
    public static void changeInputValueWithQuickApproximateDeletion(InBrowser browser, Path field, String text)
            throws OperationFailedException {
        browser.clickOn(field);
        quickTryClearInput(browser, field);
        browser.sendKeys(text).to(field);
    }

    /**
     * Change input value: clear it and then enter another text in it. This variation does not ensure cleaning
     * up the input, but tries to clean as much as it can.
     * @param browser the browser
     * @param field Path to the input field
     * @param text the text to enter in the input field
     * @throws OperationFailedException failed to perform the operation
     */
    public static void changeInputValueNonStrictClearing(InBrowser browser, Path field, String text)
            throws OperationFailedException {
        browser.clickOn(field);
        clearInputNonStrict(browser, field);
        browser.sendKeys(text).to(field);
    }

    /**
     * Similar to changeInputValue, but adds an ENTER after setting the value of the input
     * @param browser the browser
     * @param field Path to the input field
     * @param text the text to enter in the input field
     * @throws OperationFailedException failed to perform the operation
     */
    public static void changeInputValueWithEnter(InBrowser browser, Path field, String text)
            throws OperationFailedException {
       changeInputValue(browser, field, text);
        browser.sendKeys(Keys.ENTER).to(field);
    }

    /**
     * Similar to changeInputValue, but adds an ENTER after setting the value of the input
     * @param browser the browser
     * @param field Path to the input field
     * @param text the text to enter in the input field
     * @throws OperationFailedException failed to perform the operation
     */
    public static void changeInputValueWithApproximateDeletionWithEnter(InBrowser browser, Path field, String text)
            throws OperationFailedException {
        changeInputValueWithQuickApproximateDeletion(browser, field, text);
        browser.sendKeys(Keys.ENTER).to(field);
    }

    /**
     * Similar to changeInputValueNonStrictClearing, but adds an ENTER after setting the value of the input
     * @param browser the browser
     * @param field Path to the input field
     * @param text the text to enter in the input field
     * @throws OperationFailedException failed to perform the operation
     */
    public static void changeInputValueWithEnterNonStrictClearing(InBrowser browser, Path field, String text)
            throws OperationFailedException {
        changeInputValueNonStrictClearing(browser, field, text);
        browser.sendKeys(Keys.ENTER).to(field);
    }

    /**
     *
     */
    public static void selectDropdownOption(InBrowser browser, Path dropdownContent, Path myOption) {
        Path dropdown = element.parentOf(dropdownContent);
        Predicate isVisible = el -> {
            WebElement visibleContent = browser.find(dropdown);
            int bottomOfList = visibleContent.getSize().height + visibleContent.getLocation().getY();
            return el.isDisplayed() && (el.getLocation().y  + el.getSize().height <= bottomOfList);
        };

        browser.scrollElement(dropdown).toTopCorner();

        long timeoutInMillisec = browser.getImplicitTimeoutInMillisec();
        browser.setImplicitTimeout(10, MILLISECONDS);
        try {
            browser.scrollElementWithStepOverride(dropdown, 50).downUntilPredicate(myOption, isVisible);
        } finally {
            browser.setImplicitTimeout((int) timeoutInMillisec, MILLISECONDS);
        }

        // Unfortunately we have to give time for the rendering to stabilize
        // otherwise, because selenium click is non-atomic, the content can switch between
        // finding the element and executing a click.
        try {
            Thread.sleep(wait_time_for_dropdown_to_reach_stable_state_ms);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        browser.clickOn(myOption);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy