]> source.dussan.org Git - vaadin-framework.git/commitdiff
Add View.beforeLeave to support delayed navigation
authorArtur <artur@vaadin.com>
Tue, 20 Jun 2017 03:20:17 +0000 (06:20 +0300)
committerIlia Motornyi <elmot@vaadin.com>
Tue, 20 Jun 2017 03:20:17 +0000 (06:20 +0300)
server/src/main/java/com/vaadin/navigator/Navigator.java
server/src/main/java/com/vaadin/navigator/View.java
server/src/main/java/com/vaadin/navigator/ViewBeforeLeaveEvent.java [new file with mode: 0644]
server/src/main/java/com/vaadin/navigator/ViewLeaveAction.java [new file with mode: 0644]
server/src/test/java/com/vaadin/tests/server/navigator/NavigatorTest.java
uitest/src/main/java/com/vaadin/tests/navigator/DelayedViewLeaveConfirmation.java [new file with mode: 0644]
uitest/src/test/java/com/vaadin/tests/navigator/DelayedViewLeaveConfirmationTest.java [new file with mode: 0644]

index da5dfa0f87f43c4aef76dc0227bf28fd7aa77344..23d6cc5f913536b75970e81ecf9bc9f3fe092143 100644 (file)
@@ -594,6 +594,67 @@ public class Navigator implements Serializable {
      *            parameters passed in the navigation state to the view
      */
     protected void navigateTo(View view, String viewName, String parameters) {
+        runAfterLeaveConfirmation(() -> {
+            performNavigateTo(view, viewName, parameters);
+        });
+
+    }
+
+    /**
+     * Triggers {@link View#beforeLeave(ViewBeforeLeaveEvent)} for the current
+     * view with the given action.
+     * <p>
+     * This method is typically called by
+     * {@link #navigateTo(View, String, String)} but can be called from
+     * application code when you want to e.g. show a confirmation dialog before
+     * perfoming an action which is not a navigation but which would cause the
+     * view to be hidden, e.g. logging out.
+     * <p>
+     * Note that this method will not trigger any {@link ViewChangeListener}s as
+     * it does not navigate to a new view. Use {@link #navigateTo(String)} to
+     * change views and trigger all listeners.
+     *
+     * @param action
+     *            the action to execute when the view confirms it is ok to leave
+     * @since 8.1
+     */
+    public void runAfterLeaveConfirmation(ViewLeaveAction action) {
+        View currentView = getCurrentView();
+        if (currentView == null) {
+            action.run();
+        } else {
+            ViewBeforeLeaveEvent beforeLeaveEvent = new ViewBeforeLeaveEvent(
+                    this, action);
+            currentView.beforeLeave(beforeLeaveEvent);
+            if (!beforeLeaveEvent.isNavigateRun()) {
+                // The event handler prevented navigation
+                // Revert URL to previous state in case the navigation was
+                // caused by the back-button
+                revertNavigation();
+            }
+        }
+    }
+
+    /**
+     * Internal method for activating a view, setting its parameters and calling
+     * listeners.
+     * <p>
+     * Invoked after the current view has confirmed that leaving is ok.
+     * <p>
+     * This method also verifies that the user is allowed to perform the
+     * navigation operation.
+     *
+     * @param view
+     *            view to activate
+     * @param viewName
+     *            (optional) name of the view or null not to change the
+     *            navigation state
+     * @param parameters
+     *            parameters passed in the navigation state to the view
+     * @since 8.1
+     */
+    protected void performNavigateTo(View view, String viewName,
+            String parameters) {
         ViewChangeEvent event = new ViewChangeEvent(this, currentView, view,
                 viewName, parameters);
         boolean navigationAllowed = beforeViewChange(event);
@@ -1112,4 +1173,5 @@ public class Navigator implements Serializable {
         stateManager.setNavigator(null);
         ui.setNavigator(null);
     }
+
 }
index 623c8ed6e939872177023723b46cee0779436e39..6e87c6e45521e534428a03350097427ebce0d871 100644 (file)
@@ -34,9 +34,8 @@ import com.vaadin.ui.Component;
 public interface View extends Serializable {
 
     /**
-     * This view is navigated to.
-     *
-     * This method is always called before the view is shown on screen.
+     * Called before the view is shown on screen.
+     * <p>
      * {@link ViewChangeEvent#getParameters() event.getParameters()} may contain
      * extra parameters relevant to the view.
      *
@@ -48,6 +47,34 @@ public interface View extends Serializable {
      */
     public void enter(ViewChangeEvent event);
 
+    /**
+     * Called when the user is requesting navigation away from the view.
+     * <p>
+     * This method allows the view to accept or prevent navigation away from the
+     * view or optionally delay navigation away until a later stage. For
+     * navigation to take place, the {@link ViewBeforeLeaveEvent#navigate()}
+     * method must be called either directly when handling this event or later
+     * to perform delayed navigation.
+     * <p>
+     * The default implementation calls {@link ViewBeforeLeaveEvent#navigate()}
+     * directly. If you override this and do nothing, the user will never be
+     * able to leave the view.
+     * <p>
+     * This method is triggered before any methods in any added
+     * {@link ViewChangeListener ViewChangeListeners}. Whenever you call
+     * {@link ViewBeforeLeaveEvent#navigate()}, any {@link ViewChangeListener}s
+     * will be triggered. They will be handled normally and might also prevent
+     * navigation.
+     *
+     * @param event
+     *            an event object providing information about the event and
+     *            containing the {@link ViewBeforeLeaveEvent#navigate()} method
+     *            needed to perform navigation
+     */
+    public default void beforeLeave(ViewBeforeLeaveEvent event) {
+        event.navigate();
+    }
+
     /**
      * Gets the component to show when navigating to the view.
      *
diff --git a/server/src/main/java/com/vaadin/navigator/ViewBeforeLeaveEvent.java b/server/src/main/java/com/vaadin/navigator/ViewBeforeLeaveEvent.java
new file mode 100644 (file)
index 0000000..2f7fcbe
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2000-2016 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.navigator;
+
+import java.util.EventObject;
+
+/**
+ * Event sent to the View instance before navigating away from it.
+ * <p>
+ * Provides a {@link #navigate()} method which must be called for the navigation
+ * to take place.
+ *
+ * @since 8.1
+ */
+public class ViewBeforeLeaveEvent extends EventObject {
+
+    private ViewLeaveAction action;
+    private boolean navigateRun = false;
+
+    /**
+     * Creates a new event instance for the given navigator.
+     *
+     * @param navigator
+     *            the navigator instance
+     * @param action
+     *            the command to execute when calling {@link #navigate()}
+     */
+    public ViewBeforeLeaveEvent(Navigator navigator, ViewLeaveAction action) {
+        super(navigator);
+        this.action = action;
+    }
+
+    /**
+     * Performs the navigation which triggered the event in the first place.
+     */
+    public void navigate() {
+        if (navigateRun) {
+            throw new IllegalStateException(
+                    "navigate() can only be called once");
+        }
+        action.run();
+        navigateRun = true;
+    }
+
+    /**
+     * Checks if the navigate command has been executed.
+     *
+     * @return <code>true</code> if {@link #navigate()} has been called,
+     *         <code>false</code> otherwise
+     */
+    protected boolean isNavigateRun() {
+        return navigateRun;
+    }
+}
diff --git a/server/src/main/java/com/vaadin/navigator/ViewLeaveAction.java b/server/src/main/java/com/vaadin/navigator/ViewLeaveAction.java
new file mode 100644 (file)
index 0000000..945d13b
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2000-2016 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.navigator;
+
+import java.io.Serializable;
+
+/**
+ * An action to execute when navigating away from a view.
+ *
+ * @since 8.1
+ */
+@FunctionalInterface
+public interface ViewLeaveAction extends Serializable {
+    /**
+     * Executes the action.
+     */
+    public void run();
+}
index fbc55d57cce850e1da2b8c1beca8af06c90fe33b..31e1c53ec6c6c16939b0e8efc27c4111ae83e8dc 100644 (file)
@@ -25,6 +25,7 @@ import static org.junit.Assert.fail;
 import java.util.LinkedList;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
 import org.easymock.EasyMock;
@@ -36,6 +37,7 @@ import org.junit.Test;
 import com.vaadin.navigator.NavigationStateManager;
 import com.vaadin.navigator.Navigator;
 import com.vaadin.navigator.View;
+import com.vaadin.navigator.ViewBeforeLeaveEvent;
 import com.vaadin.navigator.ViewChangeListener;
 import com.vaadin.navigator.ViewChangeListener.ViewChangeEvent;
 import com.vaadin.navigator.ViewDisplay;
@@ -44,7 +46,6 @@ import com.vaadin.server.Page;
 import com.vaadin.server.VaadinRequest;
 import com.vaadin.shared.Registration;
 import com.vaadin.shared.ui.ui.PageState;
-import com.vaadin.tests.server.navigator.ClassBasedViewProviderTest.TestView;
 import com.vaadin.tests.server.navigator.ClassBasedViewProviderTest.TestView2;
 import com.vaadin.ui.Component;
 import com.vaadin.ui.HorizontalLayout;
@@ -336,8 +337,8 @@ public class NavigatorTest {
                 .createMock(NavigationStateManager.class);
         ViewDisplay display = control.createMock(ViewDisplay.class);
         ViewProvider provider = control.createMock(ViewProvider.class);
-        View view1 = control.createMock(View.class);
-        View view2 = control.createMock(View.class);
+        TestView view1 = new TestView();
+        TestView view2 = new TestView();
 
         // prepare mocks: what to expect
         manager.setNavigator(EasyMock.anyObject(Navigator.class));
@@ -346,7 +347,6 @@ public class NavigatorTest {
                 .times(2);
         EasyMock.expect(provider.getView("test1")).andReturn(view1);
         EasyMock.expect(manager.getState()).andReturn("");
-        view1.enter(eventParametersEqual(""));
         display.showView(view1);
         manager.setState("test1");
         EasyMock.expect(manager.getState()).andReturn("test1");
@@ -355,7 +355,6 @@ public class NavigatorTest {
                 .times(2);
         EasyMock.expect(provider.getView("test2")).andReturn(view2);
         EasyMock.expect(manager.getState()).andReturn("test1");
-        view2.enter(eventParametersEqual(""));
         display.showView(view2);
         manager.setState("test2");
         EasyMock.expect(manager.getState()).andReturn("test2");
@@ -364,7 +363,6 @@ public class NavigatorTest {
                 .times(2);
         EasyMock.expect(provider.getView("test1")).andReturn(view1);
         EasyMock.expect(manager.getState()).andReturn("test2");
-        view1.enter(eventParametersEqual("params"));
         display.showView(view1);
         manager.setState("test1/params");
         EasyMock.expect(manager.getState()).andReturn("test1/params");
@@ -377,12 +375,27 @@ public class NavigatorTest {
 
         navigator.navigateTo("test1");
         assertEquals("test1", navigator.getState());
-
+        assertEquals("", view1.getParams());
         navigator.navigateTo("test2/");
         assertEquals("test2", navigator.getState());
+        assertEquals("", view2.getParams());
 
         navigator.navigateTo("test1/params");
         assertEquals("test1/params", navigator.getState());
+        assertEquals("params", view1.getParams());
+    }
+
+    public static class TestView implements View {
+        private String params;
+
+        @Override
+        public void enter(ViewChangeEvent event) {
+            params = event.getParameters();
+        }
+
+        public String getParams() {
+            return params;
+        }
     }
 
     @Test
@@ -392,8 +405,8 @@ public class NavigatorTest {
                 .createMock(NavigationStateManager.class);
         ViewDisplay display = control.createMock(ViewDisplay.class);
         ViewProvider provider = control.createMock(ViewProvider.class);
-        View view1 = control.createMock(View.class);
-        View view2 = control.createMock(View.class);
+        TestView view1 = new TestView();
+        TestView view2 = new TestView();
 
         // prepare mocks: what to expect
         manager.setNavigator(EasyMock.anyObject(Navigator.class));
@@ -402,14 +415,12 @@ public class NavigatorTest {
                 .times(2);
         EasyMock.expect(provider.getView("test2")).andReturn(view2);
         EasyMock.expect(manager.getState()).andReturn("view1");
-        view2.enter(eventParametersEqual(""));
         display.showView(view2);
         manager.setState("test2");
 
         EasyMock.expect(provider.getViewName("")).andReturn("test1").times(2);
         EasyMock.expect(provider.getView("test1")).andReturn(view1);
         EasyMock.expect(manager.getState()).andReturn("");
-        view1.enter(eventParametersEqual(""));
         display.showView(view1);
         manager.setState("test1");
 
@@ -417,7 +428,6 @@ public class NavigatorTest {
                 .times(2);
         EasyMock.expect(provider.getView("test1")).andReturn(view1);
         EasyMock.expect(manager.getState()).andReturn("test2");
-        view1.enter(eventParametersEqual("params"));
         display.showView(view1);
         manager.setState("test1/params");
 
@@ -428,8 +438,12 @@ public class NavigatorTest {
         navigator.addProvider(provider);
 
         navigator.navigateTo("test2");
+        Assert.assertEquals("", view2.getParams());
+        Assert.assertEquals(null, view1.getParams());
         navigator.navigateTo("");
+        Assert.assertEquals("", view1.getParams());
         navigator.navigateTo("test1/params");
+        Assert.assertEquals("params", view1.getParams());
     }
 
     @Test
@@ -439,8 +453,8 @@ public class NavigatorTest {
                 .createMock(NavigationStateManager.class);
         ViewDisplay display = control.createMock(ViewDisplay.class);
         ViewProvider provider = control.createMock(ViewProvider.class);
-        View view1 = control.createMock(View.class);
-        View view2 = control.createMock(View.class);
+        TestView view1 = new TestView();
+        TestView view2 = new TestView();
         ViewChangeTestListener listener = new ViewChangeTestListener();
 
         // create navigator to test
@@ -454,7 +468,6 @@ public class NavigatorTest {
                 "test1", "");
         listener.addExpectedIsViewChangeAllowed(event1, true);
         EasyMock.expect(manager.getState()).andReturn("");
-        view1.enter(eventParametersEqual(""));
         display.showView(view1);
         manager.setState("test1");
         listener.addExpectedNavigatorViewChange(event1);
@@ -466,7 +479,6 @@ public class NavigatorTest {
                 "test2", "");
         listener.addExpectedIsViewChangeAllowed(event2, true);
         EasyMock.expect(manager.getState()).andReturn("test1");
-        view2.enter(eventParametersEqual(""));
         display.showView(view2);
         manager.setState("test2");
         listener.addExpectedNavigatorViewChange(event2);
@@ -524,8 +536,8 @@ public class NavigatorTest {
                 .createMock(NavigationStateManager.class);
         ViewDisplay display = control.createMock(ViewDisplay.class);
         ViewProvider provider = control.createMock(ViewProvider.class);
-        View view1 = control.createMock(View.class);
-        View view2 = control.createMock(View.class);
+        TestView view1 = new TestView();
+        TestView view2 = new TestView();
         ViewChangeTestListener listener1 = new ViewChangeTestListener();
         ViewChangeTestListener listener2 = new ViewChangeTestListener();
 
@@ -560,7 +572,6 @@ public class NavigatorTest {
                 "test1", "bar");
         listener1.addExpectedIsViewChangeAllowed(event3, true);
         listener2.addExpectedIsViewChangeAllowed(event3, true);
-        view1.enter(EasyMock.isA(ViewChangeEvent.class));
         display.showView(view1);
         manager.setState("test1/bar");
         listener1.addExpectedNavigatorViewChange(event3);
@@ -575,7 +586,6 @@ public class NavigatorTest {
                 "test2", "");
         listener1.addExpectedIsViewChangeAllowed(event4, true);
         listener2.addExpectedIsViewChangeAllowed(event4, true);
-        view2.enter(EasyMock.isA(ViewChangeEvent.class));
         display.showView(view2);
         manager.setState("test2");
         listener1.addExpectedNavigatorViewChange(event4);
@@ -803,9 +813,7 @@ public class NavigatorTest {
     public void testNavigateToUnknownView() {
         TestNavigator navigator = new TestNavigator();
 
-        View errorView = EasyMock.createMock(View.class);
-        errorView.enter(EasyMock.anyObject(ViewChangeEvent.class));
-        EasyMock.replay(errorView);
+        TestView errorView = new TestView();
 
         try {
             navigator.navigateTo("doesnotexist");
@@ -816,16 +824,11 @@ public class NavigatorTest {
         navigator.setErrorView(errorView);
         navigator.navigateTo("doesnotexist");
 
-        View testView = EasyMock.createMock(View.class);
-        testView.enter(EasyMock.anyObject(ViewChangeEvent.class));
-        EasyMock.replay(testView);
+        TestView testView = new TestView();
 
         navigator.addView("doesnotexist", testView);
         navigator.navigateTo("doesnotexist");
-
-        View errorView2 = EasyMock.createMock(View.class);
-        errorView2.enter(EasyMock.anyObject(ViewChangeEvent.class));
-        EasyMock.replay(errorView2);
+        TestView errorView2 = new TestView();
 
         ViewProvider errorProvider = EasyMock.createMock(ViewProvider.class);
         EasyMock.expect(errorProvider.getView("doesnotexist2"))
@@ -1064,34 +1067,11 @@ public class NavigatorTest {
 
     @Test
     public void parameterMapFromViewChangeEvent() {
-        // create navigator to test
         Navigator navigator = createNavigatorWithState("foo");
-        View view1 = EasyMock.createMock(View.class);
-        View view2 = EasyMock.createMock(View.class);
-        ViewProvider provider = new ViewProvider() {
-
-            @Override
-            public String getViewName(String viewAndParameters) {
-                if (viewAndParameters.contains("/")) {
-                    return viewAndParameters.substring(0,
-                            viewAndParameters.indexOf('/'));
-                } else {
-                    return viewAndParameters;
-                }
-            }
-
-            @Override
-            public View getView(String viewName) {
-                if (viewName.equals("view1")) {
-                    return view1;
-                } else if (viewName.equals("view2")) {
-                    return view2;
-                } else {
-                    return null;
-                }
-            }
-        };
-        navigator.addProvider(provider);
+        TestView view1 = new TestView();
+        TestView view2 = new TestView();
+        navigator.addView("view1", view1);
+        navigator.addView("view2", view2);
 
         AtomicReference<Map<String, String>> mapRef = new AtomicReference<>();
         AtomicReference<Map<String, String>> mapRefB = new AtomicReference<>();
@@ -1114,4 +1094,109 @@ public class NavigatorTest {
                 entry("d", ""));
         assertMap(mapRefB.get(), entry("a&", ""), entry("", "c&d"));
     }
+
+    @Test
+    public void view_beforeLeave_preventNavigation() {
+        Navigator navigator = createNavigatorWithState("foo");
+        View view1 = new View() {
+
+            @Override
+            public void enter(ViewChangeEvent event) {
+            }
+
+            @Override
+            public void beforeLeave(ViewBeforeLeaveEvent event) {
+                // Leaving this empty means the user can never leave
+            }
+
+        };
+        View view2 = EasyMock.createMock(View.class);
+        navigator.addView("view1", view1);
+        navigator.addView("view2", view2);
+        navigator.navigateTo("view1");
+        navigator.navigateTo("view2");
+        Assert.assertEquals("view1", navigator.getState());
+    }
+
+    @Test
+    public void view_beforeLeave_allowNavigation() {
+        Navigator navigator = createNavigatorWithState("foo");
+        View view1 = new View() {
+
+            @Override
+            public void enter(ViewChangeEvent event) {
+            }
+
+            @Override
+            public void beforeLeave(ViewBeforeLeaveEvent event) {
+                event.navigate();
+            }
+
+        };
+        View view2 = EasyMock.createMock(View.class);
+        navigator.addView("view1", view1);
+        navigator.addView("view2", view2);
+        navigator.navigateTo("view1");
+        navigator.navigateTo("view2");
+        Assert.assertEquals("view2", navigator.getState());
+
+    }
+
+    @Test
+    public void view_beforeLeave_delayNavigation() {
+        Navigator navigator = createNavigatorWithState("foo");
+        AtomicReference<ViewBeforeLeaveEvent> eventRef = new AtomicReference<ViewBeforeLeaveEvent>();
+        View view1 = new View() {
+
+            @Override
+            public void enter(ViewChangeEvent event) {
+            }
+
+            @Override
+            public void beforeLeave(ViewBeforeLeaveEvent event) {
+                eventRef.set(event);
+            }
+
+        };
+        View view2 = EasyMock.createMock(View.class);
+        navigator.addView("view1", view1);
+        navigator.addView("view2", view2);
+        navigator.navigateTo("view1");
+        navigator.navigateTo("view2");
+        Assert.assertEquals("view1", navigator.getState());
+        eventRef.get().navigate();
+        Assert.assertEquals("view2", navigator.getState());
+
+    }
+
+    @Test
+    public void navigator_invokeBeforeLeaveManually() {
+        Navigator navigator = createNavigatorWithState("foo");
+        AtomicReference<ViewBeforeLeaveEvent> eventRef = new AtomicReference<ViewBeforeLeaveEvent>();
+        View view1 = new View() {
+
+            @Override
+            public void enter(ViewChangeEvent event) {
+            }
+
+            @Override
+            public void beforeLeave(ViewBeforeLeaveEvent event) {
+                eventRef.set(event);
+            }
+
+        };
+        TestView view2 = new TestView();
+        navigator.addView("view1", view1);
+        navigator.addView("view2", view2);
+        navigator.navigateTo("view1");
+
+        AtomicInteger leaveCount = new AtomicInteger(0);
+        navigator.runAfterLeaveConfirmation(() -> {
+            leaveCount.incrementAndGet();
+        });
+        Assert.assertEquals(0, leaveCount.get());
+        eventRef.get().navigate();
+        Assert.assertEquals(1, leaveCount.get());
+        Assert.assertEquals("view1", navigator.getState());
+    }
 }
diff --git a/uitest/src/main/java/com/vaadin/tests/navigator/DelayedViewLeaveConfirmation.java b/uitest/src/main/java/com/vaadin/tests/navigator/DelayedViewLeaveConfirmation.java
new file mode 100644 (file)
index 0000000..adfd48c
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2000-2016 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.navigator;
+
+import com.vaadin.navigator.Navigator;
+import com.vaadin.navigator.View;
+import com.vaadin.navigator.ViewBeforeLeaveEvent;
+import com.vaadin.navigator.ViewChangeListener.ViewChangeEvent;
+import com.vaadin.navigator.ViewLeaveAction;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.HorizontalLayout;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.TextField;
+import com.vaadin.ui.VerticalLayout;
+import com.vaadin.ui.Window;
+
+public class DelayedViewLeaveConfirmation extends AbstractTestUI {
+
+    public static class OtherView extends VerticalLayout implements View {
+        public OtherView() {
+            addComponent(new Label("Just another view"));
+        }
+
+        @Override
+        public void enter(ViewChangeEvent event) {
+
+        }
+    }
+
+    public static class MainView extends VerticalLayout implements View {
+        private Label saved;
+        private TextField input;
+
+        public MainView() {
+            saved = new Label("Initial");
+            saved.setCaption("Saved value");
+            input = new TextField("Enter a value");
+            input.setId("input");
+            Button navigateAway = new Button("Navigate to the other view",
+                    e -> {
+                        getUI().getNavigator().navigateTo("other");
+                    });
+            Button logout = new Button("Simulate logout", e -> {
+                getUI().getNavigator().runAfterLeaveConfirmation(() -> {
+                    removeAllComponents();
+                    addComponent(new Label("You have been logged out"));
+                    getUI().getPage().setUriFragment("", false);
+                });
+            });
+            navigateAway.setId("navigateAway");
+            logout.setId("logout");
+            addComponents(saved, input, navigateAway, logout);
+        }
+
+        @Override
+        public void enter(ViewChangeEvent event) {
+            input.setValue(saved.getValue());
+        }
+
+        @Override
+        public void beforeLeave(ViewBeforeLeaveEvent event) {
+            boolean hasChanges = !(saved.getValue().equals(input.getValue()));
+            if (hasChanges) {
+                getUI().addWindow(new ConfirmationWindow(event::navigate));
+            } else {
+                event.navigate();
+            }
+        }
+
+    }
+
+    public static class ConfirmationWindow extends Window {
+        public ConfirmationWindow(ViewLeaveAction action) {
+            super();
+            VerticalLayout layout = new VerticalLayout();
+            layout.addComponent(new Label(
+                    "You have unsaved changes. Are you sure you want to leave?"));
+            Button leave = new Button("YES, LEAVE!", e -> {
+                close();
+                action.run();
+            });
+            leave.setId("leave");
+            Button stay = new Button("NO, STAY!", e -> {
+                close();
+            });
+            stay.setId("stay");
+            layout.addComponents(new HorizontalLayout(leave, stay));
+            setContent(layout);
+        }
+    }
+
+    @Override
+    protected void setup(VaadinRequest request) {
+        setNavigator(new Navigator(this, this));
+        getNavigator().addView("main", MainView.class);
+        getNavigator().addView("other", OtherView.class);
+    }
+
+}
diff --git a/uitest/src/test/java/com/vaadin/tests/navigator/DelayedViewLeaveConfirmationTest.java b/uitest/src/test/java/com/vaadin/tests/navigator/DelayedViewLeaveConfirmationTest.java
new file mode 100644 (file)
index 0000000..b3f7053
--- /dev/null
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2000-2016 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.navigator;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.vaadin.testbench.elements.ButtonElement;
+import com.vaadin.testbench.elements.LabelElement;
+import com.vaadin.testbench.elements.TextFieldElement;
+import com.vaadin.testbench.elements.WindowElement;
+import com.vaadin.tests.tb3.SingleBrowserTest;
+
+public class DelayedViewLeaveConfirmationTest extends SingleBrowserTest {
+
+    @Test
+    public void navigateAwayWithoutChanges() {
+        openMainView();
+        navigateToOtherView();
+        assertOnOtherView();
+    }
+
+    @Test
+    public void cancelNavigateAwayWithChanges() {
+        openMainView();
+        updateValue();
+        navigateToOtherView();
+        assertOnMainView();
+        chooseToStay();
+        assertOnMainView();
+    }
+
+    @Test
+    public void confirmNavigateAwayWithChanges() {
+        openMainView();
+        updateValue();
+        navigateToOtherView();
+        assertOnMainView();
+        chooseToLeave();
+        assertOnOtherView();
+    }
+
+    @Test
+    public void confirmLogoutWithChanges() {
+        openMainView();
+        updateValue();
+        logout();
+        assertOnMainView();
+        chooseToLeave();
+        assertLoggedOut();
+    }
+
+    @Test
+    public void cancelLogoutWithChanges() {
+        openMainView();
+        updateValue();
+        logout();
+        assertOnMainView();
+        chooseToStay();
+        assertOnMainView();
+    }
+
+    @Test
+    public void logoutWithoutChanges() {
+        openMainView();
+        getLogout().click();
+        assertLoggedOut();
+
+    }
+
+    private void openMainView() {
+        String url = getTestURL(DelayedViewLeaveConfirmation.class);
+        url += "#!main";
+
+        driver.get(url);
+    }
+
+    private void navigateToOtherView() {
+        getNavigateAway().click();
+    }
+
+    private void logout() {
+        getLogout().click();
+    }
+
+    private void assertOnOtherView() {
+        Assert.assertEquals("Just another view",
+                $(LabelElement.class).first().getText());
+    }
+
+    private void assertOnMainView() {
+        Assert.assertEquals("Saved value",
+                $(LabelElement.class).first().getCaption());
+    }
+
+    private void assertLoggedOut() {
+        Assert.assertEquals("You have been logged out",
+                $(LabelElement.class).first().getText());
+    }
+
+    private void chooseToStay() {
+        $(WindowElement.class).first().$(ButtonElement.class).id("stay")
+                .click();
+    }
+
+    private void chooseToLeave() {
+        $(WindowElement.class).first().$(ButtonElement.class).id("leave")
+                .click();
+    }
+
+    private void updateValue() {
+        TextFieldElement input = $(TextFieldElement.class).id("input");
+        input.setValue(input.getValue() + "-upd");
+    }
+
+    private ButtonElement getNavigateAway() {
+        return $(ButtonElement.class).id("navigateAway");
+    }
+
+    private ButtonElement getLogout() {
+        return $(ButtonElement.class).id("logout");
+    }
+}