]> source.dussan.org Git - vaadin-framework.git/commitdiff
Support for HTML5 push/replaceState for proper deep linking features (#8116)
authorMatti Tahvonen <matti@vaadin.com>
Fri, 13 Jan 2017 15:07:36 +0000 (17:07 +0200)
committerPekka Hyvönen <pekka@vaadin.com>
Fri, 13 Jan 2017 15:07:36 +0000 (17:07 +0200)
* Added support for HTML5 push/replaceState for proper deep linkin features

* Automated test script now works at least on chrome

* Uses html5 push/popstate to implement uri fragment feature

* fire legacy fragment change events also via popstate events rpc calls
* send new fragments via pushstate mechanism

* formatting

* Formatting and adding test and workaround for IE bug

* Formatting and depracated UriFragmentListener

* Aligned naming in the new API

* Ignored IE due to web driver bug

Tested a workaround with javascript based window.location.href fetch,
but that don’t seem to work stable enough.

14 files changed:
client/src/main/java/com/vaadin/client/ui/VUI.java
client/src/main/java/com/vaadin/client/ui/ui/UIConnector.java
client/src/main/resources/com/vaadin/DefaultWidgetSet.gwt.xml
server/src/main/java/com/vaadin/server/Page.java
server/src/main/java/com/vaadin/ui/UI.java
shared/src/main/java/com/vaadin/shared/ui/ui/UIConstants.java
shared/src/main/java/com/vaadin/shared/ui/ui/UIServerRpc.java
uitest/src/main/java/com/vaadin/tests/components/ui/PushStateAndReplaceState.java [new file with mode: 0644]
uitest/src/main/java/com/vaadin/tests/components/ui/UriFragment.java
uitest/src/test/java/com/vaadin/tests/components/ui/PushStateAndReplaceStateTest.java [new file with mode: 0644]
uitest/src/test/java/com/vaadin/tests/components/ui/UriFragmentTest.java
uitest/src/test/java/com/vaadin/tests/navigator/NavigatorViewBlocksBackButtonActionTest.java
uitest/src/test/java/com/vaadin/tests/urifragments/FragmentHandlingAndAsynchUIUpdateTest.java
uitest/src/test/java/com/vaadin/tests/urifragments/SettingNullFragmentTest.java

index ad4077d3961e2896457cfa637355d33cc8ff686a..79c918f7113cee125a1a259dbf504cee4334f26c 100644 (file)
@@ -26,11 +26,7 @@ import com.google.gwt.event.dom.client.ScrollHandler;
 import com.google.gwt.event.logical.shared.HasResizeHandlers;
 import com.google.gwt.event.logical.shared.ResizeEvent;
 import com.google.gwt.event.logical.shared.ResizeHandler;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
 import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.http.client.URL;
-import com.google.gwt.user.client.History;
 import com.google.gwt.user.client.Timer;
 import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.ui.SimplePanel;
@@ -46,7 +42,6 @@ import com.vaadin.client.ui.ShortcutActionHandler.ShortcutActionHandlerOwner;
 import com.vaadin.client.ui.TouchScrollDelegate.TouchScrollHandler;
 import com.vaadin.client.ui.ui.UIConnector;
 import com.vaadin.shared.ApplicationConstants;
-import com.vaadin.shared.ui.ui.UIConstants;
 
 /**
  *
@@ -99,51 +94,8 @@ public class VUI extends SimplePanel implements ResizeHandler,
     /** For internal use only. May be removed or replaced in the future. */
     public boolean resizeLazy = false;
 
-    private HandlerRegistration historyHandlerRegistration;
-
     private TouchScrollHandler touchScrollHandler;
 
-    /**
-     * The current URI fragment, used to avoid sending updates if nothing has
-     * changed.
-     * <p>
-     * For internal use only. May be removed or replaced in the future.
-     */
-    public String currentFragment;
-
-    /**
-     * Listener for URI fragment changes. Notifies the server of the new value
-     * whenever the value changes.
-     */
-    private final ValueChangeHandler<String> historyChangeHandler = new ValueChangeHandler<String>() {
-
-        @Override
-        public void onValueChange(ValueChangeEvent<String> event) {
-            String newFragment = event.getValue();
-
-            // Send the location to the server if the fragment has changed
-            // and flush active connectors in UI.
-            if (!newFragment.equals(currentFragment) && connection != null) {
-                /*
-                 * Ensure the fragment is properly encoded in all browsers
-                 * (#10769)
-                 *
-                 * createUrlBuilder does not properly pass an empty fragment to
-                 * UrlBuilder on Webkit browsers so do it manually (#11686)
-                 */
-                String location = Window.Location.createUrlBuilder()
-                        .setHash(URL
-                                .decodeQueryString(Window.Location.getHash()))
-                        .buildString();
-
-                currentFragment = newFragment;
-                connection.flushActiveConnector();
-                connection.updateVariable(id, UIConstants.LOCATION_VARIABLE,
-                        location, true);
-            }
-        }
-    };
-
     private VLazyExecutor delayedResizeExecutor = new VLazyExecutor(200,
             new ScheduledCommand() {
 
@@ -186,21 +138,6 @@ public class VUI extends SimplePanel implements ResizeHandler,
         }
     }
 
-    @Override
-    protected void onAttach() {
-        super.onAttach();
-        historyHandlerRegistration = History
-                .addValueChangeHandler(historyChangeHandler);
-        currentFragment = History.getToken();
-    }
-
-    @Override
-    protected void onDetach() {
-        super.onDetach();
-        historyHandlerRegistration.removeHandler();
-        historyHandlerRegistration = null;
-    }
-
     /**
      * Stop monitoring for parent element resizes.
      */
index 180898453b3ba0d126d64e474386579f6419ccac..49475901c9d870649224bf02ac8bff824a4fe0e4 100644 (file)
@@ -43,18 +43,17 @@ import com.google.gwt.event.dom.client.ScrollHandler;
 import com.google.gwt.event.logical.shared.ResizeEvent;
 import com.google.gwt.event.logical.shared.ResizeHandler;
 import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.http.client.URL;
 import com.google.gwt.user.client.Command;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.History;
 import com.google.gwt.user.client.Timer;
 import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.Window.Location;
 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.ApplicationConnection.ApplicationStoppedEvent;
+import com.vaadin.client.BrowserInfo;
 import com.vaadin.client.ComponentConnector;
 import com.vaadin.client.ConnectorHierarchyChangeEvent;
 import com.vaadin.client.Focusable;
@@ -105,6 +104,8 @@ import com.vaadin.shared.ui.ui.UIState;
 import com.vaadin.shared.util.SharedUtil;
 import com.vaadin.ui.UI;
 
+import elemental.client.Browser;
+
 @Connect(value = UI.class, loadStyle = LoadStyle.EAGER)
 public class UIConnector extends AbstractSingleComponentContainerConnector
         implements Paintable, MayScrollChildren {
@@ -115,6 +116,12 @@ public class UIConnector extends AbstractSingleComponentContainerConnector
 
     private HandlerRegistration windowOrderRegistration;
 
+    /*
+     * Used to workaround IE bug related to popstate events and certain fragment
+     * only changes
+     */
+    private String currentLocation;
+
     private final StateChangeHandler childStateChangeHandler = new StateChangeHandler() {
         @Override
         public void onStateChanged(StateChangeEvent stateChangeEvent) {
@@ -232,6 +239,28 @@ public class UIConnector extends AbstractSingleComponentContainerConnector
                 }
             }
         });
