Index: trunk/test/unit/org/openstreetmap/josm/gui/dialogs/NotesDialogTest.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/gui/dialogs/NotesDialogTest.java	(revision 18452)
+++ trunk/test/unit/org/openstreetmap/josm/gui/dialogs/NotesDialogTest.java	(revision 18454)
@@ -2,21 +2,32 @@
 package org.openstreetmap.josm.gui.dialogs;
 
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.openstreetmap.josm.tools.I18n.tr;
 
 import java.time.Instant;
+import java.util.Collections;
 
 import javax.swing.JLabel;
 import javax.swing.JList;
 
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.platform.commons.util.ReflectionUtils;
+import org.openstreetmap.josm.TestUtils;
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.notes.Note;
 import org.openstreetmap.josm.data.notes.NoteComment;
 import org.openstreetmap.josm.data.osm.User;
+import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.dialogs.NotesDialog.NoteRenderer;
+import org.openstreetmap.josm.gui.layer.NoteLayer;
+import org.openstreetmap.josm.gui.widgets.JosmTextField;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
 import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
-
-import org.junit.jupiter.api.Test;
+import org.openstreetmap.josm.testutils.mockers.ExtendedDialogMocker;
 
 /**
@@ -25,4 +36,7 @@
 @BasicPreferences
 class NotesDialogTest {
+    /** Only needed for {@link #testTicket21558} */
+    @RegisterExtension
+    JOSMTestRules rules = new JOSMTestRules().main().projection();
     private Note createMultiLineNote() {
         Note note = new Note(LatLon.ZERO);
@@ -56,3 +70,30 @@
         assertFalse(NotesDialog.matchesNote("reopened", note));
     }
+
+    /**
+     * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/21558>#21558</a>
+     */
+    @Test
+    void testTicket21558() throws Exception {
+        TestUtils.assumeWorkingJMockit();
+        new ExtendedDialogMocker(Collections.singletonMap(tr("Close note"), tr("Close note"))) {
+            @Override
+            protected String getString(ExtendedDialog instance) {
+                return instance.getTitle();
+            }
+        };
+        final NotesDialog notesDialog = new NotesDialog();
+        final NotesDialog.CloseAction closeAction = (NotesDialog.CloseAction) ReflectionUtils
+                .tryToReadFieldValue(NotesDialog.class, "closeAction", notesDialog).get();
+        final JosmTextField filter = (JosmTextField) ReflectionUtils
+                .tryToReadFieldValue(NotesDialog.class, "filter", notesDialog).get();
+        final NoteLayer noteLayer = new NoteLayer();
+        MainApplication.getLayerManager().addLayer(noteLayer);
+        final Note note = createMultiLineNote();
+        note.setState(Note.State.OPEN);
+        noteLayer.getNoteData().addNotes(Collections.singleton(note));
+        noteLayer.getNoteData().setSelectedNote(note);
+        filter.setText("open");
+        assertDoesNotThrow(() -> closeAction.actionPerformed(null));
+    }
 }
Index: trunk/test/unit/org/openstreetmap/josm/testutils/mockers/ExtendedDialogMocker.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/testutils/mockers/ExtendedDialogMocker.java	(revision 18452)
+++ trunk/test/unit/org/openstreetmap/josm/testutils/mockers/ExtendedDialogMocker.java	(revision 18454)
@@ -143,4 +143,35 @@
     }
 
+    /**
+     * Get the result field for an extended dialog instance
+     * @param instance The instance to get the result field for
+     * @return The result field. May be private.
+     * @throws NoSuchFieldException If the field cannot be found. Should never be thrown.
+     */
+    protected Field getResultField(ExtendedDialog instance) throws NoSuchFieldException {
+        // Note that subclasses of ExtendedDialogMocker will not have "result" as a declared field.
+        // Iterate up the chain until we get to a field that has "result" as a declared field.
+        // Only reason for this is just in case someone overrides the logic in ExtendedDialog.
+        Class<?> clazz = instance.getClass();
+        Field resultField = null;
+        // Store the exception, if any
+        NoSuchFieldException noSuchFieldException = null;
+        while (!Object.class.equals(clazz) && resultField == null) {
+            try {
+                resultField = clazz.getDeclaredField("result");
+            } catch (NoSuchFieldException e) {
+                clazz = instance.getClass().getSuperclass();
+                // Only save the first exception
+                if (noSuchFieldException == null) {
+                    noSuchFieldException = e;
+                }
+            }
+        }
+        if (resultField == null) {
+            throw noSuchFieldException;
+        }
+        return resultField;
+    }
+
     @Mock
     private void setupDialog(final Invocation invocation) {
@@ -160,5 +191,5 @@
                 final int mockResult = this.getMockResult(instance);
                 // TODO check validity of mockResult?
-                Field resultField = instance.getClass().getDeclaredField("result");
+                final Field resultField = this.getResultField(instance);
                 resultField.setAccessible(true);
                 resultField.set(instance, mockResult);
