Skip to content

Commit

Permalink
Remove reflective access from find/replace tests #2060
Browse files Browse the repository at this point in the history
The tests for the FindReplaceDialog and FindReplaceOverlay currently use
reflection to access specific UI elements. This ties the test
implementations to implementation details of the production classes
(i.e., specific hidden field to be present) and particularly requires
the production code to contain (hidden) fields even if they would not be
required just to provide according tests.

This change replaces the reflective access (at the code level) with
widget extraction functionality based on unique information about the
widget to be searched for. This may also be considered reflective (at
the configuration level), but at least it gets rid of requiring the code
to contain specific fields just in order to write tests that need to
extract them.

Fixes #2060
  • Loading branch information
HeikoKlare committed Sep 27, 2024
1 parent f35ef3a commit aaed0d2
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 81 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package org.eclipse.ui.internal.findandreplace;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;

import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
import org.eclipse.swt.widgets.Widget;

import org.eclipse.ui.internal.findandreplace.overlay.HistoryTextWrapper;

public final class WidgetExtractor {

private final Composite container;

public WidgetExtractor(Composite container) {
this.container= container;
}

public HistoryTextWrapper findHistoryTextWrapperWithMessage(String includedMessageText) {
List<HistoryTextWrapper> widgets= findWidgets(container, HistoryTextWrapper.class, candidate -> {
String message= removeMnemonicsAndMakeLowercase(candidate.getTextBar().getMessage());
return message.contains(includedMessageText);
});
return widgets.isEmpty() ? null : widgets.get(0);
}

public List<Combo> findCombos() {
return findWidgets(container, Combo.class, candidate -> true);
}

public Button findButtonWithText(String includedText, String... excludedTexts) {
List<Button> widgets= findWidgets(container, Button.class, candidate -> {
String text= removeMnemonicsAndMakeLowercase(candidate.getText());
boolean containsIncludeString= text.contains(includedText);
boolean containsExcludeStrings= Arrays.stream(excludedTexts).anyMatch(it -> text.contains(it));
return containsIncludeString && !containsExcludeStrings;
});
return widgets.isEmpty() ? null : widgets.get(0);
}

public Button findButtonWithTooltipText(String includedToolTipText) {
List<Button> widgets= findWidgets(container, Button.class, candidate -> {
String toolTipText= candidate.getToolTipText();
return toolTipText != null && removeMnemonicsAndMakeLowercase(toolTipText).contains(includedToolTipText);
});
return widgets.isEmpty() ? null : widgets.get(0);
}

public ToolItem findToolItemWithTooltipText(String includedToolTipText, String... excludedToolTipTexts) {
List<ToolItem> widgets= findWidgets(container, ToolItem.class, candidate -> {
String toolTipText= removeMnemonicsAndMakeLowercase(candidate.getToolTipText());
boolean containsIncludeString= toolTipText.contains(includedToolTipText);
boolean containsExcludeStrings= Arrays.stream(excludedToolTipTexts).anyMatch(it -> toolTipText.contains(it));
return containsIncludeString && !containsExcludeStrings;
});
return widgets.isEmpty() ? null : widgets.get(0);
}

private static <T extends Widget> List<T> findWidgets(Composite container, Class<T> type, Function<T, Boolean> matcher) {
List<Widget> children= new ArrayList<>();
children.addAll(List.of(container.getChildren()));
if (container instanceof ToolBar toolbar) {
children.addAll(List.of(toolbar.getItems()));
}
List<T> result= new ArrayList<>();
for (Widget child : children) {
if (type.isInstance(child)) {
@SuppressWarnings("unchecked")
T typedChild= (T) child;
if (matcher.apply(typedChild)) {
result.add(typedChild);
}
}
if (child instanceof Composite compositeChild) {
result.addAll(findWidgets(compositeChild, type, matcher));
}
}
return result;
}

private static String removeMnemonicsAndMakeLowercase(String string) {
return string == null ? "" : string.replaceAll("&", "").toLowerCase();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ public class FindReplaceOverlayTest extends FindReplaceUITest<OverlayAccess> {
public OverlayAccess openUIFromTextViewer(TextViewer viewer) {
Accessor actionAccessor= new Accessor(getFindReplaceAction(), FindReplaceAction.class);
actionAccessor.invoke("showOverlayInEditor", null);
Accessor overlayAccessor= new Accessor(actionAccessor.get("overlay"), "org.eclipse.ui.internal.findandreplace.overlay.FindReplaceOverlay", getClass().getClassLoader());
return new OverlayAccess(getFindReplaceTarget(), overlayAccessor);
FindReplaceOverlay overlay= (FindReplaceOverlay) actionAccessor.get("overlay");
return new OverlayAccess(getFindReplaceTarget(), overlay);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.eclipse.swt.SWT;
Expand All @@ -31,13 +30,12 @@
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.ToolItem;

import org.eclipse.text.tests.Accessor;

import org.eclipse.jface.text.IFindReplaceTarget;
import org.eclipse.jface.text.IFindReplaceTargetExtension;

import org.eclipse.ui.internal.findandreplace.IFindReplaceUIAccess;
import org.eclipse.ui.internal.findandreplace.SearchOptions;
import org.eclipse.ui.internal.findandreplace.WidgetExtractor;

class OverlayAccess implements IFindReplaceUIAccess {
private final IFindReplaceTarget findReplaceTarget;
Expand All @@ -64,28 +62,33 @@ class OverlayAccess implements IFindReplaceUIAccess {

private ToolItem replaceAllButton;

private final Runnable closeOperation;

private final Accessor dialogAccessor;
private final FindReplaceOverlay overlay;

private final Supplier<Shell> shellRetriever;
private final Shell shell;

OverlayAccess(IFindReplaceTarget findReplaceTarget, Accessor findReplaceOverlayAccessor) {
OverlayAccess(IFindReplaceTarget findReplaceTarget, FindReplaceOverlay findReplaceOverlay) {
this.findReplaceTarget= findReplaceTarget;
dialogAccessor= findReplaceOverlayAccessor;
find= (HistoryTextWrapper) findReplaceOverlayAccessor.get("searchBar");
replace= (HistoryTextWrapper) findReplaceOverlayAccessor.get("replaceBar");
caseSensitive= (ToolItem) findReplaceOverlayAccessor.get("caseSensitiveSearchButton");
wholeWord= (ToolItem) findReplaceOverlayAccessor.get("wholeWordSearchButton");
regEx= (ToolItem) findReplaceOverlayAccessor.get("regexSearchButton");
searchForward= (ToolItem) findReplaceOverlayAccessor.get("searchDownButton");
searchBackward= (ToolItem) findReplaceOverlayAccessor.get("searchUpButton");
closeOperation= () -> findReplaceOverlayAccessor.invoke("close", null);
openReplaceDialog= (Button) findReplaceOverlayAccessor.get("replaceToggle");
replaceButton= (ToolItem) findReplaceOverlayAccessor.get("replaceButton");
replaceAllButton= (ToolItem) findReplaceOverlayAccessor.get("replaceAllButton");
inSelection= (ToolItem) findReplaceOverlayAccessor.get("searchInSelectionButton");
shellRetriever= () -> ((Shell) findReplaceOverlayAccessor.invoke("getShell", null));
overlay= findReplaceOverlay;
shell= overlay.getShell();
WidgetExtractor widgetExtractor= new WidgetExtractor(shell);
find= widgetExtractor.findHistoryTextWrapperWithMessage("find");
caseSensitive= widgetExtractor.findToolItemWithTooltipText("case");
wholeWord= widgetExtractor.findToolItemWithTooltipText("whole");
regEx= widgetExtractor.findToolItemWithTooltipText("regular");
inSelection= widgetExtractor.findToolItemWithTooltipText("selected");
searchForward= widgetExtractor.findToolItemWithTooltipText("forward");
searchBackward= widgetExtractor.findToolItemWithTooltipText("backward");
openReplaceDialog= widgetExtractor.findButtonWithTooltipText("toggle");
extractReplaceWidgets();
}

private void extractReplaceWidgets() {
if (!isReplaceDialogOpen() && Objects.nonNull(openReplaceDialog)) {
WidgetExtractor widgetExtractor= new WidgetExtractor(shell);
replace= widgetExtractor.findHistoryTextWrapperWithMessage("replace");
replaceButton= widgetExtractor.findToolItemWithTooltipText("replace", "all");
replaceAllButton= widgetExtractor.findToolItemWithTooltipText("replace all");
}
}

private void restoreInitialConfiguration() {
Expand All @@ -100,12 +103,12 @@ private void restoreInitialConfiguration() {
public void closeAndRestore() {
restoreInitialConfiguration();
assertInitialConfiguration();
closeOperation.run();
overlay.close();
}

@Override
public void close() {
closeOperation.run();
overlay.close();
}

@Override
Expand Down Expand Up @@ -234,15 +237,13 @@ public void performReplace() {
}

public boolean isReplaceDialogOpen() {
return dialogAccessor.getBoolean("replaceBarOpen");
return replace != null;
}

public void openReplaceDialog() {
if (!isReplaceDialogOpen() && Objects.nonNull(openReplaceDialog)) {
openReplaceDialog.notifyListeners(SWT.Selection, null);
replace= (HistoryTextWrapper) dialogAccessor.get("replaceBar");
replaceButton= (ToolItem) dialogAccessor.get("replaceButton");
replaceAllButton= (ToolItem) dialogAccessor.get("replaceAllButton");
extractReplaceWidgets();
}
}

Expand Down Expand Up @@ -309,15 +310,14 @@ public void assertEnabled(SearchOptions option) {

@Override
public boolean isShown() {
return shellRetriever.get() != null && shellRetriever.get().isVisible();
return !shell.isDisposed() && shell.isVisible();
}

@Override
public boolean hasFocus() {
Shell overlayShell= shellRetriever.get();
Control focusControl= overlayShell.getDisplay().getFocusControl();
Control focusControl= shell.getDisplay().getFocusControl();
Shell focusControlShell= focusControl != null ? focusControl.getShell() : null;
return focusControlShell == overlayShell;
return focusControlShell == shell;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,81 +18,75 @@

import java.util.Arrays;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Shell;

import org.eclipse.text.tests.Accessor;
import org.eclipse.jface.dialogs.Dialog;

import org.eclipse.jface.text.IFindReplaceTarget;
import org.eclipse.jface.text.IFindReplaceTargetExtension;

import org.eclipse.ui.internal.findandreplace.IFindReplaceUIAccess;
import org.eclipse.ui.internal.findandreplace.SearchOptions;
import org.eclipse.ui.internal.findandreplace.WidgetExtractor;

class DialogAccess implements IFindReplaceUIAccess {

private final IFindReplaceTarget findReplaceTarget;

Combo findCombo;
private final Dialog findReplaceDialog;

Combo replaceCombo;
private final Combo findCombo;

Button forwardRadioButton;
private final Combo replaceCombo;

Button globalRadioButton;
private final Button forwardRadioButton;

Button searchInRangeRadioButton;
private final Button globalRadioButton;

Button caseCheckBox;
private final Button searchInRangeRadioButton;

Button wrapCheckBox;
private final Button caseCheckBox;

Button wholeWordCheckBox;
private final Button wrapCheckBox;

Button incrementalCheckBox;
private final Button wholeWordCheckBox;

Button regExCheckBox;
private final Button incrementalCheckBox;

Button findButton;
private final Button regExCheckBox;

Button replaceButton;
private final Button replaceButton;

Button replaceFindButton;
private final Button replaceFindButton;

Button replaceAllButton;
private final Button replaceAllButton;

private Supplier<Shell> shellRetriever;

private Runnable closeOperation;

Accessor dialogAccessor;

DialogAccess(IFindReplaceTarget findReplaceTarget, Accessor findReplaceDialogAccessor) {
DialogAccess(IFindReplaceTarget findReplaceTarget, Dialog findReplaceDialog) {
this.findReplaceTarget= findReplaceTarget;
dialogAccessor= findReplaceDialogAccessor;
findCombo= (Combo) findReplaceDialogAccessor.get("fFindField");
replaceCombo= (Combo) findReplaceDialogAccessor.get("fReplaceField");
forwardRadioButton= (Button) findReplaceDialogAccessor.get("fForwardRadioButton");
globalRadioButton= (Button) findReplaceDialogAccessor.get("fGlobalRadioButton");
searchInRangeRadioButton= (Button) findReplaceDialogAccessor.get("fSelectedRangeRadioButton");
caseCheckBox= (Button) findReplaceDialogAccessor.get("fCaseCheckBox");
wrapCheckBox= (Button) findReplaceDialogAccessor.get("fWrapCheckBox");
wholeWordCheckBox= (Button) findReplaceDialogAccessor.get("fWholeWordCheckBox");
incrementalCheckBox= (Button) findReplaceDialogAccessor.get("fIncrementalCheckBox");
regExCheckBox= (Button) findReplaceDialogAccessor.get("fIsRegExCheckBox");
shellRetriever= () -> ((Shell) findReplaceDialogAccessor.get("fActiveShell"));
closeOperation= () -> findReplaceDialogAccessor.invoke("close", null);
findButton= (Button) findReplaceDialogAccessor.get("fFindNextButton");
replaceButton= (Button) findReplaceDialogAccessor.get("fReplaceSelectionButton");
replaceFindButton= (Button) findReplaceDialogAccessor.get("fReplaceFindButton");
replaceAllButton= (Button) findReplaceDialogAccessor.get("fReplaceAllButton");
this.findReplaceDialog= findReplaceDialog;
WidgetExtractor widgetExtractor= new WidgetExtractor(findReplaceDialog.getShell());
findCombo= widgetExtractor.findCombos().get(0);
replaceCombo= widgetExtractor.findCombos().get(1);
forwardRadioButton= widgetExtractor.findButtonWithText("forward");
globalRadioButton= widgetExtractor.findButtonWithText("all", "select", "replace");
searchInRangeRadioButton= widgetExtractor.findButtonWithText("selected");
caseCheckBox= widgetExtractor.findButtonWithText("case");
wrapCheckBox= widgetExtractor.findButtonWithText("wrap");
wholeWordCheckBox= widgetExtractor.findButtonWithText("whole");
incrementalCheckBox= widgetExtractor.findButtonWithText("incremental");
regExCheckBox= widgetExtractor.findButtonWithText("regular");

replaceButton= widgetExtractor.findButtonWithText("replace", "find");
replaceFindButton= widgetExtractor.findButtonWithText("replace/find");
replaceAllButton= widgetExtractor.findButtonWithText("replace all");
}

void restoreInitialConfiguration() {
Expand Down Expand Up @@ -153,17 +147,19 @@ public void unselect(SearchOptions option) {
public void closeAndRestore() {
restoreInitialConfiguration();
assertInitialConfiguration();
closeOperation.run();
findReplaceDialog.close();
}

@Override
public void close() {
closeOperation.run();
findReplaceDialog.close();
}

@Override
public boolean hasFocus() {
return shellRetriever.get() != null;
Control focusControl= findReplaceDialog.getShell().getDisplay().getFocusControl();
Shell focusControlShell= focusControl != null ? focusControl.getShell() : null;
return focusControlShell == findReplaceDialog.getShell();
}

@Override
Expand Down Expand Up @@ -296,7 +292,7 @@ private Set<SearchOptions> getSelectedOptions() {

@Override
public boolean isShown() {
return shellRetriever.get() != null;
return findReplaceDialog.getShell().isVisible();
}

}
Loading

0 comments on commit aaed0d2

Please sign in to comment.