+
+        Browser.getWindow().setOnpopstate(evt -> {
+            final String newLocation = Browser.getWindow().getLocation()
+                    .toString();
+            getRpcProxy(UIServerRpc.class).popstate(newLocation);
+            currentLocation = newLocation;
+        });
+        // IE doesn't fire popstate correctly with certain hash changes.
+        // Simulate the missing event with History handler.
+        if (BrowserInfo.get().isIE()) {
+            History.addValueChangeHandler(evt -> {
+                final String newLocation = Browser.getWindow().getLocation()
+                        .toString();
+                if (!newLocation.equals(currentLocation)) {
+                    currentLocation = newLocation;
+                    getRpcProxy(UIServerRpc.class).popstate(
+                            Browser.getWindow().getLocation().toString());
+                }
+            });
+            currentLocation = Browser.getWindow().getLocation().toString();
+        }
+
     }
 
     private native void open(String url, String name)
@@ -402,34 +431,13 @@ public class UIConnector extends AbstractSingleComponentContainerConnector
             scrollIntoView(connector);
         }
 
-        if (uidl.hasAttribute(UIConstants.LOCATION_VARIABLE)) {
-            String location = uidl
-                    .getStringAttribute(UIConstants.LOCATION_VARIABLE);
-            String newFragment;
-
-            int fragmentIndex = location.indexOf('#');
-            if (fragmentIndex >= 0) {
-                // Decode fragment to avoid double encoding (#10769)
-                newFragment = URL.decodePathSegment(
-                        location.substring(fragmentIndex + 1));
-
-                if (newFragment.isEmpty()
-                        && Location.getHref().indexOf('#') == -1) {
-                    // Ensure there is a trailing # even though History and
-                    // Location.getHash() treat null and "" the same way.
-                    Location.assign(Location.getHref() + "#");
-                }
-            } else {
-                // No fragment in server-side location, but can't completely
-                // remove the browser fragment since that would reload the page
-                newFragment = "";
-            }
-
-            getWidget().currentFragment = newFragment;
-
-            if (!newFragment.equals(History.getToken())) {
-                History.newItem(newFragment, true);
-            }
+        if (uidl.hasAttribute(UIConstants.ATTRIBUTE_PUSH_STATE)) {
+            Browser.getWindow().getHistory().pushState(null, "",
+                    uidl.getStringAttribute(UIConstants.ATTRIBUTE_PUSH_STATE));
+        }
+        if (uidl.hasAttribute(UIConstants.ATTRIBUTE_REPLACE_STATE)) {
+            Browser.getWindow().getHistory().replaceState(null, "", uidl
+                    .getStringAttribute(UIConstants.ATTRIBUTE_REPLACE_STATE));
         }
 
         if (firstPaint) {
index ad345e44e2ee3dde3870302d15f2d92305f88d4b..477ec43288438acf68f7395262933970e44ead6b 100755 (executable)
@@ -8,8 +8,7 @@
 
        <inherits name="com.vaadin.Vaadin" />
 
-       <!-- Elemental is used for handling Json only -->
-       <inherits name="elemental.Json" />
+       <inherits name="elemental.Elemental" />
 
        <inherits name="com.google.gwt.precompress.Precompress" />
 
index c96114b3f738f83f653ab73c651ca74fd7284944..bf55e7ce9faf7db47c776753259c6bdab4c440bf 100644 (file)
@@ -251,7 +251,9 @@ public class Page implements Serializable {
      * changes.
      *
      * @see Page#addUriFragmentChangedListener(UriFragmentChangedListener)
+     * @deprecated Use {@link PopStateListener} instead
      */
+    @Deprecated
     @FunctionalInterface
     public interface UriFragmentChangedListener extends Serializable {
         /**
@@ -272,6 +274,32 @@ public class Page implements Serializable {
             .findMethod(Page.UriFragmentChangedListener.class,
                     "uriFragmentChanged", UriFragmentChangedEvent.class);
 
+    /**
+     * Listener that that gets notified when the URI of the page changes due to
+     * back/forward functionality of the browser.
+     *
+     * @see Page#addPopStateListener(PopStateListener)
+     * @since 8.0
+     */
+    @FunctionalInterface
+    public interface PopStateListener extends Serializable {
+        /**
+         * Event handler method invoked when the URI fragment of the page
+         * changes. Please note that the initial URI fragment has already been
+         * set when a new UI is initialized, so there will not be any initial
+         * event for listeners added during {@link UI#init(VaadinRequest)}.
+         *
+         * @see Page#addUriFragmentChangedListener(UriFragmentChangedListener)
+         *
+         * @param event
+         *            the URI fragment changed event
+         */
+        public void uriChanged(PopStateEvent event);
+    }
+
+    private static final Method URI_CHANGED_METHOD = ReflectTools.findMethod(
+            Page.PopStateListener.class, "uriChanged", PopStateEvent.class);
+
     /**
      * Resources to be opened automatically on next repaint. The list is
      * automatically cleared when it has been sent to the client.
@@ -328,6 +356,53 @@ public class Page implements Serializable {
         }
     }
 
+    /**
+     * Event fired when the URI of a <code>Page</code> changes (aka HTML 5
+     * popstate event) on the client side due to browsers back/forward
+     * functionality.
+     *
+     * @see Page#addPopStateListener(PopStateListener)
+     * @since 8.0
+     */
+    public static class PopStateEvent extends EventObject {
+
+        /**
+         * The new URI as String
+         */
+        private final String uri;
+
+        /**
+         * Creates a new instance of PopstateEvent.
+         *
+         * @param source
+         *            the Source of the event.
+         * @param uri
+         *            the new uri
+         */
+        public PopStateEvent(Page source, String uri) {
+            super(source);
+            this.uri = uri;
+        }
+
+        /**
+         * Gets the page in which the uri has changed.
+         *
+         * @return the page in which the uri has changed
+         */
+        public Page getPage() {
+            return (Page) getSource();
+        }
+
+        /**
+         * Get the new URI
+         *
+         * @return the new uri
+         */
+        public String getUri() {
+            return uri;
+        }
+    }
+
     @FunctionalInterface
     private static interface InjectedStyle extends Serializable {
         public void paint(int id, PaintTarget target) throws PaintException;
@@ -483,6 +558,9 @@ public class Page implements Serializable {
 
     private String windowName;
 
+    private String newPushState;
+    private String newReplaceState;
+
     public Page(UI uI, PageState state) {
         this.uI = uI;
         this.state = state;
@@ -516,13 +594,37 @@ public class Page implements Serializable {
      * @param listener
      *            the URI fragment listener to add
      * @return a registration object for removing the listener
+     * @deprecated Use {@link Page#addPopStateListener(PopStateListener)}
+     *             instead
      */
+    @Deprecated
     public Registration addUriFragmentChangedListener(
             Page.UriFragmentChangedListener listener) {
         return addListener(UriFragmentChangedEvent.class, listener,
                 URI_FRAGMENT_CHANGED_METHOD);
     }
 
+    /**
+     * Adds a listener that gets notified every time the URI of this page is
+     * changed due to back/forward functionality of the browser.
+     * <p>
+     * Note that one only gets notified when the back/forward button affects
+     * history changes with-in same UI, created by
+     * {@link Page#pushState(String)} or {@link Page#replaceState(String)}
+     * functions.
+     *
+     * @see #getLocation()
+     * @see Registration
+     *
+     * @param listener
+     *            the Popstate listener to add
+     * @return a registration object for removing the listener
+     * @since 8.0
+     */
+    public Registration addPopStateListener(Page.PopStateListener listener) {
+        return addListener(PopStateEvent.class, listener, URI_CHANGED_METHOD);
+    }
+
     /**
      * Removes a URI fragment listener that was previously added to this page.
      *
@@ -580,6 +682,7 @@ public class Page implements Serializable {
         try {
             location = new URI(location.getScheme(),
                     location.getSchemeSpecificPart(), newUriFragment);
+            pushState(location);
         } catch (URISyntaxException e) {
             // This should not actually happen as the fragment syntax is not
             // constrained
@@ -588,7 +691,6 @@ public class Page implements Serializable {
         if (fireEvents) {
             fireEvent(new UriFragmentChangedEvent(this, newUriFragment));
         }
-        uI.markAsDirty();
     }
 
     private void fireEvent(EventObject event) {
@@ -866,9 +968,14 @@ public class Page implements Serializable {
             notifications = null;
         }
 
-        if (location != null) {
-            target.addAttribute(UIConstants.LOCATION_VARIABLE,
-                    location.toString());
+        if (newPushState != null) {
+            target.addAttribute(UIConstants.ATTRIBUTE_PUSH_STATE, newPushState);
+            newPushState = null;
+        }
+        if (newReplaceState != null) {
+            target.addAttribute(UIConstants.ATTRIBUTE_REPLACE_STATE,
+                    newReplaceState);
+            newReplaceState = null;
         }
 
         if (styles != null) {
@@ -931,6 +1038,84 @@ public class Page implements Serializable {
         return location;
     }
 
+    /**
+     * Updates the browsers URI without causing actual page change. This method
+     * is useful if you wish implement "deep linking" to your application.
+     * Calling the method also adds a new entry to clients browser history and
+     * you can further use {@link PopStateListener} to track the usage of
+     * back/forward feature in browser.
+     * <p>
+     * Note, the current implementation supports setting only one new uri in one
+     * user interaction.
+     *
+     * @param uri
+     *            to be used for pushState operation. The URI is resolved over
+     *            the current location. If the given URI is absolute, it must be
+     *            of same origin as the current URI or the browser will not
+     *            accept the new value.
+     * @since 8.0
+     */
+    public void pushState(String uri) {
+        newPushState = uri;
+        uI.markAsDirty();
+        location = location.resolve(uri);
+    }
+
+    /**
+     * Updates the browsers URI without causing actual page change. This method
+     * is useful if you wish implement "deep linking" to your application.
+     * Calling the method also adds a new entry to clients browser history and
+     * you can further use {@link PopStateListener} to track the usage of
+     * back/forward feature in browser.
+     * <p>
+     * Note, the current implementation supports setting only one new uri in one
+     * user interaction.
+     *
+     * @param uri
+     *            the URI to be used for pushState operation. The URI is
+     *            resolved over the current location. If the given URI is
+     *            absolute, it must be of same origin as the current URI or the
+     *            browser will not accept the new value.
+     * @since 8.0
+     */
+    public void pushState(URI uri) {
+        pushState(uri.toString());
+    }
+
+    /**
+     * Updates the browsers URI without causing actual page change in the same
+     * way as {@link #pushState(String)}, but does not add new entry to browsers
+     * history.
+     *
+     * @param uri
+     *            the URI to be used for replaceState operation. The URI is
+     *            resolved over the current location. If the given URI is
+     *            absolute, it must be of same origin as the current URI or the
+     *            browser will not accept the new value.
+     * @since 8.0
+     */
+    public void replaceState(String uri) {
+        newReplaceState = uri;
+        uI.markAsDirty();
+        location = location.resolve(uri);
+    }
+
+    /**
+     * Updates the browsers URI without causing actual page change in the same
+     * way as {@link #pushState(URI)}, but does not add new entry to browsers
+     * history.
+     *
+     * @param uri
+     *            the URI to be used for replaceState operation. The URI is
+     *            resolved over the current location. If the given URI is
+     *            absolute, it must be of same origin as the current URI or the
+     *            browser will not accept the new value.
+     * @since 8.0
+     */
+    public void replaceState(URI uri) {
+        replaceState(uri.toString());
+    }
+
     /**
      * For internal use only. Used to update the server-side location when the
      * client-side location changes.
@@ -943,7 +1128,7 @@ public class Page implements Serializable {
      */
     @Deprecated
     public void updateLocation(String location) {
-        updateLocation(location, true);
+        updateLocation(location, true, false);
     }
 
     /**
@@ -957,8 +1142,11 @@ public class Page implements Serializable {
      * @param fireEvents
      *            whether to fire {@link UriFragmentChangedEvent} if the URI
      *            fragment changes
+     * @param firePopstate
+     *            whether to fire {@link PopStateEvent}
      */
-    public void updateLocation(String location, boolean fireEvents) {
+    public void updateLocation(String location, boolean fireEvents,
+            boolean firePopstate) {
         try {
             String oldUriFragment = this.location.getFragment();
             this.location = new URI(location);
@@ -967,6 +1155,9 @@ public class Page implements Serializable {
                     && !SharedUtil.equals(oldUriFragment, newUriFragment)) {
                 fireEvent(new UriFragmentChangedEvent(this, newUriFragment));
             }
+            if (firePopstate) {
+                fireEvent(new PopStateEvent(this, location));
+            }
         } catch (URISyntaxException e) {
             throw new RuntimeException(e);
         }
index 437a1883615dc1bbc21a5b06b18c238f30422957..c0dabb37c082c6dce82feff69f11fc2c6d239d66 100644 (file)
@@ -193,6 +193,12 @@ public abstract class UI extends AbstractSingleComponentContainer
         public void acknowledge() {
             // Nothing to do, just need the message to be sent and processed
         }
+
+               @Override
+               public void popstate(String uri) {
+                       getPage().updateLocation(uri, true, true);
+                       
+               }
     };
     private DebugWindowServerRpc debugRpc = new DebugWindowServerRpc() {
         @Override
@@ -437,11 +443,6 @@ public abstract class UI extends AbstractSingleComponentContainer
             actionManager.handleActions(variables, this);
         }
 
-        if (variables.containsKey(UIConstants.LOCATION_VARIABLE)) {
-            String location = (String) variables
-                    .get(UIConstants.LOCATION_VARIABLE);
-            getPage().updateLocation(location, true);
-        }
     }
 
     /*
@@ -789,10 +790,10 @@ public abstract class UI extends AbstractSingleComponentContainer
         int newWidth = page.getBrowserWindowWidth();
         int newHeight = page.getBrowserWindowHeight();
 
-        page.updateLocation(oldLocation.toString(), false);
+        page.updateLocation(oldLocation.toString(), false, false);
         page.updateBrowserWindowSize(oldWidth, oldHeight, false);
 
-        page.updateLocation(newLocation.toString(), true);
+        page.updateLocation(newLocation.toString(), true, false);
         page.updateBrowserWindowSize(newWidth, newHeight, true);
     }
 
index 17d28cb5eb0d5542ffecabeaa45d357cc88c7114..086164e75095435b874afa4afa6520257e140e3d 100644 (file)
@@ -28,7 +28,10 @@ public class UIConstants implements Serializable {
     public static final String NOTIFICATION_HTML_CONTENT_NOT_ALLOWED = "useplain";
 
     @Deprecated
-    public static final String LOCATION_VARIABLE = "location";
+    public static final String ATTRIBUTE_PUSH_STATE = "ps";
+
+    @Deprecated
+    public static final String ATTRIBUTE_REPLACE_STATE = "rs";
 
     @Deprecated
     public static final String ATTRIBUTE_NOTIFICATION_STYLE = "style";
index e6c7beb3e74c85cfaab96b88c608f29ee619ba7d..15391dc023d94d2e7181d2d215490be2a4a73b2a 100644 (file)
@@ -35,6 +35,8 @@ public interface UIServerRpc extends ClickRpc, ServerRpc {
      * should always be called to ensure the message is flushed right away.
      */
     public void poll();
+    
+    public void popstate(String uri);
 
     @NoLoadingIndicator
     public void acknowledge();
diff --git a/uitest/src/main/java/com/vaadin/tests/components/ui/PushStateAndReplaceState.java b/uitest/src/main/java/com/vaadin/tests/components/ui/PushStateAndReplaceState.java
new file mode 100644 (file)
index 0000000..22788f3
--- /dev/null
@@ -0,0 +1,80 @@
+package com.vaadin.tests.components.ui;
+
+import java.net.URI;
+
+import com.vaadin.server.Page;
+import com.vaadin.server.Page.PopStateEvent;
+import com.vaadin.server.Page.PopStateListener;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractReindeerTestUI;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.CheckBox;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.Notification;
+
+public class PushStateAndReplaceState extends AbstractReindeerTestUI {
+
+    private final Label locationLabel = new Label();
+    private CheckBox replace;
+
+    @Override
+    protected void setup(VaadinRequest request) {
+        locationLabel.setId("locationLabel");
+        addComponent(locationLabel);
+        updateLabel();
+
+        getPage().addPopStateListener(new PopStateListener() {
+
+            @Override
+            public void uriChanged(PopStateEvent event) {
+                Notification.show("Popstate event");
+                updateLabel();
+            }
+        });
+
+        replace = new CheckBox("replace");
+        replace.setId("replace");
+        addComponent(replace);
+
+        addComponent(createButton("test", "Move to ./test",
+                Page.getCurrent().getLocation().toString() + "/test"));
+        addComponent(createButton("X", "Move to X", "X"));
+        addComponent(createButton("root_X", "Move to /X", "/X"));
+    }
+
+    private Button createButton(String id, String caption,
+            final String newUri) {
+        Button button = new Button(caption, new Button.ClickListener() {
+            @Override
+            public void buttonClick(ClickEvent event) {
+                if (replace.getValue()) {
+                    getPage().replaceState(newUri);
+                } else {
+                    getPage().pushState(newUri);
+                }
+                updateLabel();
+            }
+        });
+
+        button.setId(id);
+
+        return button;
+    }
+
+    private void updateLabel() {
+        URI location = getPage().getLocation();
+        locationLabel.setValue("Current Location: " + location.toString());
+    }
+
+    @Override
+    public String getTestDescription() {
+        return "Modern web framework shouldn't force you to use hashbang style urls for deep linking";
+    }
+
+    @Override
+    protected Integer getTicketNumber() {
+        return null;
+    }
+
+}
index 0d0fdc4b63f184db31817b3795e0c9cd1d2efc3c..61d77764393b2bbe4bc8c25d8103a8d3dc00933a 100644 (file)
@@ -1,5 +1,6 @@
 package com.vaadin.tests.components.ui;
 
+import com.vaadin.server.ExternalResource;
 import com.vaadin.server.Page;
 import com.vaadin.server.Page.UriFragmentChangedEvent;
 import com.vaadin.server.VaadinRequest;
@@ -7,6 +8,7 @@ import com.vaadin.tests.components.AbstractReindeerTestUI;
 import com.vaadin.ui.Button;
 import com.vaadin.ui.Button.ClickEvent;
 import com.vaadin.ui.Label;
+import com.vaadin.ui.Link;
 
 public class UriFragment extends AbstractReindeerTestUI {
 
@@ -29,6 +31,12 @@ public class UriFragment extends AbstractReindeerTestUI {
         addComponent(createButton("test", "Navigate to #test", "test"));
         addComponent(createButton("empty", "Navigate to #", ""));
         addComponent(createButton("null", "setUriFragment(null)", null));
+
+        Link link = new Link("Navigate to #linktest",
+                new ExternalResource("#linktest"));
+        link.setId("link");
+        addComponent(link);
+
     }
 
     private Button createButton(String id, String caption,
diff --git a/uitest/src/test/java/com/vaadin/tests/components/ui/PushStateAndReplaceStateTest.java b/uitest/src/test/java/com/vaadin/tests/components/ui/PushStateAndReplaceStateTest.java
new file mode 100644 (file)
index 0000000..a57cbf6
--- /dev/null
@@ -0,0 +1,69 @@
+package com.vaadin.tests.components.ui;
+
+import static org.junit.Assert.assertEquals;
+
+import java.net.URI;
+
+import org.junit.Test;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.support.ui.ExpectedCondition;
+
+import com.vaadin.testbench.By;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+public class PushStateAndReplaceStateTest extends MultiBrowserTest {
+
+    @Test
+    public void testUriFragment() throws Exception {
+        driver.get(getTestUrl());
+        assertUri(getTestUrl());
+
+        hitButton("test");
+
+        assertUri(getTestUrl() + "/test");
+
+        driver.navigate().back();
+
+        driver.findElement(By.className("v-Notification")).getText()
+        .contains("Popstate event");
+
+        assertUri(getTestUrl());
+
+        hitButton("test");
+        URI base = new URI(getTestUrl() + "/test");
+        hitButton("X");
+        URI current = base.resolve("X");
+        driver.findElement(By.xpath("//*[@id = 'replace']/input")).click();
+        hitButton("root_X");
+        current = current.resolve("/X");
+
+        assertUri(current.toString());
+
+        // Now that last change was with replace state, two back calls should go
+        // to initial
+        driver.navigate().back();
+        driver.navigate().back();
+
+        assertUri(getTestUrl());
+
+    }
+
+    private void assertUri(String uri) {
+        final String expectedText = "Current Location: " + uri;
+        waitUntil(new ExpectedCondition<Boolean>() {
+
+            @Override
+            public Boolean apply(WebDriver input) {
+                return expectedText.equals(getLocationLabelValue());
+            }
+        });
+
+        assertEquals(uri, driver.getCurrentUrl());
+    }
+
+    private String getLocationLabelValue() {
+        String text = vaadinElementById("locationLabel").getText();
+        return text;
+    }
+
+}
index e7fbf4f7ecfae697c0794b7e69ebed189f17aee0..731573b770d7d446417e6cffbf44e0f2d7e5dbf1 100644 (file)
@@ -7,6 +7,7 @@ import org.openqa.selenium.JavascriptExecutor;
 import org.openqa.selenium.WebDriver;
 import org.openqa.selenium.support.ui.ExpectedCondition;
 
+import com.vaadin.testbench.By;
 import com.vaadin.tests.tb3.MultiBrowserTest;
 
 public class UriFragmentTest extends MultiBrowserTest {
@@ -42,6 +43,12 @@ public class UriFragmentTest extends MultiBrowserTest {
         navigateToNull(); // Setting to null when there is a fragment actually
                           // sets it to #
         assertEquals("Current URI fragment:", getFragmentLabelValue());
+
+        // ensure IE works with new popstate based implementation, see
+        // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/3740423/
+        driver.findElement(By.xpath("//*[@id = 'link']/a")).click();
+        assertFragment("linktest");
+
     }
 
     private void assertFragment(String fragment) {
index 11848344ba51d5cb2ad06df1a3ba3095d930a4db..4507c12cfea137e7b7b55571cb71fd0a74a8c381 100644 (file)
@@ -17,15 +17,26 @@ package com.vaadin.tests.navigator;
 
 import static org.junit.Assert.assertEquals;
 
+import java.util.List;
+
 import org.junit.Test;
 import org.openqa.selenium.By;
 import org.openqa.selenium.WebElement;
+import org.openqa.selenium.remote.DesiredCapabilities;
 
 import com.vaadin.testbench.elements.ButtonElement;
 import com.vaadin.tests.tb3.MultiBrowserTest;
 
 public class NavigatorViewBlocksBackButtonActionTest extends MultiBrowserTest {
 
+    @Override
+    public List<DesiredCapabilities> getBrowsersToTest() {
+        // IE web driver fails to read fragment properly, these must be tested
+        // manually. See
+        // https://github.com/SeleniumHQ/selenium-google-code-issue-archive/issues/7966
+        return getBrowsersExcludingIE();
+    }
+
     @Test
     public void testIfConfirmBack() {
         openTestURL();
index 4da3ae15416f63aa6a006ce7fd99dbe98c917a36..13b18de5167fb03add3b84d0fa003c7ac10c5257 100644 (file)
@@ -18,9 +18,12 @@ package com.vaadin.tests.urifragments;
 import static com.vaadin.tests.urifragments.FragmentHandlingAndAsynchUIUpdate.FRAG_NAME_TPL;
 import static com.vaadin.tests.urifragments.FragmentHandlingAndAsynchUIUpdate.START_FRAG_ID;
 
+import java.util.List;
+
 import org.junit.Test;
 import org.openqa.selenium.JavascriptExecutor;
 import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.remote.DesiredCapabilities;
 import org.openqa.selenium.support.ui.ExpectedCondition;
 
 import com.vaadin.testbench.By;
@@ -33,6 +36,14 @@ import com.vaadin.tests.tb3.MultiBrowserTest;
  */
 public class FragmentHandlingAndAsynchUIUpdateTest extends MultiBrowserTest {
 
+    @Override
+    public List<DesiredCapabilities> getBrowsersToTest() {
+        // IE web driver fails to read fragment properly, these must be tested
+        // manually. See
+        // https://github.com/SeleniumHQ/selenium-google-code-issue-archive/issues/7966
+        return getBrowsersExcludingIE();
+    }
+
     /**
      * The case when we successively set 10 fragments, go back 9 times and then
      * go forward 9 times
index db700b4393190e626962b9406859a704eac37480..f728be937ad284ed6087f5afa5c449888f081447 100644 (file)
  */
 package com.vaadin.tests.urifragments;
 
+import java.util.List;
+
 import org.junit.Test;
 import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.remote.DesiredCapabilities;
 import org.openqa.selenium.support.ui.ExpectedCondition;
 
 import com.vaadin.tests.tb3.MultiBrowserTest;
@@ -29,6 +32,14 @@ import com.vaadin.tests.tb3.MultiBrowserTest;
  */
 public class SettingNullFragmentTest extends MultiBrowserTest {
 
+    @Override
+    public List<DesiredCapabilities> getBrowsersToTest() {
+        // IE web driver fails to read fragment properly, these must be tested
+        // manually. See
+        // https://github.com/SeleniumHQ/selenium-google-code-issue-archive/issues/7966
+        return getBrowsersExcludingIE();
+    }
+
     @Test
     public void testSettingNullURIFragment() throws Exception {
         openTestURL();