diff --git a/addOns/client/CHANGELOG.md b/addOns/client/CHANGELOG.md index ff2434dbee9..dc9bce27293 100644 --- a/addOns/client/CHANGELOG.md +++ b/addOns/client/CHANGELOG.md @@ -4,7 +4,8 @@ All notable changes to this add-on will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased - +### Changed +- Normalise behaviour of Delete context menu item. ## [0.26.0] - 2026-05-27 ### Added diff --git a/addOns/client/src/main/java/org/zaproxy/addon/client/ui/PopupMenuClientDelete.java b/addOns/client/src/main/java/org/zaproxy/addon/client/ui/PopupMenuClientDelete.java index 9e6abe76555..c70ad2957d1 100644 --- a/addOns/client/src/main/java/org/zaproxy/addon/client/ui/PopupMenuClientDelete.java +++ b/addOns/client/src/main/java/org/zaproxy/addon/client/ui/PopupMenuClientDelete.java @@ -20,9 +20,11 @@ package org.zaproxy.addon.client.ui; import java.awt.event.ActionEvent; +import java.util.List; import javax.swing.JOptionPane; import org.parosproxy.paros.Constant; import org.parosproxy.paros.view.View; +import org.zaproxy.addon.client.internal.ClientNode; public class PopupMenuClientDelete extends PopupMenuItemClient { @@ -32,6 +34,12 @@ public PopupMenuClientDelete(ClientMapPanel clientMapPanel) { super(Constant.messages.getString("client.tree.popup.delete"), clientMapPanel); } + @Override + public boolean isButtonEnabled() { + List nodes = getClientMapPanel().getSelectedNodes(); + return !(nodes.size() == 1 && nodes.get(0).isRoot()); + } + @Override public void performAction(ActionEvent e) { if (View.getSingleton() diff --git a/addOns/client/src/main/java/org/zaproxy/addon/client/ui/PopupMenuItemClient.java b/addOns/client/src/main/java/org/zaproxy/addon/client/ui/PopupMenuItemClient.java index 24acfe3ad15..941d3d54299 100644 --- a/addOns/client/src/main/java/org/zaproxy/addon/client/ui/PopupMenuItemClient.java +++ b/addOns/client/src/main/java/org/zaproxy/addon/client/ui/PopupMenuItemClient.java @@ -40,12 +40,24 @@ public boolean isEnableForComponent(Component invoker) { if (invoker instanceof JTree) { JTree tree = (JTree) invoker; if (ClientMapPanel.CLIENT_TREE_NAME.equals(tree.getName())) { + setEnabled(isButtonEnabled()); return true; } } return false; } + /** + * Tells whether or not the menu item should be enabled for the state of the client map. + * + * @return {@code true} if the menu item should be enabled, {@code false} otherwise. + * @see #isEnabled() + * @see #getClientMapPanel() + */ + protected boolean isButtonEnabled() { + return true; + } + public ClientMapPanel getClientMapPanel() { return clientMapPanel; } diff --git a/addOns/client/src/test/java/org/zaproxy/addon/client/ui/PopupMenuClientDeleteUnitTest.java b/addOns/client/src/test/java/org/zaproxy/addon/client/ui/PopupMenuClientDeleteUnitTest.java new file mode 100644 index 00000000000..ba0653d7e2d --- /dev/null +++ b/addOns/client/src/test/java/org/zaproxy/addon/client/ui/PopupMenuClientDeleteUnitTest.java @@ -0,0 +1,99 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2026 The ZAP Development Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zaproxy.addon.client.ui; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.zaproxy.addon.client.internal.ClientNode; + +/** Unit test for {@link PopupMenuClientDelete}. */ +class PopupMenuClientDeleteUnitTest { + + private ClientMapPanel clientMapPanel; + private PopupMenuClientDelete menuItem; + + @BeforeEach + void setUp() { + clientMapPanel = mock(ClientMapPanel.class); + menuItem = new PopupMenuClientDelete(clientMapPanel); + } + + @Test + void shouldDisableItemWhenOnlyRootSelected() { + // Given + ClientNode root = mock(ClientNode.class); + given(root.isRoot()).willReturn(true); + given(clientMapPanel.getSelectedNodes()).willReturn(List.of(root)); + + // When + boolean result = menuItem.isButtonEnabled(); + + // Then + assertThat(result, is(false)); + } + + @Test + void shouldEnableItemWhenNonRootNodeSelected() { + // Given + ClientNode node = mock(ClientNode.class); + given(node.isRoot()).willReturn(false); + given(clientMapPanel.getSelectedNodes()).willReturn(List.of(node)); + + // When + boolean result = menuItem.isButtonEnabled(); + + // Then + assertThat(result, is(true)); + } + + @Test + void shouldEnableItemWhenMultipleNodesSelected() { + // Given + ClientNode root = mock(ClientNode.class); + given(root.isRoot()).willReturn(true); + ClientNode node = mock(ClientNode.class); + given(node.isRoot()).willReturn(false); + given(clientMapPanel.getSelectedNodes()).willReturn(List.of(root, node)); + + // When + boolean result = menuItem.isButtonEnabled(); + + // Then + assertThat(result, is(true)); + } + + @Test + void shouldEnableItemWhenNoNodesSelected() { + // Given + given(clientMapPanel.getSelectedNodes()).willReturn(List.of()); + + // When + boolean result = menuItem.isButtonEnabled(); + + // Then + assertThat(result, is(true)); + } +} diff --git a/addOns/client/src/test/java/org/zaproxy/addon/client/ui/PopupMenuItemClientUnitTest.java b/addOns/client/src/test/java/org/zaproxy/addon/client/ui/PopupMenuItemClientUnitTest.java new file mode 100644 index 00000000000..535484cf0f2 --- /dev/null +++ b/addOns/client/src/test/java/org/zaproxy/addon/client/ui/PopupMenuItemClientUnitTest.java @@ -0,0 +1,137 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2026 The ZAP Development Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zaproxy.addon.client.ui; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +import java.awt.event.ActionEvent; +import javax.swing.JButton; +import javax.swing.JTree; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** Unit test for {@link PopupMenuItemClient}. */ +class PopupMenuItemClientUnitTest { + + private ClientMapPanel clientMapPanel; + + @BeforeEach + void setup() { + clientMapPanel = mock(ClientMapPanel.class); + } + + @Test + void shouldReturnTrueForIsButtonEnabledByDefault() { + // Given / When + PopupMenuItemClient menuItem = + new PopupMenuItemClient("Name", clientMapPanel) { + @Override + void performAction(ActionEvent e) { + // Nothing to do. + } + }; + + // Then + assertThat(menuItem.isButtonEnabled(), is(true)); + } + + @Test + void shouldNotBeEnabledForNonTreeComponent() { + // Given + PopupMenuItemClient menuItem = new TestPopupMenuItemClient(clientMapPanel); + JButton nonTree = mock(JButton.class); + + // When + boolean result = menuItem.isEnableForComponent(nonTree); + + // Then + assertThat(result, is(false)); + } + + @Test + void shouldNotBeEnabledForTreeWithWrongName() { + // Given + PopupMenuItemClient menuItem = new TestPopupMenuItemClient(clientMapPanel); + JTree tree = createTree("otherTree"); + + // When + boolean result = menuItem.isEnableForComponent(tree); + + // Then + assertThat(result, is(false)); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void shouldReflectButtonEnabledStateForClientTree(boolean state) { + // Given + PopupMenuItemClient menuItem = new TestPopupMenuItemClient(clientMapPanel, state); + JTree tree = createClientTree(); + + // When + boolean result = menuItem.isEnableForComponent(tree); + + // Then + assertThat(result, is(true)); + assertThat(menuItem.isEnabled(), is(state)); + } + + private static JTree createClientTree() { + return createTree(ClientMapPanel.CLIENT_TREE_NAME); + } + + private static JTree createTree(String name) { + JTree tree = mock(JTree.class); + given(tree.getName()).willReturn(name); + return tree; + } + + private static class TestPopupMenuItemClient extends PopupMenuItemClient { + + private static final long serialVersionUID = 1L; + + private final boolean buttonEnabled; + + TestPopupMenuItemClient(ClientMapPanel clientMapPanel) { + this(clientMapPanel, false); + } + + TestPopupMenuItemClient(ClientMapPanel clientMapPanel, boolean buttonEnabled) { + super("Test", clientMapPanel); + + this.buttonEnabled = buttonEnabled; + } + + @Override + protected boolean isButtonEnabled() { + return buttonEnabled; + } + + @Override + void performAction(ActionEvent e) { + // Nothing to do. + } + } +}