Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is not the right place to handle the exception. ServiceConfigurationError is never thrown during autoDiscoverViews() execution.

According to this comment, most likely the error comes from Flow initialization that happens later on.
We need to try to reproduce the issue and verify if we need to move this patch to another point in the codebase.

throw new BrowserlessTestSetupException(
routeDiscoveryFailureMessage(packageNames, e), e);
}
}

static String routeDiscoveryFailureMessage(Set<String> 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.";
}

/**
Expand Down
Loading