001/*
002 * (C) Copyright 2016 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     <a href="mailto:grenard@nuxeo.com">Guillaume</a>
018 *     `Yannis JULIENNE
019 */
020package org.nuxeo.functionaltests.forms;
021
022import java.util.List;
023import java.util.concurrent.TimeUnit;
024
025import org.apache.commons.logging.Log;
026import org.apache.commons.logging.LogFactory;
027import org.nuxeo.functionaltests.AbstractTest;
028import org.nuxeo.functionaltests.AjaxRequestManager;
029import org.nuxeo.functionaltests.Locator;
030import org.nuxeo.functionaltests.fragment.WebFragmentImpl;
031import org.nuxeo.functionaltests.pages.search.SearchPage;
032import org.openqa.selenium.By;
033import org.openqa.selenium.Keys;
034import org.openqa.selenium.NoSuchElementException;
035import org.openqa.selenium.StaleElementReferenceException;
036import org.openqa.selenium.TimeoutException;
037import org.openqa.selenium.WebDriver;
038import org.openqa.selenium.WebElement;
039import org.openqa.selenium.support.ui.FluentWait;
040import org.openqa.selenium.support.ui.Wait;
041
042import com.gargoylesoftware.htmlunit.ElementNotFoundException;
043import com.google.common.base.Function;
044
045/**
046 * Convenient class to handle a select2Widget.
047 *
048 * @since 5.7.3
049 */
050public class Select2WidgetElement extends WebFragmentImpl {
051
052    private static class Select2Wait implements Function<WebElement, Boolean> {
053
054        @Override
055        public Boolean apply(WebElement element) {
056            boolean result = !element.getAttribute("class").contains(S2_CSS_ACTIVE_CLASS);
057            return result;
058        }
059    }
060
061    private static final Log log = LogFactory.getLog(Select2WidgetElement.class);
062
063    private static final String S2_CSS_ACTIVE_CLASS = "select2-active";
064
065    private static final String S2_MULTIPLE_CURRENT_SELECTION_XPATH = "ul[@class='select2-choices']/li[@class='select2-search-choice']";
066
067    private final static String S2_MULTIPLE_INPUT_XPATH = "ul/li/input";
068
069    private static final String S2_SINGLE_CURRENT_SELECTION_XPATH = "a[@class='select2-choice']/span[@class='select2-chosen']";
070
071    private final static String S2_SINGLE_INPUT_XPATH = "//*[@id='select2-drop']/div/input";
072
073    private static final String S2_SUGGEST_RESULT_XPATH = "//*[@id='select2-drop']//li[contains(@class,'select2-result-selectable')]/div";
074
075    /**
076     * Select2 loading timeout in seconds.
077     */
078    private static final int SELECT2_LOADING_TIMEOUT = 20;
079
080    protected boolean multiple = false;
081
082    /**
083     * Constructor.
084     *
085     * @param driver the driver
086     * @param id the id of the widget
087     * @since 7.1
088     */
089    public Select2WidgetElement(WebDriver driver, String id) {
090        this(driver, driver.findElement(By.id(id)));
091    }
092
093    /**
094     * Constructor.
095     *
096     * @param driver the driver
097     * @param by the by locator of the widget
098     */
099    public Select2WidgetElement(WebDriver driver, WebElement element) {
100        super(driver, element);
101    }
102
103    /**
104     * Constructor.
105     *
106     * @param driver the driver
107     * @param by the by locator of the widget
108     * @param multiple whether the widget can have multiple values
109     */
110    public Select2WidgetElement(final WebDriver driver, WebElement element, final boolean multiple) {
111        this(driver, element);
112        this.multiple = multiple;
113    }
114
115    /**
116     * @since 5.9.3
117     */
118    public WebElement getSelectedValue() {
119        if (multiple) {
120            throw new UnsupportedOperationException("The select2 is multiple and has multiple selected values");
121        }
122        return element.findElement(By.xpath(S2_SINGLE_CURRENT_SELECTION_XPATH));
123    }
124
125    /**
126     * @since 5.9.3
127     */
128    public List<WebElement> getSelectedValues() {
129        if (!multiple) {
130            throw new UnsupportedOperationException(
131                    "The select2 is not multiple and can't have multiple selected values");
132        }
133        return element.findElements(By.xpath(S2_MULTIPLE_CURRENT_SELECTION_XPATH));
134    }
135
136    /**
137     * @since 5.9.3
138     */
139    protected String getSubmittedValue() {
140        String eltId = element.getAttribute("id");
141        String submittedEltId = element.getAttribute("id").substring("s2id_".length(), eltId.length());
142        return driver.findElement(By.id(submittedEltId)).getAttribute("value");
143    }
144
145    public List<WebElement> getSuggestedEntries() {
146        try {
147            return driver.findElements(By.xpath(S2_SUGGEST_RESULT_XPATH));
148        } catch (NoSuchElementException e) {
149            return null;
150        }
151    }
152
153    /**
154     * @since 8.1
155     */
156    public void removeSelection() {
157        if (multiple) {
158            throw new UnsupportedOperationException("The select2 is multiple, use #removeSelection(value) instead");
159        }
160        element.findElement(By.className("select2-search-choice-close")).click();
161    }
162
163    /**
164     * @since 5.9.3
165     */
166    public void removeFromSelection(final String displayedText) {
167        if (!multiple) {
168            throw new UnsupportedOperationException("The select2 is not multiple, use #removeSelection instead");
169        }
170        final String submittedValueBefore = getSubmittedValue();
171        boolean found = false;
172        for (WebElement el : getSelectedValues()) {
173            if (el.getText().equals(displayedText)) {
174                Locator.waitUntilEnabledAndClick(el.findElement(By.xpath("a[@class='select2-search-choice-close']")));
175                found = true;
176            }
177        }
178        if (found) {
179            Locator.waitUntilGivenFunction(new Function<WebDriver, Boolean>() {
180                @Override
181                public Boolean apply(WebDriver driver) {
182                    return !submittedValueBefore.equals(getSubmittedValue());
183                }
184            });
185        } else {
186            throw new ElementNotFoundException("remove link for select2 '" + displayedText + "' item", "", "");
187        }
188    }
189
190    /**
191     * Select a single value.
192     *
193     * @param value the value to be selected
194     * @since 5.7.3
195     */
196    public void selectValue(final String value) {
197        selectValue(value, false, false);
198    }
199
200    /**
201     * @since 7.1
202     */
203    public void selectValue(final String value, final boolean wait4A4J) {
204        selectValue(value, wait4A4J, false);
205    }
206
207    /**
208     * Select given value, waiting for JSF ajax requests or not, and typing the whole suggestion or not (use false speed
209     * up selection when exactly one suggestion is found, use true when creating new suggestions).
210     *
211     * @param value string typed in the suggest box
212     * @param wait4A4J use true if request is triggering JSF ajax calls
213     * @param typeAll use false speed up selection when exactly one suggestion is found, use true when creating new
214     *            suggestions.
215     * @since 7.10
216     */
217    public void selectValue(final String value, final boolean wait4A4J, final boolean typeAll) {
218        selectValue(value, wait4A4J, typeAll, true);
219    }
220
221    public void selectValue(final String value, final boolean wait4A4J, final boolean typeAll, boolean click) {
222        if (click) {
223            clickSelect2Field();
224        }
225
226        WebElement suggestInput = getSuggestInput();
227
228        int nbSuggested = Integer.MAX_VALUE;
229        char c;
230        for (int i = 0; i < value.length(); i++) {
231            c = value.charAt(i);
232            suggestInput.sendKeys(c + "");
233            waitSelect2();
234            if (i >= 2) {
235                if (getSuggestedEntries().size() > nbSuggested) {
236                    throw new IllegalArgumentException(
237                            "More suggestions than expected for " + element.getAttribute("id"));
238                }
239                nbSuggested = getSuggestedEntries().size();
240                if (!typeAll && nbSuggested == 1) {
241                    break;
242                }
243            }
244        }
245
246        waitSelect2();
247
248        List<WebElement> suggestions = getSuggestedEntries();
249        if (suggestions == null || suggestions.isEmpty()) {
250            log.warn("Suggestion for element " + element.getAttribute("id") + " returned no result for value '" + value
251                    + "'.");
252            return;
253        }
254        WebElement suggestion = suggestions.get(0);
255        if (suggestions.size() > 1) {
256            log.warn("Suggestion for element " + element.getAttribute("id") + " returned more than 1 result for value '"
257                    + value + "', the first suggestion will be selected : " + suggestion.getText());
258        }
259
260        AjaxRequestManager arm = new AjaxRequestManager(driver);
261        if (wait4A4J) {
262            arm.watchAjaxRequests();
263        }
264        try {
265            suggestion.click();
266        } catch (StaleElementReferenceException e) {
267            suggestion = driver.findElement(By.xpath(S2_SUGGEST_RESULT_XPATH));
268            suggestion.click();
269        }
270        if (wait4A4J) {
271            arm.waitForAjaxRequests();
272        }
273    }
274
275    /**
276     * Select multiple values.
277     *
278     * @param values the values
279     * @since 5.7.3
280     */
281    public void selectValues(final String[] values) {
282        boolean click = true;
283        for (String value : values) {
284            // avoid clicking again when setting multiple values, to prevent accidental deletion of previously added
285            // element
286            selectValue(value, false, false, click);
287            click = false;
288        }
289    }
290
291    /**
292     * Type a value in the select2 and return the suggested entries.
293     *
294     * @param value The value to type in the select2.
295     * @return The suggested values for the parameter.
296     * @since 6.0
297     */
298    public List<WebElement> typeAndGetResult(final String value) {
299
300        clickSelect2Field();
301
302        WebElement suggestInput = getSuggestInput();
303
304        suggestInput.sendKeys(value);
305        try {
306            waitSelect2();
307        } catch (TimeoutException e) {
308            log.warn("Suggestion timed out with input : " + value + ". Let's try with next letter.");
309        }
310
311        return getSuggestedEntries();
312    }
313
314    /**
315     * Click on the select2 field.
316     *
317     * @since 6.0
318     */
319    public void clickSelect2Field() {
320        WebElement select2Field = null;
321        if (multiple) {
322            select2Field = element;
323        } else {
324            select2Field = element.findElement(By.xpath("a[contains(@class,'select2-choice')]"));
325        }
326        Locator.waitUntilEnabled(select2Field);
327        Locator.scrollToElement(select2Field);
328        select2Field.click();
329    }
330
331    /**
332     * @return The suggest input element.
333     * @since 6.0
334     */
335    private WebElement getSuggestInput() {
336        WebElement suggestInput = null;
337        if (multiple) {
338            suggestInput = element.findElement(By.xpath("ul/li[@class='select2-search-field']/input"));
339        } else {
340            suggestInput = driver.findElement(By.xpath(S2_SINGLE_INPUT_XPATH));
341        }
342
343        return suggestInput;
344    }
345
346    /**
347     * Do a wait on the select2 field.
348     *
349     * @throws TimeoutException
350     * @since 6.0
351     */
352    private void waitSelect2() throws TimeoutException {
353        Wait<WebElement> wait = new FluentWait<WebElement>(
354                !multiple ? driver.findElement(By.xpath(S2_SINGLE_INPUT_XPATH))
355                        : element.findElement(By.xpath(S2_MULTIPLE_INPUT_XPATH))).withTimeout(SELECT2_LOADING_TIMEOUT,
356                                TimeUnit.SECONDS).pollingEvery(100, TimeUnit.MILLISECONDS).ignoring(
357                                        NoSuchElementException.class);
358        Function<WebElement, Boolean> s2WaitFunction = new Select2Wait();
359        wait.until(s2WaitFunction);
360    }
361
362    /**
363     * Clear the input of the select2.
364     *
365     * @since 6.0
366     */
367    public void clearSuggestInput() {
368        WebElement suggestInput = null;
369        if (multiple) {
370            suggestInput = driver.findElement(By.xpath("//ul/li[@class='select2-search-field']/input"));
371        } else {
372            suggestInput = driver.findElement(By.xpath(S2_SINGLE_INPUT_XPATH));
373        }
374
375        if (suggestInput != null) {
376            suggestInput.clear();
377        }
378    }
379
380    /**
381     * Type a value in the select2 and then simulate the enter key.
382     *
383     * @since 6.0
384     */
385    public SearchPage typeValueAndTypeEnter(String value) {
386        clickSelect2Field();
387
388        WebElement suggestInput = getSuggestInput();
389
390        suggestInput.sendKeys(value);
391        try {
392            waitSelect2();
393        } catch (TimeoutException e) {
394            log.warn("Suggestion timed out with input : " + value + ". Let's try with next letter.");
395        }
396        suggestInput.sendKeys(Keys.RETURN);
397
398        return AbstractTest.asPage(SearchPage.class);
399    }
400
401    /**
402     * @since 8.3
403     */
404    public void hideSuggestionsByEscapeKey() {
405        getSuggestInput().sendKeys(Keys.ESCAPE);
406    }
407}