UiInterceptors: ability to intercept UI components in tests and emulate user action

Currently only popup chooser is supported
A usage example added for AddExceptionToExistingCatchTest
This commit is contained in:
Tagir Valeev
2019-03-06 13:37:43 +07:00
parent 72fea44b3d
commit 43b636f63f
19 changed files with 338 additions and 24 deletions
@@ -5,8 +5,6 @@ import com.intellij.codeInsight.ExceptionUtil;
import com.intellij.codeInsight.FileModificationService;
import com.intellij.codeInsight.daemon.QuickFixBundle;
import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
@@ -47,9 +45,7 @@ public class AddExceptionToExistingCatchFix extends PsiElementBaseIntentionActio
setText(context.getMessage());
Application application = ApplicationManager.getApplication();
if (catchSections.size() == 1 || application.isUnitTestMode()) {
if (catchSections.size() == 1) {
PsiCatchSection selectedSection = catchSections.get(0);
addTypeToCatch(unhandledExceptions, selectedSection);
}
@@ -0,0 +1,16 @@
// "Add exception to existing catch clause" "true"
import java.io.IOException;
class A extends Exception {}
class B extends A {}
class C extends A {}
class Test {
public static void main(String[] args) {
try {
throw new A();
} catch (B e) {
} catch (A e) {
}
}
}
@@ -0,0 +1,19 @@
// "Add exception to existing catch clause" "true"
import java.io.IOException;
class X extends RuntimeException {}
class Y extends RuntimeException {}
class Test {
public static void main(String[] args) {
try {
try {
throw new IOException();
}
catch (X | IOException x) {
}
}
catch (Y y) {
}
}
}
@@ -0,0 +1,19 @@
// "Add exception to existing catch clause" "true"
import java.io.IOException;
class X extends RuntimeException {}
class Y extends RuntimeException {}
class Test {
public static void main(String[] args) {
try {
try {
throw new IOException();
}
catch (X x) {
}
}
catch (Y | IOException y) {
}
}
}
@@ -0,0 +1,16 @@
// "Add exception to existing catch clause" "true"
import java.io.IOException;
class A extends Exception {}
class B extends A {}
class C extends A {}
class Test {
public static void main(String[] args) {
try {
throw new A<caret>();
} catch (B e) {
} catch (C e) {
}
}
}
@@ -0,0 +1,19 @@
// "Add exception to existing catch clause" "true"
import java.io.IOException;
class X extends RuntimeException {}
class Y extends RuntimeException {}
class Test {
public static void main(String[] args) {
try {
try {
throw new <caret>IOException();
}
catch (X x) {
}
}
catch (Y y) {
}
}
}
@@ -0,0 +1,19 @@
// "Add exception to existing catch clause" "true"
import java.io.IOException;
class X extends RuntimeException {}
class Y extends RuntimeException {}
class Test {
public static void main(String[] args) {
try {
try {
throw new <caret>IOException();
}
catch (X x) {
}
}
catch (Y y) {
}
}
}
@@ -2,13 +2,26 @@
package com.intellij.java.codeInsight.daemon.quickFix;
import com.intellij.codeInsight.daemon.LightIntentionActionTestCase;
import com.intellij.ui.ChooserInterceptor;
import com.intellij.ui.UiInterceptors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class AddExceptionToExistingCatchTest extends LightIntentionActionTestCase {
private static final Pattern CHOOSER_TEST_NAME = Pattern.compile(".+_Choose_(.+)\\.java");
@Override
protected String getBasePath() {
return "/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch";
}
@Override
protected void setUp() throws Exception {
super.setUp();
Matcher matcher = CHOOSER_TEST_NAME.matcher(getTestName(false));
if (matcher.matches()) {
UiInterceptors.register(new ChooserInterceptor(null, matcher.group(1)));
}
}
}
@@ -149,7 +149,7 @@ public class IntroduceTargetChooser {
}).createPopup();
popup.showInBestPositionFor(editor);
Project project = editor.getProject();
if (project != null) {
if (project != null && !popup.isDisposed()) {
NavigationUtil.hidePopupIfDumbModeStarts(popup, project);
}
}
@@ -177,5 +177,10 @@ public class IntroduceTargetChooser {
public String render() {
return myRenderer.fun(getPlace());
}
@Override
public String toString() {
return render();
}
}
}
@@ -29,6 +29,7 @@ import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.util.List;
@@ -166,4 +167,13 @@ public interface ComponentPopupBuilder {
default ComponentPopupBuilder setBorderColor(Color color) {
return this;
}
/**
* Set a handler to be called when popup is closed via {@link JBPopup#closeOk(InputEvent)}.
*
* @param okHandler handler to call
* @return this builder
*/
@NotNull
ComponentPopupBuilder setOkHandler(@Nullable Runnable okHandler);
}
@@ -60,7 +60,7 @@ public class PopupChooserBuilder<T> implements IPopupChooserBuilder<T> {
private Function<Object,String> myItemsNamer = null;
private boolean myMayBeParent;
private int myAdAlignment = SwingUtilities.LEFT;
private int myAdAlignment = SwingConstants.LEFT;
private boolean myModalContext;
private boolean myCloseOnEnter = true;
private boolean myCancelOnWindowDeactivation = true;
@@ -322,7 +322,7 @@ public class PopupChooserBuilder<T> implements IPopupChooserBuilder<T> {
public void mouseReleased(MouseEvent e) {
if (UIUtil.isActionClick(e, MouseEvent.MOUSE_RELEASED) && !UIUtil.isSelectionButtonDown(e) && !e.isConsumed()) {
if (myCloseOnEnter) {
closePopup(true, e, true);
closePopup(e, true);
}
else {
myItemChosenRunnable.run();
@@ -391,7 +391,8 @@ public class PopupChooserBuilder<T> implements IPopupChooserBuilder<T> {
.setModalContext(myModalContext)
.setCancelOnWindowDeactivation(myCancelOnWindowDeactivation)
.setCancelOnClickOutside(myCancelOnClickOutside)
.setCouldPin(myCouldPin);
.setCouldPin(myCouldPin)
.setOkHandler(myItemChosenRunnable);
BooleanFunction<KeyEvent> keyEventHandler = myChooserComponent.getKeyEventHandler();
if (keyEventHandler != null) {
@@ -442,7 +443,7 @@ public class PopupChooserBuilder<T> implements IPopupChooserBuilder<T> {
@Override
public void actionPerformed(ActionEvent e) {
if (!shouldPerformAction && myChooserComponent.checkResetFilter()) return;
closePopup(shouldPerformAction, null, shouldPerformAction);
closePopup(null, shouldPerformAction);
}
});
}
@@ -451,11 +452,7 @@ public class PopupChooserBuilder<T> implements IPopupChooserBuilder<T> {
myChooserComponent.getComponent().registerKeyboardAction(action, keyStroke, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
}
private void closePopup(boolean shouldPerformAction, MouseEvent e, boolean isOk) {
if (shouldPerformAction) {
myPopup.setFinalRunnable(myItemChosenRunnable);
}
private void closePopup(MouseEvent e, boolean isOk) {
if (isOk) {
myPopup.closeOk(e);
} else {
@@ -508,7 +505,7 @@ public class PopupChooserBuilder<T> implements IPopupChooserBuilder<T> {
@Override
@NotNull
public PopupChooserBuilder<T> setAdText(String ad) {
setAdText(ad, SwingUtilities.LEFT);
setAdText(ad, SwingConstants.LEFT);
return this;
}
@@ -0,0 +1,70 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.ui;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.TestOnly;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentLinkedQueue;
public class UiInterceptors {
private static final ConcurrentLinkedQueue<UiInterceptor<?>> ourInterceptors = new ConcurrentLinkedQueue<>();
/**
* Called from UI component
*
* @param uiComponent UI component which is about to be displayed
* @return true if interception was successful, in this case no UI should be actually shown
*/
public static boolean tryIntercept(@NotNull Object uiComponent) {
UiInterceptor<?> interceptor = ourInterceptors.poll();
if (interceptor == null) return false;
interceptor.intercept(uiComponent);
return true;
}
/**
* Register interceptor to intercept next shown UI component
*
* @param interceptor interceptor to register
*/
@TestOnly
public static void register(UiInterceptor<?> interceptor) {
ourInterceptors.offer(interceptor);
}
/**
* Should be called in test tearDown to ensure that all registered interceptors were actually used.
*/
@TestOnly
public static void clear() {
ArrayList<UiInterceptor<?>> interceptors = new ArrayList<>(ourInterceptors);
ourInterceptors.clear();
if (!interceptors.isEmpty()) {
throw new IllegalStateException("Expected UI was not shown: " + interceptors);
}
}
public abstract static class UiInterceptor<T> {
private final @NotNull Class<T> myClass;
protected UiInterceptor(@NotNull Class<T> componentClass) {
myClass = componentClass;
}
public final void intercept(@NotNull Object component) {
if (!myClass.isInstance(component)) {
throw new IllegalStateException("Unexpected UI component appears: wanted " + myClass.getName() + "; got: "
+ component.getClass().getName() + " (" + component + ")");
}
doIntercept(myClass.cast(component));
}
protected abstract void doIntercept(T component);
@Override
public String toString() {
return myClass.getName()+" (interceptor: " + getClass().getName() + ")";
}
}
}
@@ -33,10 +33,7 @@ import com.intellij.ui.border.CustomLineBorder;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.mac.touchbar.TouchBarsManager;
import com.intellij.ui.speedSearch.SpeedSearch;
import com.intellij.util.Alarm;
import com.intellij.util.BooleanFunction;
import com.intellij.util.IJSwingUtilities;
import com.intellij.util.Processor;
import com.intellij.util.*;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.WeakList;
import com.intellij.util.ui.*;
@@ -48,8 +45,8 @@ import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.util.List;
import java.util.*;
import java.util.function.Supplier;
import static java.awt.AWTEvent.MOUSE_EVENT_MASK;
@@ -122,6 +119,7 @@ public class AbstractPopup implements JBPopup {
InputEvent myDisposeEvent;
private Runnable myFinalRunnable;
private Runnable myOkHandler;
@Nullable private BooleanFunction<KeyEvent> myKeyEventHandler;
protected boolean myOk;
@@ -441,6 +439,7 @@ public class AbstractPopup implements JBPopup {
@Override
public void show(@NotNull RelativePoint aPoint) {
if (UiInterceptors.tryIntercept(this)) return;
HelpTooltip.setMasterPopup(aPoint.getOriginalComponent(), this);
Point screenPoint = aPoint.getScreenPoint();
show(aPoint.getComponent(), screenPoint.x, screenPoint.y, false);
@@ -541,6 +540,8 @@ public class AbstractPopup implements JBPopup {
@Override
public void showInBestPositionFor(@NotNull Editor editor) {
// Intercept before the following assert; otherwise assertion may fail
if (UiInterceptors.tryIntercept(this)) return;
assert editor.getComponent().isShowing() : "Editor must be showing on the screen";
// Set the accessible parent so that screen readers don't announce
@@ -664,6 +665,7 @@ public class AbstractPopup implements JBPopup {
@Override
public final void closeOk(@Nullable InputEvent e) {
setOk(true);
myFinalRunnable = FunctionUtil.composeRunnables(myOkHandler, myFinalRunnable);
cancel(e);
}
@@ -1869,6 +1871,10 @@ public class AbstractPopup implements JBPopup {
myWindow.setMinimumSize(new Dimension(width, height));
}
}
public void setOkHandler(Runnable okHandler) {
myOkHandler = okHandler;
}
@Override
public void setFinalRunnable(Runnable finalRunnable) {
@@ -33,8 +33,8 @@ import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.*;
import java.util.List;
import java.util.*;
/**
* @author anna
@@ -83,6 +83,7 @@ public class ComponentPopupBuilderImpl implements ComponentPopupBuilder {
private BooleanFunction<KeyEvent> myKeyEventHandler;
private Color myBorderColor;
private boolean myNormalWindowLevel;
private @Nullable Runnable myOkHandler;
public ComponentPopupBuilderImpl(@NotNull JComponent component, JComponent preferredFocusedComponent) {
myComponent = component;
@@ -242,6 +243,7 @@ public class ComponentPopupBuilderImpl implements ComponentPopupBuilder {
);
popup.setNormalWindowLevel(myNormalWindowLevel);
popup.setOkHandler(myOkHandler);
if (myUserData != null) {
popup.setUserData(myUserData);
@@ -378,4 +380,11 @@ public class ComponentPopupBuilderImpl implements ComponentPopupBuilder {
myBorderColor = color;
return this;
}
@NotNull
@Override
public ComponentPopupBuilder setOkHandler(@Nullable Runnable okHandler) {
myOkHandler = okHandler;
return this;
}
}
@@ -78,6 +78,7 @@ import com.intellij.psi.impl.PsiDocumentManagerImpl;
import com.intellij.psi.impl.PsiManagerImpl;
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageManagerImpl;
import com.intellij.psi.templateLanguages.TemplateDataLanguageMappings;
import com.intellij.ui.UiInterceptors;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.LocalTimeCounter;
import com.intellij.util.ReflectionUtil;
@@ -449,6 +450,7 @@ public abstract class LightPlatformTestCase extends UsefulTestCase implements Da
append(() -> PlatformTestCase.waitForProjectLeakingThreads(project, 10, TimeUnit.SECONDS)).
append(() -> ProjectManagerEx.getInstanceEx().closeTestProject(project)).
append(() -> application.setDataProvider(null)).
append(() -> UiInterceptors.clear()).
append(() -> ourTestCase = null).
append(() -> CompletionProgressIndicator.cleanupForNextTest()).
append(() -> {
@@ -0,0 +1,67 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.ui;
import com.intellij.openapi.ui.popup.JBPopup;
import com.intellij.ui.components.JBList;
import one.util.streamex.IntStreamEx;
import one.util.streamex.StreamEx;
import org.intellij.lang.annotations.RegExp;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.util.List;
import java.util.regex.Pattern;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
/**
* Interceptor for the next popup chooser
*/
public class ChooserInterceptor extends UiInterceptors.UiInterceptor<JBPopup> {
final List<String> myOptions;
final Pattern myToSelect;
/**
* Create an interceptor which will assert the expected options and select the given option when chooser will appear
*
* @param expectedOptions expected options to assert; pass null to skip checking the expected options
* @param pattern a regexp which should match the wanted option
*/
public ChooserInterceptor(@Nullable List<String> expectedOptions, @NotNull @RegExp String pattern) {
super(JBPopup.class);
myOptions = expectedOptions;
myToSelect = Pattern.compile(pattern);
}
@Override
protected void doIntercept(JBPopup popup) {
JComponent component = popup.getContent();
JBList<?> content = StreamEx.ofTree((Component)component, Container.class, c -> StreamEx.of(c.getComponents()))
.select(JBList.class).findFirst().orElse(null);
if (content == null) {
fail("Expected chooser; got: " + component);
}
ListModel<?> model = content.getModel();
List<String> actualOptions = IntStreamEx.range(model.getSize()).mapToObj(model::getElementAt).map(Object::toString).toList();
if (myOptions != null) {
assertEquals(myOptions, actualOptions);
}
List<String> matched = StreamEx.of(actualOptions).filter(opt -> myToSelect.matcher(opt).matches()).toList();
if (matched.isEmpty()) {
fail("No option matches " + myToSelect);
}
if (matched.size() > 1) {
fail("Several options matched: " + matched + " (pattern: " + myToSelect + ")");
}
content.setSelectedIndex(actualOptions.indexOf(matched.get(0)));
popup.closeOk(null);
}
@Override
public String toString() {
return "Popup Chooser where '" + myToSelect + "' should be selected";
}
}
@@ -15,7 +15,9 @@
*/
package com.intellij.util;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* @author nik
@@ -50,4 +52,33 @@ public class FunctionUtil {
public static <A, B, C> NotNullFunction<A, C> composition(@NotNull final NotNullFunction<? super B, ? extends C> f, @NotNull final NotNullFunction<? super A, ? extends B> g) {
return a -> f.fun(g.fun(a));
}
/**
* Returns a runnable which runs both supplied runnables. If any of them throws, the second one is still executed.
* If both throw, the second exception is added to the first one as suppressed.
*
* @param r1 first runnable to run
* @param r2 second runnable to run
* @return composed runnable. If one of arguments is null, returns other argument.
*/
@Contract(value = "_, null -> param1; null, !null -> param2", pure = true)
public static Runnable composeRunnables(@Nullable Runnable r1, @Nullable Runnable r2) {
if (r2 == null) return r1;
if (r1 == null) return r2;
return () -> {
try {
r1.run();
}
catch (RuntimeException | Error ex) {
try {
r2.run();
}
catch (RuntimeException | Error ex2) {
ex.addSuppressed(ex2);
}
throw ex;
}
r2.run();
};
}
}