aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJonatan Kronqvist <jonatan@vaadin.com>2013-09-04 15:27:38 +0300
committerJonatan Kronqvist <jonatan@vaadin.com>2013-09-13 13:51:15 +0300
commit72db2044ea2844c5c7a49a704a507f32af5755ed (patch)
tree68e7b987eb8a2a99b75c963f78c50b7f2c4a9669
parent6ab9e2d060d865f9ecd918209b0620a95a63f6a6 (diff)
downloadvaadin-framework-72db2044ea2844c5c7a49a704a507f32af5755ed.tar.gz
vaadin-framework-72db2044ea2844c5c7a49a704a507f32af5755ed.zip
Implemented the extensions to ComponentLocator needed for TB4 #12485
Change-Id: I8c7db91967003290bbff4e703235aa36d5e9e1f3
-rw-r--r--client/src/com/vaadin/client/ApplicationConnection.java3
-rw-r--r--client/src/com/vaadin/client/componentlocator/ComponentLocator.java45
-rw-r--r--client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java13
-rw-r--r--client/src/com/vaadin/client/componentlocator/LocatorStrategy.java56
-rw-r--r--client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java357
5 files changed, 472 insertions, 2 deletions
diff --git a/client/src/com/vaadin/client/ApplicationConnection.java b/client/src/com/vaadin/client/ApplicationConnection.java
index 3200b3ab38..e038f37689 100644
--- a/client/src/com/vaadin/client/ApplicationConnection.java
+++ b/client/src/com/vaadin/client/ApplicationConnection.java
@@ -523,6 +523,9 @@ public class ApplicationConnection {
client.getElementByPath = $entry(function(id) {
return componentLocator.@com.vaadin.client.componentlocator.ComponentLocator::getElementByPath(Ljava/lang/String;)(id);
});
+ client.getElementByPathStartingAt = $entry(function(id, element) {
+ return componentLocator.@com.vaadin.client.componentlocator.ComponentLocator::getElementByPathStartingAt(Ljava/lang/String;Lcom/google/gwt/user/client/Element;)(id, element);
+ });
client.getPathForElement = $entry(function(element) {
return componentLocator.@com.vaadin.client.componentlocator.ComponentLocator::getPathForElement(Lcom/google/gwt/user/client/Element;)(element);
});
diff --git a/client/src/com/vaadin/client/componentlocator/ComponentLocator.java b/client/src/com/vaadin/client/componentlocator/ComponentLocator.java
index a7afeaad9c..c1f117d992 100644
--- a/client/src/com/vaadin/client/componentlocator/ComponentLocator.java
+++ b/client/src/com/vaadin/client/componentlocator/ComponentLocator.java
@@ -15,6 +15,9 @@
*/
package com.vaadin.client.componentlocator;
+import java.util.Arrays;
+import java.util.List;
+
import com.google.gwt.user.client.Element;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.BrowserInfo;
@@ -23,12 +26,14 @@ import com.vaadin.client.BrowserInfo;
* ComponentLocator provides methods for generating a String locator for a given
* DOM element and for locating a DOM element using a String locator.
* <p>
- * The main use for this class is locating components for automated testing purposes.
+ * The main use for this class is locating components for automated testing
+ * purposes.
*
* @since 7.2, moved from {@link com.vaadin.client.ComponentLocator}
*/
public class ComponentLocator {
+ private final List<LocatorStrategy> locatorStrategies;
private final LegacyLocatorStrategy legacyLocatorStrategy = new LegacyLocatorStrategy(
this);
@@ -46,6 +51,8 @@ public class ComponentLocator {
*/
public ComponentLocator(ApplicationConnection client) {
this.client = client;
+ locatorStrategies = Arrays.asList(
+ new VaadinFinderLocatorStrategy(this), legacyLocatorStrategy);
}
/**
@@ -67,6 +74,7 @@ public class ComponentLocator {
* String locator could not be created.
*/
public String getPathForElement(Element targetElement) {
+ // For now, only use the legacy locator to find paths
return legacyLocatorStrategy
.getPathForElement(getElement(targetElement));
}
@@ -119,7 +127,40 @@ public class ComponentLocator {
* could not be located.
*/
public Element getElementByPath(String path) {
- return legacyLocatorStrategy.getElementByPath(path);
+ // As LegacyLocatorStrategy always is the last item in the list, it is
+ // always used as a last resort if no other strategies claim
+ // responsibility for the path syntax.
+ for (LocatorStrategy strategy : locatorStrategies) {
+ if (strategy.handlesPathSyntax(path)) {
+ return strategy.getElementByPath(path);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Locates an element using a String locator (path) which identifies a DOM
+ * element. The path starts from the specified root element.
+ *
+ * @see #getElementByPath(String)
+ *
+ * @param path
+ * The path of the element to be found
+ * @param root
+ * The root element where the path is anchored
+ * @return The DOM element identified by {@code path} or null if the element
+ * could not be located.
+ */
+ public Element getElementByPathStartingAt(String path, Element root) {
+ // As LegacyLocatorStrategy always is the last item in the list, it is
+ // always used as a last resort if no other strategies claim
+ // responsibility for the path syntax.
+ for (LocatorStrategy strategy : locatorStrategies) {
+ if (strategy.handlesPathSyntax(path)) {
+ return strategy.getElementByPathStartingAt(path, root);
+ }
+ }
+ return null;
}
/**
diff --git a/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java b/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java
index dd67ddbc43..34f5967092 100644
--- a/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java
+++ b/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java
@@ -205,6 +205,19 @@ public class LegacyLocatorStrategy implements LocatorStrategy {
return null;
}
+ @Override
+ public Element getElementByPathStartingAt(String path, Element root) {
+ // Not supported by the legacy format
+ return null;
+ }
+
+ @Override
+ public boolean handlesPathSyntax(String path) {
+ // The legacy strategy is always used if all else fails, so just return
+ // true here.
+ return true;
+ }
+
/**
* Finds the first widget in the hierarchy (moving upwards) that implements
* SubPartAware. Returns the SubPartAware implementor or null if none is
diff --git a/client/src/com/vaadin/client/componentlocator/LocatorStrategy.java b/client/src/com/vaadin/client/componentlocator/LocatorStrategy.java
index 53cff10d4f..835b81bdae 100644
--- a/client/src/com/vaadin/client/componentlocator/LocatorStrategy.java
+++ b/client/src/com/vaadin/client/componentlocator/LocatorStrategy.java
@@ -28,7 +28,63 @@ import com.google.gwt.user.client.Element;
* @author Vaadin Ltd
*/
public interface LocatorStrategy {
+ /**
+ * Generates a String locator which uniquely identifies the target element.
+ * The {@link #getElementByPath(String)} method can be used for the inverse
+ * operation, i.e. locating an element based on the return value from this
+ * method.
+ * <p>
+ * Note that getElementByPath(getPathForElement(element)) == element is not
+ * always true as #getPathForElement(Element) can return a path to another
+ * element if the widget determines an action on the other element will give
+ * the same result as the action on the target element.
+ * </p>
+ *
+ * @param targetElement
+ * The element to generate a path for.
+ * @return A String locator that identifies the target element or null if a
+ * String locator could not be created.
+ */
String getPathForElement(Element targetElement);
+ /**
+ * Locates an element using a String locator (path) which identifies a DOM
+ * element. The {@link #getPathForElement(Element)} method can be used for
+ * the inverse operation, i.e. generating a string expression for a DOM
+ * element.
+ *
+ * @param path
+ * The String locator which identifies the target element.
+ * @return The DOM element identified by {@code path} or null if the element
+ * could not be located.
+ */
Element getElementByPath(String path);
+
+ /**
+ * Locates an element using a String locator (path) which identifies a DOM
+ * element. The path starts from the specified root element.
+ *
+ * @see #getElementByPath(String)
+ *
+ * @param path
+ * The String locator which identifies the target element.
+ * @param root
+ * The element that is at the root of the path.
+ * @return The DOM element identified by {@code path} or null if the element
+ * could not be located.
+ */
+ Element getElementByPathStartingAt(String path, Element root);
+
+ /**
+ * Allows the component locator orchestrator to determine whether this
+ * strategy should be used to locate an element using the provided path.
+ * Paths can have (slightly) different syntax, and each locator strategy
+ * should inspect the path string to see if it can be used to locate the
+ * element by the path in question.
+ *
+ * @param path
+ * The path whose syntax to check whether handled or not
+ * @return true if this strategy handles the path syntax in question
+ */
+ boolean handlesPathSyntax(String path);
}
diff --git a/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java b/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java
new file mode 100644
index 0000000000..f597003b60
--- /dev/null
+++ b/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java
@@ -0,0 +1,357 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.componentlocator;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.gwt.regexp.shared.RegExp;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.ComponentConnector;
+import com.vaadin.client.Util;
+import com.vaadin.client.metadata.NoDataException;
+import com.vaadin.client.metadata.Property;
+import com.vaadin.client.ui.AbstractConnector;
+import com.vaadin.client.ui.AbstractHasComponentsConnector;
+import com.vaadin.client.ui.VNotification;
+import com.vaadin.shared.AbstractComponentState;
+
+/**
+ * The VaadinFinder locator strategy implements an XPath-like syntax for
+ * locating elements in Vaadin applications. This is used in the new
+ * VaadinFinder API in TestBench 4.
+ *
+ * Examples of the supported syntax:
+ * <ul>
+ * <li>Find the third text field in the DOM: {@code //VTextField[2]}</li>
+ * <li>Find the second button inside the first vertical layout:
+ * {@code //VVerticalLayout/VButton[1]}</li>
+ * <li>Find the first column on the third row of the "Accounts" table:
+ * {@code //VScrollTable[caption="Accounts"]#row[2]/col[0]}</li>
+ * </ul>
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class VaadinFinderLocatorStrategy implements LocatorStrategy {
+
+ private ComponentLocator componentLocator;
+
+ public VaadinFinderLocatorStrategy(ComponentLocator componentLocator) {
+ this.componentLocator = componentLocator;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getPathForElement(Element targetElement) {
+ // Path generation functionality is not yet implemented as there is no
+ // current need for it. This might be implemented in the future if the
+ // need arises. Until then, all locator generation is handled by
+ // LegacyLocatorStrategy.
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Element getElementByPath(String path) {
+ if (path.startsWith("//VNotification")) {
+ return findNotificationByPath(path);
+ }
+ return findElementByPath(path, componentLocator.getClient()
+ .getUIConnector());
+ }
+
+ /**
+ * Special case for finding notifications as they have no connectors and are
+ * directly attached to {@link RootPanel}.
+ *
+ * @param path
+ * The path of the notification, should be
+ * {@code "//VNotification"} optionally followed by an index in
+ * brackets.
+ * @return the notification element or null if not found.
+ */
+ private Element findNotificationByPath(String path) {
+ ArrayList<VNotification> notifications = new ArrayList<VNotification>();
+ for (Widget w : RootPanel.get()) {
+ if (w instanceof VNotification) {
+ notifications.add((VNotification) w);
+ }
+ }
+ String indexStr = extractPredicateString(path);
+ int index = indexStr == null ? 0 : Integer.parseInt(indexStr);
+ if (index >= 0 && index < notifications.size()) {
+ return notifications.get(index).getElement();
+ }
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Element getElementByPathStartingAt(String path, Element root) {
+ return findElementByPath(path,
+ Util.findPaintable(componentLocator.getClient(), root));
+ }
+
+ /**
+ * Recursively finds an element identified by the provided path by
+ * traversing the connector hierarchy starting from the {@code parent}
+ * connector.
+ *
+ * @param path
+ * The path identifying an element.
+ * @param parent
+ * The connector to start traversing from.
+ * @return The element identified by {@code path} or null if it no such
+ * element could be found.
+ */
+ private Element findElementByPath(String path, ComponentConnector parent) {
+ boolean findRecursively = path.startsWith("//");
+ // Strip away the one or two slashes from the beginning of the path
+ path = path.substring(findRecursively ? 2 : 1);
+
+ String[] fragments = splitFirstFragmentFromTheRest(path);
+ List<ComponentConnector> potentialMatches = collectPotentialMatches(
+ parent, fragments[0], findRecursively);
+ ComponentConnector connector = filterPotentialMatches(potentialMatches,
+ extractPredicateString(fragments[0]));
+ if (connector != null) {
+ if (fragments.length > 1) {
+ return findElementByPath(fragments[1], connector);
+ } else {
+ return connector.getWidget().getElement();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the predicate string, i.e. the string between the brackets in a
+ * path fragment. Examples: <code>
+ * VTextField[0] => 0
+ * VTextField[caption='foo'] => caption='foo'
+ * </code>
+ *
+ * @param pathFragment
+ * The path fragment from which to extract the predicate string.
+ * @return The predicate string for the path fragment or null if none.
+ */
+ private String extractPredicateString(String pathFragment) {
+ int ixOpenBracket = pathFragment.indexOf('[');
+ if (ixOpenBracket >= 0) {
+ int ixCloseBracket = pathFragment.indexOf(']', ixOpenBracket);
+ return pathFragment.substring(ixOpenBracket + 1, ixCloseBracket);
+ }
+ return null;
+ }
+
+ /**
+ * Returns the first ComponentConnector that matches the predicate string
+ * from a list of potential matches. If {@code predicateString} is null, the
+ * first element in the {@code potentialMatches} list is returned.
+ *
+ * @param potentialMatches
+ * A list of potential matches to check.
+ * @param predicateString
+ * The predicate that should match. Can be an index or a property
+ * name, value pair or null.
+ * @return A {@link ComponentConnector} from the {@code potentialMatches}
+ * list, which matches the {@code predicateString} or null if no
+ * matches are found.
+ */
+ private ComponentConnector filterPotentialMatches(
+ List<ComponentConnector> potentialMatches, String predicateString) {
+ if (potentialMatches.isEmpty()) {
+ return null;
+ }
+
+ if (predicateString != null) {
+ String[] parts = predicateString.split("=");
+ if (parts.length == 1) {
+ int index = Integer.valueOf(predicateString);
+ return index < potentialMatches.size() ? potentialMatches
+ .get(index) : null;
+ } else {
+ String propertyName = parts[0].trim();
+ String value = unquote(parts[1].trim());
+ for (ComponentConnector connector : potentialMatches) {
+ Property property = AbstractConnector.getStateType(
+ connector).getProperty(propertyName);
+ if (valueEqualsPropertyValue(value, property,
+ connector.getState())) {
+ return connector;
+ }
+ }
+ }
+ }
+ return potentialMatches.get(0);
+ }
+
+ /**
+ * Returns true if the value matches the value of the property in the state
+ * object.
+ *
+ * @param value
+ * The value to compare against.
+ * @param property
+ * The property, whose value to check.
+ * @param state
+ * The connector, whose state object contains the property.
+ * @return true if the values match.
+ */
+ private boolean valueEqualsPropertyValue(String value, Property property,
+ AbstractComponentState state) {
+ try {
+ return value.equals(property.getValue(state));
+ } catch (NoDataException e) {
+ // The property doesn't exist in the state object, so they aren't
+ // equal.
+ return false;
+ }
+ }
+
+ /**
+ * Removes the surrounding quotes from a string if it is quoted.
+ *
+ * @param str
+ * the possibly quoted string
+ * @return an unquoted version of str
+ */
+ private String unquote(String str) {
+ if ((str.startsWith("\"") && str.endsWith("\""))
+ || (str.startsWith("'") && str.endsWith("'"))) {
+ return str.substring(1, str.length() - 1);
+ }
+ return str;
+ }
+
+ /**
+ * Collects all connectors that match the widget class name of the path
+ * fragment. If the {@code collectRecursively} parameter is true, a
+ * depth-first search of the connector hierarchy is performed.
+ *
+ * Searching depth-first ensure that we can return the matches in correct
+ * order for selecting based on index predicates.
+ *
+ * @param parent
+ * The {@link ComponentConnector} to start the search from.
+ * @param pathFragment
+ * The path fragment identifying which type of widget to search
+ * for.
+ * @param collectRecursively
+ * If true, all matches from all levels below {@code parent} will
+ * be collected. If false only direct children will be collected.
+ * @return A list of {@link ComponentConnector}s matching the widget type
+ * specified in the {@code pathFragment}.
+ */
+ private List<ComponentConnector> collectPotentialMatches(
+ ComponentConnector parent, String pathFragment,
+ boolean collectRecursively) {
+ ArrayList<ComponentConnector> potentialMatches = new ArrayList<ComponentConnector>();
+ if (parent instanceof AbstractHasComponentsConnector) {
+ List<ComponentConnector> children = ((AbstractHasComponentsConnector) parent)
+ .getChildComponents();
+ for (ComponentConnector child : children) {
+ String widgetName = getWidgetName(pathFragment);
+ if (connectorMatchesPathFragment(child, widgetName)) {
+ potentialMatches.add(child);
+ }
+ if (collectRecursively) {
+ potentialMatches.addAll(collectPotentialMatches(child,
+ pathFragment, collectRecursively));
+ }
+ }
+ }
+ return potentialMatches;
+ }
+
+ /**
+ * Determines whether a connector matches a path fragment. This is done by
+ * comparing the path fragment to the name of the widget type of the
+ * connector.
+ *
+ * @param connector
+ * The connector to compare.
+ * @param widgetName
+ * The name of the widget class.
+ * @return true if the widget type of the connector equals the widget type
+ * identified by the path fragment.
+ */
+ private boolean connectorMatchesPathFragment(ComponentConnector connector,
+ String widgetName) {
+ return widgetName.equals(Util.getSimpleName(connector.getWidget()));
+ }
+
+ /**
+ * Extracts the name of the widget class from a path fragment
+ *
+ * @param pathFragment
+ * the path fragment
+ * @return the name of the widget class.
+ */
+ private String getWidgetName(String pathFragment) {
+ String widgetName = pathFragment;
+ int ixBracket = pathFragment.indexOf('[');
+ if (ixBracket >= 0) {
+ widgetName = pathFragment.substring(0, ixBracket);
+ }
+ return widgetName;
+ }
+
+ /**
+ * Splits off the first path fragment from a path and returns an array of
+ * two elements, where the first element is the first path fragment and the
+ * second element is the rest of the path (all remaining path fragments
+ * untouched).
+ *
+ * @param path
+ * The path to split.
+ * @return An array of two elements: The first path fragment and the rest of
+ * the path.
+ */
+ private String[] splitFirstFragmentFromTheRest(String path) {
+ int ixOfSlash = path.indexOf('/');
+ if (ixOfSlash > 0) {
+ return new String[] { path.substring(0, ixOfSlash),
+ path.substring(ixOfSlash) };
+ }
+ return new String[] { path };
+ }
+
+ /**
+ * Matches a string that contains either double slashes ({@code //}) or a
+ * complex predicate ({@code [caption = "foo"]})
+ */
+ private static RegExp syntaxMatcher = RegExp
+ .compile("^.*//.*$|^.*\\[\\w+.*=.*\\].*$");
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean handlesPathSyntax(String path) {
+ // If the path contains a double-slash at any point, it's probably ours.
+ return syntaxMatcher.test(path);
+ }
+}