Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ru.yandex.qatools.htmlelements.loader.decorator.HtmlElementDecorator error with custom ElementLocatorFactory #78

Open
emaks opened this issue Apr 1, 2015 · 19 comments

Comments

@emaks
Copy link
Contributor

emaks commented Apr 1, 2015

when I try to initialize page elements with custom ElementLocatorFactory, like

    public AbstractPage(final WebDriver driver) {
        this.driver = driver;
        PageFactory.initElements(new HtmlElementDecorator(new NotDefaultElementLocatorFactory(this.driver)), this);
    }

in private <T extends HtmlElement> T decorateHtmlElement(Class<T> elementClass, ClassLoader loader, ElementLocator locator, String elementName) { } function
when PageFactory.initElements(new HtmlElementDecorator(elementToWrap), htmlElementInstance); line is executed will be used default HtmlElementLocatorFactory class

@aik099
Copy link
Contributor

aik099 commented Apr 1, 2015

Can you please link the code line you're talking about here?

@emaks
Copy link
Contributor Author

emaks commented Apr 1, 2015

here you are

@aik099
Copy link
Contributor

aik099 commented Apr 1, 2015

In that line the HtmlElementDecorator is used, but that's ok because the locator factory you've given is still used. Isn't it?

@emaks
Copy link
Contributor Author

emaks commented Apr 1, 2015

no, instance of new HtmlElementDecorator(elementToWrap) will be initialized using

public HtmlElementDecorator(SearchContext searchContext) {
    this(new HtmlElementLocatorFactory(searchContext));
}

constructor where default ElementLocatorFactory is used

@emaks
Copy link
Contributor Author

emaks commented Apr 1, 2015

In that case, child elements will be initialized by using default(HtmlElementLocatorFactory) class. but, I expect them to be initialized by using class from parent(NotDefaultElementLocatorFactory)

@artkoshelev
Copy link
Contributor

yep, i see now, that's a tricky piece... can you please share a problem you are trying to solve with custom ElementLocatorFactory? so i can write the test and propose solution? or you can write failing test describing your problem and pull request it.

@emaks
Copy link
Contributor Author

emaks commented Apr 7, 2015

I want to add possibility to change timeOutInSeconds parameter

@aik099
Copy link
Contributor

aik099 commented Apr 7, 2015

If I remember correctly there was a task for making that value configurable on per-element basis.

@artkoshelev
Copy link
Contributor

yep, here is abandoned PR #63, you can fix all the problems here and we'll merge it

@emaks
Copy link
Contributor Author

emaks commented May 11, 2015

please, see #82

@emaks
Copy link
Contributor Author

emaks commented May 19, 2015

@artkoshelev are there any updates in #82?

@artkoshelev
Copy link
Contributor

@emaks will look at it in a few days, thank you

@andrew-sumner
Copy link

andrew-sumner commented May 31, 2016

I was just about to raise an issue on this myself as have struck the same problem on work related to issue #56. Any idea when a new version might be released?

@artkoshelev
Copy link
Contributor

@andrew-sumner which issue you are talking about?

@andrew-sumner
Copy link

@artkoshelev

I have a custom decorator called that I use to initialise the page elements but this decorator is not passed onto inner elements.

PageFactory.initElements(new PageObjectAwareHtmlElementDecorator(new HtmlElementLocatorFactory(driver), this), this);

I've ended up working around the issue and have included the code so you can see what I've had to do to implement this.

It consist of two classes:

  1. PageObjectAwareHtmlElementDecorator: the custom decorator

  2. PageObjectAwareHtmlElementsLoader: this class allows me to create HtmlElements components on the fly rather than relying on fields.

    List<WageDeclarationsComponent> taskHistoryRecords = new PageObjectAwareHtmlElementsLoader(driver, this)
            .findElements(WageDeclarationsComponent.class, By.cssSelector(TASK_HISTORY_SELECTOR));
    

The web app that I am testing is, well bloody awful to be honest. It has up to four levels of nested iframes and is written using Dojo so is heavy with Ajax requests and I've found I've had to be very selective around where I create some of the components to avoid StaleElementReference and NoSuchElement exceptions.

I have two remaining issues with my implementation of the decorator that you may be able to help with:

  1. I am having problems with lazy loading of these components.

    My suspicion is that by setting the driver/pageobject or the fact that the components often contain lists then one of these conditions is forcing the elements to be loaded. Do you have any idea what may be forcing the load? And for bonus points: a solution?

  2. The overridden method decorateHtmlElementList() in my custom decorator is not using the decorator for creation of child elements (the original issue but with lists). I cannot follow the code so if you have any suggestions then that would be much appreciated...

NOTE: I haven't looked at TypifiedElements as I don't use those much.

PageObjectAwareHtmlElementDecorator

package nz.govt.msd.driver.web.pagefactory;

import static nz.govt.msd.driver.web.pagefactory.PageObjectAwareHtmlElementsLoader.createHtmlElement;
import static ru.yandex.qatools.htmlelements.utils.HtmlElementUtils.getElementName;

