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