From f40e654e977c3f9a22534bedcef60f1408151afd Mon Sep 17 00:00:00 2001 From: Heiko Klare Date: Fri, 28 Aug 2026 12:01:04 +0200 Subject: [PATCH] Find/Replace overlay: test its key handling against a real editor The overlay's existing tests drive it on a bare text viewer, which has no workbench part behind it. Everything about how the overlay and its host editor compete for a keystroke depends on there being a part, so none of it is currently covered: the overlay could start letting the editor act on keys typed into its input fields without any test noticing. Adds an end-to-end test that opens a real editor and reaches the overlay through FindReplaceAction, the way a user does. It observes only where a command ends up taking effect, never how that is arranged, so that it keeps describing what the overlay owes its users regardless of how the arbitration is implemented: keys typed into an input field do not act on the document, Select All applies to the focused field, Enter finds in the search field and replaces in the replace one, window commands such as Save still reach the editor, the editor stays the active part, and it acts on its keys again once the overlay loses focus. Key strokes are delivered by notifying the widget rather than by posting native events, so the display filter the key binding dispatcher installs is exercised without depending on the operating system to deliver anything. Because that path is easy to get wrong in a way that would make the test vacuous, the test also asserts that a key the overlay does bind takes effect. A real editor is needed but the workspace and the IDE are not, so the test contributes a minimal editor of its own rather than opening a file. Extensions from this bundle were previously ignored because it was not marked as a singleton. Assisted-by: Claude Opus 5 --- .../META-INF/MANIFEST.MF | 2 +- .../build.properties | 1 + .../plugin.xml | 15 + .../FindReplaceOverlayInEditorTest.java | 300 ++++++++++++++++++ .../overlay/TestTextEditor.java | 79 +++++ .../overlay/TestTextEditorInput.java | 63 ++++ .../tests/WorkbenchTextEditorTestSuite.java | 2 + 7 files changed, 461 insertions(+), 1 deletion(-) create mode 100644 tests/org.eclipse.ui.workbench.texteditor.tests/plugin.xml create mode 100644 tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayInEditorTest.java create mode 100644 tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/TestTextEditor.java create mode 100644 tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/TestTextEditorInput.java diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/META-INF/MANIFEST.MF b/tests/org.eclipse.ui.workbench.texteditor.tests/META-INF/MANIFEST.MF index 4aaef1babe3..248700019c5 100644 --- a/tests/org.eclipse.ui.workbench.texteditor.tests/META-INF/MANIFEST.MF +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/META-INF/MANIFEST.MF @@ -1,7 +1,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %Plugin.name -Bundle-SymbolicName: org.eclipse.ui.workbench.texteditor.tests +Bundle-SymbolicName: org.eclipse.ui.workbench.texteditor.tests;singleton:=true Bundle-Version: 3.15.0.qualifier Bundle-Vendor: %Plugin.providerName Bundle-Localization: plugin diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/build.properties b/tests/org.eclipse.ui.workbench.texteditor.tests/build.properties index f9a3ba9a689..7077b165d0e 100644 --- a/tests/org.eclipse.ui.workbench.texteditor.tests/build.properties +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/build.properties @@ -13,6 +13,7 @@ # Mickael Istria (Red Hat Inc.) - 419531 Get rid of nested jars ############################################################################### bin.includes = plugin.properties,\ + plugin.xml,\ test.xml,\ about.html,\ .,\ diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/plugin.xml b/tests/org.eclipse.ui.workbench.texteditor.tests/plugin.xml new file mode 100644 index 00000000000..8ac744fdebd --- /dev/null +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/plugin.xml @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayInEditorTest.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayInEditorTest.java new file mode 100644 index 00000000000..05e6df9c147 --- /dev/null +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayInEditorTest.java @@ -0,0 +1,300 @@ +/******************************************************************************* + * Copyright (c) 2026 Vector Informatik GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.ui.internal.findandreplace.overlay; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ResourceBundle; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Text; + +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.ITextSelection; + +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.handlers.IHandlerService; + +import org.eclipse.ui.texteditor.FindReplaceAction; +import org.eclipse.ui.texteditor.StatusTextEditor; + +/** + * End-to-end test for whether a command acts on the Find/Replace overlay or on + * the editor it is placed on. + *

+ * The overlay's input fields sit inside the editor's widget tree, so the editor + * remains the active part while a search term is typed, and its key bindings and + * handlers would otherwise carry out keys meant for an input field. How that is + * prevented is an implementation concern and deliberately not observed here; only + * where a command ends up taking effect is. + *

