From 37214cb44cc46815b8082ddc4ddc9a918dbbb3ee Mon Sep 17 00:00:00 2001 From: thc202 Date: Tue, 19 May 2026 17:42:04 +0100 Subject: [PATCH] client: normalise context menu delete behaviour Make it behave like the delete of the Sites tree, show it disabled if only the root node is selected (since the root node can't be deleted). Signed-off-by: thc202 --- addOns/client/CHANGELOG.md | 3 +- .../client/ui/PopupMenuClientDelete.java | 8 + .../addon/client/ui/PopupMenuItemClient.java | 12 ++ .../ui/PopupMenuClientDeleteUnitTest.java | 99 +++++++++++++ .../ui/PopupMenuItemClientUnitTest.java | 137 ++++++++++++++++++ 5 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 addOns/client/src/test/java/org/zaproxy/addon/client/ui/PopupMenuClientDeleteUnitTest.java create mode 100644 addOns/client/src/test/java/org/zaproxy/addon/client/ui/PopupMenuItemClientUnitTest.java 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. + } + } +}