Skip to content

Commit 59cc8cc

Browse files
committed
Add tests for find/replace UI history store and overlay
The history feature of the find/replace UI lacks test coverage in two distinct areas: HistoryStore (unit tests): HistoryStoreTest covers the storage class in isolation — ordering (newest-first), deduplication, size-limit enforcement, remove semantics, persistence across instances sharing the same IDialogSettings, and independence of separate sections. Unit tests are necessary here because the class carries non-trivial logic (e.g. the in-place deduplication loop in write()) that should be verified without spinning up the Eclipse workbench, and because failures in this layer would otherwise only surface as hard-to-diagnose UI test failures. Find/replace UI (integration tests): five new tests in FindReplaceUITest verify that history is correctly populated after search, replace-all, and single replace, and that entries are ordered newest-first with duplicates suppressed. Running them in the common superclass ensures both the overlay and the dialog are covered. IFindReplaceUIAccess gains selectFindHistoryEntry(int) to abstract over the two different navigation mechanisms (ARROW_DOWN in the overlay text field, combo selection in the dialog). Two additional tests remain in FindReplaceOverlayTest to cover the overlay-specific forward and backward search buttons, which have no dialog counterpart. Fixes #2100
1 parent a6ae91c commit 59cc8cc

8 files changed

Lines changed: 384 additions & 3 deletions

File tree

tests/org.eclipse.ui.workbench.texteditor.tests/META-INF/MANIFEST.MF

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ Require-Bundle:
2222
Bundle-RequiredExecutionEnvironment: JavaSE-21
2323
Eclipse-BundleShape: dir
2424
Automatic-Module-Name: org.eclipse.ui.workbench.texteditor.tests
25-
Import-Package: org.mockito,
26-
org.mockito.stubbing;version="5.5.0",
27-
org.junit.jupiter.api;version="[5.14.0,6.0.0)",
25+
Import-Package: org.junit.jupiter.api;version="[5.14.0,6.0.0)",
2826
org.junit.jupiter.api.function;version="[5.14.0,6.0.0)",
2927
org.junit.platform.suite.api;version="[1.14.0,2.0.0)",
28+
org.mockito,
29+
org.mockito.invocation;version="[5.23.0,6.0.0)",
30+
org.mockito.stubbing;version="5.5.0",
3031
org.opentest4j;version="[1.3.0,2.0.0)"

tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/FindReplaceUITest.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,80 @@ public void testReplaceIfSelectedOnStart() {
376376
assertThat(fTextViewer.getDocument().get(), is("abaaefg"));
377377
}
378378

