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
24 changes: 24 additions & 0 deletions docs-site/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,30 @@ to `data-qa`, `data-test`, etc. to match your app. See

---

## Debug

Live-triage aids for watching a test run on your own machine. **Dev-only — keep them off in CI**: they
slow the run down and modify the page. Both are off by default and cost nothing when off.

```yaml
debug:
slowMoMs: 300 # pause after each Locator action (ms); 0 = off
highlight: true # red outline on the element just before each Locator action
```

### `slowMoMs`
Sleeps for the given number of milliseconds after every terminal `Locator` action
(`click`, `type`, `getText`, `hover`, …), so you can follow the browser by eye.

### `highlight`
Applies `outline: 2px solid red` to the resolved element immediately before the action.
The outline is left in place afterwards, so you can see what was touched.

These complement trace and recording: `TraceRecorder` / `RecordingManager` give you a
post-mortem after the test finishes; `debug` lets you watch it live.

---

## Environment profiles

Override the default config for a specific environment using a profile suffix:
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/com/seleniumboot/config/SeleniumBootConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,27 @@ public void setCi(Ci ci) {
this.ci = ci;
}

private Debug debug = new Debug();
public Debug getDebug() { return debug; }
public void setDebug(Debug debug) { this.debug = debug != null ? debug : new Debug(); }

/**
* Live-triage aids for watching a test run locally. Dev-only — both slow the run down or
* touch the page, so leave them off in CI.
*/
public static final class Debug {
private long slowMoMs = 0;
private boolean highlight = false;

/** Pause after each terminal {@code Locator} action, in milliseconds. 0 disables. */
public long getSlowMoMs() { return slowMoMs; }
public void setSlowMoMs(long slowMoMs) { this.slowMoMs = Math.max(0, slowMoMs); }

/** Outline the resolved element in red just before each terminal {@code Locator} action. */
public boolean isHighlight() { return highlight; }
public void setHighlight(boolean highlight) { this.highlight = highlight; }
}

private Network network;
public Network getNetwork() { return network; }
public void setNetwork(Network network) { this.network = network; }
Expand Down
25 changes: 24 additions & 1 deletion src/main/java/com/seleniumboot/locator/Locator.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.seleniumboot.locator;

import com.seleniumboot.api.SeleniumBootApi;
import com.seleniumboot.config.SeleniumBootConfig;
import com.seleniumboot.driver.DriverManager;
import com.seleniumboot.internal.SeleniumBootContext;
import org.openqa.selenium.By;
Expand Down Expand Up @@ -721,14 +722,17 @@ private <T> T whenVisible(Function<WebElement, T> action) {
* result may be {@code null}.
*/
private <T> T whenReady(Predicate<WebElement> ready, Function<WebElement, T> action) {
int timeout = SeleniumBootContext.getConfig().getTimeouts().getExplicit();
SeleniumBootConfig cfg = SeleniumBootContext.getConfig();
int timeout = cfg.getTimeouts().getExplicit();
SeleniumBootConfig.Debug debug = cfg.getDebug();
AtomicReference<T> result = new AtomicReference<>();
try {
new WebDriverWait(driver(), Duration.ofSeconds(timeout))
.ignoring(StaleElementReferenceException.class, LocatorException.class)
.until(d -> {
WebElement el = resolve();
if (!ready.test(el)) return null;
if (debug.isHighlight()) highlight(el);
result.set(action.apply(el));
return Boolean.TRUE;
});
Expand All @@ -740,9 +744,28 @@ private <T> T whenReady(Predicate<WebElement> ready, Function<WebElement, T> act
}
throw e;
}
if (debug.getSlowMoMs() > 0) slowMo(debug.getSlowMoMs());
return result.get();
}

/** Debug aid: outlines the element; best-effort, never fails the action. The outline is left in place. */
private void highlight(WebElement el) {
try {
((JavascriptExecutor) driver()).executeScript(
"arguments[0].style.outline='2px solid red';", el);
} catch (RuntimeException ignored) {
// element went stale or the page blocks script — highlighting is cosmetic
}
}

private static void slowMo(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}

// ------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------
Expand Down
121 changes: 121 additions & 0 deletions src/test/java/com/seleniumboot/unit/LocatorDebugTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package com.seleniumboot.unit;

import com.seleniumboot.config.SeleniumBootConfig;
import com.seleniumboot.driver.DriverManager;
import com.seleniumboot.internal.SeleniumBootContext;
import com.seleniumboot.locator.Locator;
import org.mockito.MockedStatic;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.util.List;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;

/** {@code debug.highlight} and {@code debug.slowMoMs}: opt-in, no-op by default. */
public class LocatorDebugTest {

private interface JsDriver extends WebDriver, JavascriptExecutor {}

private JsDriver driver;
private WebElement el;
private SeleniumBootConfig config;
private MockedStatic<DriverManager> driverManagerMock;
private MockedStatic<SeleniumBootContext> contextMock;

@BeforeMethod
public void setup() {
driver = mock(JsDriver.class);
el = mock(WebElement.class);
when(el.isDisplayed()).thenReturn(true);
when(el.isEnabled()).thenReturn(true);
when(driver.findElements(By.id("go"))).thenReturn(List.of(el));

driverManagerMock = mockStatic(DriverManager.class);
driverManagerMock.when(DriverManager::getDriver).thenReturn(driver);

SeleniumBootConfig.Timeouts timeouts = new SeleniumBootConfig.Timeouts();
timeouts.setExplicit(2);
config = new SeleniumBootConfig();
config.setTimeouts(timeouts);
contextMock = mockStatic(SeleniumBootContext.class);
contextMock.when(SeleniumBootContext::getConfig).thenReturn(config);
}

@AfterMethod
public void teardown() {
driverManagerMock.close();
contextMock.close();
}

@Test
public void defaults_areOff() {
assertEquals(config.getDebug().getSlowMoMs(), 0L);
assertFalse(config.getDebug().isHighlight());
}

@Test
public void setDebugNull_fallsBackToDefaults() {
config.setDebug(null);
assertFalse(config.getDebug().isHighlight());
}

@Test
public void disabled_neverTouchesThePageOrSleeps() {
long start = System.nanoTime();
Locator.of(By.id("go")).click();
long ms = (System.nanoTime() - start) / 1_000_000;

verify(driver, never()).executeScript(anyString(), any(Object[].class));
assertTrue(ms < 200, "default run must not be slowed, took " + ms + "ms");
}

@Test
public void highlight_outlinesElementBeforeClick() {
config.getDebug().setHighlight(true);

Locator.of(By.id("go")).click();

var order = inOrder(driver, el);
order.verify((JavascriptExecutor) driver).executeScript(contains("outline"), eq(el));
order.verify(el).click();
}

@Test
public void highlight_scriptFailureDoesNotFailTheAction() {
config.getDebug().setHighlight(true);
when(driver.executeScript(anyString(), any(Object[].class))).thenThrow(new RuntimeException("blocked"));

Locator.of(By.id("go")).click();

verify(el).click();
}

@Test
public void slowMo_pausesAfterTheAction() {
config.getDebug().setSlowMoMs(300);

long start = System.nanoTime();
Locator.of(By.id("go")).click();
long ms = (System.nanoTime() - start) / 1_000_000;

verify(el).click();
assertTrue(ms >= 300, "expected >=300ms pause, took " + ms + "ms");
}

@Test
public void negativeSlowMo_clampsToZero() {
config.getDebug().setSlowMoMs(-5);
assertEquals(config.getDebug().getSlowMoMs(), 0L);
}
}
Loading