package info.novatec.testit.webtester.pageobjects; import org.apache.commons.lang.StringUtils; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.SearchContext; import org.openqa.selenium.WebElement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import info.novatec.testit.webtester.api.browser.Browser; import info.novatec.testit.webtester.api.callbacks.PageObjectCallback; import info.novatec.testit.webtester.api.callbacks.PageObjectCallbackWithReturnValue; import info.novatec.testit.webtester.api.events.Event; import info.novatec.testit.webtester.api.exceptions.WrongElementClassException; import info.novatec.testit.webtester.api.pageobjects.Identification; import info.novatec.testit.webtester.api.pageobjects.PageObjectFactory; import info.novatec.testit.webtester.api.pageobjects.PageObjectList; import info.novatec.testit.webtester.eventsystem.EventSystem; import info.novatec.testit.webtester.eventsystem.events.pageobject.ClickedEvent; import info.novatec.testit.webtester.internal.annotations.SetViaInjection; import info.novatec.testit.webtester.internal.pageobjects.ActionTemplate; import info.novatec.testit.webtester.internal.pageobjects.PageObjectModel; import info.novatec.testit.webtester.internal.validation.MappingValidator; import info.novatec.testit.webtester.utils.Identifications; import info.novatec.testit.webtester.utils.Marker; import info.novatec.testit.webtester.utils.PageObjectFinder; import info.novatec.testit.webtester.utils.PageObjectFinder.IdentificationFinder; import info.novatec.testit.webtester.utils.PageObjectFinder.TypedFinder; /** * Base implementation for all page object classes. Any class sub-classing this * class must have a default constructor. Objects of this and any subclass * should not be initialized manually. Instead a {@linkplain PageObjectFactory} * must be used! * * @since 0.9.0 */ public class PageObject { private static final Logger logger = LoggerFactory.getLogger(PageObject.class); @SetViaInjection private PageObjectModel model; private ActionTemplate actionTemplate; private MappingValidator validator; /** * This is the hard coded {@link WebElement} used in case the page object acts as a wrapper instead of a proxy. */ private WebElement webElement; protected PageObject() { this.actionTemplate = new ActionTemplate(this); this.validator = new MappingValidator(getClass()); } /** * Tries to find the {@link WebElement web element} described by this * {@link PageObject page object's} {@link PageObjectModel model}. * <p> * If caching is active the web element will be stored once it was * successfully found and future invocations of this method will return the * cached instance. If caching is not active the web element will be * resolved anew with each invocation. * * @return the web element of this page object. * @throws NoSuchElementException if the web element could not be found. * @since 0.9.9 */ public WebElement getWebElement() { if(webElement != null) { return validate(webElement); } return validate(findWebElement()); } private WebElement findWebElement() { SearchContext searchContext = model.getSearchContext(); return searchContext.findElement(model.getSeleniumBy()); } /** * Executes a validation that checks the {@link WebElement} to make sure the * correct {@link PageObject} class was used to initialize it. If this check * fails an {@link WrongElementClassException} is thrown. * * @param element the element to check * @return the same web element instance in case the validation was successful * @throws WrongElementClassException if check fails * @since 0.9.9 */ protected final WebElement validate(WebElement element) { if (validator.canValidate()) { validator.assertValidity(element); } return element; } /** * @return the {@linkplain Browser browser} in which this * {@linkplain PageObject page object} is displayed. * @since 0.9.0 */ public Browser getBrowser() { return model.getBrowser(); } /** * @return this {@linkplain PageObject page object's} parent. Might be null * if this page object has no parent. * @since 0.9.0 */ public PageObject getParent() { return model.getParent(); } /** * @return this {@link PageObject page object's} human readable name. Might * be empty in case no name was given. * @since 0.9.9 */ public String getHumanReadableName() { return model.getName(); } protected PageObjectModel getModel() { return model; } /** * Executes a click on this {@linkplain PageObject page object}. Will throw * an exception if the page object is invisible but not if it is disabled! * * @return the same instance for fluent API * @since 0.9.0 */ public PageObject click() { executeAction(new PageObjectCallback() { @Override public void execute(PageObject pageObject) { getWebElement().click(); logger.debug(logMessage("clicked")); fireEventAndMarkAsUsed(new ClickedEvent(pageObject)); } }); return this; } /** * @return the visible text between the opening and closing tag represented * by this {@linkplain PageObject}. * @since 0.9.0 */ public String getVisibleText() { return executeAction(new PageObjectCallbackWithReturnValue<String>() { @Override public String execute(PageObject pageObject) { pageObject.markAsRead(); return getWebElement().getText(); } }); } /** * @return whether or not the {@linkplain PageObject} exists and is * currently visible * @since 0.9.0 */ public boolean isVisible() { return executeAction(new PageObjectCallbackWithReturnValue<Boolean>() { @Override public Boolean execute(PageObject pageObject) { try { return pageObject.getWebElement().isDisplayed(); } catch (NoSuchElementException e) { return Boolean.FALSE; } } }); } /** * Returns whether or not this {@link PageObject} is part of the current page's DOM. * This can be used to avoid catching the {@link NoSuchElementException} in order to check if an element exists. * * @return true if the object is part of the page's DOM * @see WebElement * @since 1.2.0 */ public boolean isPresent() { try { getWebElement(); return true; } catch (NoSuchElementException e) { return false; } } /** * @return whether or not the {@linkplain PageObject} is currently enabled * @since 0.9.0 */ public boolean isEnabled() { return executeAction(new PageObjectCallbackWithReturnValue<Boolean>() { @Override public Boolean execute(PageObject pageObject) { return getWebElement().isEnabled(); } }); } /** * Returns the underlying elements tag name. * <p> * This method is preferred to something like * <code>pageObject.getWebElement().getTageName();</code> because it uses * the page objects action mechanism to allow for default exception handling * and other features. * * @return the attribute's value as an integer * @since 0.9.7 */ public String getTagName() { return executeAction(new PageObjectCallbackWithReturnValue<String>() { @Override public String execute(PageObject pageObject) { return pageObject.getWebElement().getTagName(); } }); } /** * Sets the value of an attribute of this {@link PageObject} using JavaScript. * <p> * <b>Example:</b> * <pre> * // will change the value of a text field to 'foo' * textField.setAttribute("value", "foo"); * </pre> * * @param attributeName the name of the attribute to set * @param value the value to set the attribute to * @since 1.2.0 */ public void setAttribute(String attributeName, String value) { String escapedValue = StringUtils.replace(value, "\"", "\\\""); String script = "arguments[0]." + attributeName + " = \"" + escapedValue + "\""; getBrowser().javaScript().execute(script, this, value); } /** * @param attributeName the name of the attribute for which the value should * be returned * @return the value of the given attribute, or null if the attribute is not * set * @since 0.9.0 */ public String getAttribute(final String attributeName) { return executeAction(new PageObjectCallbackWithReturnValue<String>() { @Override public String execute(PageObject pageObject) { String attributeValue = getWebElement().getAttribute(attributeName); return log(attributeValue); } private String log(String value) { logger.trace(logMessage("returning '{}' for attribute '{}'"), value, attributeName); return value; } }); } /** * Returns the given attribute's value as an integer. If that attribute * could not be found <code>null</code> is returned! * * @param attributeName name of the attribute * @return the attribute's value as an integer */ protected Integer getAttributeAsInt(String attributeName) { /* handled in two distinct steps in order to guarantee correct exception * recognition */ final String attributeValue = getAttribute(attributeName); return executeAction(new PageObjectCallbackWithReturnValue<Integer>() { @Override public Integer execute(PageObject pageObject) { if (attributeValue == null) { return null; } return Integer.valueOf(attributeValue); } }); } /** * @param propertyName the name of the css property for which the value * should be returned * @return the value of the given css property or null if the property is * not set * @since 0.9.0 */ public String getCssProperty(final String propertyName) { return executeAction(new PageObjectCallbackWithReturnValue<String>() { @Override public String execute(PageObject pageObject) { return getWebElement().getCssValue(propertyName); } }); } /** * Creates a new {@link PageObjectFinder page object finder} for this page * object. This finder can be used to programmatically identify and create * {@link PageObject page objects}. * <p> * This page object will be used as the parent of all created page objects * an in doing so limit the search scope of the operation. Only elements * within this page objects HTML tags will be considered! * * @return the newly create finder * @since 0.9.9 */ public PageObjectFinder finder() { return new PageObjectFinder(this); } /** * Shorthand method for finding a {@link GenericElement generic page * element} via a CSS-Selector expression. * <p> * This page object will be used as the parent of all created page objects * an in doing so limit the search scope of the operation. Only elements * within this page objects HTML tags will be considered! * <p> * <b>Examples:</b> * <ul> * <li><code>pageObject.find("#username").sendKeys("testuser");</code></li> * <li><code>pageObject.find("#button").click();</code></li> * <li><code>pageObject.find("#headline").getVisibleText();</code></li> * </ul> * * @param cssSelector the CSS-Selector expression to use * @return the generic element for the given selector * @since 0.9.9 */ public GenericElement find(String cssSelector) { return finder().findGeneric().by(cssSelector); } /** * Shorthand method for finding a {@link PageObjectList list} of * {@link GenericElement generic page elements} via a CSS-Selector * expression. * <p> * This page object will be used as the parent of all created page objects * an in doing so limit the search scope of the operation. Only elements * within this page objects HTML tags will be considered! * <p> * <b>Examples:</b> * <ul> * <li><code>pageObject.findMany(".button").filter(is(visible()));</code> * </li> * </ul> * * @param cssSelector the CSS-Selector expression to use * @return the list of generic elements for the given selector * @since 0.9.9 */ public PageObjectList<GenericElement> findMany(String cssSelector) { return finder().findGeneric().manyBy(cssSelector); } /** * Shorthand method for creating a new {@link IdentificationFinder * identification based finder}. Matching {@link Identification * identification} instances can be created using the * {@link Identifications} utility class. * <p> * This page object will be used as the parent of all created page objects * an in doing so limit the search scope of the operation. Only elements * within this page objects HTML tags will be considered! * <p> * <b>Examples:</b> * <ul> * <li> * <code>pageObject.findBy(id("username")).as(TextField.class).setText("testuser");</code> * </li> * <li> * <code>pageObject.findBy(css("#button")).as(Button.class).click();</code> * </li> * <li> * <code>pageObject.findBy(xpath(".//h1")).asGeneric().getVisibleText();</code> * </li> * </ul> * * @param identification the identification to use when identifying an * element * @return the new identification finder * @since 0.9.9 */ public IdentificationFinder findBy(Identification identification) { return finder().findBy(identification); } /** * Shorthand method for creating a new {@link TypedFinder type based finder} * . The given page object class is used for all subsequent operations on * the finder. * <p> * This page object will be used as the parent of all created page objects * an in doing so limit the search scope of the operation. Only elements * within this page objects HTML tags will be considered! * <p> * <b>Examples:</b> * <ul> * <li> * <code>pageObject.find(TextField.class).by(id("username")).setText("testuser");</code> * </li> * <li> * <code>pageObject.find(Button.class).by(css("#button")).click();</code> * </li> * <li> * <code>pageObject.find(GenericElement.class).by(xpath(".//h1")).getVisibleText();</code> * </li> * </ul> * * @param <T> the type of the page object to create a finder for * @param pageObjectClass the page object class to use when creating an * element * @return the new type finder * @since 0.9.9 */ public <T extends PageObject> TypedFinder<T> find(Class<T> pageObjectClass) { return finder().find(pageObjectClass); } /** * Fires the given event using the {@linkplain EventSystem#fireEvent(Event)} * method and mark this page object as used using * {@linkplain Marker#markAsUsed(PageObject)}. * * @param event the event to fire. * @since 0.9.0 */ protected final void fireEventAndMarkAsUsed(Event event) { EventSystem.fireEvent(event); Marker.markAsUsed(this); logger.trace(logMessage("fired event: {}"), event); } protected final void markAsRead() { Marker.markAsRead(this); } protected final void markAsUsed() { Marker.markAsUsed(this); } /** * Creates a new instance for the given {@linkplain PageObject page object} * class using the {@linkplain Browser browser's} creation mechanism. This * is a convenience method. * * @param pageClass the class of which an instance should be created. * @param <T> the class of the {@link PageObject} to create * @return the created instance. * @since 0.9.6 */ protected final <T extends PageObject> T create(Class<T> pageClass) { return getBrowser().create(pageClass); } /** * Executes the given {@link PageObjectCallback callback} with this * {@link PageObject page object} as input. This is a convenience method for calling the * {@link ActionTemplate#executeAction(PageObjectCallback)} method. * * @param callback the callback to execute * @since 0.9.7 */ public final void executeAction(PageObjectCallback callback) { actionTemplate.executeAction(callback); } /** * Executes the given {@link PageObjectCallbackWithReturnValue callback} * with this {@link PageObject page object} as input and a return value of * type B as output. This is a convenience method for calling the * {@link ActionTemplate#executeAction(PageObjectCallbackWithReturnValue)} method. * * @param <B> the type of the return value of the action * @param callback the callback to execute * @return the return value of the callback * @since 0.9.7 */ public final <B> B executeAction(PageObjectCallbackWithReturnValue<B> callback) { return actionTemplate.executeAction(callback); } protected String logMessage(String message) { return model.getLogPrefix() + message; } @Override public String toString() { StringBuilder subject = new StringBuilder(getClass().getSimpleName()); String name = model.getName(); if (StringUtils.isNotBlank(name)) { subject.append(" \"").append(name).append('\"'); } Identification identification = model.getIdentification(); if (identification != null) { subject.append(" identified by ").append(identification); } return subject.toString(); } }