Skip to content

Commit 4363bbf

Browse files
tepimshabarov
andauthored
fix: prevent Flow navigation for Hilla route (#19875)
* fix: prevent Flow navigation for Hilla route * Add test and javadoc * Remove null test; add mock for failing test * fix mockstatics --------- Co-authored-by: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com>
1 parent 8cfca6e commit 4363bbf

File tree

4 files changed

+124
-19
lines changed

4 files changed

+124
-19
lines changed

flow-server/src/main/java/com/vaadin/flow/router/internal/AbstractNavigationStateRenderer.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
import com.vaadin.flow.server.Constants;
6969
import com.vaadin.flow.server.HttpStatusCode;
7070
import com.vaadin.flow.server.VaadinSession;
71+
import com.vaadin.flow.server.menu.MenuRegistry;
7172

7273
/**
7374
* Base class for navigation handlers that target a navigation state.
@@ -174,6 +175,12 @@ public int handle(NavigationEvent event) {
174175
return result.get();
175176
}
176177

178+
// If navigation target is Hilla route, terminate Flow navigation logic
179+
// here.
180+
if (MenuRegistry.hasClientRoute(event.getLocation().getPath(), true)) {
181+
return HttpStatusCode.OK.getCode();
182+
}
183+
177184
final ArrayList<HasElement> chain;
178185

179186
final boolean preserveOnRefreshTarget = isPreserveOnRefreshTarget(

flow-server/src/main/java/com/vaadin/flow/server/menu/MenuRegistry.java

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -408,13 +408,28 @@ public static ClassLoader getClassLoader() {
408408
}
409409

410410
/**
411-
* See if there is a client route available for give route path.
411+
* See if there is a client route available for given route path.
412412
*
413413
* @param route
414414
* route path to check
415415
* @return true if a client route is found.
416416
*/
417417
public static boolean hasClientRoute(String route) {
418+
return hasClientRoute(route, false);
419+
}
420+
421+
/**
422+
* See if there is a client route available for given route path, optionally
423+
* excluding layouts (routes with children) from the check.
424+
*
425+
* @param route
426+
* route path to check
427+
* @param excludeLayouts
428+
* {@literal true} to exclude layouts from the check,
429+
* {@literal false} to include them
430+
* @return true if a client route is found.
431+
*/
432+
public static boolean hasClientRoute(String route, boolean excludeLayouts) {
418433
if (VaadinSession.getCurrent() == null || route == null) {
419434
return false;
420435
}
@@ -423,8 +438,16 @@ public static boolean hasClientRoute(String route) {
423438
Map<String, AvailableViewInfo> clientItems = MenuRegistry
424439
.collectClientMenuItems(true,
425440
VaadinSession.getCurrent().getConfiguration());
426-
Set<String> clientRoutes = clientItems.keySet();
441+
final Set<String> clientRoutes = new HashSet<>();
442+
clientItems.forEach((path, info) -> {
443+
if (excludeLayouts) {
444+
if (info.children() == null || info.children().isEmpty()) {
445+
clientRoutes.add(path);
446+
}
447+
} else {
448+
clientRoutes.add(path);
449+
}
450+
});
427451
return clientRoutes.contains(route);
428452
}
429-
430453
}

flow-server/src/test/java/com/vaadin/flow/component/internal/JavaScriptBootstrapUITest.java

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.junit.Before;
1616
import org.junit.Test;
1717
import org.mockito.ArgumentCaptor;
18+
import org.mockito.MockedStatic;
1819
import org.mockito.Mockito;
1920

2021
import com.vaadin.flow.component.Component;
@@ -46,6 +47,7 @@
4647
import com.vaadin.flow.server.VaadinService;
4748
import com.vaadin.flow.server.VaadinSession;
4849
import com.vaadin.flow.server.VaadinSessionState;
50+
import com.vaadin.flow.server.menu.MenuRegistry;
4951

5052
public class JavaScriptBootstrapUITest {
5153

@@ -435,22 +437,30 @@ public void should_update_pushState_when_navigationHasBeenAlreadyStarted() {
435437
ArgumentCaptor<Serializable[]> execArg = ArgumentCaptor
436438
.forClass(Serializable[].class);
437439

438-
ui.navigate("clean/1");
439-
Mockito.verify(page).executeJs(execJs.capture(), execArg.capture());
440-
441-
boolean reactEnabled = ui.getSession().getConfiguration()
442-
.isReactEnabled();
443-
444-
final Serializable[] execValues = execArg.getValue();
445-
if (reactEnabled) {
446-
assertEquals(REACT_PUSHSTATE_TO, execJs.getValue());
447-
assertEquals(1, execValues.length);
448-
assertEquals("clean/1", execValues[0]);
449-
} else {
450-
assertEquals(CLIENT_PUSHSTATE_TO, execJs.getValue());
451-
assertEquals(2, execValues.length);
452-
assertNull(execValues[0]);
453-
assertEquals("clean/1", execValues[1]);
440+
try (MockedStatic<MenuRegistry> menuRegistry = Mockito
441+
.mockStatic(MenuRegistry.class)) {
442+
443+
menuRegistry
444+
.when(() -> MenuRegistry.hasClientRoute("clean/1", true))
445+
.thenReturn(false);
446+
447+
ui.navigate("clean/1");
448+
Mockito.verify(page).executeJs(execJs.capture(), execArg.capture());
449+
450+
boolean reactEnabled = ui.getSession().getConfiguration()
451+
.isReactEnabled();
452+
453+
final Serializable[] execValues = execArg.getValue();
454+
if (reactEnabled) {
455+
assertEquals(REACT_PUSHSTATE_TO, execJs.getValue());
456+
assertEquals(1, execValues.length);
457+
assertEquals("clean/1", execValues[0]);
458+
} else {
459+
assertEquals(CLIENT_PUSHSTATE_TO, execJs.getValue());
460+
assertEquals(2, execValues.length);
461+
assertNull(execValues[0]);
462+
assertEquals("clean/1", execValues[1]);
463+
}
454464
}
455465
}
456466

flow-server/src/test/java/com/vaadin/flow/router/internal/NavigationStateRendererTest.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.junit.Rule;
3838
import org.junit.Test;
3939
import org.junit.rules.ExpectedException;
40+
import org.mockito.MockedStatic;
4041
import org.mockito.Mockito;
4142

4243
import com.vaadin.flow.component.Component;
@@ -65,6 +66,7 @@
6566
import com.vaadin.flow.router.PreserveOnRefresh;
6667
import com.vaadin.flow.router.QueryParameters;
6768
import com.vaadin.flow.router.Route;
69+
import com.vaadin.flow.router.RouteAlias;
6870
import com.vaadin.flow.router.RouteConfiguration;
6971
import com.vaadin.flow.router.Router;
7072
import com.vaadin.flow.router.RouterLayout;
@@ -77,6 +79,7 @@
7779
import com.vaadin.flow.server.RouteRegistry;
7880
import com.vaadin.flow.server.ServiceException;
7981
import com.vaadin.flow.server.WrappedSession;
82+
import com.vaadin.flow.server.menu.MenuRegistry;
8083
import com.vaadin.flow.server.startup.ApplicationRouteRegistry;
8184
import com.vaadin.tests.util.AlwaysLockedVaadinSession;
8285
import com.vaadin.tests.util.MockDeploymentConfiguration;
@@ -323,6 +326,21 @@ private static class SingleView extends Component {
323326
}
324327
}
325328

329+
@Route(value = "/:samplePersonID?/:action?(edit)")
330+
@RouteAlias(value = "")
331+
@Tag("div")
332+
private static class RootRouteWithParam extends Component
333+
implements BeforeEnterObserver {
334+
RootRouteWithParam() {
335+
addAttachListener(e -> viewAttachCount.getAndIncrement());
336+
}
337+
338+
@Override
339+
public void beforeEnter(BeforeEnterEvent event) {
340+
beforeEnterCount.getAndIncrement();
341+
}
342+
}
343+
326344
@Test
327345
public void handle_preserveOnRefreshAndWindowNameNotKnown_clientSideCallTriggered() {
328346
// given a service with instantiator
@@ -636,6 +654,7 @@ public void handle_preserveOnRefresh_sameUI_uiIsNotClosed_childrenAreNotRemoved(
636654

637655
private static AtomicInteger layoutAttachCount;
638656
private static AtomicInteger viewAttachCount;
657+
private static AtomicInteger beforeEnterCount;
639658
private static String layoutUUID;
640659
private static String viewUUID;
641660

@@ -747,6 +766,52 @@ public void handle_normalView_refreshCurrentRouteRecreatesComponents() {
747766

748767
}
749768

769+
@Test
770+
public void handle_clientNavigation_withMatchingFlowRoute() {
771+
viewAttachCount = new AtomicInteger();
772+
beforeEnterCount = new AtomicInteger();
773+
774+
// given a service with instantiator
775+
MockVaadinServletService service = createMockServiceWithInstantiator();
776+
777+
// given a locked session
778+
MockVaadinSession session = new AlwaysLockedVaadinSession(service);
779+
session.setConfiguration(new MockDeploymentConfiguration());
780+
781+
// given a NavigationStateRenderer mapping to PreservedNestedView
782+
Router router = session.getService().getRouter();
783+
NavigationStateRenderer renderer = new NavigationStateRenderer(
784+
new NavigationStateBuilder(router)
785+
.withTarget(RootRouteWithParam.class).withPath("")
786+
.build());
787+
router.getRegistry().setRoute("", RootRouteWithParam.class, null);
788+
789+
MockUI ui = new MockUI(session);
790+
791+
renderer.handle(new NavigationEvent(router, new Location(""), ui,
792+
NavigationTrigger.PAGE_LOAD));
793+
794+
Assert.assertEquals(1, beforeEnterCount.get());
795+
Assert.assertEquals(1, viewAttachCount.get());
796+
797+
ui.getInternals().clearLastHandledNavigation();
798+
799+
try (MockedStatic<MenuRegistry> menuRegistry = Mockito
800+
.mockStatic(MenuRegistry.class)) {
801+
menuRegistry.when(
802+
() -> MenuRegistry.hasClientRoute("client-route", true))
803+
.thenReturn(true);
804+
805+
// This should not call attach or beforeEnter on root route
806+
renderer.handle(
807+
new NavigationEvent(router, new Location("client-route"),
808+
ui, NavigationTrigger.CLIENT_SIDE));
809+
810+
Assert.assertEquals(1, beforeEnterCount.get());
811+
Assert.assertEquals(1, viewAttachCount.get());
812+
}
813+
}
814+
750815
private MockVaadinServletService createMockServiceWithInstantiator() {
751816
MockVaadinServletService service = new MockVaadinServletService();
752817
service.init(new MockInstantiator() {

0 commit comments

Comments
 (0)