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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

---

### Unreleased

- **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`.

---

### v3.5.0 — 2026-09-24

- **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)
Expand Down
6 changes: 3 additions & 3 deletions docs-site/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ dependencies {

test {
useTestNG()
systemProperties System.properties
['browser', 'env'].each { k -> System.getProperty(k)?.with { systemProperty k, it } }
}
```

Expand All @@ -69,7 +69,7 @@ dependencies {

tasks.test {
useTestNG()
systemProperties(System.getProperties().mapKeys { it.key.toString() })
listOf("browser", "env").forEach { k -> System.getProperty(k)?.let { systemProperty(k, it) } }
}
```

Expand Down Expand Up @@ -198,7 +198,7 @@ mvn test
3. Your test runs
4. Screenshot captured on any failure
5. Browser closes
6. HTML report generated at `target/selenium-boot-report.html` (Maven) or `build/selenium-boot-report/` (Gradle)
6. HTML report generated at `target/selenium-boot-report.html` (Maven) or `build/selenium-boot-report.html` (Gradle)
7. Metrics JSON at `target/selenium-boot-metrics.json`

---
Expand Down
24 changes: 13 additions & 11 deletions docs-site/docs/gradle.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ test {
// suites 'src/test/resources/testng.xml'
}

// Forward system properties so -Denv=staging works from the CLI
systemProperties System.properties
// Forward -Denv=staging / -Dbrowser=firefox from the CLI. Forward only the keys you use:
// copying every JVM property (java.home, ...) breaks the run if the test JVM differs from Gradle's
['browser', 'env'].each { k -> System.getProperty(k)?.with { systemProperty k, it } }

// Display test output in the console
testLogging {
Expand All @@ -90,8 +91,9 @@ tasks.test {
// suites("src/test/resources/testng.xml")
}

// Forward system properties so -Denv=staging works from the CLI
systemProperties(System.getProperties().mapKeys { it.key.toString() })
// Forward -Denv=staging / -Dbrowser=firefox from the CLI. Forward only the keys you use:
// copying every JVM property (java.home, ...) breaks the run if the test JVM differs from Gradle's
listOf("browser", "env").forEach { k -> System.getProperty(k)?.let { systemProperty(k, it) } }

testLogging {
events("passed", "skipped", "failed")
Expand Down Expand Up @@ -119,7 +121,7 @@ dependencies {

test {
useJUnitPlatform()
systemProperties System.properties
['browser', 'env'].each { k -> System.getProperty(k)?.with { systemProperty k, it } }
}
```

Expand All @@ -135,7 +137,7 @@ dependencies {

tasks.test {
useJUnitPlatform()
systemProperties(System.getProperties().mapKeys { it.key.toString() })
listOf("browser", "env").forEach { k -> System.getProperty(k)?.let { systemProperty(k, it) } }
}
```

Expand Down Expand Up @@ -191,13 +193,13 @@ retry:

| Report type | Gradle path |
|---|---|
| HTML report (Selenium Boot) | `build/selenium-boot-report/index.html` |
| HTML report (Selenium Boot) | `build/selenium-boot-report.html` |
| JUnit XML (Selenium Boot) | `build/test-results/test/TEST-SeleniumBoot.xml` |
| Gradle's own HTML report | `build/reports/tests/test/index.html` |
| Allure results (if enabled) | `build/allure-results/` |

:::info JUnit XML auto-detection
Selenium Boot automatically detects Gradle by checking whether a `build/` directory exists and `target/` does not, then writes XML to `build/test-results/test/`. Override with `-Dseleniumboot.reports.dir=path/to/dir` if needed.
Selenium Boot detects a Gradle project by its `build.gradle` / `build.gradle.kts` (a `pom.xml` wins if both are present) and writes its reports, metrics and artifacts under `build/`, with JUnit XML in `build/test-results/test/`. Override with `-Dseleniumboot.reports.dir=path/to/dir` if needed.
:::

---
Expand All @@ -213,7 +215,7 @@ For parallel runs with Gradle, configure the `test` task alongside `selenium-boo
test {
useTestNG()
maxParallelForks = 4 // Gradle worker processes
systemProperties System.properties
['browser', 'env'].each { k -> System.getProperty(k)?.with { systemProperty k, it } }
}
```

Expand All @@ -224,7 +226,7 @@ test {
tasks.test {
useTestNG()
maxParallelForks = 4
systemProperties(System.getProperties().mapKeys { it.key.toString() })
listOf("browser", "env").forEach { k -> System.getProperty(k)?.let { systemProperty(k, it) } }
}
```

Expand Down Expand Up @@ -281,7 +283,7 @@ dependencies {

test {
useTestNG()
systemProperties System.properties
['browser', 'env'].each { k -> System.getProperty(k)?.with { systemProperty k, it } }
testLogging { events 'passed', 'skipped', 'failed' }
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ static List<File> loadHistoryFiles(int maxRuns) {
private static void export(List<FlakinessScore> scores) {
if (scores.isEmpty()) return;
try {
File out = new File("target/flakiness-report.json");
File out = com.seleniumboot.reporting.ReportPaths.resolve("flakiness-report.json");
List<Map<String, Object>> entries = scores.stream().map(s -> {
Map<String, Object> m = new LinkedHashMap<>();
m.put("testId", s.getTestId());
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/seleniumboot/healing/HealLog.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public static void clear() {
public static void export() {
if (EVENTS.isEmpty()) return;
try {
File dir = new File("target");
File dir = com.seleniumboot.reporting.ReportPaths.resolve("");
dir.mkdirs();
File out = new File(dir, "healed-locators.json");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public static void initialize() {
SeleniumBootConfig.Reporting reporting = config.getReporting();
if (reporting != null && reporting.isAllureEnabled()) {
ReportAdapterRegistry.register(new AllureReportAdapter());
System.out.println("[Selenium Boot] Allure adapter enabled → target/allure-results/");
System.out.println("[Selenium Boot] Allure adapter enabled → " + com.seleniumboot.reporting.ReportPaths.resolve("allure-results") + "/");
}

Notifications notifs = config.getNotifications();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public static String saveOnFailure(String testId) {
if (frames.isEmpty()) return null;

String safeId = testId.replaceAll("[^a-zA-Z0-9._-]", "_");
File dir = new File("target/recordings");
File dir = com.seleniumboot.reporting.ReportPaths.resolve("recordings");
dir.mkdirs();
File output = new File(dir, safeId + ".gif");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public void generate(File metricsJson) {
JsonNode root = MAPPER.readTree(metricsJson);
JsonNode tests = root.path("tests");

File outputDir = new File("target/allure-results");
File outputDir = ReportPaths.resolve("allure-results");
outputDir.mkdirs();

// Use the metrics file's modification time as the approximate suite-end timestamp.
Expand All @@ -64,7 +64,7 @@ public void generate(File metricsJson) {
writeTestResult(test, outputDir, suiteEnd);
}

System.out.println("[Selenium Boot] Allure results → target/allure-results/ ("
System.out.println("[Selenium Boot] Allure results → " + ReportPaths.resolve("allure-results") + "/ ("
+ tests.size() + " tests)");

} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public final class JUnitXmlReporter {
static String resolveOutputDir() {
String override = System.getProperty("seleniumboot.reports.dir");
if (override != null && !override.isBlank()) return override.trim();
if (new File("build").exists() && !new File("target").exists()) {
if (ReportPaths.usesGradleLayout()) {
return "build/test-results/test";
}
return "target/surefire-reports";
Expand Down
27 changes: 23 additions & 4 deletions src/main/java/com/seleniumboot/reporting/ReportPaths.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,29 @@ public static String baseDir() {
if (override != null && !override.isBlank()) {
return override.trim();
}
if (new File("build").exists() && !new File("target").exists()) {
return "build";
}
return "target";
return usesGradleLayout() ? "build" : "target";
}

/**
* {@code true} for a Gradle project. Decided from the build files first — several components
* create {@code target/} themselves, so "does {@code target/} exist" cannot tell the two apart.
* A {@code pom.xml} wins when both build systems are present; with neither, falls back to
* "{@code build/} exists and {@code target/} does not".
*/
public static boolean usesGradleLayout() {
return usesGradleLayout(new File("."));
}

public static boolean usesGradleLayout(File projectDir) {
if (new File(projectDir, "pom.xml").exists()) return false;
if (new File(projectDir, "build.gradle").exists()
|| new File(projectDir, "build.gradle.kts").exists()) return true;
return new File(projectDir, "build").exists() && !new File(projectDir, "target").exists();
}

/** A file or directory under {@link #baseDir()}, e.g. {@code resolve("recordings")}. */
public static File resolve(String relativePath) {
return new File(baseDir(), relativePath);
}

/** {@code <baseDir>/selenium-boot-metrics.json} — the report's data source. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;

/**
Expand All @@ -22,7 +21,6 @@
*/
public final class ScreenshotManager {

private static final String REPORT_DIR = "target/reports/screenshots";

private ScreenshotManager() {
}
Expand All @@ -45,7 +43,7 @@ public static String capture(String testName) {
try {
File srcFile = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);

Path directory = Paths.get(REPORT_DIR);
Path directory = ReportPaths.resolve("reports/screenshots").toPath();
Files.createDirectories(directory);

Path destination = directory.resolve(
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/seleniumboot/tracing/TraceRecorder.java
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ public static String buildHtml(TestTiming timing, String testName, String finalS
// ------------------------------------------------------------------

private static File traceFile(String className, String testName) {
return new File("target/traces/" + className + "/" + sanitize(testName) + "-trace.html");
return com.seleniumboot.reporting.ReportPaths.resolve("traces/" + className + "/" + sanitize(testName) + "-trace.html");
}

static String captureFinalScreenshot(String testName) {
Expand Down
48 changes: 48 additions & 0 deletions src/test/java/com/seleniumboot/unit/ReportPathsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import org.testng.annotations.AfterMethod;
import org.testng.annotations.Test;

import java.io.File;
import java.nio.file.Files;

import static org.testng.Assert.*;

/**
Expand Down Expand Up @@ -70,4 +73,49 @@ public void defaults_matchHistoricalTargetPaths() {
assertEquals(ReportPaths.htmlReport().getPath(),
"target/selenium-boot-report.html".replace('/', java.io.File.separatorChar));
}

// --- build-tool detection (#81): must not depend on whether target/ already exists ---

private static File project(String... files) throws Exception {
File dir = Files.createTempDirectory("sb-layout").toFile();
for (String f : files) {
File file = new File(dir, f);
if (f.endsWith("/")) file.mkdirs(); else file.createNewFile();
}
return dir;
}

@Test
public void gradleProject_isGradleLayout_evenWhenTargetExists() throws Exception {
// the #81 case: a component created target/ before the path was resolved
assertTrue(ReportPaths.usesGradleLayout(project("build.gradle", "build/", "target/")));
}

@Test
public void gradleKotlinProject_isGradleLayout() throws Exception {
assertTrue(ReportPaths.usesGradleLayout(project("build.gradle.kts")));
}

@Test
public void mavenProject_isNotGradleLayout() throws Exception {
assertFalse(ReportPaths.usesGradleLayout(project("pom.xml", "target/")));
}

@Test
public void bothBuildFiles_mavenWins() throws Exception {
assertFalse(ReportPaths.usesGradleLayout(project("pom.xml", "build.gradle")));
}

@Test
public void noBuildFile_fallsBackToBuildDirHeuristic() throws Exception {
assertTrue(ReportPaths.usesGradleLayout(project("build/")));
assertFalse(ReportPaths.usesGradleLayout(project("build/", "target/")));
assertFalse(ReportPaths.usesGradleLayout(project()));
}

@Test
public void resolve_isUnderBaseDir() {
System.setProperty("seleniumboot.reports.dir", "out");
assertEquals(ReportPaths.resolve("recordings").getPath(), "out" + File.separator + "recordings");
}
}
Loading