/*
* (C) Copyright 2016 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* <a href="mailto:grenard@nuxeo.com">Guillaume</a>
* `Yannis JULIENNE
*/
package org.nuxeo.functionaltests.forms;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.functionaltests.AbstractTest;
import org.nuxeo.functionaltests.AjaxRequestManager;
import org.nuxeo.functionaltests.Locator;
import org.nuxeo.functionaltests.fragment.WebFragmentImpl;
import org.nuxeo.functionaltests.pages.search.SearchPage;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.FluentWait;
import org.openqa.selenium.support.ui.Wait;
import com.gargoylesoftware.htmlunit.ElementNotFoundException;
import com.google.common.base.Function;
/**
* Convenient class to handle a select2Widget.
*
* @since 5.7.3
*/
public class Select2WidgetElement extends WebFragmentImpl {
private static class Select2Wait implements Function<WebElement, Boolean> {
@Override
public Boolean apply(WebElement element) {
boolean result = !element.getAttribute("class").contains(S2_CSS_ACTIVE_CLASS);
return result;
}
}
private static final Log log = LogFactory.getLog(Select2WidgetElement.class);
private static final String S2_CSS_ACTIVE_CLASS = "select2-active";
private static final String S2_MULTIPLE_CURRENT_SELECTION_XPATH = "ul[@class='select2-choices']/li[@class='select2-search-choice']";
private final static String S2_MULTIPLE_INPUT_XPATH = "ul/li/input";
private static final String S2_SINGLE_CURRENT_SELECTION_XPATH = "a[@class='select2-choice']/span[@class='select2-chosen']";
private final static String S2_SINGLE_INPUT_XPATH = "//*[@id='select2-drop']/div/input";
private static final String S2_SUGGEST_RESULT_XPATH = "//*[@id='select2-drop']//li[contains(@class,'select2-result-selectable')]/div";
/**
* Select2 loading timeout in seconds.
*/
private static final int SELECT2_LOADING_TIMEOUT = 20;
protected boolean multiple = false;
/**
* Constructor.
*
* @param driver the driver
* @param id the id of the widget
* @since 7.1
*/
public Select2WidgetElement(WebDriver driver, String id) {
this(driver, driver.findElement(By.id(id)));
}
/**
* Constructor.
*
* @param driver the driver
* @param by the by locator of the widget
*/
public Select2WidgetElement(WebDriver driver, WebElement element) {
super(driver, element);
}
/**
* Constructor.
*
* @param driver the driver
* @param by the by locator of the widget
* @param multiple whether the widget can have multiple values
*/
public Select2WidgetElement(final WebDriver driver, WebElement element, final boolean multiple) {
this(driver, element);
this.multiple = multiple;
}
/**
* @since 5.9.3
*/
public WebElement getSelectedValue() {
if (multiple) {
throw new UnsupportedOperationException("The select2 is multiple and has multiple selected values");
}
return element.findElement(By.xpath(S2_SINGLE_CURRENT_SELECTION_XPATH));
}
/**
* @since 5.9.3
*/
public List<WebElement> getSelectedValues() {
if (!multiple) {
throw new UnsupportedOperationException(
"The select2 is not multiple and can't have multiple selected values");
}
return element.findElements(By.xpath(S2_MULTIPLE_CURRENT_SELECTION_XPATH));
}
/**
* @since 5.9.3
*/
protected String getSubmittedValue() {
String eltId = element.getAttribute("id");
String submittedEltId = element.getAttribute("id").substring("s2id_".length(), eltId.length());
return driver.findElement(By.id(submittedEltId)).getAttribute("value");
}
public List<WebElement> getSuggestedEntries() {
try {
return driver.findElements(By.xpath(S2_SUGGEST_RESULT_XPATH));
} catch (NoSuchElementException e) {
return null;
}
}
/**
* @since 8.1
*/
public void removeSelection() {
if (multiple) {
throw new UnsupportedOperationException("The select2 is multiple, use #removeSelection(value) instead");
}
element.findElement(By.className("select2-search-choice-close")).click();
}
/**
* @since 5.9.3
*/
public void removeFromSelection(final String displayedText) {
if (!multiple) {
throw new UnsupportedOperationException("The select2 is not multiple, use #removeSelection instead");
}
final String submittedValueBefore = getSubmittedValue();
boolean found = false;
for (WebElement el : getSelectedValues()) {
if (el.getText().equals(displayedText)) {
Locator.waitUntilEnabledAndClick(el.findElement(By.xpath("a[@class='select2-search-choice-close']")));
found = true;
}
}
if (found) {
Locator.waitUntilGivenFunction(new Function<WebDriver, Boolean>() {
@Override
public Boolean apply(WebDriver driver) {
return !submittedValueBefore.equals(getSubmittedValue());
}
});
} else {
throw new ElementNotFoundException("remove link for select2 '" + displayedText + "' item", "", "");
}
}
/**
* Select a single value.
*
* @param value the value to be selected
* @since 5.7.3
*/
public void selectValue(final String value) {
selectValue(value, false, false);
}
/**
* @since 7.1
*/
public void selectValue(final String value, final boolean wait4A4J) {
selectValue(value, wait4A4J, false);
}
/**
* Select given value, waiting for JSF ajax requests or not, and typing the whole suggestion or not (use false speed
* up selection when exactly one suggestion is found, use true when creating new suggestions).
*
* @param value string typed in the suggest box
* @param wait4A4J use true if request is triggering JSF ajax calls
* @param typeAll use false speed up selection when exactly one suggestion is found, use true when creating new
* suggestions.
* @since 7.10
*/
public void selectValue(final String value, final boolean wait4A4J, final boolean typeAll) {
selectValue(value, wait4A4J, typeAll, true);
}
public void selectValue(final String value, final boolean wait4A4J, final boolean typeAll, boolean click) {
if (click) {
clickSelect2Field();
}
WebElement suggestInput = getSuggestInput();
int nbSuggested = Integer.MAX_VALUE;
char c;
for (int i = 0; i < value.length(); i++) {
c = value.charAt(i);
suggestInput.sendKeys(c + "");
waitSelect2();
if (i >= 2) {
if (getSuggestedEntries().size() > nbSuggested) {
throw new IllegalArgumentException(
"More suggestions than expected for " + element.getAttribute("id"));
}
nbSuggested = getSuggestedEntries().size();
if (!typeAll && nbSuggested == 1) {
break;
}
}
}
waitSelect2();
List<WebElement> suggestions = getSuggestedEntries();
if (suggestions == null || suggestions.isEmpty()) {
log.warn("Suggestion for element " + element.getAttribute("id") + " returned no result for value '" + value
+ "'.");
return;
}
WebElement suggestion = suggestions.get(0);
if (suggestions.size() > 1) {
log.warn("Suggestion for element " + element.getAttribute("id") + " returned more than 1 result for value '"
+ value + "', the first suggestion will be selected : " + suggestion.getText());
}
AjaxRequestManager arm = new AjaxRequestManager(driver);
if (wait4A4J) {
arm.watchAjaxRequests();
}
try {
suggestion.click();
} catch (StaleElementReferenceException e) {
suggestion = driver.findElement(By.xpath(S2_SUGGEST_RESULT_XPATH));
suggestion.click();
}
if (wait4A4J) {
arm.waitForAjaxRequests();
}
}
/**
* Select multiple values.
*
* @param values the values
* @since 5.7.3
*/
public void selectValues(final String[] values) {
boolean click = true;
for (String value : values) {
// avoid clicking again when setting multiple values, to prevent accidental deletion of previously added
// element
selectValue(value, false, false, click);
click = false;
}
}
/**
* Type a value in the select2 and return the suggested entries.
*
* @param value The value to type in the select2.
* @return The suggested values for the parameter.
* @since 6.0
*/
public List<WebElement> typeAndGetResult(final String value) {
clickSelect2Field();
WebElement suggestInput = getSuggestInput();
suggestInput.sendKeys(value);
try {
waitSelect2();
} catch (TimeoutException e) {
log.warn("Suggestion timed out with input : " + value + ". Let's try with next letter.");
}
return getSuggestedEntries();
}
/**
* Click on the select2 field.
*
* @since 6.0
*/
public void clickSelect2Field() {
WebElement select2Field = null;
if (multiple) {
select2Field = element;
} else {
select2Field = element.findElement(By.xpath("a[contains(@class,'select2-choice')]"));
}
Locator.waitUntilEnabled(select2Field);
Locator.scrollToElement(select2Field);
select2Field.click();
}
/**
* @return The suggest input element.
* @since 6.0
*/
private WebElement getSuggestInput() {
WebElement suggestInput = null;
if (multiple) {
suggestInput = element.findElement(By.xpath("ul/li[@class='select2-search-field']/input"));
} else {
suggestInput = driver.findElement(By.xpath(S2_SINGLE_INPUT_XPATH));
}
return suggestInput;
}
/**
* Do a wait on the select2 field.
*
* @throws TimeoutException
* @since 6.0
*/
private void waitSelect2() throws TimeoutException {
Wait<WebElement> wait = new FluentWait<WebElement>(
!multiple ? driver.findElement(By.xpath(S2_SINGLE_INPUT_XPATH))
: element.findElement(By.xpath(S2_MULTIPLE_INPUT_XPATH))).withTimeout(SELECT2_LOADING_TIMEOUT,
TimeUnit.SECONDS).pollingEvery(100, TimeUnit.MILLISECONDS).ignoring(
NoSuchElementException.class);
Function<WebElement, Boolean> s2WaitFunction = new Select2Wait();
wait.until(s2WaitFunction);
}
/**
* Clear the input of the select2.
*
* @since 6.0
*/
public void clearSuggestInput() {
WebElement suggestInput = null;
if (multiple) {
suggestInput = driver.findElement(By.xpath("//ul/li[@class='select2-search-field']/input"));
} else {
suggestInput = driver.findElement(By.xpath(S2_SINGLE_INPUT_XPATH));
}
if (suggestInput != null) {
suggestInput.clear();
}
}
/**
* Type a value in the select2 and then simulate the enter key.
*
* @since 6.0
*/
public SearchPage typeValueAndTypeEnter(String value) {
clickSelect2Field();
WebElement suggestInput = getSuggestInput();
suggestInput.sendKeys(value);
try {
waitSelect2();
} catch (TimeoutException e) {
log.warn("Suggestion timed out with input : " + value + ". Let's try with next letter.");
}
suggestInput.sendKeys(Keys.RETURN);
return AbstractTest.asPage(SearchPage.class);
}
/**
* @since 8.3
*/
public void hideSuggestionsByEscapeKey() {
getSuggestInput().sendKeys(Keys.ESCAPE);
}
}