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}