diff --git a/docs-site/docs/configuration.md b/docs-site/docs/configuration.md index 2b710d1..6bd4d81 100644 --- a/docs-site/docs/configuration.md +++ b/docs-site/docs/configuration.md @@ -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: diff --git a/src/main/java/com/seleniumboot/config/SeleniumBootConfig.java b/src/main/java/com/seleniumboot/config/SeleniumBootConfig.java index 5ec9c9f..f2a90e3 100644 --- a/src/main/java/com/seleniumboot/config/SeleniumBootConfig.java +++ b/src/main/java/com/seleniumboot/config/SeleniumBootConfig.java @@ -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; } diff --git a/src/main/java/com/seleniumboot/locator/Locator.java b/src/main/java/com/seleniumboot/locator/Locator.java index 3694ebe..9a89a41 100644 --- a/src/main/java/com/seleniumboot/locator/Locator.java +++ b/src/main/java/com/seleniumboot/locator/Locator.java @@ -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; @@ -721,7 +722,9 @@ private T whenVisible(Function action) { * result may be {@code null}. */ private T whenReady(Predicate ready, Function action) { - int timeout = SeleniumBootContext.getConfig().getTimeouts().getExplicit(); + SeleniumBootConfig cfg = SeleniumBootContext.getConfig(); + int timeout = cfg.getTimeouts().getExplicit(); + SeleniumBootConfig.Debug debug = cfg.getDebug(); AtomicReference result = new AtomicReference<>(); try { new WebDriverWait(driver(), Duration.ofSeconds(timeout)) @@ -729,6 +732,7 @@ private T whenReady(Predicate ready, Function act .until(d -> { WebElement el = resolve(); if (!ready.test(el)) return null; + if (debug.isHighlight()) highlight(el); result.set(action.apply(el)); return Boolean.TRUE; }); @@ -740,9 +744,28 @@ private T whenReady(Predicate ready, Function 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 // ------------------------------------------------------------------ diff --git a/src/test/java/com/seleniumboot/unit/LocatorDebugTest.java b/src/test/java/com/seleniumboot/unit/LocatorDebugTest.java new file mode 100644 index 0000000..a2df99e --- /dev/null +++ b/src/test/java/com/seleniumboot/unit/LocatorDebugTest.java @@ -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 driverManagerMock; + private MockedStatic 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); + } +}