From d01932c0151e8b5ef20f84bdf95e46f18a86bd28 Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 15:53:29 +0200 Subject: [PATCH] docs: document what find() can and cannot see (#236) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Corrects the documentation of what `find()` can reach. A closed context menu is not attached to the UI, so its items cannot be clicked — the old Javadoc and README said `open()` was optional, which was wrong. This also names the grid context menu tester as the way in for grid menus. Fixes #218 --- README.md | 15 +++++-- .../AbstractBrowserlessExtension.java | 4 +- .../contextmenu/ContextMenuTesterTest.java | 6 ++- .../component/grid/BasicGridTesterTest.java | 9 +++++ .../browserless/BaseBrowserlessTest.java | 4 +- .../browserless/BrowserlessUIContext.java | 4 +- .../vaadin/browserless/ComponentQuery.java | 8 ++++ .../vaadin/browserless/ComponentTester.java | 4 +- .../contextmenu/ContextMenuTester.java | 40 +++++++++++-------- 9 files changed, 69 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 40c88829..f93a3c2d 100644 --- a/README.md +++ b/README.md @@ -444,10 +444,17 @@ test(menu).open(); find(Div.class).withText("Rename").all(); // one match ``` -`ContextMenuTester` works either way: `clickItem("Rename")` and -`test(menu).find(Div.class)` read the server-side menu state and need no -`open()` at all; `open()` additionally attaches the menu to the UI, which is -what makes a top-level `find()` see the items. +A closed menu is not attached to the UI, so, as in the browser, its items +cannot be interacted with: `clickItem("Rename")`, `isItemChecked(...)` and +`getItemTooltipText(...)` throw an `IllegalStateException` until the menu is +opened. The tester-scoped `test(menu).find(Div.class)` is the exception, since +it reads the menu contents rather than the UI; it finds the items whether the +menu is open or not, and returns them detached while it is closed. + +A `GridContextMenu` is always about a row, so its tester takes one: +`test(grid).contextMenu(row)` targets a row without opening the menu, `open()` +then opens it there, and `clickItem("Rename")` clicks an item of the open menu. +`GridContextMenuTester.open(row)` opens the menu on a row directly. ## Per-test Vaadin configuration diff --git a/junit6/src/main/java/com/vaadin/browserless/AbstractBrowserlessExtension.java b/junit6/src/main/java/com/vaadin/browserless/AbstractBrowserlessExtension.java index 886ca5ca..fbef6236 100644 --- a/junit6/src/main/java/com/vaadin/browserless/AbstractBrowserlessExtension.java +++ b/junit6/src/main/java/com/vaadin/browserless/AbstractBrowserlessExtension.java @@ -276,7 +276,9 @@ public T navigate(String location, * the tree until then, and the lookup returns an empty result rather than * failing, so reach those components through the owning component tester * instead: {@code GridTester.getCellComponent(row, column)} for grid cells, - * {@code ContextMenuTester.open()} or {@code clickItem(...)} for menus. + * {@code ContextMenuTester.open()} and then {@code clickItem(...)} for a + * context menu, and {@code GridTester.contextMenu(row).open()} for a grid + * context menu. * * @param type * component type to search for 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 401cab98..8b6c223e 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 @@ -460,9 +460,13 @@ void find_menuContent_notInTheUiTreeUntilTheMenuIsOpened() { @Test void openAndFind_ContextMenuItemsCanBeAccessed() { var menuTester = test(view.menu); - menuTester.open(); var div = menuTester.find(Div.class).withText("Component Item") .single(); + Assertions.assertFalse(div.isAttached(), + "items of a menu that was never opened should be found, but detached"); + + menuTester.open(); + div = menuTester.find(Div.class).withText("Component Item").single(); Assertions.assertTrue(div.isAttached()); menuTester.close(); diff --git a/junit6/src/test/java/com/vaadin/flow/component/grid/BasicGridTesterTest.java b/junit6/src/test/java/com/vaadin/flow/component/grid/BasicGridTesterTest.java index ecd232ed..823880ba 100644 --- a/junit6/src/test/java/com/vaadin/flow/component/grid/BasicGridTesterTest.java +++ b/junit6/src/test/java/com/vaadin/flow/component/grid/BasicGridTesterTest.java @@ -345,6 +345,15 @@ void basicGrid_doubleClick() { } + @Test + void find_componentRenderedByColumn_notInComponentTree() { + // a component rendered per item does not exist until the renderer is + // asked to render that item, so the finder cannot see it. The cell + // component is reached through the tester instead. + Assertions.assertTrue(find(Button.class).all().isEmpty(), + "component column cells should not be part of the component tree"); + } + @Test void getCellComponent_columnByKey_canClickAButton() { final Component cellComponent = test(view.basicGrid).getCellComponent(1, diff --git a/shared/src/main/java/com/vaadin/browserless/BaseBrowserlessTest.java b/shared/src/main/java/com/vaadin/browserless/BaseBrowserlessTest.java index 92e7bc9e..69b3a332 100644 --- a/shared/src/main/java/com/vaadin/browserless/BaseBrowserlessTest.java +++ b/shared/src/main/java/com/vaadin/browserless/BaseBrowserlessTest.java @@ -452,7 +452,9 @@ public , Y extends Component> T test( * the tree until then, and the lookup returns an empty result rather than * failing, so reach those components through the owning component tester * instead: {@code GridTester.getCellComponent(row, column)} for grid cells, - * {@code ContextMenuTester.open()} or {@code clickItem(...)} for menus. + * {@code ContextMenuTester.open()} and then {@code clickItem(...)} for a + * context menu, and {@code GridTester.contextMenu(row).open()} for a grid + * context menu. * * @param componentType * the type of the component(s) to search for diff --git a/shared/src/main/java/com/vaadin/browserless/BrowserlessUIContext.java b/shared/src/main/java/com/vaadin/browserless/BrowserlessUIContext.java index 2a6a6e32..ff1b2f16 100644 --- a/shared/src/main/java/com/vaadin/browserless/BrowserlessUIContext.java +++ b/shared/src/main/java/com/vaadin/browserless/BrowserlessUIContext.java @@ -316,7 +316,9 @@ public static BrowserlessUIContext forComponent( * the tree until then, and the lookup returns an empty result rather than * failing, so reach those components through the owning component tester * instead: {@code GridTester.getCellComponent(row, column)} for grid cells, - * {@code ContextMenuTester.open()} or {@code clickItem(...)} for menus. + * {@code ContextMenuTester.open()} and then {@code clickItem(...)} for a + * context menu, and {@code GridTester.contextMenu(row).open()} for a grid + * context menu. * * @param componentType * the type of component to search for diff --git a/shared/src/main/java/com/vaadin/browserless/ComponentQuery.java b/shared/src/main/java/com/vaadin/browserless/ComponentQuery.java index ed3f89be..6a9d55de 100644 --- a/shared/src/main/java/com/vaadin/browserless/ComponentQuery.java +++ b/shared/src/main/java/com/vaadin/browserless/ComponentQuery.java @@ -41,6 +41,14 @@ * instance which searches through the whole component tree, or a * {@link com.vaadin.flow.component.Component} instance, which limits the search * to the component subtree. + *

+ * Not everything a view shows is in that tree. A component that another + * component renders per item, and the content of an overlay that is not open, + * are reached through the tester of the component that owns them: + * {@code GridTester.getCellComponent(row, column)} for grid cells, + * {@code ContextMenuTester.open()} or {@code GridTester.contextMenu(row)} for + * menus. A query returns an empty result rather than failing, so such a + * component reads as if it was never created. * * @param * the type of the component(s) to search for diff --git a/shared/src/main/java/com/vaadin/browserless/ComponentTester.java b/shared/src/main/java/com/vaadin/browserless/ComponentTester.java index c6156b88..fc0ae9e6 100644 --- a/shared/src/main/java/com/vaadin/browserless/ComponentTester.java +++ b/shared/src/main/java/com/vaadin/browserless/ComponentTester.java @@ -160,7 +160,9 @@ public void setModal(boolean modal) { * the tree until then, and the lookup returns an empty result rather than * failing, so reach those components through the owning component tester * instead: {@code GridTester.getCellComponent(row, column)} for grid cells, - * {@code ContextMenuTester.open()} or {@code clickItem(...)} for menus. + * {@code ContextMenuTester.open()} and then {@code clickItem(...)} for a + * context menu, and {@code GridTester.contextMenu(row).open()} for a grid + * context menu. * * @param componentType * type of the component to search. 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 72ef3ddb..6e8374bc 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 @@ -57,11 +57,13 @@ public ContextMenuTester(T component) { * simulates the server-side state changes that would occur when a user * opens the menu in the browser. *

- * Calling {@link #open()} is not required before using - * {@code clickItem(...)} or {@code find(Class)} methods. Those methods - * operate on the server component state and can be used regardless of the - * menu {@code opened} state. Use {@link #open()} only if you want to - * explicitly simulate the act of opening. + * A closed context menu is not attached to the UI, so, exactly as in the + * browser, its items cannot be interacted with: open the menu before + * calling {@code clickItem(...)}, {@code isItemChecked(...)} or + * {@code getItemTooltipText(...)}, otherwise they throw an + * {@link IllegalStateException}. Only {@code find(Class)} works on a closed + * menu, since it queries the menu contents instead of the UI; the + * components it returns are detached until the menu is opened. *

* A top level {@code find(...)} on the UI is a different matter: the menu * content is attached to the UI only while the menu is open, so it is found @@ -120,8 +122,8 @@ public void close() { * } * * - * Note: Opening the menu via {@link #open()} is not required before - * invoking this method; the lookup operates on server-side state. + * Note: the menu must be opened with {@link #open()} before an item can be + * clicked, since a closed menu is not attached to the UI. * * @param topLevelText * the text content of the top level menu item, not @@ -131,8 +133,9 @@ public void close() { * @throws IllegalArgumentException * if the provided text does not identify a menu item. * @throws IllegalStateException - * if there are multiple visible matching items at any level, or - * if the item at the given path is disabled or not visible. + * if the menu is not opened, if there are multiple visible + * matching items at any level, or if the item at the given path + * is disabled or not visible. */ public void clickItem(String topLevelText, String... nestedItemsText) { ensureComponentIsUsable(); @@ -171,8 +174,8 @@ public void clickItem(String topLevelText, String... nestedItemsText) { * } * * - * Note: Opening the menu via {@link #open()} is not required before - * invoking this method; the lookup operates on server-side state. + * Note: the menu must be opened with {@link #open()} before an item can be + * clicked, since a closed menu is not attached to the UI. * * @param topLevelPosition * the zero-based position of the item in the menu, as it will be @@ -183,7 +186,8 @@ public void clickItem(String topLevelText, String... nestedItemsText) { * @throws IllegalArgumentException * if the provided position does not identify a menu item. * @throws IllegalStateException - * if the item at the given position is disabled or not visible. + * if the menu is not opened, or if the item at the given + * position is disabled or not visible. */ public void clickItem(int topLevelPosition, int... nestedItemsPositions) { ensureComponentIsUsable(); @@ -228,7 +232,8 @@ public void clickItem(int topLevelPosition, int... nestedItemsPositions) { * if the provided text does not identify a menu item or if the * menu item is not checkable. * @throws IllegalStateException - * if the item at given path is not usable. + * if the menu is not opened, or if the item at given path is + * not usable. */ public boolean isItemChecked(String topLevelText, String... nestedItemsText) { @@ -278,7 +283,8 @@ public boolean isItemChecked(String topLevelText, * if the provided position does not identify a menu item or if * the menu item is not checkable. * @throws IllegalStateException - * if the item at given position is not usable. + * if the menu is not opened, or if the item at given position + * is not usable. */ public boolean isItemChecked(int topLevelPosition, int... nestedItemsPositions) { @@ -324,7 +330,8 @@ public boolean isItemChecked(int topLevelPosition, * @throws IllegalArgumentException * if the provided text does not identify a menu item. * @throws IllegalStateException - * if the item at given path is not usable. + * if the menu is not opened, or if the item at given path is + * not usable. * @since 1.1 */ public String getItemTooltipText(String topLevelText, @@ -372,7 +379,8 @@ public String getItemTooltipText(String topLevelText, * @throws IllegalArgumentException * if the provided position does not identify a menu item. * @throws IllegalStateException - * if the item at given position is not usable. + * if the menu is not opened, or if the item at given position + * is not usable. * @since 1.1 */ public String getItemTooltipText(int topLevelPosition,