diff --git a/vaadin-testbench-integration-tests/src/main/java/com/vaadin/testUI/ButtonView.java b/vaadin-testbench-integration-tests/src/main/java/com/vaadin/testUI/ButtonView.java new file mode 100644 index 000000000..e13f81a25 --- /dev/null +++ b/vaadin-testbench-integration-tests/src/main/java/com/vaadin/testUI/ButtonView.java @@ -0,0 +1,42 @@ +/** + * Copyright (C) 2000-2026 Vaadin Ltd + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * See for the full + * license. + */ +package com.vaadin.testUI; + +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.router.Route; + +@Route("ButtonView") +public class ButtonView extends Div { + + public ButtonView() { + NativeButton button = new NativeButton("Click me"); + button.setId("test-button"); + button.addSingleClickListener(event -> { + Span newSpan = new Span( + "Button single clicked: " + event.getClickCount()); + newSpan.setId("single-click"); + add(newSpan); + }); + button.addDoubleClickListener(event -> { + Span newSpan = new Span( + "Button double clicked: " + event.getClickCount()); + newSpan.setId("double-click"); + add(newSpan); + }); + button.addFocusListener(event -> { + Span newSpan = new Span("Button focused"); + newSpan.setId("focus-event"); + add(newSpan); + }); + add(button); + } + +} diff --git a/vaadin-testbench-integration-tests/src/test/java/com/vaadin/tests/elements/ButtonIT.java b/vaadin-testbench-integration-tests/src/test/java/com/vaadin/tests/elements/ButtonIT.java new file mode 100644 index 000000000..565df9d36 --- /dev/null +++ b/vaadin-testbench-integration-tests/src/test/java/com/vaadin/tests/elements/ButtonIT.java @@ -0,0 +1,51 @@ +/** + * Copyright (C) 2000-2026 Vaadin Ltd + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * See for the full + * license. + */ +package com.vaadin.tests.elements; + +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.By; + +import com.vaadin.flow.component.Component; +import com.vaadin.testUI.ButtonView; +import com.vaadin.tests.AbstractTB6Test; + +import static org.junit.Assert.assertEquals; + +public class ButtonIT extends AbstractTB6Test { + + @Override + protected Class getTestView() { + return ButtonView.class; + } + + @Before + public void openAndFindElement() { + openTestURL(); + } + + @Test + public void buttonClick_singleClick() { + $(NativeButtonElement.class).id("test-button").click(); + assertEquals("Button single clicked: 1", + findElement(By.id("single-click")).getText()); + assertEquals("Button focused", + findElement(By.id("focus-event")).getText()); + } + + @Test + public void buttonClick_doubleClick() { + $(NativeButtonElement.class).id("test-button").doubleClick(); + assertEquals("Button double clicked: 2", + findElement(By.id("double-click")).getText()); + assertEquals("Button focused", + findElement(By.id("focus-event")).getText()); + } + +} diff --git a/vaadin-testbench-shared/src/main/java/com/vaadin/testbench/TestBenchElement.java b/vaadin-testbench-shared/src/main/java/com/vaadin/testbench/TestBenchElement.java index 1d528dce1..a3e8d2310 100644 --- a/vaadin-testbench-shared/src/main/java/com/vaadin/testbench/TestBenchElement.java +++ b/vaadin-testbench-shared/src/main/java/com/vaadin/testbench/TestBenchElement.java @@ -191,23 +191,9 @@ public boolean isFocused() { @Override public void click() { - // JS call to click does not focus element, hence ensure focus - focus(); - try { - // Avoid strange "element not clickable at point" problems - callFunction("click"); - } catch (Exception e) { - if (e.getMessage() - .contains("Inspected target navigated or closed")) { - // This happens with chromedriver although e.g. navigation - // succeeds - return; - } - // SVG elements and maybe others do not have a 'click' method - autoScrollIntoView(); - waitForVaadin(); - wrappedElement.click(); - } + autoScrollIntoView(); + waitForVaadin(); + new Actions(getDriver()).click(wrappedElement).build().perform(); } @Override @@ -363,12 +349,20 @@ public void click(int x, int y, Keys... modifiers) { actions.build().perform(); } + /** + * Performs a double-click action on this element. + * + * @see click() + */ public void doubleClick() { autoScrollIntoView(); waitForVaadin(); new Actions(getDriver()).doubleClick(wrappedElement).build().perform(); } + /** + * Performs a context-click (right click) action on this element. + */ public void contextClick() { autoScrollIntoView(); waitForVaadin(); @@ -520,7 +514,20 @@ public boolean compareScreen(BufferedImage reference, String referenceName) (TakesScreenshot) this, (HasCapabilities) getDriver()); } - /*** + /** + * Scrolls the element into the visible area of the browser window with the + * given options. Check + * https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView + * for more information on the options. + * + * @param options + * the parameters for scrolling into view + */ + public void scrollIntoView(Map options) { + callFunction("scrollIntoView", options); + } + + /** * Scrolls the element into the visible area of the browser window */ public void scrollIntoView() { @@ -534,15 +541,65 @@ public void scrollIntoView() { */ private void autoScrollIntoView() { try { - if (getCommandExecutor().isAutoScrollIntoView()) { - if (!wrappedElement.isDisplayed()) { - scrollIntoView(); - } + if (getCommandExecutor().isAutoScrollIntoView() + && !isElementInViewport()) { + var params = Map. of("block", "end", "inline", + "end"); + scrollIntoView(params); } } catch (Exception e) { } } + private boolean isElementInViewport() { + try { + Boolean result = (Boolean) executeScript( + """ + function isVisible(elem) { + // Check if element has zero dimensions (truly hidden) + if (!elem.offsetParent && elem.offsetWidth === 0 && elem.offsetHeight === 0) { + return false; + } + + var rect = elem.getBoundingClientRect(); + if (rect.width === 0 || rect.height === 0) { + return false; + } + + // Check if element intersects with viewport + var windowHeight = window.innerHeight || document.documentElement.clientHeight; + var windowWidth = window.innerWidth || document.documentElement.clientWidth; + if (rect.bottom < 0 || rect.top > windowHeight || rect.right < 0 || rect.left > windowWidth) { + return false; + } + + // Check if clipped by any scrollable ancestor + var parent = elem.parentElement; + while (parent && parent !== document.body) { + var style = window.getComputedStyle(parent); + var overflow = style.overflow + style.overflowX + style.overflowY; + + if (overflow.includes('hidden') || overflow.includes('scroll') || overflow.includes('auto')) { + var parentRect = parent.getBoundingClientRect(); + // Check if element is outside parent's visible area + if (rect.bottom < parentRect.top || rect.top > parentRect.bottom || + rect.right < parentRect.left || rect.left > parentRect.right) { + return false; + } + } + parent = parent.parentElement; + } + return true; + } + return isVisible(arguments[0]); + """, + this); + return result != null && result; + } catch (Exception e) { + return true; // Assume visible on error + } + } + /** * Waits the given number of seconds for the given condition to become * neither null nor false. {@link NotFoundException}s are ignored by