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}