diff --git a/flow-client/src/main/frontend/Flow.ts b/flow-client/src/main/frontend/Flow.ts index d08fa5f544e..30ae84eb881 100644 --- a/flow-client/src/main/frontend/Flow.ts +++ b/flow-client/src/main/frontend/Flow.ts @@ -217,7 +217,8 @@ export class Flow { this.container.localName, this.container.id, this.getFlowRoute(ctx), - this.appShellTitle + this.appShellTitle, + history.state ); }); } else { diff --git a/flow-server/src/main/java/com/vaadin/flow/component/internal/JavaScriptBootstrapUI.java b/flow-server/src/main/java/com/vaadin/flow/component/internal/JavaScriptBootstrapUI.java index 121f09b34db..d5daf6a3463 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/internal/JavaScriptBootstrapUI.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/internal/JavaScriptBootstrapUI.java @@ -26,6 +26,7 @@ import com.vaadin.flow.component.HasElement; import com.vaadin.flow.component.Tag; import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.page.History; import com.vaadin.flow.dom.Element; import com.vaadin.flow.internal.nodefeature.NodeProperties; import com.vaadin.flow.router.ErrorNavigationEvent; @@ -43,10 +44,12 @@ import com.vaadin.flow.router.internal.PathUtil; import com.vaadin.flow.server.communication.JavaScriptBootstrapHandler; +import elemental.json.JsonValue; + /** * Custom UI for {@link JavaScriptBootstrapHandler}. This class is intended for * internal use in client side bootstrapping. - * + * *

* For internal use only. May be renamed or removed in a future release. */ @@ -105,13 +108,25 @@ public String getForwardToClientUrl() { * client side element id * @param flowRoute * flow route that should be attached to the client element + * @param appShellTitle + * client side title of the application shell + * @param historyState + * client side history state value */ @ClientCallable public void connectClient(String clientElementTag, String clientElementId, - String flowRoute, String appShellTitle) { + String flowRoute, String appShellTitle, JsonValue historyState) { if (appShellTitle != null && !appShellTitle.isEmpty()) { getInternals().setAppShellTitle(appShellTitle); } + + final String trimmedRoute = PathUtil.trimPath(flowRoute); + if (!trimmedRoute.equals(flowRoute)) { + // See InternalRedirectHandler invoked via Router. + getPage().getHistory().replaceState(null, trimmedRoute); + } + final Location location = new Location(trimmedRoute); + if (wrapperElement == null) { // Create flow reference for the client outlet element wrapperElement = new Element(clientElementTag); @@ -120,23 +135,25 @@ public void connectClient(String clientElementTag, String clientElementId, getElement().getStateProvider().appendVirtualChild( getElement().getNode(), wrapperElement, NodeProperties.INJECT_BY_ID, clientElementId); - } - final String trimmedRoute = PathUtil.trimPath(flowRoute); - if (!trimmedRoute.equals(flowRoute)) { - // See InternalRedirectHandler invoked via Router. - getPage().getHistory().replaceState(null, trimmedRoute); - } + getPage().getHistory().setHistoryStateChangeHandler( + event -> renderViewForRoute(event.getLocation(), + NavigationTrigger.CLIENT_SIDE)); - // Render the flow view that the user wants to navigate to. - renderViewForRoute(new Location(trimmedRoute), - NavigationTrigger.CLIENT_SIDE); + // Render the flow view that the user wants to navigate to. + renderViewForRoute(location, NavigationTrigger.CLIENT_SIDE); + } else { + History.HistoryStateChangeHandler handler = getPage().getHistory() + .getHistoryStateChangeHandler(); + handler.onHistoryStateChange(new History.HistoryStateChangeEvent( + getPage().getHistory(), historyState, location, + NavigationTrigger.CLIENT_SIDE)); + } // true if the target is client-view and the push mode is disable if (getForwardToClientUrl() != null) { navigateToClient(getForwardToClientUrl()); acknowledgeClient(); - } else if (isPostponed()) { cancelClient(); } else { @@ -181,41 +198,41 @@ public void navigate(String pathname, QueryParameters queryParameters) { if (Boolean.TRUE.equals(getSession().getAttribute(SERVER_ROUTING))) { // server-side routing renderViewForRoute(location, NavigationTrigger.UI_NAVIGATE); - } else { - // client-side routing + return; + } - // There is an in-progress navigation or there are no changes, - // prevent looping - if (navigationInProgress || getInternals().hasLastHandledLocation() - && sameLocation(getInternals().getLastHandledLocation(), - location)) { - return; - } + // client-side routing + + // There is an in-progress navigation or there are no changes, + // prevent looping + if (navigationInProgress + || getInternals().hasLastHandledLocation() && sameLocation( + getInternals().getLastHandledLocation(), location)) { + return; + } - navigationInProgress = true; - try { - Optional navigationState = getInternals() - .getRouter().resolveNavigationTarget(location); - - if (navigationState.isPresent()) { - // Navigation can be done in server side without extra - // round-trip - handleNavigation(location, navigationState.get(), - NavigationTrigger.UI_NAVIGATE); - if (getForwardToClientUrl() != null) { - // Server is forwarding to a client route from a - // BeforeEnter. - navigateToClient(getForwardToClientUrl()); - } - } else { - // Server cannot resolve navigation, let client-side to - // handle it. - navigateToClient(location.getPathWithQueryParameters()); + navigationInProgress = true; + try { + Optional navigationState = getInternals() + .getRouter().resolveNavigationTarget(location); + + if (navigationState.isPresent()) { + // Navigation can be done in server side without extra + // round-trip + handleNavigation(location, navigationState.get(), + NavigationTrigger.UI_NAVIGATE); + if (getForwardToClientUrl() != null) { + // Server is forwarding to a client route from a + // BeforeEnter. + navigateToClient(getForwardToClientUrl()); } - } finally { - navigationInProgress = false; + } else { + // Server cannot resolve navigation, let client-side to + // handle it. + navigateToClient(location.getPathWithQueryParameters()); } - + } finally { + navigationInProgress = false; } } diff --git a/flow-server/src/main/java/com/vaadin/flow/component/page/History.java b/flow-server/src/main/java/com/vaadin/flow/component/page/History.java index 687ed079a74..f384c24bd68 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/page/History.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/page/History.java @@ -182,11 +182,13 @@ public void pushState(JsonValue state, String location) { * to only change the JSON state */ public void pushState(JsonValue state, Location location) { + final String pathWithQueryParameters = Optional.ofNullable(location) + .map(Location::getPathWithQueryParameters).orElse(null); // Second parameter is title which is currently ignored according to // https://developer.mozilla.org/en-US/docs/Web/API/History_API ui.getPage().executeJs( "setTimeout(() => window.history.pushState($0, '', $1))", state, - location.getPathWithQueryParameters()); + pathWithQueryParameters); } /** @@ -219,11 +221,13 @@ public void replaceState(JsonValue state, String location) { * to only change the JSON state */ public void replaceState(JsonValue state, Location location) { + final String pathWithQueryParameters = Optional.ofNullable(location) + .map(Location::getPathWithQueryParameters).orElse(null); // Second parameter is title which is currently ignored according to // https://developer.mozilla.org/en-US/docs/Web/API/History_API ui.getPage().executeJs( "setTimeout(() => window.history.replaceState($0, '', $1))", - state, location.getPathWithQueryParameters()); + state, pathWithQueryParameters); } /** diff --git a/flow-server/src/test/java/com/vaadin/flow/component/internal/JavaScriptBootstrapUITest.java b/flow-server/src/test/java/com/vaadin/flow/component/internal/JavaScriptBootstrapUITest.java index a877101bc39..be99aa3d248 100644 --- a/flow-server/src/test/java/com/vaadin/flow/component/internal/JavaScriptBootstrapUITest.java +++ b/flow-server/src/test/java/com/vaadin/flow/component/internal/JavaScriptBootstrapUITest.java @@ -207,13 +207,13 @@ public void cleanup() { @Test public void should_allow_navigation() { - ui.connectClient("foo", "bar", "/clean", ""); + ui.connectClient("foo", "bar", "/clean", "", null); assertEquals(Tag.HEADER, ui.wrapperElement.getChild(0).getTag()); assertEquals(Tag.H2, ui.wrapperElement.getChild(0).getChild(0).getTag()); // Dirty view is allowed after clean view - ui.connectClient("foo", "bar", "/dirty", ""); + ui.connectClient("foo", "bar", "/dirty", "", null); assertEquals(Tag.SPAN, ui.wrapperElement.getChild(0).getTag()); assertEquals(Tag.H1, ui.wrapperElement.getChild(0).getChild(0).getTag()); @@ -221,7 +221,7 @@ public void should_allow_navigation() { @Test public void should_navigate_when_endingSlash() { - ui.connectClient("foo", "bar", "/clean/", ""); + ui.connectClient("foo", "bar", "/clean/", "", null); assertEquals(Tag.HEADER, ui.wrapperElement.getChild(0).getTag()); assertEquals(Tag.H2, ui.wrapperElement.getChild(0).getChild(0).getTag()); @@ -229,7 +229,7 @@ public void should_navigate_when_endingSlash() { @Test public void getChildren_should_notReturnAnEmptyList() { - ui.connectClient("foo", "bar", "/clean", ""); + ui.connectClient("foo", "bar", "/clean", "", null); assertEquals(1, ui.getChildren().count()); } @@ -237,7 +237,7 @@ public void getChildren_should_notReturnAnEmptyList() { public void addRemoveComponent_clientSideRouting_addsToBody() { final Element uiElement = ui.getElement(); - ui.connectClient("foo", "bar", "/clean", ""); + ui.connectClient("foo", "bar", "/clean", "", null); // router outlet is a virtual child that is not reflected on element // level assertEquals(1, ui.getChildren().count()); @@ -293,7 +293,7 @@ public void addRemoveComponent_serverSideRouting_addsDirectlyToUI() { public void addComponent_clientSideRouterAndNavigation_componentsRemain() { final Element uiElement = ui.getElement(); // trigger route via client - ui.connectClient("foo", "bar", "/clean", ""); + ui.connectClient("foo", "bar", "/clean", "", null); final RouterLink routerLink = new RouterLink(); ui.add(routerLink); @@ -331,25 +331,25 @@ public void addComponent_serverSideRouterAndNavigation_componentsRemain() { @Test public void should_prevent_navigation_on_dirty() { - ui.connectClient("foo", "bar", "/dirty", ""); + ui.connectClient("foo", "bar", "/dirty", "", null); assertEquals(Tag.SPAN, ui.wrapperElement.getChild(0).getTag()); assertEquals(Tag.H1, ui.wrapperElement.getChild(0).getChild(0).getTag()); // clean view cannot be rendered after dirty - ui.connectClient("foo", "bar", "/clean", ""); + ui.connectClient("foo", "bar", "/clean", "", null); assertEquals(Tag.H1, ui.wrapperElement.getChild(0).getChild(0).getTag()); // an error route cannot be rendered after dirty - ui.connectClient("foo", "bar", "/errr", ""); + ui.connectClient("foo", "bar", "/errr", "", null); assertEquals(Tag.H1, ui.wrapperElement.getChild(0).getChild(0).getTag()); } @Test public void should_remove_content_on_leaveNavigation() { - ui.connectClient("foo", "bar", "/clean", ""); + ui.connectClient("foo", "bar", "/clean", "", null); assertEquals(Tag.HEADER, ui.wrapperElement.getChild(0).getTag()); assertEquals(Tag.H2, ui.wrapperElement.getChild(0).getChild(0).getTag()); @@ -361,7 +361,7 @@ public void should_remove_content_on_leaveNavigation() { @Test public void should_keep_content_on_leaveNavigation_postpone() { - ui.connectClient("foo", "bar", "/dirty", ""); + ui.connectClient("foo", "bar", "/dirty", "", null); assertEquals(Tag.SPAN, ui.wrapperElement.getChild(0).getTag()); assertEquals(Tag.H1, ui.wrapperElement.getChild(0).getChild(0).getTag()); @@ -375,7 +375,7 @@ public void should_keep_content_on_leaveNavigation_postpone() { @Test public void should_handle_forward_to_client_side_view_on_beforeEnter() { ui.connectClient("foo", "bar", "/forwardToClientSideViewOnBeforeEnter", - ""); + "", null); assertEquals("client-view", ui.getForwardToClientUrl()); } @@ -383,14 +383,15 @@ public void should_handle_forward_to_client_side_view_on_beforeEnter() { @Test public void should_not_handle_forward_to_client_side_view_on_beforeLeave() { ui.connectClient("foo", "bar", "/forwardToClientSideViewOnBeforeLeave", - ""); + "", null); assertNull(ui.getForwardToClientUrl()); } @Test public void should_not_handle_forward_to_client_side_view_on_reroute() { - ui.connectClient("foo", "bar", "/forwardToClientSideViewOnReroute", ""); + ui.connectClient("foo", "bar", "/forwardToClientSideViewOnReroute", "", + null); assertNull(ui.getForwardToClientUrl()); } @@ -398,7 +399,7 @@ public void should_not_handle_forward_to_client_side_view_on_reroute() { @Test public void should_handle_forward_to_server_side_view_on_beforeEnter_and_update_url() { ui.connectClient("foo", "bar", "/forwardToServerSideViewOnBeforeEnter", - ""); + "", null); assertEquals(Tag.HEADER, ui.wrapperElement.getChild(0).getTag()); assertEquals(Tag.H2, @@ -417,7 +418,7 @@ public void should_handle_forward_to_server_side_view_on_beforeEnter_and_update_ @Test public void should_show_error_page() { - ui.connectClient("foo", "bar", "/err", ""); + ui.connectClient("foo", "bar", "/err", "", null); assertEquals(Tag.DIV, ui.wrapperElement.getChild(0).getTag()); assertTrue(ui.wrapperElement.toString().contains("Available routes:")); } @@ -433,7 +434,7 @@ public void should_initializeUI_when_wrapperElement_null() { @Test public void should_navigate_when_server_routing() { - ui.connectClient("foo", "bar", "/clean", ""); + ui.connectClient("foo", "bar", "/clean", "", null); assertEquals(Tag.HEADER, ui.wrapperElement.getChild(0).getTag()); assertEquals(Tag.H2, ui.wrapperElement.getChild(0).getChild(0).getTag()); @@ -538,7 +539,7 @@ public void should_not_notify_clientRoute_when_navigatingToTheSame() { @Test public void server_should_not_doClientRoute_when_navigatingToServer() { - ui.connectClient("foo", "bar", "/clean", ""); + ui.connectClient("foo", "bar", "/clean", "", null); assertEquals(Tag.HEADER, ui.wrapperElement.getChild(0).getTag()); assertEquals(Tag.H2, ui.wrapperElement.getChild(0).getChild(0).getTag()); @@ -581,22 +582,23 @@ public void should_removeTitle_when_noAppShellTitle() { @Test public void should_restoreIndexHtmlTitle() { - ui.connectClient("foo", "bar", "empty", "app-shell-title"); + ui.connectClient("foo", "bar", "empty", "app-shell-title", null); assertEquals("", ui.getInternals().getTitle()); - ui.connectClient("foo", "bar", "dirty", "app-shell-title"); + ui.connectClient("foo", "bar", "dirty", "app-shell-title", null); assertEquals("app-shell-title", ui.getInternals().getTitle()); } @Test public void should_not_share_dynamic_app_title_for_different_UIs() { String dynamicTitle = UUID.randomUUID().toString(); - ui.connectClient("foo", "bar", "clean", dynamicTitle); + ui.connectClient("foo", "bar", "clean", dynamicTitle, null); assertEquals(dynamicTitle, ui.getInternals().getTitle()); String anotherDynamicTitle = UUID.randomUUID().toString(); JavaScriptBootstrapUI anotherUI = new JavaScriptBootstrapUI(); anotherUI.getInternals().setSession(mocks.getSession()); - anotherUI.connectClient("foo", "bar", "clean", anotherDynamicTitle); + anotherUI.connectClient("foo", "bar", "clean", anotherDynamicTitle, + null); assertEquals(anotherDynamicTitle, anotherUI.getInternals().getTitle()); ui.navigate("dirty"); diff --git a/flow-server/src/test/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandlerTest.java b/flow-server/src/test/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandlerTest.java index a4d3b950fc0..7328cf60199 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandlerTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/communication/IndexHtmlRequestHandlerTest.java @@ -535,7 +535,7 @@ public void should_use_client_routing_when_there_is_a_router_call() Boolean.FALSE); ((JavaScriptBootstrapUI) UI.getCurrent()).connectClient("foo", "bar", - "/foo", ""); + "/foo", "", null); Mockito.verify(session, Mockito.times(1)).setAttribute(SERVER_ROUTING, Boolean.FALSE); diff --git a/flow-server/src/test/java/com/vaadin/flow/server/communication/JavaScriptBootstrapHandlerTest.java b/flow-server/src/test/java/com/vaadin/flow/server/communication/JavaScriptBootstrapHandlerTest.java index 5dc4aed5366..a594dcc2834 100644 --- a/flow-server/src/test/java/com/vaadin/flow/server/communication/JavaScriptBootstrapHandlerTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/server/communication/JavaScriptBootstrapHandlerTest.java @@ -143,7 +143,7 @@ public void should_attachViewTo_UiContainer() throws Exception { jsInitHandler.handleRequest(session, request, response); JavaScriptBootstrapUI ui = (JavaScriptBootstrapUI) UI.getCurrent(); - ui.connectClient("a-tag", "an-id", "a-route", ""); + ui.connectClient("a-tag", "an-id", "a-route", "", null); TestNodeVisitor visitor = new TestNodeVisitor(true); BasicElementStateProvider.get().visit(ui.getElement().getNode(), diff --git a/flow-tests/test-root-context/src/main/java/com/vaadin/flow/uitest/ui/RouterLinkView.java b/flow-tests/test-root-context/src/main/java/com/vaadin/flow/uitest/ui/RouterLinkView.java index df87a864e6b..c3d093e546f 100644 --- a/flow-tests/test-root-context/src/main/java/com/vaadin/flow/uitest/ui/RouterLinkView.java +++ b/flow-tests/test-root-context/src/main/java/com/vaadin/flow/uitest/ui/RouterLinkView.java @@ -2,12 +2,13 @@ import com.vaadin.flow.component.UI; import com.vaadin.flow.component.html.Anchor; -import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Image; import com.vaadin.flow.dom.Element; import com.vaadin.flow.dom.ElementFactory; +import com.vaadin.flow.router.Location; import com.vaadin.flow.router.Route; +import elemental.json.Json; import elemental.json.JsonObject; @Route("com.vaadin.flow.uitest.ui.RouterLinkView") diff --git a/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/HistoryIT.java b/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/HistoryIT.java index 3877a013eab..4e1ff4f3a57 100644 --- a/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/HistoryIT.java +++ b/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/HistoryIT.java @@ -22,7 +22,6 @@ import java.util.stream.Collectors; import org.junit.Assert; -import org.junit.Ignore; import org.junit.Test; import org.openqa.selenium.By; import org.openqa.selenium.WebElement; @@ -33,7 +32,6 @@ public class HistoryIT extends ChromeBrowserTest { @Test - @Ignore("Ignored because of fusion issue: https://github.com/vaadin/flow/issues/8213") public void testHistory() throws URISyntaxException { open(); diff --git a/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/PopStateHandlerIT.java b/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/PopStateHandlerIT.java index bcf6e79ee41..35c766d78a5 100644 --- a/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/PopStateHandlerIT.java +++ b/flow-tests/test-root-context/src/test/java/com/vaadin/flow/uitest/ui/PopStateHandlerIT.java @@ -5,6 +5,8 @@ import org.junit.Test; import org.openqa.selenium.By; +import com.vaadin.flow.component.ClientCallable; +import com.vaadin.flow.router.internal.PathUtil; import com.vaadin.flow.testutil.ChromeBrowserTest; public class PopStateHandlerIT extends ChromeBrowserTest { @@ -35,7 +37,6 @@ public void testDifferentPath_ServerSideEvent() { } @Test - @Ignore("Ignored because of fusion issue: https://github.com/vaadin/flow/issues/10485") public void testDifferentPath_doubleBack_ServerSideEvent() { open(); @@ -78,7 +79,7 @@ public void testSamePathHashChanges_noServerSideEvent() { } @Test - @Ignore("Ignored because of fusion issue: https://github.com/vaadin/flow/issues/10485") + @Ignore("Ignored because of the issue https://github.com/vaadin/flow/issues/10825") public void testSamePathHashChanges_tripleeBack_noServerSideEvent() { open(); @@ -134,7 +135,6 @@ public void testEmptyHash_noHashServerToServer() { } @Test - @Ignore("Ignored because of fusion issue: https://github.com/vaadin/flow/issues/10485") public void testEmptyHash_quadrupleBack_noHashServerToServer() { open(); @@ -149,7 +149,8 @@ public void testEmptyHash_quadrupleBack_noHashServerToServer() { goBack(); verifyPopStateEvent(FORUM); - verifyInsideServletLocation(EMPTY_HASH); + // NOTE: see https://github.com/vaadin/flow/issues/10865 + verifyInsideServletLocation(isClientRouter() ? FORUM : EMPTY_HASH); goBack(); @@ -159,7 +160,8 @@ public void testEmptyHash_quadrupleBack_noHashServerToServer() { goBack(); verifyPopStateEvent(FORUM); - verifyInsideServletLocation(EMPTY_HASH); + // NOTE: see https://github.com/vaadin/flow/issues/10865 + verifyInsideServletLocation(isClientRouter() ? FORUM : EMPTY_HASH); goBack(); @@ -175,10 +177,16 @@ private void pushState(String id) { findElement(By.id(id)).click(); } + private String trimPathForClientRouter(String path) { + // NOTE: see https://github.com/vaadin/flow/issues/10865 + return isClientRouter() ? PathUtil.trimPath(path) : path; + } + private void verifyInsideServletLocation(String pathAfterServletMapping) { Assert.assertEquals("Invalid URL", - getRootURL() + "/view/" + pathAfterServletMapping, - getDriver().getCurrentUrl()); + trimPathForClientRouter( + getRootURL() + "/view/" + pathAfterServletMapping), + trimPathForClientRouter(getDriver().getCurrentUrl())); } private void verifyNoServerVisit() { @@ -186,7 +194,9 @@ private void verifyNoServerVisit() { } private void verifyPopStateEvent(String location) { - Assert.assertEquals("Invalid server side event location", location, - findElement(By.id("location")).getText()); + Assert.assertEquals("Invalid server side event location", + trimPathForClientRouter( + findElement(By.id("location")).getText()), + trimPathForClientRouter(location)); } }