From 277e697f7d9258983e52042757baac09b4fe1908 Mon Sep 17 00:00:00 2001 From: Henri Sara Date: Fri, 30 Aug 2013 08:37:27 +0300 Subject: Refactor the debug window hierarchy section (#12471) Various parts of the hierarchy section are split out to separate classes to permit reusing some of them in other parts of the debug window. Change-Id: I32ed562bc9d95df2d3c899d9a17a60d45aa703fd --- .../client/debug/internal/AnalyzeLayoutsPanel.java | 267 ++++++++++++ .../client/debug/internal/ConnectorInfoPanel.java | 107 +++++ .../client/debug/internal/HierarchyPanel.java | 138 ++++++ .../client/debug/internal/HierarchySection.java | 474 ++------------------- .../vaadin/client/debug/internal/Highlight.java | 15 - .../debug/internal/OptimizedWidgetsetPanel.java | 137 ++++++ .../debug/internal/SelectConnectorListener.java | 37 ++ 7 files changed, 728 insertions(+), 447 deletions(-) create mode 100644 client/src/com/vaadin/client/debug/internal/AnalyzeLayoutsPanel.java create mode 100644 client/src/com/vaadin/client/debug/internal/ConnectorInfoPanel.java create mode 100644 client/src/com/vaadin/client/debug/internal/HierarchyPanel.java create mode 100644 client/src/com/vaadin/client/debug/internal/OptimizedWidgetsetPanel.java create mode 100644 client/src/com/vaadin/client/debug/internal/SelectConnectorListener.java diff --git a/client/src/com/vaadin/client/debug/internal/AnalyzeLayoutsPanel.java b/client/src/com/vaadin/client/debug/internal/AnalyzeLayoutsPanel.java new file mode 100644 index 0000000000..7561bc2c03 --- /dev/null +++ b/client/src/com/vaadin/client/debug/internal/AnalyzeLayoutsPanel.java @@ -0,0 +1,267 @@ +/* + * 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.debug.internal; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.google.gwt.core.client.JsArray; +import com.google.gwt.dom.client.Style.TextDecoration; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.MouseOutEvent; +import com.google.gwt.event.dom.client.MouseOutHandler; +import com.google.gwt.event.dom.client.MouseOverEvent; +import com.google.gwt.event.dom.client.MouseOverHandler; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.VerticalPanel; +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.ComputedStyle; +import com.vaadin.client.ConnectorMap; +import com.vaadin.client.ServerConnector; +import com.vaadin.client.SimpleTree; +import com.vaadin.client.Util; +import com.vaadin.client.ValueMap; + +/** + * Analyze layouts view panel of the debug window. + * + * @since 7.1.4 + */ +public class AnalyzeLayoutsPanel extends FlowPanel { + + private List listeners = new ArrayList(); + + public void update() { + clear(); + add(new Label("Analyzing layouts...")); + List runningApplications = ApplicationConfiguration + .getRunningApplications(); + for (ApplicationConnection applicationConnection : runningApplications) { + applicationConnection.analyzeLayouts(); + } + } + + public void meta(ApplicationConnection ac, ValueMap meta) { + clear(); + JsArray valueMapArray = meta + .getJSValueMapArray("invalidLayouts"); + int size = valueMapArray.length(); + + if (size > 0) { + SimpleTree root = new SimpleTree("Layouts analyzed, " + size + + " top level problems"); + for (int i = 0; i < size; i++) { + printLayoutError(ac, valueMapArray.get(i), root); + } + root.open(false); + add(root); + } else { + add(new Label("Layouts analyzed, no top level problems")); + } + + Set zeroHeightComponents = new HashSet(); + Set zeroWidthComponents = new HashSet(); + findZeroSizeComponents(zeroHeightComponents, zeroWidthComponents, + ac.getUIConnector()); + if (zeroHeightComponents.size() > 0 || zeroWidthComponents.size() > 0) { + add(new HTML("

Client side notifications

" + + " The following relative sized components were " + + "rendered to a zero size container on the client side." + + " Note that these are not necessarily invalid " + + "states, but reported here as they might be.")); + if (zeroHeightComponents.size() > 0) { + add(new HTML("

Vertically zero size:

")); + printClientSideDetectedIssues(zeroHeightComponents, ac); + } + if (zeroWidthComponents.size() > 0) { + add(new HTML("

Horizontally zero size:

")); + printClientSideDetectedIssues(zeroWidthComponents, ac); + } + } + + } + + private void printClientSideDetectedIssues( + Set zeroSized, ApplicationConnection ac) { + + // keep track of already highlighted parents + HashSet parents = new HashSet(); + + for (final ComponentConnector connector : zeroSized) { + final ServerConnector parent = connector.getParent(); + final String parentId = parent.getConnectorId(); + + final Label errorDetails = new Label(Util.getSimpleName(connector) + + "[" + connector.getConnectorId() + "]" + " inside " + + Util.getSimpleName(parent)); + + if (parent instanceof ComponentConnector) { + final ComponentConnector parentConnector = (ComponentConnector) parent; + if (!parents.contains(parentId)) { + parents.add(parentId); + Highlight.show(parentConnector, "yellow"); + } + + errorDetails.addMouseOverHandler(new MouseOverHandler() { + @Override + public void onMouseOver(MouseOverEvent event) { + Highlight.hideAll(); + Highlight.show(parentConnector, "yellow"); + Highlight.show(connector); + errorDetails.getElement().getStyle() + .setTextDecoration(TextDecoration.UNDERLINE); + } + }); + errorDetails.addMouseOutHandler(new MouseOutHandler() { + @Override + public void onMouseOut(MouseOutEvent event) { + Highlight.hideAll(); + errorDetails.getElement().getStyle() + .setTextDecoration(TextDecoration.NONE); + } + }); + errorDetails.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + fireSelectEvent(connector); + } + }); + + } + + Highlight.show(connector); + add(errorDetails); + + } + } + + private void printLayoutError(ApplicationConnection ac, ValueMap valueMap, + SimpleTree root) { + final String pid = valueMap.getString("id"); + + // find connector + final ComponentConnector connector = (ComponentConnector) ConnectorMap + .get(ac).getConnector(pid); + + if (connector == null) { + root.add(new SimpleTree("[" + pid + "] NOT FOUND")); + return; + } + + Highlight.show(connector); + + final SimpleTree errorNode = new SimpleTree( + Util.getSimpleName(connector) + " id: " + pid); + errorNode.addDomHandler(new MouseOverHandler() { + @Override + public void onMouseOver(MouseOverEvent event) { + Highlight.showOnly(connector); + ((Widget) event.getSource()).getElement().getStyle() + .setTextDecoration(TextDecoration.UNDERLINE); + } + }, MouseOverEvent.getType()); + errorNode.addDomHandler(new MouseOutHandler() { + @Override + public void onMouseOut(MouseOutEvent event) { + Highlight.hideAll(); + ((Widget) event.getSource()).getElement().getStyle() + .setTextDecoration(TextDecoration.NONE); + } + }, MouseOutEvent.getType()); + + errorNode.addDomHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + if (event.getNativeEvent().getEventTarget().cast() == errorNode + .getElement().getChild(1).cast()) { + fireSelectEvent(connector); + } + } + }, ClickEvent.getType()); + + VerticalPanel errorDetails = new VerticalPanel(); + + if (valueMap.containsKey("heightMsg")) { + errorDetails.add(new Label("Height problem: " + + valueMap.getString("heightMsg"))); + } + if (valueMap.containsKey("widthMsg")) { + errorDetails.add(new Label("Width problem: " + + valueMap.getString("widthMsg"))); + } + if (errorDetails.getWidgetCount() > 0) { + errorNode.add(errorDetails); + } + if (valueMap.containsKey("subErrors")) { + HTML l = new HTML( + "Expand this node to show problems that may be dependent on this problem."); + errorDetails.add(l); + JsArray suberrors = valueMap + .getJSValueMapArray("subErrors"); + for (int i = 0; i < suberrors.length(); i++) { + ValueMap value = suberrors.get(i); + printLayoutError(ac, value, errorNode); + } + + } + root.add(errorNode); + } + + private void findZeroSizeComponents( + Set zeroHeightComponents, + Set zeroWidthComponents, + ComponentConnector connector) { + Widget widget = connector.getWidget(); + ComputedStyle computedStyle = new ComputedStyle(widget.getElement()); + if (computedStyle.getIntProperty("height") == 0) { + zeroHeightComponents.add(connector); + } + if (computedStyle.getIntProperty("width") == 0) { + zeroWidthComponents.add(connector); + } + List children = connector.getChildren(); + for (ServerConnector serverConnector : children) { + if (serverConnector instanceof ComponentConnector) { + findZeroSizeComponents(zeroHeightComponents, + zeroWidthComponents, + (ComponentConnector) serverConnector); + } + } + } + + public void addListener(SelectConnectorListener listener) { + listeners.add(listener); + } + + public void removeListener(SelectConnectorListener listener) { + listeners.remove(listener); + } + + private void fireSelectEvent(ServerConnector connector) { + for (SelectConnectorListener listener : listeners) { + listener.select(connector, null); + } + } + +} diff --git a/client/src/com/vaadin/client/debug/internal/ConnectorInfoPanel.java b/client/src/com/vaadin/client/debug/internal/ConnectorInfoPanel.java new file mode 100644 index 0000000000..fc7b55497e --- /dev/null +++ b/client/src/com/vaadin/client/debug/internal/ConnectorInfoPanel.java @@ -0,0 +1,107 @@ +/* + * 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.debug.internal; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HTML; +import com.vaadin.client.ComponentConnector; +import com.vaadin.client.JsArrayObject; +import com.vaadin.client.ServerConnector; +import com.vaadin.client.Util; +import com.vaadin.client.VConsole; +import com.vaadin.client.metadata.NoDataException; +import com.vaadin.client.metadata.Property; +import com.vaadin.client.ui.AbstractConnector; +import com.vaadin.shared.AbstractComponentState; +import com.vaadin.shared.communication.SharedState; + +/** + * Connector information view panel of the debug window. + * + * @since 7.1.4 + */ +public class ConnectorInfoPanel extends FlowPanel { + + /** + * Update the panel to show information about a connector. + * + * @param connector + */ + public void update(ServerConnector connector) { + SharedState state = connector.getState(); + + Set ignoreProperties = new HashSet(); + ignoreProperties.add("id"); + + String html = getRowHTML("Id", connector.getConnectorId()); + html += getRowHTML("Connector", Util.getSimpleName(connector)); + + if (connector instanceof ComponentConnector) { + ComponentConnector component = (ComponentConnector) connector; + + ignoreProperties.addAll(Arrays.asList("caption", "description", + "width", "height")); + + AbstractComponentState componentState = component.getState(); + + html += getRowHTML("Widget", + Util.getSimpleName(component.getWidget())); + html += getRowHTML("Caption", componentState.caption); + html += getRowHTML("Description", componentState.description); + html += getRowHTML("Width", componentState.width + " (actual: " + + component.getWidget().getOffsetWidth() + "px)"); + html += getRowHTML("Height", componentState.height + " (actual: " + + component.getWidget().getOffsetHeight() + "px)"); + } + + try { + JsArrayObject properties = AbstractConnector + .getStateType(connector).getPropertiesAsArray(); + for (int i = 0; i < properties.size(); i++) { + Property property = properties.get(i); + String name = property.getName(); + if (!ignoreProperties.contains(name)) { + html += getRowHTML(property.getDisplayName(), + property.getValue(state)); + } + } + } catch (NoDataException e) { + html += "
Could not read state, error has been logged to the console
"; + VConsole.error(e); + } + + clear(); + add(new HTML(html)); + } + + private String getRowHTML(String caption, Object value) { + return "
" + caption + + "" + + Util.escapeHTML(String.valueOf(value)) + "
"; + } + + /** + * Clear the contents of the panel. + */ + public void clearContents() { + clear(); + } +} diff --git a/client/src/com/vaadin/client/debug/internal/HierarchyPanel.java b/client/src/com/vaadin/client/debug/internal/HierarchyPanel.java new file mode 100644 index 0000000000..759dbf00dd --- /dev/null +++ b/client/src/com/vaadin/client/debug/internal/HierarchyPanel.java @@ -0,0 +1,138 @@ +/* + * 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.debug.internal; + +import java.util.ArrayList; +import java.util.List; + +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.DoubleClickEvent; +import com.google.gwt.event.dom.client.DoubleClickHandler; +import com.google.gwt.event.dom.client.HasDoubleClickHandlers; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ApplicationConfiguration; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ServerConnector; +import com.vaadin.client.SimpleTree; +import com.vaadin.client.Util; + +/** + * Hierarchy view panel of the debug window. This class can be used in various + * debug window sections to show the current connector hierarchy. + * + * @since 7.1.4 + */ +public class HierarchyPanel extends FlowPanel { + + // TODO separate click listeners for simple selection and doubleclick + private List listeners = new ArrayList(); + + public void update() { + clear(); + // TODO Clearing and rebuilding the contents is not optimal for UX as + // any previous expansions are lost. + SimplePanel trees = new SimplePanel(); + + for (ApplicationConnection application : ApplicationConfiguration + .getRunningApplications()) { + ServerConnector uiConnector = application.getUIConnector(); + Widget connectorTree = buildConnectorTree(uiConnector); + + trees.add(connectorTree); + } + + add(trees); + } + + private Widget buildConnectorTree(final ServerConnector connector) { + String connectorString = Util.getConnectorString(connector); + + List children = connector.getChildren(); + + Widget widget; + if (children == null || children.isEmpty()) { + // Leaf node, just add a label + Label label = new Label(connectorString); + label.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + Highlight.showOnly(connector); + showServerDebugInfo(connector); + } + }); + widget = label; + } else { + SimpleTree tree = new SimpleTree(connectorString) { + @Override + protected void select(ClickEvent event) { + super.select(event); + Highlight.showOnly(connector); + showServerDebugInfo(connector); + } + }; + for (ServerConnector child : children) { + tree.add(buildConnectorTree(child)); + } + widget = tree; + } + + if (widget instanceof HasDoubleClickHandlers) { + HasDoubleClickHandlers has = (HasDoubleClickHandlers) widget; + has.addDoubleClickHandler(new DoubleClickHandler() { + @Override + public void onDoubleClick(DoubleClickEvent event) { + fireSelectEvent(connector); + } + }); + } + + return widget; + } + + public void addListener(SelectConnectorListener listener) { + listeners.add(listener); + } + + public void removeListener(SelectConnectorListener listener) { + listeners.remove(listener); + } + + private void fireSelectEvent(ServerConnector connector) { + for (SelectConnectorListener listener : listeners) { + listener.select(connector, null); + } + } + + /** + * Outputs debug information on the server - usually in the console of an + * IDE, with a clickable reference to the relevant code location. + * + * @since 7.1 + * @param connector + * show debug info for this connector + */ + static void showServerDebugInfo(ServerConnector connector) { + if (connector != null) { + connector.getConnection().getUIConnector() + .showServerDebugInfo(connector); + } + } + +} diff --git a/client/src/com/vaadin/client/debug/internal/HierarchySection.java b/client/src/com/vaadin/client/debug/internal/HierarchySection.java index 90c9086d7d..616bf70c38 100644 --- a/client/src/com/vaadin/client/debug/internal/HierarchySection.java +++ b/client/src/com/vaadin/client/debug/internal/HierarchySection.java @@ -15,23 +15,9 @@ */ package com.vaadin.client.debug.internal; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import com.google.gwt.core.client.JsArray; -import com.google.gwt.dom.client.Style.TextDecoration; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.event.dom.client.DoubleClickEvent; -import com.google.gwt.event.dom.client.DoubleClickHandler; -import com.google.gwt.event.dom.client.HasDoubleClickHandlers; import com.google.gwt.event.dom.client.KeyCodes; -import com.google.gwt.event.dom.client.MouseOutEvent; -import com.google.gwt.event.dom.client.MouseOutHandler; -import com.google.gwt.event.dom.client.MouseOverEvent; -import com.google.gwt.event.dom.client.MouseOverHandler; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.Event; @@ -40,28 +26,15 @@ import com.google.gwt.user.client.Event.NativePreviewHandler; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.HTML; -import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.RootPanel; import com.google.gwt.user.client.ui.SimplePanel; -import com.google.gwt.user.client.ui.VerticalPanel; 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.ComputedStyle; -import com.vaadin.client.ConnectorMap; -import com.vaadin.client.JsArrayObject; import com.vaadin.client.ServerConnector; -import com.vaadin.client.SimpleTree; import com.vaadin.client.Util; -import com.vaadin.client.VConsole; import com.vaadin.client.ValueMap; -import com.vaadin.client.metadata.NoDataException; -import com.vaadin.client.metadata.Property; -import com.vaadin.client.ui.AbstractConnector; -import com.vaadin.client.ui.UnknownComponentConnector; -import com.vaadin.shared.AbstractComponentState; -import com.vaadin.shared.communication.SharedState; /** * Provides functionality for examining the UI component hierarchy. @@ -73,7 +46,15 @@ public class HierarchySection implements Section { private final DebugButton tabButton = new DebugButton(Icon.HIERARCHY, "Examine component hierarchy"); - private final FlowPanel content = new FlowPanel(); + private final SimplePanel content = new SimplePanel(); + + // TODO highlighting logic is split between these, should be refactored + private final FlowPanel helpPanel = new FlowPanel(); + private final ConnectorInfoPanel infoPanel = new ConnectorInfoPanel(); + private final HierarchyPanel hierarchyPanel = new HierarchyPanel(); + private final OptimizedWidgetsetPanel widgetsetPanel = new OptimizedWidgetsetPanel(); + private final AnalyzeLayoutsPanel analyzeLayoutsPanel = new AnalyzeLayoutsPanel(); + private final FlowPanel controls = new FlowPanel(); private final Button find = new DebugButton(Icon.HIGHLIGHT, @@ -125,79 +106,40 @@ public class HierarchySection implements Section { } }); + hierarchyPanel.addListener(new SelectConnectorListener() { + @Override + public void select(ServerConnector connector, Element element) { + printState(connector, true); + } + }); + + analyzeLayoutsPanel.addListener(new SelectConnectorListener() { + @Override + public void select(ServerConnector connector, Element element) { + printState(connector, true); + } + }); + content.setStylePrimaryName(VDebugWindow.STYLENAME + "-hierarchy"); + initializeHelpPanel(); + content.setWidget(helpPanel); + } + + private void initializeHelpPanel() { HTML info = new HTML(showHierarchy.getHTML() + " " + showHierarchy.getTitle() + "
" + find.getHTML() + " " + find.getTitle() + "
" + analyze.getHTML() + " " + analyze.getTitle() + "
" + generateWS.getHTML() + " " + generateWS.getTitle() + "
"); info.setStyleName(VDebugWindow.STYLENAME + "-info"); - content.add(info); + helpPanel.add(info); } private void showHierarchy() { Highlight.hideAll(); - content.clear(); - - // TODO Clearing and rebuilding the contents is not optimal for UX as - // any previous expansions are lost. - SimplePanel trees = new SimplePanel(); - - for (ApplicationConnection application : ApplicationConfiguration - .getRunningApplications()) { - ServerConnector uiConnector = application.getUIConnector(); - Widget connectorTree = buildConnectorTree(uiConnector); - - trees.add(connectorTree); - } - - content.add(trees); - } - - private Widget buildConnectorTree(final ServerConnector connector) { - String connectorString = Util.getConnectorString(connector); - - List children = connector.getChildren(); - - Widget widget; - if (children == null || children.isEmpty()) { - // Leaf node, just add a label - Label label = new Label(connectorString); - label.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - Highlight.showOnly(connector); - Highlight.showServerDebugInfo(connector); - } - }); - widget = label; - } else { - SimpleTree tree = new SimpleTree(connectorString) { - @Override - protected void select(ClickEvent event) { - super.select(event); - Highlight.showOnly(connector); - Highlight.showServerDebugInfo(connector); - } - }; - for (ServerConnector child : children) { - tree.add(buildConnectorTree(child)); - } - widget = tree; - } - - if (widget instanceof HasDoubleClickHandlers) { - HasDoubleClickHandlers has = (HasDoubleClickHandlers) widget; - has.addDoubleClickHandler(new DoubleClickHandler() { - @Override - public void onDoubleClick(DoubleClickEvent event) { - printState(connector, true); - } - }); - } - - return widget; + hierarchyPanel.update(); + content.setWidget(hierarchyPanel); } @Override @@ -226,302 +168,19 @@ public class HierarchySection implements Section { } private void generateWidgetset() { - - content.clear(); - HTML h = new HTML("Getting used connectors"); - content.add(h); - - String s = ""; - for (ApplicationConnection ac : ApplicationConfiguration - .getRunningApplications()) { - ApplicationConfiguration conf = ac.getConfiguration(); - s += "

Used connectors for " + conf.getServiceUrl() + "

"; - - for (String connectorName : getUsedConnectorNames(conf)) { - s += connectorName + "
"; - } - - s += "

To make an optimized widgetset based on these connectors, do:

"; - s += "

1. Add to your widgetset.gwt.xml file:

"; - s += ""; - - s += "

2. Add the following java file to your project:

"; - s += ""; - s += "

3. Recompile widgetset

"; - - } - - h.setHTML(s); - } - - private Set getUsedConnectorNames( - ApplicationConfiguration configuration) { - int tag = 0; - Set usedConnectors = new HashSet(); - while (true) { - String serverSideClass = configuration - .getServerSideClassNameForTag(tag); - if (serverSideClass == null) { - break; - } - Class connectorClass = configuration - .getConnectorClassByEncodedTag(tag); - if (connectorClass == null) { - break; - } - - if (connectorClass != UnknownComponentConnector.class) { - usedConnectors.add(connectorClass.getName()); - } - tag++; - if (tag > 10000) { - // Sanity check - VConsole.error("Search for used connector classes was forcefully terminated"); - break; - } - } - return usedConnectors; - } - - public String generateOptimizedWidgetSet(Set usedConnectors) { - String s = "import java.util.HashSet;\n"; - s += "import java.util.Set;\n"; - - s += "import com.google.gwt.core.ext.typeinfo.JClassType;\n"; - s += "import com.vaadin.client.ui.ui.UIConnector;\n"; - s += "import com.vaadin.server.widgetsetutils.ConnectorBundleLoaderFactory;\n"; - s += "import com.vaadin.shared.ui.Connect.LoadStyle;\n\n"; - - s += "public class OptimizedConnectorBundleLoaderFactory extends\n"; - s += " ConnectorBundleLoaderFactory {\n"; - s += " private Set eagerConnectors = new HashSet();\n"; - s += " {\n"; - for (String c : usedConnectors) { - s += " eagerConnectors.add(" + c - + ".class.getName());\n"; - } - s += " }\n"; - s += "\n"; - s += " @Override\n"; - s += " protected LoadStyle getLoadStyle(JClassType connectorType) {\n"; - s += " if (eagerConnectors.contains(connectorType.getQualifiedBinaryName())) {\n"; - s += " return LoadStyle.EAGER;\n"; - s += " } else {\n"; - s += " // Loads all other connectors immediately after the initial view has\n"; - s += " // been rendered\n"; - s += " return LoadStyle.DEFERRED;\n"; - s += " }\n"; - s += " }\n"; - s += "}\n"; - - return s; + widgetsetPanel.update(); + content.setWidget(widgetsetPanel); } private void analyzeLayouts() { - content.clear(); - content.add(new Label("Analyzing layouts...")); - List runningApplications = ApplicationConfiguration - .getRunningApplications(); - for (ApplicationConnection applicationConnection : runningApplications) { - applicationConnection.analyzeLayouts(); - } + analyzeLayoutsPanel.update(); + content.setWidget(analyzeLayoutsPanel); } @Override public void meta(ApplicationConnection ac, ValueMap meta) { - content.clear(); - JsArray valueMapArray = meta - .getJSValueMapArray("invalidLayouts"); - int size = valueMapArray.length(); - - if (size > 0) { - SimpleTree root = new SimpleTree("Layouts analyzed, " + size - + " top level problems"); - for (int i = 0; i < size; i++) { - printLayoutError(ac, valueMapArray.get(i), root); - } - root.open(false); - content.add(root); - } else { - content.add(new Label("Layouts analyzed, no top level problems")); - } - - Set zeroHeightComponents = new HashSet(); - Set zeroWidthComponents = new HashSet(); - findZeroSizeComponents(zeroHeightComponents, zeroWidthComponents, - ac.getUIConnector()); - if (zeroHeightComponents.size() > 0 || zeroWidthComponents.size() > 0) { - content.add(new HTML("

Client side notifications

" - + " The following relative sized components were " - + "rendered to a zero size container on the client side." - + " Note that these are not necessarily invalid " - + "states, but reported here as they might be.")); - if (zeroHeightComponents.size() > 0) { - content.add(new HTML( - "

Vertically zero size:

")); - printClientSideDetectedIssues(zeroHeightComponents, ac); - } - if (zeroWidthComponents.size() > 0) { - content.add(new HTML( - "

Horizontally zero size:

")); - printClientSideDetectedIssues(zeroWidthComponents, ac); - } - } - - } - - private void printClientSideDetectedIssues( - Set zeroSized, ApplicationConnection ac) { - - // keep track of already highlighted parents - HashSet parents = new HashSet(); - - for (final ComponentConnector connector : zeroSized) { - final ServerConnector parent = connector.getParent(); - final String parentId = parent.getConnectorId(); - - final Label errorDetails = new Label(Util.getSimpleName(connector) - + "[" + connector.getConnectorId() + "]" + " inside " - + Util.getSimpleName(parent)); - - if (parent instanceof ComponentConnector) { - final ComponentConnector parentConnector = (ComponentConnector) parent; - if (!parents.contains(parentId)) { - parents.add(parentId); - Highlight.show(parentConnector, "yellow"); - } - - errorDetails.addMouseOverHandler(new MouseOverHandler() { - @Override - public void onMouseOver(MouseOverEvent event) { - Highlight.hideAll(); - Highlight.show(parentConnector, "yellow"); - Highlight.show(connector); - errorDetails.getElement().getStyle() - .setTextDecoration(TextDecoration.UNDERLINE); - } - }); - errorDetails.addMouseOutHandler(new MouseOutHandler() { - @Override - public void onMouseOut(MouseOutEvent event) { - Highlight.hideAll(); - errorDetails.getElement().getStyle() - .setTextDecoration(TextDecoration.NONE); - } - }); - errorDetails.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - printState(connector, true); - } - }); - - } - - Highlight.show(connector); - content.add(errorDetails); - - } - } - - private void printLayoutError(ApplicationConnection ac, ValueMap valueMap, - SimpleTree root) { - final String pid = valueMap.getString("id"); - - // find connector - final ComponentConnector connector = (ComponentConnector) ConnectorMap - .get(ac).getConnector(pid); - - if (connector == null) { - root.add(new SimpleTree("[" + pid + "] NOT FOUND")); - return; - } - - Highlight.show(connector); - - final SimpleTree errorNode = new SimpleTree( - Util.getSimpleName(connector) + " id: " + pid); - errorNode.addDomHandler(new MouseOverHandler() { - @Override - public void onMouseOver(MouseOverEvent event) { - Highlight.showOnly(connector); - ((Widget) event.getSource()).getElement().getStyle() - .setTextDecoration(TextDecoration.UNDERLINE); - } - }, MouseOverEvent.getType()); - errorNode.addDomHandler(new MouseOutHandler() { - @Override - public void onMouseOut(MouseOutEvent event) { - Highlight.hideAll(); - ((Widget) event.getSource()).getElement().getStyle() - .setTextDecoration(TextDecoration.NONE); - } - }, MouseOutEvent.getType()); - - errorNode.addDomHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - if (event.getNativeEvent().getEventTarget().cast() == errorNode - .getElement().getChild(1).cast()) { - printState(connector, true); - } - } - }, ClickEvent.getType()); - - VerticalPanel errorDetails = new VerticalPanel(); - - if (valueMap.containsKey("heightMsg")) { - errorDetails.add(new Label("Height problem: " - + valueMap.getString("heightMsg"))); - } - if (valueMap.containsKey("widthMsg")) { - errorDetails.add(new Label("Width problem: " - + valueMap.getString("widthMsg"))); - } - if (errorDetails.getWidgetCount() > 0) { - errorNode.add(errorDetails); - } - if (valueMap.containsKey("subErrors")) { - HTML l = new HTML( - "Expand this node to show problems that may be dependent on this problem."); - errorDetails.add(l); - JsArray suberrors = valueMap - .getJSValueMapArray("subErrors"); - for (int i = 0; i < suberrors.length(); i++) { - ValueMap value = suberrors.get(i); - printLayoutError(ac, value, errorNode); - } - - } - root.add(errorNode); - } - - private void findZeroSizeComponents( - Set zeroHeightComponents, - Set zeroWidthComponents, - ComponentConnector connector) { - Widget widget = connector.getWidget(); - ComputedStyle computedStyle = new ComputedStyle(widget.getElement()); - if (computedStyle.getIntProperty("height") == 0) { - zeroHeightComponents.add(connector); - } - if (computedStyle.getIntProperty("width") == 0) { - zeroWidthComponents.add(connector); - } - List children = connector.getChildren(); - for (ServerConnector serverConnector : children) { - if (serverConnector instanceof ComponentConnector) { - findZeroSizeComponents(zeroHeightComponents, - zeroWidthComponents, - (ComponentConnector) serverConnector); - } - } + // show the results of analyzeLayouts + analyzeLayoutsPanel.meta(ac, meta); } @Override @@ -561,60 +220,11 @@ public class HierarchySection implements Section { private void printState(ServerConnector connector, boolean serverDebug) { Highlight.showOnly(connector); if (serverDebug) { - Highlight.showServerDebugInfo(connector); + HierarchyPanel.showServerDebugInfo(connector); } - SharedState state = connector.getState(); - - Set ignoreProperties = new HashSet(); - ignoreProperties.add("id"); - - String html = getRowHTML("Id", connector.getConnectorId()); - html += getRowHTML("Connector", Util.getSimpleName(connector)); - - if (connector instanceof ComponentConnector) { - ComponentConnector component = (ComponentConnector) connector; - - ignoreProperties.addAll(Arrays.asList("caption", "description", - "width", "height")); - - AbstractComponentState componentState = component.getState(); - - html += getRowHTML("Widget", - Util.getSimpleName(component.getWidget())); - html += getRowHTML("Caption", componentState.caption); - html += getRowHTML("Description", componentState.description); - html += getRowHTML("Width", componentState.width + " (actual: " - + component.getWidget().getOffsetWidth() + "px)"); - html += getRowHTML("Height", componentState.height + " (actual: " - + component.getWidget().getOffsetHeight() + "px)"); - } - - try { - JsArrayObject properties = AbstractConnector - .getStateType(connector).getPropertiesAsArray(); - for (int i = 0; i < properties.size(); i++) { - Property property = properties.get(i); - String name = property.getName(); - if (!ignoreProperties.contains(name)) { - html += getRowHTML(property.getDisplayName(), - property.getValue(state)); - } - } - } catch (NoDataException e) { - html += "
Could not read state, error has been logged to the console
"; - VConsole.error(e); - } - - content.clear(); - content.add(new HTML(html)); - } - - private String getRowHTML(String caption, Object value) { - return "
" + caption - + "" - + Util.escapeHTML(String.valueOf(value)) + "
"; + infoPanel.update(connector); + content.setWidget(infoPanel); } private final NativePreviewHandler highlightModeHandler = new NativePreviewHandler() { @@ -634,7 +244,7 @@ public class HierarchySection implements Section { .getNativeEvent().getClientX(), event.getNativeEvent() .getClientY()); if (VDebugWindow.get().getElement().isOrHasChild(eventTarget)) { - content.clear(); + infoPanel.clear(); return; } @@ -654,7 +264,7 @@ public class HierarchySection implements Section { return; } } - content.clear(); + infoPanel.clear(); } if (event.getTypeInt() == Event.ONCLICK) { Highlight.hideAll(); diff --git a/client/src/com/vaadin/client/debug/internal/Highlight.java b/client/src/com/vaadin/client/debug/internal/Highlight.java index 3c1af445a9..f2695f58ca 100644 --- a/client/src/com/vaadin/client/debug/internal/Highlight.java +++ b/client/src/com/vaadin/client/debug/internal/Highlight.java @@ -207,19 +207,4 @@ public class Highlight { } } - /** - * Outputs debug information on the server - usually in the console of an - * IDE, with a clickable reference to the relevant code location. - * - * @since 7.1 - * @param connector - * show debug info for this connector - */ - static void showServerDebugInfo(ServerConnector connector) { - if (connector != null) { - connector.getConnection().getUIConnector() - .showServerDebugInfo(connector); - } - } - } diff --git a/client/src/com/vaadin/client/debug/internal/OptimizedWidgetsetPanel.java b/client/src/com/vaadin/client/debug/internal/OptimizedWidgetsetPanel.java new file mode 100644 index 0000000000..a8d8aad888 --- /dev/null +++ b/client/src/com/vaadin/client/debug/internal/OptimizedWidgetsetPanel.java @@ -0,0 +1,137 @@ +/* + * 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.debug.internal; + +import java.util.HashSet; +import java.util.Set; + +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HTML; +import com.vaadin.client.ApplicationConfiguration; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ServerConnector; +import com.vaadin.client.VConsole; +import com.vaadin.client.ui.UnknownComponentConnector; + +/** + * Optimized widgetset view panel of the debug window. + * + * @since 7.1.4 + */ +public class OptimizedWidgetsetPanel extends FlowPanel { + + /** + * Update the panel contents based on the connectors that have been used so + * far on this execution of the application. + */ + public void update() { + clear(); + HTML h = new HTML("Getting used connectors"); + add(h); + + String s = ""; + for (ApplicationConnection ac : ApplicationConfiguration + .getRunningApplications()) { + ApplicationConfiguration conf = ac.getConfiguration(); + s += "

Used connectors for " + conf.getServiceUrl() + "

"; + + for (String connectorName : getUsedConnectorNames(conf)) { + s += connectorName + "
"; + } + + s += "

To make an optimized widgetset based on these connectors, do:

"; + s += "

1. Add to your widgetset.gwt.xml file:

"; + s += ""; + + s += "

2. Add the following java file to your project:

"; + s += ""; + s += "

3. Recompile widgetset

"; + + } + + h.setHTML(s); + } + + private Set getUsedConnectorNames( + ApplicationConfiguration configuration) { + int tag = 0; + Set usedConnectors = new HashSet(); + while (true) { + String serverSideClass = configuration + .getServerSideClassNameForTag(tag); + if (serverSideClass == null) { + break; + } + Class connectorClass = configuration + .getConnectorClassByEncodedTag(tag); + if (connectorClass == null) { + break; + } + + if (connectorClass != UnknownComponentConnector.class) { + usedConnectors.add(connectorClass.getName()); + } + tag++; + if (tag > 10000) { + // Sanity check + VConsole.error("Search for used connector classes was forcefully terminated"); + break; + } + } + return usedConnectors; + } + + public String generateOptimizedWidgetSet(Set usedConnectors) { + String s = "import java.util.HashSet;\n"; + s += "import java.util.Set;\n"; + + s += "import com.google.gwt.core.ext.typeinfo.JClassType;\n"; + s += "import com.vaadin.client.ui.ui.UIConnector;\n"; + s += "import com.vaadin.server.widgetsetutils.ConnectorBundleLoaderFactory;\n"; + s += "import com.vaadin.shared.ui.Connect.LoadStyle;\n\n"; + + s += "public class OptimizedConnectorBundleLoaderFactory extends\n"; + s += " ConnectorBundleLoaderFactory {\n"; + s += " private Set eagerConnectors = new HashSet();\n"; + s += " {\n"; + for (String c : usedConnectors) { + s += " eagerConnectors.add(" + c + + ".class.getName());\n"; + } + s += " }\n"; + s += "\n"; + s += " @Override\n"; + s += " protected LoadStyle getLoadStyle(JClassType connectorType) {\n"; + s += " if (eagerConnectors.contains(connectorType.getQualifiedBinaryName())) {\n"; + s += " return LoadStyle.EAGER;\n"; + s += " } else {\n"; + s += " // Loads all other connectors immediately after the initial view has\n"; + s += " // been rendered\n"; + s += " return LoadStyle.DEFERRED;\n"; + s += " }\n"; + s += " }\n"; + s += "}\n"; + + return s; + } + +} diff --git a/client/src/com/vaadin/client/debug/internal/SelectConnectorListener.java b/client/src/com/vaadin/client/debug/internal/SelectConnectorListener.java new file mode 100644 index 0000000000..409f9d14ce --- /dev/null +++ b/client/src/com/vaadin/client/debug/internal/SelectConnectorListener.java @@ -0,0 +1,37 @@ +/* + * 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.debug.internal; + +import com.google.gwt.user.client.Element; +import com.vaadin.client.ServerConnector; + +/** + * Listener for the selection of a connector in the debug window. + * + * @since 7.1.4 + */ +public interface SelectConnectorListener { + /** + * Listener method called when a connector has been selected. If a specific + * element of the connector was selected, it is also given. + * + * @param connector + * selected connector + * @param element + * selected element of the connector or null if unknown + */ + public void select(ServerConnector connector, Element element); +} \ No newline at end of file -- cgit v1.2.3 From 8d4518822770c79893aa06cfa22d275931f5b422 Mon Sep 17 00:00:00 2001 From: Henri Sara Date: Thu, 29 Aug 2013 14:26:04 +0300 Subject: Preserve open nodes in debug window hierarchy over refresh (#12472) Change-Id: Ib64f40db6e5568e236db410b0ad0ec2960be37f7 --- client/src/com/vaadin/client/SimpleTree.java | 8 ++++ .../client/debug/internal/HierarchyPanel.java | 50 +++++++++++++++++++--- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/client/src/com/vaadin/client/SimpleTree.java b/client/src/com/vaadin/client/SimpleTree.java index 7370496cb8..edfa23fb13 100644 --- a/client/src/com/vaadin/client/SimpleTree.java +++ b/client/src/com/vaadin/client/SimpleTree.java @@ -116,6 +116,14 @@ public class SimpleTree extends ComplexPanel implements HasDoubleClickHandlers { } } + public boolean isOpen() { + return "-".equals(handle.getInnerHTML()); + } + + public String getCaption() { + return text.getInnerText(); + } + public SimpleTree(String caption) { this(); setText(caption); diff --git a/client/src/com/vaadin/client/debug/internal/HierarchyPanel.java b/client/src/com/vaadin/client/debug/internal/HierarchyPanel.java index 759dbf00dd..755f076b7a 100644 --- a/client/src/com/vaadin/client/debug/internal/HierarchyPanel.java +++ b/client/src/com/vaadin/client/debug/internal/HierarchyPanel.java @@ -16,6 +16,7 @@ package com.vaadin.client.debug.internal; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import com.google.gwt.event.dom.client.ClickEvent; @@ -24,11 +25,13 @@ import com.google.gwt.event.dom.client.DoubleClickEvent; import com.google.gwt.event.dom.client.DoubleClickHandler; import com.google.gwt.event.dom.client.HasDoubleClickHandlers; import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HasWidgets; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.ApplicationConfiguration; import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.FastStringSet; import com.vaadin.client.ServerConnector; import com.vaadin.client.SimpleTree; import com.vaadin.client.Util; @@ -45,15 +48,21 @@ public class HierarchyPanel extends FlowPanel { private List listeners = new ArrayList(); public void update() { + // Try to keep track of currently open nodes and reopen them + FastStringSet openNodes = FastStringSet.create(); + Iterator it = iterator(); + while (it.hasNext()) { + collectOpenNodes(it.next(), openNodes); + } + clear(); - // TODO Clearing and rebuilding the contents is not optimal for UX as - // any previous expansions are lost. + SimplePanel trees = new SimplePanel(); for (ApplicationConnection application : ApplicationConfiguration .getRunningApplications()) { ServerConnector uiConnector = application.getUIConnector(); - Widget connectorTree = buildConnectorTree(uiConnector); + Widget connectorTree = buildConnectorTree(uiConnector, openNodes); trees.add(connectorTree); } @@ -61,7 +70,35 @@ public class HierarchyPanel extends FlowPanel { add(trees); } - private Widget buildConnectorTree(final ServerConnector connector) { + /** + * Adds the captions of all open (non-leaf) nodes in the hierarchy tree + * recursively. + * + * @param widget + * the widget in which to search for open nodes (if SimpleTree) + * @param openNodes + * the set in which open nodes should be added + */ + private void collectOpenNodes(Widget widget, FastStringSet openNodes) { + if (widget instanceof SimpleTree) { + SimpleTree tree = (SimpleTree) widget; + if (tree.isOpen()) { + openNodes.add(tree.getCaption()); + } else { + // no need to look inside closed nodes + return; + } + } + if (widget instanceof HasWidgets) { + Iterator it = ((HasWidgets) widget).iterator(); + while (it.hasNext()) { + collectOpenNodes(it.next(), openNodes); + } + } + } + + private Widget buildConnectorTree(final ServerConnector connector, + FastStringSet openNodes) { String connectorString = Util.getConnectorString(connector); List children = connector.getChildren(); @@ -88,7 +125,10 @@ public class HierarchyPanel extends FlowPanel { } }; for (ServerConnector child : children) { - tree.add(buildConnectorTree(child)); + tree.add(buildConnectorTree(child, openNodes)); + } + if (openNodes.contains(connectorString)) { + tree.open(false); } widget = tree; } -- cgit v1.2.3 From 09d7d2799cd4f17084b611155cdbf0650f7a0acc Mon Sep 17 00:00:00 2001 From: Henri Sara Date: Thu, 29 Aug 2013 14:27:00 +0300 Subject: Add TestBenchSection for the debug window (#12445) Change-Id: I552885348368f497b8b572614301127fe0460d26 --- .../vaadin/client/ApplicationConfiguration.java | 2 + .../src/com/vaadin/client/debug/internal/Icon.java | 2 + .../client/debug/internal/TestBenchSection.java | 307 +++++++++++++++++++++ 3 files changed, 311 insertions(+) create mode 100644 client/src/com/vaadin/client/debug/internal/TestBenchSection.java diff --git a/client/src/com/vaadin/client/ApplicationConfiguration.java b/client/src/com/vaadin/client/ApplicationConfiguration.java index da8f521799..f27f9d65d2 100644 --- a/client/src/com/vaadin/client/ApplicationConfiguration.java +++ b/client/src/com/vaadin/client/ApplicationConfiguration.java @@ -41,6 +41,7 @@ import com.vaadin.client.debug.internal.LogSection; import com.vaadin.client.debug.internal.NetworkSection; import com.vaadin.client.debug.internal.ProfilerSection; import com.vaadin.client.debug.internal.Section; +import com.vaadin.client.debug.internal.TestBenchSection; import com.vaadin.client.debug.internal.VDebugWindow; import com.vaadin.client.metadata.BundleLoadCallback; import com.vaadin.client.metadata.ConnectorBundleLoader; @@ -578,6 +579,7 @@ public class ApplicationConfiguration implements EntryPoint { window.addSection((Section) GWT.create(InfoSection.class)); window.addSection((Section) GWT.create(HierarchySection.class)); window.addSection((Section) GWT.create(NetworkSection.class)); + window.addSection((Section) GWT.create(TestBenchSection.class)); if (Profiler.isEnabled()) { window.addSection((Section) GWT.create(ProfilerSection.class)); } diff --git a/client/src/com/vaadin/client/debug/internal/Icon.java b/client/src/com/vaadin/client/debug/internal/Icon.java index cc2ef3b348..9ef6d833e2 100644 --- a/client/src/com/vaadin/client/debug/internal/Icon.java +++ b/client/src/com/vaadin/client/debug/internal/Icon.java @@ -32,6 +32,8 @@ public enum Icon { LOG(""), // OPTIMIZE(""), // HIERARCHY(""), // + // TODO create more appropriate icon + SELECTOR("≣"), // MENU(""), // NETWORK(""), // ANALYZE(""), // diff --git a/client/src/com/vaadin/client/debug/internal/TestBenchSection.java b/client/src/com/vaadin/client/debug/internal/TestBenchSection.java new file mode 100644 index 0000000000..613ae3abac --- /dev/null +++ b/client/src/com/vaadin/client/debug/internal/TestBenchSection.java @@ -0,0 +1,307 @@ +/* + * 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.debug.internal; + +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.MouseOutEvent; +import com.google.gwt.event.dom.client.MouseOutHandler; +import com.google.gwt.event.dom.client.MouseOverEvent; +import com.google.gwt.event.dom.client.MouseOverHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Event.NativePreviewEvent; +import com.google.gwt.user.client.Event.NativePreviewHandler; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.HTML; +import com.google.gwt.user.client.ui.RootPanel; +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; + +/** + * Provides functionality for picking selectors for Vaadin TestBench. + * + * @since 7.1.4 + * @author Vaadin Ltd + */ +public class TestBenchSection implements Section { + + /** + * Selector widget showing a selector in a program-usable form. + */ + private static class SelectorWidget extends HTML { + private static int selectorIndex = 1; + final private String path; + + public SelectorWidget(final String path) { + this.path = path; + String html = "
" + + Util.escapeHTML("WebElement element" + (selectorIndex++) + + " = getDriver().findElement(By.vaadin(\"" + path + + "\"));") + "
"; + setHTML(html); + + addMouseOverHandler(new MouseOverHandler() { + @Override + public void onMouseOver(MouseOverEvent event) { + for (ApplicationConnection a : ApplicationConfiguration + .getRunningApplications()) { + Element element = new ComponentLocator(a) + .getElementByPath(SelectorWidget.this.path); + ComponentConnector connector = Util + .getConnectorForElement(a, a.getUIConnector() + .getWidget(), element); + if (connector == null) { + connector = Util.getConnectorForElement(a, + RootPanel.get(), element); + } + if (connector != null) { + Highlight.showOnly(connector); + break; + } + } + } + }); + addMouseOutHandler(new MouseOutHandler() { + @Override + public void onMouseOut(MouseOutEvent event) { + Highlight.hideAll(); + } + }); + } + } + + private final DebugButton tabButton = new DebugButton(Icon.SELECTOR, + "Pick Vaadin TestBench selectors"); + + private final FlowPanel content = new FlowPanel(); + + private final HierarchyPanel hierarchyPanel = new HierarchyPanel(); + private final FlowPanel selectorPanel = new FlowPanel(); + + private final FlowPanel controls = new FlowPanel(); + + private final Button find = new DebugButton(Icon.HIGHLIGHT, + "Select a component on the page to inspect it"); + private final Button refreshHierarchy = new DebugButton(Icon.HIERARCHY, + "Refresh the connector hierarchy tree"); + + private HandlerRegistration highlightModeRegistration = null; + + public TestBenchSection() { + controls.add(refreshHierarchy); + refreshHierarchy.setStylePrimaryName(VDebugWindow.STYLENAME_BUTTON); + refreshHierarchy.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + hierarchyPanel.update(); + } + }); + + controls.add(find); + find.setStylePrimaryName(VDebugWindow.STYLENAME_BUTTON); + find.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + toggleFind(); + } + }); + + hierarchyPanel.addListener(new SelectConnectorListener() { + @Override + public void select(ServerConnector connector, Element element) { + pickSelector(connector, element); + } + }); + + content.setStylePrimaryName(VDebugWindow.STYLENAME + "-testbench"); + content.add(hierarchyPanel); + content.add(selectorPanel); + } + + @Override + public DebugButton getTabButton() { + return tabButton; + } + + @Override + public Widget getControls() { + return controls; + } + + @Override + public Widget getContent() { + return content; + } + + @Override + public void show() { + + } + + @Override + public void hide() { + stopFind(); + } + + @Override + public void meta(ApplicationConnection ac, ValueMap meta) { + // NOP + } + + @Override + public void uidl(ApplicationConnection ac, ValueMap uidl) { + // NOP + } + + private boolean isFindMode() { + return (highlightModeRegistration != null); + } + + private void toggleFind() { + if (isFindMode()) { + stopFind(); + } else { + startFind(); + } + } + + private void startFind() { + Highlight.hideAll(); + if (!isFindMode()) { + highlightModeRegistration = Event + .addNativePreviewHandler(highlightModeHandler); + find.addStyleDependentName(VDebugWindow.STYLENAME_ACTIVE); + } + } + + private void stopFind() { + if (isFindMode()) { + highlightModeRegistration.removeHandler(); + highlightModeRegistration = null; + find.removeStyleDependentName(VDebugWindow.STYLENAME_ACTIVE); + } + } + + private void pickSelector(ServerConnector connector, Element element) { + String path = findTestBenchSelector(connector, element); + + if (null != path && !path.isEmpty()) { + selectorPanel.add(new SelectorWidget(path)); + } + } + + private String findTestBenchSelector(ServerConnector connector, + Element element) { + String path = null; + ApplicationConnection connection = connector.getConnection(); + if (connection != null) { + if (null == element) { + // try to find the root element of the connector + if (connector instanceof ComponentConnector) { + Widget widget = ((ComponentConnector) connector) + .getWidget(); + if (widget != null) { + element = widget.getElement(); + } + } + } + if (null != element) { + path = new ComponentLocator(connection) + .getPathForElement(element); + } + } + return path; + } + + private final NativePreviewHandler highlightModeHandler = new NativePreviewHandler() { + + @Override + public void onPreviewNativeEvent(NativePreviewEvent event) { + + if (event.getTypeInt() == Event.ONKEYDOWN + && event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ESCAPE) { + stopFind(); + Highlight.hideAll(); + return; + } + if (event.getTypeInt() == Event.ONMOUSEMOVE + || event.getTypeInt() == Event.ONCLICK) { + Element eventTarget = Util.getElementFromPoint(event + .getNativeEvent().getClientX(), event.getNativeEvent() + .getClientY()); + if (VDebugWindow.get().getElement().isOrHasChild(eventTarget)) { + return; + } + + // make sure that not finding the highlight element only + Highlight.hideAll(); + eventTarget = Util.getElementFromPoint(event.getNativeEvent() + .getClientX(), event.getNativeEvent().getClientY()); + ComponentConnector connector = findConnector(eventTarget); + + if (event.getTypeInt() == Event.ONMOUSEMOVE) { + if (connector != null) { + Highlight.showOnly(connector); + event.cancel(); + event.consume(); + event.getNativeEvent().stopPropagation(); + return; + } + } else if (event.getTypeInt() == Event.ONCLICK) { + event.cancel(); + event.consume(); + event.getNativeEvent().stopPropagation(); + if (connector != null) { + Highlight.showOnly(connector); + pickSelector(connector, eventTarget); + return; + } + } + } + event.cancel(); + } + + }; + + private ComponentConnector findConnector(Element element) { + for (ApplicationConnection a : ApplicationConfiguration + .getRunningApplications()) { + ComponentConnector connector = Util.getConnectorForElement(a, a + .getUIConnector().getWidget(), element); + if (connector == null) { + connector = Util.getConnectorForElement(a, RootPanel.get(), + element); + } + if (connector != null) { + return connector; + } + } + return null; + } + +} -- cgit v1.2.3 From 486d9d3d57822edb1add2872fa9037912e692221 Mon Sep 17 00:00:00 2001 From: Marko Gronroos Date: Wed, 4 Sep 2013 17:57:31 +0300 Subject: Added a target to publish to local Maven repository. Change-Id: Ifa1f6b12c1ce9f6d65f707673d3a20e31857064f --- publish.xml | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/publish.xml b/publish.xml index 008451a2f8..c2b8369b15 100644 --- a/publish.xml +++ b/publish.xml @@ -13,8 +13,6 @@ - - @@ -25,6 +23,9 @@ + + + Installing ${src} to ${target} @@ -39,7 +40,7 @@ - + @@ -70,4 +71,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file -- cgit v1.2.3 From 6ab9e2d060d865f9ecd918209b0620a95a63f6a6 Mon Sep 17 00:00:00 2001 From: Jonatan Kronqvist Date: Tue, 3 Sep 2013 09:20:49 +0300 Subject: Allow different locator strategies #12485 Refactored the ComponentLocator class to allow for implementing different locator strategies. Change-Id: I93f3decbce4d4361cc605bcf0ce4379a187c482c --- .../com/vaadin/client/ApplicationConnection.java | 57 +- client/src/com/vaadin/client/ComponentLocator.java | 700 +-------------------- .../client/componentlocator/ComponentLocator.java | 131 ++++ .../componentlocator/LegacyLocatorStrategy.java | 637 +++++++++++++++++++ .../client/componentlocator/LocatorStrategy.java | 34 + .../client/debug/internal/TestBenchSection.java | 2 +- client/src/com/vaadin/client/ui/SubPartAware.java | 4 +- .../componentlocator/TestDetachedNotPresent.html | 62 ++ .../componentlocator/TestDetachedNotPresent.html | 62 -- 9 files changed, 903 insertions(+), 786 deletions(-) create mode 100644 client/src/com/vaadin/client/componentlocator/ComponentLocator.java create mode 100644 client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java create mode 100644 client/src/com/vaadin/client/componentlocator/LocatorStrategy.java create mode 100644 uitest/src/com/vaadin/tests/componentlocator/TestDetachedNotPresent.html delete mode 100644 uitest/src/com/vaadin/tests/gwtadapter/componentlocator/TestDetachedNotPresent.html diff --git a/client/src/com/vaadin/client/ApplicationConnection.java b/client/src/com/vaadin/client/ApplicationConnection.java index 0d9c859ee8..3200b3ab38 100644 --- a/client/src/com/vaadin/client/ApplicationConnection.java +++ b/client/src/com/vaadin/client/ApplicationConnection.java @@ -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() diff --git a/client/src/com/vaadin/client/ComponentLocator.java b/client/src/com/vaadin/client/ComponentLocator.java index af934470c2..ef7ccc3b65 100644 --- a/client/src/com/vaadin/client/ComponentLocator.java +++ b/client/src/com/vaadin/client/ComponentLocator.java @@ -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. - *

- * 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. - *

- * - * @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 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 iterator; - - /* - * VWindow and VContextMenu workarounds for backwards - * compatibility - */ - if (widgetClassName.equals("VWindow")) { - List windows = client.getUIConnector() - .getSubWindows(); - List windowWidgets = new ArrayList( - windows.size()); - for (WindowConnector wc : windows) { - windowWidgets.add(wc.getWidget()); - } - iterator = windowWidgets.iterator(); - } else if (widgetClassName.equals("VContextMenu")) { - return client.getContextMenu(); - } else { - iterator = (Iterator) 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; - } - } diff --git a/client/src/com/vaadin/client/componentlocator/ComponentLocator.java b/client/src/com/vaadin/client/componentlocator/ComponentLocator.java new file mode 100644 index 0000000000..a7afeaad9c --- /dev/null +++ b/client/src/com/vaadin/client/componentlocator/ComponentLocator.java @@ -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. + *

+ * 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. + *

+ * 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. + *

+ * + * @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; + } +} diff --git a/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java b/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java new file mode 100644 index 0000000000..dd67ddbc43 --- /dev/null +++ b/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java @@ -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)}. + *

+ * 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 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 iterator; + + /* + * VWindow and VContextMenu workarounds for backwards + * compatibility + */ + if (widgetClassName.equals("VWindow")) { + List windows = componentLocator + .getClient().getUIConnector().getSubWindows(); + List windowWidgets = new ArrayList( + 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) 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; + } +} diff --git a/client/src/com/vaadin/client/componentlocator/LocatorStrategy.java b/client/src/com/vaadin/client/componentlocator/LocatorStrategy.java new file mode 100644 index 0000000000..53cff10d4f --- /dev/null +++ b/client/src/com/vaadin/client/componentlocator/LocatorStrategy.java @@ -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); +} diff --git a/client/src/com/vaadin/client/debug/internal/TestBenchSection.java b/client/src/com/vaadin/client/debug/internal/TestBenchSection.java index 613ae3abac..a283b18912 100644 --- a/client/src/com/vaadin/client/debug/internal/TestBenchSection.java +++ b/client/src/com/vaadin/client/debug/internal/TestBenchSection.java @@ -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. diff --git a/client/src/com/vaadin/client/ui/SubPartAware.java b/client/src/com/vaadin/client/ui/SubPartAware.java index a7d72fab01..36959e7b1f 100644 --- a/client/src/com/vaadin/client/ui/SubPartAware.java +++ b/client/src/com/vaadin/client/ui/SubPartAware.java @@ -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); -} \ No newline at end of file +} diff --git a/uitest/src/com/vaadin/tests/componentlocator/TestDetachedNotPresent.html b/uitest/src/com/vaadin/tests/componentlocator/TestDetachedNotPresent.html new file mode 100644 index 0000000000..24e5e992ca --- /dev/null +++ b/uitest/src/com/vaadin/tests/componentlocator/TestDetachedNotPresent.html @@ -0,0 +1,62 @@ + + + + + + + +New Test + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
New Test
open/run/com.vaadin.tests.components.table.Tables?restartApplication
mouseClickvaadin=runcomvaadintestscomponentstableTables::PID_Smenu#item040,10
mouseClickvaadin=runcomvaadintestscomponentstableTables::Root/VOverlay[0]/VMenuBar[0]#item829,8
mouseClickvaadin=runcomvaadintestscomponentstableTables::Root/VOverlay[1]/VMenuBar[0]#item2103,3
mouseClickvaadin=runcomvaadintestscomponentstableTables::Root/VOverlay[2]/VMenuBar[0]#item134,6
contextmenuvaadin=runcomvaadintestscomponentstableTables::PID_StestComponent/domChild[1]/domChild[0]/domChild[1]/domChild[0]/domChild[0]/domChild[0]/domChild[0]
assertElementPresentvaadin=runcomvaadintestscomponentstableTables::Root/VContextMenu[0]#option015,8
mouseClickvaadin=runcomvaadintestscomponentstableTables::PID_StestComponent/domChild[1]/domChild[0]/domChild[1]/domChild[0]/domChild[0]/domChild[2]/domChild[0]37,9
assertElementNotPresentvaadin=runcomvaadintestscomponentstableTables::Root/VContextMenu[0]#option015,8
+ + \ No newline at end of file diff --git a/uitest/src/com/vaadin/tests/gwtadapter/componentlocator/TestDetachedNotPresent.html b/uitest/src/com/vaadin/tests/gwtadapter/componentlocator/TestDetachedNotPresent.html deleted file mode 100644 index 24e5e992ca..0000000000 --- a/uitest/src/com/vaadin/tests/gwtadapter/componentlocator/TestDetachedNotPresent.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - -New Test - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
New Test
open/run/com.vaadin.tests.components.table.Tables?restartApplication
mouseClickvaadin=runcomvaadintestscomponentstableTables::PID_Smenu#item040,10
mouseClickvaadin=runcomvaadintestscomponentstableTables::Root/VOverlay[0]/VMenuBar[0]#item829,8
mouseClickvaadin=runcomvaadintestscomponentstableTables::Root/VOverlay[1]/VMenuBar[0]#item2103,3
mouseClickvaadin=runcomvaadintestscomponentstableTables::Root/VOverlay[2]/VMenuBar[0]#item134,6
contextmenuvaadin=runcomvaadintestscomponentstableTables::PID_StestComponent/domChild[1]/domChild[0]/domChild[1]/domChild[0]/domChild[0]/domChild[0]/domChild[0]
assertElementPresentvaadin=runcomvaadintestscomponentstableTables::Root/VContextMenu[0]#option015,8
mouseClickvaadin=runcomvaadintestscomponentstableTables::PID_StestComponent/domChild[1]/domChild[0]/domChild[1]/domChild[0]/domChild[0]/domChild[2]/domChild[0]37,9
assertElementNotPresentvaadin=runcomvaadintestscomponentstableTables::Root/VContextMenu[0]#option015,8
- - \ No newline at end of file -- cgit v1.2.3 From 72db2044ea2844c5c7a49a704a507f32af5755ed Mon Sep 17 00:00:00 2001 From: Jonatan Kronqvist Date: Wed, 4 Sep 2013 15:27:38 +0300 Subject: Implemented the extensions to ComponentLocator needed for TB4 #12485 Change-Id: I8c7db91967003290bbff4e703235aa36d5e9e1f3 --- .../com/vaadin/client/ApplicationConnection.java | 3 + .../client/componentlocator/ComponentLocator.java | 45 ++- .../componentlocator/LegacyLocatorStrategy.java | 13 + .../client/componentlocator/LocatorStrategy.java | 56 ++++ .../VaadinFinderLocatorStrategy.java | 357 +++++++++++++++++++++ 5 files changed, 472 insertions(+), 2 deletions(-) create mode 100644 client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java 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. *

