diff --git a/junit6/src/test/java/com/vaadin/flow/component/contextmenu/ContextMenuTesterTest.java b/junit6/src/test/java/com/vaadin/flow/component/contextmenu/ContextMenuTesterTest.java index dc7f7159..dc38b20f 100644 --- a/junit6/src/test/java/com/vaadin/flow/component/contextmenu/ContextMenuTesterTest.java +++ b/junit6/src/test/java/com/vaadin/flow/component/contextmenu/ContextMenuTesterTest.java @@ -15,6 +15,7 @@ */ package com.vaadin.flow.component.contextmenu; +import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Assertions; @@ -51,6 +52,19 @@ void openCloseMenu_menuIsAttachedAndDetached() { "context menu should be detached from the UI, but was not"); } + @Test + void openCloseMenu_openedChangeEventsComeFromClient() { + List fromClient = new ArrayList<>(); + view.menu.addOpenedChangeListener( + event -> fromClient.add(event.isFromClient())); + + test(view.menu).open(); + test(view.menu).close(); + + Assertions.assertEquals(List.of(true, true), fromClient, + "opening and closing the menu should be reported as user actions"); + } + @Test void programmaticallyClose_menuIsDetached() { test(view.menu).open(); @@ -70,6 +84,23 @@ void openMenu_alreadyOpen_throws() { Assertions.assertTrue(exception.getMessage().contains("already open")); } + @Test + void openMenu_notUsable_throws() { + view.menu.setVisible(false); + + IllegalStateException exception = Assertions.assertThrows( + IllegalStateException.class, test(view.menu)::open); + Assertions.assertTrue(exception.getMessage().contains("is not usable")); + + Assertions.assertFalse(view.menu.isOpened(), + "a refused open should leave the menu closed"); + Assertions.assertFalse(view.menu.isAttached(), + "a refused open should leave the menu detached from the UI"); + Assertions.assertEquals(0, + find(Div.class).withText("Component Item").all().size(), + "a refused open should not leave the menu content reachable through a top level find()"); + } + @Test void closeMenu_menuNotOpened_throws() { Assertions.assertThrows(IllegalStateException.class, diff --git a/junit6/src/test/java/com/vaadin/flow/component/grid/contextmenu/GridContextMenuTesterTest.java b/junit6/src/test/java/com/vaadin/flow/component/grid/contextmenu/GridContextMenuTesterTest.java index 51b61733..70d1b745 100644 --- a/junit6/src/test/java/com/vaadin/flow/component/grid/contextmenu/GridContextMenuTesterTest.java +++ b/junit6/src/test/java/com/vaadin/flow/component/grid/contextmenu/GridContextMenuTesterTest.java @@ -190,6 +190,22 @@ void clickItem_disabledItem_throws() { Assertions.assertTrue(exception.getMessage().contains("not usable")); } + @Test + void openOnRow_menuNotUsable_throws() { + view.menu.setVisible(false); + + IllegalStateException exception = Assertions.assertThrows( + IllegalStateException.class, () -> test(view.menu).open(0)); + Assertions.assertTrue(exception.getMessage().contains("is not usable")); + + Assertions.assertFalse(view.menu.isOpened(), + "a refused open should leave the menu closed"); + Assertions.assertFalse(view.menu.isAttached(), + "a refused open should leave the menu detached from the UI"); + Assertions.assertEquals(0, find(Checkbox.class).all().size(), + "a refused open should not leave the menu content reachable through a top level find()"); + } + @Test void clickItem_menuNotOpened_throws() { Assertions.assertThrows(IllegalStateException.class, diff --git a/shared/src/main/java/com/vaadin/browserless/ComponentTester.java b/shared/src/main/java/com/vaadin/browserless/ComponentTester.java index fc0ae9e6..dc40ebc4 100644 --- a/shared/src/main/java/com/vaadin/browserless/ComponentTester.java +++ b/shared/src/main/java/com/vaadin/browserless/ComponentTester.java @@ -186,6 +186,29 @@ public final void ensureComponentIsUsable() { this::notUsableReasons); } + /** + * Checks that the wrapped component is usable and, if it is not, detaches + * it from the UI before rethrowing. + *

+ * For an overlay the tester has to attach to the UI before it can tell + * whether it is usable, such as a context menu opened by a before-open + * event. Detaching it again keeps a refused interaction from leaving a + * closed overlay behind in the UI tree, where a top level {@code find(...)} + * would still reach its content. + * + * @throws IllegalStateException + * if the component is not usable, with details on its current + * state. + */ + protected void ensureComponentIsUsableOrDetach() { + try { + ensureComponentIsUsable(); + } catch (RuntimeException e) { + getComponent().getElement().removeFromParent(); + throw e; + } + } + /** * Throws an {@link IllegalStateException} with details on the current state * of the component if it is not usable according to the provided test. diff --git a/shared/src/main/java/com/vaadin/flow/component/contextmenu/ContextMenuTester.java b/shared/src/main/java/com/vaadin/flow/component/contextmenu/ContextMenuTester.java index 074a8d1b..a047c07e 100644 --- a/shared/src/main/java/com/vaadin/flow/component/contextmenu/ContextMenuTester.java +++ b/shared/src/main/java/com/vaadin/flow/component/contextmenu/ContextMenuTester.java @@ -80,8 +80,10 @@ public void open() { } attachMenuToUI(); roundTrip(); - getComponent().getElement().setProperty("opened", true); - ensureComponentIsUsable(); + ensureComponentIsUsableOrDetach(); + // Simulate the overlay reporting itself as opened so that the + // resulting OpenedChangeEvent is seen as a user action. + setPropertyAsUser("opened", true); } /** @@ -89,7 +91,7 @@ public void open() { */ public void close() { ensureComponentIsUsable(); - getComponent().getElement().setProperty("opened", false); + setPropertyAsUser("opened", false); } /** diff --git a/shared/src/main/java/com/vaadin/flow/component/grid/contextmenu/GridContextMenuTester.java b/shared/src/main/java/com/vaadin/flow/component/grid/contextmenu/GridContextMenuTester.java index a89af38e..cf69c829 100644 --- a/shared/src/main/java/com/vaadin/flow/component/grid/contextmenu/GridContextMenuTester.java +++ b/shared/src/main/java/com/vaadin/flow/component/grid/contextmenu/GridContextMenuTester.java @@ -168,11 +168,11 @@ public void open(int row, String columnKey) { throw new IllegalStateException( "Context menu did not open. Its dynamic content handler returned false for the target row."); } + ensureComponentIsUsableOrDetach(); // opened is a synchronized property, so pushing it through the // client path makes the GridContextMenuOpenedEvent report // isFromClient() as true, the way a real open does setPropertyAsUser("opened", true); - ensureComponentIsUsable(); } /**