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 0000000..37b9c71 --- /dev/null +++ b/junit6/src/test/java/com/vaadin/browserless/RouteDiscoveryFailureMessageTest.java @@ -0,0 +1,68 @@ +/* + * 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 9133ddc..e236968 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; @@ -158,10 +159,43 @@ 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."; } /**