From da1cc9efa7ee98f845e9f727ab95d345cc44f93b Mon Sep 17 00:00:00 2001 From: mikhail Date: Mon, 4 May 2026 13:07:26 +0300 Subject: [PATCH 1/2] feat: Improve error messaging for discover routes --- .../RouteDiscoveryFailureMessageTest.java | 71 +++++++++++++++++++ .../browserless/BaseBrowserlessTest.java | 45 ++++++++++-- 2 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 junit6/src/test/java/com/vaadin/browserless/RouteDiscoveryFailureMessageTest.java diff --git a/junit6/src/test/java/com/vaadin/browserless/RouteDiscoveryFailureMessageTest.java b/junit6/src/test/java/com/vaadin/browserless/RouteDiscoveryFailureMessageTest.java new file mode 100644 index 00000000..ebdf6067 --- /dev/null +++ b/junit6/src/test/java/com/vaadin/browserless/RouteDiscoveryFailureMessageTest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2000-2026 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.browserless; + +import java.util.ServiceConfigurationError; +import java.util.Set; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class RouteDiscoveryFailureMessageTest { + + @Test + void packagedScan_messageNamesPackagesAndCause() { + ServiceConfigurationError cause = new ServiceConfigurationError( + "com.example.MyProvider not a subtype"); + + String msg = BaseBrowserlessTest.routeDiscoveryFailureMessage( + Set.of("com.example.views"), cause); + + Assertions.assertTrue(msg.contains("com.example.views"), + "message should name the scanned package, was:\n" + msg); + Assertions.assertTrue(msg.contains("ServiceConfigurationError"), + "message should name the underlying error type, was:\n" + + msg); + Assertions.assertTrue( + msg.contains("com.example.MyProvider not a subtype"), + "message should include the original cause message, was:\n" + + msg); + } + + @Test + void wholeClasspathScan_messageDescribesFullScan() { + String msg = BaseBrowserlessTest.routeDiscoveryFailureMessage( + Set.of(""), new ServiceConfigurationError("boom")); + + Assertions.assertTrue(msg.contains("the whole classpath"), + "empty package set should be described as a full classpath " + + "scan, was:\n" + msg); + } + + @Test + void message_pointsAtDiscoverRoutesOverrideAndViewPackages() { + String msg = BaseBrowserlessTest.routeDiscoveryFailureMessage( + Set.of("com.example"), + new ServiceConfigurationError("boom")); + + Assertions.assertTrue(msg.contains("@ViewPackages"), + "message should suggest @ViewPackages, was:\n" + msg); + Assertions.assertTrue( + msg.contains("protected Routes discoverRoutes()"), + "message should show a discoverRoutes() override snippet, " + + "was:\n" + msg); + Assertions.assertTrue(msg.contains("new Routes()") + && msg.contains("getRoutes().add"), + "message should show how to populate Routes, was:\n" + msg); + } +} diff --git a/shared/src/main/java/com/vaadin/browserless/BaseBrowserlessTest.java b/shared/src/main/java/com/vaadin/browserless/BaseBrowserlessTest.java index 5ee9ee4c..8c3661ae 100644 --- a/shared/src/main/java/com/vaadin/browserless/BaseBrowserlessTest.java +++ b/shared/src/main/java/com/vaadin/browserless/BaseBrowserlessTest.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.ServiceConfigurationError; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; @@ -161,10 +162,46 @@ protected static synchronized Routes discoverRoutes( ? Set.of("") : packageNames; - return packageNames.stream() - .map(pkg -> routesCache.computeIfAbsent(pkg, - p -> new Routes().autoDiscoverViews(p))) - .reduce(new Routes(), Routes::merge); + try { + return packageNames.stream() + .map(pkg -> routesCache.computeIfAbsent(pkg, + p -> new Routes().autoDiscoverViews(p))) + .reduce(new Routes(), Routes::merge); + } catch (ServiceConfigurationError e) { + throw new BrowserlessTestSetupException( + routeDiscoveryFailureMessage(packageNames, e), e); + } + } + + static String routeDiscoveryFailureMessage(Set packages, + Throwable cause) { + String scope = packages.equals(Set.of("")) ? "the whole classpath" + : "package(s) " + packages; + return "Failed to auto-discover Vaadin routes by scanning " + scope + + ": " + + cause.getClass().getSimpleName() + ": " + cause.getMessage() + + "\n\nThis usually means a class loaded during the scan " + + "triggers java.util.ServiceLoader (e.g. via Vaadin's Lookup, " + + "an InstantiatorFactory, or another SPI) and one of the " + + "registered providers cannot be loaded on the test classpath." + + "\n\nWorkarounds:" + + "\n 1. Restrict the scan to your view package(s) by adding " + + "@ViewPackages(MyView.class) (or @ViewPackages(packages = \"com.example.views\")) " + + "to your test class." + + "\n 2. Override discoverRoutes() in your test class (or " + + "test base class) and build the Routes explicitly, skipping " + + "the classpath scan entirely:" + + "\n" + + "\n @Override" + + "\n protected Routes discoverRoutes() {" + + "\n Routes routes = new Routes();" + + "\n routes.getRoutes().add(MyView.class);" + + "\n // routes.getLayouts().add(MyLayout.class);" + + "\n // routes.getErrorRoutes().add(MyErrorView.class);" + + "\n return routes;" + + "\n }" + + "\n\nSee the original ServiceConfigurationError chained as " + + "the cause for the SPI/provider that failed to load."; } /** From 54fbad6a08332bfcabe0628c5965bde4f9f5feb4 Mon Sep 17 00:00:00 2001 From: mikhail Date: Mon, 4 May 2026 13:13:45 +0300 Subject: [PATCH 2/2] formatting --- .../RouteDiscoveryFailureMessageTest.java | 13 +++++-------- .../com/vaadin/browserless/BaseBrowserlessTest.java | 11 ++++------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/junit6/src/test/java/com/vaadin/browserless/RouteDiscoveryFailureMessageTest.java b/junit6/src/test/java/com/vaadin/browserless/RouteDiscoveryFailureMessageTest.java index ebdf6067..37b9c718 100644 --- a/junit6/src/test/java/com/vaadin/browserless/RouteDiscoveryFailureMessageTest.java +++ b/junit6/src/test/java/com/vaadin/browserless/RouteDiscoveryFailureMessageTest.java @@ -34,8 +34,7 @@ void packagedScan_messageNamesPackagesAndCause() { Assertions.assertTrue(msg.contains("com.example.views"), "message should name the scanned package, was:\n" + msg); Assertions.assertTrue(msg.contains("ServiceConfigurationError"), - "message should name the underlying error type, was:\n" - + msg); + "message should name the underlying error type, was:\n" + msg); Assertions.assertTrue( msg.contains("com.example.MyProvider not a subtype"), "message should include the original cause message, was:\n" @@ -55,17 +54,15 @@ void wholeClasspathScan_messageDescribesFullScan() { @Test void message_pointsAtDiscoverRoutesOverrideAndViewPackages() { String msg = BaseBrowserlessTest.routeDiscoveryFailureMessage( - Set.of("com.example"), - new ServiceConfigurationError("boom")); + Set.of("com.example"), new ServiceConfigurationError("boom")); Assertions.assertTrue(msg.contains("@ViewPackages"), "message should suggest @ViewPackages, was:\n" + msg); - Assertions.assertTrue( - msg.contains("protected Routes discoverRoutes()"), + Assertions.assertTrue(msg.contains("protected Routes discoverRoutes()"), "message should show a discoverRoutes() override snippet, " + "was:\n" + msg); - Assertions.assertTrue(msg.contains("new Routes()") - && msg.contains("getRoutes().add"), + Assertions.assertTrue( + msg.contains("new Routes()") && msg.contains("getRoutes().add"), "message should show how to populate Routes, was:\n" + msg); } } diff --git a/shared/src/main/java/com/vaadin/browserless/BaseBrowserlessTest.java b/shared/src/main/java/com/vaadin/browserless/BaseBrowserlessTest.java index 8c3661ae..86d56391 100644 --- a/shared/src/main/java/com/vaadin/browserless/BaseBrowserlessTest.java +++ b/shared/src/main/java/com/vaadin/browserless/BaseBrowserlessTest.java @@ -178,8 +178,8 @@ static String routeDiscoveryFailureMessage(Set packages, String scope = packages.equals(Set.of("")) ? "the whole classpath" : "package(s) " + packages; return "Failed to auto-discover Vaadin routes by scanning " + scope - + ": " - + cause.getClass().getSimpleName() + ": " + cause.getMessage() + + ": " + cause.getClass().getSimpleName() + ": " + + cause.getMessage() + "\n\nThis usually means a class loaded during the scan " + "triggers java.util.ServiceLoader (e.g. via Vaadin's Lookup, " + "an InstantiatorFactory, or another SPI) and one of the " @@ -190,16 +190,13 @@ static String routeDiscoveryFailureMessage(Set packages, + "to your test class." + "\n 2. Override discoverRoutes() in your test class (or " + "test base class) and build the Routes explicitly, skipping " - + "the classpath scan entirely:" - + "\n" - + "\n @Override" + + "the classpath scan entirely:" + "\n" + "\n @Override" + "\n protected Routes discoverRoutes() {" + "\n Routes routes = new Routes();" + "\n routes.getRoutes().add(MyView.class);" + "\n // routes.getLayouts().add(MyLayout.class);" + "\n // routes.getErrorRoutes().add(MyErrorView.class);" - + "\n return routes;" - + "\n }" + + "\n return routes;" + "\n }" + "\n\nSee the original ServiceConfigurationError chained as " + "the cause for the SPI/provider that failed to load."; }