dynamic Using in FindsBy with selenium

2020-02-05 05:28发布

I have this spec

Scenario Outline: Display widget
    Given I have a valid connection
    When I navigate to home using <browser>
    Then The element in css selector #<id> > svg > g.x.axis.percent > text:nth-child(1) should be <value>
    Examples:
        | browser | id      | valye  |
        | Chrome  | Widget1 | 213.00 |

With this page definition

class BarSummaryPage
{

    [FindsBy(How = How.CssSelector, Using="#{DYNAMIC-ID} > svg > g.x.axis.percent > text:nth-child(1)")]
    private IWebElement Mes;
}

I need to configure the Using property in FindsBy dynamic, like above: SEE #{DYNAMIC-ID}

1条回答
Rolldiameter
2楼-- · 2020-02-05 06:05

As far as I know, this doesn't exist out of the box. The FindBy annotation takes static Strings only. You probably need to custom modify the FindBy annotation processor similarly to what this blogger did: https://web.archive.org/web/20180612042724/http://brimllc.com/2011/01/selenium-2-0-webdriver-extending-findby-annotation-to-support-dynamic-idxpath/

Another discussion thread here: https://groups.google.com/forum/#!topic/webdriver/awxOw0FoiYU where Simon Stewart shows an example of how this could be accomplished.

UPDATE:

I have actually implemented this because I needed it enough to try. I didn't create a custom finder annotation (which I may have to do in the future).

I wrote implementations for ElementLocator and ElementLocatorFactory that allow for string substitutions for locators specified using the existing annotations. If you know, or can determine, at runtime the values to substitute, this will work for you.

By default, PageFactory uses the classes DefaultElementLocator and DefaultElementLocatorFactory implementations of the ElementLocator and ElementLocatorFactory interfaces for setting up the processing of annotations, but the real logic is in the Annotations class. I wrote my own implementations of ElementLocator, and ElementLocatorFactory and wrote my own version of Annotations to do the processing. There are just a few differences between the source of my customized classes and the ones that are in the Selenium source code.

public class DynamicElementLocator implements ElementLocator {

    private static final XLogger log = XLoggerFactory.getXLogger(DynamicElementLocator.class.getCanonicalName());

    private final SearchContext searchContext;
    private final boolean shouldCache;
    private final By by;
    private WebElement cachedElement;
    private List<WebElement> cachedElementList;

    //The only thing that differs from DefaultElementLocator is
    //the substitutions parameter for this method. 
    public DynamicElementLocator(final SearchContext searchContext, final Field field, final Map<String,String>
            substitutions) {
        log.entry(searchContext, field, substitutions);
        this.searchContext = searchContext;
        //DynamicAnnotations is my implementation of annotation processing
        //that uses the substitutions to find and replace values in the
        //locator strings in the FindBy, FindAll, FindBys annotations 
        DynamicAnnotations annotations = new DynamicAnnotations(field, substitutions);
        shouldCache = annotations.isLookupCached();
        by = annotations.buildBy();
        log.debug("Successful completion of the dynamic element locator");
        log.exit();
    }

    /**
     * Find the element.
     */
    public WebElement findElement() {
        log.entry();
        if (cachedElement != null && shouldCache) {
            return log.exit(cachedElement);
        }

        WebElement element = searchContext.findElement(by);
        if (shouldCache) {
            cachedElement = element;
        }

        return log.exit(element);
    }

    /**
     * Find the element list.
     */
    public List<WebElement> findElements() {
        log.entry();
        if (cachedElementList != null && shouldCache) {
            return log.exit(cachedElementList);
        }

        List<WebElement> elements = searchContext.findElements(by);
        if (shouldCache) {
            cachedElementList = elements;
        }

        return log.exit(elements);
    }
}

And here is the DynamicElementLocatorFactory:

public final class DynamicElementLocatorFactory implements ElementLocatorFactory {
    private final SearchContext searchContext;
    private final Map<String,String> substitutions;

        //The only thing that is different from DefaultElementLocatorFactory
        //is that the constructor for this class takes the substitutions
        //parameter that consists of the key/value mappings to use
        //for substituting keys in locator strings for FindBy, FindAll and     
        //FindBys with values known or determined at runtime.
        public DynamicElementLocatorFactory(final SearchContext searchContext, final Map<String,String> substitutions) {
            this.searchContext = searchContext;
            this.substitutions = substitutions;
        }

