diff options
author | Jonatan Kronqvist <jonatan@vaadin.com> | 2013-09-04 15:27:38 +0300 |
---|---|---|
committer | Jonatan Kronqvist <jonatan@vaadin.com> | 2013-09-13 13:51:15 +0300 |
commit | 72db2044ea2844c5c7a49a704a507f32af5755ed (patch) | |
tree | 68e7b987eb8a2a99b75c963f78c50b7f2c4a9669 | |
parent | 6ab9e2d060d865f9ecd918209b0620a95a63f6a6 (diff) | |
download | vaadin-framework-72db2044ea2844c5c7a49a704a507f32af5755ed.tar.gz vaadin-framework-72db2044ea2844c5c7a49a704a507f32af5755ed.zip |
Implemented the extensions to ComponentLocator needed for TB4 #12485
Change-Id: I8c7db91967003290bbff4e703235aa36d5e9e1f3
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); + } +} |