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