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 c39e7b8fbdd..8fc2605ffed 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 @@ -22,9 +22,10 @@ Require-Bundle: Bundle-RequiredExecutionEnvironment: JavaSE-21 Eclipse-BundleShape: dir Automatic-Module-Name: org.eclipse.ui.workbench.texteditor.tests -Import-Package: org.mockito, - org.mockito.stubbing;version="5.5.0", - org.junit.jupiter.api;version="[5.14.0,6.0.0)", +Import-Package: org.junit.jupiter.api;version="[5.14.0,6.0.0)", org.junit.jupiter.api.function;version="[5.14.0,6.0.0)", org.junit.platform.suite.api;version="[1.14.0,2.0.0)", + org.mockito, + org.mockito.invocation;version="[5.23.0,6.0.0)", + org.mockito.stubbing;version="5.5.0", org.opentest4j;version="[1.3.0,2.0.0)" diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/FindReplaceUITest.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/FindReplaceUITest.java index c253586ccc3..efca3df0db4 100644 --- a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/FindReplaceUITest.java +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/FindReplaceUITest.java @@ -376,6 +376,80 @@ public void testReplaceIfSelectedOnStart() { assertThat(fTextViewer.getDocument().get(), is("abaaefg")); } + @Test + public void testSearchTermStoredInHistoryAfterSearch() { + // Performing a search must persist the search term so that the user can + // navigate back to it via history in a subsequent session. + initializeTextViewerWithFindReplaceUI("foo bar foo"); + dialog.setFindText("foo"); + dialog.simulateKeyboardInteractionInFindInputField(SWT.CR, false); + + dialog.selectFindHistoryEntry(0); + assertEquals("foo", dialog.getFindText()); + } + + @Test + public void testSearchHistoryContainsAllRecentTermsNewestFirst() { + // Multiple searches must all appear in history ordered newest-first, so that + // index 0 always yields the most recently used term. + initializeTextViewerWithFindReplaceUI("foo bar baz foo bar baz"); + dialog.setFindText("foo"); + dialog.simulateKeyboardInteractionInFindInputField(SWT.CR, false); + dialog.setFindText("bar"); + dialog.simulateKeyboardInteractionInFindInputField(SWT.CR, false); + + dialog.selectFindHistoryEntry(0); + assertEquals("bar", dialog.getFindText()); + + dialog.selectFindHistoryEntry(1); + assertEquals("foo", dialog.getFindText()); + } + + @Test + public void testSearchHistoryDeduplicatesRepeatedSearchTerms() { + // Searching for the same term twice must not create a duplicate entry in + // history. If it did, index 1 would show "foo" again instead of "bar". + initializeTextViewerWithFindReplaceUI("foo bar foo bar"); + dialog.setFindText("bar"); + dialog.simulateKeyboardInteractionInFindInputField(SWT.CR, false); + dialog.setFindText("foo"); + dialog.simulateKeyboardInteractionInFindInputField(SWT.CR, false); + // Search "foo" a second time — must not insert a second "foo" entry. + dialog.setFindText("foo"); + dialog.simulateKeyboardInteractionInFindInputField(SWT.CR, false); + + dialog.selectFindHistoryEntry(0); + assertEquals("foo", dialog.getFindText()); + // A duplicate "foo" would appear here instead of "bar". + dialog.selectFindHistoryEntry(1); + assertEquals("bar", dialog.getFindText()); + } + + @Test + public void testSearchTermStoredInHistoryAfterReplaceAll() { + // A replace-all operation must persist the search term to history so that + // it is available for future searches. + initializeTextViewerWithFindReplaceUI("foo foo foo"); + dialog.setFindText("foo"); + dialog.setReplaceText("bar"); + dialog.performReplaceAll(); + + dialog.selectFindHistoryEntry(0); + assertEquals("foo", dialog.getFindText()); + } + + @Test + public void testSearchTermStoredInHistoryAfterSingleReplace() { + // A single replace operation must also persist the search term to history. + initializeTextViewerWithFindReplaceUI("foo bar"); + dialog.setFindText("foo"); + dialog.setReplaceText("baz"); + dialog.performReplace(); + + dialog.selectFindHistoryEntry(0); + assertEquals("foo", dialog.getFindText()); + } + protected AccessType getDialog() { return dialog; } diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/HistoryStoreTest.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/HistoryStoreTest.java new file mode 100644 index 00000000000..ddc5cdd285d --- /dev/null +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/HistoryStoreTest.java @@ -0,0 +1,255 @@ +/******************************************************************************* + * Copyright (c) 2025 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 + * + * Contributors: + * Vector Informatik GmbH - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.internal.findandreplace; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.eclipse.jface.dialogs.IDialogSettings; + +public class HistoryStoreTest { + + /** + * Returns a minimal {@link IDialogSettings} stub that stores and retrieves + * {@code String[]} values in memory, keyed by section name. All other + * {@code IDialogSettings} methods are left as Mockito no-ops / default returns. + */ + private IDialogSettings createInMemoryDialogSettings() { + Map store = new HashMap<>(); + IDialogSettings settings = mock(IDialogSettings.class); + when(settings.getArray(anyString())).thenAnswer(inv -> store.get(inv.getArgument(0))); + doAnswer(inv -> { + store.put(inv.getArgument(0), inv.getArgument(1)); + return null; + }).when(settings).put(anyString(), any(String[].class)); + return settings; + } + + @Test + public void testConstructorThrowsOnNullSectionName() { + assertThrows(IllegalStateException.class, + () -> new HistoryStore(createInMemoryDialogSettings(), null, 5)); + } + + @Test + public void testNewStoreIsEmpty() { + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + assertTrue(store.isEmpty()); + assertEquals(0, store.size()); + } + + @Test + public void testAddSingleItem() { + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + + store.add("item"); + + assertFalse(store.isEmpty()); + assertEquals(1, store.size()); + assertEquals("item", store.get(0)); + } + + @Test + public void testAddNullIsIgnored() { + // Null items must not be stored; history stays empty. + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + + store.add(null); + + assertTrue(store.isEmpty()); + } + + @Test + public void testAddEmptyStringIsIgnored() { + // Empty strings must not be stored; an empty search term is not useful history. + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + + store.add(""); + + assertTrue(store.isEmpty()); + } + + @Test + public void testMostRecentlyAddedItemIsFirst() { + // The most recently added item should appear at index 0 so that history + // navigation reaches recent searches first. + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + + store.add("first"); + store.add("second"); + store.add("third"); + + assertEquals("third", store.get(0)); + assertEquals("second", store.get(1)); + assertEquals("first", store.get(2)); + assertEquals(3, store.size()); + } + + @Test + public void testAddingExistingItemMovesItToFront() { + // Re-adding a term already in history should move it to index 0 without + // creating a duplicate. This mirrors how every search toolbar works: the most + // recently used term comes first. + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + store.add("first"); + store.add("second"); + + store.add("first"); + + assertEquals(2, store.size()); + assertEquals("first", store.get(0)); + assertEquals("second", store.get(1)); + } + + @Test + public void testAddingItemAlreadyAtFrontKeepsItThere() { + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + store.add("item"); + + store.add("item"); + + assertEquals(1, store.size()); + assertEquals("item", store.get(0)); + } + + @Test + public void testHistorySizeLimitDropsOldestEntries() { + // Once the capacity is reached, the oldest (highest-index) entry must be + // dropped to make room for the new one. + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 3); + store.add("first"); + store.add("second"); + store.add("third"); + + store.add("fourth"); + + assertEquals(3, store.size()); + assertEquals("fourth", store.get(0)); + assertEquals("third", store.get(1)); + assertEquals("second", store.get(2)); + } + + @Test + public void testRemoveExistingItem() { + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + store.add("first"); + store.add("second"); + + store.remove("first"); + + assertEquals(1, store.size()); + assertEquals("second", store.get(0)); + } + + @Test + public void testRemoveItemAtFront() { + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + store.add("first"); + store.add("second"); + + store.remove("second"); + + assertEquals(1, store.size()); + assertEquals("first", store.get(0)); + } + + @Test + public void testRemoveNonExistentItemIsNoOp() { + // Removing a term that was never stored must not alter the history. + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + store.add("item"); + + store.remove("nonexistent"); + + assertEquals(1, store.size()); + assertEquals("item", store.get(0)); + } + + @Test + public void testIndexOfReturnsCorrectPositions() { + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + store.add("first"); + store.add("second"); + store.add("third"); + + assertEquals(0, store.indexOf("third")); + assertEquals(1, store.indexOf("second")); + assertEquals(2, store.indexOf("first")); + } + + @Test + public void testIndexOfReturnsMinusOneForAbsentItem() { + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + store.add("item"); + + assertEquals(-1, store.indexOf("nonexistent")); + } + + @Test + public void testGetIterableReturnsItemsNewestFirst() { + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + store.add("first"); + store.add("second"); + + List items = new ArrayList<>(); + for (String item : store.get()) { + items.add(item); + } + + assertEquals(List.of("second", "first"), items); + } + + @Test + public void testHistoryPersistedAcrossInstances() { + // Two HistoryStore instances pointing to the same IDialogSettings section must + // share the same data, modelling persistence across workbench sessions. + IDialogSettings sharedSettings = createInMemoryDialogSettings(); + HistoryStore store1 = new HistoryStore(sharedSettings, "section", 5); + store1.add("first"); + store1.add("second"); + + HistoryStore store2 = new HistoryStore(sharedSettings, "section", 5); + + assertEquals(2, store2.size()); + assertEquals("second", store2.get(0)); + assertEquals("first", store2.get(1)); + } + + @Test + public void testDistinctSectionsAreIndependent() { + // Two stores sharing the same IDialogSettings but using different section names + // must not interfere with each other (models separate find/replace histories). + IDialogSettings sharedSettings = createInMemoryDialogSettings(); + HistoryStore findHistory = new HistoryStore(sharedSettings, "findhistory", 5); + HistoryStore replaceHistory = new HistoryStore(sharedSettings, "replacehistory", 5); + findHistory.add("findterm"); + + assertTrue(replaceHistory.isEmpty()); + } + +} diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/IFindReplaceUIAccess.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/IFindReplaceUIAccess.java index 57a4edd6f04..55d60f7f685 100644 --- a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/IFindReplaceUIAccess.java +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/IFindReplaceUIAccess.java @@ -49,6 +49,8 @@ public interface IFindReplaceUIAccess { void performReplaceAndFind(); + void selectFindHistoryEntry(int index); + void assertInitialConfiguration(); void assertUnselected(SearchOptions option); diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayTest.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayTest.java index ee073791589..e74eb92d3b4 100644 --- a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayTest.java +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; +import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Point; import org.eclipse.core.runtime.preferences.IEclipsePreferences; @@ -184,4 +185,34 @@ public void testDisableOverlayViaPreference() { } } + @Test + public void testSearchTermStoredInHistoryAfterSearchForward() { + // After a forward search, the term must be retrievable from history so that + // the user can navigate back to it in a subsequent session. + initializeTextViewerWithFindReplaceUI("foo bar foo"); + OverlayAccess dialog= getDialog(); + dialog.setFindText("foo"); + dialog.pressSearch(true); + + // Down-arrow navigates to the most recently stored entry (index 0). + dialog.setFindText(""); + dialog.simulateKeyboardInteractionInFindInputField(SWT.ARROW_DOWN, false); + + assertEquals("foo", dialog.getFindText()); + } + + @Test + public void testSearchTermStoredInHistoryAfterSearchBackward() { + // Backward search must persist the term to history just like forward search. + initializeTextViewerWithFindReplaceUI("foo bar foo"); + OverlayAccess dialog= getDialog(); + dialog.setFindText("foo"); + dialog.pressSearch(false); + + dialog.setFindText(""); + dialog.simulateKeyboardInteractionInFindInputField(SWT.ARROW_DOWN, false); + + assertEquals("foo", dialog.getFindText()); + } + } diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/OverlayAccess.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/OverlayAccess.java index ae07a9643ec..16de0671b77 100644 --- a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/OverlayAccess.java +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/OverlayAccess.java @@ -216,6 +216,17 @@ private Predicate isOptionSelected() { }; } + @Override + public void selectFindHistoryEntry(int index) { + // Clear the field directly (no SWT.Modify, so no incremental search fires), + // then step down once per index position. From an empty field ARROW_DOWN + // always lands on index 0 (newest); each additional press moves one step older. + find.setText(""); + for (int i = 0; i <= index; i++) { + simulateKeyboardInteractionInFindInputField(SWT.ARROW_DOWN, false); + } + } + public void pressSearch(boolean forward) { if (forward) { searchForward.notifyListeners(SWT.Selection, null); diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/DialogAccess.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/DialogAccess.java index 29247c3c0f1..9d5a10dddb0 100644 --- a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/DialogAccess.java +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/DialogAccess.java @@ -230,6 +230,11 @@ public void performReplace() { } } + @Override + public void selectFindHistoryEntry(int index) { + findCombo.select(index); + } + public void performFindNext() { if (findNextButton.getEnabled()) { findNextButton.notifyListeners(SWT.Selection, 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 30ba98d200c..d2ad80884ed 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 @@ -17,6 +17,7 @@ import org.junit.platform.suite.api.SelectClasses; import org.eclipse.ui.internal.findandreplace.FindReplaceLogicTest; +import org.eclipse.ui.internal.findandreplace.HistoryStoreTest; import org.eclipse.ui.internal.findandreplace.overlay.FindReplaceOverlayTest; import org.eclipse.ui.workbench.texteditor.tests.minimap.MinimapPageTest; @@ -48,6 +49,7 @@ FindReplaceDialogTest.class, FindReplaceOverlayTest.class, FindReplaceLogicTest.class, + HistoryStoreTest.class, }) public class WorkbenchTextEditorTestSuite { // see @SelectClasses