        //This produces an instance of the DynamicElementLocator class and
        //specifies the key value mappings to substitute in locator Strings
        public DynamicElementLocator createLocator(final Field field) {
            return new DynamicElementLocator(searchContext, field, substitutions);
        }
    }

And here is my custom annotation processor. This is where most of the work was:

public class DynamicAnnotations extends Annotations {
    private static final XLogger log = XLoggerFactory.getXLogger(DynamicAnnotations.class.getCanonicalName());

    private final Field field;
    private final Map<String,String> substitutions;

    //Again, not much is different from the Selenium default class here
    //other than the additional substitutions parameter
    public DynamicAnnotations(final Field field, final Map<String,String> substitutions) {
        super(field);
        log.entry(field, substitutions);
        this.field = field;
        this.substitutions = substitutions;
        log.debug("Successful completion of the dynamic annotations constructor");
        log.exit();
    }

    public boolean isLookupCached() {
        log.entry();
        return log.exit((field.getAnnotation(CacheLookup.class) != null));
    }

    public By buildBy() {
        log.entry();
        assertValidAnnotations();

        By ans = null;

        FindBys findBys = field.getAnnotation(FindBys.class);
        if (findBys != null) {
            log.debug("Building a chained locator");
            ans = buildByFromFindBys(findBys);
        }

        FindAll findAll = field.getAnnotation(FindAll.class);
        if (ans == null && findAll != null) {
            log.debug("Building a find by one of locator");
            ans = buildBysFromFindByOneOf(findAll);
        }

        FindBy findBy = field.getAnnotation(FindBy.class);
        if (ans == null && findBy != null) {
            log.debug("Building an ordinary locator");
            ans = buildByFromFindBy(findBy);
        }

        if (ans == null) {
            log.debug("No locator annotation specified, so building a locator for id or name based on field name");
            ans = buildByFromDefault();
        }

        if (ans == null) {
            throw log.throwing(new IllegalArgumentException("Cannot determine how to locate element " + field));
        }

        return log.exit(ans);
    }

    protected By buildByFromDefault() {
        log.entry();
        return log.exit(new ByIdOrName(field.getName()));
    }

    protected By buildByFromFindBys(final FindBys findBys) {
        log.entry(findBys);
        assertValidFindBys(findBys);

        FindBy[] findByArray = findBys.value();
        By[] byArray = new By[findByArray.length];
        for (int i = 0; i < findByArray.length; i++) {
            byArray[i] = buildByFromFindBy(findByArray[i]);
        }

        return log.exit(new ByChained(byArray));
    }

    protected By buildBysFromFindByOneOf(final FindAll findBys) {
        log.entry(findBys);
        assertValidFindAll(findBys);

        FindBy[] findByArray = findBys.value();
        By[] byArray = new By[findByArray.length];
        for (int i = 0; i < findByArray.length; i++) {
            byArray[i] = buildByFromFindBy(findByArray[i]);
        }

        return log.exit(new ByAll(byArray));
    }

    protected By buildByFromFindBy(final FindBy findBy) {
        log.entry(findBy);
        assertValidFindBy(findBy);

        By ans = buildByFromShortFindBy(findBy);
        if (ans == null) {
            ans = buildByFromLongFindBy(findBy);
        }

        return log.exit(ans);
    }