import java.lang.reflect.Field;
import java.util.List;

import org.openqa.selenium.WebElement;

import nz.govt.msd.driver.web.BasePageObject;
import ru.yandex.qatools.htmlelements.element.HtmlElement;
import ru.yandex.qatools.htmlelements.loader.decorator.HtmlElementDecorator;
import ru.yandex.qatools.htmlelements.pagefactory.CustomElementLocatorFactory;

/**
 * A custom implementation of the {@link HtmlElementDecorator} that supports the
 * {@link WebDriverAware} and {@link PageObjectAware} interfaces.
 * 
 * @author Andrew Sumner
 */
public class PageObjectAwareHtmlElementDecorator extends HtmlElementDecorator {
    private BasePageObject<?> pageObject;

    /**
     * Constructor.
     * 
     * @param factory Element locator factory
     * @param pageObject PageObject being decorated
     */
    public PageObjectAwareHtmlElementDecorator(CustomElementLocatorFactory factory, BasePageObject<?> pageObject) {
        super(factory);

        this.pageObject = pageObject;
    }

    @SuppressWarnings("unchecked")
    @Override
    protected <T extends HtmlElement> T decorateHtmlElement(ClassLoader loader, Field field) {
        WebElement elementToWrap = decorateWebElement(loader, field);

        // Calling our custom createHtmlElement method rather than the one supplied by Yandex
        return createHtmlElement((Class<T>) field.getType(), elementToWrap, getElementName(field), pageObject);
    }

    // TODO Will need to implement our own version of decorateHtmlElementList() in the same manner
    // that we have for decorateHtmlElement() so using PageObjectAwareHtmlElementDecorator
    @Override
    protected <T extends HtmlElement> List<T> decorateHtmlElementList(ClassLoader loader, Field field) {
        List<T> list = super.decorateHtmlElementList(loader, field);

        if (!list.isEmpty()) {
            if (list.get(0) instanceof WebDriverAware || list.get(0) instanceof PageObjectAware) {
                for (T element : list) {
                    setAwareValue(element);
                }
            }
        }

        return list;
    }

    private void setAwareValue(HtmlElement element) {
        if (element instanceof WebDriverAware) {
            ((WebDriverAware)element).setWebDriver(pageObject.getBrowser().getDriver());
        }

        if (element instanceof PageObjectAware) {
            ((PageObjectAware)element).setPageObject(pageObject);
        }   
    }
}

PageObjectAwareHtmlElementsLoader

package nz.govt.msd.driver.web.pagefactory;

import static ru.yandex.qatools.htmlelements.utils.HtmlElementUtils.getElementName;
import static ru.yandex.qatools.htmlelements.utils.HtmlElementUtils.newInstance;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.LinkedList;
import java.util.List;

import org.openqa.selenium.By;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.internal.WrapsDriver;
import org.openqa.selenium.support.PageFactory;

import nz.govt.msd.driver.web.BasePageObject;
import ru.yandex.qatools.htmlelements.element.HtmlElement;
import ru.yandex.qatools.htmlelements.exceptions.HtmlElementsException;
import ru.yandex.qatools.htmlelements.loader.decorator.HtmlElementLocatorFactory;

/**
 * A helper class for finding custom HtmlElement(s) in the same way that driver.findElement() and driver.findElements() methods work.
 * 
 * 
 * @author Andrew Sumner
 */
public class PageObjectAwareHtmlElementsLoader implements WrapsDriver {
    private final WebDriver driver;
    private BasePageObject<?> pageObject;

    /**
     * Constructor.
     * 
     * @param driver WebDriver
     */
    public PageObjectAwareHtmlElementsLoader(WebDriver driver, BasePageObject<?> pageObject) {
        this.driver = driver;
        this.pageObject = pageObject;
    }

    @Override
    public WebDriver getWrappedDriver() {
        return driver;
    }

