package org.syftkog.web.test.framework; import java.net.MalformedURLException; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import static org.syftkog.web.test.framework.StepLogger.*; import org.openqa.selenium.By; import org.openqa.selenium.Dimension; import org.openqa.selenium.InvalidCookieDomainException; import org.openqa.selenium.InvalidElementStateException; import org.openqa.selenium.Keys; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.NotFoundException; import org.openqa.selenium.Point; import org.openqa.selenium.SearchContext; import org.openqa.selenium.StaleElementReferenceException; import org.openqa.selenium.TimeoutException; import org.openqa.selenium.UnableToSetCookieException; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.WebElement; import org.openqa.selenium.interactions.Actions; import org.openqa.selenium.interactions.internal.Coordinates; import org.openqa.selenium.internal.Locatable; import org.openqa.selenium.internal.WrapsElement; import org.openqa.selenium.support.ui.Select; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.Assert; /** * @author BenjaminLimb * @param <T> */ public class Element<T extends Element> implements Locatable, WrapsElement, SearchContext, Coordinates, HasDriver, HasSearchContext { /** * */ public final Logger LOG = LoggerFactory.getLogger(Element.class); /** * */ public static final long IMPLICIT_WAIT_TIME_IN_SECONDS = Long.parseLong(PropertiesRetriever.getString("element.implicitWaitTimeInSeconds", "10")); /** * */ public static final long ELEMENT_RETRY_INTERVAL_MILLISECONDS = Long.parseLong(PropertiesRetriever.getString("element.actionRetryIntervalInMilliseconds", "100")); private final String elementName; private String elementSelector; private final Driver driver; private final SearchContext searchContext; private WebElement element; private WebElementFinder elementFinder; private final ElementAssertions<T> assertThatElement = new ElementAssertions<>((T) this); private final ElementWaits<T> waitUntil = new ElementWaits<>((T) this); /** * * @return */ public ElementAssertions assertElement() { return assertThatElement; } /** * * @return */ public ElementWaits waitUntil() { return waitUntil; } /** * * @param hasDriver * @param name * @param selector */ public Element(HasDriver hasDriver, String name, String selector) { this.driver = hasDriver.getDriver(); this.searchContext = hasDriver.getDriver(); this.elementName = name; this.elementSelector = selector; } /** * * @param hasDriver * @param hasSearchContext * @param name * @param selector */ public Element(HasDriver hasDriver, HasSearchContext hasSearchContext, String name, String selector) { this.driver = hasDriver.getDriver(); this.searchContext = hasSearchContext.getSearchContext(); this.elementName = name; this.elementSelector = selector; } /** * * @return */ @Override public WebElement getWrappedElement() { return element; } /** * * @return */ @Override public Driver getDriver() { return driver; } /** * * @return */ @Override public SearchContext getSearchContext() { return this; } /** * * @return */ protected By getBy() { Assert.assertNotNull(elementSelector, "Element selector is null."); return SeleniumHelperUtil.convertToBy(elementSelector); } /** * * @param element * @return */ public Element initializeTo(WebElement element) { this.element = element; return this; } private WebElement findWebElement() { if (elementSelector == null && elementFinder != null) { return elementFinder.find(); } Assert.assertNotNull(elementSelector, "Cannot find an element if the selector is null and no elementFinder has been specified."); return searchContext.findElement(getBy()); } private void initializeElementIfNull() { if (element == null) { initializeElement(); } } public void initializeElement() { this.element = findWebElement(); Assert.assertNotNull(element); } /** * * @return */ public String getName() { return elementName; } /** * * @return */ public String getType() { return this.getClass().getSimpleName(); } /** * * @return */ public Element prepareToNavigateToNewUrl() { getDriver().prepareToNavigateToNewUrl(); return this; } /** * * @return */ public Element waitForNavigateToNewUrl() { logAction("WAIT FOR NAVIGATE TO NEW URL"); getDriver().waitForNavigateToNewUrl(); return this; } /** * * @param keysToSend * @return */ public Element sendKeys(final CharSequence keysToSend) { StringBuilder b = new StringBuilder(keysToSend); logActionWithParameter("TYPE", "\"" + displayKeys(b.toString() + "\"")); new RetryUntilTimeout<Object>() { @Override Object commandsToRun() { element.sendKeys(keysToSend); return null; } }.run(); return this; } /** * * @return */ protected String elementStates() { if (element != null) { Boolean enabled = element.isEnabled(); Boolean displayed = element.isDisplayed(); Boolean selected = element.isSelected(); return "\r\n " + "Element Name:\"" + this.elementName + "\"" + "\r\n " + "Element Type: " + getType() + "\r\n " + "Selector:\"" + this.elementSelector + "\"" + "\r\n " + "Enabled:" + enabled + "\r\n " + "Displayed:" + displayed + "\r\n " + "Selected:" + selected; } else { return "Element was not initialized."; } } /** * * @param text * @return */ protected String displayKeys(String text) { if (text == null) { return null; } text = text.replace("\r\n", "[ENTER]"); text = text.replace("\t", "[TAB]"); return text; } /** * * @param text */ protected void logStep(String text) { getDriver().logStep(text); } /** * * @param action */ protected void logAction(String action) { logStep(action + " " + toString()); } /** * * @param action * @param parameter */ protected void logActionWithParameter(String action, String parameter) { logStep(action + " " + displayKeys(parameter) + " in " + toString()); } /** * * @param action * @param ex */ protected void logActionFailed(String action, Exception ex) { logStep(action + " failed. " + ex.getMessage() + toString()); } /** * * @param action * @param ex */ protected void logActionFailedWillRetry(String action, Exception ex) { logStep(action + " failed. " + ex.getMessage() + " Will Retry:" + toString()); } // add handling to scroll when needed. // WebDriverHelperUtil.scrollIntoViewThenClick(driver, link); /** * * @return */ public Element click() { this.logAction("CLICK"); new RetryUntilTimeout<Boolean>() { @Override Boolean commandsToRun() { // int elementPosition = element.getLocation().getY(); // String js = String.format("window.scroll(0, %s)", elementPosition); // ((JavascriptExecutor) driver).executeScript(js); try { Thread.sleep(500); } catch (InterruptedException ex) { java.util.logging.Logger.getLogger(Element.class.getName()).log(Level.SEVERE, null, ex); } element.click(); return true; } }.run(); return this; } /** * * @return */ public Element hover() { this.logAction("HOVER OVER"); new RetryUntilTimeout<Object>() { @Override Object commandsToRun() { Actions actions = new Actions(getDriver()); actions.moveToElement(element); actions.perform(); return null; } }.run(); return this; } //comment about this function /** * * @return */ public Element pressShift() { this.logAction("SHIFT CLICK"); new RetryUntilTimeout<Object>() { @Override Object commandsToRun() { Actions actions = new Actions(getDriver()); actions.moveToElement(element); actions.keyDown(Keys.SHIFT); actions.perform(); return null; } }.run(); return this; } /** * * @return */ public Long getNumberAsLong() { String text = getText(); if (text == null) { return null; } text = text.replace(",", ""); // remove commas TODO: this may break in internationization try { return Long.parseLong(text); } catch (NumberFormatException ex) { getDriver().getStepLogger().log(TRACE, "ParseException: Could not parse \"" + text + "\" into Long "); throw new RuntimeException("\"ParseException: Could not parse \\\"\" + text+\"\\\" into Number \""); } } /** * * @return */ public String getTagName() { return new RetryUntilTimeout<String>() { @Override String commandsToRun() { return element.getTagName(); } }.run(); } /** * * @return */ public Point getLocation() { return new RetryUntilTimeout<Point>() { @Override Point commandsToRun() { return element.getLocation(); } }.run(); } /** * * @return */ public Dimension getSize() { return new RetryUntilTimeout<Dimension>() { @Override Dimension commandsToRun() { return element.getSize(); } }.run(); } /** * * @param propertyName * @return */ public String getCssValue(final String propertyName) { return new RetryUntilTimeout<String>() { @Override String commandsToRun() { return element.getCssValue(propertyName); } }.run(); } /** * */ public void submit() { logAction("SUBMIT"); new RetryUntilTimeout<Boolean>() { @Override Boolean commandsToRun() { element.submit(); return true; } }.run(); } /** * * @param attributeKey * @return */ public String getAttribute(final String attributeKey) { return new RetryUntilTimeout<String>() { @Override String commandsToRun() { return element.getAttribute(attributeKey); } }.run(); } /** * * @return */ public Element clear() { this.logAction("CLEAR"); new RetryUntilTimeout<Boolean>() { @Override Boolean commandsToRun() { element.clear(); return true; } }.run(); return this; } /** * * @return */ public String getText() { return new RetryUntilTimeout<String>() { @Override String commandsToRun() { String text = element.getText(); if (text == null || text.equals("")) { text = element.getAttribute("value"); } return text; } }.run(); } /** * * @param text * @return */ public Element selectByValue(final String text) { driver.logStep("SELECT ELEMENT BY VALUE value='" + text + "' : \"" + toString() + ") "); new RetryUntilTimeout<Boolean>() { @Override Boolean commandsToRun() { Select select = new Select(element); select.selectByValue(text); return true; } }.run(); return this; } /** * * @param text * @return */ public Element selectByVisibleText(final String text) { driver.logStep("SELECT ELEMENT BY TEXT [" + text + "' : \"" + toString() + " (" + this.elementSelector + ")"); new RetryUntilTimeout<Boolean>() { @Override Boolean commandsToRun() { Select select = new Select(element); select.selectByVisibleText(text); return true; } }.run(); return this; } /** * * @return */ public String getSelectboxValue() { return new RetryUntilTimeout<String>() { @Override String commandsToRun() { Select select = new Select(element); String value = select.getFirstSelectedOption().getText(); return value; } }.run(); } /** * Sometimes we want to fail immediately, such as when we are waiting for an * element to be invisible.For this reason it it not wrapped in the * webDriverRetry. * * @param by * @return */ @Override public List<WebElement> findElements(final By by) { try { initializeElementIfNull(); return element.findElements(by); } catch (StaleElementReferenceException ex) { getDriver().getStepLogger().log(WARN, toString() + "\n" + ex.toString()); initializeElement(); return element.findElements(by); } } /** * Sometimes we want to fail immediately, such as when we are waiting for an * element to be invisible. * * @param by * @return */ @Override public WebElement findElement(final By by) { try { initializeElementIfNull(); return element.findElement(by); } catch (StaleElementReferenceException ex) { getDriver().getStepLogger().log(WARN, toString() + "\n" + ex.toString()); initializeElement(); return element.findElement(by); } } /** * * @param where * @return */ public Element mouseMove(final Coordinates where) { this.logAction("MOUSE MOVE"); new RetryUntilTimeout<Boolean>() { @Override Boolean commandsToRun() { getDriver().getMouse().mouseMove(where); return true; } }.run(); return this; } /** * * @param where * @param xOffset * @param yOffset * @return */ public Element mouseMove(final Coordinates where, final long xOffset, final long yOffset) { this.logAction("MOUSE MOVE"); new RetryUntilTimeout<Boolean>() { @Override Boolean commandsToRun() { getDriver().getMouse().mouseMove(where, xOffset, yOffset); return true; } }.run(); return this; } /** * * @param relativePoint * @return */ public Element click(Point relativePoint) { return click(relativePoint.getX(), relativePoint.getY()); } /** * * @param xOffset * @param yOffset * @return */ public Element click(final long xOffset, final long yOffset) { this.logAction("MOUSE CLICK at relative position: (" + xOffset + "," + yOffset + ")"); new RetryUntilTimeout<Boolean>() { @Override Boolean commandsToRun() { getDriver().getMouse().mouseMove(getCoordinates(), xOffset, yOffset); getDriver().getMouse().mouseDown(null); getDriver().getMouse().mouseUp(null); return true; } }.run(); return this; } /** * * @return */ public Element mouseDown() { this.logAction("MOUSE DOWN"); new RetryUntilTimeout<Boolean>() { @Override Boolean commandsToRun() { getDriver().getMouse().mouseDown(getCoordinates()); return true; } }.run(); return this; } /** * * @return */ public Element mouseUp() { this.logAction("MOUSE UP"); new RetryUntilTimeout<Boolean>() { @Override Boolean commandsToRun() { getDriver().getMouse().mouseUp(null); return true; } }.run(); return this; } /** * * @return */ @Override public Coordinates getCoordinates() { return new RetryUntilTimeout<Coordinates>() { @Override Coordinates commandsToRun() { return ((Locatable) element).getCoordinates(); } }.run(); } /** * * @return */ @Override public Point onScreen() { return getCoordinates().onScreen(); } /** * * @return */ @Override public Point inViewPort() { return getCoordinates().inViewPort(); } /** * * @return */ @Override public Point onPage() { return getCoordinates().onPage(); } /** * * @return */ @Override public Object getAuxiliary() { return getCoordinates().getAuxiliary(); } /** * * @return */ public String getElementName() { return elementName; } /** * * @return */ public String getElementSelector() { return elementSelector; } /** * * @param <T> */ protected abstract class RetryUntilTimeout<T> { /** * * @return */ public T run() { //getDriver().manage().timeouts().implicitlyWait(1, TimeUnit.SECONDS); // DON'T USE - see http://stackoverflow.com/questions/20268396/mixing-implicit-and-explicit-waits long timeout = System.currentTimeMillis() + IMPLICIT_WAIT_TIME_IN_SECONDS * 1000; while (System.currentTimeMillis() < timeout) { try { initializeElementIfNull(); return commandsToRun(); } catch (StaleElementReferenceException ex) { getDriver().getStepLogger().log(WARN, "StaleElementReferenceException - Refresh element to increase performance. " + parentToString()); element = null; // Since it's stale, we clear it and let it start the loop over immediately } catch (InvalidElementStateException | InvalidCookieDomainException | NotFoundException | UnableToSetCookieException ex) { getDriver().getStepLogger().log(WARN, parentToString() + "\n" + ex.toString()); element = null; Driver.sleepForMilliseconds(ELEMENT_RETRY_INTERVAL_MILLISECONDS); } catch (WebDriverException ex) { if (ex.getMessage().contains("Element is not currently interactable and may not be manipulated")) { Driver.sleepForMilliseconds(ELEMENT_RETRY_INTERVAL_MILLISECONDS); } else if (ex.getMessage().contains("Element is not clickable at point")) { // needed because chrome driver doesn't throw ElementNotVisibleException when something is positioned off of the screen Driver.sleepForMilliseconds(ELEMENT_RETRY_INTERVAL_MILLISECONDS); } else { throw ex; } } } //Last Try before timeout try { initializeElementIfNull(); return commandsToRun(); } catch (WebDriverException ex) { throw new TimeoutException(ex); } } abstract T commandsToRun(); } /** * * @return */ public WebElementFinder getElementFinder() { return elementFinder; } /** * * @param elementFinder * @return */ public T setElementFinder(WebElementFinder elementFinder) { this.elementFinder = elementFinder; return (T) this; } protected T setElementSelector(String selector){ this.elementSelector = selector; return (T) this; } /** * * @return */ public SearchContext getParentSearchContext() { return searchContext; } /** * * @return - is the element in the dom? */ public Boolean isLoaded() { getDriver().manage().timeouts().implicitlyWait(0, TimeUnit.SECONDS); try { initializeElement(); return element != null; } catch (NoSuchElementException ex) { return false; } } /** * * @return */ public Boolean isDisplayed() { getDriver().manage().timeouts().implicitlyWait(0, TimeUnit.SECONDS); try { initializeElement(); return element.isDisplayed(); } catch (NoSuchElementException ex) { return false; } } /** * * @return */ public Boolean isNotDisplayed() { return !isDisplayed(); } /** * * @return */ public Boolean isSelected() { getDriver().manage().timeouts().implicitlyWait(0, TimeUnit.SECONDS); try { initializeElement(); return element.isSelected(); } catch (NoSuchElementException ex) { return false; } } /** * * @return */ public Boolean isEnabled() { getDriver().manage().timeouts().implicitlyWait(0, TimeUnit.SECONDS); try { initializeElement(); return element.isEnabled(); } catch (NoSuchElementException ex) { return false; } } /** * * @return */ public Boolean isNotEnabled() { return !isEnabled(); } /** * * @param <P> * @param clazz * @param env * @return */ public <P extends Page> P fixUrlAndGotoLink(final Class<P> clazz, final Environment env) { this.logAction("LOAD PAGE " + clazz); return new RetryUntilTimeout<P>() { @Override P commandsToRun() { String href = getAttribute("href"); try { Assert.assertNotNull(env); String newhref = env.fixHost(href); driver.navigate().to(newhref); return PageFactory.createPage(getDriver(), clazz); } catch (MalformedURLException ex) { getDriver().getStepLogger().log(WARN, "INVALID HREF ON LINK:" + href); throw new InvalidElementStateException("INVALID HREF ON LINK:" + href); } } }.run(); } /** * * @param <P> * @param clazz * @return */ public <P extends Page> P clickAndLoadPage(Class<P> clazz) { click(); this.logAction("LOAD PAGE " + clazz); P page = PageFactory.createPage(getDriver(), clazz); page.waitUntil().correctPage(); page.waitUntilLoaded(); return page; } /** * * @param page * @return */ public Page clickAndLoadPage(Page page) { click(); logStep("WAIT FOR " + page.getUrl()); getDriver().addPageToDriverHistory(page); page.waitUntil().correctPage(); page.waitUntilLoaded(); return page; } /** * * @return */ public String parentToString() { return toString(); } @Override public String toString() { return "Element:\"" + this.elementName + "\" Type:\"" + this.getType() + "\" Selector:\"" + this.elementSelector + "\""; } }