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 @@ -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

Expand Down
9 changes: 8 additions & 1 deletion docs-site/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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"
Expand Down
8 changes: 6 additions & 2 deletions src/main/java/com/seleniumboot/test/BaseTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand All @@ -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();
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
Loading