package com.persado.oss.quality.stevia.selenium.core.controllers.webdriverapi; /* * #%L * Stevia QA Framework - Core * %% * Copyright (C) 2013 - 2014 Persado * %% * Copyright (c) Persado Intellectual Property Limited. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * * Neither the name of the Persado Intellectual Property Limited nor the names * of its contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * #L% */ import com.persado.oss.quality.stevia.selenium.core.SteviaContext; import com.persado.oss.quality.stevia.selenium.core.controllers.WebDriverWebController; import org.openqa.selenium.By; import org.openqa.selenium.InvalidElementStateException; import org.openqa.selenium.InvalidSelectorException; import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.SearchContext; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.WebElement; import org.openqa.selenium.internal.FindsByCssSelector; import org.openqa.selenium.internal.FindsByXPath; import org.openqa.selenium.remote.RemoteWebElement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Method; import java.util.Collections; import java.util.List; import java.util.regex.Pattern; public abstract class ByExtended extends By { private static final Logger LOG = LoggerFactory.getLogger(ByExtended.class); /** * Finds elements via the driver's underlying W3 Selector engine. If the * browser does not implement the Selector API, a best effort is made to * emulate the API. In this case, we strive for at least CSS2 support, but * offer no guarantees. */ public static By cssSelector(final String selector) { if (selector == null) throw new IllegalArgumentException( "Cannot find elements when the selector is null"); return new ByCssSelectorExtended(selector); } /** * @param xpathExpression The xpath to use * @return a By which locates elements via XPath */ public static By xpath(final String xpathExpression) { if (xpathExpression == null) throw new IllegalArgumentException( "Cannot find elements when the XPath expression is null."); return new ByXPathExtended(xpathExpression); } public static class ByCssSelectorExtended extends ByCssSelector { private static final String HTTPS = "https://"; private static final String HTTP = "http://"; /** * uid */ private static final long serialVersionUID = 1L; private static final String DEFAULT_SIZZLE_URL = "http://cdnjs.cloudflare.com/ajax/libs/sizzle/2.3.3/sizzle.min.js"; private String ownSelector; public ByCssSelectorExtended(String selector) { super(selector); ownSelector = selector; } @Override public WebElement findElement(SearchContext context) { try { if (context instanceof FindsByCssSelector) { return ((FindsByCssSelector) context) .findElementByCssSelector(ownSelector); } } catch(InvalidSelectorException e){ return findElementBySizzleCss(context,ownSelector); } catch (InvalidElementStateException e) { return findElementBySizzleCss(context, ownSelector); } catch (WebDriverException e) { if (e.getMessage().startsWith( "An invalid or illegal string was specified")) { return findElementBySizzleCss(context, ownSelector); } throw e; } throw new WebDriverException("Driver does not support finding an element by selector: " + ownSelector); } @Override public List<WebElement> findElements(SearchContext context) { try { if (context instanceof FindsByCssSelector) { return ((FindsByCssSelector) context) .findElementsByCssSelector(ownSelector); } } catch(InvalidSelectorException e){ return findElementsBySizzleCss(context, ownSelector); } catch (InvalidElementStateException e) { return findElementsBySizzleCss(context, ownSelector); } catch (WebDriverException e) { if (e.getMessage().startsWith( "An invalid or illegal string was specified")) { return findElementsBySizzleCss(context, ownSelector); } throw e; } throw new WebDriverException("Driver does not support finding an element by selector: " + ownSelector); } @Override public String toString() { return "ByExtended.selector: " + ownSelector; } /********************************* SIZZLE SUPPORT CODE**************************************/ /** * Find element by sizzle css. * @param context * * @param cssLocator * the cssLocator * @return the web element */ public WebElement findElementBySizzleCss(SearchContext context, String cssLocator) { List<WebElement> elements = findElementsBySizzleCss(context, cssLocator); if (elements != null && elements.size() > 0 ) { return elements.get(0); } // if we get here, we cannot find the element via Sizzle. throw new NoSuchElementException("selector '"+cssLocator+"' cannot be found in DOM"); } private void fixLocator(SearchContext context, String cssLocator, WebElement element) { if (element instanceof RemoteWebElement) { try { @SuppressWarnings("rawtypes") Class[] parameterTypes = new Class[] { SearchContext.class, String.class, String.class }; Method m = element.getClass().getDeclaredMethod( "setFoundBy", parameterTypes); m.setAccessible(true); Object[] parameters = new Object[] { context, "css selector", cssLocator }; m.invoke(element, parameters); } catch (Exception fail) { //NOOP Would like to log here? } } } private WebDriver getDriver() { WebDriverWebController controller = ((WebDriverWebController) SteviaContext.getWebController()); return controller.getDriver(); } /** * Find elements by sizzle css. * * @param cssLocator * the cssLocator * @return the list of the web elements that match this locator */ public List<WebElement> findElementsBySizzleCss(SearchContext context, String cssLocator) { injectSizzleIfNeeded(); String javascriptExpression = createSizzleSelectorExpression(cssLocator); List<WebElement> elements = executeRemoteScript(javascriptExpression); if (elements.size() > 0) { for (WebElement el : elements) { fixLocator(context, cssLocator, el); } } return elements; } @SuppressWarnings("unchecked") private final List<WebElement> executeRemoteScript(String javascriptExpression) { List<WebElement> list = null; JavascriptExecutor executor = (JavascriptExecutor) getDriver(); try { list = (List<WebElement>) executor .executeScript(javascriptExpression); } catch (WebDriverException wde) { if (wde.getMessage().contains("Sizzle is not defined")) { LOG.error("Attempt to execute the code '"+javascriptExpression+"' has failed - Sizzle was not detected. Trying once more"); // we wait for 1/2 sec try { Thread.sleep(500); } catch (InterruptedException e) { } // try to inject sizzle once more. injectSizzleIfNeeded(); // now, try again to execute list = (List<WebElement>) executor .executeScript(javascriptExpression); } else { // not a Sizzle case, just throw it throw wde; } } finally { if (list == null) { list = Collections.emptyList(); } } return list; } /** * Creates the sizzle selector expression. * * @param cssLocator * the cssLocator * @return string that represents the sizzle selector expression. */ private String createSizzleSelectorExpression(String cssLocator) { return "return Sizzle(\"" + cssLocator + "\")"; } /** * Inject sizzle if needed. */ private void injectSizzleIfNeeded() { if (!sizzleLoaded()) { injectSizzle(); } else { return; // sizzle is ready } for (int i = 0; i<40; i++ ) { if(sizzleLoaded() ) { return; // sizzle is loaded } try { Thread.sleep(500); } catch (InterruptedException e) { // FIX: nothing to print here } if (i % 10 == 0) { LOG.warn("Attempting to re-load SizzleCSS from {}",getSizzleUrl()); injectSizzle(); } } //Try on last time if (!sizzleLoaded()) { LOG.error("After so many tries, sizzle does not appear in DOM"); } // sizzle is not loaded yet throw new RuntimeException("Sizzle loading from ("+ getSizzleUrl() +") has failed - " + "provide a better sizzle URL via -DsizzleUrl"); } private String getSizzleUrl() { return System.getProperty("sizzleUrl",DEFAULT_SIZZLE_URL ); } /** * Check if the Sizzle library is loaded. * * @return the true if Sizzle is loaded in the web page */ public Boolean sizzleLoaded() { Boolean loaded = true; try { loaded = (Boolean) ((JavascriptExecutor) getDriver()) .executeScript("return (window.Sizzle != null);"); } catch (WebDriverException e) { LOG.error("while trying to verify Sizzle loading, WebDriver threw exception {} {}",e.getMessage(),e.getCause() != null ? "with cause "+e.getCause() : ""); loaded = false; } return loaded; } /** * Inject sizzle 1.8.2 */ public void injectSizzle() { String sizzleUrl = getSizzleUrl(); if (sizzleUrl.startsWith(HTTP)) { sizzleUrl = sizzleUrl.substring(HTTP.length()); } else if (sizzleUrl.startsWith(HTTPS)) { sizzleUrl = sizzleUrl.substring(HTTPS.length()); } StringBuilder script = new StringBuilder() .append(" var bodyTag = document.getElementsByTagName('body')[0];") .append("if (bodyTag) {") .append(" var sizzl = document.createElement('script');") .append(" sizzl.type = 'text/javascript';") .append(" sizzl.src = document.location.protocol + '//").append(sizzleUrl).append("';") .append(" bodyTag.appendChild(sizzl);") .append("} else if (window.jQuery) { ") .append(" $.getScript(document.location.protocol + '//").append(sizzleUrl).append("');") .append("}"); final String stringified = script.toString(); LOG.debug("Executing injection script: {}",stringified); ((JavascriptExecutor) getDriver()).executeScript(stringified); } /** * ******************** SIZZLE SUPPORT CODE */ } public static class ByXPathExtended extends ByXPath { /** * uid */ private static final long serialVersionUID = 1L; private final String ownXpathExpression; public ByXPathExtended(String xpathExpression) { super(xpathExpression); ownXpathExpression = xpathExpression; } @Override public List<WebElement> findElements(SearchContext context) { long t0 = System.currentTimeMillis(); try { return ((FindsByXPath) context) .findElementsByXPath(ownXpathExpression); } finally { long l = System.currentTimeMillis()-t0; if (l > 100) { LOG.warn("SLOW findElements() = {}ms. Slow selector : {} ", l, ownXpathExpression); } } } @Override public WebElement findElement(SearchContext context) { long t0 = System.currentTimeMillis(); try { int indexOf = ownXpathExpression.indexOf("//", 3); if (indexOf > -1) { // we found an // inside the selector String[] splitSelectors = ownXpathExpression.substring(2).split(Pattern.quote("//")); WebElement parent = ((FindsByXPath) context).findElementByXPath("//"+splitSelectors[0]); for (int i = 1; i < splitSelectors.length; i++) { if (parent == null) { throw new WebDriverException("Failed to match the parent selector : "+splitSelectors[i-1]); } WebElement found = parent.findElement(By.xpath(".//"+splitSelectors[i])); if (found != null) { parent = found; } else { throw new WebDriverException("Failed to match the selector : "+splitSelectors[i]+" within "+ownXpathExpression); } } // by here, we should have the parent WebElement to contain what we want. //LOG.info("Found compound selector : "+parent.toString()); return parent; } // simple case: one selector return ((FindsByXPath) context).findElementByXPath(ownXpathExpression); } finally { long l = System.currentTimeMillis()-t0; if (l > 100) { LOG.warn("SLOW findElement() = {}ms. Slow selector : {} ", l, ownXpathExpression); } } } @Override public String toString() { return "ByExtended.xpath: " + ownXpathExpression; } } }