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:[email protected]">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}