Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package com.vaadin.flow.component.contextmenu;

import java.util.ArrayList;
import java.util.List;

import org.junit.jupiter.api.Assertions;
Expand Down Expand Up @@ -51,6 +52,19 @@ void openCloseMenu_menuIsAttachedAndDetached() {
"context menu should be detached from the UI, but was not");
}

@Test
void openCloseMenu_openedChangeEventsComeFromClient() {
List<Boolean> 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();
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
23 changes: 23 additions & 0 deletions shared/src/main/java/com/vaadin/browserless/ComponentTester.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,18 @@ 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);
}

/**
* Closes the context menu.
*/
public void close() {
ensureComponentIsUsable();
getComponent().getElement().setProperty("opened", false);
setPropertyAsUser("opened", false);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

/**
Expand Down
Loading