diff --git a/CHANGELOG.md b/CHANGELOG.md index 8145ae8..0370e53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ### Unreleased +- **`open()` names `execution.baseUrl` when the site can't be reached** — a DNS or connection failure now reads `Could not reach (ERR_NAME_NOT_RESOLVED) — check execution.baseUrl in selenium-boot.yml …` (the driver's error stays as the cause) instead of a bare `net::ERR_NAME_NOT_RESOLVED`. Applies to `BaseTest`, `BaseJUnit5Test`, `BaseCucumberSteps` and `BaseConditions`. Also fixes `BaseJUnit5Test.open()` with no `execution.baseUrl` failing with a bare driver error instead of naming the missing key. - **Gradle projects now get their reports under `build/`** (#81) — the base directory is decided from `build.gradle` / `build.gradle.kts` (a `pom.xml` wins if both exist) instead of "does `target/` exist", which other components had already created. Flakiness report, healed locators, traces, recordings, Allure results, screenshots and JUnit XML all follow it; `-Dseleniumboot.reports.dir` still overrides. - **Docs: the Gradle `systemProperties System.properties` snippet is replaced** (#82) — copying every JVM property broke test runs when the test JVM differs from Gradle's. It now forwards only `browser` and `env`. diff --git a/src/main/java/com/seleniumboot/cucumber/BaseCucumberSteps.java b/src/main/java/com/seleniumboot/cucumber/BaseCucumberSteps.java index 3459aa3..d6d1b04 100644 --- a/src/main/java/com/seleniumboot/cucumber/BaseCucumberSteps.java +++ b/src/main/java/com/seleniumboot/cucumber/BaseCucumberSteps.java @@ -5,6 +5,7 @@ import com.seleniumboot.assertion.SeleniumAssert; import com.seleniumboot.browser.ConsoleErrorCollector; import com.seleniumboot.driver.DriverManager; +import com.seleniumboot.internal.BaseUrlNavigation; import com.seleniumboot.internal.SeleniumBootContext; import com.seleniumboot.locator.Locator; import io.cucumber.java.Scenario; @@ -55,7 +56,7 @@ protected WebDriverWait getWait() { /** Navigates to {@code execution.baseUrl} from {@code selenium-boot.yml}. */ protected void open() { - getDriver().get(baseUrl()); + BaseUrlNavigation.go(getDriver(), baseUrl()); if (ConsoleErrorCollector.isEnabled()) ConsoleErrorCollector.injectShim(); } @@ -63,7 +64,7 @@ protected void open() { protected void open(String path) { String base = baseUrl(); String normalized = base.endsWith("/") ? base.substring(0, base.length() - 1) : base; - getDriver().get(normalized + path); + BaseUrlNavigation.go(getDriver(), normalized + path); if (ConsoleErrorCollector.isEnabled()) ConsoleErrorCollector.injectShim(); } @@ -102,11 +103,6 @@ protected Scenario getScenario() { } private String baseUrl() { - String url = SeleniumBootContext.getConfig().getExecution().getBaseUrl(); - if (url == null || url.isEmpty()) { - throw new IllegalStateException( - "[BaseCucumberSteps] execution.baseUrl is not set in selenium-boot.yml"); - } - return url; + return BaseUrlNavigation.require(SeleniumBootContext.getConfig().getExecution().getBaseUrl()); } } diff --git a/src/main/java/com/seleniumboot/internal/BaseUrlNavigation.java b/src/main/java/com/seleniumboot/internal/BaseUrlNavigation.java new file mode 100644 index 0000000..5f0aee6 --- /dev/null +++ b/src/main/java/com/seleniumboot/internal/BaseUrlNavigation.java @@ -0,0 +1,51 @@ +package com.seleniumboot.internal; + +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebDriverException; + +import java.util.List; + +/** + * Shared by every {@code open()} in the base classes: requires {@code execution.baseUrl} and, + * when the browser cannot reach it, says so in terms of the config key instead of a raw + * {@code net::ERR_NAME_NOT_RESOLVED}. + */ +public final class BaseUrlNavigation { + + static final String MISSING_BASE_URL = + "execution.baseUrl is not set — add it to selenium-boot.yml, " + + "or navigate with getDriver().get(url)"; + + // Chrome/Edge net errors, Firefox error pages, and the JDK's own wording. + private static final List UNREACHABLE = List.of( + "ERR_NAME_NOT_RESOLVED", "ERR_CONNECTION_REFUSED", "ERR_CONNECTION_TIMED_OUT", + "ERR_INTERNET_DISCONNECTED", "ERR_ADDRESS_UNREACHABLE", "ERR_CONNECTION_RESET", + "dnsNotFound", "connectionFailure", "netTimeout", "UnknownHost"); + + private BaseUrlNavigation() {} + + /** Returns {@code baseUrl}, or throws naming the missing config key. */ + public static String require(String baseUrl) { + if (baseUrl == null || baseUrl.isBlank()) { + throw new IllegalStateException(MISSING_BASE_URL); + } + return baseUrl; + } + + /** {@code driver.get(url)}, with a clearer error when {@code url} cannot be reached. */ + public static void go(WebDriver driver, String url) { + try { + driver.get(url); + } catch (WebDriverException e) { + String msg = String.valueOf(e.getMessage()); + for (String marker : UNREACHABLE) { + if (msg.contains(marker)) { + throw new WebDriverException( + "Could not reach " + url + " (" + marker + ") — check execution.baseUrl " + + "in selenium-boot.yml, and that the site is up and reachable from this machine.", e); + } + } + throw e; + } + } +} diff --git a/src/main/java/com/seleniumboot/junit5/BaseJUnit5Test.java b/src/main/java/com/seleniumboot/junit5/BaseJUnit5Test.java index 03767fa..1a63452 100644 --- a/src/main/java/com/seleniumboot/junit5/BaseJUnit5Test.java +++ b/src/main/java/com/seleniumboot/junit5/BaseJUnit5Test.java @@ -9,6 +9,7 @@ import com.seleniumboot.driver.DriverManager; import com.seleniumboot.email.EmailCriteria; import com.seleniumboot.email.MailboxClient; +import com.seleniumboot.internal.BaseUrlNavigation; import com.seleniumboot.internal.SeleniumBootContext; import com.seleniumboot.locator.Locator; import com.seleniumboot.session.MultiSessionManager; @@ -59,16 +60,16 @@ protected WebDriverWait getWait() { /** Navigates to {@code execution.baseUrl} from {@code selenium-boot.yml}. */ protected void open() { - String url = SeleniumBootContext.getConfig().getExecution().getBaseUrl(); - getDriver().get(url); + String url = BaseUrlNavigation.require(SeleniumBootContext.getConfig().getExecution().getBaseUrl()); + BaseUrlNavigation.go(getDriver(), url); if (ConsoleErrorCollector.isEnabled()) ConsoleErrorCollector.injectShim(); } /** Navigates to {@code baseUrl + path}. */ protected void open(String path) { - String base = SeleniumBootContext.getConfig().getExecution().getBaseUrl(); + String base = BaseUrlNavigation.require(SeleniumBootContext.getConfig().getExecution().getBaseUrl()); String sep = base.endsWith("/") || path.startsWith("/") ? "" : "/"; - getDriver().get(base + sep + path); + BaseUrlNavigation.go(getDriver(), base + sep + path); if (ConsoleErrorCollector.isEnabled()) ConsoleErrorCollector.injectShim(); } diff --git a/src/main/java/com/seleniumboot/precondition/BaseConditions.java b/src/main/java/com/seleniumboot/precondition/BaseConditions.java index 939144d..e7553bb 100644 --- a/src/main/java/com/seleniumboot/precondition/BaseConditions.java +++ b/src/main/java/com/seleniumboot/precondition/BaseConditions.java @@ -2,6 +2,7 @@ import com.seleniumboot.api.SeleniumBootApi; import com.seleniumboot.driver.DriverManager; +import com.seleniumboot.internal.BaseUrlNavigation; import com.seleniumboot.internal.SeleniumBootContext; import com.seleniumboot.test.BasePage; import com.seleniumboot.wait.WaitEngine; @@ -51,21 +52,15 @@ protected WebDriver getDriver() { /** Navigates to the configured {@code baseUrl}. */ protected void open() { - String baseUrl = SeleniumBootContext.getConfig().getExecution().getBaseUrl(); - if (baseUrl == null || baseUrl.isEmpty()) { - throw new IllegalStateException("baseUrl is not configured in selenium-boot.yml"); - } - getDriver().get(baseUrl); + String baseUrl = BaseUrlNavigation.require(SeleniumBootContext.getConfig().getExecution().getBaseUrl()); + BaseUrlNavigation.go(getDriver(), baseUrl); } /** Navigates to {@code baseUrl + path}. */ protected void open(String path) { - String baseUrl = SeleniumBootContext.getConfig().getExecution().getBaseUrl(); - if (baseUrl == null || baseUrl.isEmpty()) { - throw new IllegalStateException("baseUrl is not configured in selenium-boot.yml"); - } + String baseUrl = BaseUrlNavigation.require(SeleniumBootContext.getConfig().getExecution().getBaseUrl()); String normalized = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; - getDriver().get(normalized + path); + BaseUrlNavigation.go(getDriver(), normalized + path); } /** Waits for the element to be clickable and clicks it. */ diff --git a/src/main/java/com/seleniumboot/test/BaseTest.java b/src/main/java/com/seleniumboot/test/BaseTest.java index e871cd8..1a49633 100644 --- a/src/main/java/com/seleniumboot/test/BaseTest.java +++ b/src/main/java/com/seleniumboot/test/BaseTest.java @@ -24,6 +24,7 @@ import com.seleniumboot.driver.DriverManager; import com.seleniumboot.email.EmailCriteria; import com.seleniumboot.email.MailboxClient; +import com.seleniumboot.internal.BaseUrlNavigation; import com.seleniumboot.internal.SeleniumBootContext; import com.seleniumboot.listeners.SuiteExecutionListener; import com.seleniumboot.listeners.TestExecutionListener; @@ -55,36 +56,25 @@ }) public abstract class BaseTest { - private static final String MISSING_BASE_URL = - "execution.baseUrl is not set — add it to selenium-boot.yml, " - + "or navigate with getDriver().get(url)"; - protected WebDriver getDriver() { return DriverManager.getDriver(); } protected void open() { - String baseURL = SeleniumBootContext.getConfig() - .getExecution().getBaseUrl(); - - if (baseURL == null || baseURL.isEmpty()) { - throw new IllegalStateException(MISSING_BASE_URL); - } - getDriver().get(baseURL); + String baseURL = BaseUrlNavigation.require(SeleniumBootContext.getConfig() + .getExecution().getBaseUrl()); + BaseUrlNavigation.go(getDriver(), baseURL); if (ConsoleErrorCollector.isEnabled()) ConsoleErrorCollector.injectShim(); } protected void open(String path) { - String baseUrl = SeleniumBootContext.getConfig() - .getExecution().getBaseUrl(); - if (baseUrl == null || baseUrl.isEmpty()) { - throw new IllegalStateException(MISSING_BASE_URL); - } + String baseUrl = BaseUrlNavigation.require(SeleniumBootContext.getConfig() + .getExecution().getBaseUrl()); String normalized = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; String fullUrl = normalized + path; - getDriver().get(fullUrl); + BaseUrlNavigation.go(getDriver(), fullUrl); if (ConsoleErrorCollector.isEnabled()) ConsoleErrorCollector.injectShim(); } diff --git a/src/test/java/com/seleniumboot/unit/BaseUrlNavigationTest.java b/src/test/java/com/seleniumboot/unit/BaseUrlNavigationTest.java new file mode 100644 index 0000000..7d25cf3 --- /dev/null +++ b/src/test/java/com/seleniumboot/unit/BaseUrlNavigationTest.java @@ -0,0 +1,89 @@ +package com.seleniumboot.unit; + +import com.seleniumboot.config.SeleniumBootConfig; +import com.seleniumboot.internal.BaseUrlNavigation; +import com.seleniumboot.internal.SeleniumBootContext; +import com.seleniumboot.junit5.BaseJUnit5Test; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebDriverException; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.Test; + +import java.lang.reflect.Field; +import java.util.concurrent.atomic.AtomicReference; + +import static org.mockito.Mockito.*; +import static org.testng.Assert.*; + +/** Unreachable / missing {@code execution.baseUrl} must be reported in terms of the config key. */ +public class BaseUrlNavigationTest { + + @AfterMethod + public void resetContext() throws Exception { + Field f = SeleniumBootContext.class.getDeclaredField("CONFIG"); + f.setAccessible(true); + ((AtomicReference) f.get(null)).set(null); + } + + private static WebDriver failingWith(String message) { + WebDriver driver = mock(WebDriver.class); + doThrow(new WebDriverException(message)).when(driver).get(anyString()); + return driver; + } + + @Test + public void chromeDnsFailure_namesUrlAndConfigKey() { + WebDriver driver = failingWith("unknown error: net::ERR_NAME_NOT_RESOLVED (Session info: chrome=153)"); + WebDriverException e = expectThrows(WebDriverException.class, + () -> BaseUrlNavigation.go(driver, "https://nope.invalid")); + assertTrue(e.getMessage().contains("https://nope.invalid"), e.getMessage()); + assertTrue(e.getMessage().contains("execution.baseUrl"), e.getMessage()); + assertTrue(e.getMessage().contains("ERR_NAME_NOT_RESOLVED"), e.getMessage()); + assertNotNull(e.getCause(), "original driver error must stay as the cause"); + } + + @Test + public void firefoxDnsFailure_isRecognised() { + WebDriver driver = failingWith("Reached error page: about:neterror?e=dnsNotFound&u=https%3A//x"); + WebDriverException e = expectThrows(WebDriverException.class, + () -> BaseUrlNavigation.go(driver, "https://x")); + assertTrue(e.getMessage().contains("execution.baseUrl"), e.getMessage()); + } + + @Test + public void unrelatedDriverError_isRethrownUntouched() { + WebDriver driver = failingWith("invalid session id"); + WebDriverException e = expectThrows(WebDriverException.class, + () -> BaseUrlNavigation.go(driver, "https://example.com")); + assertTrue(e.getMessage().startsWith("invalid session id"), e.getMessage()); + } + + @Test + public void reachableUrl_navigatesNormally() { + WebDriver driver = mock(WebDriver.class); + BaseUrlNavigation.go(driver, "https://example.com"); + verify(driver).get("https://example.com"); + } + + @Test + public void require_rejectsNullEmptyAndBlank() { + for (String bad : new String[] {null, "", " "}) { + IllegalStateException e = expectThrows(IllegalStateException.class, () -> BaseUrlNavigation.require(bad)); + assertTrue(e.getMessage().contains("execution.baseUrl"), e.getMessage()); + } + assertEquals(BaseUrlNavigation.require("https://a"), "https://a"); + } + + /** BaseJUnit5Test.open() used to hit a null baseUrl with a bare driver error. */ + @Test + public void junit5Open_withoutBaseUrl_namesTheMissingKey() { + SeleniumBootConfig config = new SeleniumBootConfig(); + config.setExecution(new SeleniumBootConfig.Execution()); + SeleniumBootContext.initialize(config); + class Probe extends BaseJUnit5Test { void doOpen() { open(); } void doOpen(String p) { open(p); } } + assertTrue(expectThrows(IllegalStateException.class, () -> new Probe().doOpen()) + .getMessage().contains("execution.baseUrl")); + assertTrue(expectThrows(IllegalStateException.class, () -> new Probe().doOpen("/x")) + .getMessage().contains("execution.baseUrl")); + } +}