379+
@Test
380+
public void testSearchTermStoredInHistoryAfterSearch() {
381+
// Performing a search must persist the search term so that the user can
382+
// navigate back to it via history in a subsequent session.
383+
initializeTextViewerWithFindReplaceUI("foo bar foo");
384+
dialog.setFindText("foo");
385+
dialog.simulateKeyboardInteractionInFindInputField(SWT.CR, false);
386+
387+
dialog.selectFindHistoryEntry(0);
388+
assertEquals("foo", dialog.getFindText());
389+
}
390+
391+
@Test
392+
public void testSearchHistoryContainsAllRecentTermsNewestFirst() {
393+
// Multiple searches must all appear in history ordered newest-first, so that
394+
// index 0 always yields the most recently used term.
395+
initializeTextViewerWithFindReplaceUI("foo bar baz foo bar baz");
396+
dialog.setFindText("foo");
397+
dialog.simulateKeyboardInteractionInFindInputField(SWT.CR, false);
398+
dialog.setFindText("bar");
399+
dialog.simulateKeyboardInteractionInFindInputField(SWT.CR, false);
400+
401+
dialog.selectFindHistoryEntry(0);
402+
assertEquals("bar", dialog.getFindText());
403+
404+
dialog.selectFindHistoryEntry(1);
405+
assertEquals("foo", dialog.getFindText());
406+
}
407+
408+
@Test
409+
public void testSearchHistoryDeduplicatesRepeatedSearchTerms() {
410+
// Searching for the same term twice must not create a duplicate entry in
411+
// history. If it did, index 1 would show "foo" again instead of "bar".
412+
initializeTextViewerWithFindReplaceUI("foo bar foo bar");
413+
dialog.setFindText("bar");
414+
dialog.simulateKeyboardInteractionInFindInputField(SWT.CR, false);
415+
dialog.setFindText("foo");
416+
dialog.simulateKeyboardInteractionInFindInputField(SWT.CR, false);
417+
// Search "foo" a second time — must not insert a second "foo" entry.
418+
dialog.setFindText("foo");
419+
dialog.simulateKeyboardInteractionInFindInputField(SWT.CR, false);
420+
421+
dialog.selectFindHistoryEntry(0);
422+
assertEquals("foo", dialog.getFindText());
423+
// A duplicate "foo" would appear here instead of "bar".
424+
dialog.selectFindHistoryEntry(1);
425+
assertEquals("bar", dialog.getFindText());
426+
}
427+
428+
@Test
429+
public void testSearchTermStoredInHistoryAfterReplaceAll() {
430+
// A replace-all operation must persist the search term to history so that
431+
// it is available for future searches.
432+
initializeTextViewerWithFindReplaceUI("foo foo foo");
433+
dialog.setFindText("foo");
434+
dialog.setReplaceText("bar");
435+
dialog.performReplaceAll();
436+
437+
dialog.selectFindHistoryEntry(0);
438+
assertEquals("foo", dialog.getFindText());
439+
}
440+
441+
@Test
442+
public void testSearchTermStoredInHistoryAfterSingleReplace() {
443+
// A single replace operation must also persist the search term to history.
444+
initializeTextViewerWithFindReplaceUI("foo bar");
445+
dialog.setFindText("foo");
446+
dialog.setReplaceText("baz");
447+
dialog.performReplace();
448+
449+
dialog.selectFindHistoryEntry(0);
450+
assertEquals("foo", dialog.getFindText());
451+
}
452+
379453
protected AccessType getDialog() {
380454
return dialog;
381455
}
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Vector Informatik GmbH and others.
3+
*
4+
* This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Public License 2.0
6+
* which accompanies this distribution, and is available at
7+
* https://www.eclipse.org/legal/epl-2.0/
8+
*
9+
* SPDX-License-Identifier: EPL-2.0
10+
*
11+
* Contributors:
12+
* Vector Informatik GmbH - initial API and implementation
13+
*******************************************************************************/
14+
package org.eclipse.ui.internal.findandreplace;
15+
16+
import static org.junit.jupiter.api.Assertions.assertEquals;
17+
import static org.junit.jupiter.api.Assertions.assertFalse;
18+
import static org.junit.jupiter.api.Assertions.assertThrows;
19+
import static org.junit.jupiter.api.Assertions.assertTrue;
20+
import static org.mockito.ArgumentMatchers.any;
21+
import static org.mockito.ArgumentMatchers.anyString;
22+
import static org.mockito.Mockito.doAnswer;
23+
import static org.mockito.Mockito.mock;
24+
import static org.mockito.Mockito.when;
25+
26+
import java.util.ArrayList;
27+
import java.util.HashMap;
28+
import java.util.List;
29+
import java.util.Map;
30+
31+
import org.junit.jupiter.api.Test;
32+
33+
import org.eclipse.jface.dialogs.IDialogSettings;
34+
35+
public class HistoryStoreTest {
36+
37+
/**
38+
* Returns a minimal {@link IDialogSettings} stub that stores and retrieves
39+
* {@code String[]} values in memory, keyed by section name. All other
40+
* {@code IDialogSettings} methods are left as Mockito no-ops / default returns.
41+
*/
42+
private IDialogSettings createInMemoryDialogSettings() {
43+
Map<String, String[]> store = new HashMap<>();
44+
IDialogSettings settings = mock(IDialogSettings.class);
45+
when(settings.getArray(anyString())).thenAnswer(inv -> store.get(inv.getArgument(0)));
46+
doAnswer(inv -> {
47+
store.put(inv.getArgument(0), inv.getArgument(1));
48+
return null;
49+
}).when(settings).put(anyString(), any(String[].class));
50+
return settings;
51+
}
52+
53+
@Test
54+
public void testConstructorThrowsOnNullSectionName() {
55+
assertThrows(IllegalStateException.class,
56+
() -> new HistoryStore(createInMemoryDialogSettings(), null, 5));
57+
}
58+
59+
@Test
60+
public void testNewStoreIsEmpty() {
61+
HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5);
62+
assertTrue(store.isEmpty());
63+
assertEquals(0, store.size());
64+
}
65+
66+
@Test
67+
public void testAddSingleItem() {
68+
HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5);
69+
70+
store.add("item");
71+
72+
assertFalse(store.isEmpty());
73+
assertEquals(1, store.size());
74+
assertEquals("item", store.get(0));
75+
}
76+
77+
@Test
78+
public void testAddNullIsIgnored() {
79+
// Null items must not be stored; history stays empty.
80+
HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5);
81+
82+
store.add(null);
83+
84+
assertTrue(store.isEmpty());
85+
}
86+
87+
@Test
88+
public void testAddEmptyStringIsIgnored() {
89+
// Empty strings must not be stored; an empty search term is not useful history.
90+
HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5);
91+
92+
store.add("");
93+
94+
assertTrue(store.isEmpty());
95+
}
96+
97+
@Test
98+
public void testMostRecentlyAddedItemIsFirst() {
99+
// The most recently added item should appear at index 0 so that history
100+
// navigation reaches recent searches first.
101+
HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5);
102+
103+
store.add("first");
104+
store.add("second");
105+
store.add("third");
106+
107+
assertEquals("third", store.get(0));
108+
assertEquals("second", store.get(1));
109+
assertEquals("first", store.get(2));
110+
assertEquals(3, store.size());
111+
}
112+
113+
@Test
114+
public void testAddingExistingItemMovesItToFront() {
115+
// Re-adding a term already in history should move it to index 0 without
116+
// creating a duplicate. This mirrors how every search toolbar works: the most
117+
// recently used term comes first.
118+
HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5);
119+
store.add("first");
120+
store.add("second");
121+
122+
store.add("first");
123+
124+
assertEquals(2, store.size());
125+
assertEquals("first", store.get(0));
126+
assertEquals("second", store.get(1));
127+
}
128+
129+
@Test
130+
public void testAddingItemAlreadyAtFrontKeepsItThere() {
131+
HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5);
132+
store.add("item");
133+
134+
store.add("item");
135+
136+
assertEquals(1, store.size());
137+
assertEquals("item", store.get(0));
138+
}
139+
140+
@Test
141+
public void testHistorySizeLimitDropsOldestEntries() {
142+
// Once the capacity is reached, the oldest (highest-index) entry must be
143+
// dropped to make room for the new one.
144+
HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 3);
145+
store.add("first");
146+
store.add("second");
147+
store.add("third");
148+
149+
store.add("fourth");
150+
151+
assertEquals(3, store.size());
152+
assertEquals("fourth", store.get(0));
153+
assertEquals("third", store.get(1));
154+
assertEquals("second", store.get(2));
155+
}
156+
157+
@Test
158+
public void testRemoveExistingItem() {
159+
HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5);
160+
store.add("first");
161+
store.add("second");
162+
163+
store.remove("first");
164+
165+
assertEquals(1, store.size());
166+
assertEquals("second", store.get(0));
167+
}
168+
169+
@Test
170+
public void testRemoveItemAtFront() {
171+
HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5);
172+
store.add("first");
173+
store.add("second");
174+
175+
store.remove("second");
176+
177+
assertEquals(1, store.size());
178+
assertEquals("first", store.get(0));
179+
}
180+
181+
@Test
182+
public void testRemoveNonExistentItemIsNoOp() {
183+
// Removing a term that was never stored must not alter the history.
184+
HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5);
185+
store.add("item");
186+
187+
store.remove("nonexistent");
188+
189+
assertEquals(1, store.size());
190+
assertEquals("item", store.get(0));
191+
}
192+
193+
@Test
194+
public void testIndexOfReturnsCorrectPositions() {
195+
HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5);
196+
store.add("first");
197+
store.add("second");
198+
store.add("third");
199+
200+
assertEquals(0, store.indexOf("third"));
201+
assertEquals(1, store.indexOf("second"));
202+
assertEquals(2, store.indexOf("first"));
203+
}
204+
205+
@Test
206+
public void testIndexOfReturnsMinusOneForAbsentItem() {
207+
HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5);
208+
store.add("item");
209+
210+
assertEquals(-1, store.indexOf("nonexistent"));
211+
}
212+
213+
@Test
214+
public void testGetIterableReturnsItemsNewestFirst() {
215+
HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5);
216+
store.add("first");
217+
store.add("second");
218+
219+
List<String> items = new ArrayList<>();
220+
for (String item : store.get()) {
221+
items.add(item);
222+
}
223+
224+
assertEquals(List.of("second", "first"), items);
225+
}
226+
227+
@Test
228+
public void testHistoryPersistedAcrossInstances() {
229+
// Two HistoryStore instances pointing to the same IDialogSettings section must
230+
// share the same data, modelling persistence across workbench sessions.
231+
IDialogSettings sharedSettings = createInMemoryDialogSettings();
232+
HistoryStore store1 = new HistoryStore(sharedSettings, "section", 5);
233+
store1.add("first");
234+
store1.add("second");
235+
236+
HistoryStore store2 = new HistoryStore(sharedSettings, "section", 5);
237+
238+
assertEquals(2, store2.size());
239+
assertEquals("second", store2.get(0));
240+
assertEquals("first", store2.get(1));
241+
}
242+
243+
@Test
244+
public void testDistinctSectionsAreIndependent() {
245+
// Two stores sharing the same IDialogSettings but using different section names
246+
// must not interfere with each other (models separate find/replace histories).
247+
IDialogSettings sharedSettings = createInMemoryDialogSettings();
248+
HistoryStore findHistory = new HistoryStore(sharedSettings, "findhistory", 5);
249+
HistoryStore replaceHistory = new HistoryStore(sharedSettings, "replacehistory", 5);
250+
findHistory.add("findterm");
251+
252+
assertTrue(replaceHistory.isEmpty());
253+
}
254+
255+
}

tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/IFindReplaceUIAccess.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ public interface IFindReplaceUIAccess {
4949

5050
void performReplaceAndFind();
5151

52+
void selectFindHistoryEntry(int index);
53+
5254
void assertInitialConfiguration();
5355

5456
void assertUnselected(SearchOptions option);

0 commit comments

Comments
 (0)