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}