Bläddra i källkod

Allow different locator strategies #12485

Refactored the ComponentLocator class to allow for implementing
different locator strategies.

Change-Id: I93f3decbce4d4361cc605bcf0ce4379a187c482c
tags/7.2.0.beta1
Jonatan Kronqvist 10 år sedan
förälder
incheckning
6ab9e2d060

+ 29
- 28
client/src/com/vaadin/client/ApplicationConnection.java Visa fil

@@ -72,6 +72,7 @@ import com.vaadin.client.communication.JsonEncoder;
import com.vaadin.client.communication.PushConnection;
import com.vaadin.client.communication.RpcManager;
import com.vaadin.client.communication.StateChangeEvent;
import com.vaadin.client.componentlocator.ComponentLocator;
import com.vaadin.client.extensions.AbstractExtensionConnector;
import com.vaadin.client.metadata.ConnectorBundleLoader;
import com.vaadin.client.metadata.Method;
@@ -496,38 +497,38 @@ public class ApplicationConnection {
private native void initializeTestbenchHooks(
ComponentLocator componentLocator, String TTAppId)
/*-{
var ap = this;
var client = {};
client.isActive = $entry(function() {
return ap.@com.vaadin.client.ApplicationConnection::hasActiveRequest()()
|| ap.@com.vaadin.client.ApplicationConnection::isExecutingDeferredCommands()();
});
var vi = ap.@com.vaadin.client.ApplicationConnection::getVersionInfo()();
if (vi) {
client.getVersionInfo = function() {
return vi;
}
}
var ap = this;
var client = {};
client.isActive = $entry(function() {
return ap.@com.vaadin.client.ApplicationConnection::hasActiveRequest()()
|| ap.@com.vaadin.client.ApplicationConnection::isExecutingDeferredCommands()();
});
var vi = ap.@com.vaadin.client.ApplicationConnection::getVersionInfo()();
if (vi) {
client.getVersionInfo = function() {
return vi;
}
}

client.getProfilingData = $entry(function() {
var pd = [
ap.@com.vaadin.client.ApplicationConnection::lastProcessingTime,
client.getProfilingData = $entry(function() {
var pd = [
ap.@com.vaadin.client.ApplicationConnection::lastProcessingTime,
ap.@com.vaadin.client.ApplicationConnection::totalProcessingTime
];
pd = pd.concat(ap.@com.vaadin.client.ApplicationConnection::serverTimingInfo);
pd[pd.length] = ap.@com.vaadin.client.ApplicationConnection::bootstrapTime;
return pd;
});
];
pd = pd.concat(ap.@com.vaadin.client.ApplicationConnection::serverTimingInfo);
pd[pd.length] = ap.@com.vaadin.client.ApplicationConnection::bootstrapTime;
return pd;
});

client.getElementByPath = $entry(function(id) {
return componentLocator.@com.vaadin.client.ComponentLocator::getElementByPath(Ljava/lang/String;)(id);
});
client.getPathForElement = $entry(function(element) {
return componentLocator.@com.vaadin.client.ComponentLocator::getPathForElement(Lcom/google/gwt/user/client/Element;)(element);
});
client.initializing = false;
client.getElementByPath = $entry(function(id) {
return componentLocator.@com.vaadin.client.componentlocator.ComponentLocator::getElementByPath(Ljava/lang/String;)(id);
});
client.getPathForElement = $entry(function(element) {
return componentLocator.@com.vaadin.client.componentlocator.ComponentLocator::getPathForElement(Lcom/google/gwt/user/client/Element;)(element);
});
client.initializing = false;

$wnd.vaadin.clients[TTAppId] = client;
$wnd.vaadin.clients[TTAppId] = client;
}-*/;

private static native final int calculateBootstrapTime()

+ 7
- 693
client/src/com/vaadin/client/ComponentLocator.java Visa fil

@@ -15,706 +15,20 @@
*/
package com.vaadin.client;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.ui.HasWidgets;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.ui.SubPartAware;
import com.vaadin.client.ui.VCssLayout;
import com.vaadin.client.ui.VGridLayout;
import com.vaadin.client.ui.VOverlay;
import com.vaadin.client.ui.VTabsheetPanel;
import com.vaadin.client.ui.VUI;
import com.vaadin.client.ui.VWindow;
import com.vaadin.client.ui.orderedlayout.Slot;
import com.vaadin.client.ui.orderedlayout.VAbstractOrderedLayout;
import com.vaadin.client.ui.window.WindowConnector;
import com.vaadin.shared.AbstractComponentState;
import com.vaadin.shared.Connector;
import com.vaadin.shared.communication.SharedState;

/**
* ComponentLocator provides methods for generating a String locator for a given
* DOM element and for locating a DOM element using a String locator.
*
* @since 5.4
* @deprecated Moved to com.vaadin.client.componentlocator.ComponentLocator
*/
public class ComponentLocator {

/**
* Separator used in the String locator between a parent and a child widget.
*/
private static final String PARENTCHILD_SEPARATOR = "/";

/**
* Separator used in the String locator between the part identifying the
* containing widget and the part identifying the target element within the
* widget.
*/
private static final String SUBPART_SEPARATOR = "#";

/**
* String that identifies the root panel when appearing first in the String
* locator.
*/
private static final String ROOT_ID = "Root";

/**
* Reference to ApplicationConnection instance.
*/
private ApplicationConnection client;

public class ComponentLocator extends com.vaadin.client.componentlocator.ComponentLocator {
/**
* Construct a ComponentLocator for the given ApplicationConnection.
*
* @param client
* ApplicationConnection instance for the application.
*
* @param client ApplicationConnection instance for the application.
*/
public ComponentLocator(ApplicationConnection client) {
this.client = client;
}

/**
* 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 {@link #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>
*
* @since 5.4
* @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.
*/
public String getPathForElement(Element targetElement) {
String pid = null;

targetElement = getElement(targetElement);

Element e = targetElement;

while (true) {
pid = ConnectorMap.get(client).getConnectorId(e);
if (pid != null) {
break;
}

e = DOM.getParent(e);
if (e == null) {
break;
}
}

Widget w = null;
if (pid != null) {
// If we found a Paintable then we use that as reference. We should
// find the Paintable for all but very special cases (like
// overlays).
w = ((ComponentConnector) ConnectorMap.get(client)
.getConnector(pid)).getWidget();

/*
* Still if the Paintable contains a widget that implements
* SubPartAware, we want to use that as a reference
*/
Widget targetParent = findParentWidget(targetElement, w);
while (targetParent != w && targetParent != null) {
if (targetParent instanceof SubPartAware) {
/*
* The targetParent widget is a child of the Paintable and
* the first parent (of the targetElement) that implements
* SubPartAware
*/
w = targetParent;
break;
}
targetParent = targetParent.getParent();
}
}
if (w == null) {
// Check if the element is part of a widget that is attached
// directly to the root panel
RootPanel rootPanel = RootPanel.get();
int rootWidgetCount = rootPanel.getWidgetCount();
for (int i = 0; i < rootWidgetCount; i++) {
Widget rootWidget = rootPanel.getWidget(i);
if (rootWidget.getElement().isOrHasChild(targetElement)) {
// The target element is contained by this root widget
w = findParentWidget(targetElement, rootWidget);
break;
}
}
if (w != null) {
// We found a widget but we should still see if we find a
// SubPartAware implementor (we cannot find the Paintable as
// there is no link from VOverlay to its paintable/owner).
Widget subPartAwareWidget = findSubPartAwareParentWidget(w);
if (subPartAwareWidget != null) {
w = subPartAwareWidget;
}
}
}

if (w == null) {
// Containing widget not found
return null;
}

// Determine the path for the target widget
String path = getPathForWidget(w);
if (path == null) {
/*
* No path could be determined for the target widget. Cannot create
* a locator string.
*/
return null;
}

// The parent check is a work around for Firefox 15 which fails to
// compare elements properly (#9534)
if (w.getElement() == targetElement) {
/*
* We are done if the target element is the root of the target
* widget.
*/
return path;
} else if (w instanceof SubPartAware) {
/*
* If the widget can provide an identifier for the targetElement we
* let it do that
*/
String elementLocator = ((SubPartAware) w)
.getSubPartName(targetElement);
if (elementLocator != null) {
return path + SUBPART_SEPARATOR + elementLocator;
}
}
/*
* If everything else fails we use the DOM path to identify the target
* element
*/
String domPath = getDOMPathForElement(targetElement, w.getElement());
if (domPath == null) {
return path;
} else {
return path + domPath;
}
}

/**
* Returns the element passed to the method. Or in case of Firefox 15,
* returns the real element that is in the DOM instead of the element passed
* to the method (which is the same element but not ==).
*
* @param targetElement
* the element to return
* @return the element passed to the method
*/
private Element getElement(Element targetElement) {
if (targetElement == null) {
return null;
}

if (!BrowserInfo.get().isFirefox()) {
return targetElement;
}

if (BrowserInfo.get().getBrowserMajorVersion() != 15) {
return targetElement;
}

// Firefox 15, you make me sad
if (targetElement.getNextSibling() != null) {
return (Element) targetElement.getNextSibling()
.getPreviousSibling();
}
if (targetElement.getPreviousSibling() != null) {
return (Element) targetElement.getPreviousSibling()
.getNextSibling();
}
// No siblings so this is the only child
return (Element) targetElement.getParentNode().getChild(0);
}

/**
* Finds the first widget in the hierarchy (moving upwards) that implements
* SubPartAware. Returns the SubPartAware implementor or null if none is
* found.
*
* @param w
* The widget to start from. This is returned if it implements
* SubPartAware.
* @return The first widget (upwards in hierarchy) that implements
* SubPartAware or null
*/
private Widget findSubPartAwareParentWidget(Widget w) {

while (w != null) {
if (w instanceof SubPartAware) {
return w;
}
w = w.getParent();
}
return null;
}

/**
* Returns the first widget found when going from {@code targetElement}
* upwards in the DOM hierarchy, assuming that {@code ancestorWidget} is a
* parent of {@code targetElement}.
*
* @param targetElement
* @param ancestorWidget
* @return The widget whose root element is a parent of
* {@code targetElement}.
*/
private Widget findParentWidget(Element targetElement, Widget ancestorWidget) {
/*
* As we cannot resolve Widgets from the element we start from the
* widget and move downwards to the correct child widget, as long as we
* find one.
*/
if (ancestorWidget instanceof HasWidgets) {
for (Widget w : ((HasWidgets) ancestorWidget)) {
if (w.getElement().isOrHasChild(targetElement)) {
return findParentWidget(targetElement, w);
}
}
}

// No children found, this is it
return ancestorWidget;
}

/**
* Locates an element based on a DOM path and a base element.
*
* @param baseElement
* The base element which the path is relative to
* @param path
* String locator (consisting of domChild[x] parts) that
* identifies the element
* @return The element identified by path, relative to baseElement or null
* if the element could not be found.
*/
private Element getElementByDOMPath(Element baseElement, String path) {
String parts[] = path.split(PARENTCHILD_SEPARATOR);
Element element = baseElement;

for (String part : parts) {
if (part.startsWith("domChild[")) {
String childIndexString = part.substring("domChild[".length(),
part.length() - 1);

if (Util.findWidget(baseElement, null) instanceof VAbstractOrderedLayout) {
if (element.hasChildNodes()) {
Element e = element.getFirstChildElement().cast();
String cn = e.getClassName();
if (cn != null
&& (cn.equals("v-expand") || cn
.contains("v-has-caption"))) {
element = e;
}
}
}

try {
int childIndex = Integer.parseInt(childIndexString);
element = DOM.getChild(element, childIndex);
} catch (Exception e) {
return null;
}

if (element == null) {
return null;
}

}
}

return element;
super(client);
}

/**
* Generates a String locator using domChild[x] parts for the element
* relative to the baseElement.
*
* @param element
* The target element
* @param baseElement
* The starting point for the locator. The generated path is
* relative to this element.
* @return A String locator that can be used to locate the target element
* using {@link #getElementByDOMPath(Element, String)} or null if
* the locator String cannot be created.
*/
private String getDOMPathForElement(Element element, Element baseElement) {
Element e = element;
String path = "";
while (true) {
int childIndex = -1;
Element siblingIterator = e;
while (siblingIterator != null) {
childIndex++;
siblingIterator = siblingIterator.getPreviousSiblingElement()
.cast();
}

path = PARENTCHILD_SEPARATOR + "domChild[" + childIndex + "]"
+ path;

JavaScriptObject parent = e.getParentElement();
if (parent == null) {
return null;
}
// The parent check is a work around for Firefox 15 which fails to
// compare elements properly (#9534)
if (parent == baseElement) {
break;
}

e = parent.cast();
}

return path;
}

/**
* 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.
*
* @since 5.4
* @param path
* The String locater which identifies the target element.
* @return The DOM element identified by {@code path} or null if the element
* could not be located.
*/
public Element getElementByPath(String path) {
/*
* Path is of type "targetWidgetPath#componentPart" or
* "targetWidgetPath".
*/
String parts[] = path.split(SUBPART_SEPARATOR, 2);
String widgetPath = parts[0];
Widget w = getWidgetFromPath(widgetPath);
if (w == null || !Util.isAttachedAndDisplayed(w)) {
return null;
}

if (parts.length == 1) {
int pos = widgetPath.indexOf("domChild");
if (pos == -1) {
return w.getElement();
}

// Contains dom reference to a sub element of the widget
String subPath = widgetPath.substring(pos);
return getElementByDOMPath(w.getElement(), subPath);
} else if (parts.length == 2) {
if (w instanceof SubPartAware) {
return ((SubPartAware) w).getSubPartElement(parts[1]);
}
}

return null;
}

/**
* Creates a locator String for the given widget. The path can be used to
* locate the widget using {@link #getWidgetFromPath(String)}.
*
* Returns null if no path can be determined for the widget or if the widget
* is null.
*
* @param w
* The target widget
* @return A String locator for the widget
*/
private String getPathForWidget(Widget w) {
if (w == null) {
return null;
}
String elementId = w.getElement().getId();
if (elementId != null && !elementId.isEmpty()
&& !elementId.startsWith("gwt-uid-")) {
// Use PID_S+id if the user has set an id but do not use it for auto
// generated id:s as these might not be consistent
return "PID_S" + elementId;
} else if (w instanceof VUI) {
return "";
} else if (w instanceof VWindow) {
Connector windowConnector = ConnectorMap.get(client)
.getConnector(w);
List<WindowConnector> subWindowList = client.getUIConnector()
.getSubWindows();
int indexOfSubWindow = subWindowList.indexOf(windowConnector);
return PARENTCHILD_SEPARATOR + "VWindow[" + indexOfSubWindow + "]";
} else if (w instanceof RootPanel) {
return ROOT_ID;
}

Widget parent = w.getParent();

String basePath = getPathForWidget(parent);
if (basePath == null) {
return null;
}
String simpleName = Util.getSimpleName(w);

/*
* Check if the parent implements Iterable. At least VPopupView does not
* implement HasWdgets so we cannot check for that.
*/
if (!(parent instanceof Iterable<?>)) {
// Parent does not implement Iterable so we cannot find out which
// child this is
return null;
}

Iterator<?> i = ((Iterable<?>) parent).iterator();
int pos = 0;
while (i.hasNext()) {
Object child = i.next();
if (child == w) {
return basePath + PARENTCHILD_SEPARATOR + simpleName + "["
+ pos + "]";
}
String simpleName2 = Util.getSimpleName(child);
if (simpleName.equals(simpleName2)) {
pos++;
}
}

return null;
}

/**
* Locates the widget based on a String locator.
*
* @param path
* The String locator that identifies the widget.
* @return The Widget identified by the String locator or null if the widget
* could not be identified.
*/
private Widget getWidgetFromPath(String path) {
Widget w = null;
String parts[] = path.split(PARENTCHILD_SEPARATOR);

for (int i = 0; i < parts.length; i++) {
String part = parts[i];

if (part.equals(ROOT_ID)) {
w = RootPanel.get();
} else if (part.equals("")) {
w = client.getUIConnector().getWidget();
} else if (w == null) {
String id = part;
// Must be old static pid (PID_S*)
ServerConnector connector = ConnectorMap.get(client)
.getConnector(id);
if (connector == null) {
// Lookup by component id
// TODO Optimize this
connector = findConnectorById(client.getUIConnector(),
id.substring(5));
}

if (connector instanceof ComponentConnector) {
w = ((ComponentConnector) connector).getWidget();
} else {
// Not found
return null;
}
} else if (part.startsWith("domChild[")) {
// The target widget has been found and the rest identifies the
// element
break;
} else if (w instanceof Iterable) {
// W identifies a widget that contains other widgets, as it
// should. Try to locate the child
Iterable<?> parent = (Iterable<?>) w;

// Part is of type "VVerticalLayout[0]", split this into
// VVerticalLayout and 0
String[] split = part.split("\\[", 2);
String widgetClassName = split[0];
String indexString = split[1].substring(0,
split[1].length() - 1);
int widgetPosition = Integer.parseInt(indexString);

// AbsolutePanel in GridLayout has been removed -> skip it
if (w instanceof VGridLayout
&& "AbsolutePanel".equals(widgetClassName)) {
continue;
}

// FlowPane in CSSLayout has been removed -> skip it
if (w instanceof VCssLayout
&& "VCssLayout$FlowPane".equals(widgetClassName)) {
continue;
}

// ChildComponentContainer and VOrderedLayout$Slot have been
// replaced with Slot
if (w instanceof VAbstractOrderedLayout
&& ("ChildComponentContainer".equals(widgetClassName) || "VOrderedLayout$Slot"
.equals(widgetClassName))) {
widgetClassName = "Slot";
}

if (w instanceof VTabsheetPanel && widgetPosition != 0) {
// TabSheetPanel now only contains 1 connector => the index
// is always 0 which indicates the widget in the active tab
widgetPosition = 0;
}
if (w instanceof VOverlay
&& "VCalendarPanel".equals(widgetClassName)) {
// Vaadin 7.1 adds a wrapper for datefield popups
parent = (Iterable<?>) ((Iterable) parent).iterator()
.next();
}
/*
* The new grid and ordered layotus do not contain
* ChildComponentContainer widgets. This is instead simulated by
* constructing a path step that would find the desired widget
* from the layout and injecting it as the next search step
* (which would originally have found the widget inside the
* ChildComponentContainer)
*/
if ((w instanceof VGridLayout)
&& "ChildComponentContainer".equals(widgetClassName)
&& i + 1 < parts.length) {

HasWidgets layout = (HasWidgets) w;

String nextPart = parts[i + 1];
String[] nextSplit = nextPart.split("\\[", 2);
String nextWidgetClassName = nextSplit[0];

// Find the n:th child and count the number of children with
// the same type before it
int nextIndex = 0;
for (Widget child : layout) {
boolean matchingType = nextWidgetClassName.equals(Util
.getSimpleName(child));
if (matchingType && widgetPosition == 0) {
// This is the n:th child that we looked for
break;
} else if (widgetPosition < 0) {
// Error if we're past the desired position without
// a match
return null;
} else if (matchingType) {
// If this was another child of the expected type,
// increase the count for the next step
nextIndex++;
}

// Don't count captions
if (!(child instanceof VCaption)) {
widgetPosition--;
}
}

// Advance to the next step, this time checking for the
// actual child widget
parts[i + 1] = nextWidgetClassName + '[' + nextIndex + ']';
continue;
}

// Locate the child
Iterator<? extends Widget> iterator;

/*
* VWindow and VContextMenu workarounds for backwards
* compatibility
*/
if (widgetClassName.equals("VWindow")) {
List<WindowConnector> windows = client.getUIConnector()
.getSubWindows();
List<VWindow> windowWidgets = new ArrayList<VWindow>(
windows.size());
for (WindowConnector wc : windows) {
windowWidgets.add(wc.getWidget());
}
iterator = windowWidgets.iterator();
} else if (widgetClassName.equals("VContextMenu")) {
return client.getContextMenu();
} else {
iterator = (Iterator<? extends Widget>) parent.iterator();
}

boolean ok = false;

// Find the widgetPosition:th child of type "widgetClassName"
while (iterator.hasNext()) {

Widget child = iterator.next();
String simpleName2 = Util.getSimpleName(child);

if (!widgetClassName.equals(simpleName2)
&& child instanceof Slot) {
/*
* Support legacy tests without any selector for the
* Slot widget (i.e. /VVerticalLayout[0]/VButton[0]) by
* directly checking the stuff inside the slot
*/
child = ((Slot) child).getWidget();
simpleName2 = Util.getSimpleName(child);
}

if (widgetClassName.equals(simpleName2)) {
if (widgetPosition == 0) {
w = child;
ok = true;
break;
}
widgetPosition--;

}
}

if (!ok) {
// Did not find the child
return null;
}
} else {
// W identifies something that is not a "HasWidgets". This
// should not happen as all widget containers should implement
// HasWidgets.
return null;
}
}

return w;
}

private ServerConnector findConnectorById(ServerConnector root, String id) {
SharedState state = root.getState();
if (state instanceof AbstractComponentState
&& id.equals(((AbstractComponentState) state).id)) {
return root;
}
for (ServerConnector child : root.getChildren()) {
ServerConnector found = findConnectorById(child, id);
if (found != null) {
return found;
}
}

return null;
}

}

+ 131
- 0
client/src/com/vaadin/client/componentlocator/ComponentLocator.java Visa fil

@@ -0,0 +1,131 @@
/*
* 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 com.google.gwt.user.client.Element;
import com.vaadin.client.ApplicationConnection;
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.
*
* @since 7.2, moved from {@link com.vaadin.client.ComponentLocator}
*/
public class ComponentLocator {

private final LegacyLocatorStrategy legacyLocatorStrategy = new LegacyLocatorStrategy(
this);

/**
* Reference to ApplicationConnection instance.
*/

private ApplicationConnection client;

/**
* Construct a ComponentLocator for the given ApplicationConnection.
*
* @param client
* ApplicationConnection instance for the application.
*/
public ComponentLocator(ApplicationConnection client) {
this.client = client;
}

/**
* 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>
*
* @since 5.4
* @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.
*/
public String getPathForElement(Element targetElement) {
return legacyLocatorStrategy
.getPathForElement(getElement(targetElement));
}

/**
* Returns the element passed to the method. Or in case of Firefox 15,
* returns the real element that is in the DOM instead of the element passed
* to the method (which is the same element but not ==).
*
* @param targetElement
* the element to return
* @return the element passed to the method
*/
private Element getElement(Element targetElement) {
if (targetElement == null) {
return null;
}

if (!BrowserInfo.get().isFirefox()) {
return targetElement;
}

if (BrowserInfo.get().getBrowserMajorVersion() != 15) {
return targetElement;
}

// Firefox 15, you make me sad
if (targetElement.getNextSibling() != null) {
return (Element) targetElement.getNextSibling()
.getPreviousSibling();
}
if (targetElement.getPreviousSibling() != null) {
return (Element) targetElement.getPreviousSibling()
.getNextSibling();
}
// No siblings so this is the only child
return (Element) targetElement.getParentNode().getChild(0);
}

/**
* 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.
*
* @since 5.4
* @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.
*/
public Element getElementByPath(String path) {
return legacyLocatorStrategy.getElementByPath(path);
}

/**
* @return the application connection
*/
ApplicationConnection getClient() {
return client;
}
}

+ 637
- 0
client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java Visa fil

@@ -0,0 +1,637 @@
/*
* 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.Iterator;
import java.util.List;

import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.ui.HasWidgets;
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.ConnectorMap;
import com.vaadin.client.ServerConnector;
import com.vaadin.client.Util;
import com.vaadin.client.VCaption;
import com.vaadin.client.ui.SubPartAware;
import com.vaadin.client.ui.VCssLayout;
import com.vaadin.client.ui.VGridLayout;
import com.vaadin.client.ui.VOverlay;
import com.vaadin.client.ui.VTabsheetPanel;
import com.vaadin.client.ui.VUI;
import com.vaadin.client.ui.VWindow;
import com.vaadin.client.ui.orderedlayout.Slot;
import com.vaadin.client.ui.orderedlayout.VAbstractOrderedLayout;
import com.vaadin.client.ui.window.WindowConnector;
import com.vaadin.shared.AbstractComponentState;
import com.vaadin.shared.Connector;
import com.vaadin.shared.communication.SharedState;

/**
* The LegacyLocatorStrategy class handles the legacy locator syntax that was
* introduced in version 5.4 of the framework. The legacy locator strategy is
* always used if no other strategy claims responsibility for a locator string.
*
* @since 7.2
* @author Vaadin Ltd
*/
public class LegacyLocatorStrategy implements LocatorStrategy {
private final ComponentLocator componentLocator;
/**
* Separator used in the String locator between a parent and a child widget.
*/
static final String PARENTCHILD_SEPARATOR = "/";
/**
* Separator used in the String locator between the part identifying the
* containing widget and the part identifying the target element within the
* widget.
*/
static final String SUBPART_SEPARATOR = "#";
/**
* String that identifies the root panel when appearing first in the String
* locator.
*/
static final String ROOT_ID = "Root";

public LegacyLocatorStrategy(ComponentLocator componentLocator) {
this.componentLocator = componentLocator;
}

@Override
public String getPathForElement(Element targetElement) {
ComponentConnector connector = Util.findPaintable(
componentLocator.getClient(), targetElement);

Widget w = null;
if (connector != null) {
// If we found a Paintable then we use that as reference. We should
// find the Paintable for all but very special cases (like
// overlays).
w = connector.getWidget();

/*
* Still if the Paintable contains a widget that implements
* SubPartAware, we want to use that as a reference
*/
Widget targetParent = findParentWidget(targetElement, w);
while (targetParent != w && targetParent != null) {
if (targetParent instanceof SubPartAware) {
/*
* The targetParent widget is a child of the Paintable and
* the first parent (of the targetElement) that implements
* SubPartAware
*/
w = targetParent;
break;
}
targetParent = targetParent.getParent();
}
}
if (w == null) {
// Check if the element is part of a widget that is attached
// directly to the root panel
RootPanel rootPanel = RootPanel.get();
int rootWidgetCount = rootPanel.getWidgetCount();
for (int i = 0; i < rootWidgetCount; i++) {
Widget rootWidget = rootPanel.getWidget(i);
if (rootWidget.getElement().isOrHasChild(targetElement)) {
// The target element is contained by this root widget
w = findParentWidget(targetElement, rootWidget);
break;
}
}
if (w != null) {
// We found a widget but we should still see if we find a
// SubPartAware implementor (we cannot find the Paintable as
// there is no link from VOverlay to its paintable/owner).
Widget subPartAwareWidget = findSubPartAwareParentWidget(w);
if (subPartAwareWidget != null) {
w = subPartAwareWidget;
}
}
}

if (w == null) {
// Containing widget not found
return null;
}

// Determine the path for the target widget
String path = getPathForWidget(w);
if (path == null) {
/*
* No path could be determined for the target widget. Cannot create
* a locator string.
*/
return null;
}

// The parent check is a work around for Firefox 15 which fails to
// compare elements properly (#9534)
if (w.getElement() == targetElement) {
/*
* We are done if the target element is the root of the target
* widget.
*/
return path;
} else if (w instanceof SubPartAware) {
/*
* If the widget can provide an identifier for the targetElement we
* let it do that
*/
String elementLocator = ((SubPartAware) w)
.getSubPartName(targetElement);
if (elementLocator != null) {
return path + LegacyLocatorStrategy.SUBPART_SEPARATOR
+ elementLocator;
}
}
/*
* If everything else fails we use the DOM path to identify the target
* element
*/
String domPath = getDOMPathForElement(targetElement, w.getElement());
if (domPath == null) {
return path;
} else {
return path + domPath;
}
}

@Override
public Element getElementByPath(String path) {
/*
* Path is of type "targetWidgetPath#componentPart" or
* "targetWidgetPath".
*/
String parts[] = path.split(LegacyLocatorStrategy.SUBPART_SEPARATOR, 2);
String widgetPath = parts[0];
Widget w = getWidgetFromPath(widgetPath);
if (w == null || !Util.isAttachedAndDisplayed(w)) {
return null;
}

if (parts.length == 1) {
int pos = widgetPath.indexOf("domChild");
if (pos == -1) {
return w.getElement();
}

// Contains dom reference to a sub element of the widget
String subPath = widgetPath.substring(pos);
return getElementByDOMPath(w.getElement(), subPath);
} else if (parts.length == 2) {
if (w instanceof SubPartAware) {
return ((SubPartAware) w).getSubPartElement(parts[1]);
}
}
return null;
}

/**
* Finds the first widget in the hierarchy (moving upwards) that implements
* SubPartAware. Returns the SubPartAware implementor or null if none is
* found.
*
* @param w
* The widget to start from. This is returned if it implements
* SubPartAware.
* @return The first widget (upwards in hierarchy) that implements
* SubPartAware or null
*/
Widget findSubPartAwareParentWidget(Widget w) {

while (w != null) {
if (w instanceof SubPartAware) {
return w;
}
w = w.getParent();
}
return null;
}

/**
* Returns the first widget found when going from {@code targetElement}
* upwards in the DOM hierarchy, assuming that {@code ancestorWidget} is a
* parent of {@code targetElement}.
*
* @param targetElement
* @param ancestorWidget
* @return The widget whose root element is a parent of
* {@code targetElement}.
*/
private Widget findParentWidget(Element targetElement, Widget ancestorWidget) {
/*
* As we cannot resolve Widgets from the element we start from the
* widget and move downwards to the correct child widget, as long as we
* find one.
*/
if (ancestorWidget instanceof HasWidgets) {
for (Widget w : ((HasWidgets) ancestorWidget)) {
if (w.getElement().isOrHasChild(targetElement)) {
return findParentWidget(targetElement, w);
}
}
}

// No children found, this is it
return ancestorWidget;
}

/**
* Locates an element based on a DOM path and a base element.
*
* @param baseElement
* The base element which the path is relative to
* @param path
* String locator (consisting of domChild[x] parts) that
* identifies the element
* @return The element identified by path, relative to baseElement or null
* if the element could not be found.
*/
private Element getElementByDOMPath(Element baseElement, String path) {
String parts[] = path.split(PARENTCHILD_SEPARATOR);
Element element = baseElement;

for (String part : parts) {
if (part.startsWith("domChild[")) {
String childIndexString = part.substring("domChild[".length(),
part.length() - 1);

if (Util.findWidget(baseElement, null) instanceof VAbstractOrderedLayout) {
if (element.hasChildNodes()) {
Element e = element.getFirstChildElement().cast();
String cn = e.getClassName();
if (cn != null
&& (cn.equals("v-expand") || cn
.contains("v-has-caption"))) {
element = e;
}
}
}

try {
int childIndex = Integer.parseInt(childIndexString);
element = DOM.getChild(element, childIndex);
} catch (Exception e) {
return null;
}

if (element == null) {
return null;
}

}
}

return element;
}

/**
* Generates a String locator using domChild[x] parts for the element
* relative to the baseElement.
*
* @param element
* The target element
* @param baseElement
* The starting point for the locator. The generated path is
* relative to this element.
* @return A String locator that can be used to locate the target element
* using
* {@link #getElementByDOMPath(com.google.gwt.user.client.Element, String)}
* or null if the locator String cannot be created.
*/
private String getDOMPathForElement(Element element, Element baseElement) {
Element e = element;
String path = "";
while (true) {
int childIndex = -1;
Element siblingIterator = e;
while (siblingIterator != null) {
childIndex++;
siblingIterator = siblingIterator.getPreviousSiblingElement()
.cast();
}

path = PARENTCHILD_SEPARATOR + "domChild[" + childIndex + "]"
+ path;

JavaScriptObject parent = e.getParentElement();
if (parent == null) {
return null;
}
// The parent check is a work around for Firefox 15 which fails to
// compare elements properly (#9534)
if (parent == baseElement) {
break;
}

e = parent.cast();
}

return path;
}

/**
* Creates a locator String for the given widget. The path can be used to
* locate the widget using {@link #getWidgetFromPath(String)}.
* <p/>
* Returns null if no path can be determined for the widget or if the widget
* is null.
*
* @param w
* The target widget
* @return A String locator for the widget
*/
private String getPathForWidget(Widget w) {
if (w == null) {
return null;
}
String elementId = w.getElement().getId();
if (elementId != null && !elementId.isEmpty()
&& !elementId.startsWith("gwt-uid-")) {
// Use PID_S+id if the user has set an id but do not use it for auto
// generated id:s as these might not be consistent
return "PID_S" + elementId;
} else if (w instanceof VUI) {
return "";
} else if (w instanceof VWindow) {
Connector windowConnector = ConnectorMap.get(
componentLocator.getClient()).getConnector(w);
List<WindowConnector> subWindowList = componentLocator.getClient()
.getUIConnector().getSubWindows();
int indexOfSubWindow = subWindowList.indexOf(windowConnector);
return PARENTCHILD_SEPARATOR + "VWindow[" + indexOfSubWindow + "]";
} else if (w instanceof RootPanel) {
return ROOT_ID;
}

Widget parent = w.getParent();

String basePath = getPathForWidget(parent);
if (basePath == null) {
return null;
}
String simpleName = Util.getSimpleName(w);

/*
* Check if the parent implements Iterable. At least VPopupView does not
* implement HasWdgets so we cannot check for that.
*/
if (!(parent instanceof Iterable<?>)) {
// Parent does not implement Iterable so we cannot find out which
// child this is
return null;
}

Iterator<?> i = ((Iterable<?>) parent).iterator();
int pos = 0;
while (i.hasNext()) {
Object child = i.next();
if (child == w) {
return basePath + PARENTCHILD_SEPARATOR + simpleName + "["
+ pos + "]";
}
String simpleName2 = Util.getSimpleName(child);
if (simpleName.equals(simpleName2)) {
pos++;
}
}

return null;
}

/**
* Locates the widget based on a String locator.
*
* @param path
* The String locator that identifies the widget.
* @return The Widget identified by the String locator or null if the widget
* could not be identified.
*/
private Widget getWidgetFromPath(String path) {
Widget w = null;
String parts[] = path.split(PARENTCHILD_SEPARATOR);

for (int i = 0; i < parts.length; i++) {
String part = parts[i];

if (part.equals(ROOT_ID)) {
w = RootPanel.get();
} else if (part.equals("")) {
w = componentLocator.getClient().getUIConnector().getWidget();
} else if (w == null) {
String id = part;
// Must be old static pid (PID_S*)
ServerConnector connector = ConnectorMap.get(
componentLocator.getClient()).getConnector(id);
if (connector == null) {
// Lookup by component id
// TODO Optimize this
connector = findConnectorById(componentLocator.getClient()
.getUIConnector(), id.substring(5));
}

if (connector instanceof ComponentConnector) {
w = ((ComponentConnector) connector).getWidget();
} else {
// Not found
return null;
}
} else if (part.startsWith("domChild[")) {
// The target widget has been found and the rest identifies the
// element
break;
} else if (w instanceof Iterable) {
// W identifies a widget that contains other widgets, as it
// should. Try to locate the child
Iterable<?> parent = (Iterable<?>) w;

// Part is of type "VVerticalLayout[0]", split this into
// VVerticalLayout and 0
String[] split = part.split("\\[", 2);
String widgetClassName = split[0];
String indexString = split[1].substring(0,
split[1].length() - 1);
int widgetPosition = Integer.parseInt(indexString);

// AbsolutePanel in GridLayout has been removed -> skip it
if (w instanceof VGridLayout
&& "AbsolutePanel".equals(widgetClassName)) {
continue;
}

// FlowPane in CSSLayout has been removed -> skip it
if (w instanceof VCssLayout
&& "VCssLayout$FlowPane".equals(widgetClassName)) {
continue;
}

// ChildComponentContainer and VOrderedLayout$Slot have been
// replaced with Slot
if (w instanceof VAbstractOrderedLayout
&& ("ChildComponentContainer".equals(widgetClassName) || "VOrderedLayout$Slot"
.equals(widgetClassName))) {
widgetClassName = "Slot";
}

if (w instanceof VTabsheetPanel && widgetPosition != 0) {
// TabSheetPanel now only contains 1 connector => the index
// is always 0 which indicates the widget in the active tab
widgetPosition = 0;
}
if (w instanceof VOverlay
&& "VCalendarPanel".equals(widgetClassName)) {
// Vaadin 7.1 adds a wrapper for datefield popups
parent = (Iterable<?>) ((Iterable) parent).iterator()
.next();
}
/*
* The new grid and ordered layotus do not contain
* ChildComponentContainer widgets. This is instead simulated by
* constructing a path step that would find the desired widget
* from the layout and injecting it as the next search step
* (which would originally have found the widget inside the
* ChildComponentContainer)
*/
if ((w instanceof VGridLayout)
&& "ChildComponentContainer".equals(widgetClassName)
&& i + 1 < parts.length) {

HasWidgets layout = (HasWidgets) w;

String nextPart = parts[i + 1];
String[] nextSplit = nextPart.split("\\[", 2);
String nextWidgetClassName = nextSplit[0];

// Find the n:th child and count the number of children with
// the same type before it
int nextIndex = 0;
for (Widget child : layout) {
boolean matchingType = nextWidgetClassName.equals(Util
.getSimpleName(child));
if (matchingType && widgetPosition == 0) {
// This is the n:th child that we looked for
break;
} else if (widgetPosition < 0) {
// Error if we're past the desired position without
// a match
return null;
} else if (matchingType) {
// If this was another child of the expected type,
// increase the count for the next step
nextIndex++;
}

// Don't count captions
if (!(child instanceof VCaption)) {
widgetPosition--;
}
}

// Advance to the next step, this time checking for the
// actual child widget
parts[i + 1] = nextWidgetClassName + '[' + nextIndex + ']';
continue;
}

// Locate the child
Iterator<? extends Widget> iterator;

/*
* VWindow and VContextMenu workarounds for backwards
* compatibility
*/
if (widgetClassName.equals("VWindow")) {
List<WindowConnector> windows = componentLocator
.getClient().getUIConnector().getSubWindows();
List<VWindow> windowWidgets = new ArrayList<VWindow>(
windows.size());
for (WindowConnector wc : windows) {
windowWidgets.add(wc.getWidget());
}
iterator = windowWidgets.iterator();
} else if (widgetClassName.equals("VContextMenu")) {
return componentLocator.getClient().getContextMenu();
} else {
iterator = (Iterator<? extends Widget>) parent.iterator();
}

boolean ok = false;

// Find the widgetPosition:th child of type "widgetClassName"
while (iterator.hasNext()) {

Widget child = iterator.next();
String simpleName2 = Util.getSimpleName(child);

if (!widgetClassName.equals(simpleName2)
&& child instanceof Slot) {
/*
* Support legacy tests without any selector for the
* Slot widget (i.e. /VVerticalLayout[0]/VButton[0]) by
* directly checking the stuff inside the slot
*/
child = ((Slot) child).getWidget();
simpleName2 = Util.getSimpleName(child);
}

if (widgetClassName.equals(simpleName2)) {
if (widgetPosition == 0) {
w = child;
ok = true;
break;
}
widgetPosition--;

}
}

if (!ok) {
// Did not find the child
return null;
}
} else {
// W identifies something that is not a "HasWidgets". This
// should not happen as all widget containers should implement
// HasWidgets.
return null;
}
}

return w;
}

private ServerConnector findConnectorById(ServerConnector root, String id) {
SharedState state = root.getState();
if (state instanceof AbstractComponentState
&& id.equals(((AbstractComponentState) state).id)) {
return root;
}
for (ServerConnector child : root.getChildren()) {
ServerConnector found = findConnectorById(child, id);
if (found != null) {
return found;
}
}

return null;
}
}

+ 34
- 0
client/src/com/vaadin/client/componentlocator/LocatorStrategy.java Visa fil

@@ -0,0 +1,34 @@
/*
* 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 com.google.gwt.user.client.Element;

/**
* This interface should be implemented by all locator strategies. A locator
* strategy is responsible for generating and decoding a string that identifies
* an element in the DOM. A strategy can implement its own syntax for the
* locator string, which may be completely different from any other strategy's
* syntax.
*
* @since 7.2
* @author Vaadin Ltd
*/
public interface LocatorStrategy {
String getPathForElement(Element targetElement);

Element getElementByPath(String path);
}

+ 1
- 1
client/src/com/vaadin/client/debug/internal/TestBenchSection.java Visa fil

@@ -35,10 +35,10 @@ import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.ApplicationConfiguration;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.ComponentConnector;
import com.vaadin.client.ComponentLocator;
import com.vaadin.client.ServerConnector;
import com.vaadin.client.Util;
import com.vaadin.client.ValueMap;
import com.vaadin.client.componentlocator.ComponentLocator;

/**
* Provides functionality for picking selectors for Vaadin TestBench.

+ 2
- 2
client/src/com/vaadin/client/ui/SubPartAware.java Visa fil

@@ -17,7 +17,7 @@ package com.vaadin.client.ui;

import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.ComponentLocator;
import com.vaadin.client.componentlocator.ComponentLocator;

/**
* Interface implemented by {@link Widget}s which can provide identifiers for at
@@ -59,4 +59,4 @@ public interface SubPartAware {
*/
String getSubPartName(Element subElement);

}
}

uitest/src/com/vaadin/tests/gwtadapter/componentlocator/TestDetachedNotPresent.html → uitest/src/com/vaadin/tests/componentlocator/TestDetachedNotPresent.html Visa fil


Laddar…
Avbryt
Spara