diff --git a/docs-site/docs/changelog.md b/docs-site/docs/changelog.md index a28340d..b601d19 100644 --- a/docs-site/docs/changelog.md +++ b/docs-site/docs/changelog.md @@ -11,6 +11,8 @@ All notable changes to Selenium Boot are documented here. ## [Unreleased] +- **`Locator` keyboard and pointer actions: `press(Keys...)`, `focus()`, `doubleClick()`, `rightClick()`** — send keys without clearing the value, focus without clicking, or perform native double/context clicks directly on a locator. Each action re-resolves on every poll and retries stale or not-yet-present elements until `timeouts.explicit` elapses. (#134) + - **AI provider is configurable** — `ai.provider` selects the backend for failure analysis and the run summary: `anthropic` (default, unchanged), `gemini`, or `openai` (any OpenAI-compatible `/chat/completions` endpoint — OpenAI, Azure, Groq, local Ollama via `ai.baseUrl`). `ai.model` now defaults per provider. Existing configs keep working with no change. - **AI run summary (opt-in)** — `ai.runSummary: true` adds one short paragraph to the HTML dashboard summarising the run's failures: which same-cause groups account for most of them, which look like product bugs versus test or environment problems, and what to check first. One API call per run, independent of `ai.failureAnalysis`; only grouped error messages and test names are sent (see the data note in the HTML report docs). Absent when disabled, when nothing failed, or on any error; the suite result is never affected. The text is exported as `aiRunSummary` in the metrics JSON. diff --git a/docs-site/docs/guides/semantic-locators.md b/docs-site/docs/guides/semantic-locators.md index 9f1a52d..673b0f7 100644 --- a/docs-site/docs/guides/semantic-locators.md +++ b/docs-site/docs/guides/semantic-locators.md @@ -24,6 +24,8 @@ Every semantic locator returns the same chainable, **auto-waiting** `Locator` used by the `$()` API — no `Thread.sleep`, no explicit waits. They're available on both `BaseTest` and `BasePage`. Narrow multiple matches using `.first()`, `.last()`, or `.nth(index)` (e.g. `getByRole(Role.LISTITEM).first().click()`). +Use `press(org.openqa.selenium.Keys...)` (e.g. `getByLabel("Search").press(Keys.ENTER)`, with `import org.openqa.selenium.Keys;`) to send keys without clearing, `focus()` to focus a focusable element without clicking, and `doubleClick()` / `rightClick()` for native pointer gestures; all four auto-wait and re-resolve on each poll up to `timeouts.explicit`. + Assertions can retry focus and empty-state checks, and accept regular expressions for text and values: `assertThat(getByLabel("Email")).isFocused()`, `isEmpty()`, or `hasValue(Pattern.compile("user-\\d+"))`. Auto-waiting covers the element *existing* as well as being visible: an action such as `click()` or diff --git a/src/main/java/com/seleniumboot/locator/Locator.java b/src/main/java/com/seleniumboot/locator/Locator.java index 47bb02d..dc77c00 100644 --- a/src/main/java/com/seleniumboot/locator/Locator.java +++ b/src/main/java/com/seleniumboot/locator/Locator.java @@ -7,6 +7,7 @@ import com.seleniumboot.metrics.ExecutionMetrics; import org.openqa.selenium.By; import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.Keys; import org.openqa.selenium.WebDriver; import org.openqa.selenium.OutputType; import org.openqa.selenium.StaleElementReferenceException; @@ -429,6 +430,45 @@ public void append(String text) { whenVisible(el -> { el.sendKeys(text); return null; }); } + /** + * Waits for the element to be visible, then sends the given keys without clearing its value. + * + *
Keys are sent in order using {@link WebElement#sendKeys(CharSequence...)}. + * Re-resolves on every poll and retries stale or not-yet-present elements + * until {@code timeouts.explicit} elapses, including staleness while sending keys. + * + *
getByLabel("Search").press(Keys.ENTER);
+ *
+ * @param keys Selenium keys to send to the element
+ * @throws LocatorException if no element matches within {@code timeouts.explicit}
+ * @throws TimeoutException if a matching element never becomes visible or remains stale
+ */
+ public void press(Keys... keys) {
+ whenVisible(el -> {
+ el.sendKeys(keys);
+ return null;
+ });
+ }
+
+ /**
+ * Waits for the element to be visible, then focuses it using JavaScript without clicking it.
+ *
+ * The target must be focusable (for example, an input or an element with {@code tabindex}). + * Re-resolves on every poll and retries stale or not-yet-present elements + * until {@code timeouts.explicit} elapses, including staleness while focusing. + * + *
getByLabel("Search").focus();
+ *
+ * @throws LocatorException if no element matches within {@code timeouts.explicit}
+ * @throws TimeoutException if a matching element never becomes visible or remains stale
+ */
+ public void focus() {
+ whenVisible(el -> {
+ ((JavascriptExecutor) driver()).executeScript("arguments[0].focus();", el);
+ return null;
+ });
+ }
+
/**
* Waits for the element to be visible and clears its text or value.
*
diff --git a/src/test/java/com/seleniumboot/unit/LocatorActionsTest.java b/src/test/java/com/seleniumboot/unit/LocatorActionsTest.java
index ffbcf00..603b1c4 100644
--- a/src/test/java/com/seleniumboot/unit/LocatorActionsTest.java
+++ b/src/test/java/com/seleniumboot/unit/LocatorActionsTest.java
@@ -9,6 +9,7 @@
import org.mockito.MockedStatic;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.Keys;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebDriver;
@@ -31,6 +32,7 @@
public class LocatorActionsTest {
private static final By TARGET = By.id("target");
+ private static final String FOCUS_SCRIPT = "arguments[0].focus();";
private WebDriver driver;
private JavascriptExecutor js;
@@ -62,7 +64,7 @@ public void teardown() {
@DataProvider(name = "actions")
public Object[][] actions() {
- return new Object[][] {{"doubleClick"}, {"rightClick"}};
+ return new Object[][] {{"press"}, {"focus"}, {"doubleClick"}, {"rightClick"}};
}
@Test(dataProvider = "actions")
@@ -165,6 +167,7 @@ public void action_elementNeverVisible_timesOutWithoutActing(String action) {
expectThrows(TimeoutException.class, () -> act(action));
+ verify(hidden, never()).sendKeys(any(Keys[].class));
verify(js, never()).executeScript(anyString(), any(WebElement.class));
verify(interactive, never()).perform(any());
verify(driver, atLeast(2)).findElements(TARGET);
@@ -192,6 +195,8 @@ private static WebElement readyElement() {
private void act(String action) {
Locator locator = Locator.of(TARGET);
switch (action) {
+ case "press" -> locator.press(Keys.ENTER, Keys.ESCAPE);
+ case "focus" -> locator.focus();
case "doubleClick" -> locator.doubleClick();
case "rightClick" -> locator.rightClick();
default -> throw new IllegalArgumentException(action);
@@ -205,6 +210,8 @@ private static boolean isPointerAction(String action) {
private void failActionWithStale(String action, WebElement element, boolean always) {
StaleElementReferenceException error = new StaleElementReferenceException("re-rendered during action");
switch (action) {
+ case "press" -> doThrow(error).when(element).sendKeys(Keys.ENTER, Keys.ESCAPE);
+ case "focus" -> when(js.executeScript(FOCUS_SCRIPT, element)).thenThrow(error);
case "doubleClick", "rightClick" -> {
if (always) {
doThrow(error).when(interactive).perform(any());
@@ -218,6 +225,8 @@ private void failActionWithStale(String action, WebElement element, boolean alwa
private void verifySuccessfulAction(String action, WebElement element, int gestureAttempts) {
switch (action) {
+ case "press" -> verify(element, times(1)).sendKeys(Keys.ENTER, Keys.ESCAPE);
+ case "focus" -> verify(js, times(1)).executeScript(FOCUS_SCRIPT, element);
case "doubleClick", "rightClick" -> {
ArgumentCaptor