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..d966ab37 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 @@ -470,4 +470,67 @@ void openAndFind_ContextMenuItemsCanBeAccessed() { Assertions.assertFalse(div.isAttached()); } + @Test + void getItemTexts_hiddenItemIgnored_componentItemHasNoText() { + ContextMenuTester menu_ = test(view.menu); + menu_.open(); + + Assertions.assertIterableEquals( + List.of("Foo", "Bar", "Text", "Duplicated", "Duplicated", "", + "Checkable", "Disabled", "Hierarchical", + "Duplicated Hidden"), + menu_.getItemTexts(), + "texts should be the visible items, in the order the browser shows them"); + } + + @Test + void getItemTexts_positionsMatchClickItemPositions() { + ContextMenuTester menu_ = test(view.menu); + menu_.open(); + + // Hidden is filtered out of both, so Hierarchical is at position 8 + // although it is the tenth item that was added + menu_.clickItem(menu_.getItemTexts().indexOf("Hierarchical"), 0); + + Assertions.assertIterableEquals(List.of("Hierarchical / Level2"), + view.clickedItems); + } + + @Test + void getItemTexts_subMenuByPath_hiddenItemIgnored() { + ContextMenuTester menu_ = test(view.menu); + menu_.open(); + + Assertions.assertIterableEquals( + List.of("Level2", "NestedSubMenu", "Nested Checkable", + "NestedDisabled"), + menu_.getItemTexts("Hierarchical"), + "NestedInvisible should not be reported"); + Assertions.assertIterableEquals(List.of("Level3"), + menu_.getItemTexts("Hierarchical", "NestedSubMenu")); + } + + @Test + void getItemTexts_itemWithoutSubMenu_throws() { + ContextMenuTester menu_ = test(view.menu); + menu_.open(); + + IllegalArgumentException exception = Assertions.assertThrows( + IllegalArgumentException.class, + () -> menu_.getItemTexts("Foo")); + Assertions.assertTrue( + exception.getMessage().contains("has no children"), + "expected the missing sub menu to be reported, but got: " + + exception.getMessage()); + } + + @Test + void getItemTexts_menuNotOpened_throws() { + ContextMenuTester menu_ = test(view.menu); + + Assertions.assertThrows(IllegalStateException.class, + menu_::getItemTexts); + Assertions.assertThrows(IllegalStateException.class, + () -> menu_.getItemTexts("Hierarchical")); + } } 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 df72501f..51b61733 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 @@ -335,4 +335,50 @@ void contextMenuFromGridTester_gridWithSeveralContextMenus_throws() { "expected the ambiguity to be reported, but got: " + exception.getMessage()); } + + @Test + void getItemTexts_openMenu_hiddenItemIgnored_subMenuByPath() { + GridContextMenuTester, String> menu_ = test( + view.menu); + menu_.open(0); + + Assertions.assertIterableEquals( + List.of("Edit", "", "Checkable", "Disabled", "Share"), + menu_.getItemTexts(), + "texts should be the visible items, with no text for the checkbox item"); + + Assertions.assertIterableEquals(List.of("Copy link", "Email"), + menu_.getItemTexts("Share")); + } + + @Test + void getItemTexts_menuNotOpened_throws() { + GridContextMenuTester, String> menu_ = test( + view.menu); + + Assertions.assertThrows(IllegalStateException.class, + menu_::getItemTexts); + Assertions.assertThrows(IllegalStateException.class, + () -> menu_.getItemTexts("Share")); + } + + @Test + void getItemTexts_dynamicContentHandler_reportsItemsForTheOpenedRow() { + GridMenuItem edit = view.menu.getItems().get(0); + view.menu.setDynamicContentHandler(item -> { + edit.setVisible(GridContextMenuView.BOB.equals(item)); + return true; + }); + GridContextMenuTester, String> menu_ = test( + view.menu); + + menu_.open(1); + Assertions.assertTrue(menu_.getItemTexts().contains("Edit"), + "Edit should be reported for the row the handler shows it for"); + + menu_.close(); + menu_.open(0); + Assertions.assertFalse(menu_.getItemTexts().contains("Edit"), + "Edit should not be reported for a row the handler hides it for"); + } } diff --git a/junit6/src/test/java/com/vaadin/flow/component/menubar/MenuBarTesterTest.java b/junit6/src/test/java/com/vaadin/flow/component/menubar/MenuBarTesterTest.java index 6c04c01f..6275264a 100644 --- a/junit6/src/test/java/com/vaadin/flow/component/menubar/MenuBarTesterTest.java +++ b/junit6/src/test/java/com/vaadin/flow/component/menubar/MenuBarTesterTest.java @@ -306,4 +306,29 @@ void getItemTooltipText_notExisting_throws() { () -> menu_.getItemTooltipText(22)); } + @Test + void getItemTexts_hiddenItemIgnored_subMenuByPath() { + Assertions.assertIterableEquals( + List.of("Foo", "Bar", "Text", "Duplicated", "Duplicated", + "Checkables", "Disabled", "Hierarchical"), + menu_.getItemTexts(), + "texts should be the visible items, in the order the browser shows them"); + + Assertions.assertIterableEquals( + List.of("Level2", "NestedSubMenu", "Nested Checkable", + "NestedDisabled"), + menu_.getItemTexts("Hierarchical"), + "NestedInvisible should not be reported"); + } + + @Test + void getItemTexts_disabledMenuBar_itemsAreStillReported() { + view.menu.setEnabled(false); + + Assertions.assertIterableEquals( + List.of("Foo", "Bar", "Text", "Duplicated", "Duplicated", + "Checkables", "Disabled", "Hierarchical"), + menu_.getItemTexts(), + "a user can read the items of a disabled menu bar"); + } } diff --git a/shared/src/main/java/com/vaadin/browserless/internal/MenuItemNavigation.java b/shared/src/main/java/com/vaadin/browserless/internal/MenuItemNavigation.java index 6959e3bd..b6164bb1 100644 --- a/shared/src/main/java/com/vaadin/browserless/internal/MenuItemNavigation.java +++ b/shared/src/main/java/com/vaadin/browserless/internal/MenuItemNavigation.java @@ -116,6 +116,55 @@ public static , I extends MenuItemBase + * An item created from a component has no text of its own, so it is + * reported as an empty string. + * + * @param items + * the items of a menu level + * @return the texts of the visible items + */ + public static List visibleTexts( + List> items) { + return items.stream().filter(Component::isVisible) + .map(MenuItemBase::getText).collect(Collectors.toList()); + } + + /** + * Gets the texts of the visible items of the sub menu of the item addressed + * by the given text path. + * + * @param rootItems + * the items of the top level menu + * @param topLevelText + * the text content of the top level menu item, not + * {@literal null} + * @param nestedItemsText + * text content of the nested menu items + * @param + * menu type + * @param + * menu item type + * @param + * sub menu type + * @return the texts of the visible items of the sub menu + * @throws IllegalArgumentException + * if the provided text does not identify a menu item, or if the + * item at the given path has no sub menu + * @throws IllegalStateException + * if there are multiple matching items at any level, or if the + * item at the given path is disabled or not visible + */ + public static , I extends MenuItemBase, S extends SubMenuBase> List visibleSubMenuTexts( + List rootItems, String topLevelText, String... nestedItemsText) { + I menuItem = findByPath(rootItems, topLevelText, nestedItemsText); + ensureParentItem(menuItem, pathToString(topLevelText, nestedItemsText)); + return visibleTexts(menuItem.getSubMenu().getItems()); + } + /** * Ensures that the given menu item is checkable, as only a checkable item * has a checked state to read. 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..04a724eb 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 @@ -15,6 +15,8 @@ */ package com.vaadin.flow.component.contextmenu; +import java.util.List; + import com.vaadin.browserless.ComponentQuery; import com.vaadin.browserless.ComponentTester; import com.vaadin.browserless.Tests; @@ -383,6 +385,94 @@ public String getItemTooltipText(int topLevelPosition, return menuItem.getElement().getProperty("tooltip"); } + /** + * Gets the texts of the menu items, as the browser shows them. + *

+ * Hidden items are ignored, so the returned texts are aligned with the + * positions used by {@link #clickItem(int, int...)}. A text can also be + * given to {@link #clickItem(String, String...)}, as long as it identifies + * a single enabled item: a text that several visible items share is + * ambiguous, and a disabled item cannot be clicked. + *

+ * An item created from a component has no text of its own, and is reported + * as an empty string. Use {@link #find(Class)} to reach such an item. + *

+ * The menu has to be open, since its items are not part of the UI before + * that. + * + *

+     * {@code
+     *
+     * menu.addItem("Preview", event -> {
+     * });
+     * menu.addItem("Hidden", event -> {
+     * }).setVisible(false);
+     * menu.addItem("Share");
+     *
+     * // ["Preview", "Share"]
+     * tester.getItemTexts();
+     * }
+     * 
+ * + * @return the texts of the visible top level menu items, in the order they + * are shown in + * @throws IllegalStateException + * if the menu is not open, or is not visible + */ + public List getItemTexts() { + ensureVisible(); + return MenuItemNavigation.visibleTexts(getComponent().getItems()); + } + + /** + * Gets the texts of the items of the sub menu of the item matching the + * given text, as the browser shows them. + *

+ * For a nested sub menu, provide the text of each menu item in the + * hierarchy, the same way as in {@link #clickItem(String, String...)}. + *

+ * Hidden items are ignored at every level, both when following the path and + * in the returned texts. + *

+ * The menu has to be open, since its items are not part of the UI before + * that. + * + *

+     * {@code
+     *
+     * var subMenu = menu.addItem("Share").getSubMenu();
+     * subMenu.addItem("Copy link", event -> {
+     * });
+     * subMenu.addItem("Email", event -> {
+     * });
+     *
+     * // ["Copy link", "Email"]
+     * tester.getItemTexts("Share");
+     * }
+     * 
+ * + * @param topLevelText + * the text content of the top level menu item, not + * {@literal null}. + * @param nestedItemsText + * text content of the nested menu items + * @return the texts of the visible items of the sub menu, in the order they + * are shown in + * @throws IllegalArgumentException + * if the provided text does not identify a menu item, or if the + * item at the given path has no sub menu. + * @throws IllegalStateException + * if the menu is not open or not visible, if there are multiple + * matching items at any level, or if the item at the given path + * is disabled or not visible. + */ + public List getItemTexts(String topLevelText, + String... nestedItemsText) { + ensureVisible(); + return MenuItemNavigation.visibleSubMenuTexts(getComponent().getItems(), + topLevelText, nestedItemsText); + } + /** * {@inheritDoc} *

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 b1b42f25..a89af38e 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 @@ -15,6 +15,8 @@ */ package com.vaadin.flow.component.grid.contextmenu; +import java.util.List; + import tools.jackson.databind.node.ObjectNode; import com.vaadin.browserless.ComponentQuery; @@ -363,6 +365,99 @@ public String getItemTooltipText(int topLevelPosition, .getElement().getProperty("tooltip"); } + /** + * Gets the texts of the menu items, as the browser shows them. + *

+ * Hidden items are ignored, so the returned texts are aligned with the + * positions used by {@link #clickItem(int, int...)}. A text can also be + * given to {@link #clickItem(String, String...)}, as long as it identifies + * a single enabled item: a text that several visible items share is + * ambiguous, and a disabled item cannot be clicked. + *

+ * An item created from a component has no text of its own, and is reported + * as an empty string. Use {@link #find(Class)} to reach such an item. + *

+ * The menu has to be open, since its items are not part of the UI before + * that. The items are the ones the menu offers for the row it was opened + * on, so a dynamic content handler has run by then. + * + *

+     * {@code
+     *
+     * menu.addItem("Edit", event -> {
+     * });
+     * menu.addItem("Hidden", event -> {
+     * }).setVisible(false);
+     * menu.addItem("Share");
+     *
+     * tester.open(0);
+     *
+     * // ["Edit", "Share"]
+     * tester.getItemTexts();
+     * }
+     * 
+ * + * @return the texts of the visible top level menu items, in the order they + * are shown in + * @throws IllegalStateException + * if the menu is not open, or is not visible + */ + public List getItemTexts() { + ensureVisible(); + return MenuItemNavigation.visibleTexts(getComponent().getItems()); + } + + /** + * Gets the texts of the items of the sub menu of the item matching the + * given text, as the browser shows them. + *

+ * For a nested sub menu, provide the text of each menu item in the + * hierarchy, the same way as in {@link #clickItem(String, String...)}. + *

+ * Hidden items are ignored at every level, both when following the path and + * in the returned texts. + *

+ * The menu has to be open, since its items are not part of the UI before + * that. + * + *

+     * {@code
+     *
+     * var subMenu = menu.addItem("Share").getSubMenu();
+     * subMenu.addItem("Copy link", event -> {
+     * });
+     * subMenu.addItem("Email", event -> {
+     * });
+     *
+     * tester.open(0);
+     *
+     * // ["Copy link", "Email"]
+     * tester.getItemTexts("Share");
+     * }
+     * 
+ * + * @param topLevelText + * the text content of the top level menu item, not + * {@literal null}. + * @param nestedItemsText + * text content of the nested menu items + * @return the texts of the visible items of the sub menu, in the order they + * are shown in + * @throws IllegalArgumentException + * if the provided text does not identify a menu item, or if the + * item at the given path has no sub menu. + * @throws IllegalStateException + * if the menu is not open or not visible, if there are multiple + * matching items at any level, or if the item at the given path + * is disabled or not visible. + */ + public List getItemTexts(String topLevelText, + String... nestedItemsText) { + ensureVisible(); + return MenuItemNavigation.visibleSubMenuTexts(getComponent().getItems(), + topLevelText, nestedItemsText); + } + /** * {@inheritDoc} *

diff --git a/shared/src/main/java/com/vaadin/flow/component/menubar/MenuBarTester.java b/shared/src/main/java/com/vaadin/flow/component/menubar/MenuBarTester.java index 1eb809d8..1cba462e 100644 --- a/shared/src/main/java/com/vaadin/flow/component/menubar/MenuBarTester.java +++ b/shared/src/main/java/com/vaadin/flow/component/menubar/MenuBarTester.java @@ -15,6 +15,8 @@ */ package com.vaadin.flow.component.menubar; +import java.util.List; + import com.vaadin.browserless.ComponentTester; import com.vaadin.browserless.Tests; import com.vaadin.browserless.internal.MenuItemNavigation; @@ -321,6 +323,92 @@ public String getItemTooltipText(int topLevelPosition, return menuItem.getElement().getProperty("tooltip"); } + /** + * Gets the texts of the menu items, as the browser shows them. + *

+ * Hidden items are ignored, so the returned texts are aligned with the + * positions used by {@link #clickItem(int, int...)}. A text can also be + * given to {@link #clickItem(String, String...)}, as long as it identifies + * a single enabled item: a text that several visible items share is + * ambiguous, and a disabled item cannot be clicked. + *

+ * An item created from a component has no text of its own, and is reported + * as an empty string. Use {@link #find(Class)} to reach such an item. + *

+ * All items of the menu bar are reported. A browser collapses the items + * that do not fit into an overflow menu, which cannot be simulated without + * a layout. + * + *

+     * {@code
+     *
+     * menu.addItem("Preview", event -> {
+     * });
+     * menu.addItem("Hidden", event -> {
+     * }).setVisible(false);
+     * menu.addItem("Share");
+     *
+     * // ["Preview", "Share"]
+     * wrapper.getItemTexts();
+     * }
+     * 
+ * + * @return the texts of the visible top level menu items, in the order they + * are shown in + * @throws IllegalStateException + * if the menu bar is not visible + */ + public List getItemTexts() { + ensureVisible(); + return MenuItemNavigation.visibleTexts(getComponent().getItems()); + } + + /** + * Gets the texts of the items of the sub menu of the item matching the + * given text, as the browser shows them. + *

+ * For a nested sub menu, provide the text of each menu item in the + * hierarchy, the same way as in {@link #clickItem(String, String...)}. + *

+ * Hidden items are ignored at every level, both when following the path and + * in the returned texts. + * + *

+     * {@code
+     *
+     * var subMenu = menu.addItem("Share").getSubMenu();
+     * subMenu.addItem("Copy link", event -> {
+     * });
+     * subMenu.addItem("Email", event -> {
+     * });
+     *
+     * // ["Copy link", "Email"]
+     * wrapper.getItemTexts("Share");
+     * }
+     * 
+ * + * @param topLevelText + * the text content of the top level menu item, not + * {@literal null}. + * @param nestedItemsText + * text content of the nested menu items + * @return the texts of the visible items of the sub menu, in the order they + * are shown in + * @throws IllegalArgumentException + * if the provided text does not identify a menu item, or if the + * item at the given path has no sub menu. + * @throws IllegalStateException + * if the menu bar is not visible, if there are multiple + * matching items at any level, or if the item at the given path + * is disabled or not visible. + */ + public List getItemTexts(String topLevelText, + String... nestedItemsText) { + ensureVisible(); + return MenuItemNavigation.visibleSubMenuTexts(getComponent().getItems(), + topLevelText, nestedItemsText); + } + private MenuItem findMenuItemByPath(String topLevelText, String... nestedItemsText) { return MenuItemNavigation.findByPath(getComponent().getItems(),