yandex-qatools / htmlelements

Html Elements is a Java framework providing easy-to-use way of interaction with web-page elements in web-page tests.
Other
271 stars 116 forks source link

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

Open emaks opened 9 years ago

emaks commented 9 years ago

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 commented 9 years ago

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

emaks commented 9 years ago

here you are

aik099 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

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 commented 9 years ago

I want to add possibility to change timeOutInSeconds parameter

aik099 commented 9 years ago

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

artkoshelev commented 9 years ago

yep, here is abandoned PR https://github.com/yandex-qatools/htmlelements/pull/63, you can fix all the problems here and we'll merge it

emaks commented 9 years ago

please, see #82

emaks commented 9 years ago

@artkoshelev are there any updates in #82?

artkoshelev commented 9 years ago

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

andrew-sumner commented 8 years ago

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 commented 8 years ago

@andrew-sumner which issue you are talking about?

andrew-sumner commented 8 years ago

@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 commented 5 years ago

@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 commented 5 years ago

I have, I’ll post a link to the code tomorrow.

On 27/08/2019, at 7:33 AM, IZaiarnyi notifications@github.com<mailto:notifications@github.com> wrote:

@andrew-sumnerhttps://github.com/andrew-sumner Have you succeeded to overcome these issues? I am trying to make the solution #56 https://github.com/yandex-qatools/htmlelements/issues/56#issuecomment-190935126 works, 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.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHubhttps://github.com/yandex-qatools/htmlelements/issues/78?email_source=notifications&email_token=ACB74KBJDREN43ZC5HAMHKDQGQVZTA5CNFSM4A7BJ2N2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD5FNQXQ#issuecomment-524998750, or mute the threadhttps://github.com/notifications/unsubscribe-auth/ACB74KBLYXOL3MB7MP63QKDQGQVZTANCNFSM4A7BJ2NQ.

andrew-sumner commented 5 years ago

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 commented 5 years ago

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