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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ All source lives under `src/main/java/com/seleniumboot/`. Key packages:
- **Registry + SPI** — `DriverProviderRegistry`, `PluginRegistry`, `ReportAdapterRegistry` use `ServiceLoader` for extensibility.
- **ThreadLocal driver isolation** — `DriverManager` stores `WebDriver` per thread; safe for parallel TestNG execution.
- **Template Method** — `BaseTest` / `BasePage` define framework-managed lifecycle; users override hook methods.
- **Convention over configuration** — `SeleniumBootDefaults` supplies fallbacks for optional fields, but `selenium-boot.yml` itself is **required**: `ConfigurationLoader` throws if the file is absent, and validates that `execution.mode`, `browser.name` (or `browser.matrix`), `timeouts.explicit` and `timeouts.pageLoad` are all present and positive.
- **Convention over configuration** — `selenium-boot.yml` is optional. `ConfigurationLoader` falls back to built-in defaults (`browser.name: chrome`, `execution.mode: local`, `timeouts.explicit: 10`, `timeouts.pageLoad: 30`) for an absent file (only when no profile / explicit path was requested — those still throw if missing) and for any omitted field; `SeleniumBootDefaults` programmatic overrides take precedence over the built-ins. Present-but-invalid values (unknown `execution.mode`, negative timeout) still fail validation.

### Public API Contract

Expand Down
20 changes: 20 additions & 0 deletions docs-site/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,26 @@ The framework looks for the config file in this priority order:
1. **System property** — `-Dselenium.boot.config=/path/to/custom.yml`
2. **Working directory** — `./selenium-boot.yml` (next to `pom.xml`)
3. **Classpath** — `src/test/resources/selenium-boot.yml`
4. **Built-in defaults** — used only when none of the above exist *and* no profile or explicit
path was requested. The framework prints one line saying so.

A requested profile (`-Dselenium.boot.profile=staging`) or explicit path that cannot be found is
always an error, so a typo never silently runs against defaults.

## Defaults for omitted fields

A file only has to say what it wants to change. Anything it leaves out (or an empty file) falls
back to:

| Field | Default |
|---|---|
| `browser.name` | `chrome` (not applied when `browser.matrix` is set) |
| `execution.mode` | `local` |
| `timeouts.explicit` | `10` |
| `timeouts.pageLoad` | `30` |

Values that are present but invalid — an unknown `execution.mode`, a negative timeout — still fail
at startup.

---

