package jp.vmi.selenium.selenese.locator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.NoSuchFrameException;
import org.openqa.selenium.UnsupportedCommandException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.WebElement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jp.vmi.selenium.selenese.SeleneseRunnerRuntimeException;
import jp.vmi.selenium.selenese.utils.SeleniumUtils;
/**
* WebDriver Element Locator.
*
* Fully override the implementation of original ElementFinder.
*
* Note: This does not support ui locator.
*
* <ul>
* <li>@see <a href="https://code.google.com/p/selenium/source/browse/ide/main/src/content/selenium-core/reference.html">SeleniumIDE Reference</a>
* <li>@see <a href="https://code.google.com/p/selenium/source/browse/javascript/selenium-core/scripts/ui-doc.html">UI document</a>
* </ul>
*/
public class WebDriverElementFinder {
private static final Logger log = LoggerFactory.getLogger(WebDriverElementFinder.class);
/**
* Convert to option locator with parent.
*
* @param parentLocator parent locator.
* @param optionLocator child option locator.
* @return option locator with parent.
*/
@Deprecated
public static String convertToOptionLocatorWithParent(String parentLocator, String optionLocator) {
return parentLocator + Locator.OPTION_LOCATOR_SEPARATOR + optionLocator;
}
private final Map<String, LocatorHandler> handlerMap = new HashMap<>();
private final Map<String, OptionLocatorHandler> optionHandlerMap = new HashMap<>();
private final List<Locator> currentFrameLocators = new ArrayList<>();
private WebDriver noParentFrameWebDriver = null;
/**
* Constructor.
*/
public WebDriverElementFinder() {
registerHandler(new IdentifierHandler());
registerHandler(new IdHandler());
registerHandler(new NameHandler());
registerHandler(new DomHandler());
registerHandler(new XPathHandler());
registerHandler(new LinkHandler());
registerHandler(new CSSHandler());
registerHandler(new ClassHandler());
registerOptionHandler(new OptionLabelHandler());
registerOptionHandler(new OptionIdHandler());
registerOptionHandler(new OptionIndexHandler());
registerOptionHandler(new OptionValueHandler());
}
/**
* Register locator handler.
*
* @param handler locator handler.
* @return this.
*/
public WebDriverElementFinder registerHandler(LocatorHandler handler) {
handlerMap.put(handler.locatorType(), handler);
return this;
}
/**
* Register option locator handler.
*
* @param handler option locator handler.
* @return this.
*/
public WebDriverElementFinder registerOptionHandler(OptionLocatorHandler handler) {
optionHandlerMap.put(handler.optionLocatorType(), handler);
return this;
}
/**
* Get option locator handler.
*
* @param poptloc option locator.
* @return option locator handler.
*/
public OptionLocatorHandler getOptionHandler(OptionLocator poptloc) {
OptionLocatorHandler handler = optionHandlerMap.get(poptloc.type);
if (handler == null)
throw new UnsupportedOperationException("Unknown option locator type: " + poptloc);
return handler;
}
/**
* Add user defined handler of element finder.
*
* @param strategyName strategy name.
* @param implementation JavaScript code.
*/
public void add(String strategyName, String implementation) {
registerHandler(new AdditionalHandler(strategyName, implementation));
}
private void switchToFrame(WebDriver driver, List<Locator> plocs) {
driver.switchTo().defaultContent();
try {
List<Locator> selectedFrameLocators = new ArrayList<>();
for (Locator ploc : plocs) {
for (int index : ploc.frameIndexList)
driver.switchTo().frame(index);
if (!ploc.isTypeIndex()) {
WebElement frame = findElementsInternal(driver, ploc, selectedFrameLocators).get(0);
driver.switchTo().frame(frame);
}
selectedFrameLocators.add(ploc);
}
} catch (RuntimeException e) {
if (SeleniumUtils.isElementNotFound(e)) {
if (plocs == currentFrameLocators) {
log.warn("The selected frame has disapeared: {}", StringUtils.join(plocs, '/'));
log.warn("Reset selected frame.");
}
driver.switchTo().defaultContent();
plocs.clear();
} else {
throw e;
}
}
}
private void pushFrame(WebDriver driver, Locator ploc, int index) {
driver.switchTo().frame(index);
ploc.frameIndexList.addLast(index);
}
private boolean isParentFrameUnsupported(RuntimeException e) {
return (e instanceof UnsupportedCommandException)
|| (e instanceof WebDriverException
&& StringUtils.contains(e.getMessage(), "switchToParentFrame"));
}
private void popFrame(WebDriver driver, Locator ploc, List<Locator> selectedFrameLocators) {
ploc.frameIndexList.pollLast();
if (noParentFrameWebDriver != driver) {
try {
driver.switchTo().parentFrame();
return;
} catch (RuntimeException e) {
if (!isParentFrameUnsupported(e))
throw e;
log.warn("{} does not support \"parentFrame\".", driver.getClass().getSimpleName());
noParentFrameWebDriver = driver;
}
}
switchToFrame(driver, selectedFrameLocators);
for (int index : ploc.frameIndexList)
driver.switchTo().frame(index);
}
private List<WebElement> findElementsByLocator(LocatorHandler handler, WebDriver driver, Locator ploc, List<Locator> selectedFrameLocators) {
List<WebElement> result = handler.handle(driver, ploc.arg);
if (!result.isEmpty())
return result;
int iframeCount = driver.findElements(By.tagName("iframe")).size();
for (int index = 0; index < iframeCount; index++) {
pushFrame(driver, ploc, index);
result = findElementsByLocator(handler, driver, ploc, selectedFrameLocators);
if (result != null)
return result;
popFrame(driver, ploc, selectedFrameLocators);
}
int frameCount = driver.findElements(By.tagName("frame")).size();
for (int index = 0; index < frameCount; index++) {
pushFrame(driver, ploc, index);
result = findElementsByLocator(handler, driver, ploc, selectedFrameLocators);
if (result != null)
return result;
popFrame(driver, ploc, selectedFrameLocators);
}
return null;
}
/**
* Find "option" elements of specified option locator.
*
* @param element "select" element.
* @param poptloc parsed option locator.
* @return list of found "option" elements.
*/
public List<WebElement> findOptions(WebElement element, OptionLocator poptloc) {
return getOptionHandler(poptloc).handle(element, poptloc.arg);
}
private List<WebElement> findOptions(List<WebElement> elements, OptionLocator poptloc) {
if (elements.size() == 1) {
return findOptions(elements.get(0), poptloc);
} else {
OptionLocatorHandler handler = getOptionHandler(poptloc);
List<WebElement> result = new ArrayList<>();
for (WebElement element : elements)
result.addAll(handler.handle(element, poptloc.arg));
return result;
}
}
private List<WebElement> findElementsInternal(WebDriver driver, Locator ploc, List<Locator> selectedFrameLocators) {
LocatorHandler handler = handlerMap.get(ploc.type);
if (handler == null)
throw new UnsupportedOperationException("Unknown locator type: " + ploc);
List<WebElement> elements = findElementsByLocator(handler, driver, ploc, selectedFrameLocators);
if (elements == null)
throw new NoSuchElementException("Element " + ploc + " not found", new NoSuchElementException(ploc.toString()));
return ploc.poptloc == null ? elements : findOptions(elements, ploc.poptloc);
}
/**
* Find elements of specified locator.
*
* @param driver WebDriver.
* @param ploc parsed locator.
* @param selectedFrameLocators selected frame locators.
* @return list of found elements. (empty if no element)
*/
public List<WebElement> findElements(WebDriver driver, Locator ploc, List<Locator> selectedFrameLocators) {
if (ploc.isTypeRelative()) {
if (ploc.isRelativeTop()) {
driver.switchTo().defaultContent();
} else if (ploc.isRelativeParent()) {
int size = selectedFrameLocators.size();
if (size > 0)
switchToFrame(driver, selectedFrameLocators.subList(0, size - 1));
else
log.warn("The current selected frame is top level. \"" + ploc + "\" is ignored.");
} else {
throw new SeleneseRunnerRuntimeException("Invalid \"relative\" locator argument: " + ploc.arg);
}
return Arrays.asList(driver.switchTo().activeElement());
} else if (ploc.isTypeIndex()) {
switchToFrame(driver, selectedFrameLocators);
List<WebElement> frames = driver.findElements(By.tagName("iframe"));
if (frames.isEmpty())
frames = driver.findElements(By.tagName("frame"));
int index = ploc.getIndex();
if (index < 0 || index >= frames.size())
throw new SeleneseRunnerRuntimeException("\"index\" locator argument is out of range: " + ploc.arg);
return Arrays.asList(frames.get(index));
} else {
switchToFrame(driver, selectedFrameLocators);
return findElementsInternal(driver, ploc, selectedFrameLocators);
}
}
/**
* Find elements of specified locator.
*
* @param driver WebDriver.
* @param ploc parsed locator.
* @return list of found elements. (empty if no element)
*/
public List<WebElement> findElements(WebDriver driver, Locator ploc) {
return findElements(driver, ploc, currentFrameLocators);
}
/**
* Find elements of specified locator.
*
* @param driver WebDriver.
* @param locator locator.
* @return list of found elements. (empty if no element)
*/
public List<WebElement> findElements(WebDriver driver, String locator) {
return findElements(driver, new Locator(locator));
}
/**
* Find an element of specified locator.
*
* @param driver WebDriver.
* @param ploc parsed locator.
* @param selectedFrameLocators selected frame locators.
* @return found element.
* @throws NoSuchElementException throw if element not found.
*/
public WebElement findElement(WebDriver driver, Locator ploc, List<Locator> selectedFrameLocators) {
List<WebElement> elements = findElements(driver, ploc, selectedFrameLocators);
if (elements.isEmpty())
throw new NoSuchElementException(ploc.toString());
return elements.get(0);
}
/**
* Find an element.
*
* @param driver WebDriver.
* @param ploc parsed locator.
* @return found element.
* @throws NoSuchElementException throw if element not found.
*/
public WebElement findElement(WebDriver driver, Locator ploc) {
List<WebElement> elements = findElements(driver, ploc);
if (elements.isEmpty())
throw new NoSuchElementException(ploc.toString());
return elements.get(0);
}
/**
* Find an element.
*
* @param driver WebDriver.
* @param locator locator.
* @return found element.
* @throws NoSuchElementException throw if element not found.
*/
public WebElement findElement(WebDriver driver, String locator) {
return findElement(driver, new Locator(locator));
}
/**
* Select frame or iframe.
*
* @param driver WebDriver.
* @param ploc parsed locator to frame/iframe.
*/
public void selectFrame(WebDriver driver, Locator ploc) {
if (ploc.isTypeRelative()) {
if (ploc.isRelativeTop()) {
driver.switchTo().defaultContent();
currentFrameLocators.clear();
} else if (ploc.isRelativeParent()) {
int size = currentFrameLocators.size();
if (size > 0) {
currentFrameLocators.remove(size - 1);
switchToFrame(driver, currentFrameLocators);
} else {
log.warn("The current selected frame is top level. \"" + ploc + "\" is ignored.");
}
} else { // neither "relative=top" nor "relative=parent"
throw new SeleneseRunnerRuntimeException("Invalid frame locator: " + ploc);
}
} else if (ploc.isTypeIndex()) {
switchToFrame(driver, currentFrameLocators);
int index = ploc.getIndex();
try {
driver.switchTo().frame(index);
} catch (NoSuchFrameException e) {
throw new SeleneseRunnerRuntimeException(e);
}
ploc.frameIndexList.add(index);
currentFrameLocators.add(ploc);
} else {
switchToFrame(driver, currentFrameLocators);
WebElement frame = findElementsInternal(driver, ploc, currentFrameLocators).get(0);
driver.switchTo().frame(frame);
currentFrameLocators.add(ploc);
}
}
/**
* Select frame or iframe.
*
* @param driver WebDriver.
* @param locator locator to frame/iframe.
*/
public void selectFrame(WebDriver driver, String locator) {
selectFrame(driver, new Locator(locator));
}
/**
* Get copy of current frame locators.
*
* @return current frame locators.
*/
public List<Locator> getCurrentFrameLocators() {
return new ArrayList<>(currentFrameLocators);
}
}