package com.nordstrom.automation.selenium.model; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; import org.apache.commons.lang3.StringUtils; import org.apache.http.NameValuePair; import org.apache.http.client.utils.URIBuilder; import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.message.BasicNameValuePair; import org.openqa.selenium.By; import org.openqa.selenium.SearchContext; import org.openqa.selenium.StaleElementReferenceException; import org.openqa.selenium.TimeoutException; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.ui.Select; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Function; import com.nordstrom.automation.selenium.AbstractSeleniumConfig.SeleniumSettings; import com.nordstrom.automation.selenium.AbstractSeleniumConfig.WaitType; import com.nordstrom.automation.selenium.SeleniumConfig; import com.nordstrom.automation.selenium.annotations.PageUrl; import com.nordstrom.automation.selenium.core.WebDriverUtils; import com.nordstrom.automation.selenium.exceptions.LandingPageMismatchException; import com.nordstrom.automation.selenium.exceptions.PageNotLoadedException; import com.nordstrom.automation.selenium.interfaces.WrapsContext; import com.nordstrom.automation.selenium.model.Page.WindowState; import com.nordstrom.automation.selenium.support.Coordinator; import com.nordstrom.automation.selenium.support.SearchContextWait; import com.nordstrom.common.base.UncheckedThrow; /** * This is a abstract base class for all of the container classes defined by <b>Selenium Foundation</b>. */ @SuppressWarnings({"squid:S1200", "squid:S1774"}) public abstract class ComponentContainer extends Enhanceable<ComponentContainer> implements SearchContext, WrapsContext { /** * This interface provides common methods for collections of Selenium locators ({@link By} objects) */ public interface ByEnum { /** * Get the Selenium locator for this enumerated constant. * * @return Selenium locator ({@link By} object) for this constant */ By locator(); } protected WebDriver driver; protected SearchContext context; protected ComponentContainer parent; protected Method vacater; protected SearchContextWait wait; private List<Class<?>> bypassClasses; private List<String> bypassMethods; public static final By SELF = By.xpath("."); private static final String PLACEHOLDER = "{}"; private static final Class<?>[] BYPASS_CLASSES = {Object.class, WrapsContext.class}; private static final String[] BYPASS_METHODS = {"validateParent", "getDriver", "getContext", "getParent", "getParentPage", "getWait", "switchTo", "switchToContext", "getVacater", "setVacater", "isVacated", "enhanceContainer", "bypassClassOf", "bypassMethod", "getLogger", "hashCode", "equals", "getArgumentTypes", "getArguments"}; private static final Class<?>[] ARG_TYPES = {SearchContext.class, ComponentContainer.class}; private static final Class<?>[] COLLECTIBLE_ARGS = {RobustWebElement.class, ComponentContainer.class}; private static final String ELEMENT_MESSAGE = "[element] must be non-null"; private static final int PARAM_NAME_ONLY = 1; private static final int NAME_WITH_VALUE = 2; private static final String LOOPBACK = "http://127.0.0.1/"; private final Logger logger; /** * Constructor for component container * * @param context container search context * @param parent container parent (may be {@code null} for {@link Page} objects */ public ComponentContainer(final SearchContext context, final ComponentContainer parent) { Objects.requireNonNull(context, "[context] must be non-null"); validateParent(parent); this.context = context; this.driver = WebDriverUtils.getDriver(context); this.parent = parent; logger = LoggerFactory.getLogger(getContainerClass(this)); } /** * Validate the specified parent object * * @param parent container parent */ protected void validateParent(final ComponentContainer parent) { Objects.requireNonNull(parent, "[parent] must be non-null"); } /** * Get the driver associated with this container * * @return container driver */ public WebDriver getDriver() { return driver; } /** * Get the container search context * * @return container search context */ public SearchContext getContext() { return context; } /** * Get the parent of this container * * @return parent container */ public ComponentContainer getParent() { return parent; } /** * Get the parent page for this container * * @return container parent page */ public Page getParentPage() { if (parent != null) { return parent.getParentPage(); } return (Page) this; } /** * Convenience method to get a search context wait object for this container * * @return {@link SearchContextWait} object with timeout specified by {@link SeleniumSettings#WAIT_TIMEOUT} */ public SearchContextWait getWait() { if (wait == null) { wait = WaitType.WAIT.getWait(this); } return wait; } /** * Get SearchContextWait object with 10 second timeout * * @param context search context * @return new SearchContextWait object */ public static SearchContextWait getWait(SearchContext context) { return new SearchContextWait(context, WaitType.WAIT.getInterval()); } /** * Wait until with specified condition is met * * @param <T> return type of the specified condition * @param condition 'condition' function object * @return output of the specified condition */ public <T> T waitUntil(Function<SearchContext, T> condition) { try { return getWait().until(condition); } catch (TimeoutException e) { if (e.getClass().equals(TimeoutException.class) && (condition instanceof Coordinator)) { e = ((Coordinator<T>) condition).differentiateTimeout(e); } throw e; } } /** * Convenience method to get a search context wait object of the specified type for this container * * @param waitType wait type being requested * @return {@link SearchContextWait} object of the specified type for this container */ public SearchContextWait getWait(final WaitType waitType) { return waitType.getWait(this); } /** * Switch focus to this container's search context. * <p> * <b>NOTE</b>: This method walks down the container lineage to the parent page object, then back up to this * container, focusing the driver on each container as it goes. * * @return this container's context */ @Override public SearchContext switchTo() { return getWait().until(contextIsSwitched(this)); } /** * Returns a 'wait' proxy that switches focus to the specified context * * @param context search context on which to focus * @return target search context */ static Coordinator<SearchContext> contextIsSwitched(final ComponentContainer context) { return new Coordinator<SearchContext>() { /** * {@inheritDoc} */ @Override public SearchContext apply(final SearchContext ignore) { if (context.parent != null) { context.parent.switchTo(); } try { return context.switchToContext(); } catch (StaleElementReferenceException e) { //NOSONAR return context.refreshContext(context.acquiredAt()); } } /** * {@inheritDoc} */ @Override public String toString() { return "context to be switched"; } }; } /** * Switch focus to this container's search context. * <p> * <b>NOTE</b>: This method walks down the container lineage to the parent page object, then back up to this * container, focusing the driver on each container as it goes. * * @return this container's context */ protected abstract SearchContext switchToContext(); /** * Get the method that caused this container to be vacated. * * @return vacating method; 'null' if container is still valid */ Method getVacater() { if (vacater != null) { return vacater; } else if (parent != null) { return parent.getVacater(); } else { return null; } } /** * Set the method that caused this container to be vacated. * * @param vacater vacating method */ void setVacater(final Method vacater) { this.vacater = vacater; if (parent != null) { parent.setVacater(vacater); } } /** * Determine if this container has been vacated. * * @return 'true' if container has been vacated; otherwise 'false' */ boolean isVacated() { return (null != getVacater()); } /** * Find all elements within the current context using the given locator constant. * * @param constant the locator constant * @return a list of all WebElements, or an empty list if nothing matches */ public List<WebElement> findElements(final ByEnum constant) { return findElements(constant.locator()); } /** * Find all elements within the current context using the given mechanism. * * @param by the locating mechanism * @return a list of all WebElements, or an empty list if nothing matches */ @Override public List<WebElement> findElements(final By by) { return RobustElementFactory.getElements(this, by); } /** * Find the first WebElement using the given locator constant. * * @param constant the locator constant * @return the first matching element on the current context */ public WebElement findElement(final ByEnum constant) { return findElement(constant.locator()); } /** * Find the first WebElement using the given method. * * @param by the locating mechanism * @return the first matching element on the current context */ @Override public WebElement findElement(final By by) { return RobustElementFactory.getElement(this, by); } /** * Get a wrapped reference to the first element matching the specified locator constant. * <p> * <b>NOTE</b>: Use {@link RobustWebElement#hasReference()} to determine if a valid reference was acquired. * * @param constant the locator constant * @return robust web element */ public RobustWebElement findOptional(final ByEnum constant) { return findOptional(constant.locator()); } /** * Get a wrapped reference to the first element matching the specified locator. * <p> * <b>NOTE</b>: Use {@link RobustWebElement#hasReference()} to determine if a valid reference was acquired. * * @param by the locating mechanism * @return robust web element */ public RobustWebElement findOptional(final By by) { return (RobustWebElement) RobustElementFactory.getElement(this, by, RobustElementWrapper.OPTIONAL); } /** * Get the driver object associated with this container. * * @return container driver object */ @Override public WebDriver getWrappedDriver() { return driver; } /** * Update the specified element with the indicated value * * @param element target element (checkbox) * @param value desired value * @return 'true' if element value changed; otherwise 'false' */ public static boolean updateValue(final WebElement element, final boolean value) { Objects.requireNonNull(element, ELEMENT_MESSAGE); String tagName = element.getTagName().toLowerCase(); if ("input".equals(tagName) && "checkbox".equals(element.getAttribute("type"))) { if (element.isSelected() != value) { element.click(); return true; } else { return false; } } return updateValue(element, Boolean.toString(value)); } /** * Update the specified element with the indicated value * * @param element target element (input, select) * @param value desired value * @return 'true' if element value changed; otherwise 'false' */ @SuppressWarnings("squid:S1142") public static boolean updateValue(final WebElement element, final String value) { Objects.requireNonNull(element, ELEMENT_MESSAGE); String tagName = element.getTagName().toLowerCase(); if ("input".equals(tagName)) { if ("checkbox".equals(element.getAttribute("type"))) { return updateValue(element, Boolean.parseBoolean(value)); } else if (!valueEquals(element, value)) { if (value == null) { element.clear(); } else { WebDriverUtils.getExecutor(element).executeScript("arguments[0].select();", element); element.sendKeys(value); } return true; } } else if ("select".equals(tagName) && !valueEquals(element, value)) { new Select(element).selectByValue(value); return true; } return false; } /** * Determine if the specified element has the desired value. * * @param element target element (input, select) * @param value desired value * @return 'true' if element has the desired value; otherwise 'false' */ private static boolean valueEquals(final WebElement element, final String value) { Objects.requireNonNull(element, ELEMENT_MESSAGE); String exist = element.getAttribute("value"); return (exist != null) ? exist.equals(value) : (value == null); } /** * Scroll the specified element into view * * @param element target element * @return the specified element */ public static WebElement scrollIntoView(final WebElement element) { WebDriverUtils.getExecutor(element).executeScript("arguments[0].scrollIntoView(true);", element); return element; } /** * {@inheritDoc} */ @Override Class<?>[] getArgumentTypes() { return Arrays.copyOf(ARG_TYPES, ARG_TYPES.length); } /** * {@inheritDoc} */ @Override Object[] getArguments() { return new Object[] {context, parent}; } /** * {@inheritDoc} */ @Override protected List<Class<?>> getBypassClasses() { if (bypassClasses == null) { bypassClasses = super.getBypassClasses(); Collections.addAll(bypassClasses, myBypassClasses()); } return Collections.unmodifiableList(bypassClasses); } /** * Returns an array of classes whose methods should not be intercepted * * @return array of bypass classes */ Class<?>[] myBypassClasses() { return Arrays.copyOf(BYPASS_CLASSES, BYPASS_CLASSES.length); } /** * {@inheritDoc} */ @Override protected List<String> getBypassMethods() { if (bypassMethods == null) { bypassMethods = super.getBypassMethods(); Collections.addAll(bypassMethods, myBypassMethods()); } return Collections.unmodifiableList(bypassMethods); } /** * Returns an array of names for methods that should not be intercepted * * @return array of bypass method names */ String[] myBypassMethods() { return Arrays.copyOf(BYPASS_METHODS, BYPASS_METHODS.length); } /** * Get the logger for this container * * @return logger object */ protected Logger getLogger() { return logger; } /** * Open the page defined by the {@link PageUrl} annotation of the specified page class. * * @param <T> page class * @param pageClass type of page object to instantiate * @param newWindow 'true' to open page in new window; 'false' to open page in current window * @return new instance of the specified page class */ public <T extends Page> T openAnnotatedPage(final Class<T> pageClass, final boolean newWindow) { PageUrl pageUrl = pageClass.getAnnotation(PageUrl.class); String url = getPageUrl(pageUrl, SeleniumConfig.getConfig().getTargetUri()); Objects.requireNonNull(url, pageClass.toString() + " has no @PageUrl annotation, or the specified @PageUrl has no value"); return openPageAtUrl(pageClass, url, newWindow); } /** * Open the specified relative path under the current target URI. * * @param <T> page class * @param pageClass type of page object to instantiate * @param path path to open * @param newWindow 'true' to open page in new window; 'false' to open page in current window * @return new instance of the specified page class */ public <T extends Page> T openPageAtPath(final Class<T> pageClass, final String path, final boolean newWindow) { URIBuilder builder = new URIBuilder(SeleniumConfig.getConfig().getTargetUri()); builder.setPath(URI.create(LOOPBACK + builder.getPath() + "/").resolve("./" + path).getPath()); return openPageAtUrl(pageClass, builder.toString(), newWindow); } /** * Open the specified URL. * * @param <T> page class * @param pageClass type of page object to instantiate * @param url URL to open * @param newWindow 'true' to open page in new window; 'false' to open page in current window * @return new instance of the specified page class */ public <T extends Page> T openPageAtUrl(final Class<T> pageClass, final String url, final boolean newWindow) { Objects.requireNonNull(pageClass, "[pageClass] must be non-null"); Objects.requireNonNull(url, "[url] must be non-null"); T pageObj = Page.newPage(pageClass, driver); if (newWindow) { pageObj.setWindowState(WindowState.WILL_OPEN); WebDriverUtils.getExecutor(driver).executeScript("window.open('" + url + "','_blank');"); } else { driver.get(url); } return pageObj; } /** * Get the URL defined by the specified {@link PageUrl} annotation. * <p> * <b>NOTES</b>: <ul> * <li>If the {@code pageUrl} argument is {@code null} or the {@code value} element of the specified * {@link PageUrl} annotation is unspecified, this method returns {@code null}. * <li>If {@code scheme} of the specified {@code pageUrl} argument is unspecified or set to {@code http/https}, * the specified {@code targetUri} is overlaid by the elements of the {@link PageUrl} annotation to * produce the fully-qualified <b>HTTP</b> target page URL.<ul> * <li>If the {@code value} element specifies an absolute path, this path is returned as-is.</li> * <li>If the {@code value} element specifies a relative path, this is appended to the path specified by * {@code targetUri} to resolve the page URL.</li> * <li>If the {@code scheme} element is specified, its value overrides the scheme of {@code targetUri}. * If the value of the {@code scheme} element is empty, the scheme of {@code targetUri} is set to * {@code null}.</li> * <li>If the {@code userInfo} element is specified, its value overrides the userInfo of {@code targetUrl}. * If the value of the {@code userInfo} element is empty, the userInfo of {@code targetUri} is set to * {@code null}.</li> * <li>If the {@code host} element is specified, its value overrides the host of {@code targetUrl}. If the * value of the {@code host} element is empty, the host of {@code targetUri} is set to {@code null}. * </li> * <li>If the {@code port} element is specified, its value overrides the port of {@code targetUri}. If the * value of the {@code port} element is empty, the port of {@code targetUri} is set to <b>-1</b>.</li> * </ul></li> * <li>For <b>HTTP</b> URLs that require query parameters, these parameters must be included in the * {@code value} element of the specified {@link PageUrl} annotation. The {@code params} element of the * annotation is only used for pattern-based landing page verification.</li> * <li>If {@code scheme} of the specified {@code pageUrl} is set to {@code file}, the value of the * {@code targetUri} argument is ignored. The only element of the {@link PageUrl} annotation that * is used to produce the fully-qualified <b>FILE</b> target page URL is {@code value}. The value of the * {@code value} element specifies the relative path of a file within your project's resources, which is * resolved via {@link ClassLoader#getResource}.</li> * </ul> * * @param pageUrl page URL annotation * @param targetUri target URI * @return defined page URL as a string (may be 'null') */ @SuppressWarnings({"squid:S3776", "squid:MethodCyclomaticComplexity"}) public static String getPageUrl(final PageUrl pageUrl, final URI targetUri) { if (pageUrl == null || PLACEHOLDER.equals(pageUrl.value())) { return null; } String result = null; String scheme = pageUrl.scheme(); String path = pageUrl.value(); if ("file".equals(scheme)) { result = Thread.currentThread().getContextClassLoader().getResource(path).toString(); } else { String userInfo = pageUrl.userInfo(); String host = pageUrl.host(); String port = pageUrl.port(); URIBuilder builder = new URIBuilder(targetUri); if (!path.isEmpty()) { URI pathUri = URI.create(path); if (pathUri.isAbsolute()) { return pathUri.toString(); } else { builder.setPath(URI.create(LOOPBACK + builder.getPath() + "/").resolve("./" + path).getPath()); } } if (!PLACEHOLDER.equals(scheme)) { builder.setScheme(scheme.isEmpty() ? null : scheme); } if (!PLACEHOLDER.equals(userInfo)) { builder.setUserInfo(userInfo.isEmpty() ? null : userInfo); } if (!PLACEHOLDER.equals(host)) { builder.setHost(host.isEmpty() ? null : host); } if (!PLACEHOLDER.equals(port)) { builder.setPort(port.isEmpty() ? -1 : Integer.parseInt(port)); } result = builder.toString(); } return result; } /** * Wait for the expected landing page to appear in the target browser window. * * @param pageObj target page object */ static void waitForLandingPage(final Page pageObj) { SearchContextWait wait = (SearchContextWait) pageObj.getWait(WaitType.PAGE_LOAD).ignoring(LandingPageMismatchException.class); wait.until(landingPageAppears()); } /** * Returns a 'wait' proxy that determines if the expected landing page has appeared. * * @return 'true' if the expected landing page has appeared */ private static Coordinator<Boolean> landingPageAppears() { return new Coordinator<Boolean>() { /** * {@inheritDoc} */ @Override public Boolean apply(final SearchContext context) { ContainerMethodInterceptor.scanForErrors(context); verifyLandingPage((Page) context); return Boolean.TRUE; } /** * {@inheritDoc} */ @Override public String toString() { return "expected landing page to appear"; } }; } /** * Verify actual landing page against elements of the {@link PageUrl} annotation of the specified page object. * <p> * <b>NOTES</b>: <ul> * <li>The values and patterns used to verify the actual landing page URL are provided by the {@link PageUrl} * annotation of the specified page object combined with the configured {@link SeleniumConfig#getTargetUri * target URI}.</li> * <li>Expected path can be specified by either explicit value or pattern. If the {@code pattern} element of * the {@link PageUrl} annotation is specified, its value provides a template to verify the actual path. * Otherwise, the actual path must match the path component of the specified {@code value} element of the * {@link PageUrl} annotation.</li> * <li>Expected parameters can be specified by either explicit query or a collection of name/pattern pairs. * If the {@code params} element of the {@link PageUrl} annotation is specified, its value provides the * collection of name/pattern pairs used to verify the actual parameters. Otherwise, the actual query * parameters must include all of the name/value pairs in the query component of the specified {@code * value} element of the {@link PageUrl} annotation.</li> * </ul> * * @param pageObj page object whose landing page is to be verified */ @SuppressWarnings({"squid:S3776", "squid:MethodCyclomaticComplexity", "squid:S134"}) private static void verifyLandingPage(final Page pageObj) { Class<?> pageClass = getContainerClass(pageObj); PageUrl pageUrl = pageClass.getAnnotation(PageUrl.class); if (pageUrl != null) { URI targetUri = SeleniumConfig.getConfig().getTargetUri(); verifyLandingPage(pageObj, pageClass, pageUrl, targetUri); } } /** * <b>INTERNAL</b>: Verify actual landing page against elements of the {@link PageUrl} annotation of the specified * page object. * * @param pageObj page object whose landing page is to be verified * @param pageClass class of the specified page object * @param pageUrl {@link PageUrl} annotation for the indicate page class * @param targetUri configured target URI */ protected static final void verifyLandingPage(final Page pageObj, Class<?> pageClass, PageUrl pageUrl, URI targetUri) { String actual; String expect; URI actualUri = URI.create(pageObj.getCurrentUrl()); String expectUrl = getPageUrl(pageUrl, targetUri); URI expectUri = (expectUrl != null) ? URI.create(expectUrl) : null; if (expectUri != null) { actual = actualUri.getScheme(); expect = expectUri.getScheme(); if ( ! StringUtils.equals(actual, expect)) { throw new LandingPageMismatchException(pageClass, "scheme", actual, expect); } actual = actualUri.getHost(); expect = expectUri.getHost(); if ( ! StringUtils.equals(actual, expect)) { throw new LandingPageMismatchException(pageClass, "host", actual, expect); } actual = actualUri.getUserInfo(); expect = expectUri.getUserInfo(); if ( ! StringUtils.equals(actual, expect)) { throw new LandingPageMismatchException(pageClass, "user info", actual, expect); } actual = Integer.toString(actualUri.getPort()); expect = Integer.toString(expectUri.getPort()); if ( ! StringUtils.equals(actual, expect)) { throw new LandingPageMismatchException(pageClass, "port", actual, expect); } } String pattern = pageUrl.pattern(); if (!PLACEHOLDER.equals(pattern)) { actual = actualUri.getPath(); String target = targetUri.getPath(); if (StringUtils.isNotBlank(target)) { int actualLen = actual.length(); int targetLen = target.length(); if ((actualLen > targetLen) && (actual.startsWith(target))) { actual = actual.substring(targetLen); } else { throw new LandingPageMismatchException(pageClass, "base path", actual, target); } } if ( ! actual.matches(pattern)) { throw new LandingPageMismatchException(pageClass, pageObj.getCurrentUrl()); } } else if (expectUri != null) { actual = actualUri.getPath(); expect = expectUri.getPath(); if ( ! StringUtils.equals(actual, expect)) { throw new LandingPageMismatchException(pageClass, "path", actual, expect); } } List<NameValuePair> actualParams = URLEncodedUtils.parse(actualUri, "UTF-8"); for (NameValuePair expectPair : getExpectedParams(pageUrl, expectUri)) { if (!hasExpectedParam(actualParams, expectPair)) { throw new LandingPageMismatchException( pageClass, "query parameter", actualUri.getQuery(), expectPair.toString()); } } } /** * Check the specified page-load condition to determine if this condition has been met.<br> * NOTE - This method indicates failure to meet the condition by throwing {@link PageNotLoadedException}. * * @param <T> coordinator type parameter * @param condition expected page-load condition * @param message the detail message for the {@link PageNotLoadedException} thrown if the condition isn't met * @return result from the {@link Function#apply(Object) apply} method of the specified coordinator */ public <T> T checkPageLoadCondition(final Coordinator<T> condition, final String message) { T result = null; Throwable cause = null; try { result = condition.apply(getContext()); } catch (RuntimeException t) { cause = t; } if (cause != null) { throw new PageNotLoadedException(message, cause); } else if (result == null || result == Boolean.FALSE) { throw new PageNotLoadedException(message); } return result; } /** * Get list of expected query parameters. * * @param pageUrl page URL annotation * @param expectUri expected landing page URI * @return list of expected query parameters */ private static List<NameValuePair> getExpectedParams(final PageUrl pageUrl, final URI expectUri) { List<NameValuePair> expectParams = new ArrayList<>(); String[] params = pageUrl.params(); if (params.length > 0) { for (String param : params) { String name = null; String value = null; String[] nameValueBits = param.split("="); switch (nameValueBits.length) { case NAME_WITH_VALUE: value = nameValueBits[1].trim(); case PARAM_NAME_ONLY: name = nameValueBits[0].trim(); expectParams.add(new BasicNameValuePair(name, value)); break; default: throw new IllegalArgumentException("Format of PageUrl parameter '" + param + "' does not conform to template [name] or [name]=[pattern]"); } } } else if (expectUri != null) { expectParams = URLEncodedUtils.parse(expectUri, "UTF-8"); } return expectParams; } /** * Determine if actual query parameters include all expected name/value pairs. * * @param actualParams actual query parameters of landing page * @param expectPair expected query parameters * @return 'true' of actual query parameters include all expected name/value pairs; otherwise 'false' */ private static boolean hasExpectedParam(final List<NameValuePair> actualParams, final NameValuePair expectPair) { Iterator<NameValuePair> iterator = actualParams.iterator(); while (iterator.hasNext()) { NameValuePair actualPair = iterator.next(); if ( ! actualPair.getName().equals(expectPair.getName())) { continue; } String actualValue = actualPair.getValue(); String expectValue = expectPair.getValue(); if ((actualValue == null) ^ (expectValue == null)) { continue; } if ((actualValue == null) || (actualValue.matches(expectValue))) { iterator.remove(); return true; } } return false; } /** * Get {@link Method} object for the static {@code getKey(SearchContext)} method declared by the specified * container type. * * @param <T> component container type * @param containerType target container type * @return method object for getKey(SearchContext) * @throws UnsupportedOperationException The required method is missing */ static <T extends ComponentContainer> Method getKeyMethod(final Class<T> containerType) { try { Method method = containerType.getMethod("getKey", SearchContext.class); if (Modifier.isStatic(method.getModifiers())) { return method; } } catch (NoSuchMethodException e) { //NOSONAR // fall through to 'throw' statement below } throw new UnsupportedOperationException( "Container class must declare method: public static Object getKey(SearchContext)"); } /** * Verify that the specified container type declares the required constructor. * * @param <T> component container type * @param containerType target container type * @throws UnsupportedOperationException The required constructor is missing */ static <T extends ComponentContainer> void verifyCollectible(final Class<T> containerType) { try { containerType.getConstructor(COLLECTIBLE_ARGS); } catch (NoSuchMethodException | SecurityException e) { //NOSONAR String format = "Container class must declare constructor: public %s(RobustWebElement, ComponentContainer)"; throw new UnsupportedOperationException(String.format(format, containerType.getSimpleName())); } } /** * Get the types of the arguments used to instantiate collectible containers. * * @return an array of constructor argument types */ static Class<?>[] getCollectibleArgs() { return Arrays.copyOf(COLLECTIBLE_ARGS, COLLECTIBLE_ARGS.length); } /** * Instantiate a new container of the specified type with the supplied arguments. * * @param <T> component container type * @param containerType type of container to instantiate * @param argumentTypes array of constructor argument types * @param arguments array of constructor argument values * @return new container of the specified type */ public static <T extends ComponentContainer> T newContainer( final Class<T> containerType, final Class<?>[] argumentTypes, final Object... arguments) { try { Constructor<T> ctor = containerType.getConstructor(argumentTypes); return ctor.newInstance(arguments); } catch (InvocationTargetException e) { //NOSONAR throw UncheckedThrow.throwUnchecked(e.getCause()); } catch (SecurityException | IllegalAccessException | IllegalArgumentException | NoSuchMethodException | InstantiationException e) { throw UncheckedThrow.throwUnchecked(e); } } /** * Instantiate a list of page components of the specified type.<br> * <b>NOTE</b>: The specified page component class must declare a constructor with arguments * (RobustWebElement, ComponentContainer). * * @param <T> page component type * @param componentType page component type * @param locator locator for page component container elements * @return list of page components * @see #verifyCollectible */ public <T extends PageComponent> List<T> newComponentList(final Class<T> componentType, final By locator) { return new ComponentList<>(this, componentType, locator); } /** * Instantiate a map of page components of the specified type, using self-generated keys.<br> * <b>NOTE</b>: The specified page component class must declare a constructor with arguments * (RobustWebElement, ComponentContainer).<br> * <b>NOTE</b>: The specified page component class must declare a static {@code getKey} method that generates a * unique key for each map entry. * * @param <T> page component type * @param componentType page component type * @param locator locator for page component container elements * @return map of page components * @see #verifyCollectible * @see #getKeyMethod */ public <T extends PageComponent> Map<Object, T> newComponentMap(final Class<T> componentType, final By locator) { return new ComponentMap<>(this, componentType, locator); } /** * Instantiate a list of frames of the specified type.<br> * <b>NOTE</b>: The specified frame class must declare a constructor with arguments * (RobustWebElement, ComponentContainer). * * @param <T> frame type * @param frameType frame type * @param locator locator for frame container elements * @return list of frames * @see #verifyCollectible */ public <T extends Frame> List<T> newFrameList(final Class<T> frameType, final By locator) { return new FrameList<>(this, frameType, locator); } /** * Instantiate a map of frames of the specified type, using self-generated keys.<br> * <b>NOTE</b>: The specified frame class must declare a constructor with arguments * (RobustWebElement, ComponentContainer).<br> * <b>NOTE</b>: The specified frame class must declare a static {@code getKey} method that generates a * unique key for each map entry. * * @param <T> frame type * @param frameType frame type * @param locator locator for frame container elements * @return map of frames * @see #verifyCollectible * @see #getKeyMethod */ public <T extends Frame> Map<Object, T> newFrameMap(final Class<T> frameType, final By locator) { return new FrameMap<>(this, frameType, locator); } /** * {@inheritDoc} */ @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + context.hashCode(); result = prime * result + ((parent == null) ? 0 : parent.hashCode()); result = prime * result + ((bypassClasses == null) ? 0 : bypassClasses.hashCode()); result = prime * result + ((bypassMethods == null) ? 0 : bypassMethods.hashCode()); return result; } /** * {@inheritDoc} */ @Override @SuppressWarnings({"squid:S3776", "squid:S1142"}) public boolean equals(final Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; ComponentContainer other = (ComponentContainer) obj; if (!context.equals(other.context)) return false; if (parent == null) { if (other.parent != null) return false; } else if (!parent.equals(other.parent)) return false; if (bypassClasses == null) { if (other.bypassClasses != null) return false; } else if (!bypassClasses.equals(other.bypassClasses)) return false; if (bypassMethods == null) { if (other.bypassMethods != null) return false; } else if (!bypassMethods.equals(other.bypassMethods)) return false; return true; } }