Expand Down
4 changes: 4 additions & 0 deletions docs-site/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ timeouts:
pageLoad: 30
```

Every field here is optional — anything you leave out falls back to a default (Chrome, local
execution, 10 s / 30 s timeouts), and with no file at all the framework runs on those defaults.
See the [configuration reference](configuration.md) for the full list.

---

## Step 3 — Write your first test
Expand Down
72 changes: 67 additions & 5 deletions src/main/java/com/seleniumboot/config/ConfigurationLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@

public final class ConfigurationLoader {

private static final String DEFAULT_BROWSER = "chrome";
private static final String DEFAULT_MODE = "local";
private static final int DEFAULT_EXPLICIT_TIMEOUT = 10;
private static final int DEFAULT_PAGE_LOAD_TIMEOUT = 30;

private ConfigurationLoader() {
// utility class
}
Expand All @@ -22,9 +27,20 @@ private ConfigurationLoader() {
* <li>System property {@code -Dselenium.boot.config=/path/to/file.yml} (explicit override)</li>
* <li>{@code ./selenium-boot[-profile].yml} in the current working directory</li>
* <li>{@code selenium-boot[-profile].yml} on the classpath (original behaviour)</li>
* <li>Built-in defaults — only when <em>no profile and no explicit path</em> was requested
* and the default {@code selenium-boot.yml} exists nowhere</li>
* </ol>
*
* <p>Fields a file leaves out fall back to the built-in defaults ({@link #applyBuiltInDefaults});
* values that are present but invalid still fail validation. A requested profile or explicit
* path that cannot be found is always an error — silently falling back would hide a typo.
*/
public static SeleniumBootConfig load() {
return load(new File("."), ConfigurationLoader.class.getClassLoader());
}

/** Same as {@link #load()} with the working directory and classpath supplied — for tests. */
public static SeleniumBootConfig load(File workingDir, ClassLoader classLoader) {
String profile = System.getProperty("selenium.boot.profile");

String configFile = (profile == null || profile.isBlank())
Expand All @@ -38,16 +54,23 @@ public static SeleniumBootConfig load() {
}

// Priority 2: working directory
File workingDirFile = new File(configFile);
File workingDirFile = new File(workingDir, configFile);
if (workingDirFile.exists()) {
return loadFromFile(workingDirFile);
}

// Priority 3: classpath (original behaviour)
InputStream inputStream = ConfigurationLoader.class
.getClassLoader()
.getResourceAsStream(configFile);
InputStream inputStream = classLoader.getResourceAsStream(configFile);
if (inputStream == null) {
if (profile == null || profile.isBlank()) {
System.out.println("[Selenium Boot] No " + configFile + " found — running with built-in defaults ("
+ DEFAULT_BROWSER + ", " + DEFAULT_MODE + ", timeouts.explicit=" + DEFAULT_EXPLICIT_TIMEOUT
+ "s, timeouts.pageLoad=" + DEFAULT_PAGE_LOAD_TIMEOUT
+ "s). Add a " + configFile + " to customise.");
SeleniumBootConfig config = new SeleniumBootConfig();
completeAndValidate(config);
return config;
}
throw new IllegalStateException(
"Configuration file '" + configFile + "' not found. " +
"Checked: -Dselenium.boot.config, working directory, and classpath.");
Expand Down Expand Up @@ -76,12 +99,51 @@ private static SeleniumBootConfig parseAndValidate(InputStream inputStream) {

Yaml yaml = new Yaml(constructor);
SeleniumBootConfig config = yaml.load(inputStream);
if (config == null) {
config = new SeleniumBootConfig(); // empty file — every field falls back to defaults
}

validate(config);
completeAndValidate(config);

return config;
}

private static void completeAndValidate(SeleniumBootConfig config) {
// Sections a file omits entirely are created so their fields can be defaulted.
if (config.getBrowser() == null) config.setBrowser(new SeleniumBootConfig.Browser());
if (config.getExecution() == null) config.setExecution(new SeleniumBootConfig.Execution());
if (config.getTimeouts() == null) config.setTimeouts(new SeleniumBootConfig.Timeouts());

// Programmatic defaults (SeleniumBootDefaults) take precedence over the built-in ones.
SeleniumBootDefaults.applyMissing(config);
applyBuiltInDefaults(config);

validate(config);
}

/**
* Fills fields the file left out. Only <em>absent</em> values are defaulted:
* {@code browser.name} (unless {@code browser.matrix} is set), {@code execution.mode},
* and zero-valued timeouts. A negative timeout or an unrecognised mode is still rejected by
* {@link #validate}.
*/
private static void applyBuiltInDefaults(SeleniumBootConfig config) {
SeleniumBootConfig.Browser browser = config.getBrowser();
boolean matrixConfigured = browser.getMatrix() != null && !browser.getMatrix().isEmpty();
if (browser.getName() == null && !matrixConfigured) {
browser.setName(DEFAULT_BROWSER);
}
if (config.getExecution().getMode() == null) {
config.getExecution().setMode(DEFAULT_MODE);
}
if (config.getTimeouts().getExplicit() == 0) {
config.getTimeouts().setExplicit(DEFAULT_EXPLICIT_TIMEOUT);
}
if (config.getTimeouts().getPageLoad() == 0) {
config.getTimeouts().setPageLoad(DEFAULT_PAGE_LOAD_TIMEOUT);
}
}

private static void validate(SeleniumBootConfig config) {
Objects.requireNonNull(config, "Configuration must not be null");

Expand Down
91 changes: 91 additions & 0 deletions src/test/java/com/seleniumboot/unit/ConfigurationLoaderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@

import com.seleniumboot.config.ConfigurationLoader;
import com.seleniumboot.config.SeleniumBootConfig;
import com.seleniumboot.config.SeleniumBootDefaults;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;

import static org.testng.Assert.*;

/**
Expand Down Expand Up @@ -91,4 +99,87 @@ public void load_configHasPositiveThreadCount() {
SeleniumBootConfig config = ConfigurationLoader.load();
assertTrue(config.getExecution().getMaxActiveSessions() > 0);
}

// ----------------------------------------------------------
// Zero-config first run: defaults for what a file leaves out
// ----------------------------------------------------------

/** A classpath with no selenium-boot*.yml on it (null parent = no test resources). */
private static ClassLoader emptyClasspath() {
return new URLClassLoader(new URL[0], null);
}

private static File emptyWorkingDir() throws IOException {
return Files.createTempDirectory("sb-config").toFile();
}

@Test
public void load_noConfigFileAnywhere_usesBuiltInDefaults() throws IOException {
SeleniumBootConfig config = ConfigurationLoader.load(emptyWorkingDir(), emptyClasspath());

assertEquals(config.getBrowser().getName(), "chrome");
assertEquals(config.getExecution().getMode(), "local");
assertEquals(config.getTimeouts().getExplicit(), 10);
assertEquals(config.getTimeouts().getPageLoad(), 30);
}

@Test(expectedExceptions = IllegalStateException.class)
public void load_noFileButProfileRequested_stillThrows() throws IOException {
System.setProperty("selenium.boot.profile", "staging");
ConfigurationLoader.load(emptyWorkingDir(), emptyClasspath());
}

@Test
public void load_emptyFile_usesBuiltInDefaults() throws IOException {
Path file = Files.createTempFile("sb-empty", ".yml");
System.setProperty("selenium.boot.config", file.toString());

SeleniumBootConfig config = ConfigurationLoader.load();

assertEquals(config.getBrowser().getName(), "chrome");
assertEquals(config.getTimeouts().getExplicit(), 10);
}

@Test
public void load_partialFile_keepsGivenValuesAndDefaultsTheRest() throws IOException {
Path file = Files.createTempFile("sb-partial", ".yml");
Files.writeString(file, "browser:\n name: firefox\n headless: true\ntimeouts:\n explicit: 5\n");
System.setProperty("selenium.boot.config", file.toString());

SeleniumBootConfig config = ConfigurationLoader.load();

assertEquals(config.getBrowser().getName(), "firefox");
assertTrue(config.getBrowser().isHeadless());
assertEquals(config.getTimeouts().getExplicit(), 5);
assertEquals(config.getTimeouts().getPageLoad(), 30, "omitted pageLoad falls back to the default");
assertEquals(config.getExecution().getMode(), "local", "omitted execution section falls back");
}

@Test
public void load_matrixWithoutName_doesNotInjectDefaultBrowserName() throws IOException {
Path file = Files.createTempFile("sb-matrix", ".yml");
Files.writeString(file, "browser:\n matrix: [chrome, firefox]\n");
System.setProperty("selenium.boot.config", file.toString());

SeleniumBootConfig config = ConfigurationLoader.load();

assertNull(config.getBrowser().getName());
assertEquals(config.getBrowser().getMatrix().size(), 2);
}

@Test(expectedExceptions = IllegalStateException.class)
public void load_invalidModeStillRejected() throws IOException {
Path file = Files.createTempFile("sb-badmode", ".yml");
Files.writeString(file, "execution:\n mode: banana\n");
System.setProperty("selenium.boot.config", file.toString());
ConfigurationLoader.load();
}

@Test(expectedExceptions = IllegalStateException.class)
public void load_negativeTimeoutStillRejected() throws IOException {
Path file = Files.createTempFile("sb-negtimeout", ".yml");
Files.writeString(file, "timeouts:\n explicit: -3\n");
System.setProperty("selenium.boot.config", file.toString());
ConfigurationLoader.load();
}
}
Loading