    /**
     * Get the named field from the class.
     * 
     * @param elementClass Class that field belongs to
     * @param fieldName Name of field to find
     * @return Field if found, or null if not found.
     */
    public Field getFieldFromClass(Class<?> elementClass, String fieldName) {
        try {
            return elementClass.getDeclaredField(fieldName);
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * Find all elements within the current page using the given mechanism.
     * This method is affected by the 'implicit wait' times in force at the time of execution. When
     * implicitly waiting, this method will return as soon as there are more than 0 items in the
     * found collection, or will return an empty list if the timeout is reached.
     *
     * @param <T> A class that extends HtmlElement
     * @param elementClass The specific HtmlElement class wrapping the element 
     * @param by The locating mechanism to use
     * @return A list of all {@link WebElement}s, or an empty list if nothing matches
     * @see org.openqa.selenium.By
     * @see org.openqa.selenium.WebDriver.Timeouts
     */
    public <T extends HtmlElement> List<T> findElements(Class<T> elementClass, By by) {
        return findElements(elementClass, by, null);
    }

    /**
     * Find all elements within the current page using the given mechanism.
     * This method is affected by the 'implicit wait' times in force at the time of execution. When
     * implicitly waiting, this method will return as soon as there are more than 0 items in the
     * found collection, or will return an empty list if the timeout is reached.
     *
     * @param <T> A class that extends HtmlElement
     * @param elementClass The specific HtmlElement class wrapping the element
     * @param by The locating mechanism to use
     * @param field the field that is being updated - used to get the @name annotation
     * @return A list of all {@link WebElement}s, or an empty list if nothing matches
     * @see org.openqa.selenium.By
     * @see org.openqa.selenium.WebDriver.Timeouts
     */
    public <T extends HtmlElement> List<T> findElements(Class<T> elementClass, By by, Field field) {
        String name = null;

        if (field != null) {
            name = getElementName(field);
        }

        List<T> elements = new LinkedList<T>();

        for (WebElement element : driver.findElements(by)) {
            elements.add(createHtmlElement(elementClass, element, name, pageObject));
        }

        return elements;
    }

    /**
     * Find the first {@link WebElement} using the given method.
     * This method is affected by the 'implicit wait' times in force at the time of execution.
     * The findElement(..) invocation will return a matching row, or try again repeatedly until
     * the configured timeout is reached.
     *
     * findElement should not be used to look for non-present elements, use {@link #findElements(By)}
     * and assert zero length response instead.
     *
     * @param <T> A class that extends HtmlElement
     * @param elementClass The specific HtmlElement class wrapping the element 
     * @param by The locating mechanism
     * @return The first matching element on the current page
     * @throws org.openqa.selenium.NoSuchElementException If no matching elements are found
     * @see org.openqa.selenium.By
     * @see org.openqa.selenium.WebDriver.Timeouts
     */
    public <T extends HtmlElement> T findElement(Class<T> elementClass, By by) {
        WebElement element = driver.findElement(by);

        return createHtmlElement(elementClass, element, null, pageObject);
    }

    /**
     * Creates an instance of the given class representing a block of elements and initializes its fields
     * with lazy proxies.
     * 
     * Is a COPY of the Yandex HtmlElementLoader classes method so that replace
     * the call to populatePageObject() with one that supports our page decorator.
     *
     * @param <T> A class to be instantiated and initialized.
     * @param elementClass A class to be instantiated and initialized.
     * @param elementToWrap WebElement to wrap
     * @param name Name of element
     * @return Initialised instance of the specified class.
     */
    public static <T extends HtmlElement> T createHtmlElement(Class<T> elementClass, WebElement elementToWrap, String name, BasePageObject<?> parentPageObject) {
        try {
            T instance = newInstance(elementClass);
            instance.setWrappedElement(elementToWrap);
            instance.setName(name);

            // Add reference to pageobject / browser for those classes that want them
            setAwareValue(instance, parentPageObject);

            // Recursively initialize elements of the block
            populatePageObject(instance, elementToWrap, parentPageObject);
            return instance;
        } catch (NoSuchMethodException | InstantiationException | IllegalAccessException
                | InvocationTargetException e) {
            throw new HtmlElementsException(e);
        }
    }

    /**
     * Initialises fields of the given HtmlElements object using specified locator factory.
     *
     * @param page Page object to be initialised.
     * @param locatorFactory Locator factory that will be used to locate elements.
     */
    private static void populatePageObject(Object instance, SearchContext searchContext, BasePageObject<?> parentPageObject) {
        PageFactory.initElements(new PageObjectAwareHtmlElementDecorator(new HtmlElementLocatorFactory(searchContext), parentPageObject), instance);
    }

    private static void setAwareValue(HtmlElement element, BasePageObject<?> parentPageObject) {
        if (element instanceof WebDriverAware) {
            ((WebDriverAware) element).setWebDriver(parentPageObject.getBrowser().getDriver());
        }

        if (element instanceof PageObjectAware) {
            ((PageObjectAware) element).setPageObject(parentPageObject);
        }
    }
}

@IZaiarnyi
Copy link

IZaiarnyi commented Aug 26, 2019

@andrew-sumner Have you succeeded to overcome these issues? I am trying to make the solution #56 work, however, I am facing with same problems that you have mentioned above. So, I would be very grateful for any assistance or solution from your side.

@andrew-sumner
Copy link

andrew-sumner commented Aug 28, 2019 via email

@andrew-sumner
Copy link

This is my impletentaion, not sure if it will help you or not. I've been meaning to try to get these merged back into this project but haven't yet looked to see if I can maked it generic enough and other things always take priority...

https://github.com/concordion/cubano/tree/master/cubano-webdriver/src/main/java/org/concordion/cubano/driver/web/pagefactory

@IZaiarnyi
Copy link

Thank you, it has helped me a lot.
This functionality is definitely worth to be moved into htmlelements project.

This is my impletentaion, not sure if it will help you or not. I've been meaning to try to get these merged back into this project but haven't yet looked to see if I can maked it generic enough and other things always take priority...

https://github.com/concordion/cubano/tree/master/cubano-webdriver/src/main/java/org/concordion/cubano/driver/web/pagefactory

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants