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 mutliple = 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        mutliple = multiple;
112    }
113
114    /**
115     * @since 5.9.3
116     */
117    public WebElement getSelectedValue() {
118        if (mutliple) {
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 (!mutliple) {
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 5.9.3
154     */
155    public void removeFromSelection(final String displayedText) {
156        if (!mutliple) {
157            throw new UnsupportedOperationException(
158                    "The select2 is not multiple and you can't remove a specific value");
159        }
160        final String submittedValueBefore = getSubmittedValue();
161        boolean found = false;
162        for (WebElement el : getSelectedValues()) {
163            if (el.getText().equals(displayedText)) {
164                el.findElement(By.xpath("a[@class='select2-search-choice-close']")).click();
165                found = true;
166            }
167        }
168        if (found) {
169            Locator.waitUntilGivenFunction(new Function<WebDriver, Boolean>() {
170                @Override
171                public Boolean apply(WebDriver driver) {
172                    return !submittedValueBefore.equals(getSubmittedValue());
173                }
174            });
175        } else {
176            throw new ElementNotFoundException("remove link for select2 '" + displayedText + "' item", "", "");
177        }
178    }
179
180    /**
181     * Select a single value.
182     *
183     * @param value the value to be selected
184     * @since 5.7.3
185     */
186    public void selectValue(final String value) {
187        selectValue(value, false, false);
188    }
189
190    /**
191     * @since 7.1
192     */
193    public void selectValue(final String value, final boolean wait4A4J) {
194        selectValue(value, wait4A4J, false);
195    }
196
197    /**
198     * Select given value, waiting for JSF ajax requests or not, and typing the whole suggestion or not (use false speed
199     * up selection when exactly one suggestion is found, use true when creating new suggestions).
200     *
201     * @param value string typed in the suggest box
202     * @param wait4A4J use true if request is triggering JSF ajax calls
203     * @param typeAll use false speed up selection when exactly one suggestion is found, use true when creating new
204     *            suggestions.
205     * @since 7.10
206     */
207    public void selectValue(final String value, final boolean wait4A4J, final boolean typeAll) {
208        clickSelect2Field();
209
210        WebElement suggestInput = getSuggestInput();
211
212        int nbSuggested = Integer.MAX_VALUE;
213        char c;
214        for (int i = 0; i < value.length(); i++) {
215            c = value.charAt(i);
216            suggestInput.sendKeys(c + "");
217            waitSelect2();
218            if (i >= 2) {
219                if (getSuggestedEntries().size() > nbSuggested) {
220                    throw new IllegalArgumentException(
221                            "More suggestions than expected for " + element.getAttribute("id"));
222                }
223                nbSuggested = getSuggestedEntries().size();
224                if (!typeAll && nbSuggested == 1) {
225                    break;
226                }
227            }
228        }
229
230        waitSelect2();
231
232        List<WebElement> suggestions = getSuggestedEntries();
233        if (suggestions == null || suggestions.isEmpty()) {
234            log.warn("Suggestion for element " + element.getAttribute("id") + " returned no result.");
235            return;
236        }
237        WebElement suggestion = suggestions.get(0);
238        if (suggestions.size() > 1) {
239            log.warn("Suggestion for element " + element.getAttribute("id")
240                    + " returned more than 1 result, the first suggestion will be selected : " + suggestion.getText());
241        }
242
243        AjaxRequestManager arm = new AjaxRequestManager(driver);
244        ;
245        if (wait4A4J) {
246            arm.watchAjaxRequests();
247        }
248        try {
249            suggestion.click();
250        } catch (StaleElementReferenceException e) {
251            suggestion = driver.findElement(By.xpath(S2_SUGGEST_RESULT_XPATH));
252            suggestion.click();
253        }
254        if (wait4A4J) {
255            arm.waitForAjaxRequests();
256        }
257    }
258
259    /**
260     * Select multiple values.
261     *
262     * @param values the values
263     * @since 5.7.3
264     */
265    public void selectValues(final String[] values) {
266        for (String value : values) {
267            selectValue(value);
268        }
269    }
270
271    /**
272     * Type a value in the select2 and return the suggested entries.
273     *
274     * @param value The value to type in the select2.
275     * @return The suggested values for the parameter.
276     * @since 6.0
277     */
278    public List<WebElement> typeAndGetResult(final String value) {
279
280        clickSelect2Field();
281
282        WebElement suggestInput = getSuggestInput();
283
284        suggestInput.sendKeys(value);
285        try {
286            waitSelect2();
287        } catch (TimeoutException e) {
288            log.warn("Suggestion timed out with input : " + value + ". Let's try with next letter.");
289        }
290
291        return getSuggestedEntries();
292    }
293
294    /**
295     * Click on the select2 field.
296     *
297     * @since 6.0
298     */
299    public void clickSelect2Field() {
300        WebElement select2Field = null;
301        if (mutliple) {
302            select2Field = element;
303        } else {
304            select2Field = element.findElement(By.xpath("a[contains(@class,'select2-choice')]"));
305        }
306        select2Field.click();
307    }
308
309    /**
310     * @return The suggest input element.
311     * @since 6.0
312     */
313    private WebElement getSuggestInput() {
314        WebElement suggestInput = null;
315        if (mutliple) {
316            suggestInput = element.findElement(By.xpath("ul/li[@class='select2-search-field']/input"));
317        } else {
318            suggestInput = driver.findElement(By.xpath(S2_SINGLE_INPUT_XPATH));
319        }
320
321        return suggestInput;
322    }
323
324    /**
325     * Do a wait on the select2 field.
326     *
327     * @throws TimeoutException
328     * @since 6.0
329     */
330    private void waitSelect2() throws TimeoutException {
331        Wait<WebElement> wait = new FluentWait<WebElement>(
332                !mutliple ? driver.findElement(By.xpath(S2_SINGLE_INPUT_XPATH))
333                        : element.findElement(By.xpath(S2_MULTIPLE_INPUT_XPATH))).withTimeout(SELECT2_LOADING_TIMEOUT,
334                                TimeUnit.SECONDS).pollingEvery(100, TimeUnit.MILLISECONDS).ignoring(
335                                        NoSuchElementException.class);
336        Function<WebElement, Boolean> s2WaitFunction = new Select2Wait();
337        wait.until(s2WaitFunction);
338    }
339
340    /**
341     * Clear the input of the select2.
342     *
343     * @since 6.0
344     */
345    public void clearSuggestInput() {
346        WebElement suggestInput = null;
347        if (mutliple) {
348            suggestInput = driver.findElement(By.xpath("//ul/li[@class='select2-search-field']/input"));
349        } else {
350            suggestInput = driver.findElement(By.xpath(S2_SINGLE_INPUT_XPATH));
351        }
352
353        if (suggestInput != null) {
354            suggestInput.clear();
355        }
356    }
357
358    /**
359     * Type a value in the select2 and then simulate the enter key.
360     *
361     * @since 6.0
362     */
363    public SearchPage typeValueAndTypeEnter(String value) {
364        clickSelect2Field();
365
366        WebElement suggestInput = getSuggestInput();
367
368        suggestInput.sendKeys(value);
369        try {
370            waitSelect2();
371        } catch (TimeoutException e) {
372            log.warn("Suggestion timed out with input : " + value + ". Let's try with next letter.");
373        }
374        suggestInput.sendKeys(Keys.RETURN);
375
376        return AbstractTest.asPage(SearchPage.class);
377    }
378
379}