Skip to content

Commit

Permalink
fix: retain fragment when using push/replaceState (#11630)
Browse files Browse the repository at this point in the history
Fixes #11628
  • Loading branch information
pleku authored and caalador committed Aug 30, 2021
1 parent 56a5e48 commit 25912db
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ public UI getUI() {
}

/**
* Invokes <code>history.pushState</code> in the browser with the given
* Invokes <code>window.history.pushState</code> in the browser with the given
* parameters. This is a shorthand method for
* {@link History#pushState(JsonValue, Location)}, creating {@link Location}
* from the string provided.
Expand All @@ -171,7 +171,7 @@ public void pushState(JsonValue state, String location) {
}

/**
* Invokes <code>history.pushState</code> in the browser with the given
* Invokes <code>window.history.pushState</code> in the browser with the given
* parameters.
*
* @param state
Expand All @@ -184,12 +184,12 @@ public void pushState(JsonValue state, String location) {
public void pushState(JsonValue state, Location location) {
// Second parameter is title which is currently ignored according to
// https://developer.mozilla.org/en-US/docs/Web/API/History_API
ui.getPage().executeJs("history.pushState($0, '', $1)", state,
ui.getPage().executeJs("window.history.pushState($0, '', $1)", state,
location.getPathWithQueryParameters());
}

/**
* Invokes <code>history.replaceState</code> in the browser with the given
* Invokes <code>window.history.replaceState</code> in the browser with the given
* parameters. This is a shorthand method for
* {@link History#replaceState(JsonValue, Location)}, creating
* {@link Location} from the string provided.
Expand All @@ -207,7 +207,7 @@ public void replaceState(JsonValue state, String location) {
}

/**
* Invokes <code>history.replaceState</code> in the browser with the given
* Invokes <code>window.history.replaceState</code> in the browser with the given
* parameters.
*
* @param state
Expand All @@ -220,8 +220,8 @@ public void replaceState(JsonValue state, String location) {
public void replaceState(JsonValue state, Location location) {
// Second parameter is title which is currently ignored according to
// https://developer.mozilla.org/en-US/docs/Web/API/History_API
ui.getPage().executeJs("history.replaceState($0, '', $1)",
state, location.getPathWithQueryParameters());
ui.getPage().executeJs("window.history.replaceState($0, '', $1)", state,
location.getPathWithQueryParameters());
}

/**
Expand Down
23 changes: 18 additions & 5 deletions flow-server/src/main/java/com/vaadin/flow/router/Location.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@ public class Location implements Serializable {

private final List<String> segments;
private final QueryParameters queryParameters;
private String fragment;

/**
* Creates a new {@link Location} object for given location string.
* <p>
* This string can contain relative path and query parameters, if needed.
* This string can contain relative path and query parameters, if needed. A
* possible fragment {@code #fragment} is also retained.
* <p>
* A possible "/" prefix of the location is ignored and a <code>null</code>
* location is interpreted as <code>""</code>
Expand All @@ -54,6 +56,10 @@ public Location(String location) throws InvalidLocationException {
this(LocationUtil.parsePathToSegments(
LocationUtil.ensureRelativeNonNull(location)),
LocationUtil.parseQueryParameters(location));
int fragmentIndex = location == null ? -1 : location.indexOf('#');
if (fragmentIndex > -1) {
fragment = location.substring(fragmentIndex);
}
}

/**
Expand Down Expand Up @@ -181,7 +187,8 @@ public String getPath() {
}

/**
* Gets the path string with {@link QueryParameters}.
* Gets the path string with {@link QueryParameters} and including the
* possible fragment if one existed.
*
* @return path string with parameters
*/
Expand All @@ -190,12 +197,18 @@ public String getPathWithQueryParameters() {
assert !basePath.contains(
QUERY_SEPARATOR) : "Base path can not contain query separator="
+ QUERY_SEPARATOR;
assert !basePath.contains("#") : "Base path can not contain fragment #";

final StringBuilder pathBuilder = new StringBuilder(basePath);
String params = queryParameters.getQueryString();
if (params.isEmpty()) {
return !basePath.isEmpty() ? basePath : ".";
if (!params.isEmpty()) {
pathBuilder.append(QUERY_SEPARATOR).append(params);
}
if (fragment != null) {
pathBuilder.append(fragment);
}
return basePath + QUERY_SEPARATOR + params;
String path = pathBuilder.toString();
return path.isEmpty() ? "." : path;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public static void verifyRelativePath(String path) {

/**
* Parses the given path to parts split by the path separator, ignoring the
* query string if present. The path is verified with
* query string and fragment if either present. The path is verified with
* {@link #verifyRelativePath(String)}.
*
* @param path
Expand All @@ -89,6 +89,8 @@ public static List<String> parsePathToSegments(String path) {
int endIndex = path.indexOf(Location.QUERY_SEPARATOR);
if (endIndex >= 0) {
basePath = path.substring(0, endIndex);
} else if (path.contains("#")) {
basePath = path.substring(0, path.indexOf('#'));
} else {
basePath = path;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* Copyright 2000-2021 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.flow.component.page;

import java.io.Serializable;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import com.vaadin.flow.component.UI;

import elemental.json.Json;
import elemental.json.JsonString;

public class HistoryTest {

private class TestUI extends UI {
@Override
public Page getPage() {
return page;
}
}

private class TestPage extends Page {

private String expression;

private Serializable[] parameters;

public TestPage(UI ui) {
super(ui);
}

@Override
public PendingJavaScriptResult executeJs(String expression,
Serializable... parameters) {
this.expression = expression;
this.parameters = parameters;
return null;
}
}

private UI ui = new TestUI();
private TestPage page = new TestPage(ui);
private History history;

@Before
public void setup() {
history = new History(ui);
}

@Test
public void pushState_locationWithQueryParameters_queryParametersRetained() {
history.pushState(Json.create("{foo:bar;}"), "context/view?param=4");

Assert.assertEquals("push state JS not included",
"window.history.pushState($0, '', $1)", page.expression);
Assert.assertEquals("push state not included", "{foo:bar;}",
((JsonString) page.parameters[0]).getString());
Assert.assertEquals("invalid location", "context/view?param=4",
page.parameters[1]);

history.pushState(Json.create("{foo:bar;}"), "context/view/?param=4");

Assert.assertEquals("push state JS not included",
"window.history.pushState($0, '', $1)", page.expression);
Assert.assertEquals("push state not included", "{foo:bar;}",
((JsonString) page.parameters[0]).getString());
Assert.assertEquals("invalid location", "context/view/?param=4",
page.parameters[1]);
}

@Test
public void pushState_locationWithFragment_fragmentRetained() {
history.pushState(null, "context/view#foobar");

Assert.assertEquals("push state JS not included",
"window.history.pushState($0, '', $1)", page.expression);
Assert.assertEquals(null, page.parameters[0]);
Assert.assertEquals("fragment not retained", "context/view#foobar",
page.parameters[1]);

history.pushState(null, "context/view/#foobar");

Assert.assertEquals("push state JS not included",
"window.history.pushState($0, '', $1)", page.expression);
Assert.assertEquals(null, page.parameters[0]);
Assert.assertEquals("fragment not retained", "context/view/#foobar",
page.parameters[1]);
}

@Test // #11628
public void pushState_locationWithQueryParametersAndFragment_QueryParametersAndFragmentRetained() {
history.pushState(null, "context/view?foo=bar#foobar");

Assert.assertEquals("push state JS not included",
"window.history.pushState($0, '', $1)", page.expression);
Assert.assertEquals(null, page.parameters[0]);
Assert.assertEquals("invalid location", "context/view?foo=bar#foobar",
page.parameters[1]);

history.pushState(null, "context/view/?foo=bar#foobar");

Assert.assertEquals("push state JS not included",
"window.history.pushState($0, '', $1)", page.expression);
Assert.assertEquals(null, page.parameters[0]);
Assert.assertEquals("invalid location", "context/view/?foo=bar#foobar",
page.parameters[1]);
}

@Test // #11628
public void replaceState_locationWithQueryParametersAndFragment_QueryParametersAndFragmentRetained() {
history.replaceState(null, "context/view?foo=bar#foobar");

Assert.assertEquals("push state JS not included",
"window.history.replaceState($0, '', $1)", page.expression);
Assert.assertEquals(null, page.parameters[0]);
Assert.assertEquals("invalid location", "context/view?foo=bar#foobar",
page.parameters[1]);

history.replaceState(null, "context/view/?foo=bar#foobar");

Assert.assertEquals("push state JS not included",
"window.history.replaceState($0, '', $1)", page.expression);
Assert.assertEquals(null, page.parameters[0]);
Assert.assertEquals("invalid location", "context/view/?foo=bar#foobar",
page.parameters[1]);
}

}
33 changes: 33 additions & 0 deletions flow-server/src/test/java/com/vaadin/flow/router/LocationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -299,4 +299,37 @@ public void colonInLocationPath_locationIsParsed() {
Assert.assertEquals("baz",
location.getQueryParameters().getQueryString());
}

@Test
public void locationWithFragment_fragmentRetainedForPathWithQueryParameters() {
String locationString = "foo#fragment";
Location location = new Location(locationString);
Assert.assertEquals(locationString,
location.getPathWithQueryParameters());

locationString = "foo/#fragment";
location = new Location(locationString);
Assert.assertEquals(locationString,
location.getPathWithQueryParameters());

locationString = "foo?bar#fragment";
location = new Location(locationString);
Assert.assertEquals(locationString,
location.getPathWithQueryParameters());

locationString = "foo/?bar=baz#fragment";
location = new Location(locationString);
Assert.assertEquals(locationString,
location.getPathWithQueryParameters());

locationString = "foo#";
location = new Location(locationString);
Assert.assertEquals(locationString,
location.getPathWithQueryParameters());

locationString = "foo/?bar=baz#";
location = new Location(locationString);
Assert.assertEquals(locationString,
location.getPathWithQueryParameters());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2798,9 +2798,9 @@ public void ui_navigate_should_only_have_one_history_marking_on_loop()
long historyInvocations = ui.getInternals()
.dumpPendingJavaScriptInvocations().stream()
.filter(js -> js.getInvocation().getExpression()
.startsWith("history.pushState"))
.startsWith("window.history.pushState"))
.count();
assertEquals(1, historyInvocations);
assertEquals("Invalid number of window.history.pushState calls", 1, historyInvocations);

Assert.assertNull("Last handled location should have been cleared",
ui.getInternals().getLastHandledLocation());
Expand Down

0 comments on commit 25912db

Please sign in to comment.