Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I extend the Selenium By.class to create more flexibility?

How can I extend the Selenium By.class to create more flexibility? I looked at the By.class and I don't quite know how to approach this. It looks like I need to create an interface class and a static By class, such as ByJavascriptGetWebElement, in order to create this kind of wrapper?

I would like to be able to call it like:

By.javascript("return document.querySelector(\"div#item div\");", el );

And I have also heard of an easier way to do it but I would prefer to do it the more traditional way:

public By byJavascriptGetElement( WebElement we ) {
    return By.  ???
}

Any ideas?

like image 761
djangofan Avatar asked Oct 18 '13 20:10

djangofan


People also ask

How do I use extends in Selenium WebDriver?

public class T3EmployeeMaintenance extends Setup{ //public static WebDriver driver = new FirefoxDriver(); //no need of this now @BeforeClass public void invokeBrowser() { //... Show activity on this post. You have to finish class at the end. in your html simply add at the end of each class you declared.

What is the use of by class in Selenium?

Initializes a new instance of the By class using the given functions to find elements. Gets or sets the value of the description for this By class instance. Gets or sets the method used to find a single element matching specified criteria. Gets or sets the method used to find all elements matching specified criteria.

What is the Supermost class in Selenium?

SearchContext is the super most interface in selenium, which is extended by another interface called WebDriver. All the abstract methods of SearchContext and WebDriver interfaces are implemented in RemoteWebDriver class.


1 Answers

This can actually be easily done.

The idea is to get access to the WebDriver instance and run the JavaScript on it (if it supports it). Then there's a lot of validating, because we need to make sure we only return what we promised.

The ByJavaScript class itself will look like this:

public class ByJavaScript extends By implements Serializable {
    private final String script;

    public ByJavaScript(String script) {
        checkNotNull(script, "Cannot find elements with a null JavaScript expression.");
        this.script = script;
    }

    @Override
    public List<WebElement> findElements(SearchContext context) {
        JavascriptExecutor js = getJavascriptExecutorFromSearchContext(context);

        // call the JS, inspect and validate response
        Object response = js.executeScript(script);
        List<WebElement> elements = getElementListFromJsResponse(response);

        // filter out the elements that aren't descendants of the context node
        if (context instanceof WebElement) {
            filterOutElementsWithoutCommonAncestor(elements, (WebElement)context);
        }

        return elements;
    }

    private static JavascriptExecutor getJavascriptExecutorFromSearchContext(SearchContext context) {
        if (context instanceof JavascriptExecutor) {
            // context is most likely the whole WebDriver
            return (JavascriptExecutor)context;
        }
        if (context instanceof WrapsDriver) {
            // context is most likely some WebElement
            WebDriver driver = ((WrapsDriver)context).getWrappedDriver();
            checkState(driver instanceof JavascriptExecutor, "This WebDriver doesn't support JavaScript.");
            return (JavascriptExecutor)driver;
        }
        throw new IllegalStateException("We can't invoke JavaScript from this context.");
    }

    @SuppressWarnings("unchecked")  // cast thoroughly checked
    private static List<WebElement> getElementListFromJsResponse(Object response) {
        if (response == null) {
            // no element found
            return Lists.newArrayList();
        }
        if (response instanceof WebElement) {
            // a single element found
            return Lists.newArrayList((WebElement)response);
        }
        if (response instanceof List) {
            // found multiple things, check whether every one of them is a WebElement
            checkArgument(
                    Iterables.all((List<?>)response, Predicates.instanceOf(WebElement.class)),
                    "The JavaScript query returned something that isn't a WebElement.");
            return (List<WebElement>)response;  // cast is checked as far as we can tell
        }
        throw new IllegalArgumentException("The JavaScript query returned something that isn't a WebElement.");
    }

    private static void filterOutElementsWithoutCommonAncestor(List<WebElement> elements, WebElement ancestor) {
        for (Iterator<WebElement> iter = elements.iterator(); iter.hasNext(); ) {
            WebElement elem = iter.next();

            // iterate over ancestors
            while (!elem.equals(ancestor) && !elem.getTagName().equals("html")) {
                elem = elem.findElement(By.xpath("./.."));
            }

            if (!elem.equals(ancestor)) {
                iter.remove();
            }
        }
    }

    @Override
    public String toString() {
          return "By.javaScript: \"" + script + "\"";
    }

}

This code uses the Google Guava library. It is a dependency of Selenium, so you should have it on your classpath. But if there's something you don't understand, look into Guava.

Things to consider:

  1. Document the whole thing.
  2. Use better and more helpful exceptions. Consider some custom subclasses of WebDriverException. Also add more helpful messages and info.
  3. Restrict the class' visibility to package. Or embed it into a static factory (as seen in the original By class) so that it won't be accessible directly etc.
  4. Write tests. I have tried the most obvious usages (element can't be found, search from driver, search from some context) and everything seems to be ok, but I didn't test it extensively.

Usage:

WebElement elem = driver.findElement(new ByJavaScript("return document.querySelector('.haha');"));

Now, the original By class is a static factory that gives out various implementations of itself. Unfortunately, we can't add a new static method to it (without changing its source), so we won't be able to type By.javascript("return something;"). We have to create our own static factory to get something similar:

public class MyBy {
    /**
     * Returns a {@code By} which locates elements by the JavaScript expression passed to it.
     * 
     * @param script The JavaScript expression to run and whose result to return
     */
    public static By javascript(String script) {
        return new ByJavaScript(script);
    }
}

Usage:

WebElement elem = driver.findElement(MyBy.javascript("return document.querySelector('.haha');"));
like image 93
Petr Janeček Avatar answered Sep 19 '22 15:09

Petr Janeček