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: 2 additions & 0 deletions docs-site/docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions docs-site/docs/guides/semantic-locators.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions src/main/java/com/seleniumboot/locator/Locator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
* <p>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.
*
* <pre>getByLabel("Search").press(Keys.ENTER);</pre>
*
* @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.
*
* <p>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.
*
* <pre>getByLabel("Search").focus();</pre>
*
* @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.
*
Expand Down
11 changes: 10 additions & 1 deletion src/test/java/com/seleniumboot/unit/LocatorActionsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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());
Expand All @@ -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<Collection<Sequence>> gestures = ArgumentCaptor.captor();
verify(interactive, times(gestureAttempts)).perform(gestures.capture());
Expand Down