    //The only thing that is different from the default Selenium implementation is that the locator string is processed for substitutions by the processForSubstitutions(using) method, which I have added
    protected By buildByFromLongFindBy(final FindBy findBy) {
        log.entry(findBy);
        How how = findBy.how();
        String using = findBy.using();

        switch (how) {
            case CLASS_NAME:
                log.debug("Long FindBy annotation specified lookup by class name, using {}", using);
                String className = processForSubstitutions(using);
                return log.exit(By.className(className));

            case CSS:
                log.debug("Long FindBy annotation specified lookup by css name, using {}", using);
                String css = processForSubstitutions(using);
                return log.exit(By.cssSelector(css));

            case ID:
                log.debug("Long FindBy annotation specified lookup by id, using {}", using);
                String id = processForSubstitutions(using);
                return log.exit(By.id(id));

            case ID_OR_NAME:
                log.debug("Long FindBy annotation specified lookup by id or name, using {}", using);
                String idOrName = processForSubstitutions(using);
                return log.exit(new ByIdOrName(idOrName));

            case LINK_TEXT:
                log.debug("Long FindBy annotation specified lookup by link text, using {}", using);
                String linkText = processForSubstitutions(using);
                return log.exit(By.linkText(linkText));

            case NAME:
                log.debug("Long FindBy annotation specified lookup by name, using {}", using);
                String name = processForSubstitutions(using);
                return log.exit(By.name(name));

            case PARTIAL_LINK_TEXT:
                log.debug("Long FindBy annotation specified lookup by partial link text, using {}", using);
                String partialLinkText = processForSubstitutions(using);
                return log.exit(By.partialLinkText(partialLinkText));

            case TAG_NAME:
                log.debug("Long FindBy annotation specified lookup by tag name, using {}", using);
                String tagName = processForSubstitutions(using);
                return log.exit(By.tagName(tagName));

            case XPATH:
                log.debug("Long FindBy annotation specified lookup by xpath, using {}", using);
                String xpath = processForSubstitutions(using);
                return log.exit(By.xpath(xpath));

            default:
                // Note that this shouldn't happen (eg, the above matches all
                // possible values for the How enum)
                throw log.throwing(new IllegalArgumentException("Cannot determine how to locate element " + field));
        }
    }

    //The only thing that differs from the default Selenium implementation is that the locator string is processed for substitutions by processForSubstitutions(using), which I wrote
    protected By buildByFromShortFindBy(final FindBy findBy) {
        log.entry(findBy);
        log.debug("Building from a short FindBy annotation");

        if (!"".equals(findBy.className())) {
            log.debug("Short FindBy annotation specifies lookup by class name: {}", findBy.className());
            String className = processForSubstitutions(findBy.className());
            return log.exit(By.className(className));
        }

        if (!"".equals(findBy.css())) {
            log.debug("Short FindBy annotation specifies lookup by css");
            String css = processForSubstitutions(findBy.css());
            return log.exit(By.cssSelector(css));
        }

        if (!"".equals(findBy.id())) {
            log.debug("Short FindBy annotation specified lookup by id");
            String id = processForSubstitutions(findBy.id());
            return log.exit(By.id(id));
        }

        if (!"".equals(findBy.linkText())) {
            log.debug("Short FindBy annotation specified lookup by link text");
            String linkText = processForSubstitutions(findBy.linkText());
            return log.exit(By.linkText(linkText));
        }

        if (!"".equals(findBy.name())) {
            log.debug("Short FindBy annotation specified lookup by name");
            String name = processForSubstitutions(findBy.name());
            return log.exit(By.name(name));
        }

        if (!"".equals(findBy.partialLinkText())) {
            log.debug("Short FindBy annotation specified lookup by partial link text");
            String partialLinkText = processForSubstitutions(findBy.partialLinkText());
            return log.exit(By.partialLinkText(partialLinkText));
        }

        if (!"".equals(findBy.tagName())) {
            log.debug("Short FindBy annotation specified lookup by tag name");
            String tagName = processForSubstitutions(findBy.tagName());
            return log.exit(By.tagName(tagName));
        }

        if (!"".equals(findBy.xpath())) {
            log.debug("Short FindBy annotation specified lookup by xpath");
            String xpath = processForSubstitutions(findBy.xpath());
            return log.exit(By.xpath(xpath));
        }

        // Fall through
        log.debug("Locator does not match any expected locator type");
        return log.exit(null);
    }

    //This method is where I find and do replacements. The method looks
    //for instances of ${key} and if there is a key in the substitutions
    //map that is equal to 'key', the substring ${key} is replaced by the
    //value mapped to 'key'
    private String processForSubstitutions(final String locator) {
        log.entry(locator);
        log.debug("Processing locator '{}' for substitutions");
        List<String> subs = Arrays.asList(StringUtils.substringsBetween(locator, "${", "}"));
        log.debug("List of substrings in locator which match substitution pattern: {}", subs);
        String processed = locator;

        for(String sub : subs) {
            log.debug("Processing substring {}", sub);
            //If there is no matching key, the substring "${ ..}" is treated as a literal
            if(substitutions.get(sub) != null) {
                log.debug("Replacing with {}", substitutions.get(sub));
                processed = StringUtils.replace(locator, "${" + sub + "}",substitutions.get(sub));
                log.debug("Locator after substitution: {}", processed);
            }
        }

        return log.exit(processed);
    }