+ * Key strokes are delivered by notifying the widget rather than by posting native + * events, which still runs the display filter the key binding dispatcher installs + * but does not depend on the operating system, so the test also works headless. + * The native editing inside the SWT text widget does not happen that way, so the + * assertions say what must not reach the editor, plus one control + * proving that keys are dispatched at all. + *

+ * Unlike {@link FindReplaceOverlayTest}, which uses a bare viewer, this needs a + * real editor part: without one there is nothing to arbitrate. + */ +public class FindReplaceOverlayInEditorTest { + + private static final String CONTENT = "word one word two"; //$NON-NLS-1$ + + /** Tagged onto the widgets under {@link FindReplaceOverlay#ID_DATA_KEY}. */ + private static final String SEARCH_FIELD = "searchInput"; //$NON-NLS-1$ + + private static final String REPLACE_FIELD = "replaceInput"; //$NON-NLS-1$ + + private StatusTextEditor editor; + + private Text searchField; + + @BeforeEach + void openEditorWithOverlay() throws PartInitException { + PlatformUI.getWorkbench().getWorkbenchWindows()[0].getShell().forceActive(); + IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + editor = (StatusTextEditor) page.openEditor(new TestTextEditorInput(CONTENT), TestTextEditor.ID); + processPendingEvents(); + + // Opening through the action rather than through its internals also asserts + // that an editor of this kind gets the overlay rather than the dialog. + new FindReplaceAction(ResourceBundle.getBundle("org.eclipse.ui.texteditor.ConstructedEditorMessages"), //$NON-NLS-1$ + "Editor.FindReplace.", editor).run(); //$NON-NLS-1$ + processPendingEvents(); + + searchField = focusedInputField("opening the overlay is expected to focus its search field", //$NON-NLS-1$ + SEARCH_FIELD); + } + + @AfterEach + void closeEditor() { + searchField = null; + if (editor != null) { + editor.getSite().getPage().closeEditor(editor, false); + editor = null; + } + } + + /** + * A key typed into an input field belongs to that field: it must never be + * carried out on the editor's document instead. + */ + @Test + public void testKeysTypedIntoTheOverlayDoNotReachTheEditor() { + focusSearchField(); + int selectionLengthBefore = editorSelectionLength(); + + type(searchField, SWT.MOD1, 'a'); + + assertEquals(selectionLengthBefore, editorSelectionLength(), + "Select All's key binding must not select the editor's document"); //$NON-NLS-1$ + assertEquals(CONTENT, documentText(), "the document must not change"); //$NON-NLS-1$ + + type(searchField, SWT.MOD1, SWT.DEL); + + assertEquals(CONTENT, documentText(), + "delete-next-word's key binding must not delete from the document"); //$NON-NLS-1$ + + // Control: a key the overlay does bind must take effect, otherwise the + // assertions above would hold simply because no key was dispatched at all. + assertTrue(searchField.isVisible(), "precondition: the overlay is open"); //$NON-NLS-1$ + type(searchField, SWT.NONE, SWT.ESC); + assertFalse(searchField.isVisible(), "Escape in the search field must close the overlay"); //$NON-NLS-1$ + } + + /** + * The retargetable global actions must act on the focused input field, so that + * Edit > Select All does what the user expects while typing a search term. + */ + @Test + public void testSelectAllActsOnTheFocusedInputField() throws Exception { + focusSearchField(); + searchField.setText("abc"); //$NON-NLS-1$ + searchField.setSelection(0, 0); + int editorSelectionBefore = editorSelectionLength(); + + executeCommand("org.eclipse.ui.edit.selectAll"); //$NON-NLS-1$ + + assertEquals(3, searchField.getSelectionCount(), + "Select All must select the text of the focused input field"); //$NON-NLS-1$ + assertEquals(editorSelectionBefore, editorSelectionLength(), + "Select All must not select the editor's document"); //$NON-NLS-1$ + } + + /** + * Enter finds the next match in the search field and replaces in the replace + * field, so the two must be told apart although the key is the same. + */ + @Test + public void testEnterMeansSomethingElseInEachInputField() throws Exception { + focusSearchField(); + searchField.setText("word"); //$NON-NLS-1$ + processPendingEvents(); + int firstMatch = editorSelectionOffset(); + + type(searchField, SWT.NONE, SWT.CR); + + assertNotEquals(firstMatch, editorSelectionOffset(), + "Enter in the search field must move on to the next match"); //$NON-NLS-1$ + assertEquals(CONTENT, documentText(), "finding must not change the document"); //$NON-NLS-1$ + + Text replaceField = showReplaceField(); + replaceField.setText("X"); //$NON-NLS-1$ + processPendingEvents(); + + type(replaceField, SWT.NONE, SWT.CR); + + assertNotEquals(CONTENT, documentText(), + "Enter in the replace field must replace the current match in the document"); //$NON-NLS-1$ + } + + /** + * Commands of the surrounding window, Save among them, must stay executable + * while an input field has focus: only the editor's own are out of place there. + */ + @Test + public void testWindowCommandsStillWorkWhileTheOverlayHasFocus() throws Exception { + document().set("edited"); //$NON-NLS-1$ + processPendingEvents(); + assertTrue(editor.isDirty(), "precondition: the editor has unsaved changes"); //$NON-NLS-1$ + focusSearchField(); + + executeCommand("org.eclipse.ui.file.save"); //$NON-NLS-1$ + + assertFalse(editor.isDirty(), "Save must still reach the editor while the overlay has focus"); //$NON-NLS-1$ + } + + /** + * The overlay is not a part of its own as far as the workbench is concerned, so + * views tracking the active part or the selection must see nothing change. + */ + @Test + public void testTheEditorStaysTheActivePartWhileTheOverlayHasFocus() { + focusSearchField(); + IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); + + assertEquals(editor, page.getActiveEditor(), "active editor"); //$NON-NLS-1$ + assertEquals(editor, page.getActivePart(), "active part"); //$NON-NLS-1$ + assertNotNull(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getSelectionService().getSelection(), + "the selection service must keep reporting the editor's selection"); //$NON-NLS-1$ + } + + /** + * The editor acts on its keys again once the overlay no longer has focus. Only + * asserting the other direction would be satisfied by an overlay that disabled + * the editor's keys for good. + */ + @Test + public void testTheEditorActsOnKeysAgainOnceTheOverlayLostFocus() { + focusSearchField(); + editor.setFocus(); + processPendingEvents(); + + type(editorWidget(), SWT.MOD1, 'a'); + + assertEquals(CONTENT.length(), editorSelectionLength(), + "Select All must apply to the document again once the editor has focus"); //$NON-NLS-1$ + } + + private void focusSearchField() { + // Focus the editor first, so that focusing the search field is a real + // transition rather than a no-op on an already focused control. + editor.setFocus(); + processPendingEvents(); + searchField.forceFocus(); + processPendingEvents(); + assertTrue(searchField.isFocusControl(), "the search field is expected to have focus"); //$NON-NLS-1$ + } + + /** Reveals the replace field through the overlay's own command, which focuses it. */ + private Text showReplaceField() throws Exception { + executeCommand(FindReplaceOverlayCommandSupport.CMD_TOGGLE_REPLACE); + return focusedInputField("showing the replace field is expected to focus it", //$NON-NLS-1$ + REPLACE_FIELD); + } + + private static Text focusedInputField(String message, String expectedId) { + Text field = assertInstanceOf(Text.class, Display.getCurrent().getFocusControl(), message); + assertEquals(expectedId, field.getParent().getData(FindReplaceOverlay.ID_DATA_KEY), message); + return field; + } + + private static void executeCommand(String commandId) throws Exception { + PlatformUI.getWorkbench().getService(IHandlerService.class).executeCommand(commandId, null); + processPendingEvents(); + } + + /** + * Delivers a key stroke the way the workbench sees it, through the display + * filter the key binding dispatcher installs. + */ + private static void type(org.eclipse.swt.widgets.Control target, int stateMask, int keyCode) { + Event keyEvent = new Event(); + keyEvent.widget = target; + keyEvent.type = SWT.KeyDown; + keyEvent.stateMask = stateMask; + keyEvent.keyCode = keyCode; + // Control, and only Control, turns a letter into a control character. On macOS + // SWT.MOD1 is Command, which does not. + keyEvent.character = (char) ((stateMask & SWT.CTRL) != 0 && Character.isLetter(keyCode) + ? Character.toUpperCase(keyCode) - 64 + : keyCode); + target.notifyListeners(SWT.KeyDown, keyEvent); + processPendingEvents(); + } + + private org.eclipse.swt.widgets.Control editorWidget() { + return editor.getAdapter(org.eclipse.jface.text.ITextViewer.class).getTextWidget(); + } + + private IDocument document() { + return editor.getDocumentProvider().getDocument(editor.getEditorInput()); + } + + private String documentText() { + return document().get(); + } + + private ITextSelection editorSelection() { + return (ITextSelection) editor.getSelectionProvider().getSelection(); + } + + private int editorSelectionLength() { + return editorSelection().getLength(); + } + + private int editorSelectionOffset() { + return editorSelection().getOffset(); + } + + private static void processPendingEvents() { + Display display = Display.getCurrent(); + while (display != null && !display.isDisposed() && display.readAndDispatch()) { + // keep dispatching + } + } + +} diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/TestTextEditor.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/TestTextEditor.java new file mode 100644 index 00000000000..a7fd00c5767 --- /dev/null +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/TestTextEditor.java @@ -0,0 +1,79 @@ +/******************************************************************************* + * Copyright (c) 2026 Vector Informatik GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.ui.internal.findandreplace.overlay; + +import org.eclipse.core.runtime.IProgressMonitor; + +import org.eclipse.jface.operation.IRunnableContext; + +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.source.IAnnotationModel; + +import org.eclipse.ui.texteditor.AbstractDocumentProvider; +import org.eclipse.ui.texteditor.StatusTextEditor; + +/** + * A minimal text editor for tests that need a real editor part rather than a bare + * viewer, without pulling in the workspace or the IDE: its input is held in memory + * and nothing is ever saved. Contributed by this bundle's {@code plugin.xml} and + * opened by {@link #ID}, with no file name or content type association, so it is + * never offered for a real file. + */ +public class TestTextEditor extends StatusTextEditor { + + public static final String ID = "org.eclipse.ui.workbench.texteditor.tests.testTextEditor"; //$NON-NLS-1$ + + public TestTextEditor() { + setDocumentProvider(new InMemoryDocumentProvider()); + // Normally established by AbstractDecoratedTextEditor, which belongs to a higher + // layer. This scope is what makes an editor a text editor for key bindings. + setKeyBindingScopes(new String[] { "org.eclipse.ui.textEditorScope" }); //$NON-NLS-1$ + } + + private static final class InMemoryDocumentProvider extends AbstractDocumentProvider { + + @Override + protected IDocument createDocument(Object element) { + return new Document(element instanceof TestTextEditorInput input ? input.getContent() : ""); //$NON-NLS-1$ + } + + @Override + protected IAnnotationModel createAnnotationModel(Object element) { + return null; + } + + @Override + protected void doSaveDocument(IProgressMonitor monitor, Object element, IDocument document, + boolean overwrite) { + // nothing to save, the document only lives for the duration of a test + } + + @Override + protected IRunnableContext getOperationRunner(IProgressMonitor monitor) { + return null; + } + + // AbstractDocumentProvider defaults to read-only, for which the overlay hides + // its replace field, unlike the editors it is actually used with. + + @Override + public boolean isReadOnly(Object element) { + return false; + } + + @Override + public boolean isModifiable(Object element) { + return true; + } + } + +} diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/TestTextEditorInput.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/TestTextEditorInput.java new file mode 100644 index 00000000000..0e094e9d7ff --- /dev/null +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/TestTextEditorInput.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright (c) 2026 Vector Informatik GmbH and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.ui.internal.findandreplace.overlay; + +import org.eclipse.jface.resource.ImageDescriptor; + +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IPersistableElement; + +/** + * Input for {@link TestTextEditor}, holding its content in memory. + */ +public class TestTextEditorInput implements IEditorInput { + + private final String content; + + public TestTextEditorInput(String content) { + this.content = content; + } + + public String getContent() { + return content; + } + + @Override + public boolean exists() { + return true; + } + + @Override + public ImageDescriptor getImageDescriptor() { + return ImageDescriptor.getMissingImageDescriptor(); + } + + @Override + public String getName() { + return "Test"; //$NON-NLS-1$ + } + + @Override + public IPersistableElement getPersistable() { + return null; + } + + @Override + public String getToolTipText() { + return getName(); + } + + @Override + public T getAdapter(Class adapter) { + return null; + } + +} diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/WorkbenchTextEditorTestSuite.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/WorkbenchTextEditorTestSuite.java index 492442c11be..3ae8d48c338 100644 --- a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/WorkbenchTextEditorTestSuite.java +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/WorkbenchTextEditorTestSuite.java @@ -18,6 +18,7 @@ import org.eclipse.ui.internal.findandreplace.FindReplaceLogicTest; import org.eclipse.ui.internal.findandreplace.HistoryStoreTest; +import org.eclipse.ui.internal.findandreplace.overlay.FindReplaceOverlayInEditorTest; import org.eclipse.ui.internal.findandreplace.overlay.FindReplaceOverlayTest; import org.eclipse.ui.workbench.texteditor.tests.minimap.MinimapPageTest; @@ -48,6 +49,7 @@ TextViewerDeleteLineTargetTest.class, FindReplaceLogicTest.class, FindReplaceOverlayTest.class, + FindReplaceOverlayInEditorTest.class, FindReplaceDialogTest.class, HistoryStoreTest.class, })