diff --git a/docs-site/docs/changelog.md b/docs-site/docs/changelog.md index 4892aea..ce5e7ab 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) + - **Failure Intelligence: high-level triage and suggested actions** — each failure is classified as `Product bug`, `Test bug`, `Environment`, `Flaky` or `Unknown` (flaky keeps its cause, e.g. `Flaky (Locator not found)`), with a rule-based suggested action. Shown in the HTML report, as `failureType` / `suggestedAction` in the metrics JSON, and in the JUnit XML; opt-in AI analysis uses it as a baseline. No API key needed. (#138) - **HTML report: tab and failure fixes** — the report always opens on the Dashboard (it no longer restores the last-open tab), failure details start collapsed, and the Failures tab no longer goes empty after you expand or collapse a class in Test Cases (both tabs shared one group id). 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 e670813..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; @@ -347,6 +348,42 @@ public void click() { whenReady(el -> el.isDisplayed() && el.isEnabled(), el -> { el.click(); return null; }); } + /** + * Waits for the element to be visible and enabled, then double-clicks it. + * + *
Re-resolves on every poll and retries stale or not-yet-present elements + * until {@code timeouts.explicit} elapses, including staleness during the gesture. + * + *
getByText("Edit item").doubleClick();
+ *
+ * @throws LocatorException if no element matches within {@code timeouts.explicit}
+ * @throws TimeoutException if a matching element never becomes ready or remains stale
+ */
+ public void doubleClick() {
+ whenReady(el -> el.isDisplayed() && el.isEnabled(), el -> {
+ new Actions(driver()).doubleClick(el).perform();
+ return null;
+ });
+ }
+
+ /**
+ * Waits for the element to be visible, then right-clicks it (context click).
+ *
+ * Re-resolves on every poll and retries stale or not-yet-present elements + * until {@code timeouts.explicit} elapses, including staleness during the gesture. + * + *
getByText("Options").rightClick();
+ *
+ * @throws LocatorException if no element matches within {@code timeouts.explicit}
+ * @throws TimeoutException if a matching element never becomes visible or remains stale
+ */
+ public void rightClick() {
+ whenVisible(el -> {
+ new Actions(driver()).contextClick(el).perform();
+ return null;
+ });
+ }
+
/**
* Waits for the element to be visible and enabled, then checks it if it is not already checked.
*
@@ -393,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
new file mode 100644
index 0000000..603b1c4
--- /dev/null
+++ b/src/test/java/com/seleniumboot/unit/LocatorActionsTest.java
@@ -0,0 +1,254 @@
+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 com.seleniumboot.locator.LocatorException;
+import org.mockito.ArgumentCaptor;
+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;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.interactions.Interactive;
+import org.openqa.selenium.interactions.Sequence;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import static org.mockito.Mockito.*;
+import static org.testng.Assert.*;
+
+/** Public keyboard, focus and pointer actions share the existing terminal-action contract. */
+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;
+ private Interactive interactive;
+ private MockedStatic