    private void assertValidAnnotations() {
        log.entry();

        FindBys findBys = field.getAnnotation(FindBys.class);
        FindAll findAll = field.getAnnotation(FindAll.class);
        FindBy findBy = field.getAnnotation(FindBy.class);

        if (findBys != null && findBy != null) {
            throw log.throwing(new IllegalArgumentException("If you use a '@FindBys' annotation, " +
                    "you must not also use a '@FindBy' annotation"));
        }
        if (findAll != null && findBy != null) {
            throw log.throwing(new IllegalArgumentException("If you use a '@FindAll' annotation, " +
                    "you must not also use a '@FindBy' annotation"));
        }
        if (findAll != null && findBys != null) {
            throw log.throwing(new IllegalArgumentException("If you use a '@FindAll' annotation, " +
                    "you must not also use a '@FindBys' annotation"));
        }
    }

    private void assertValidFindBys(final FindBys findBys) {
        log.entry(findBys);
        for (FindBy findBy : findBys.value()) {
            assertValidFindBy(findBy);
        }
        log.exit();
    }

    private void assertValidFindAll(final FindAll findBys) {
        log.entry(findBys);
        for (FindBy findBy : findBys.value()) {
            assertValidFindBy(findBy);
        }
        log.exit();
    }

    private void assertValidFindBy(final FindBy findBy) {
        log.entry();
        if (findBy.how() != null) {
            if (findBy.using() == null) {
                throw log.throwing(new IllegalArgumentException(
                        "If you set the 'how' property, you must also set 'using'"));
            }
        }

        Set<String> finders = new HashSet<>();
        if (!"".equals(findBy.using())) {
            log.debug("Locator string is: {}", findBy.using());
            finders.add("how: " + findBy.using());
        }
        if (!"".equals(findBy.className())) {
            log.debug("Class name locator string is {}", findBy.className());
            finders.add("class name:" + findBy.className());
        }

        if (!"".equals(findBy.css())) {
            log.debug("Css locator string is {}", findBy.css());
            finders.add("css:" + findBy.css());
        }

        if (!"".equals(findBy.id())) {
            log.debug("Id locator string is {}", findBy.id());
            finders.add("id: " + findBy.id());
        }

        if (!"".equals(findBy.linkText())) {
            log.debug("Link text locator string is {}", findBy.linkText());
            finders.add("link text: " + findBy.linkText());
        }

        if (!"".equals(findBy.name())) {
            log.debug("Name locator string is {}", findBy.name());
            finders.add("name: " + findBy.name());
        }

        if (!"".equals(findBy.partialLinkText())) {
            log.debug("Partial text locator string is {}", findBy.partialLinkText());
            finders.add("partial link text: " + findBy.partialLinkText());
        }

        if (!"".equals(findBy.tagName())) {
            log.debug("Tag name locator string is {}", findBy.tagName());
            finders.add("tag name: " + findBy.tagName());
        }
        if (!"".equals(findBy.xpath())) {
            log.debug("Xpath locator string is {}", findBy.xpath());
            finders.add("xpath: " + findBy.xpath());
        }

        // A zero count is okay: it means to look by name or id.
        if (finders.size() > 1) {
            throw log.throwing(new IllegalArgumentException(
                    String.format("You must specify at most one location strategy. Number found: %d (%s)",
                            finders.size(), finders.toString())));
        }
    }
}

Example usage:

public class ExampleClass extends SlowLoadableComponent<ExampleClass> {

    private final Map<String, String> substitutions;

    @FindBy(how = How.ID, using = "somelocator_with_a dynamic_${id}")
    private WebElement someElement;

    public ExampleClass(final WebDriver driver, final int
            loadTimeoutInSeconds, final String idValue) {

        substitutions = new HashMap<>(); substitutions.put("id", idValue);

    }

    //When you call PageFactory.initElements, you need to tell it to use the DynamicElementLocatorFactory
    protected void load() {
        PageFactory.initElements(new DynamicElementLocatorFactory(getDriver(), substitutions), this);
    }
}

UPDATED 5/1/2019: I had to use a Web Archive link for the blog post I referenced at the beginning of my answer because that blog post is not accessible at its original link.

查看更多
登录 后发表回答