From 43b636f63fcf7421ccfe9553a80daf4e6860fd2e Mon Sep 17 00:00:00 2001 From: Tagir Valeev Date: Wed, 6 Mar 2019 13:20:47 +0700 Subject: [PATCH] UiInterceptors: ability to intercept UI components in tests and emulate user action Currently only popup chooser is supported A usage example added for AddExceptionToExistingCatchTest --- .../AddExceptionToExistingCatchFix.java | 6 +- ...eplace.java => afterReplace_Choose_B.java} | 0 .../afterReplace_Choose_C.java | 16 +++++ .../afterSeveralCatches_Choose_X.java | 19 +++++ .../afterSeveralCatches_Choose_Y.java | 19 +++++ ...place.java => beforeReplace_Choose_B.java} | 0 .../beforeReplace_Choose_C.java | 16 +++++ .../beforeSeveralCatches_Choose_X.java | 19 +++++ .../beforeSeveralCatches_Choose_Y.java | 19 +++++ .../AddExceptionToExistingCatchTest.java | 17 ++++- .../refactoring/IntroduceTargetChooser.java | 7 +- .../ui/popup/ComponentPopupBuilder.java | 10 +++ .../openapi/ui/popup/PopupChooserBuilder.java | 17 ++--- .../src/com/intellij/ui/UiInterceptors.java | 70 +++++++++++++++++++ .../com/intellij/ui/popup/AbstractPopup.java | 16 +++-- .../ui/popup/ComponentPopupBuilderImpl.java | 11 ++- .../testFramework/LightPlatformTestCase.java | 2 + .../com/intellij/ui/ChooserInterceptor.java | 67 ++++++++++++++++++ .../src/com/intellij/util/FunctionUtil.java | 31 ++++++++ 19 files changed, 338 insertions(+), 24 deletions(-) rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/{afterReplace.java => afterReplace_Choose_B.java} (100%) create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/afterReplace_Choose_C.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/afterSeveralCatches_Choose_X.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/afterSeveralCatches_Choose_Y.java rename java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/{beforeReplace.java => beforeReplace_Choose_B.java} (100%) create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/beforeReplace_Choose_C.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/beforeSeveralCatches_Choose_X.java create mode 100644 java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/beforeSeveralCatches_Choose_Y.java create mode 100644 platform/platform-impl/src/com/intellij/ui/UiInterceptors.java create mode 100644 platform/testFramework/src/com/intellij/ui/ChooserInterceptor.java diff --git a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/AddExceptionToExistingCatchFix.java b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/AddExceptionToExistingCatchFix.java index 0cc0057ffbec..7732524db580 100644 --- a/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/AddExceptionToExistingCatchFix.java +++ b/java/java-impl/src/com/intellij/codeInsight/daemon/impl/quickfix/AddExceptionToExistingCatchFix.java @@ -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); } diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/afterReplace.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/afterReplace_Choose_B.java similarity index 100% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/afterReplace.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/afterReplace_Choose_B.java diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/afterReplace_Choose_C.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/afterReplace_Choose_C.java new file mode 100644 index 000000000000..e06b968779b7 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/afterReplace_Choose_C.java @@ -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) { + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/afterSeveralCatches_Choose_X.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/afterSeveralCatches_Choose_X.java new file mode 100644 index 000000000000..c08f4345a768 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/afterSeveralCatches_Choose_X.java @@ -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) { + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/afterSeveralCatches_Choose_Y.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/afterSeveralCatches_Choose_Y.java new file mode 100644 index 000000000000..022d9d2fb412 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/afterSeveralCatches_Choose_Y.java @@ -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) { + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/beforeReplace.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/beforeReplace_Choose_B.java similarity index 100% rename from java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/beforeReplace.java rename to java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/beforeReplace_Choose_B.java diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/beforeReplace_Choose_C.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/beforeReplace_Choose_C.java new file mode 100644 index 000000000000..07fd991f4f64 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/beforeReplace_Choose_C.java @@ -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 (C e) { + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/beforeSeveralCatches_Choose_X.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/beforeSeveralCatches_Choose_X.java new file mode 100644 index 000000000000..debd90f05cd7 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/beforeSeveralCatches_Choose_X.java @@ -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 y) { + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/beforeSeveralCatches_Choose_Y.java b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/beforeSeveralCatches_Choose_Y.java new file mode 100644 index 000000000000..debd90f05cd7 --- /dev/null +++ b/java/java-tests/testData/codeInsight/daemonCodeAnalyzer/quickFix/addExceptionToExistingCatch/beforeSeveralCatches_Choose_Y.java @@ -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 y) { + } + } +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/quickFix/AddExceptionToExistingCatchTest.java b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/quickFix/AddExceptionToExistingCatchTest.java index c5473ac02415..783f52e2e45f 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/quickFix/AddExceptionToExistingCatchTest.java +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/daemon/quickFix/AddExceptionToExistingCatchTest.java @@ -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))); + } + } } - diff --git a/platform/lang-impl/src/com/intellij/refactoring/IntroduceTargetChooser.java b/platform/lang-impl/src/com/intellij/refactoring/IntroduceTargetChooser.java index 3acd65983e42..8d1b1316ce44 100644 --- a/platform/lang-impl/src/com/intellij/refactoring/IntroduceTargetChooser.java +++ b/platform/lang-impl/src/com/intellij/refactoring/IntroduceTargetChooser.java @@ -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(); + } } } diff --git a/platform/platform-api/src/com/intellij/openapi/ui/popup/ComponentPopupBuilder.java b/platform/platform-api/src/com/intellij/openapi/ui/popup/ComponentPopupBuilder.java index a04351438add..e57e176640a9 100644 --- a/platform/platform-api/src/com/intellij/openapi/ui/popup/ComponentPopupBuilder.java +++ b/platform/platform-api/src/com/intellij/openapi/ui/popup/ComponentPopupBuilder.java @@ -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); } diff --git a/platform/platform-api/src/com/intellij/openapi/ui/popup/PopupChooserBuilder.java b/platform/platform-api/src/com/intellij/openapi/ui/popup/PopupChooserBuilder.java index 9443330f51ab..e63521df1e07 100644 --- a/platform/platform-api/src/com/intellij/openapi/ui/popup/PopupChooserBuilder.java +++ b/platform/platform-api/src/com/intellij/openapi/ui/popup/PopupChooserBuilder.java @@ -60,7 +60,7 @@ public class PopupChooserBuilder implements IPopupChooserBuilder { private Function 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 implements IPopupChooserBuilder { 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 implements IPopupChooserBuilder { .setModalContext(myModalContext) .setCancelOnWindowDeactivation(myCancelOnWindowDeactivation) .setCancelOnClickOutside(myCancelOnClickOutside) - .setCouldPin(myCouldPin); + .setCouldPin(myCouldPin) + .setOkHandler(myItemChosenRunnable); BooleanFunction keyEventHandler = myChooserComponent.getKeyEventHandler(); if (keyEventHandler != null) { @@ -442,7 +443,7 @@ public class PopupChooserBuilder implements IPopupChooserBuilder { @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 implements IPopupChooserBuilder { 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 implements IPopupChooserBuilder { @Override @NotNull public PopupChooserBuilder setAdText(String ad) { - setAdText(ad, SwingUtilities.LEFT); + setAdText(ad, SwingConstants.LEFT); return this; } diff --git a/platform/platform-impl/src/com/intellij/ui/UiInterceptors.java b/platform/platform-impl/src/com/intellij/ui/UiInterceptors.java new file mode 100644 index 000000000000..d2058c2bf2d8 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/ui/UiInterceptors.java @@ -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> 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> interceptors = new ArrayList<>(ourInterceptors); + ourInterceptors.clear(); + if (!interceptors.isEmpty()) { + throw new IllegalStateException("Expected UI was not shown: " + interceptors); + } + } + + public abstract static class UiInterceptor { + private final @NotNull Class myClass; + + protected UiInterceptor(@NotNull Class 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() + ")"; + } + } +} diff --git a/platform/platform-impl/src/com/intellij/ui/popup/AbstractPopup.java b/platform/platform-impl/src/com/intellij/ui/popup/AbstractPopup.java index 13ae526454f7..41b9be75db89 100644 --- a/platform/platform-impl/src/com/intellij/ui/popup/AbstractPopup.java +++ b/platform/platform-impl/src/com/intellij/ui/popup/AbstractPopup.java @@ -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 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) { diff --git a/platform/platform-impl/src/com/intellij/ui/popup/ComponentPopupBuilderImpl.java b/platform/platform-impl/src/com/intellij/ui/popup/ComponentPopupBuilderImpl.java index 7cbcddec6608..3df623cf7592 100644 --- a/platform/platform-impl/src/com/intellij/ui/popup/ComponentPopupBuilderImpl.java +++ b/platform/platform-impl/src/com/intellij/ui/popup/ComponentPopupBuilderImpl.java @@ -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 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; + } } diff --git a/platform/testFramework/src/com/intellij/testFramework/LightPlatformTestCase.java b/platform/testFramework/src/com/intellij/testFramework/LightPlatformTestCase.java index 55ccaac6d88b..1156e744bbcd 100644 --- a/platform/testFramework/src/com/intellij/testFramework/LightPlatformTestCase.java +++ b/platform/testFramework/src/com/intellij/testFramework/LightPlatformTestCase.java @@ -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(() -> { diff --git a/platform/testFramework/src/com/intellij/ui/ChooserInterceptor.java b/platform/testFramework/src/com/intellij/ui/ChooserInterceptor.java new file mode 100644 index 000000000000..f8bc69a3a53b --- /dev/null +++ b/platform/testFramework/src/com/intellij/ui/ChooserInterceptor.java @@ -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 { + final List 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 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 actualOptions = IntStreamEx.range(model.getSize()).mapToObj(model::getElementAt).map(Object::toString).toList(); + if (myOptions != null) { + assertEquals(myOptions, actualOptions); + } + List 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"; + } +} diff --git a/platform/util/src/com/intellij/util/FunctionUtil.java b/platform/util/src/com/intellij/util/FunctionUtil.java index 11e8c00e9cc7..fbd88570e595 100644 --- a/platform/util/src/com/intellij/util/FunctionUtil.java +++ b/platform/util/src/com/intellij/util/FunctionUtil.java @@ -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 NotNullFunction composition(@NotNull final NotNullFunction f, @NotNull final NotNullFunction 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(); + }; + } }