001/*
002 * (C) Copyright 2013 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     <a href="mailto:grenard@nuxeo.com">Guillaume</a>
016 */
017package org.nuxeo.functionaltests.forms;
018
019import java.util.List;
020import java.util.concurrent.TimeUnit;
021
022import org.apache.commons.logging.Log;
023import org.apache.commons.logging.LogFactory;
024import org.nuxeo.functionaltests.AbstractTest;
025import org.nuxeo.functionaltests.AjaxRequestManager;
026import org.nuxeo.functionaltests.Locator;
027import org.nuxeo.functionaltests.fragment.WebFragmentImpl;
028import org.nuxeo.functionaltests.pages.search.SearchPage;
029import org.openqa.selenium.By;
030import org.openqa.selenium.Keys;
031import org.openqa.selenium.NoSuchElementException;
032import org.openqa.selenium.StaleElementReferenceException;
033import org.openqa.selenium.TimeoutException;
034import org.openqa.selenium.WebDriver;
035import org.openqa.selenium.WebElement;
036import org.openqa.selenium.support.ui.FluentWait;
037import org.openqa.selenium.support.ui.Wait;
038
039import com.gargoylesoftware.htmlunit.ElementNotFoundException;
040import com.google.common.base.Function;
041
042/**
043 * Convenient class to handle a select2Widget.
044 *
045 * @since 5.7.3
046 */
047public class Select2WidgetElement extends WebFragmentImpl {
048
049    private static class Select2Wait implements Function<WebElement, Boolean> {
050
051        @Override
052        public Boolean apply(WebElement element) {
053            boolean result = !element.getAttribute("class").contains(S2_CSS_ACTIVE_CLASS);
054            return result;
055        }
056    }
057
058    private static final Log log = LogFactory.getLog(Select2WidgetElement.class);
059
060    private static final String S2_CSS_ACTIVE_CLASS = "select2-active";
061
062    private static final String S2_MULTIPLE_CURRENT_SELECTION_XPATH = "ul[@class='select2-choices']/li[@class='select2-search-choice']";
063
064    private final static String S2_MULTIPLE_INPUT_XPATH = "ul/li/input";
065
066    private static final String S2_SINGLE_CURRENT_SELECTION_XPATH = "a[@class='select2-choice']/span[@class='select2-chosen']";
067
068    private final static String S2_SINGLE_INPUT_XPATH = "//*[@id='select2-drop']/div/input";
069
070    private static final String S2_SUGGEST_RESULT_XPATH = "//*[@id='select2-drop']//li[contains(@class,'select2-result-selectable')]/div";
071
072    /**
073     * Select2 loading timeout in seconds.
074     */
075    private static final int SELECT2_LOADING_TIMEOUT = 20;
076
077    protected boolean mutliple = false;
078
079    /**
080     * Constructor.
081     *
082     * @param driver the driver
083     * @param id the id of the widget
084     * @since 7.1
085     */
086    public Select2WidgetElement(WebDriver driver, String id) {
087        this(driver, driver.findElement(By.id(id)));
088    }
089
090    /**
091     * Constructor.
092     *
093     * @param driver the driver
094     * @param by the by locator of the widget
095     */
096    public Select2WidgetElement(WebDriver driver, WebElement element) {
097        super(driver, element);
098    }
099
100    /**
101     * Constructor.
102     *
103     * @param driver the driver
104     * @param by the by locator of the widget
105     * @param multiple whether the widget can have multiple values
106     */
107    public Select2WidgetElement(final WebDriver driver, WebElement element, final boolean multiple) {
108        this(driver, element);
109        mutliple = multiple;
110    }
111
112    /**
113     * @since 5.9.3
114     */
115    public WebElement getSelectedValue() {
116        if (mutliple) {
117            throw new UnsupportedOperationException("The select2 is multiple and has multiple selected values");
118        }
119        return element.findElement(By.xpath(S2_SINGLE_CURRENT_SELECTION_XPATH));
120    }
121
122    /**
123     * @since 5.9.3
124     */
125    public List<WebElement> getSelectedValues() {
126        if (!mutliple) {
127            throw new UnsupportedOperationException(
128                    "The select2 is not multiple and can't have multiple selected values");
129        }
130        return element.findElements(By.xpath(S2_MULTIPLE_CURRENT_SELECTION_XPATH));
131    }
132
133    /**
134     * @since 5.9.3
135     */
136    protected String getSubmittedValue() {
137        String eltId = element.getAttribute("id");
138        String submittedEltId = element.getAttribute("id").substring("s2id_".length(), eltId.length());
139        return driver.findElement(By.id(submittedEltId)).getAttribute("value");
140    }
141
142    public List<WebElement> getSuggestedEntries() {
143        try {
144            return driver.findElements(By.xpath(S2_SUGGEST_RESULT_XPATH));
145        } catch (NoSuchElementException e) {
146            return null;
147        }
148    }
149
150    /**
151     * @since 5.9.3
152     */
153    public void removeFromSelection(final String displayedText) {
154        if (!mutliple) {
155            throw new UnsupportedOperationException("The select2 is not multiple and you can't remove a specific value");
156        }
157        final String submittedValueBefore = getSubmittedValue();
158        boolean found = false;
159        for (WebElement el : getSelectedValues()) {
160            if (el.getText().equals(displayedText)) {
161                el.findElement(By.xpath("a[@class='select2-search-choice-close']")).click();
162                found = true;
163            }
164        }
165        if (found) {
166            Locator.waitUntilGivenFunction(new Function<WebDriver, Boolean>() {
167                @Override
168                public Boolean apply(WebDriver driver) {
169                    return !submittedValueBefore.equals(getSubmittedValue());
170                }
171            });
172        } else {
173            throw new ElementNotFoundException("remove link for select2 '" + displayedText + "' item", "", "");
174        }
175    }
176
177    /**
178     * Select a single value.
179     *
180     * @param value the value to be selected
181     * @since 5.7.3
182     */
183    public void selectValue(final String value) {
184        selectValue(value, false);
185    }
186
187    /**
188     * @since 7.1
189     */
190    public void selectValue(final String value, final boolean wait4A4J) {
191        clickOnSelect2Field();
192
193        WebElement suggestInput = getSuggestInput();
194
195        char c;
196        for (int i = 0; i < value.length(); i++) {
197            c = value.charAt(i);
198            suggestInput.sendKeys(c + "");
199        }
200
201        waitSelect2();
202
203        if (getSuggestedEntries() != null && getSuggestedEntries().size() > 1) {
204            log.warn("Suggestion for element " + element.getAttribute("id")
205                    + " returned more than 1 result, the first suggestion will be selected : "
206                    + getSuggestedEntries().get(0).getText());
207        }
208
209        WebElement suggestion = driver.findElement(By.xpath(S2_SUGGEST_RESULT_XPATH));
210        AjaxRequestManager arm = new AjaxRequestManager(driver);;
211        if (wait4A4J) {
212            arm.watchAjaxRequests();
213        }
214        try {
215            suggestion.click();
216        } catch (StaleElementReferenceException e) {
217            suggestion = driver.findElement(By.xpath(S2_SUGGEST_RESULT_XPATH));
218            suggestion.click();
219        }
220        if (wait4A4J) {
221            arm.waitForAjaxRequests();
222        }
223    }
224
225    /**
226     * Select multiple values.
227     *
228     * @param values the values
229     * @since 5.7.3
230     */
231    public void selectValues(final String[] values) {
232        for (String value : values) {
233            selectValue(value);
234        }
235    }
236
237    /**
238     * Type a value in the select2 and return the suggested entries.
239     *
240     * @param value The value to type in the select2.
241     * @return The suggested values for the parameter.
242     * @since 6.0
243     */
244    public List<WebElement> typeAndGetResult(final String value) {
245
246        clickOnSelect2Field();
247
248        WebElement suggestInput = getSuggestInput();
249
250        suggestInput.sendKeys(value);
251        try {
252            waitSelect2();
253        } catch (TimeoutException e) {
254            log.warn("Suggestion timed out with input : " + value + ". Let's try with next letter.");
255        }
256
257        return getSuggestedEntries();
258    }
259
260    /**
261     * Click on the select2 field.
262     *
263     * @since 6.0
264     */
265    protected void clickOnSelect2Field() {
266        WebElement select2Field = null;
267        if (mutliple) {
268            select2Field = element;
269        } else {
270            select2Field = element.findElement(By.xpath("a[contains(@class,'select2-choice')]"));
271        }
272        select2Field.click();
273    }
274
275    /**
276     * @return The suggest input element.
277     * @since 6.0
278     */
279    private WebElement getSuggestInput() {
280        WebElement suggestInput = null;
281        if (mutliple) {
282            suggestInput = element.findElement(By.xpath("ul/li[@class='select2-search-field']/input"));
283        } else {
284            suggestInput = driver.findElement(By.xpath(S2_SINGLE_INPUT_XPATH));
285        }
286
287        return suggestInput;
288    }
289
290    /**
291     * Do a wait on the select2 field.
292     *
293     * @throws TimeoutException
294     * @since 6.0
295     */
296    private void waitSelect2() throws TimeoutException {
297        Wait<WebElement> wait = new FluentWait<WebElement>(
298                !mutliple ? driver.findElement(By.xpath(S2_SINGLE_INPUT_XPATH))
299                        : element.findElement(By.xpath(S2_MULTIPLE_INPUT_XPATH))).withTimeout(SELECT2_LOADING_TIMEOUT,
300                TimeUnit.SECONDS).pollingEvery(100, TimeUnit.MILLISECONDS).ignoring(NoSuchElementException.class);
301        Function<WebElement, Boolean> s2WaitFunction = new Select2Wait();
302        wait.until(s2WaitFunction);
303    }
304
305    /**
306     * Clear the input of the select2.
307     *
308     * @since 6.0
309     */
310    public void clearSuggestInput() {
311        WebElement suggestInput = null;
312        if (mutliple) {
313            suggestInput = driver.findElement(By.xpath("//ul/li[@class='select2-search-field']/input"));
314        } else {
315            suggestInput = driver.findElement(By.xpath(S2_SINGLE_INPUT_XPATH));
316        }
317
318        if (suggestInput != null) {
319            suggestInput.clear();
320        }
321    }
322
323    /**
324     * Click on the select2 element.
325     *
326     * @since 6.0
327     */
328    public void clickSelect2Field() {
329        WebElement select2Field = null;
330        if (mutliple) {
331            select2Field = element;
332        } else {
333            select2Field = element.findElement(By.xpath("a[contains(@class,'select2-choice select2-default')]"));
334        }
335        select2Field.click();
336    }
337
338    /**
339     * Type a value in the select2 and then simulate the enter key.
340     *
341     * @since 6.0
342     */
343    public SearchPage typeValueAndTypeEnter(String value) {
344        clickOnSelect2Field();
345
346        WebElement suggestInput = getSuggestInput();
347
348        suggestInput.sendKeys(value);
349        try {
350            waitSelect2();
351        } catch (TimeoutException e) {
352            log.warn("Suggestion timed out with input : " + value + ". Let's try with next letter.");
353        }
354        suggestInput.sendKeys(Keys.RETURN);
355
356        return AbstractTest.asPage(SearchPage.class);
357    }
358
359}