- * 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 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. + *

+ * 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. + *

+ * + * @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: + *
    + *
  • Find the third text field in the DOM: {@code //VTextField[2]}
  • + *
  • Find the second button inside the first vertical layout: + * {@code //VVerticalLayout/VButton[1]}
  • + *
  • Find the first column on the third row of the "Accounts" table: + * {@code //VScrollTable[caption="Accounts"]#row[2]/col[0]}
  • + *
+ * + * @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 notifications = new ArrayList(); + 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 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: + * VTextField[0] => 0 + * VTextField[caption='foo'] => caption='foo' + * + * + * @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 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 collectPotentialMatches( + ComponentConnector parent, String pathFragment, + boolean collectRecursively) { + ArrayList potentialMatches = new ArrayList(); + if (parent instanceof AbstractHasComponentsConnector) { + List 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); + } +} -- cgit v1.2.3 From dc14ea4ca1cec8a4fc1ac23c07151e11cd2ac72b Mon Sep 17 00:00:00 2001 From: Jonatan Kronqvist Date: Mon, 9 Sep 2013 15:52:23 +0300 Subject: Implemented SubPartAware in VScrollTable #12514 This allows for "//VScrollTable#row[n]/col[m]" -style locator strings. Change-Id: I65bea31ab0eaa59fcd5a566ea3d1a43465ef298f --- .../VaadinFinderLocatorStrategy.java | 55 ++++++++--- client/src/com/vaadin/client/ui/VScrollTable.java | 108 ++++++++++++++++++++- 2 files changed, 151 insertions(+), 12 deletions(-) diff --git a/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java b/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java index f597003b60..3516aa3e93 100644 --- a/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java +++ b/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java @@ -28,6 +28,7 @@ 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.SubPartAware; import com.vaadin.client.ui.VNotification; import com.vaadin.shared.AbstractComponentState; @@ -50,6 +51,7 @@ import com.vaadin.shared.AbstractComponentState; */ public class VaadinFinderLocatorStrategy implements LocatorStrategy { + private static final String SUBPART_SEPARATOR = "#"; private ComponentLocator componentLocator; public VaadinFinderLocatorStrategy(ComponentLocator componentLocator) { @@ -76,8 +78,8 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy { if (path.startsWith("//VNotification")) { return findNotificationByPath(path); } - return findElementByPath(path, componentLocator.getClient() - .getUIConnector()); + return getElementByPathStartingAtConnector(path, componentLocator + .getClient().getUIConnector()); } /** @@ -110,23 +112,54 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy { */ @Override public Element getElementByPathStartingAt(String path, Element root) { - return findElementByPath(path, + return getElementByPathStartingAtConnector(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. + * Finds an element by the specified path, starting traversal of the + * connector hierarchy from the specified root. + * + * @param path + * the locator path + * @param root + * the root connector + * @return the element identified by path or null if not found. + */ + private Element getElementByPathStartingAtConnector(String path, + ComponentConnector root) { + String[] pathComponents = path.split(SUBPART_SEPARATOR); + ComponentConnector connector = findConnectorByPath(pathComponents[0], + root); + if (connector != null) { + if (pathComponents.length > 1) { + // We have subparts + if (connector.getWidget() instanceof SubPartAware) { + return ((SubPartAware) connector.getWidget()) + .getSubPartElement(pathComponents[1]); + } else { + return null; + } + } + return connector.getWidget().getElement(); + } + return null; + } + + /** + * Recursively finds a connector for the 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. + * @return The connector identified by {@code path} or null if it no such + * connector could be found. */ - private Element findElementByPath(String path, ComponentConnector parent) { + private ComponentConnector findConnectorByPath(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); @@ -138,9 +171,9 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy { extractPredicateString(fragments[0])); if (connector != null) { if (fragments.length > 1) { - return findElementByPath(fragments[1], connector); + return findConnectorByPath(fragments[1], connector); } else { - return connector.getWidget().getElement(); + return connector; } } return null; diff --git a/client/src/com/vaadin/client/ui/VScrollTable.java b/client/src/com/vaadin/client/ui/VScrollTable.java index 3733ee204a..fe2e99a28f 100644 --- a/client/src/com/vaadin/client/ui/VScrollTable.java +++ b/client/src/com/vaadin/client/ui/VScrollTable.java @@ -61,6 +61,8 @@ import com.google.gwt.event.dom.client.ScrollHandler; import com.google.gwt.event.logical.shared.CloseEvent; import com.google.gwt.event.logical.shared.CloseHandler; import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.regexp.shared.MatchResult; +import com.google.gwt.regexp.shared.RegExp; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Element; @@ -123,7 +125,7 @@ import com.vaadin.shared.ui.table.TableConstants; */ public class VScrollTable extends FlowPanel implements HasWidgets, ScrollHandler, VHasDropHandler, FocusHandler, BlurHandler, Focusable, - ActionOwner { + ActionOwner, SubPartAware { public static final String STYLENAME = "v-table"; @@ -5031,6 +5033,20 @@ public class VScrollTable extends FlowPanel implements HasWidgets, } } + public int indexOf(Widget row) { + int relIx = -1; + for (int ix = 0; ix < renderedRows.size(); ix++) { + if (renderedRows.get(ix) == row) { + relIx = ix; + break; + } + } + if (relIx >= 0) { + return this.firstRendered + relIx; + } + return -1; + } + public class VScrollTableRow extends Panel implements ActionOwner { private static final int TOUCHSCROLL_TIMEOUT = 100; @@ -7728,4 +7744,94 @@ public class VScrollTable extends FlowPanel implements HasWidgets, return this; } + private static final String SUBPART_HEADER = "header"; + private static final String SUBPART_FOOTER = "footer"; + private static final String SUBPART_ROW = "row"; + private static final String SUBPART_COL = "col"; + /** Matches header[ix] - used for extracting the index of the targeted header cell */ + private static final RegExp SUBPART_HEADER_REGEXP = RegExp + .compile(SUBPART_HEADER + "\\[(\\d+)\\]"); + /** Matches footer[ix] - used for extracting the index of the targeted footer cell */ + private static final RegExp SUBPART_FOOTER_REGEXP = RegExp + .compile(SUBPART_FOOTER + "\\[(\\d+)\\]"); + /** Matches row[ix] - used for extracting the index of the targeted row */ + private static final RegExp SUBPART_ROW_REGEXP = RegExp.compile(SUBPART_ROW + + "\\[(\\d+)]"); + /** Matches col[ix] - used for extracting the index of the targeted column */ + private static final RegExp SUBPART_ROW_COL_REGEXP = RegExp + .compile(SUBPART_ROW + "\\[(\\d+)\\]/" + SUBPART_COL + "\\[(\\d+)\\]"); + + @Override + public Element getSubPartElement(String subPart) { + if (SUBPART_ROW_COL_REGEXP.test(subPart)) { + MatchResult result = SUBPART_ROW_COL_REGEXP.exec(subPart); + int rowIx = Integer.valueOf(result.getGroup(1)); + int colIx = Integer.valueOf(result.getGroup(2)); + VScrollTableRow row = scrollBody.getRowByRowIndex(rowIx); + if (row != null) { + Element rowElement = row.getElement(); + if (colIx < rowElement.getChildCount()) { + return rowElement.getChild(colIx).getFirstChild().cast(); + } + } + + } else if (SUBPART_ROW_REGEXP.test(subPart)) { + MatchResult result = SUBPART_ROW_REGEXP.exec(subPart); + int rowIx = Integer.valueOf(result.getGroup(1)); + VScrollTableRow row = scrollBody.getRowByRowIndex(rowIx); + if (row != null) { + return row.getElement(); + } + + } else if (SUBPART_HEADER_REGEXP.test(subPart)) { + MatchResult result = SUBPART_HEADER_REGEXP.exec(subPart); + int headerIx = Integer.valueOf(result.getGroup(1)); + HeaderCell headerCell = tHead.getHeaderCell(headerIx); + if (headerCell != null) { + return headerCell.getElement(); + } + + } else if (SUBPART_FOOTER_REGEXP.test(subPart)) { + MatchResult result = SUBPART_FOOTER_REGEXP.exec(subPart); + int footerIx = Integer.valueOf(result.getGroup(1)); + FooterCell footerCell = tFoot.getFooterCell(footerIx); + if (footerCell != null) { + return footerCell.getElement(); + } + } + // Nothing found. + return null; + } + + @Override + public String getSubPartName(Element subElement) { + Widget widget = Util.findWidget(subElement, null); + if (widget instanceof HeaderCell) { + return SUBPART_HEADER + "[" + tHead.visibleCells.indexOf(widget) + + "]"; + } else if (widget instanceof FooterCell) { + return SUBPART_FOOTER + "[" + tFoot.visibleCells.indexOf(widget) + + "]"; + } else if (widget instanceof VScrollTableRow) { + // a cell in a row + VScrollTableRow row = (VScrollTableRow) widget; + int rowIx = scrollBody.indexOf(row); + if (rowIx >= 0) { + int colIx = -1; + for (int ix = 0; ix < row.getElement().getChildCount(); ix++) { + if (row.getElement().getChild(ix).isOrHasChild(subElement)) { + colIx = ix; + break; + } + } + if (colIx >= 0) { + return SUBPART_ROW + "[" + rowIx + "]/" + SUBPART_COL + "[" + + colIx + "]"; + } + return SUBPART_ROW + "[" + rowIx + "]"; + } + } + // Nothing found. + return null; + } } -- cgit v1.2.3 From a9f332fddb5ff70338527c1ddad3ca40dca70d6d Mon Sep 17 00:00:00 2001 From: Patrik Lindström Date: Mon, 23 Sep 2013 08:52:22 +0300 Subject: Add simple calculator test application (#12444) Change-Id: Ie2880c9321f46a004e53ca0fa08aa43f840e6a5d --- .../vaadin/tests/application/calculator/Calc.java | 190 +++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 uitest/src/com/vaadin/tests/application/calculator/Calc.java diff --git a/uitest/src/com/vaadin/tests/application/calculator/Calc.java b/uitest/src/com/vaadin/tests/application/calculator/Calc.java new file mode 100644 index 0000000000..7d080adfa6 --- /dev/null +++ b/uitest/src/com/vaadin/tests/application/calculator/Calc.java @@ -0,0 +1,190 @@ +/* + * 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.tests.application.calculator; + +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.components.AbstractTestUI; +import com.vaadin.ui.Alignment; +import com.vaadin.ui.Button; +import com.vaadin.ui.Button.ClickEvent; +import com.vaadin.ui.Button.ClickListener; +import com.vaadin.ui.GridLayout; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Label; +import com.vaadin.ui.TextArea; +import com.vaadin.ui.TextField; +import com.vaadin.ui.VerticalLayout; + +@SuppressWarnings("serial") +public class Calc extends AbstractTestUI { + + private class Log extends TextArea { + + public Log() { + super(); + setWidth("200px"); + } + + public void addRow(String row) { + setValue(getValue() + "\n" + row); + } + } + + // All variables are automatically stored in the session. + private Double current = 0.0; + private double stored = 0.0; + private char lastOperationRequested = 'C'; + private VerticalLayout topLayout = new VerticalLayout(); + + // User interface components + private final TextField display = new TextField("0.0"); + + private final Log log = new Log(); + + // Calculator "business logic" implemented here to keep the example + // minimal + private double calculate(char requestedOperation) { + if ('0' <= requestedOperation && requestedOperation <= '9') { + if (current == null) { + current = 0.0; + } + current = current * 10 + + Double.parseDouble("" + requestedOperation); + return current; + } + + if (current == null) { + current = stored; + } + switch (lastOperationRequested) { + case '+': + stored += current; + break; + case '-': + stored -= current; + break; + case '/': + stored /= current; + break; + case '*': + stored *= current; + break; + default: + stored = current; + break; + } + + switch (requestedOperation) { + case '+': + log.addRow(current + " +"); + break; + case '-': + log.addRow(current + " -"); + break; + case '/': + log.addRow(current + " /"); + break; + case '*': + log.addRow(current + " x"); + break; + case '=': + log.addRow(current + " ="); + log.addRow("------------"); + log.addRow("" + stored); + break; + } + + lastOperationRequested = requestedOperation; + current = null; + if (requestedOperation == 'C') { + log.addRow("0.0"); + stored = 0.0; + } + return stored; + } + + @Override + protected void setup(VaadinRequest request) { + setContent(topLayout); + + // Create the main layout for our application (4 columns, 5 rows) + final GridLayout layout = new GridLayout(4, 5); + + topLayout.setMargin(true); + topLayout.setSpacing(true); + Label title = new Label("Calculator"); + topLayout.addComponent(title); + topLayout.addComponent(log); + + HorizontalLayout horizontalLayout = new HorizontalLayout(); + horizontalLayout.setSpacing(true); + horizontalLayout.addComponent(layout); + horizontalLayout.addComponent(log); + topLayout.addComponent(horizontalLayout); + + // Create a result label that over all 4 columns in the first row + layout.addComponent(display, 0, 0, 3, 0); + layout.setComponentAlignment(display, Alignment.MIDDLE_RIGHT); + display.setSizeUndefined(); + display.setId("display"); + + // The operations for the calculator in the order they appear on the + // screen (left to right, top to bottom) + String[] operations = new String[] { "7", "8", "9", "/", "4", "5", "6", + "*", "1", "2", "3", "-", "0", "=", "C", "+" }; + + for (String caption : operations) { + + // Create a button and use this application for event handling + Button button = new Button(caption); + button.setHeight("30px"); + button.setWidth("40px"); + button.addClickListener(new ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + // Get the button that was clicked + Button button = event.getButton(); + + // Get the requested operation from the button caption + char requestedOperation = button.getCaption().charAt(0); + + // Calculate the new value + double newValue = calculate(requestedOperation); + + // Update the result label with the new value + display.setValue("" + newValue); + } + }); + button.setId("button_" + caption); + + // Add the button to our main layout + layout.addComponent(button); + } + + } + + @Override + protected String getTestDescription() { + return "Provide test application for generic testing purposes"; + } + + @Override + protected Integer getTicketNumber() { + return 12444; + } + +} -- cgit v1.2.3 From 5cc968b378ccf99ed511deaadd8c47073c300365 Mon Sep 17 00:00:00 2001 From: Henri Sara Date: Tue, 1 Oct 2013 14:35:17 +0300 Subject: Support mixed legacy and VaadinFinder locators (#12641, #12642, #12643) ComponentLocator tries to use all available locator strategies until a match is found (#12641) and VaadinFinderLocatorStrategy also accepts simple indexed paths (#12642) with legacy locators as a backup. To enable mixed use of locators, LegacyLocatorStrategy now implements getElementByPathStartingAt() (#12643). Change-Id: I6b763565adef0e294b353ef6e2dfdf70ae0d77a9 --- .../client/componentlocator/ComponentLocator.java | 16 +++++--- .../componentlocator/LegacyLocatorStrategy.java | 43 ++++++++++++---------- .../client/componentlocator/LocatorStrategy.java | 13 ------- .../VaadinFinderLocatorStrategy.java | 19 +--------- 4 files changed, 36 insertions(+), 55 deletions(-) diff --git a/client/src/com/vaadin/client/componentlocator/ComponentLocator.java b/client/src/com/vaadin/client/componentlocator/ComponentLocator.java index c1f117d992..e319650d23 100644 --- a/client/src/com/vaadin/client/componentlocator/ComponentLocator.java +++ b/client/src/com/vaadin/client/componentlocator/ComponentLocator.java @@ -131,8 +131,9 @@ public class ComponentLocator { // 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); + Element element = strategy.getElementByPath(path); + if (null != element) { + return element; } } return null; @@ -156,17 +157,22 @@ public class ComponentLocator { // 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); + Element element = strategy.getElementByPathStartingAt(path, root); + if (null != element) { + return element; } } return null; } /** + * Returns the {@link ApplicationConnection} used by this locator. + *

+ * This method is primarily for internal use by the framework. + * * @return the application connection */ - ApplicationConnection getClient() { + public ApplicationConnection getClient() { return client; } } diff --git a/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java b/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java index 34f5967092..ed70486ff5 100644 --- a/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java +++ b/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java @@ -177,17 +177,30 @@ public class LegacyLocatorStrategy implements LocatorStrategy { @Override public Element getElementByPath(String path) { + return getElementByPathStartingAt(path, null); + } + + @Override + public Element getElementByPathStartingAt(String path, Element baseElement) { /* * Path is of type "targetWidgetPath#componentPart" or * "targetWidgetPath". */ String parts[] = path.split(LegacyLocatorStrategy.SUBPART_SEPARATOR, 2); String widgetPath = parts[0]; - Widget w = getWidgetFromPath(widgetPath); + + Widget baseWidget = null; + if (null != baseElement) { + // Note that this only works if baseElement can be mapped to a + // widget to which the path is relative. Otherwise, the current + // implementation simply interprets the path as if baseElement was + // null. + baseWidget = Util.findWidget(baseElement, null); + } + Widget w = getWidgetFromPath(widgetPath, baseWidget); if (w == null || !Util.isAttachedAndDisplayed(w)) { return null; } - if (parts.length == 1) { int pos = widgetPath.indexOf("domChild"); if (pos == -1) { @@ -196,7 +209,7 @@ public class LegacyLocatorStrategy implements LocatorStrategy { // Contains dom reference to a sub element of the widget String subPath = widgetPath.substring(pos); - return getElementByDOMPath(w.getElement(), subPath); + return getElementByDOMPath(baseElement, subPath); } else if (parts.length == 2) { if (w instanceof SubPartAware) { return ((SubPartAware) w).getSubPartElement(parts[1]); @@ -205,19 +218,6 @@ 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 @@ -364,7 +364,7 @@ public class LegacyLocatorStrategy implements LocatorStrategy { /** * Creates a locator String for the given widget. The path can be used to - * locate the widget using {@link #getWidgetFromPath(String)}. + * locate the widget using {@link #getWidgetFromPath(String, Widget)}. *

* Returns null if no path can be determined for the widget or if the widget * is null. @@ -436,11 +436,14 @@ public class LegacyLocatorStrategy implements LocatorStrategy { * * @param path * The String locator that identifies the widget. + * @param baseWidget + * the widget to which the path is relative, null if relative to + * root * @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; + private Widget getWidgetFromPath(String path, Widget baseWidget) { + Widget w = baseWidget; String parts[] = path.split(PARENTCHILD_SEPARATOR); for (int i = 0; i < parts.length; i++) { @@ -517,7 +520,7 @@ public class LegacyLocatorStrategy implements LocatorStrategy { .next(); } /* - * The new grid and ordered layotus do not contain + * The new grid and ordered layouts 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 diff --git a/client/src/com/vaadin/client/componentlocator/LocatorStrategy.java b/client/src/com/vaadin/client/componentlocator/LocatorStrategy.java index 835b81bdae..56ed396609 100644 --- a/client/src/com/vaadin/client/componentlocator/LocatorStrategy.java +++ b/client/src/com/vaadin/client/componentlocator/LocatorStrategy.java @@ -74,17 +74,4 @@ public interface LocatorStrategy { * 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 index 3516aa3e93..a95b2013ed 100644 --- a/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java +++ b/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java @@ -18,7 +18,6 @@ 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; @@ -51,7 +50,8 @@ import com.vaadin.shared.AbstractComponentState; */ public class VaadinFinderLocatorStrategy implements LocatorStrategy { - private static final String SUBPART_SEPARATOR = "#"; + public static final String SUBPART_SEPARATOR = "#"; + private ComponentLocator componentLocator; public VaadinFinderLocatorStrategy(ComponentLocator componentLocator) { @@ -372,19 +372,4 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy { 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); - } } -- cgit v1.2.3 From c2b81b20ca3a981b775ac5f16506861724119df4 Mon Sep 17 00:00:00 2001 From: Patrik Lindström Date: Wed, 9 Oct 2013 16:19:00 +0300 Subject: Accept "=" in caption/id strings (#12737) Change-Id: I220fe86bcbfbf1c7188d7da78bb0ee446a5df7c2 --- .../VaadinFinderLocatorStrategy.java | 26 ++++++++++++++-------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java b/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java index a95b2013ed..264574788c 100644 --- a/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java +++ b/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java @@ -51,7 +51,6 @@ import com.vaadin.shared.AbstractComponentState; public class VaadinFinderLocatorStrategy implements LocatorStrategy { public static final String SUBPART_SEPARATOR = "#"; - private ComponentLocator componentLocator; public VaadinFinderLocatorStrategy(ComponentLocator componentLocator) { @@ -215,19 +214,22 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy { */ private ComponentConnector filterPotentialMatches( List 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()); + + int split_idx = predicateString.indexOf('='); + + if (split_idx != -1) { + + String propertyName = predicateString.substring(0, split_idx) + .trim(); + String value = unquote(predicateString.substring(split_idx + 1) + .trim()); + for (ComponentConnector connector : potentialMatches) { Property property = AbstractConnector.getStateType( connector).getProperty(propertyName); @@ -236,8 +238,14 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy { return connector; } } + + } else { + int index = Integer.valueOf(predicateString); + return index < potentialMatches.size() ? potentialMatches + .get(index) : null; } } + return potentialMatches.get(0); } -- cgit v1.2.3 From c141df78b37ccb36dfbf098c09b14df5535b99cb Mon Sep 17 00:00:00 2001 From: Henri Sara Date: Wed, 2 Oct 2013 13:27:38 +0300 Subject: Generate correct selectors in debug window for TestBench 4 (#12694) Selectors use IDs, captions, indices of the widget type in parent and then as a fallback domChild/XPath paths to support sub-part selection and highlighting as well as other elements without subpart information. Change-Id: I1ce30a9eb4a96ef0387635ae7464db7e9bd6f542 --- .../vaadin/client/debug/internal/Highlight.java | 43 +- .../vaadin/client/debug/internal/SelectorPath.java | 813 +++++++++++++++++++++ .../client/debug/internal/TestBenchSection.java | 118 ++- 3 files changed, 910 insertions(+), 64 deletions(-) create mode 100644 client/src/com/vaadin/client/debug/internal/SelectorPath.java diff --git a/client/src/com/vaadin/client/debug/internal/Highlight.java b/client/src/com/vaadin/client/debug/internal/Highlight.java index f2695f58ca..5ee3a25e2c 100644 --- a/client/src/com/vaadin/client/debug/internal/Highlight.java +++ b/client/src/com/vaadin/client/debug/internal/Highlight.java @@ -144,20 +144,55 @@ public class Highlight { */ static Element show(Widget widget, String color) { if (widget != null) { + show(widget.getElement(), color); + } + return null; + } + + /** + * Highlights the given {@link Element}. + *

+ * Pass the returned {@link Element} to {@link #hide(Element)} to remove + * this particular highlight. + *

+ * + * @param element + * Element to highlight + * @return Highlight element + */ + static Element show(Element element) { + return show(element, DEFAULT_COLOR); + } + + /** + * Highlight the given {@link Element} using the given color. + *

+ * Pass the returned highlight {@link Element} to {@link #hide(Element)} to + * remove this particular highlight. + *

+ * + * @param element + * Element to highlight + * @param color + * Color of highlight + * @return Highlight element + */ + static Element show(Element element, String color) { + if (element != null) { if (highlights == null) { highlights = new HashSet(); } Element highlight = DOM.createDiv(); Style style = highlight.getStyle(); - style.setTop(widget.getAbsoluteTop(), Unit.PX); - style.setLeft(widget.getAbsoluteLeft(), Unit.PX); - int width = widget.getOffsetWidth(); + style.setTop(element.getAbsoluteTop(), Unit.PX); + style.setLeft(element.getAbsoluteLeft(), Unit.PX); + int width = element.getOffsetWidth(); if (width < MIN_WIDTH) { width = MIN_WIDTH; } style.setWidth(width, Unit.PX); - int height = widget.getOffsetHeight(); + int height = element.getOffsetHeight(); if (height < MIN_HEIGHT) { height = MIN_HEIGHT; } diff --git a/client/src/com/vaadin/client/debug/internal/SelectorPath.java b/client/src/com/vaadin/client/debug/internal/SelectorPath.java new file mode 100644 index 0000000000..d4502daeda --- /dev/null +++ b/client/src/com/vaadin/client/debug/internal/SelectorPath.java @@ -0,0 +1,813 @@ +/* + * 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.debug.internal; + +import com.google.gwt.regexp.shared.MatchResult; +import com.google.gwt.regexp.shared.RegExp; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ApplicationConnection; +import com.vaadin.client.ComponentConnector; +import com.vaadin.client.ServerConnector; +import com.vaadin.client.Util; +import com.vaadin.client.componentlocator.ComponentLocator; +import com.vaadin.client.componentlocator.VaadinFinderLocatorStrategy; +import com.vaadin.client.metadata.NoDataException; +import com.vaadin.client.metadata.Property; +import com.vaadin.client.ui.AbstractConnector; +import com.vaadin.client.ui.SubPartAware; + +/** + * A single segment of a selector path with optional parent. + *

+ * The static method {@link #findTestBenchSelector(ServerConnector, Element)} + * permits looking up a selector chain for an element (a selector and its + * parents, each selector relative to its parent). + *

+ * The method {@link #findElement()} can be used to locate the element + * referenced by a {@link SelectorPath}. {@link #getJUnitSelector(String)} can + * be used to obtain the string to add to a JUnit test to refer to the element + * identified by the path. + *

+ * This class should be considered internal to the framework and may change at + * any time. + * + * @since 7.1.x + */ +public abstract class SelectorPath { + private final SelectorPath parent; + private final ComponentLocator locator; + + private static final String SUBPART_SEPARATOR = VaadinFinderLocatorStrategy.SUBPART_SEPARATOR; + + /** + * Creates a {@link SelectorPath} from the root of the UI (without a parent) + * to identify an element. + *

+ * The {@link ComponentLocator} is used to locate the corresponding + * {@link Element} in the context of a UI. If there are multiple UIs on a + * single page, the locator should correspond to the correct + * {@link ApplicationConnection}. + * + * @param locator + * {@link ComponentLocator} to use + */ + protected SelectorPath(ComponentLocator locator) { + this(null, locator); + } + + /** + * Creates a {@link SelectorPath} which is relative to another + * {@link SelectorPath}. to identify an element. + *

+ * The {@link ComponentLocator} is used to locate the corresponding + * {@link Element} in the context of a UI. If there are multiple UIs on a + * single page, the locator should correspond to the correct + * {@link ApplicationConnection}. + * + * @param parent + * parent {@link SelectorPath} or null for root paths + * @param locator + * {@link ComponentLocator} to use + */ + protected SelectorPath(SelectorPath parent, ComponentLocator locator) { + this.parent = parent; + this.locator = locator; + } + + /** + * Returns the parent {@link SelectorPath} to which this path is relative. + * + * @return parent path + */ + public SelectorPath getParent() { + return parent; + } + + @Override + public String toString() { + return "SelectorPath: " + getJUnitSelector("..."); + } + + /** + * Returns the JUnit test fragment which can be used to refer to the element + * in a test. + * + * @param context + * the context to use (usually "getDriver()" or a variable name) + * @return string to add in a JUnit test + */ + public abstract String getJUnitSelector(String context); + + /** + * Returns the {@link Element} that this {@link SelectorPath} points to in + * the context of the {@link ComponentLocator} of the {@link SelectorPath}. + * + * @return Element identified by the path in the current UI + */ + public abstract Element findElement(); + + /** + * Returns the path to an element/connector, including separate intermediate + * paths and the final path segment. + * + * @param connector + * the connector to find + * @param element + * sub-element inside connector or null to use connector root + * element + * @return Vaadin locator path + */ + public static SelectorPath findTestBenchSelector(ServerConnector connector, + Element element) { + // TODO there should be a better way to locate and refer to captions - + // now using domChild in layout + SelectorPath selectorPath = null; + ApplicationConnection connection = connector.getConnection(); + if (connection != null) { + if (null == element) { + element = findConnectorRootElement(connector); + } + if (null != element) { + ComponentLocator locator = new ComponentLocator(connection); + String path = locator.getPathForElement(element); + SelectorPath parent = null; + + if (!path.isEmpty()) { + selectorPath = extractIdSelectorPath(path, locator); + if (null == selectorPath) { + // parent paths first if not rooted on an ID + if (connector.getParent() != null) { + parent = findTestBenchSelector( + connector.getParent(), null); + } + + if (parent != null) { + // update path to be relative to parent + Element parentElement = parent.findElement(); + if (null != parentElement) { + String parentPath = locator + .getPathForElement(parentElement); + if (path.startsWith(parentPath)) { + // remove path of parent to look for the + // children + path = path.substring(parentPath.length()); + } + } + } + + selectorPath = extractVaadinSelectorPath(path, parent, + locator); + } + if (null == selectorPath) { + if (path.startsWith("/V")) { + // fall-back: Vaadin + // this branch is needed for /VTabsheetPanel etc. + selectorPath = SelectorPath.vaadinPath(path, + parent, locator); + } else { + // fall-back: XPath + selectorPath = SelectorPath.xpath(path, parent, + locator); + } + } + } + } + } + return selectorPath; + } + + private static SelectorPath extractIdSelectorPath(String path, + ComponentLocator locator) { + SelectorPath selectorPath = null; + if (path.startsWith("PID_S")) { + // remove internal prefix + path = path.substring(5); + + // no parent for an ID selector + String pid = path; + String rest = null; + // split at first slash that is not in the subpart (if any) + int slashPos = path.indexOf("/"); + int subPartPos = path.indexOf(SUBPART_SEPARATOR); + if (subPartPos >= 0 && slashPos > subPartPos) { + // ignore slashes in subpart + slashPos = -1; + } else if (slashPos >= 0 && subPartPos > slashPos) { + // ignore subpart after slashes - handled as a part of rest + subPartPos = -1; + } + // split the ID part and any relative path after it + if (slashPos > 0) { + pid = path.substring(0, slashPos); + rest = path.substring(slashPos); + } + + // if there is a subpart directly after the id, need to use a Vaadin + // selector + SelectorPath pidSelector = null; + if (subPartPos > 0) { + String id = pid.substring(0, subPartPos); + // include the subpart separator + String subPart = pid.substring(subPartPos); + Element element = locator.getElementByPath("PID_S" + pid); + ComponentConnector connector = Util.findPaintable( + locator.getClient(), element); + if (null != connector && null != connector.getWidget()) { + String type = connector.getWidget().getClass() + .getSimpleName(); + pidSelector = SelectorPath.vaadinPath("//" + type + + "[id=\\\"" + id + "\\\"]" + subPart, null, + locator); + } else { + // no valid connector for the subpart + return null; + } + } else { + pidSelector = SelectorPath.id(pid, locator); + } + if (null != rest && !rest.isEmpty()) { + selectorPath = extractVaadinSelectorPath(path, pidSelector, + locator); + if (selectorPath == null) { + selectorPath = SelectorPath.xpath(rest, pidSelector, + locator); + } + } else { + selectorPath = pidSelector; + } + } + return selectorPath; + } + + private static SelectorPath extractVaadinSelectorPath(String path, + SelectorPath parent, ComponentLocator locator) { + SelectorPath selectorPath = null; + + String xpathPart = null; + int xpathPos = Math.min(path.indexOf("/div"), path.indexOf("/span")); + if (xpathPos >= 0) { + xpathPart = path.substring(xpathPos); + path = path.substring(0, xpathPos); + } + + String subPartPart = null; + int subPartPos = path.indexOf("#"); + if (subPartPos >= 0) { + subPartPart = path.substring(subPartPos + 1); + path = path.substring(0, subPartPos); + } + + String domChildPart = null; + int domChildPos = path.indexOf("/domChild"); + if (domChildPos >= 0) { + // include the slash + domChildPart = path.substring(domChildPos); + path = path.substring(0, domChildPos); + } + + // is it something VaadinSelectorPath can handle? + String widgetClass = null; + // first cases in a layout slot + RegExp widgetInSlotMatcher = RegExp + .compile("^/(Slot\\[(\\d+)\\]/)([a-zA-Z]+)(\\[0\\])?$"); + MatchResult matchResult = widgetInSlotMatcher.exec(path); + if (null != matchResult) { + if (matchResult.getGroupCount() >= 3) { + widgetClass = matchResult.getGroup(3); + } + } + // handle cases without intervening slot + if (null == widgetClass) { + RegExp widgetDirectlyMatcher = RegExp + .compile("^//?([a-zA-Z]+)(\\[(\\d+)\\])?$"); + matchResult = widgetDirectlyMatcher.exec(path); + if (null != matchResult) { + if (matchResult.getGroupCount() >= 1) { + widgetClass = matchResult.getGroup(1); + } + } + } + if (null != widgetClass && !widgetClass.isEmpty()) { + selectorPath = findVaadinSelectorInParent(path, widgetClass, + parent, locator); + if (null != subPartPart + && selectorPath instanceof VaadinSelectorPath) { + ((VaadinSelectorPath) selectorPath).setSubPart(subPartPart); + } else if (null != xpathPart + && selectorPath instanceof VaadinSelectorPath) { + // try to find sub-part if supported + ComponentConnector connector = Util.findPaintable( + locator.getClient(), selectorPath.findElement()); + if (connector != null + && connector.getWidget() instanceof SubPartAware) { + // for SubPartAware, skip the XPath fall-back path + Element element = locator.getElementByPathStartingAt(path, + selectorPath.findElement()); + SubPartAware subPartAware = (SubPartAware) connector + .getWidget(); + String subPart = subPartAware.getSubPartName(element); + if (null != subPart) { + // type checked above + ((VaadinSelectorPath) selectorPath).setSubPart(subPart); + } + } else { + // fall-back to XPath for the last part of the path + selectorPath = SelectorPath.xpath(xpathPart, selectorPath, + locator); + } + } + + // the whole /domChild[i]/domChild[j]... part as a single selector + if (null != domChildPart + && selectorPath instanceof VaadinSelectorPath) { + selectorPath = SelectorPath.vaadinPath(domChildPart, + selectorPath, locator); + } + } else if (null != domChildPart) { + // cases with domChild path only (parent contains rest) + selectorPath = SelectorPath.vaadinPath(domChildPart, parent, + locator); + } + return selectorPath; + } + + /** + * Find the zero-based index of the widget of type widgetClass identified by + * path within its parent and returns the corresponding Vaadin path (if + * any). For instance, the second button in a layout has index 1 regardless + * of non-button components in the parent. + *

+ * The approach used internally is to try to find the caption of the element + * inside its parent and check whether it is sufficient to identify the + * element correctly. If not, possible indices are looped through to see if + * the component of the specified type within the specified parent + * identifies the correct element. This is inefficient but more reliable + * than some alternative approaches, and does not require special cases for + * various layouts etc. + * + * @param path + * relative path for the widget of interest + * @param widgetClass + * type of the widget of interest + * @param parent + * parent component to which the path is relative + * @param locator + * ComponentLocator used to map paths to elements + * @return selector path for the element, null if none found + */ + private static SelectorPath findVaadinSelectorInParent(String path, + String widgetClass, SelectorPath parent, ComponentLocator locator) { + if (null == parent) { + SelectorPath selectorPath = SelectorPath.vaadin(widgetClass, 0, + null, locator); + if (selectorPath.findElement() == locator.getElementByPath(path)) { + return selectorPath; + } else { + return null; + } + } + // This method uses an inefficient brute-force approach but its + // results should match what is used by the TestBench selectors. + Element parentElement = parent.findElement(); + String parentPathString = locator.getPathForElement(parentElement); + if (null == parentPathString) { + parentPathString = ""; + } + Element elementToFind = locator.getElementByPath(parentPathString + + path); + if (null == elementToFind) { + return null; + } + // if the connector has a caption, first try if the element can be + // located in parent with it; if that fails, use the index in parent + String caption = getCaptionForElement(elementToFind, locator); + if (null != caption) { + SelectorPath testPath = SelectorPath.vaadin(widgetClass, caption, + parent, locator); + Element testElement = testPath.findElement(); + // TODO in theory could also iterate upwards into parents, using + // "//" before the caption to find the shortest matching path that + // identifies the correct element + if (testElement == elementToFind) { + return testPath; + } + } + + // Assumes that the number of logical child elements is at most the + // number of direct children of the DOM element - e.g. layouts have a + // single component per slot. + for (int i = 0; i < parentElement.getChildCount(); ++i) { + SelectorPath testPath = SelectorPath.vaadin(widgetClass, i, parent, + locator); + Element testElement = testPath.findElement(); + if (testElement == elementToFind) { + return testPath; + } + } + return null; + } + + private static String getCaptionForElement(Element element, + ComponentLocator locator) { + String caption = null; + ComponentConnector connector = Util.findPaintable(locator.getClient(), + element); + if (null != connector) { + Property property = AbstractConnector.getStateType(connector) + .getProperty("caption"); + try { + Object value = property.getValue(connector.getState()); + if (null != value) { + caption = String.valueOf(value); + } + } catch (NoDataException e) { + // skip the caption based selection and use index below + } + } + return caption; + } + + private static Element findConnectorRootElement(ServerConnector connector) { + Element element = null; + // try to find the root element of the connector + if (connector instanceof ComponentConnector) { + Widget widget = ((ComponentConnector) connector).getWidget(); + if (widget != null) { + element = widget.getElement(); + } + } + return element; + } + + public ComponentLocator getLocator() { + return locator; + } + + @Override + public int hashCode() { + return getJUnitSelector("context").hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + SelectorPath other = (SelectorPath) obj; + if (parent == null) { + if (other.parent != null) { + return false; + } + } else if (!parent.equals(other.parent)) { + return false; + } + if (!other.getJUnitSelector("context").equals( + getJUnitSelector("context"))) { + return false; + } + return true; + } + + protected static SelectorPath xpath(String path, SelectorPath parent, + ComponentLocator locator) { + return new XPathSelectorPath(path, parent, locator); + } + + protected static SelectorPath id(String id, ComponentLocator locator) { + return new IdSelectorPath(id, locator); + } + + protected static SelectorPath vaadin(String widgetClass, + String widgetCaption, SelectorPath parent, ComponentLocator locator) { + return new VaadinSelectorPath(widgetClass, widgetCaption, 0, parent, + locator); + } + + protected static SelectorPath vaadin(String widgetClass, int widgetIndex, + SelectorPath parent, ComponentLocator locator) { + return new VaadinSelectorPath(widgetClass, null, widgetIndex, parent, + locator); + } + + protected static SelectorPath vaadinPath(String vaadinPath, + SelectorPath parent, ComponentLocator locator) { + return new ByVaadinSelectorPath(vaadinPath, parent, locator); + } + + /** + * Selector path for finding an {@link Element} based on an XPath (relative + * to the parent {@link SelectorPath}). + */ + private static class XPathSelectorPath extends SelectorPath { + // path segment relative to parent + private final String path; + + /** + * Creates a relative XPath based component selector path. + * + * @param path + * XPath + * @param parent + * {@link SelectorPath} to which the XPath is relative, null + * if from the root + * @param locator + * ComponentLocator to use to find the element + */ + public XPathSelectorPath(String path, SelectorPath parent, + ComponentLocator locator) { + super(parent, locator); + this.path = path; + } + + /** + * Returns the XPath relative to the parent element. + * + * @return relative path string + */ + public String getPath() { + return path; + } + + @Override + public String getJUnitSelector(String context) { + return context + ".findElement(By.xpath(\"" + getPath() + "\"))"; + } + + @Override + public Element findElement() { + if (null != getParent()) { + Element parentElement = getParent().findElement(); + if (null == parentElement) { + // broken path - possibly removed parent + return null; + } + Element element = getLocator().getElementByPathStartingAt( + getPath(), parentElement); + return element; + } else { + Element element = getLocator().getElementByPath(getPath()); + return element; + } + } + } + + /** + * Element identifier based locator path. + *

+ * Identifier paths never have a parent and the identifiers should be unique + * within the context of the {@link ComponentLocator}/page. + */ + private static class IdSelectorPath extends SelectorPath { + private final String id; + + /** + * Creates an identifier based {@link SelectorPath}. The identifier + * should not contain the old "PID_S" prefix. + * + * @param id + * @param locator + */ + public IdSelectorPath(String id, ComponentLocator locator) { + super(locator); + this.id = id; + } + + /** + * Returns the ID in the DOM used to identify the element. + * + * @return Vaadin debug ID or equivalent + */ + public String getId() { + return id; + } + + @Override + public String getJUnitSelector(String context) { + return context + ".findElement(By.id(\"" + getId() + "\"))"; + } + + @Override + public Element findElement() { + // this also works for IDs + return getLocator().getElementByPath("PID_S" + getId()); + } + } + + /** + * Common base class for Vaadin selector paths (By.vaadin(...)). + */ + private static abstract class AbstractVaadinSelectorPath extends + SelectorPath { + + protected AbstractVaadinSelectorPath(SelectorPath parent, + ComponentLocator locator) { + super(parent, locator); + } + + /** + * Returns the {@link ComponentLocator} path of the element relative to + * the parent path. + * + * @return path of the element for By.vaadin(...) + */ + protected abstract String getPath(); + + @Override + public String getJUnitSelector(String context) { + return context + ".findElement(By.vaadin(\"" + getPath() + "\"))"; + } + + @Override + public Element findElement() { + if (null != getParent()) { + Element parentElement = getParent().findElement(); + Element element = getLocator().getElementByPathStartingAt( + getPath(), parentElement); + return element; + } else { + return getLocator().getElementByPath(getPath()); + } + } + + } + + /** + * TestBench selector path for Vaadin widgets. These selectors are based on + * the widget class and either the index among the widgets of that type in + * the parent or the widget caption. + */ + private static class VaadinSelectorPath extends AbstractVaadinSelectorPath { + private final String widgetClass; + private final String widgetCaption; + // negative for no index + private final int widgetIndex; + private String subPart; + + /** + * Creates a Vaadin {@link SelectorPath}. The path identifies an element + * of a given type under its parent based on either its caption or its + * index (if both are given, only the caption is used). See also + * {@link ComponentLocator} for more details. + * + * @param widgetClass + * client-side widget class + * @param widgetCaption + * caption of the widget - null to use the index instead + * @param widgetIndex + * index of the widget of the type within its parent, used + * only if the caption is not given + * @param parent + * parent {@link SelectorPath} or null + * @param locator + * component locator to use to find the corresponding + * {@link Element} + */ + public VaadinSelectorPath(String widgetClass, String widgetCaption, + int widgetIndex, SelectorPath parent, ComponentLocator locator) { + super(parent, locator); + this.widgetClass = widgetClass; + this.widgetCaption = widgetCaption; + this.widgetIndex = widgetIndex; + } + + /** + * Returns the widget type used to identify the element. + * + * @return Vaadin widget class + */ + public String getWidgetClass() { + return widgetClass; + } + + /** + * Returns the widget caption to look for or null if index is used + * instead. + * + * @return widget caption to match + */ + public String getWidgetCaption() { + return widgetCaption; + } + + /** + * Returns the index of the widget of that type within its parent - only + * used if caption is null. + * + * @return widget index + */ + public int getWidgetIndex() { + return widgetIndex; + } + + /** + * Returns the sub-part string (e.g. row and column identifiers within a + * table) used to identify a part of a component. See + * {@link ComponentLocator} and especially Vaadin selectors for more + * information. + * + * @return sub-part string or null if none + */ + public String getSubPart() { + return subPart; + } + + /** + * Sets the sub-part string (e.g. row and column identifiers within a + * table) used to identify a part of a component. See + * {@link ComponentLocator} and especially Vaadin selectors for more + * information. + * + * @param subPart + * sub-part string to use or null for none + */ + public void setSubPart(String subPart) { + this.subPart = subPart; + } + + @Override + protected String getPath() { + return "/" + getWidgetClass() + getIndexString(false) + + getSubPartPostfix(); + } + + private String getIndexString(boolean escapeQuotes) { + if (null != getWidgetCaption()) { + if (escapeQuotes) { + return "[caption=\\\"" + widgetCaption + "\\\"]"; + } else { + return "[caption=\"" + widgetCaption + "\"]"; + } + } else if (widgetIndex >= 0) { + return "[" + getWidgetIndex() + "]"; + } else { + return ""; + } + } + + private String getSubPartPostfix() { + String subPartString = ""; + if (null != getSubPart()) { + subPartString = SUBPART_SEPARATOR + getSubPart(); + } + return subPartString; + } + } + + /** + * TestBench selector path for Vaadin widgets, always using a + * By.vaadin(path) rather than other convenience methods. + */ + private static class ByVaadinSelectorPath extends + AbstractVaadinSelectorPath { + private final String path; + + /** + * Vaadin selector path for an exact path (including any preceding + * slash). + * + * @param path + * path of the element (normally with a leading slash), not + * null + * @param parent + * parent selector path or null if none + * @param locator + * ComponentLocator to use to find the corresponding element + */ + public ByVaadinSelectorPath(String path, SelectorPath parent, + ComponentLocator locator) { + super(parent, locator); + this.path = path; + } + + /** + * Returns the By.vaadin(...) path relative to the parent element. + * + * @return relative path string + */ + @Override + public String getPath() { + return path; + } + } +} \ No newline at end of file diff --git a/client/src/com/vaadin/client/debug/internal/TestBenchSection.java b/client/src/com/vaadin/client/debug/internal/TestBenchSection.java index a283b18912..efab5ac11f 100644 --- a/client/src/com/vaadin/client/debug/internal/TestBenchSection.java +++ b/client/src/com/vaadin/client/debug/internal/TestBenchSection.java @@ -15,6 +15,9 @@ */ package com.vaadin.client.debug.internal; +import java.util.HashMap; +import java.util.Map; + import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.KeyCodes; @@ -38,12 +41,11 @@ import com.vaadin.client.ComponentConnector; 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. * - * @since 7.1.4 + * @since 7.1.x * @author Vaadin Ltd */ public class TestBenchSection implements Section { @@ -51,47 +53,46 @@ public class TestBenchSection implements Section { /** * Selector widget showing a selector in a program-usable form. */ - private static class SelectorWidget extends HTML { - private static int selectorIndex = 1; - final private String path; + private static class SelectorWidget extends HTML implements + MouseOverHandler, MouseOutHandler { + private static int selectorCounter = 1; + + final private SelectorPath path; + final private SelectorWidget parent; + final private int selectorIndex = selectorCounter++; - public SelectorWidget(final String path) { + public SelectorWidget(final SelectorPath path, + final SelectorWidget parent) { this.path = path; + this.parent = parent; + + String parentString = (parent != null) ? ("element" + parent.selectorIndex) + : "getDriver()"; String html = "

" - + Util.escapeHTML("WebElement element" + (selectorIndex++) - + " = getDriver().findElement(By.vaadin(\"" + path - + "\"));") + "
"; + + Util.escapeHTML("WebElement element" + selectorIndex + + " = " + path.getJUnitSelector(parentString) + ";") + + ""; setHTML(html); - addMouseOverHandler(new MouseOverHandler() { - @Override - public void onMouseOver(MouseOverEvent event) { - for (ApplicationConnection a : ApplicationConfiguration - .getRunningApplications()) { - Element element = new ComponentLocator(a) - .getElementByPath(SelectorWidget.this.path); - ComponentConnector connector = Util - .getConnectorForElement(a, a.getUIConnector() - .getWidget(), element); - if (connector == null) { - connector = Util.getConnectorForElement(a, - RootPanel.get(), element); - } - if (connector != null) { - Highlight.showOnly(connector); - break; - } - } - } - }); - addMouseOutHandler(new MouseOutHandler() { - @Override - public void onMouseOut(MouseOutEvent event) { - Highlight.hideAll(); - } - }); + addMouseOverHandler(this); + addMouseOutHandler(this); + } + + @Override + public void onMouseOver(MouseOverEvent event) { + ApplicationConnection a = path.getLocator().getClient(); + Element element = path.findElement(); + if (null != element) { + Highlight.hideAll(); + Highlight.show(element); + } + } + + @Override + public void onMouseOut(MouseOutEvent event) { + Highlight.hideAll(); } } @@ -101,7 +102,10 @@ public class TestBenchSection implements Section { private final FlowPanel content = new FlowPanel(); private final HierarchyPanel hierarchyPanel = new HierarchyPanel(); + private final FlowPanel selectorPanel = new FlowPanel(); + // map from full path to SelectorWidget to enable reuse of old selectors + private Map selectorWidgets = new HashMap(); private final FlowPanel controls = new FlowPanel(); @@ -208,34 +212,28 @@ public class TestBenchSection implements Section { } private void pickSelector(ServerConnector connector, Element element) { - String path = findTestBenchSelector(connector, element); + SelectorPath path = SelectorPath.findTestBenchSelector(connector, + element); - if (null != path && !path.isEmpty()) { - selectorPanel.add(new SelectorWidget(path)); - } + addSelectorWidgets(path); } - private String findTestBenchSelector(ServerConnector connector, - Element element) { - String path = null; - ApplicationConnection connection = connector.getConnection(); - if (connection != null) { - if (null == element) { - // try to find the root element of the connector - if (connector instanceof ComponentConnector) { - Widget widget = ((ComponentConnector) connector) - .getWidget(); - if (widget != null) { - element = widget.getElement(); - } - } - } - if (null != element) { - path = new ComponentLocator(connection) - .getPathForElement(element); - } + private SelectorWidget addSelectorWidgets(SelectorPath path) { + // add selector widgets recursively from root towards children, reusing + // old ones + SelectorPath parent = path.getParent(); + SelectorWidget parentWidget = null; + if (null != parent) { + parentWidget = addSelectorWidgets(parent); + } + SelectorWidget widget = selectorWidgets.get(path); + if (null == widget) { + // the parent has already been added above + widget = new SelectorWidget(path, parentWidget); + selectorWidgets.put(path, widget); + selectorPanel.add(widget); } - return path; + return widget; } private final NativePreviewHandler highlightModeHandler = new NativePreviewHandler() { -- cgit v1.2.3 From 90626d3583f04800c02e6c7856aac0921c6c2adb Mon Sep 17 00:00:00 2001 From: Patrik Lindström Date: Fri, 11 Oct 2013 14:19:52 +0300 Subject: Improve Calculator test application (#12744) Change-Id: Ib65ade5ecc589717e68e34b7469b5bae3c58786d --- .../vaadin/tests/application/calculator/Calc.java | 90 ++++++++++++++++++++-- 1 file changed, 84 insertions(+), 6 deletions(-) diff --git a/uitest/src/com/vaadin/tests/application/calculator/Calc.java b/uitest/src/com/vaadin/tests/application/calculator/Calc.java index 7d080adfa6..7911556f4e 100644 --- a/uitest/src/com/vaadin/tests/application/calculator/Calc.java +++ b/uitest/src/com/vaadin/tests/application/calculator/Calc.java @@ -25,23 +25,100 @@ import com.vaadin.ui.Button.ClickListener; import com.vaadin.ui.GridLayout; import com.vaadin.ui.HorizontalLayout; import com.vaadin.ui.Label; -import com.vaadin.ui.TextArea; +import com.vaadin.ui.Table; +import com.vaadin.ui.Table.ColumnHeaderMode; import com.vaadin.ui.TextField; import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.Window; @SuppressWarnings("serial") public class Calc extends AbstractTestUI { - private class Log extends TextArea { + private class Log extends VerticalLayout { + + private Table table; + private Button addCommentButton; + private int line = 0; public Log() { super(); + + table = new Table(); + table.setSizeFull(); + setWidth("200px"); + setHeight("100%"); + + table.setColumnHeaderMode(ColumnHeaderMode.HIDDEN); + table.addContainerProperty("Operation", String.class, ""); + + addComponent(table); + + addCommentButton = new Button("Add Comment"); + addCommentButton.setWidth("100%"); + + addCommentButton.addClickListener(new ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + + final Window w = new Window("Add comment"); + VerticalLayout vl = new VerticalLayout(); + vl.setMargin(true); + + final TextField tf = new TextField(); + tf.setSizeFull(); + vl.addComponent(tf); + + HorizontalLayout hl = new HorizontalLayout(); + + Button okButton = new Button("OK"); + okButton.setWidth("100%"); + okButton.addClickListener(new ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + addRow("[ " + tf.getValue() + " ]"); + tf.setValue(""); + w.close(); + removeWindow(w); + } + }); + + Button cancelButton = new Button("Cancel"); + cancelButton.setWidth("100%"); + cancelButton.addClickListener(new ClickListener() { + @Override + public void buttonClick(ClickEvent event) { + tf.setValue(""); + w.close(); + removeWindow(w); + } + }); + + hl.addComponent(cancelButton); + hl.addComponent(okButton); + hl.setSpacing(true); + hl.setWidth("100%"); + + vl.addComponent(hl); + vl.setSpacing(true); + + w.setContent(vl); + addWindow(w); + } + }); + + addComponent(addCommentButton); + + setExpandRatio(table, 1); + setSpacing(true); } public void addRow(String row) { - setValue(getValue() + "\n" + row); + Integer id = ++line; + table.addItem(new Object[] { row }, id); + table.setCurrentPageFirstItemIndex(line + 1); } + } // All variables are automatically stored in the session. @@ -51,7 +128,7 @@ public class Calc extends AbstractTestUI { private VerticalLayout topLayout = new VerticalLayout(); // User interface components - private final TextField display = new TextField("0.0"); + private final TextField display = new TextField(); private final Log log = new Log(); @@ -137,10 +214,12 @@ public class Calc extends AbstractTestUI { topLayout.addComponent(horizontalLayout); // Create a result label that over all 4 columns in the first row + layout.setSpacing(true); layout.addComponent(display, 0, 0, 3, 0); layout.setComponentAlignment(display, Alignment.MIDDLE_RIGHT); - display.setSizeUndefined(); + display.setSizeFull(); display.setId("display"); + display.setValue("0.0"); // The operations for the calculator in the order they appear on the // screen (left to right, top to bottom) @@ -151,7 +230,6 @@ public class Calc extends AbstractTestUI { // Create a button and use this application for event handling Button button = new Button(caption); - button.setHeight("30px"); button.setWidth("40px"); button.addClickListener(new ClickListener() { @Override -- cgit v1.2.3 From 29ec642eb1321f58bbcfbde79c1f1362cf1e3f9b Mon Sep 17 00:00:00 2001 From: Henri Sara Date: Thu, 31 Oct 2013 11:56:04 +0200 Subject: Produce TB4 style selectors in debug window (#12694) Change-Id: Ie2e5cc48c493d065221a30fa6d2058f7a61f0274 --- .../vaadin/client/debug/internal/SelectorPath.java | 69 +++++++++++++++++++--- .../client/debug/internal/TestBenchSection.java | 6 +- .../com/vaadin/client/metadata/TypeDataStore.java | 16 +++++ 3 files changed, 81 insertions(+), 10 deletions(-) diff --git a/client/src/com/vaadin/client/debug/internal/SelectorPath.java b/client/src/com/vaadin/client/debug/internal/SelectorPath.java index d4502daeda..2ad77a246b 100644 --- a/client/src/com/vaadin/client/debug/internal/SelectorPath.java +++ b/client/src/com/vaadin/client/debug/internal/SelectorPath.java @@ -16,18 +16,21 @@ package com.vaadin.client.debug.internal; +import com.google.gwt.core.client.JsArrayString; import com.google.gwt.regexp.shared.MatchResult; import com.google.gwt.regexp.shared.RegExp; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.ApplicationConnection; import com.vaadin.client.ComponentConnector; +import com.vaadin.client.FastStringSet; import com.vaadin.client.ServerConnector; import com.vaadin.client.Util; import com.vaadin.client.componentlocator.ComponentLocator; import com.vaadin.client.componentlocator.VaadinFinderLocatorStrategy; import com.vaadin.client.metadata.NoDataException; import com.vaadin.client.metadata.Property; +import com.vaadin.client.metadata.TypeDataStore; import com.vaadin.client.ui.AbstractConnector; import com.vaadin.client.ui.SubPartAware; @@ -108,7 +111,8 @@ public abstract class SelectorPath { * in a test. * * @param context - * the context to use (usually "getDriver()" or a variable name) + * the context to use (usually a variable name) or null for + * default * @return string to add in a JUnit test */ public abstract String getJUnitSelector(String context); @@ -551,7 +555,10 @@ public abstract class SelectorPath { @Override public String getJUnitSelector(String context) { - return context + ".findElement(By.xpath(\"" + getPath() + "\"))"; + // use driver by default + String contextString = null != context ? context : "getDriver()"; + return contextString + ".findElement(By.xpath(\"" + getPath() + + "\"))"; } @Override @@ -604,7 +611,8 @@ public abstract class SelectorPath { @Override public String getJUnitSelector(String context) { - return context + ".findElement(By.id(\"" + getId() + "\"))"; + String contextPart = null != context ? ", " + context : ""; + return "getElementById(\"" + getId() + "\"" + contextPart + ")"; } @Override @@ -633,11 +641,6 @@ public abstract class SelectorPath { */ protected abstract String getPath(); - @Override - public String getJUnitSelector(String context) { - return context + ".findElement(By.vaadin(\"" + getPath() + "\"))"; - } - @Override public Element findElement() { if (null != getParent()) { @@ -745,6 +748,50 @@ public abstract class SelectorPath { this.subPart = subPart; } + @Override + public String getJUnitSelector(String context) { + String componentClass = getComponentClass(); + String contextPart = null != context ? ", " + context : ""; + // TODO update after subpart API finished + if (null != getSubPart() || null == componentClass) { + return "getElementByPath(\"" + getPath() + "\"" + contextPart + + ")"; + } else if (null != getWidgetCaption()) { + return "getElementByCaption(" + componentClass + ".class, \"" + + getWidgetCaption() + "\"" + contextPart + ")"; + } else if (getWidgetIndex() >= 0) { + return "getElementByIndex(" + componentClass + ".class, " + + getWidgetIndex() + contextPart + ")"; + } else { + return "getElement(" + componentClass + ".class" + contextPart + + ")"; + } + } + + /** + * Returns the Vaadin server side component class to use for a widget + * class. + * + * @return fully qualified server side class name, null if unable to + * determine it + */ + private String getComponentClass() { + ComponentConnector connector = Util.findPaintable(getLocator() + .getClient(), findElement()); + Class connectorClass = connector + .getClass(); + FastStringSet identifiers = TypeDataStore.get().findIdentifiersFor( + connectorClass); + JsArrayString ids = identifiers.dump(); + if (ids.length() == 1) { + return ids.get(0); + } else { + return null; + } + } + + // these are used only to locate components on the client side by path + @Override protected String getPath() { return "/" + getWidgetClass() + getIndexString(false) @@ -800,6 +847,12 @@ public abstract class SelectorPath { this.path = path; } + @Override + public String getJUnitSelector(String context) { + String contextPart = null != context ? ", " + context : ""; + return "getElementByPath(\"" + getPath() + "\"" + contextPart + ")"; + } + /** * Returns the By.vaadin(...) path relative to the parent element. * diff --git a/client/src/com/vaadin/client/debug/internal/TestBenchSection.java b/client/src/com/vaadin/client/debug/internal/TestBenchSection.java index efab5ac11f..462309768f 100644 --- a/client/src/com/vaadin/client/debug/internal/TestBenchSection.java +++ b/client/src/com/vaadin/client/debug/internal/TestBenchSection.java @@ -67,7 +67,7 @@ public class TestBenchSection implements Section { this.parent = parent; String parentString = (parent != null) ? ("element" + parent.selectorIndex) - : "getDriver()"; + : null; String html = "
" @@ -215,7 +215,9 @@ public class TestBenchSection implements Section { SelectorPath path = SelectorPath.findTestBenchSelector(connector, element); - addSelectorWidgets(path); + if (null != path) { + addSelectorWidgets(path); + } } private SelectorWidget addSelectorWidgets(SelectorPath path) { diff --git a/client/src/com/vaadin/client/metadata/TypeDataStore.java b/client/src/com/vaadin/client/metadata/TypeDataStore.java index aa37d75dc8..649f018f95 100644 --- a/client/src/com/vaadin/client/metadata/TypeDataStore.java +++ b/client/src/com/vaadin/client/metadata/TypeDataStore.java @@ -69,6 +69,22 @@ public class TypeDataStore { return class1; } + // this is a very inefficient implementation for getting all the identifiers + // for a class + public FastStringSet findIdentifiersFor(Class type) { + FastStringSet result = FastStringSet.create(); + + JsArrayString keys = identifiers.getKeys(); + for (int i = 0; i < keys.length(); i++) { + String key = keys.get(i); + if (identifiers.get(key) == type) { + result.add(key); + } + } + + return result; + } + public static Type getType(Class clazz) { return new Type(clazz); } -- cgit v1.2.3 From 5b8b824942019ab6fc9d70263c22de3644f1f785 Mon Sep 17 00:00:00 2001 From: Patrik Lindström Date: Tue, 5 Nov 2013 15:58:28 +0200 Subject: Fix TB3 style selectors (#12902, #12904) Change-Id: Ia1ad464b6890248e700ed2dbea03746671210eee --- client/src/com/vaadin/client/Util.java | 1 + .../client/componentlocator/ComponentLocator.java | 58 +++--------------- .../componentlocator/LegacyLocatorStrategy.java | 69 +++++++++++++--------- .../VaadinFinderLocatorStrategy.java | 14 +++-- 4 files changed, 59 insertions(+), 83 deletions(-) diff --git a/client/src/com/vaadin/client/Util.java b/client/src/com/vaadin/client/Util.java index 8972670232..c0e5f621af 100644 --- a/client/src/com/vaadin/client/Util.java +++ b/client/src/com/vaadin/client/Util.java @@ -855,6 +855,7 @@ public class Util { * @param class1 * the Widget type to seek for */ + @SuppressWarnings("unchecked") public static T findWidget(Element element, Class class1) { if (element != null) { diff --git a/client/src/com/vaadin/client/componentlocator/ComponentLocator.java b/client/src/com/vaadin/client/componentlocator/ComponentLocator.java index e319650d23..6f6e52c0e1 100644 --- a/client/src/com/vaadin/client/componentlocator/ComponentLocator.java +++ b/client/src/com/vaadin/client/componentlocator/ComponentLocator.java @@ -20,7 +20,6 @@ import java.util.List; 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 @@ -34,14 +33,12 @@ import com.vaadin.client.BrowserInfo; public class ComponentLocator { private final List locatorStrategies; - private final LegacyLocatorStrategy legacyLocatorStrategy = new LegacyLocatorStrategy( - this); /** * Reference to ApplicationConnection instance. */ - private ApplicationConnection client; + private final ApplicationConnection client; /** * Construct a ComponentLocator for the given ApplicationConnection. @@ -51,8 +48,8 @@ public class ComponentLocator { */ public ComponentLocator(ApplicationConnection client) { this.client = client; - locatorStrategies = Arrays.asList( - new VaadinFinderLocatorStrategy(this), legacyLocatorStrategy); + locatorStrategies = Arrays.asList(new VaadinFinderLocatorStrategy( + client), new LegacyLocatorStrategy(client)); } /** @@ -74,44 +71,13 @@ 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)); - } - - /** - * 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(); + for (LocatorStrategy strategy : locatorStrategies) { + String path = strategy.getPathForElement(targetElement); + if (null != path) { + return path; + } } - // No siblings so this is the only child - return (Element) targetElement.getParentNode().getChild(0); + return null; } /** @@ -127,9 +93,6 @@ public class ComponentLocator { * could not be located. */ public Element getElementByPath(String 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) { Element element = strategy.getElementByPath(path); if (null != element) { @@ -153,9 +116,6 @@ public class ComponentLocator { * 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) { Element element = strategy.getElementByPathStartingAt(path, root); if (null != element) { diff --git a/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java b/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java index ed70486ff5..5f5e0279ce 100644 --- a/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java +++ b/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java @@ -25,6 +25,7 @@ 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.ApplicationConnection; import com.vaadin.client.ComponentConnector; import com.vaadin.client.ConnectorMap; import com.vaadin.client.ServerConnector; @@ -53,7 +54,7 @@ import com.vaadin.shared.communication.SharedState; * @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. */ @@ -70,14 +71,16 @@ public class LegacyLocatorStrategy implements LocatorStrategy { */ static final String ROOT_ID = "Root"; - public LegacyLocatorStrategy(ComponentLocator componentLocator) { - this.componentLocator = componentLocator; + private final ApplicationConnection client; + + public LegacyLocatorStrategy(ApplicationConnection clientConnection) { + client = clientConnection; } @Override public String getPathForElement(Element targetElement) { - ComponentConnector connector = Util.findPaintable( - componentLocator.getClient(), targetElement); + ComponentConnector connector = Util + .findPaintable(client, targetElement); Widget w = null; if (connector != null) { @@ -189,14 +192,12 @@ public class LegacyLocatorStrategy implements LocatorStrategy { String parts[] = path.split(LegacyLocatorStrategy.SUBPART_SEPARATOR, 2); String widgetPath = parts[0]; - Widget baseWidget = null; - if (null != baseElement) { - // Note that this only works if baseElement can be mapped to a - // widget to which the path is relative. Otherwise, the current - // implementation simply interprets the path as if baseElement was - // null. - baseWidget = Util.findWidget(baseElement, null); - } + // Note that this only works if baseElement can be mapped to a + // widget to which the path is relative. Otherwise, the current + // implementation simply interprets the path as if baseElement was + // null. + Widget baseWidget = Util.findWidget(baseElement, null); + Widget w = getWidgetFromPath(widgetPath, baseWidget); if (w == null || !Util.isAttachedAndDisplayed(w)) { return null; @@ -209,7 +210,7 @@ public class LegacyLocatorStrategy implements LocatorStrategy { // Contains dom reference to a sub element of the widget String subPath = widgetPath.substring(pos); - return getElementByDOMPath(baseElement, subPath); + return getElementByDOMPath(w.getElement(), subPath); } else if (parts.length == 2) { if (w instanceof SubPartAware) { return ((SubPartAware) w).getSubPartElement(parts[1]); @@ -283,7 +284,8 @@ public class LegacyLocatorStrategy implements LocatorStrategy { String parts[] = path.split(PARENTCHILD_SEPARATOR); Element element = baseElement; - for (String part : parts) { + for (int i = 0, l = parts.length; i < l; ++i) { + String part = parts[i]; if (part.startsWith("domChild[")) { String childIndexString = part.substring("domChild[".length(), part.length() - 1); @@ -311,6 +313,14 @@ public class LegacyLocatorStrategy implements LocatorStrategy { return null; } + } else { + + path = parts[i]; + for (int j = i + 1; j < l; ++j) { + path += PARENTCHILD_SEPARATOR + parts[j]; + } + + return getElementByPathStartingAt(path, element); } } @@ -386,10 +396,10 @@ public class LegacyLocatorStrategy implements LocatorStrategy { } else if (w instanceof VUI) { return ""; } else if (w instanceof VWindow) { - Connector windowConnector = ConnectorMap.get( - componentLocator.getClient()).getConnector(w); - List subWindowList = componentLocator.getClient() - .getUIConnector().getSubWindows(); + Connector windowConnector = ConnectorMap.get(client) + .getConnector(w); + List subWindowList = client.getUIConnector() + .getSubWindows(); int indexOfSubWindow = subWindowList.indexOf(windowConnector); return PARENTCHILD_SEPARATOR + "VWindow[" + indexOfSubWindow + "]"; } else if (w instanceof RootPanel) { @@ -442,6 +452,7 @@ public class LegacyLocatorStrategy implements LocatorStrategy { * @return The Widget identified by the String locator or null if the widget * could not be identified. */ + @SuppressWarnings("unchecked") private Widget getWidgetFromPath(String path, Widget baseWidget) { Widget w = baseWidget; String parts[] = path.split(PARENTCHILD_SEPARATOR); @@ -452,17 +463,19 @@ public class LegacyLocatorStrategy implements LocatorStrategy { if (part.equals(ROOT_ID)) { w = RootPanel.get(); } else if (part.equals("")) { - w = componentLocator.getClient().getUIConnector().getWidget(); + if (w == null) { + w = client.getUIConnector().getWidget(); + } } else if (w == null) { String id = part; // Must be old static pid (PID_S*) - ServerConnector connector = ConnectorMap.get( - componentLocator.getClient()).getConnector(id); + ServerConnector connector = ConnectorMap.get(client) + .getConnector(id); if (connector == null) { // Lookup by component id // TODO Optimize this - connector = findConnectorById(componentLocator.getClient() - .getUIConnector(), id.substring(5)); + connector = findConnectorById(client.getUIConnector(), + id.substring(5)); } if (connector instanceof ComponentConnector) { @@ -516,7 +529,7 @@ public class LegacyLocatorStrategy implements LocatorStrategy { if (w instanceof VOverlay && "VCalendarPanel".equals(widgetClassName)) { // Vaadin 7.1 adds a wrapper for datefield popups - parent = (Iterable) ((Iterable) parent).iterator() + parent = (Iterable) ((Iterable) parent).iterator() .next(); } /* @@ -576,8 +589,8 @@ public class LegacyLocatorStrategy implements LocatorStrategy { * compatibility */ if (widgetClassName.equals("VWindow")) { - List windows = componentLocator - .getClient().getUIConnector().getSubWindows(); + List windows = client.getUIConnector() + .getSubWindows(); List windowWidgets = new ArrayList( windows.size()); for (WindowConnector wc : windows) { @@ -585,7 +598,7 @@ public class LegacyLocatorStrategy implements LocatorStrategy { } iterator = windowWidgets.iterator(); } else if (widgetClassName.equals("VContextMenu")) { - return componentLocator.getClient().getContextMenu(); + return client.getContextMenu(); } else { iterator = (Iterator) parent.iterator(); } diff --git a/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java b/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java index 264574788c..3390de86a4 100644 --- a/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java +++ b/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java @@ -21,6 +21,7 @@ import java.util.List; 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.ApplicationConnection; import com.vaadin.client.ComponentConnector; import com.vaadin.client.Util; import com.vaadin.client.metadata.NoDataException; @@ -51,10 +52,11 @@ import com.vaadin.shared.AbstractComponentState; public class VaadinFinderLocatorStrategy implements LocatorStrategy { public static final String SUBPART_SEPARATOR = "#"; - private ComponentLocator componentLocator; - public VaadinFinderLocatorStrategy(ComponentLocator componentLocator) { - this.componentLocator = componentLocator; + private final ApplicationConnection client; + + public VaadinFinderLocatorStrategy(ApplicationConnection clientConnection) { + client = clientConnection; } /** @@ -77,8 +79,8 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy { if (path.startsWith("//VNotification")) { return findNotificationByPath(path); } - return getElementByPathStartingAtConnector(path, componentLocator - .getClient().getUIConnector()); + return getElementByPathStartingAtConnector(path, + client.getUIConnector()); } /** @@ -112,7 +114,7 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy { @Override public Element getElementByPathStartingAt(String path, Element root) { return getElementByPathStartingAtConnector(path, - Util.findPaintable(componentLocator.getClient(), root)); + Util.findPaintable(client, root)); } /** -- cgit v1.2.3 From d70d6a2cac7271b2f49805611d77cd3f7eb431f1 Mon Sep 17 00:00:00 2001 From: Patrik Lindström Date: Wed, 6 Nov 2013 16:15:52 +0200 Subject: Make component predicate matches exact (#12913) Change-Id: Ia9a938977437a2188a6f5b207789058abce62965 --- .../vaadin/client/componentlocator/LegacyLocatorStrategy.java | 11 ++++++++++- .../client/componentlocator/VaadinFinderLocatorStrategy.java | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java b/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java index 5f5e0279ce..5123a57e5d 100644 --- a/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java +++ b/client/src/com/vaadin/client/componentlocator/LegacyLocatorStrategy.java @@ -499,7 +499,16 @@ public class LegacyLocatorStrategy implements LocatorStrategy { String widgetClassName = split[0]; String indexString = split[1].substring(0, split[1].length() - 1); - int widgetPosition = Integer.parseInt(indexString); + + int widgetPosition; + try { + widgetPosition = Integer.parseInt(indexString); + } catch (NumberFormatException e) { + // We've probably been fed a new-style Vaadin locator with a + // string-form predicate, that doesn't match anything in the + // search space. + return null; + } // AbsolutePanel in GridLayout has been removed -> skip it if (w instanceof VGridLayout diff --git a/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java b/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java index 3390de86a4..96374e898a 100644 --- a/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java +++ b/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java @@ -241,6 +241,8 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy { } } + return null; + } else { int index = Integer.valueOf(predicateString); return index < potentialMatches.size() ? potentialMatches -- cgit v1.2.3 From d2e7b366ef40bd5a27ba4693eba8d80ae8fc4f77 Mon Sep 17 00:00:00 2001 From: Teemu Suo-Anttila Date: Thu, 7 Nov 2013 11:32:28 +0200 Subject: Support special characters in quotes in locators (#12738). Change-Id: I5674bfdab3a5ff3ff9799178bbf9e187fa7ea544 --- .../VaadinFinderLocatorStrategy.java | 34 ++++++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java b/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java index 96374e898a..95b2745bf8 100644 --- a/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java +++ b/client/src/com/vaadin/client/componentlocator/VaadinFinderLocatorStrategy.java @@ -192,9 +192,10 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy { * @return The predicate string for the path fragment or null if none. */ private String extractPredicateString(String pathFragment) { - int ixOpenBracket = pathFragment.indexOf('['); + int ixOpenBracket = indexOfIgnoringQuotes(pathFragment, '['); if (ixOpenBracket >= 0) { - int ixCloseBracket = pathFragment.indexOf(']', ixOpenBracket); + int ixCloseBracket = indexOfIgnoringQuotes(pathFragment, ']', + ixOpenBracket); return pathFragment.substring(ixOpenBracket + 1, ixCloseBracket); } return null; @@ -376,7 +377,7 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy { * the path. */ private String[] splitFirstFragmentFromTheRest(String path) { - int ixOfSlash = path.indexOf('/'); + int ixOfSlash = indexOfIgnoringQuotes(path, '/'); if (ixOfSlash > 0) { return new String[] { path.substring(0, ixOfSlash), path.substring(ixOfSlash) }; @@ -384,4 +385,31 @@ public class VaadinFinderLocatorStrategy implements LocatorStrategy { return new String[] { path }; } + private int indexOfIgnoringQuotes(String str, char find) { + return indexOfIgnoringQuotes(str, find, 0); + } + + private int indexOfIgnoringQuotes(String str, char find, int startingAt) { + boolean quote = false; + String quoteChars = "'\""; + char currentQuote = '"'; + for (int i = startingAt; i < str.length(); ++i) { + char cur = str.charAt(i); + if (quote) { + if (cur == currentQuote) { + quote = !quote; + } + continue; + } else if (cur == find) { + return i; + } else { + if (quoteChars.indexOf(cur) >= 0) { + currentQuote = cur; + quote = !quote; + } + } + } + return -1; + } + } -- cgit v1.2.3