Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <url> (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`.

Expand Down
12 changes: 4 additions & 8 deletions src/main/java/com/seleniumboot/cucumber/BaseCucumberSteps.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -55,15 +56,15 @@ 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();
}

/** Navigates to {@code baseUrl + path}. */
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();
}

Expand Down Expand Up @@ -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());
}
}
51 changes: 51 additions & 0 deletions src/main/java/com/seleniumboot/internal/BaseUrlNavigation.java
Original file line number Diff line number Diff line change
@@ -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<String> 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;
}
}
}
9 changes: 5 additions & 4 deletions src/main/java/com/seleniumboot/junit5/BaseJUnit5Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}

Expand Down
15 changes: 5 additions & 10 deletions src/main/java/com/seleniumboot/precondition/BaseConditions.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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. */
Expand Down
24 changes: 7 additions & 17 deletions src/main/java/com/seleniumboot/test/BaseTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}

Expand Down
89 changes: 89 additions & 0 deletions src/test/java/com/seleniumboot/unit/BaseUrlNavigationTest.java
Original file line number Diff line number Diff line change
@@ -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"));
}
}
Loading