From 9f49d5e670826f8034dde03e45756c2366515fae Mon Sep 17 00:00:00 2001 From: Mohammad Saddam Hossain Date: Thu, 24 Sep 2026 06:43:11 +0530 Subject: [PATCH] fix: name execution.baseUrl in open() error; docs say Step 3 needs the yml (#77) Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 1 + docs-site/docs/getting-started.md | 9 +++- .../java/com/seleniumboot/test/BaseTest.java | 8 ++- .../unit/BaseTestMissingBaseUrlTest.java | 52 +++++++++++++++++++ 4 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 src/test/java/com/seleniumboot/unit/BaseTestMissingBaseUrlTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index e39a20a..b3b8fe3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/). - **Zero-config first run: `selenium-boot.yml` is now optional** — with no default `selenium-boot.yml` in the working directory or on the classpath, the framework runs on built-in defaults (`browser.name: chrome`, `execution.mode: local`, `timeouts.explicit: 10`, `timeouts.pageLoad: 30`) and prints one `[Selenium Boot] No selenium-boot.yml found — running with built-in defaults ...` notice line. Fields a file omits (and an empty file) fall back to the same defaults; `browser.name` is only defaulted when `browser.matrix` is not set, and `SeleniumBootDefaults` overrides still take precedence over the built-ins. Nothing that was an error before stops being one: a requested profile (`-Dselenium.boot.profile`) or explicit path (`-Dselenium.boot.config`) whose file is missing still throws, so a typo never silently runs on defaults, and present-but-invalid values (an unknown `execution.mode`, a negative timeout) still fail validation. Adds `ConfigurationLoader.load(File, ClassLoader)` (working directory and classpath supplied, for tests). (#66) - **Terminal `Locator` actions wait for the element and re-resolve it on every poll** — `click()`, `type()`, `append()`, `getText()`, `getAttribute()`, `hover()`, `scrollIntoView()`, `jsClick()` and `element()` used to look the element up once and act on that cached reference, so a page that re-rendered it in between surfaced as a `StaleElementReferenceException`, and a locator that matched nothing yet threw immediately. They now resolve the locator from scratch on each poll, retrying on a stale or not-yet-present element until `timeouts.explicit` elapses. **Behavior change:** a locator that matches nothing is now retried for up to `timeouts.explicit` instead of failing at once. If it never matches, the call still ends in a `LocatorException` (cause: `TimeoutException`), so existing catches keep working; an element that exists but never becomes visible/clickable still times out with a `TimeoutException`, as before. `isVisible()`, `isHidden()`, `isEnabled()` and `count()` still never wait, and `rows()` inherits the wait for its table through `element()`. (#67, #68) - **JUnit Jupiter API bumped 5.10.2 → 6.1.3** (`junit-jupiter-api`, optional dependency of the JUnit 5 bridge). `junit-platform-launcher` is now bumped to match (6.1.3, was 1.10.2), closing the pairing gap left by #64. Verified both directions against the consumer integration suite (11/11 real-browser JUnit 5 tests): a consumer still on JUnit 5.10.2 + launcher 1.10.2 keeps working unchanged, and a consumer on JUnit 6 (jupiter 6.1.3 + launcher 6.1.3) now works too. (#64) +- **Clearer error when `execution.baseUrl` is unset** — `open()` and `open(String)` now throw `execution.baseUrl is not set — add it to selenium-boot.yml, or navigate with getDriver().get(url)` instead of `baseURL is null or empty`. Message only; still an `IllegalStateException`. The getting-started guide now says Step 3 needs the Step 2 file, since zero-config has no default `baseUrl`. (#77) ### v3.4.0 — 2026-09-19 diff --git a/docs-site/docs/getting-started.md b/docs-site/docs/getting-started.md index a8994c4..c5ee385 100644 --- a/docs-site/docs/getting-started.md +++ b/docs-site/docs/getting-started.md @@ -82,6 +82,13 @@ See the full [Gradle Setup Guide](/docs/gradle) for parallel config, JUnit 5, op defaults (Chrome, local execution, 10 s / 30 s timeouts) and prints one notice line saying so. Create it in your project root (next to `pom.xml` or `build.gradle`) when you want to change anything, such as the `baseUrl` below. + +:::note Step 3 needs this file +There is no default `baseUrl`, so `open()` in Step 3 only works once +`execution.baseUrl` is set. Create the file below to follow this walkthrough. Without it, +navigate with `getDriver().get("https://example.com")` instead. +::: + This example uses `https://example.com` — a stable real site reserved for documentation — so you can copy the files as-is and `mvn test` goes green. Swap in your own URL once it passes. @@ -111,7 +118,7 @@ absent file. See the [configuration reference](configuration.md) for the full li ## Step 3 — Write your first test -Copy this as-is — it passes against the `baseUrl` from Step 2 with a real +Copy this as-is — with the `selenium-boot.yml` from Step 2 in place, it passes against its `baseUrl` with a real Chrome, no changes needed: ```java title="src/test/java/SmokeTest.java" diff --git a/src/main/java/com/seleniumboot/test/BaseTest.java b/src/main/java/com/seleniumboot/test/BaseTest.java index 1ac456f..e871cd8 100644 --- a/src/main/java/com/seleniumboot/test/BaseTest.java +++ b/src/main/java/com/seleniumboot/test/BaseTest.java @@ -55,6 +55,10 @@ }) 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(); } @@ -64,7 +68,7 @@ protected void open() { .getExecution().getBaseUrl(); if (baseURL == null || baseURL.isEmpty()) { - throw new IllegalStateException("baseURL is null or empty"); + throw new IllegalStateException(MISSING_BASE_URL); } getDriver().get(baseURL); if (ConsoleErrorCollector.isEnabled()) ConsoleErrorCollector.injectShim(); @@ -74,7 +78,7 @@ protected void open(String path) { String baseUrl = SeleniumBootContext.getConfig() .getExecution().getBaseUrl(); if (baseUrl == null || baseUrl.isEmpty()) { - throw new IllegalStateException("baseURL is null or empty"); + throw new IllegalStateException(MISSING_BASE_URL); } String normalized = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; diff --git a/src/test/java/com/seleniumboot/unit/BaseTestMissingBaseUrlTest.java b/src/test/java/com/seleniumboot/unit/BaseTestMissingBaseUrlTest.java new file mode 100644 index 0000000..5b92351 --- /dev/null +++ b/src/test/java/com/seleniumboot/unit/BaseTestMissingBaseUrlTest.java @@ -0,0 +1,52 @@ +package com.seleniumboot.unit; + +import com.seleniumboot.config.SeleniumBootConfig; +import com.seleniumboot.internal.SeleniumBootContext; +import com.seleniumboot.test.BaseTest; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.Test; + +import java.lang.reflect.Field; +import java.util.concurrent.atomic.AtomicReference; + +import static org.testng.Assert.*; + +/** + * {@link BaseTest#open()} / {@link BaseTest#open(String)} without {@code execution.baseUrl} + * must name the missing key (#77). Fails before the browser is touched, so no driver is needed. + */ +public class BaseTestMissingBaseUrlTest { + + private static class Probe extends BaseTest { + void doOpen() { open(); } + void doOpen(String path) { open(path); } + } + + @AfterMethod + public void resetContext() throws Exception { + Field f = SeleniumBootContext.class.getDeclaredField("CONFIG"); + f.setAccessible(true); + ((AtomicReference) f.get(null)).set(null); + } + + private void initWithoutBaseUrl() { + SeleniumBootConfig config = new SeleniumBootConfig(); + config.setExecution(new SeleniumBootConfig.Execution()); + SeleniumBootContext.initialize(config); + } + + @Test + public void open_withoutBaseUrl_namesTheMissingKey() { + initWithoutBaseUrl(); + IllegalStateException e = expectThrows(IllegalStateException.class, () -> new Probe().doOpen()); + assertTrue(e.getMessage().contains("execution.baseUrl"), e.getMessage()); + assertTrue(e.getMessage().contains("selenium-boot.yml"), e.getMessage()); + } + + @Test + public void openPath_withoutBaseUrl_namesTheMissingKey() { + initWithoutBaseUrl(); + IllegalStateException e = expectThrows(IllegalStateException.class, () -> new Probe().doOpen("/x")); + assertTrue(e.getMessage().contains("execution.baseUrl"), e.getMessage()); + } +}