From 4697dede9226e87f3cb14c6172ec8c5bf05c1c35 Mon Sep 17 00:00:00 2001 From: Sergey Karashevich Date: Wed, 1 Jun 2016 19:19:13 +0300 Subject: [PATCH] gui-tests-framework: GUI tests framework is forked from Android GUI tests framework; excision of android and gradle dependencies; addition dependencies to community-tests module and main_idea_tests --- .idea/libraries/jsr305_1_3_9.xml | 11 + community-tests/community-tests.iml | 42 + .../intellij/tests/gui/NewProjectTest.java | 154 +++ .../gui/driver/SearchTextFieldDriver.java | 68 + .../gui/fixtures/ActionButtonFixture.java | 114 ++ .../tests/gui/fixtures/ActionLinkFixture.java | 73 ++ .../gui/fixtures/ComboBoxActionFixture.java | 158 +++ .../tests/gui/fixtures/ComponentFixture.java | 49 + .../ConfigureProjectSubsetDialogFixture.java | 60 + .../gui/fixtures/DebugToolWindowFixture.java | 24 + .../tests/gui/fixtures/EditorFixture.java | 1089 +++++++++++++++++ .../EditorNotificationPanelFixture.java | 41 + .../fixtures/ExecutionToolWindowFixture.java | 254 ++++ .../fixtures/FileChooserDialogFixture.java | 113 ++ .../tests/gui/fixtures/FileFixture.java | 196 +++ .../tests/gui/fixtures/FindDialogFixture.java | 61 + .../gui/fixtures/FindToolWindowFixture.java | 94 ++ .../tests/gui/fixtures/IdeFrameFixture.java | 868 +++++++++++++ .../fixtures/IdeSettingsDialogFixture.java | 97 ++ .../tests/gui/fixtures/IdeaDialogFixture.java | 120 ++ .../gui/fixtures/InputDialogFixture.java | 69 ++ .../gui/fixtures/InspectionsFixture.java | 83 ++ .../tests/gui/fixtures/JComponentFixture.java | 44 + .../tests/gui/fixtures/LibraryFixture.java | 37 + .../tests/gui/fixtures/MenuFixture.java | 125 ++ .../gui/fixtures/MessageDialogFixture.java | 72 ++ .../tests/gui/fixtures/MessagesFixture.java | 174 +++ .../fixtures/MessagesToolWindowFixture.java | 375 ++++++ .../gui/fixtures/ProjectViewFixture.java | 282 +++++ .../gui/fixtures/RenameDialogFixture.java | 138 +++ .../RenameRefactoringDialogFixture.java | 110 ++ .../ResourceChooserDialogFixture.java | 59 + .../RunConfigurationComboBoxFixture.java | 66 + .../RunConfigurationsDialogFixture.java | 153 +++ .../gui/fixtures/RunToolWindowFixture.java | 24 + .../gui/fixtures/SearchTextFieldFixture.java | 49 + .../SelectRefactoringDialogFixture.java | 70 ++ .../gui/fixtures/SelectSdkDialogFixture.java | 48 + .../tests/gui/fixtures/ToolWindowFixture.java | 222 ++++ .../gui/fixtures/UnitTestTreeFixture.java | 81 ++ .../gui/fixtures/WelcomeFrameFixture.java | 62 + .../AbstractWizardFixture.java | 105 ++ .../AbstractWizardStepFixture.java | 60 + .../ChooseOptionsForNewFileStepFixture.java | 49 + .../ConfigureAndroidProjectStepFixture.java | 62 + .../ConfigureFormFactorStepFixture.java | 65 + .../NewProjectWizardFixture.java | 76 ++ .../tests/gui/framework/GuiTestCase.java | 403 ++++++ .../gui/framework/GuiTestConfigurator.java | 188 +++ .../tests/gui/framework/GuiTestRunner.java | 180 +++ .../tests/gui/framework/GuiTests.java | 562 +++++++++ .../tests/gui/framework/IdeGuiTest.java | 33 + .../tests/gui/framework/IdeGuiTestSetup.java | 31 + .../gui/framework/IdeTestApplication.java | 274 +++++ .../tests/gui/framework/MethodInvoker.java | 151 +++ .../tests/gui/matcher/ClassNameMatcher.java | 52 + 56 files changed, 8320 insertions(+) create mode 100644 .idea/libraries/jsr305_1_3_9.xml create mode 100644 community-tests/src/com/intellij/tests/gui/NewProjectTest.java create mode 100644 community-tests/src/com/intellij/tests/gui/driver/SearchTextFieldDriver.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/ActionButtonFixture.java create mode 100755 community-tests/src/com/intellij/tests/gui/fixtures/ActionLinkFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/ComboBoxActionFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/ComponentFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/ConfigureProjectSubsetDialogFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/DebugToolWindowFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/EditorFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/EditorNotificationPanelFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/ExecutionToolWindowFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/FileChooserDialogFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/FileFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/FindDialogFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/FindToolWindowFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/IdeFrameFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/IdeSettingsDialogFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/IdeaDialogFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/InputDialogFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/InspectionsFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/JComponentFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/LibraryFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/MenuFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/MessageDialogFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/MessagesFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/MessagesToolWindowFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/ProjectViewFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/RenameDialogFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/RenameRefactoringDialogFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/ResourceChooserDialogFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/RunConfigurationComboBoxFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/RunConfigurationsDialogFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/RunToolWindowFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/SearchTextFieldFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/SelectRefactoringDialogFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/SelectSdkDialogFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/ToolWindowFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/UnitTestTreeFixture.java create mode 100755 community-tests/src/com/intellij/tests/gui/fixtures/WelcomeFrameFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/newProjectWizard/AbstractWizardFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/newProjectWizard/AbstractWizardStepFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/newProjectWizard/ChooseOptionsForNewFileStepFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/newProjectWizard/ConfigureAndroidProjectStepFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/newProjectWizard/ConfigureFormFactorStepFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/fixtures/newProjectWizard/NewProjectWizardFixture.java create mode 100644 community-tests/src/com/intellij/tests/gui/framework/GuiTestCase.java create mode 100644 community-tests/src/com/intellij/tests/gui/framework/GuiTestConfigurator.java create mode 100755 community-tests/src/com/intellij/tests/gui/framework/GuiTestRunner.java create mode 100644 community-tests/src/com/intellij/tests/gui/framework/GuiTests.java create mode 100644 community-tests/src/com/intellij/tests/gui/framework/IdeGuiTest.java create mode 100644 community-tests/src/com/intellij/tests/gui/framework/IdeGuiTestSetup.java create mode 100644 community-tests/src/com/intellij/tests/gui/framework/IdeTestApplication.java create mode 100644 community-tests/src/com/intellij/tests/gui/framework/MethodInvoker.java create mode 100644 community-tests/src/com/intellij/tests/gui/matcher/ClassNameMatcher.java diff --git a/.idea/libraries/jsr305_1_3_9.xml b/.idea/libraries/jsr305_1_3_9.xml new file mode 100644 index 000000000000..af2783af9f2e --- /dev/null +++ b/.idea/libraries/jsr305_1_3_9.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/community-tests/community-tests.iml b/community-tests/community-tests.iml index 142051034725..198696e4de5c 100644 --- a/community-tests/community-tests.iml +++ b/community-tests/community-tests.iml @@ -7,5 +7,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/community-tests/src/com/intellij/tests/gui/NewProjectTest.java b/community-tests/src/com/intellij/tests/gui/NewProjectTest.java new file mode 100644 index 000000000000..c7ead4db7c1a --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/NewProjectTest.java @@ -0,0 +1,154 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui; + +import com.intellij.tests.gui.fixtures.newProjectWizard.NewProjectWizardFixture; +import com.intellij.tests.gui.framework.GuiTestCase; +import com.intellij.tests.gui.framework.IdeGuiTest; +import org.jetbrains.annotations.NotNull; +import org.junit.Test; + +import java.io.IOException; + + +/** + * Created by karashevich on 27/05/16. + */ +public class NewProjectTest extends GuiTestCase { + + @Test @IdeGuiTest + public void testNewProject() throws IOException, InterruptedException { + String myName = "TestProject"; + String myDomain = "com.test"; + String myPkg = "com.android.test.app"; + + + findWelcomeFrame().createNewProject(); + NewProjectWizardFixture newProjectWizard = findNewProjectWizard(); + newProjectWizard.selectProjectType("Java"); + //newProjectWizard. + + Thread.sleep(10000); + + //newProjectWizard.clickNext() + // + //newProjectWizard.getConfigureFormFactorStep().selectMinimumSdkApi(MOBILE, myMinSdk); + //newProjectWizard.clickNext(); + + // Skip "Add Activity" step + newProjectWizard.clickNext(); + + + //newProjectWizard.getChooseOptionsForNewFileStep().enterActivityName(myActivity);ё + //newProjectWizard.clickFinish(); + + } + + @NotNull + NewProjectDescriptor newProject(@NotNull String name) { + return new NewProjectDescriptor(name); + } + + private class NewProjectDescriptor { + private String myActivity = "MainActivity"; + private String myPkg = "com.android.test.app"; + private String myMinSdk = "19"; + private String myName = "TestProject"; + private String myDomain = "com.android"; + private boolean myWaitForSync = true; + + + private NewProjectDescriptor(@NotNull String name) { + withName(name); + } + + /** + * Set a custom package to use in the new project + */ + NewProjectDescriptor withPackageName(@NotNull String pkg) { + myPkg = pkg; + return this; + } + + /** + * Set a new project name to use for the new project + */ + NewProjectDescriptor withName(@NotNull String name) { + myName = name; + return this; + } + + /** + * Set a custom activity name to use in the new project + */ + NewProjectDescriptor withActivity(@NotNull String activity) { + myActivity = activity; + return this; + } + + /** + * Set a custom minimum SDK version to use in the new project + */ + NewProjectDescriptor withMinSdk(@NotNull String minSdk) { + myMinSdk = minSdk; + return this; + } + + /** + * Set a custom company domain to enter in the new project wizard + */ + NewProjectDescriptor withCompanyDomain(@NotNull String domain) { + myDomain = domain; + return this; + } + + /** + * Picks brief names in order to make the test execute faster (less slow typing in name text fields) + */ + NewProjectDescriptor withBriefNames() { + withActivity("A").withCompanyDomain("C").withName("P").withPackageName("a.b"); + return this; + } + + /** Turns off the automatic wait-for-sync that normally happens on {@link #create} */ + NewProjectDescriptor withoutSync() { + myWaitForSync = false; + return this; + } + + /** + * Creates a project fixture for this description + */ + void create() { + findWelcomeFrame().createNewProject(); + + NewProjectWizardFixture newProjectWizard = findNewProjectWizard(); + + newProjectWizard.clickNext(); + + newProjectWizard.clickNext(); + + // Skip "Add Activity" step + newProjectWizard.clickNext(); + + newProjectWizard.getChooseOptionsForNewFileStep().enterActivityName(myActivity); + newProjectWizard.clickFinish(); + + } + } + + +} diff --git a/community-tests/src/com/intellij/tests/gui/driver/SearchTextFieldDriver.java b/community-tests/src/com/intellij/tests/gui/driver/SearchTextFieldDriver.java new file mode 100644 index 000000000000..1baf254f661e --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/driver/SearchTextFieldDriver.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.driver; + +import com.intellij.ui.SearchTextField; +import org.fest.swing.annotation.RunsInEDT; +import org.fest.swing.core.Robot; +import org.fest.swing.driver.JComponentDriver; +import org.fest.swing.driver.TextDisplayDriver; +import org.fest.swing.edt.GuiQuery; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.regex.Pattern; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.swing.edt.GuiActionRunner.execute; + +public class SearchTextFieldDriver extends JComponentDriver implements TextDisplayDriver { + public SearchTextFieldDriver(@NotNull Robot robot) { + super(robot); + } + + @Override + @RunsInEDT + public void requireText(@NotNull SearchTextField component, String expected) { + assertThat(textOf(component)).isEqualTo(expected); + } + + @Override + @RunsInEDT + public void requireText(@NotNull SearchTextField component, final @NotNull Pattern pattern) { + assertThat(textOf(component)).matches(pattern.pattern()); + } + + @Override + @RunsInEDT + @Nullable + public String textOf(final @NotNull SearchTextField component) { + return execute(new GuiQuery() { + @Override + protected + @Nullable + String executeInEDT() { + return component.getText(); + } + }); + } + + @RunsInEDT + public void enterText(@NotNull SearchTextField textBox, @NotNull String text) { + focusAndWaitForFocusGain(textBox); + robot.enterText(text); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/ActionButtonFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/ActionButtonFixture.java new file mode 100644 index 000000000000..8c054dfe65fb --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/ActionButtonFixture.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.impl.ActionButton; +import com.intellij.openapi.util.Ref; +import org.fest.swing.core.GenericTypeMatcher; +import org.fest.swing.core.Robot; +import org.fest.swing.edt.GuiQuery; +import org.fest.swing.exception.ComponentLookupException; +import org.fest.swing.timing.Condition; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.util.Collection; + +import static com.intellij.tests.gui.framework.GuiTests.LONG_TIMEOUT; +import static com.intellij.tests.gui.framework.GuiTests.SHORT_TIMEOUT; +import static com.intellij.util.containers.ContainerUtil.getFirstItem; +import static org.fest.swing.edt.GuiActionRunner.execute; +import static org.fest.swing.timing.Pause.pause; + +public class ActionButtonFixture extends JComponentFixture { + + @NotNull + public static ActionButtonFixture findByActionId(@NotNull final String actionId, + @NotNull final Robot robot, + @NotNull final Container container) { + final Ref actionButtonRef = new Ref(); + pause(new Condition("Find ActionButton with ID '" + actionId + "'") { + @Override + public boolean test() { + Collection found = robot.finder().findAll(container, new GenericTypeMatcher(ActionButton.class) { + @Override + protected boolean isMatching(@NotNull ActionButton button) { + if (button.isVisible()) { + AnAction action = button.getAction(); + if (action != null) { + String id = ActionManager.getInstance().getId(action); + return actionId.equals(id); + } + } + return false; + } + }); + if (found.size() == 1) { + actionButtonRef.set(getFirstItem(found)); + return true; + } + return false; + } + }, SHORT_TIMEOUT); + + ActionButton button = actionButtonRef.get(); + if (button == null) { + throw new ComponentLookupException("Failed to find ActionButton with ID '" + actionId + "'"); + } + return new ActionButtonFixture(robot, button); + } + + @NotNull + public ActionButtonFixture waitUntilEnabledAndShowing() { + pause(new Condition("wait for action to be enabled and showing") { + @Override + public boolean test() { + //noinspection ConstantConditions + return execute(new GuiQuery() { + @Nullable + @Override + protected Boolean executeInEDT() throws Throwable { + ActionButton target = target(); + if (target.getAction().getTemplatePresentation().isEnabledAndVisible()) { + return target.isShowing() && target.isVisible() && target.isEnabled(); + } + return false; + } + }); + } + }, LONG_TIMEOUT); + return this; + } + + @NotNull + public static ActionButtonFixture findByText(@NotNull final String text, @NotNull Robot robot, @NotNull Container container) { + final ActionButton button = robot.finder().find(container, new GenericTypeMatcher(ActionButton.class) { + @Override + protected boolean isMatching(@NotNull ActionButton button) { + AnAction action = button.getAction(); + return text.equals(action.getTemplatePresentation().getText()); + } + }); + return new ActionButtonFixture(robot, button); + } + + private ActionButtonFixture(@NotNull Robot robot, @NotNull ActionButton target) { + super(ActionButtonFixture.class, robot, target); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/ActionLinkFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/ActionLinkFixture.java new file mode 100755 index 000000000000..24bbda785a64 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/ActionLinkFixture.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import org.fest.swing.core.Robot; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.util.Ref; +import com.intellij.ui.components.labels.ActionLink; +import org.fest.swing.core.GenericTypeMatcher; +import org.fest.swing.exception.ComponentLookupException; +import org.fest.swing.timing.Condition; +import org.jetbrains.annotations.NotNull; + +import java.awt.*; +import java.util.Collection; + +import static com.intellij.tests.gui.framework.GuiTests.SHORT_TIMEOUT; +import static com.intellij.util.containers.ContainerUtil.getFirstItem; +import static org.fest.swing.timing.Pause.pause; + +public class ActionLinkFixture extends JComponentFixture { + @NotNull + public static ActionLinkFixture findByActionId(@NotNull final String actionId, + @NotNull final Robot robot, + @NotNull final Container container) { + final Ref actionLinkRef = new Ref(); + pause(new Condition("Find ActionLink with ID '" + actionId + "'") { + @Override + public boolean test() { + Collection found = robot.finder().findAll(container, new GenericTypeMatcher(ActionLink.class) { + @Override + protected boolean isMatching(@NotNull ActionLink actionLink) { + if (actionLink.isVisible()) { + AnAction action = actionLink.getAction(); + String id = ActionManager.getInstance().getId(action); + return actionId.equals(id); + } + return false; + } + }); + if (found.size() == 1) { + actionLinkRef.set(getFirstItem(found)); + return true; + } + return false; + } + }, SHORT_TIMEOUT); + + ActionLink actionLink = actionLinkRef.get(); + if (actionLink == null) { + throw new ComponentLookupException("Failed to find ActionLink with ID '" + actionId + "'"); + } + return new ActionLinkFixture(robot, actionLink); + } + + private ActionLinkFixture(@NotNull Robot robot, @NotNull ActionLink target) { + super(ActionLinkFixture.class, robot, target); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/ComboBoxActionFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/ComboBoxActionFixture.java new file mode 100644 index 000000000000..ac7ae2836ed2 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/ComboBoxActionFixture.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.intellij.openapi.actionSystem.ex.ComboBoxAction; +import com.intellij.ui.JBListWithHintProvider; +import com.intellij.ui.popup.PopupFactoryImpl; +import com.intellij.ui.popup.list.ListPopupModel; +import org.fest.swing.core.GenericTypeMatcher; +import org.fest.swing.core.Robot; +import org.fest.swing.edt.GuiQuery; +import org.fest.swing.edt.GuiTask; +import org.fest.swing.fixture.JButtonFixture; +import org.fest.swing.timing.Condition; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; + +import java.awt.*; + +import static com.intellij.tests.gui.framework.GuiTests.SHORT_TIMEOUT; +import static org.fest.swing.edt.GuiActionRunner.execute; +import static org.fest.swing.timing.Pause.pause; +import static org.junit.Assert.*; + +public class ComboBoxActionFixture { + @NotNull private Robot myRobot; + @NotNull private JButton myTarget; + private static final Class ourComboBoxButtonClass; + static { + Class temp = null; + try { + temp = ComboBoxActionFixture.class.getClassLoader().loadClass(ComboBoxAction.class.getCanonicalName() + "$ComboBoxButton"); + } + catch (ClassNotFoundException e) { + e.printStackTrace(); + } + ourComboBoxButtonClass = temp; + } + + public static ComboBoxActionFixture findComboBox(@NotNull Robot robot, @NotNull Container root) { + JButton comboBoxButton = robot.finder().find(root, new GenericTypeMatcher(JButton.class) { + @Override + protected boolean isMatching(@NotNull JButton component) { + return ourComboBoxButtonClass.isInstance(component); + } + }); + return new ComboBoxActionFixture(robot, comboBoxButton); + } + + public static ComboBoxActionFixture findComboBoxByText(@NotNull Robot robot, @NotNull Container root, @NotNull final String text) { + JButton comboBoxButton = robot.finder().find(root, new GenericTypeMatcher(JButton.class) { + @Override + protected boolean isMatching(@NotNull JButton component) { + return ourComboBoxButtonClass.isInstance(component) && component.getText().equals(text); + } + }); + return new ComboBoxActionFixture(robot, comboBoxButton); + } + + public ComboBoxActionFixture(@NotNull Robot robot, @NotNull JButton target) { + myRobot = robot; + myTarget = target; + } + + public void selectItem(@NotNull String itemName) { + click(); + selectItemByText(getPopupList(), itemName); + } + + public String getSelectedItemText() { + return execute(new GuiQuery() { + @Nullable + @Override + protected String executeInEDT() throws Throwable { + return myTarget.getText(); + } + }); + } + + private void click() { + final JButtonFixture comboBoxButtonFixture = new JButtonFixture(myRobot, myTarget); + pause(new Condition("Wait until comboBoxButton is enabled") { + @Override + public boolean test() { + //noinspection ConstantConditions + return execute(new GuiQuery() { + @Override + protected Boolean executeInEDT() throws Throwable { + return comboBoxButtonFixture.target().isEnabled(); + } + }); + } + }, SHORT_TIMEOUT); + comboBoxButtonFixture.click(); + } + + @NotNull + private JList getPopupList() { + return myRobot.finder().findByType(JBListWithHintProvider.class); + } + + private static void selectItemByText(@NotNull final JList list, @NotNull final String text) { + pause(new Condition("Wait until the list is populated.") { + @Override + public boolean test() { + ListPopupModel popupModel = (ListPopupModel)list.getModel(); + for (int i = 0; i < popupModel.getSize(); ++i) { + PopupFactoryImpl.ActionItem actionItem = (PopupFactoryImpl.ActionItem)popupModel.get(i); + assertNotNull(actionItem); + if (text.equals(actionItem.getText())) { + return true; + } + } + return false; + } + }, SHORT_TIMEOUT); + + final Integer appIndex = execute(new GuiQuery() { + @Override + protected Integer executeInEDT() throws Throwable { + ListPopupModel popupModel = (ListPopupModel)list.getModel(); + for (int i = 0; i < popupModel.getSize(); ++i) { + PopupFactoryImpl.ActionItem actionItem = (PopupFactoryImpl.ActionItem)popupModel.get(i); + assertNotNull(actionItem); + if (text.equals(actionItem.getText())) { + return i; + } + } + return -1; + } + }); + //noinspection ConstantConditions + assertTrue(appIndex >= 0); + + execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + list.setSelectedIndex(appIndex); + } + }); + assertEquals(text, ((PopupFactoryImpl.ActionItem)list.getSelectedValue()).getText()); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/ComponentFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/ComponentFixture.java new file mode 100644 index 000000000000..5cca67e2e075 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/ComponentFixture.java @@ -0,0 +1,49 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import org.fest.swing.core.Robot; +import org.fest.swing.driver.ComponentDriver; +import org.fest.swing.fixture.AbstractComponentFixture; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; + + +/** + * Created by karashevich on 30/05/16. + */ +public class ComponentFixture extends AbstractComponentFixture { + public ComponentFixture(@NotNull Class selfType, @NotNull Robot robot, @NotNull Class type) { + super(selfType, robot, type); + } + + public ComponentFixture(@NotNull Class selfType, @NotNull Robot robot, @Nullable String name, @NotNull Class type) { + super(selfType, robot, name, type); + } + + public ComponentFixture(@NotNull Class selfType, @NotNull Robot robot, @NotNull C target) { + super(selfType, robot, target); + } + + @Override + @NotNull + protected ComponentDriver createDriver(@NotNull Robot robot) { + return new ComponentDriver(robot); + } +} + diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/ConfigureProjectSubsetDialogFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/ConfigureProjectSubsetDialogFixture.java new file mode 100644 index 000000000000..117c98e19f5f --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/ConfigureProjectSubsetDialogFixture.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import org.fest.swing.core.Robot; +import org.fest.swing.core.matcher.DialogMatcher; +import org.fest.swing.fixture.DialogFixture; +import org.fest.swing.fixture.JTableCellFixture; +import org.fest.swing.fixture.JTableFixture; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +import static com.intellij.tests.gui.framework.GuiTests.SHORT_TIMEOUT; +import static com.intellij.tests.gui.framework.GuiTests.findAndClickOkButton; +import static org.fest.swing.core.matcher.DialogMatcher.withTitle; +import static org.fest.swing.data.TableCell.row; +import static org.fest.swing.finder.WindowFinder.findDialog; + +public class ConfigureProjectSubsetDialogFixture { + @NotNull private DialogFixture myDialog; + @NotNull private final JTableFixture myModulesTable; + + @NotNull + public static ConfigureProjectSubsetDialogFixture find(@NotNull Robot robot) { + DialogMatcher matcher = withTitle("Select Modules to Include in Project Subset").andShowing(); + DialogFixture dialog = findDialog(matcher).withTimeout(SHORT_TIMEOUT.duration()).using(robot); + return new ConfigureProjectSubsetDialogFixture(dialog); + } + + private ConfigureProjectSubsetDialogFixture(@NotNull DialogFixture dialog) { + myDialog = dialog; + Robot robot = dialog.robot(); + myModulesTable = new JTableFixture(robot, robot.finder().findByType(dialog.target(), JTable.class, true)); + } + + @NotNull + public ConfigureProjectSubsetDialogFixture selectModule(@NotNull String moduleName, boolean selected) { + JTableCellFixture cell = myModulesTable.cell(moduleName); + myModulesTable.enterValue(row(cell.row()).column(0), String.valueOf(selected)); + return this; + } + + public void clickOk() { + findAndClickOkButton(myDialog); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/DebugToolWindowFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/DebugToolWindowFixture.java new file mode 100644 index 000000000000..8902ce1e4da0 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/DebugToolWindowFixture.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import org.jetbrains.annotations.NotNull; + +public class DebugToolWindowFixture extends ExecutionToolWindowFixture { + public DebugToolWindowFixture(@NotNull IdeFrameFixture frameFixture) { + super("Debug", frameFixture); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/EditorFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/EditorFixture.java new file mode 100644 index 000000000000..c67b5a81894d --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/EditorFixture.java @@ -0,0 +1,1089 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.google.common.collect.Lists; +import com.intellij.codeInsight.daemon.impl.HighlightInfo; +import com.intellij.lang.annotation.HighlightSeverity; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.KeyboardShortcut; +import com.intellij.openapi.actionSystem.Shortcut; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.fileEditor.FileEditor; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.fileEditor.OpenFileDescriptor; +import com.intellij.openapi.keymap.Keymap; +import com.intellij.openapi.keymap.KeymapManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.components.JBList; +import org.fest.swing.core.GenericTypeMatcher; +import org.fest.swing.core.Robot; +import org.fest.swing.driver.ComponentDriver; +import org.fest.swing.edt.GuiQuery; +import org.fest.swing.edt.GuiTask; +import org.fest.swing.fixture.DialogFixture; +import org.fest.swing.timing.Condition; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.FocusManager; +import javax.swing.*; +import java.awt.*; +import java.awt.event.InputMethodEvent; +import java.awt.event.KeyEvent; +import java.awt.font.TextHitInfo; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.util.List; + +import static com.intellij.tests.gui.framework.GuiTests.*; +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.reflect.core.Reflection.method; +import static org.fest.swing.edt.GuiActionRunner.execute; +import static org.fest.swing.timing.Pause.pause; +import static org.fest.util.Strings.quote; +import static org.junit.Assert.*; + +/** + * Fixture wrapping the IDE source editor, providing convenience methods + * for controlling the source editor and verifying editor state. Note that unlike + * the IntelliJ Editor class, which is one per file, this fixture represents an + * editor in the more traditional sense: a container for multiple files, so you + * ask "the" editor its current file, to select text in that file, to switch to + * a different file, etc. + */ +public class EditorFixture { + public static final String CARET = "^"; + public static final String SELECT_BEGIN = "|>"; + public static final String SELECT_END = "<|"; + + /** + * Performs simulation of user events on {@link #target} + */ + public final Robot robot; + private final IdeFrameFixture myFrame; + + /** + * Constructs a new editor fixture, tied to the given project + */ + public EditorFixture(Robot robot, IdeFrameFixture frame) { + this.robot = robot; + myFrame = frame; + } + + /** + * Returns the current file being shown in the editor, if there is a current + * editor open and it's a file editor + * + * @return the currently edited file or null + */ + @Nullable + public VirtualFile getCurrentFile() { + FileEditorManager manager = FileEditorManager.getInstance(myFrame.getProject()); + VirtualFile[] selectedFiles = manager.getSelectedFiles(); + if (selectedFiles.length > 0) { + return selectedFiles[0]; + } + + return null; + } + + /** + * Returns the name of the current file, if any. Convenience method + * for {@link #getCurrentFile()}.getName(). + * + * @return the current file name, or null + */ + @Nullable + public String getCurrentFileName() { + VirtualFile currentFile = getCurrentFile(); + return currentFile != null ? currentFile.getName() : null; + } + + /** + * Returns the line number of the current caret position (0-based). + * + * @return the current 0-based line number, or -1 if there is no current file + */ + public int getCurrentLineNumber() { + //noinspection ConstantConditions + return execute(new GuiQuery() { + @Override + @Nullable + protected Integer executeInEDT() throws Throwable { + FileEditorManager manager = FileEditorManager.getInstance(myFrame.getProject()); + Editor editor = manager.getSelectedTextEditor(); + if (editor != null) { + CaretModel caretModel = editor.getCaretModel(); + Caret primaryCaret = caretModel.getPrimaryCaret(); + int offset = primaryCaret.getOffset(); + Document document = editor.getDocument(); + return document.getLineNumber(offset); + } + + return -1; + } + }); + } + + /** + * Returns the contents of the current line, or null if there is no + * file open. The caret position is indicated by {@code ^}, and + * the selection text range, if on the current line, is indicated by + * the text inside {@code |> <|}. + * + * @param trim if true, trim whitespace around the line + * @param showPositions if true, show the editor positions (carets, selection) + * @param additionalLines 0, or a count for additional number of lines to include on each side of the current line + * @return the text contents at the current caret position + */ + @Nullable + public String getCurrentLineContents(boolean trim, boolean showPositions, int additionalLines) { + if (showPositions) { + return getCurrentLineContents(trim, CARET, SELECT_BEGIN, SELECT_END, additionalLines); + } + else { + return getCurrentLineContents(trim, null, null, null, additionalLines); + } + } + + /** + * Returns the contents of the current line, or null if there is no + * file open. + * + * @param trim if true, trim whitespace around the line + * @param caretString typically "^" which will insert "^" to indicate the + * caret position. If null, the caret position is not shown. + * @param selectBegin the text string to insert at the beginning of the selection boundary + * @param selectEnd the text string to insert at the end of the selection boundary + * @return the text contents at the current caret position + */ + @Nullable + public String getCurrentLineContents(final boolean trim, + @Nullable final String caret, + @Nullable final String selectBegin, + @Nullable final String selectEnd, + final int additionalLines) { + return execute(new GuiQuery() { + @Override + @Nullable + protected String executeInEDT() throws Throwable { + FileEditorManager manager = FileEditorManager.getInstance(myFrame.getProject()); + Editor editor = manager.getSelectedTextEditor(); + if (editor != null) { + CaretModel caretModel = editor.getCaretModel(); + Caret primaryCaret = caretModel.getPrimaryCaret(); + int offset = primaryCaret.getOffset(); + int start = primaryCaret.getSelectionStart(); + int end = primaryCaret.getSelectionEnd(); + if (start == end) { + start = -1; + end = -1; + } + Document document = editor.getDocument(); + int lineNumber = document.getLineNumber(offset); + int lineStart = document.getLineStartOffset(lineNumber); + int lineEnd = document.getLineEndOffset(lineNumber); + int lineCount = document.getLineCount(); + for (int i = 1; i <= additionalLines; i++) { + if (lineNumber - i >= 0) { + lineStart = document.getLineStartOffset(lineNumber - i); + } + if (lineNumber + i < lineCount) { + lineEnd = document.getLineEndOffset(lineNumber + i); + } + } + + String line = document.getText(new TextRange(lineStart, lineEnd)); + offset -= lineStart; + start -= lineStart; + end -= lineStart; + StringBuilder sb = new StringBuilder(line.length() + 10); + for (int i = 0, n = line.length(); i < n; i++) { + if (selectBegin != null && start == i) { + sb.append(selectBegin); + } + if (caret != null && offset == i) { + sb.append(caret); + } + sb.append(line.charAt(i)); + if (selectEnd != null && end == i + 1) { + sb.append(selectEnd); + } + } + String result = sb.toString(); + if (trim) { + result = result.trim(); + } + return result; + } + + return null; + } + }); + } + + /** + * Returns the contents of the current file, or null if there is no + * file open. The caret position is indicated by {@code ^}, and + * the selection text range, if on the current line, is indicated by + * the text inside {@code |> <|}. + * + * @param showPositions if true, show the editor positions (carets, selection) + * @return the text contents at the current caret position + */ + @Nullable + public String getCurrentFileContents(boolean showPositions) { + if (showPositions) { + return getCurrentFileContents(CARET, SELECT_BEGIN, SELECT_END); + } + else { + return getCurrentFileContents(null, null, null); + } + } + + /** + * Returns the contents of the current file, or null if there is no + * file open. + * + * @param caretString typically "^" which will insert "^" to indicate the + * caret position. If null, the caret position is not shown. + * @param selectBegin the text string to insert at the beginning of the selection boundary + * @param selectEnd the text string to insert at the end of the selection boundary + * @return the text contents at the current caret position + */ + @Nullable + public String getCurrentFileContents(@Nullable final String caret, @Nullable final String selectBegin, @Nullable final String selectEnd) { + return execute(new GuiQuery() { + @Override + @Nullable + protected String executeInEDT() throws Throwable { + FileEditorManager manager = FileEditorManager.getInstance(myFrame.getProject()); + Editor editor = manager.getSelectedTextEditor(); + if (editor != null) { + CaretModel caretModel = editor.getCaretModel(); + Caret primaryCaret = caretModel.getPrimaryCaret(); + int offset = primaryCaret.getOffset(); + int start = primaryCaret.getSelectionStart(); + int end = primaryCaret.getSelectionEnd(); + if (start == end) { + start = -1; + end = -1; + } + Document document = editor.getDocument(); + int lineStart = 0; + int lineEnd = document.getTextLength(); + String text = document.getText(new TextRange(lineStart, lineEnd)); + StringBuilder sb = new StringBuilder(text.length() + 10); + for (int i = 0, n = text.length(); i < n; i++) { + if (selectBegin != null && start == i) { + sb.append(selectBegin); + } + if (caret != null && offset == i) { + sb.append(caret); + } + sb.append(text.charAt(i)); + if (selectEnd != null && end == i + 1) { + sb.append(selectEnd); + } + } + return sb.toString(); + } + + return null; + } + }); + } + + /** + * Type the given text into the editor + * + * @param text the text to type at the current editor position + */ + public EditorFixture enterText(@NotNull final String text) { + Component component = getFocusedEditor(); + if (component != null) { + robot.enterText(text); + } + + return this; + } + + /** + * Type the given text into the editor as if the user had typed it + * with an IME (an input method editor) + * + * @param text the text to type at the current editor position + */ + public EditorFixture enterImeText(@NotNull final String text) { + final Component component = getFocusedEditor(); + if (component != null && !text.isEmpty()) { + execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + // Simulate editing by sending the same IME events that we observe arriving from a real input method + int characterCount = text.length(); + TextHitInfo caret = TextHitInfo.afterOffset(characterCount - 1); + TextHitInfo visiblePosition = TextHitInfo.beforeOffset(0); + AttributedCharacterIterator iterator = new AttributedString(text).getIterator(); + int id = InputMethodEvent.INPUT_METHOD_TEXT_CHANGED; + InputMethodEvent event = new InputMethodEvent(component, id, iterator, characterCount, caret, visiblePosition); + component.dispatchEvent(event); + } + }); + } + + return this; + } + + /** + * Press and release the given key as indicated by the {@code VK_} codes in {@link KeyEvent}. + * Used to transfer key presses to the editor which may have an effect but does not insert text into + * the editor (e.g. pressing an arrow key to move the caret) + * + * @param keyCode the key code to press + */ + public EditorFixture typeKey(int keyCode) { + Component component = getFocusedEditor(); + if (component != null) { + new ComponentDriver(robot).pressAndReleaseKeys(component, keyCode); + } + return this; + } + + /** + * Press (but don't release yet) the given key as indicated by the {@code VK_} codes in {@link KeyEvent}. + * Used to transfer key presses to the editor which may have an effect but does not insert text into + * the editor (e.g. pressing an arrow key to move the caret) + * + * @param keyCode the key code to press + */ + public EditorFixture pressKey(int keyCode) { + Component component = getFocusedEditor(); + if (component != null) { + new ComponentDriver(robot).pressKey(component, keyCode); + } + return this; + } + + /** + * Release the given key (as indicated by the {@code VK_} codes in {@link KeyEvent}) which + * must be currently pressed by a previous call to {@link #pressKey(int)}. + * + * @param keyCode the key code + */ + public EditorFixture releaseKey(int keyCode) { + Component component = getFocusedEditor(); + if (component != null) { + new ComponentDriver(robot).releaseKey(component, keyCode); + } + return this; + } + + /** + * Requests focus in the editor + */ + public EditorFixture requestFocus() { + getFocusedEditor(); + return this; + } + + /** + * Requests focus in the editor, waits and returns editor component + */ + @Nullable + private JComponent getFocusedEditor() { + Editor editor = execute(new GuiQuery() { + @Override + @Nullable + protected Editor executeInEDT() throws Throwable { + FileEditorManager manager = FileEditorManager.getInstance(myFrame.getProject()); + return manager.getSelectedTextEditor(); // Must be called from the EDT + } + }); + + if (editor != null) { + JComponent contentComponent = editor.getContentComponent(); + new ComponentDriver(robot).focusAndWaitForFocusGain(contentComponent); + assertSame(contentComponent, FocusManager.getCurrentManager().getFocusOwner()); + return contentComponent; + } else { + fail("Expected to find editor to focus, but there is no current editor"); + return null; + } + } + + /** + * Moves the caret to the start of the given line number (0-based). + * + * @param lineNumber the line number. + */ + @NotNull + public EditorFixture moveToLine(final int lineNumber) { + assertThat(lineNumber).isGreaterThanOrEqualTo(0); + execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + // TODO: Do this via mouse clicks! + FileEditorManager manager = FileEditorManager.getInstance(myFrame.getProject()); + Editor editor = manager.getSelectedTextEditor(); + if (editor != null) { + Document document = editor.getDocument(); + int offset = document.getLineStartOffset(lineNumber); + editor.getCaretModel().moveToOffset(offset); + editor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + } + }); + return this; + } + + /** + * Moves the caret to the given caret offset (0-based). + * + * @param offset the character offset. + */ + public EditorFixture moveTo(final int offset) { + assertThat(offset).isGreaterThanOrEqualTo(0); + execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + // TODO: Do this via mouse clicks! + FileEditorManager manager = FileEditorManager.getInstance(myFrame.getProject()); + Editor editor = manager.getSelectedTextEditor(); + if (editor != null) { + editor.getCaretModel().moveToOffset(offset); + editor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + } + }); + + return this; + } + + /** + * Selects the given range. If the first and second offsets are the same, it simply + * moves the caret to the given position. The caret is always placed at the second offset, + * which is allowed to be smaller than the first offset. Calling {@code select(10, 7)} + * would be the same as dragging the mouse from offset 10 to offset 7 and releasing the mouse + * button; the caret is now at the beginning of the selection. + * + * @param firstOffset the character offset where we start the selection, or -1 to remove the selection + * @param secondOffset the character offset where we end the selection, which can be an earlier + * offset than the firstOffset + */ + public EditorFixture select(final int firstOffset, final int secondOffset) { + execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + // TODO: Do this via mouse drags! + FileEditorManager manager = FileEditorManager.getInstance(myFrame.getProject()); + Editor editor = manager.getSelectedTextEditor(); + if (editor != null) { + editor.getCaretModel().getPrimaryCaret().setSelection(firstOffset, secondOffset); + editor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE); + } + } + }); + + return this; + } + + /** + * Finds the next position (or if {@code searchFromTop} is true, from the beginning) of + * the given string indicated by a prefix and a suffix. The offset returned will be the position exactly + * in the middle of the two. For example, if you have the text "The quick brown fox jumps over the lazy dog" + * and you search via {@code moveTo("The qui", "ck brown", true)} the returned offset will be at the 7th + * position in the string, between the "i" and "c". + *

+ * Note that on Windows, any {@code \r}'s are hidden from the editor, so they never count in offset + * computations. + * + * @param prefix the target prefix which must immediately precede the returned position + * @param suffix the target string, which must immediately follow the given prefix + * @param searchFromTop if true, search from the beginning of the file instead of from the current editor position + * @return the 0-based offset in the document, or -1 if not found. + */ + public int findOffset(@Nullable final String prefix, @Nullable final String suffix, final boolean searchFromTop) { + assertTrue(prefix != null || suffix != null); + //noinspection ConstantConditions + return execute(new GuiQuery() { + @Override + @Nullable + protected Integer executeInEDT() throws Throwable { + FileEditorManager manager = FileEditorManager.getInstance(myFrame.getProject()); + Editor editor = manager.getSelectedTextEditor(); + if (editor != null) { + CaretModel caretModel = editor.getCaretModel(); + Caret primaryCaret = caretModel.getPrimaryCaret(); + Document document = editor.getDocument(); + String contents = document.getCharsSequence().toString(); + String target = (prefix != null ? prefix : "") + (suffix != null ? suffix : ""); + int targetIndex = contents.indexOf(target, searchFromTop ? 0 : primaryCaret.getOffset()); + return targetIndex != -1 ? targetIndex + (prefix != null ? prefix.length() : 0) : -1; + } + return -1; + } + }); + } + + /** + * Finds the first position in the editor document indicated by the given text segment, where ^ (or if not defined, |) indicates + * the caret position. + * + * @param line the line segment to search for (with ^ or | indicating the caret position) + * @return the 0-based offset in the document, or -1 if not found. + */ + public int findOffset(@NotNull final String line) { + int index = line.indexOf('^'); + if (index == -1) { + // Also look for |. ^ has higher precedence since in many Android XML files we'll have | appearing as + // the XML value flag delimiter. + index = line.indexOf('|'); + } + assertTrue("The text segment should contain a caret position indicated by ^ or |", index != -1); + String prefix = line.substring(0, index); + if (prefix.isEmpty()) { + prefix = null; + } + String suffix = line.substring(index + 1); + if (suffix.isEmpty()) { + suffix = null; + } + assertTrue("The text segment should have more text than just the caret position", prefix != null || suffix != null); + return findOffset(prefix, suffix, true); + } + + /** + * Closes the current editor + */ + public EditorFixture close() { + execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + VirtualFile currentFile = getCurrentFile(); + if (currentFile != null) { + FileEditorManager manager = FileEditorManager.getInstance(myFrame.getProject()); + manager.closeFile(currentFile); + } + } + }); + return this; + } + + /** + * Selects the given tab in the current editor. Used to switch between + * design mode and editor mode for example. + * + * @param tab the tab to switch to + */ + public EditorFixture selectEditorTab(@NotNull final Tab tab) { + switch (tab) { + case EDITOR: + selectEditorTab("Text"); + break; + case DESIGN: + selectEditorTab("Design"); + break; + case DEFAULT: + selectEditorTab((String)null); + break; + default: + fail("Unknown tab " + tab); + } + return this; + } + + /** + * Selects the given tab in the current editor. Used to switch between + * design mode and editor mode for example. + * + * @param tabName the label in the editor, or null for the default (first) tab + */ + public EditorFixture selectEditorTab(@Nullable final String tabName) { + execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + VirtualFile currentFile = getCurrentFile(); + assertNotNull("Can't switch to tab " + tabName + " when no file is open in the editor", currentFile); + FileEditorManager manager = FileEditorManager.getInstance(myFrame.getProject()); + FileEditor[] editors = manager.getAllEditors(currentFile); + FileEditor target = null; + for (FileEditor editor : editors) { + if (tabName == null || tabName.equals(editor.getName())) { + target = editor; + break; + } + } + if (target != null) { + // Have to use reflection + //FileEditorManagerImpl#setSelectedEditor(final FileEditor editor) + method("setSelectedEditor").withParameterTypes(FileEditor.class).in(manager).invoke(target); + return; + } + List tabNames = Lists.newArrayList(); + for (FileEditor editor : editors) { + tabNames.add(editor.getName()); + } + fail("Could not find editor tab \"" + (tabName != null ? tabName : "") + "\": Available tabs = " + tabNames); + } + }); + return this; + } + + /** + * Opens up a different file. This will run through the "Open File..." dialog to + * find and select the given file. + * + * @param file the file to open + * @param tab which tab to open initially, if there are multiple editors + */ + public EditorFixture open(@NotNull final VirtualFile file, @NotNull final Tab tab) { + execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + // TODO: Use UI to navigate to the file instead + Project project = myFrame.getProject(); + FileEditorManager manager = FileEditorManager.getInstance(project); + if (tab == Tab.EDITOR) { + manager.openTextEditor(new OpenFileDescriptor(project, file), true); + } + else { + manager.openFile(file, true); + } + } + }); + + pause(new Condition("File " + quote(file.getPath()) + " to be opened") { + @Override + public boolean test() { + //noinspection ConstantConditions + return execute(new GuiQuery() { + @Override + protected Boolean executeInEDT() throws Throwable { + return file.equals(getCurrentFile()); + } + }); + } + }, SHORT_TIMEOUT); + + // TODO: Maybe find a better way to keep Documents in sync with their VirtualFiles. + invokeActionViaKeystroke("Synchronize"); + + return this; + } + + /** + * Opens up a different file. This will run through the "Open File..." dialog to + * find and select the given file. + * + * @param file the project-relative path (with /, not File.separator, as the path separator) + * @param tab which tab to open initially, if there are multiple editors + */ + public EditorFixture open(@NotNull final String relativePath, @NotNull Tab tab) { + assertFalse("Should use '/' in test relative paths, not File.separator", relativePath.contains("\\")); + VirtualFile file = myFrame.findFileByRelativePath(relativePath, true); + return open(file, tab); + } + + /** + * Like {@link #open(String, com.android.tools.idea.tests.gui.framework.fixture.EditorFixture.Tab)} but + * always uses the default tab + * + * @param file the project-relative path (with /, not File.separator, as the path separator) + */ + public EditorFixture open(@NotNull final String relativePath) { + return open(relativePath, Tab.DEFAULT); + } + + /** + * Invokes the given action. This will look up the corresponding action's key bindings, if any, and invoke + * it. It will fail if the action is not enabled, or if it is interactive. + * + * @param action the action to invoke + */ + public EditorFixture invokeAction(@NotNull EditorAction action) { + switch (action) { + case BACK_SPACE: + invokeActionViaKeystroke("EditorBackSpace"); + break; + case UNDO: + invokeActionViaKeystroke("$Undo"); + break; + case REDO: + invokeActionViaKeystroke("$Redo"); + break; + case CUT: + invokeActionViaKeystroke("$Cut"); + break; + case COPY: + invokeActionViaKeystroke("$Copy"); + break; + case PASTE: + invokeActionViaKeystroke("$Paste"); + break; + case SELECT_ALL: + invokeActionViaKeystroke("$SelectAll"); + break; + case FORMAT: { + // To format without showing dialog: + // invokeActionViaKeystroke("ReformatCode"); + // However, before we replace this, make sure the dialog isn't shown in some scenarios (e.g. first users) + invokeActionViaKeystroke("ShowReformatFileDialog"); + JDialog dialog = robot.finder().find(new GenericTypeMatcher(JDialog.class) { + @Override + protected boolean isMatching(@NotNull JDialog dialog) { + return dialog.isShowing() && dialog.getTitle().contains("Reformat"); + } + }); + DialogFixture dialogFixture = new DialogFixture(robot, dialog); + + // Find and click the Run button. We can't just invoke + // dialogFixture.button("Run").click(); + // because that searches by button name (which is null for the Run button), not the button *title*. + dialogFixture.button(new GenericTypeMatcher(JButton.class) { + @Override + protected boolean isMatching(@NotNull JButton component) { + return component.getText().equals("Run"); + } + }).click(); + + break; + } + case GOTO_DECLARATION: + invokeActionViaKeystroke("GotoDeclaration"); + break; + case COMPLETE_CURRENT_STATEMENT: + invokeActionViaKeystroke("EditorCompleteStatement"); + break; + case SAVE: + invokeActionViaKeystroke("SaveAll"); + break; + case TOGGLE_COMMENT: + invokeActionViaKeystroke("CommentByLineComment"); + break; + case DUPLICATE_LINES: + invokeActionViaKeystroke("EditorDuplicate"); + break; + case DELETE_LINE: + invokeActionViaKeystroke("EditorDeleteLine"); + break; + case NEXT_METHOD: + invokeActionViaKeystroke("MethodDown"); + break; + case PREVIOUS_METHOD: + invokeActionViaKeystroke("MethodUp"); + break; + case NEXT_ERROR: + invokeActionViaKeystroke("GotoNextError"); + break; + case PREVIOUS_ERROR: + invokeActionViaKeystroke("GotoPreviousError"); + break; + case JOIN_LINES: + invokeActionViaKeystroke("EditorJoinLines"); + break; + case SHOW_INTENTION_ACTIONS: + invokeActionViaKeystroke("ShowIntentionActions"); + break; + case RUN_FROM_CONTEXT: + invokeActionViaKeystroke("RunClass"); + break; + case EXTEND_SELECTION: + case SHRINK_SELECTION: + // Need to find the right action id's for these; didn't see them in the default keymap + default: + fail("Not yet implemented"); + break; + } + return this; + } + + private void invokeActionViaKeystroke(@NotNull String actionId) { + AnAction action = ActionManager.getInstance().getAction(actionId); + assertNotNull(actionId, action); + assertTrue(actionId + " is not enabled", action.getTemplatePresentation().isEnabled()); + + Keymap keymap = KeymapManager.getInstance().getActiveKeymap(); + Shortcut[] shortcuts = keymap.getShortcuts(actionId); + assertNotNull(shortcuts); + assertThat(shortcuts).isNotEmpty(); + Shortcut shortcut = shortcuts[0]; + if (shortcut instanceof KeyboardShortcut) { + KeyboardShortcut cs = (KeyboardShortcut)shortcut; + KeyStroke firstKeyStroke = cs.getFirstKeyStroke(); + Component component = getFocusedEditor(); + if (component != null) { + ComponentDriver driver = new ComponentDriver(robot); + System.out.println("Invoking editor action " + actionId + " via shortcut " + + KeyEvent.getKeyModifiersText(firstKeyStroke.getModifiers()) + + KeyEvent.getKeyText(firstKeyStroke.getKeyCode())); + driver.pressAndReleaseKey(component, firstKeyStroke.getKeyCode(), new int[]{firstKeyStroke.getModifiers()}); + KeyStroke secondKeyStroke = cs.getSecondKeyStroke(); + if (secondKeyStroke != null) { + System.out.println(" and " + + KeyEvent.getKeyModifiersText(secondKeyStroke.getModifiers()) + + KeyEvent.getKeyText(secondKeyStroke.getKeyCode())); + driver.pressAndReleaseKey(component, secondKeyStroke.getKeyCode(), new int[]{secondKeyStroke.getModifiers()}); + } + } else { + fail("Editor not focused for action"); + } + } + else { + fail("Unsupported shortcut type " + shortcut.getClass().getName()); + } + } + + /** + * Checks that the editor has a given number of issues. This is a convenience wrapper + * for {@link FileFixture#requireCodeAnalysisHighlightCount(HighlightSeverity, int)} + * + * @param severity the severity of the issues you want to count + * @param expected the expected count + * @return this + */ + @NotNull + public EditorFixture requireCodeAnalysisHighlightCount(@NotNull HighlightSeverity severity, int expected) { + FileFixture file = getCurrentFileFixture(); + file.requireCodeAnalysisHighlightCount(severity, expected); + return this; + } + + @NotNull + public EditorFixture requireHighlights(HighlightSeverity severity, String... highlights) { + List infos = Lists.newArrayList(); + for (HighlightInfo info : getCurrentFileFixture().getHighlightInfos(severity)) { + infos.add(info.getDescription()); + } + assertThat(infos).containsOnly(highlights); + return this; + } + + /** + * Waits until the editor has the given number of errors at the given severity. + * Typically used when you want to invoke an intention action, but need to wait until + * the code analyzer has found an error it needs to resolve first. + * + * @param severity the severity of the issues you want to count + * @param expected the expected count + * @return this + */ + @NotNull + public EditorFixture waitForCodeAnalysisHighlightCount(@NotNull final HighlightSeverity severity, int expected) { + FileFixture file = getCurrentFileFixture(); + file.waitForCodeAnalysisHighlightCount(severity, expected); + return this; + } + + @NotNull + public EditorFixture waitUntilErrorAnalysisFinishes() { + FileFixture file = getCurrentFileFixture(); + file.waitUntilErrorAnalysisFinishes(); + return this; + } + + @NotNull + private FileFixture getCurrentFileFixture() { + VirtualFile currentFile = getCurrentFile(); + assertNotNull("Expected a file to be open", currentFile); + return new FileFixture(myFrame.getProject(), currentFile); + } + + /** + * Invokes the show intentions action, waits for the actions to be displayed and then picks the + * one with the given label prefix + * + * @param labelPrefix the prefix of the action description to be shown + * @return this + */ + @NotNull + public EditorFixture invokeIntentionAction(@NotNull String labelPrefix) { + invokeAction(EditorFixture.EditorAction.SHOW_INTENTION_ACTIONS); + JBList popup = waitForPopup(robot); + clickPopupMenuItem(labelPrefix, popup, robot); + return this; + } + + /** + * Returns a fixture around the layout editor, if the currently edited file + * is a layout file and it is currently showing the layout editor tab or the parameter + * requests that it be opened if necessary + * + * @param switchToTabIfNecessary if true, switch to the design tab if it is not already showing + * @return a layout editor fixture, or null if the current file is not a layout file or the + * wrong tab is showing + */ + //@Nullable + //public LayoutEditorFixture getLayoutEditor(boolean switchToTabIfNecessary) { + // VirtualFile currentFile = getCurrentFile(); + // if (ResourceHelper.getFolderType(currentFile) != ResourceFolderType.LAYOUT) { + // return null; + // } + // + // if (switchToTabIfNecessary) { + // selectEditorTab(Tab.DESIGN); + // } + // + // return execute(new GuiQuery() { + // @Override + // @Nullable + // protected LayoutEditorFixture executeInEDT() throws Throwable { + // FileEditorManager manager = FileEditorManager.getInstance(myFrame.getProject()); + // FileEditor[] editors = manager.getSelectedEditors(); + // if (editors.length == 0) { + // return null; + // } + // FileEditor selected = editors[0]; + // if (!(selected instanceof AndroidDesignerEditor)) { + // return null; + // } + // + // return new LayoutEditorFixture(robot, (AndroidDesignerEditor)selected); + // } + // }); + //} + + /** + * Returns a fixture around the layout preview window, if the currently edited file + * is a layout file and it the XML editor tab of the layout is currently showing. + * + * @param switchToTabIfNecessary if true, switch to the editor tab if it is not already showing + * @return a layout preview fixture, or null if the current file is not a layout file or the + * wrong tab is showing + */ + //@Nullable + //public LayoutPreviewFixture getLayoutPreview(boolean switchToTabIfNecessary) { + // VirtualFile currentFile = getCurrentFile(); + // if (ResourceHelper.getFolderType(currentFile) != ResourceFolderType.LAYOUT) { + // return null; + // } + // + // if (switchToTabIfNecessary) { + // selectEditorTab(Tab.EDITOR); + // } + // + // Boolean visible = GuiActionRunner.execute(new GuiQuery() { + // @Override + // protected Boolean executeInEDT() throws Throwable { + // AndroidLayoutPreviewToolWindowManager manager = AndroidLayoutPreviewToolWindowManager.getInstance(myFrame.getProject()); + // return manager.getToolWindowForm() != null; + // } + // }); + // if (visible == null || !visible) { + // myFrame.invokeMenuPath("View", "Tool Windows", "Preview"); + // } + // + // pause(new Condition("Preview window is visible") { + // @Override + // public boolean test() { + // AndroidLayoutPreviewToolWindowManager manager = AndroidLayoutPreviewToolWindowManager.getInstance(myFrame.getProject()); + // return manager.getToolWindowForm() != null; + // } + // }, SHORT_TIMEOUT); + // + // return new LayoutPreviewFixture(robot, myFrame.getProject()); + //} + + + /** + * Returns a fixture around the {@link com.android.tools.idea.editors.theme.ThemeEditor} if the currently + * displayed editor is a theme editor. + */ + //@NotNull + //public ThemeEditorFixture getThemeEditor() { + // final ThemeEditorComponent themeEditorComponent = + // GuiTests.waitUntilFound(robot, new GenericTypeMatcher(ThemeEditorComponent.class) { + // @Override + // protected boolean isMatching(@NotNull ThemeEditorComponent component) { + // return true; + // } + // }); + // + // return new ThemeEditorFixture(robot, themeEditorComponent); + //} + + /** + * Requires the source editor's current file name to be the given name (or if null, for there + * to be no current file) + */ + public void requireName(@Nullable String name) { + VirtualFile currentFile = getCurrentFile(); + if (name == null) { + assertNull("Expected editor to not have an open file, but is showing " + currentFile, currentFile); + } else if (currentFile == null) { + fail("Expected file " + name + " to be showing, but the editor is not showing anything"); + } else { + assertEquals(name, currentFile.getName()); + } + } + + /** + * Requires the source editor's current file to be in the given folder (or if null, for there + * to be no current file) + */ + public void requireFolderName(@Nullable String name) { + VirtualFile currentFile = getCurrentFile(); + if (name == null) { + assertNull("Expected editor to not have an open file, but is showing " + currentFile, currentFile); + } else if (currentFile == null) { + fail("Expected file " + name + " to be showing, but the editor is not showing anything"); + } else { + VirtualFile parent = currentFile.getParent(); + assertNotNull("File " + currentFile.getName() + " does not have a parent", parent); + assertEquals(name, parent.getName()); + } + } + + + /** + * Common editor actions, invokable via {@link #invokeAction(EditorAction)} + */ + public enum EditorAction { + SHOW_INTENTION_ACTIONS, + FORMAT, + SAVE, + UNDO, + REDO, + COPY, + PASTE, + CUT, + BACK_SPACE, + COMPLETE_CURRENT_STATEMENT, + EXTEND_SELECTION, + SHRINK_SELECTION, + SELECT_ALL, + JOIN_LINES, + DUPLICATE_LINES, + DELETE_LINE, + TOGGLE_COMMENT, + GOTO_DECLARATION, + NEXT_ERROR, + PREVIOUS_ERROR, + NEXT_METHOD, + PREVIOUS_METHOD, + RUN_FROM_CONTEXT + } + + /** + * The different tabs of an editor; used by for example {@link #open(VirtualFile, EditorFixture.Tab)} to indicate which + * tab should be opened + */ + public enum Tab { EDITOR, DESIGN, DEFAULT } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/EditorNotificationPanelFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/EditorNotificationPanelFixture.java new file mode 100644 index 000000000000..d37ad4de55b8 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/EditorNotificationPanelFixture.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.intellij.ui.EditorNotificationPanel; +import com.intellij.ui.HyperlinkLabel; +import org.fest.reflect.core.Reflection; +import org.fest.swing.core.GenericTypeMatcher; +import org.fest.swing.core.Robot; +import org.jetbrains.annotations.NotNull; + +public class EditorNotificationPanelFixture extends JComponentFixture { + public EditorNotificationPanelFixture(@NotNull Robot robot, @NotNull EditorNotificationPanel target) { + super(EditorNotificationPanelFixture.class, robot, target); + } + + public void performAction(@NotNull final String label) { + HyperlinkLabel link = robot().finder().find(target(), new GenericTypeMatcher(HyperlinkLabel.class) { + @Override + protected boolean isMatching(@NotNull HyperlinkLabel hyperlinkLabel) { + // IntelliJ's HyperLinkLabel class does not expose the getText method (it is package private) + return hyperlinkLabel.isShowing() && + label.equals(Reflection.method("getText").withReturnType(String.class).in(hyperlinkLabel).invoke()); + } + }); + driver().click(link); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/ExecutionToolWindowFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/ExecutionToolWindowFixture.java new file mode 100644 index 000000000000..b648f98e6e9f --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/ExecutionToolWindowFixture.java @@ -0,0 +1,254 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.intellij.execution.impl.ConsoleViewImpl; +import com.intellij.execution.testframework.TestTreeView; +import com.intellij.execution.ui.layout.impl.GridImpl; +import com.intellij.execution.ui.layout.impl.JBRunnerTabs; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.impl.ActionButton; +import com.intellij.openapi.actionSystem.impl.ActionToolbarImpl; +import com.intellij.openapi.ui.ThreeComponentsSplitter; +import com.intellij.ui.content.Content; +import com.intellij.ui.tabs.impl.JBTabsImpl; +import com.intellij.ui.tabs.impl.TabLabel; +import com.intellij.xdebugger.impl.ui.tree.XDebuggerTree; +import com.intellij.xdebugger.impl.ui.tree.nodes.XDebuggerTreeNode; +import org.fest.swing.core.GenericTypeMatcher; +import org.fest.swing.core.Robot; +import org.fest.swing.edt.GuiActionRunner; +import org.fest.swing.edt.GuiTask; +import org.fest.swing.exception.ComponentLookupException; +import org.fest.swing.timing.Condition; +import org.fest.swing.timing.Pause; +import org.fest.swing.timing.Timeout; +import org.fest.swing.util.TextMatcher; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; + +import javax.swing.*; +import java.util.List; + +import static com.intellij.tests.gui.framework.GuiTests.waitUntilFound; +import static com.intellij.util.ui.UIUtil.findComponentOfType; +import static com.intellij.util.ui.UIUtil.findComponentsOfType; +import static junit.framework.Assert.assertNotNull; +import static org.fest.reflect.core.Reflection.method; +import static org.fest.swing.timing.Pause.pause; + +public class ExecutionToolWindowFixture extends ToolWindowFixture { + public static class ContentFixture { + @NotNull private final ExecutionToolWindowFixture myParentToolWindow; + @NotNull private final Robot myRobot; + @NotNull private final Content myContent; + + private ContentFixture(@NotNull ExecutionToolWindowFixture parentToolWindow, @NotNull Robot robot, @NotNull Content content) { + myParentToolWindow = parentToolWindow; + myRobot = robot; + myContent = content; + } + + public void waitForOutput(@NotNull final TextMatcher matcher, @NotNull Timeout timeout) { + pause(new Condition("LogCat tool window output check for package name.") { + @Override + public boolean test() { + return outputMatches(matcher); + } + }, timeout); + } + + /** + * Waits until it grabs the console window and then returns true if its text matches that of {@code matcher}. + * Note: The caller should wrap it in something like a org.fest.swing.timing.Pause.pause to make sure they don't hang forever if the + * console view cannot be found for some reason. + */ + public boolean outputMatches(@NotNull TextMatcher matcher) { + ConsoleViewImpl consoleView; + while ((consoleView = findConsoleView()) == null || consoleView.getEditor() == null) { + // If our handle has been replaced, find it again. + JComponent consoleComponent = getTabComponent("Console"); + myRobot.click(consoleComponent); + } + return matcher.isMatching(consoleView.getEditor().getDocument().getText()); + } + + // Returns the console or null if it is not found. + @Nullable + private ConsoleViewImpl findConsoleView() { + try { + return myRobot.finder().findByType(myParentToolWindow.myToolWindow.getComponent(), ConsoleViewImpl.class, false); + } catch (ComponentLookupException e) { + return null; + } + } + + @NotNull + public JComponent getTabComponent(@NotNull final String tabName) { + return getTabContent(myParentToolWindow.myToolWindow.getComponent(), JBRunnerTabs.class, GridImpl.class, tabName); + } + + @NotNull + public UnitTestTreeFixture getUnitTestTree() { + return new UnitTestTreeFixture(this, myRobot.finder().findByType(myContent.getComponent(), TestTreeView.class)); + } + + // Returns the root of the debugger tree or null. + @Nullable + public XDebuggerTreeNode getDebuggerTreeRoot() { + try { + JComponent debuggerComponent = getTabComponent("Debugger"); + if (debuggerComponent != null) { + myRobot.click(debuggerComponent); + } + ThreeComponentsSplitter threeComponentsSplitter = + myRobot.finder().findByType(debuggerComponent, ThreeComponentsSplitter.class, false); + JComponent innerComponent = threeComponentsSplitter.getInnerComponent(); + assertNotNull(innerComponent); + return myRobot.finder().findByType(innerComponent, XDebuggerTree.class, false).getRoot(); + } catch (ComponentLookupException e) { + return null; + } + } + + public void clickDebuggerTreeRoot() { + try { + JComponent debuggerComponent = getTabComponent("Debugger"); + myRobot.click(debuggerComponent); + } catch (ComponentLookupException e) { } + } + + @NotNull + private JComponent getTabContent(@NotNull final JComponent root, + final Class parentComponentType, + @NotNull final Class tabContentType, + @NotNull final String tabName) { + myParentToolWindow.activate(); + myParentToolWindow.waitUntilIsVisible(); + + TabLabel tabLabel; + if (parentComponentType == null) { + tabLabel = waitUntilFound(myRobot, new GenericTypeMatcher(TabLabel.class) { + @Override + protected boolean isMatching(@NotNull TabLabel component) { + return component.toString().equals(tabName); + } + }); + } + else { + final JComponent parent = myRobot.finder().findByType(root, parentComponentType, false); + tabLabel = waitUntilFound(myRobot, parent, new GenericTypeMatcher(TabLabel.class) { + @Override + protected boolean isMatching(@NotNull TabLabel component) { + return component.getParent() == parent && component.toString().equals(tabName); + } + }); + } + myRobot.click(tabLabel); + return myRobot.finder().findByType(tabContentType); + } + + public boolean isExecutionInProgress() { + // Consider that execution is in progress if 'stop' toolbar button is enabled. + for (ActionButton button : getToolbarButtons()) { + if ("com.intellij.execution.actions.StopAction".equals(button.getAction().getClass().getCanonicalName())) { + //noinspection ConstantConditions + return method("isButtonEnabled").withReturnType(boolean.class).in(button).invoke(); + } + } + return true; + } + + public void rerun() { + for (ActionButton button : getToolbarButtons()) { + if ("com.intellij.execution.runners.FakeRerunAction".equals(button.getAction().getClass().getCanonicalName())) { + myRobot.click(button); + return; + } + } + + throw new IllegalStateException("Could not find the Re-run button."); + } + + public void rerunFailed() { + for (ActionButton button : getToolbarButtons()) { + if ("com.intellij.execution.junit2.ui.actions.RerunFailedTestsAction".equals(button.getAction().getClass().getCanonicalName())) { + myRobot.click(button); + return; + } + } + + throw new IllegalStateException("Could not find the Re-run failed tests button."); + } + + public void waitForExecutionToFinish(@NotNull Timeout timeout) { + Pause.pause(new Condition("Wait for execution to finish") { + @Override + public boolean test() { + return !isExecutionInProgress(); + } + }, timeout); + } + + @TestOnly + public boolean stop() { + for (final ActionButton button : getToolbarButtons()) { + final AnAction action = button.getAction(); + if (action != null && action.getClass().getName().equals("com.intellij.execution.actions.StopAction")) { + //noinspection ConstantConditions + boolean enabled = method("isButtonEnabled").withReturnType(boolean.class).in(button).invoke(); + if (enabled) { + GuiActionRunner.execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + button.click(); + } + }); + return true; + } + return false; + } + } + return false; + } + + @NotNull + private List getToolbarButtons() { + ActionToolbarImpl toolbar = findComponentOfType(myContent.getComponent(), ActionToolbarImpl.class); + assert toolbar != null; + return findComponentsOfType(toolbar, ActionButton.class); + } + } // End class ContentFixture + + protected ExecutionToolWindowFixture(@NotNull String toolWindowId, @NotNull IdeFrameFixture ideFrame) { + super(toolWindowId, ideFrame.getProject(), ideFrame.robot()); + } + + @NotNull + public ContentFixture findContent(@NotNull String tabName) { + Content content = getContent(tabName); + assertNotNull(content); + return new ContentFixture(this, myRobot, content); + } + + @NotNull + public ContentFixture findContent(@NotNull TextMatcher tabNameMatcher) { + Content content = getContent(tabNameMatcher); + assertNotNull(content); + return new ContentFixture(this, myRobot, content); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/FileChooserDialogFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/FileChooserDialogFixture.java new file mode 100644 index 000000000000..7065037dc9e7 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/FileChooserDialogFixture.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.intellij.openapi.fileChooser.ex.FileChooserDialogImpl; +import com.intellij.openapi.fileChooser.ex.FileSystemTreeImpl; +import com.intellij.openapi.vfs.VirtualFile; +import org.fest.swing.core.GenericTypeMatcher; +import org.fest.swing.core.Robot; +import org.fest.swing.edt.GuiTask; +import org.fest.swing.timing.Condition; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.intellij.tests.gui.framework.GuiTests.SHORT_TIMEOUT; +import static com.intellij.tests.gui.framework.GuiTests.findAndClickOkButton; +import static org.fest.reflect.core.Reflection.field; +import static org.fest.swing.edt.GuiActionRunner.execute; +import static org.fest.swing.timing.Pause.pause; +import static org.fest.util.Strings.quote; +import static org.junit.Assert.assertNotNull; + +public class FileChooserDialogFixture extends IdeaDialogFixture { + @NotNull + public static FileChooserDialogFixture findOpenProjectDialog(@NotNull Robot robot) { + return findDialog(robot, new GenericTypeMatcher(JDialog.class) { + @Override + protected boolean isMatching(@NotNull JDialog dialog) { + return dialog.isShowing() && "Open File or Project".equals(dialog.getTitle()); + } + }); + } + + @NotNull + public static FileChooserDialogFixture findImportProjectDialog(@NotNull Robot robot) { + return findDialog(robot, new GenericTypeMatcher(JDialog.class) { + @Override + protected boolean isMatching(@NotNull JDialog dialog) { + String title = dialog.getTitle(); + return dialog.isShowing() && title != null && title.startsWith("Select") && title.endsWith("Project to Import"); + } + }); + } + + @NotNull + public static FileChooserDialogFixture findDialog(@NotNull Robot robot, @NotNull final GenericTypeMatcher matcher) { + return new FileChooserDialogFixture(robot, find(robot, FileChooserDialogImpl.class, matcher)); + } + + private FileChooserDialogFixture(@NotNull Robot robot, @NotNull DialogAndWrapper dialogAndWrapper) { + super(robot, dialogAndWrapper); + } + + @NotNull + public FileChooserDialogFixture select(@NotNull final VirtualFile file) { + sleepWithTimeBomb(); + final FileSystemTreeImpl fileSystemTree = field("myFileSystemTree").ofType(FileSystemTreeImpl.class) + .in(getDialogWrapper()) + .get(); + assertNotNull(fileSystemTree); + final AtomicBoolean fileSelected = new AtomicBoolean(); + execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + fileSystemTree.select(file, new Runnable() { + @Override + public void run() { + fileSelected.set(true); + } + }); + } + }); + + pause(new Condition("File " + quote(file.getPath()) + " is selected") { + @Override + public boolean test() { + return fileSelected.get(); + } + }, SHORT_TIMEOUT); + + return this; + } + + private void sleepWithTimeBomb() { + //TODO: WTF?!! + assert System.currentTimeMillis() < 1452600000000L; // 2016-01-12 12:00 + try { + Thread.sleep(5000); + } + catch (InterruptedException e) {} + } + + @NotNull + public FileChooserDialogFixture clickOk() { + findAndClickOkButton(this); + return this; + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/FileFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/FileFixture.java new file mode 100644 index 000000000000..73b1d278ac78 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/FileFixture.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.intellij.codeInsight.daemon.impl.DaemonCodeAnalyzerEx; +import com.intellij.codeInsight.daemon.impl.HighlightInfo; +import com.intellij.lang.annotation.HighlightSeverity; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.fileEditor.FileEditor; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; +import com.intellij.util.CommonProcessors; +import org.fest.swing.edt.GuiQuery; +import org.fest.swing.timing.Condition; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.io.File; +import java.util.Collection; + +import static com.intellij.openapi.vfs.VfsUtilCore.virtualToIoFile; +import static com.intellij.tests.gui.framework.GuiTests.SHORT_TIMEOUT; +import static junit.framework.Assert.assertNotNull; +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.reflect.core.Reflection.method; +import static org.fest.swing.edt.GuiActionRunner.execute; +import static org.fest.swing.timing.Pause.pause; +import static org.fest.util.Strings.quote; + +public class FileFixture { + @NotNull private final Project myProject; + @NotNull private final File myPath; + @NotNull private final VirtualFile myVirtualFile; + + public FileFixture(@NotNull Project project, @NotNull VirtualFile file) { + myProject = project; + myPath = virtualToIoFile(file); + myVirtualFile = file; + } + + @NotNull + public FileFixture requireOpenAndSelected() { + requireVirtualFile(); + pause(new Condition("File " + quote(myPath.getPath()) + " to be opened") { + @Override + public boolean test() { + //noinspection ConstantConditions + return execute(new GuiQuery() { + @Override + protected Boolean executeInEDT() throws Throwable { + return isOpenAndSelected(); + } + }); + } + }, SHORT_TIMEOUT); + return this; + } + + private boolean isOpenAndSelected() { + FileEditorManager editorManager = FileEditorManager.getInstance(myProject); + FileEditor selectedEditor = editorManager.getSelectedEditor(myVirtualFile); + if (selectedEditor != null) { + JComponent component = selectedEditor.getComponent(); + if (component.isVisible() && component.isShowing()) { + Document document = FileDocumentManager.getInstance().getDocument(myVirtualFile); + if (document != null) { + PsiFile psiFile = PsiDocumentManager.getInstance(myProject).getPsiFile(document); + if (psiFile != null) { + DaemonCodeAnalyzerEx codeAnalyzer = DaemonCodeAnalyzerEx.getInstanceEx(myProject); + //noinspection ConstantConditions + boolean isRunning = method("isRunning").withReturnType(boolean.class).in(codeAnalyzer).invoke(); + return !isRunning; + } + } + } + } + return false; + } + + @NotNull + public FileFixture waitUntilErrorAnalysisFinishes() { + pause(new Condition("error analysis finishes") { + @Override + public boolean test() { + //noinspection ConstantConditions + return execute(new GuiQuery() { + @Override + protected Boolean executeInEDT() throws Throwable { + return DaemonCodeAnalyzerEx.getInstanceEx(myProject).isErrorAnalyzingFinished(getPsiFile()); + } + }); + } + }, SHORT_TIMEOUT); + return this; + } + + @NotNull + public FileFixture requireCodeAnalysisHighlightCount(@NotNull HighlightSeverity severity, int expected) { + Collection highlightInfos = getHighlightInfos(severity); + assertThat(highlightInfos).hasSize(expected); + return this; + } + + @NotNull + public Collection getHighlightInfos(@NotNull final HighlightSeverity severity) { + waitUntilErrorAnalysisFinishes(); + + final Document document = getNotNullDocument(); + Collection highlightInfos = execute(new GuiQuery>() { + @Override + protected Collection executeInEDT() throws Throwable { + CommonProcessors.CollectProcessor processor = new CommonProcessors.CollectProcessor(); + DaemonCodeAnalyzerEx.processHighlights(document, myProject, severity, 0, document.getTextLength(), processor); + return processor.getResults(); + } + }); + assert highlightInfos != null; + return highlightInfos; + } + + @NotNull + private PsiFile getPsiFile() { + final PsiFile psiFile = execute(new GuiQuery() { + @Override + protected PsiFile executeInEDT() throws Throwable { + return PsiManager.getInstance(myProject).findFile(myVirtualFile); + } + }); + assertNotNull("No Psi file found for path " + quote(myVirtualFile.getPath()), psiFile); + return psiFile; + } + + @NotNull + public FileFixture waitForCodeAnalysisHighlightCount(@NotNull final HighlightSeverity severity, final int expected) { + final Document document = getNotNullDocument(); + pause(new Condition("Waiting for code analysis " + severity + " count to reach " + expected) { + @Override + public boolean test() { + Collection highlightInfos = execute(new GuiQuery>() { + @Override + protected Collection executeInEDT() throws Throwable { + CommonProcessors.CollectProcessor processor = new CommonProcessors.CollectProcessor(); + DaemonCodeAnalyzerEx.processHighlights(document, myProject, severity, 0, document.getTextLength(), processor); + return processor.getResults(); + } + }); + assertNotNull(highlightInfos); + return highlightInfos.size() == expected; + } + }, SHORT_TIMEOUT); + + return this; + } + + @NotNull + private Document getNotNullDocument() { + Document document = getDocument(myVirtualFile); + assertNotNull("No Document found for path " + quote(myPath.getPath()), document); + return document; + } + + @NotNull + public FileFixture requireVirtualFile() { + assertNotNull("No VirtualFile found for path " + quote(myPath.getPath()), myVirtualFile); + return this; + } + + @Nullable + public static Document getDocument(@NotNull final VirtualFile file) { + return execute(new GuiQuery() { + @Override + protected Document executeInEDT() throws Throwable { + return FileDocumentManager.getInstance().getDocument(file); + } + }); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/FindDialogFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/FindDialogFixture.java new file mode 100644 index 000000000000..93393d128fce --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/FindDialogFixture.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.intellij.find.impl.FindDialog; +import com.intellij.openapi.ui.ComboBox; +import org.fest.swing.core.Robot; +import org.fest.swing.edt.GuiTask; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +import static com.intellij.tests.gui.framework.GuiTests.findAndClickButton; +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.swing.edt.GuiActionRunner.execute; +import static org.junit.Assert.assertNotNull; + +public class FindDialogFixture extends IdeaDialogFixture { + @NotNull + public static FindDialogFixture find(@NotNull Robot robot) { + return new FindDialogFixture(robot, find(robot, FindDialog.class)); + } + + private FindDialogFixture(@NotNull Robot robot, @NotNull DialogAndWrapper dialogAndWrapper) { + super(robot, dialogAndWrapper); + } + + @NotNull + public FindDialogFixture setTextToFind(@NotNull final String text) { + execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + JComponent c = getDialogWrapper().getPreferredFocusedComponent(); + assertThat(c).isInstanceOf(ComboBox.class); + ComboBox input = (ComboBox)c; + assertNotNull(input); + input.setSelectedItem(text); + } + }); + return this; + } + + @NotNull + public FindDialogFixture clickFind() { + findAndClickButton(this, "Find"); + return this; + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/FindToolWindowFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/FindToolWindowFixture.java new file mode 100644 index 000000000000..01c8dc519f61 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/FindToolWindowFixture.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.intellij.ide.OccurenceNavigatorSupport; +import com.intellij.ui.content.Content; +import com.intellij.ui.treeStructure.Tree; +import com.intellij.usageView.UsageViewManager; +import com.intellij.usages.impl.GroupNode; +import org.fest.swing.edt.GuiQuery; +import org.fest.util.Lists; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.util.List; + +import static org.fest.reflect.core.Reflection.field; +import static org.fest.swing.edt.GuiActionRunner.execute; +import static org.junit.Assert.assertNotNull; + +public class FindToolWindowFixture { + @NotNull private final IdeFrameFixture myParent; + + public FindToolWindowFixture(@NotNull IdeFrameFixture parent) { + myParent = parent; + } + + @NotNull + public ContentFixture getSelectedContext() { + return new ContentFixture(myParent); + } + + public static class ContentFixture { + @NotNull private final Content myContent; + + ContentFixture(@NotNull IdeFrameFixture parent) { + UsageViewManager usageViewManager = UsageViewManager.getInstance(parent.getProject()); + myContent = usageViewManager.getSelectedContent(); + assertNotNull(myContent); + } + + + public void findUsagesInGeneratedCodeGroup() { + findUsageGroup("Usages in generated code"); + } + + public void findUsageGroup(@NotNull final String groupText) { + final Tree tree = getContentsTree(); + final List groupNames = Lists.newArrayList(); + GroupNode foundGroup = execute(new GuiQuery() { + @Override + @Nullable + protected GroupNode executeInEDT() throws Throwable { + GroupNode rootNode = (GroupNode)tree.getModel().getRoot(); + GroupNode found = null; + for (GroupNode subGroup : rootNode.getSubGroups()) { + String subGroupText = subGroup.getGroup().getText(null); + groupNames.add(subGroupText); + if (groupText.equals(subGroupText)) { + found = subGroup; + } + } + return found; + } + }); + String msg = String.format("Failed to find usage group '%1$s' in %2$s", groupText, groupNames); + assertNotNull(msg, foundGroup); + } + + @NotNull + private Tree getContentsTree() { + JComponent component = myContent.getComponent(); + OccurenceNavigatorSupport navigatorSupport = field("mySupport").ofType(OccurenceNavigatorSupport.class).in(component).get(); + assertNotNull(navigatorSupport); + JTree tree = field("myTree").ofType(JTree.class).in(navigatorSupport).get(); + assertNotNull(tree); + return (Tree)tree; + } + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/IdeFrameFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/IdeFrameFixture.java new file mode 100644 index 000000000000..26d6410a59bd --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/IdeFrameFixture.java @@ -0,0 +1,868 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.intellij.codeInspection.ui.InspectionTree; +import com.intellij.ide.RecentProjectsManager; +import com.intellij.ide.actions.ShowSettingsUtilImpl; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.externalSystem.model.ExternalSystemException; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleManager; +import com.intellij.openapi.options.ShowSettingsUtil; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.ContentEntry; +import com.intellij.openapi.roots.ModifiableRootModel; +import com.intellij.openapi.roots.ModuleRootManager; +import com.intellij.openapi.roots.SourceFolder; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.util.Ref; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.wm.impl.IdeFrameImpl; +import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeFrame; +import com.intellij.ui.EditorNotificationPanel; +import com.intellij.xdebugger.impl.ui.tree.nodes.XDebuggerTreeNode; +import org.fest.swing.core.GenericTypeMatcher; +import org.fest.swing.core.Robot; +import org.fest.swing.core.matcher.JButtonMatcher; +import org.fest.swing.core.matcher.JLabelMatcher; +import org.fest.swing.edt.GuiQuery; +import org.fest.swing.edt.GuiTask; +import org.fest.swing.timing.Condition; +import org.fest.swing.timing.Timeout; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.jps.model.module.JpsModuleSourceRootType; + +import javax.swing.*; +import javax.swing.tree.TreeNode; +import java.awt.*; +import java.awt.event.KeyEvent; +import java.io.File; +import java.util.Collection; +import java.util.Enumeration; +import java.util.List; +import java.util.Set; + +import static com.intellij.ide.impl.ProjectUtil.closeAndDispose; +import static com.intellij.openapi.util.io.FileUtil.*; +import static com.intellij.openapi.util.text.StringUtil.isNotEmpty; +import static com.intellij.openapi.vfs.VfsUtilCore.urlToPath; +import static com.intellij.tests.gui.framework.GuiTests.*; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.fail; +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.swing.edt.GuiActionRunner.execute; +import static org.fest.swing.timing.Pause.pause; +import static org.fest.util.Strings.quote; +import static org.junit.Assert.*; + +/** + * Created by karashevich on 30/05/16. + */ +public class IdeFrameFixture extends ComponentFixture { + @NotNull private final File myProjectPath; + + private EditorFixture myEditor; + + @NotNull + public static IdeFrameFixture find(@NotNull final Robot robot, @NotNull final File projectPath, @Nullable final String projectName) { + final GenericTypeMatcher matcher = new GenericTypeMatcher(IdeFrameImpl.class) { + @Override + protected boolean isMatching(@NotNull IdeFrameImpl frame) { + Project project = frame.getProject(); + if (project != null && toSystemIndependentName(projectPath.getPath()).equals(project.getBasePath())) { + return projectName == null || projectName.equals(project.getName()); + } + return false; + } + }; + + pause(new Condition("IdeFrame " + quote(projectPath.getPath()) + " to show up") { + @Override + public boolean test() { + Collection frames = robot.finder().findAll(matcher); + return !frames.isEmpty(); + } + }, LONG_TIMEOUT); + + IdeFrameImpl ideFrame = robot.finder().find(matcher); + return new IdeFrameFixture(robot, ideFrame, projectPath); + } + + public IdeFrameFixture(@NotNull Robot robot, @NotNull IdeFrameImpl target, @NotNull File projectPath) { + super(IdeFrameFixture.class, robot, target); + myProjectPath = projectPath; + final Project project = getProject(); + + Disposable disposable = new NoOpDisposable(); + Disposer.register(project, disposable); + + //myGradleProjectEventListener = new GradleProjectEventListener(); + + //GradleSyncState.subscribe(project, myGradleProjectEventListener); + //PostProjectBuildTasksExecutor.subscribe(project, myGradleProjectEventListener); + } + + @NotNull + public File getProjectPath() { + return myProjectPath; + } + + @NotNull + public List getModuleNames() { + List names = Lists.newArrayList(); + for (Module module : getModuleManager().getModules()) { + names.add(module.getName()); + } + return names; + } + + @NotNull + public IdeFrameFixture requireModuleCount(int expected) { + Module[] modules = getModuleManager().getModules(); + assertThat(modules).as("Module count in project " + quote(getProject().getName())).hasSize(expected); + return this; + } + + + @NotNull + public Collection getSourceFolderRelativePaths(@NotNull String moduleName, @NotNull final JpsModuleSourceRootType sourceType) { + final Set paths = Sets.newHashSet(); + + Module module = getModule(moduleName); + final ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(module); + + execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + ModifiableRootModel rootModel = moduleRootManager.getModifiableModel(); + try { + for (ContentEntry contentEntry : rootModel.getContentEntries()) { + for (SourceFolder folder : contentEntry.getSourceFolders()) { + JpsModuleSourceRootType rootType = folder.getRootType(); + if (rootType.equals(sourceType)) { + String path = urlToPath(folder.getUrl()); + String relativePath = getRelativePath(myProjectPath, new File(toSystemDependentName(path))); + paths.add(relativePath); + } + } + } + } + finally { + rootModel.dispose(); + } + } + }); + + return paths; + } + + @NotNull + public Module getModule(@NotNull String name) { + Module module = findModule(name); + assertNotNull("Unable to find module with name " + quote(name), module); + return module; + } + + @Nullable + public Module findModule(@NotNull String name) { + for (Module module : getModuleManager().getModules()) { + if (name.equals(module.getName())) { + return module; + } + } + return null; + } + + + @NotNull + private ModuleManager getModuleManager() { + return ModuleManager.getInstance(getProject()); + } + + @NotNull + public EditorFixture getEditor() { + if (myEditor == null) { + myEditor = new EditorFixture(robot(), this); + } + + return myEditor; + } + + + //@NotNull + //public GradleInvocationResult invokeProjectMake() { + // return invokeProjectMake(null); + //} + + //@NotNull + //public GradleInvocationResult invokeProjectMake(@Nullable Runnable executeAfterInvokingMake) { + // myGradleProjectEventListener.reset(); + // + // final AtomicReference resultRef = new AtomicReference(); + // AndroidProjectBuildNotifications.subscribe(getProject(), new AndroidProjectBuildNotifications.AndroidProjectBuildListener() { + // @Override + // public void buildComplete(@NotNull AndroidProjectBuildNotifications.BuildContext context) { + // if (context instanceof GradleBuildContext) { + // resultRef.set(((GradleBuildContext)context).getBuildResult()); + // } + // } + // }); + // selectProjectMakeAction(); + // + // if (executeAfterInvokingMake != null) { + // executeAfterInvokingMake.run(); + // } + // + // waitForBuildToFinish(COMPILE_JAVA); + // + // GradleInvocationResult result = resultRef.get(); + // assertNotNull(result); + // + // return result; + //} + + @NotNull + public IdeFrameFixture invokeProjectMakeAndSimulateFailure(@NotNull final String failure) { + Runnable failTask = new Runnable() { + @Override + public void run() { + throw new ExternalSystemException(failure); + } + }; + //ApplicationManager.getApplication().putUserData(EXECUTE_BEFORE_PROJECT_BUILD_IN_GUI_TEST_KEY, failTask); + selectProjectMakeAction(); + return this; + } + + + + /** + * Finds the Run button in the IDE interface. + * + * @return ActionButtonFixture for the run button. + */ + @NotNull + public ActionButtonFixture findRunApplicationButton() { + return findActionButtonByActionId("Run"); + } + + public void debugApp(@NotNull String appName) throws ClassNotFoundException { + selectApp(appName); + findActionButtonByActionId("Debug").click(); + } + + public void runApp(@NotNull String appName) throws ClassNotFoundException { + selectApp(appName); + findActionButtonByActionId("Run").click(); + } + + + @NotNull + public RunToolWindowFixture getRunToolWindow() { + return new RunToolWindowFixture(this); + } + + @NotNull + public DebugToolWindowFixture getDebugToolWindow() { + return new DebugToolWindowFixture(this); + } + + protected void selectProjectMakeAction() { + invokeMenuPath("Build", "Make Project"); + } + + /** + * Invokes an action by menu path + * + * @param path the series of menu names, e.g. {@link invokeActionByMenuPath("Build", "Make Project")} + */ + public void invokeMenuPath(@NotNull String... path) { + getMenuFixture().invokeMenuPath(path); + } + + /** + * Invokes an action by menu path (where each segment is a regular expression). This is particularly + * useful when the menu items can change dynamically, such as the labels of Undo actions, Run actions, + * etc. + * + * @param path the series of menu name regular expressions, e.g. {@link invokeActionByMenuPath("Build", "Make( Project)?")} + */ + public void invokeMenuPathRegex(@NotNull String... path) { + getMenuFixture().invokeMenuPathRegex(path); + } + + @NotNull + private MenuFixture getMenuFixture() { + return new MenuFixture(robot(), target()); + } + + //@NotNull + //public IdeFrameFixture waitForBuildToFinish(@NotNull final BuildMode buildMode) { + // final Project project = getProject(); + // if (buildMode == SOURCE_GEN && !GradleProjectBuilder.getInstance(project).isSourceGenerationEnabled()) { + // return this; + // } + // + // pause(new Condition("Build (" + buildMode + ") for project " + quote(project.getName()) + " to finish'") { + // @Override + // public boolean test() { + // if (buildMode == SOURCE_GEN) { + // PostProjectBuildTasksExecutor tasksExecutor = PostProjectBuildTasksExecutor.getInstance(project); + // if (tasksExecutor.getLastBuildTimestamp() > -1) { + // // This will happen when creating a new project. Source generation happens before the IDE frame is found and build listeners + // // are created. It is fairly safe to assume that source generation happened if we have a timestamp for a "last performed build". + // return true; + // } + // } + // return myGradleProjectEventListener.isBuildFinished(buildMode); + // } + // }, LONG_TIMEOUT); + // + // waitForBackgroundTasksToFinish(); + // robot().waitForIdle(); + // + // return this; + //} + + @NotNull + public FileFixture findExistingFileByRelativePath(@NotNull String relativePath) { + VirtualFile file = findFileByRelativePath(relativePath, true); + return new FileFixture(getProject(), file); + } + + @Nullable + @Contract("_, true -> !null") + public VirtualFile findFileByRelativePath(@NotNull String relativePath, boolean requireExists) { + //noinspection Contract + assertFalse("Should use '/' in test relative paths, not File.separator", relativePath.contains("\\")); + Project project = getProject(); + VirtualFile file = project.getBaseDir().findFileByRelativePath(relativePath); + if (requireExists) { + //noinspection Contract + assertNotNull("Unable to find file with relative path " + quote(relativePath), file); + } + return file; + } + + //@NotNull + //public IdeFrameFixture requestProjectSyncAndExpectFailure() { + // requestProjectSync(); + // return waitForGradleProjectSyncToFail(); + //} + + @NotNull + public IdeFrameFixture requestProjectSyncAndSimulateFailure(@NotNull final String failure) { + Runnable failTask = new Runnable() { + @Override + public void run() { + throw new ExternalSystemException(failure); + } + }; + //ApplicationManager.getApplication().putUserData(EXECUTE_BEFORE_PROJECT_SYNC_TASK_IN_GUI_TEST_KEY, failTask); + // When simulating the error, we don't have to wait for sync to happen. Sync never happens because the error is thrown before it (sync) + // is started. + return requestProjectSync(); + } + + @NotNull + public IdeFrameFixture requestProjectSync() { + //myGradleProjectEventListener.reset(); + + // We wait until all "Run Configurations" are populated in the toolbar combo-box. Until then the "Project Sync" button is not in its + // final position, and FEST will click the wrong button. + pause(new Condition("Waiting for 'Run Configurations' to be populated") { + @Override + public boolean test() { + RunConfigurationComboBoxFixture runConfigurationComboBox = RunConfigurationComboBoxFixture.find(IdeFrameFixture.this); + return isNotEmpty(runConfigurationComboBox.getText()); + } + }, SHORT_TIMEOUT); + + waitForBackgroundTasksToFinish(); + findGradleSyncAction().waitUntilEnabledAndShowing(); + // TODO figure out why in IDEA 15 even though an action is enabled, visible and showing, clicking it (via UI testing infrastructure) + // does not work consistently + + return this; + } + + @NotNull + private ActionButtonFixture findGradleSyncAction() { + return findActionButtonByActionId("Android.SyncProject"); + } + + //@NotNull + //public IdeFrameFixture waitForGradleProjectSyncToFail() { + // try { + // waitForGradleProjectSyncToFinish(true); + // fail("Expecting project sync to fail"); + // } + // catch (RuntimeException expected) { + // // expected failure. + // } + // return waitForBackgroundTasksToFinish(); + //} + + //@NotNull + //public IdeFrameFixture waitForGradleProjectSyncToStart() { + // Project project = getProject(); + // final GradleSyncState syncState = GradleSyncState.getInstance(project); + // if (!syncState.isSyncInProgress()) { + // pause(new Condition("Syncing project " + quote(project.getName()) + " to finish") { + // @Override + // public boolean test() { + // return myGradleProjectEventListener.isSyncStarted(); + // } + // }, SHORT_TIMEOUT); + // } + // return this; + //} + + //@NotNull + //public IdeFrameFixture waitForGradleProjectSyncToFinish() { + // waitForGradleProjectSyncToFinish(false); + // return this; + //} + + //private void waitForGradleProjectSyncToFinish(final boolean expectSyncFailure) { + // final Project project = getProject(); + // + // // ensure GradleInvoker (in-process build) is always enabled. + // AndroidGradleBuildConfiguration buildConfiguration = AndroidGradleBuildConfiguration.getInstance(project); + // buildConfiguration.USE_EXPERIMENTAL_FASTER_BUILD = true; + // + // pause(new Condition("Syncing project " + quote(project.getName()) + " to finish") { + // @Override + // public boolean test() { + // GradleSyncState syncState = GradleSyncState.getInstance(project); + // boolean syncFinished = + // (myGradleProjectEventListener.isSyncFinished() || syncState.isSyncNeeded() != ThreeState.YES) && !syncState.isSyncInProgress(); + // if (expectSyncFailure) { + // syncFinished = syncFinished && myGradleProjectEventListener.hasSyncError(); + // } + // return syncFinished; + // } + // }, LONG_TIMEOUT); + // + // findGradleSyncAction().waitUntilEnabledAndShowing(); + // + // if (myGradleProjectEventListener.hasSyncError()) { + // RuntimeException syncError = myGradleProjectEventListener.getSyncError(); + // myGradleProjectEventListener.reset(); + // throw syncError; + // } + // + // if (!myGradleProjectEventListener.isSyncSkipped()) { + // waitForBuildToFinish(SOURCE_GEN); + // } + // + // waitForBackgroundTasksToFinish(); + //} + + @NotNull + public IdeFrameFixture waitForBackgroundTasksToFinish() { + pause(new Condition("Background tasks to finish") { + @Override + public boolean test() { + ProgressManager progressManager = ProgressManager.getInstance(); + return !progressManager.hasModalProgressIndicator() && + !progressManager.hasProgressIndicator() && + !progressManager.hasUnsafeProgressIndicator(); + } + }, LONG_TIMEOUT); + robot().waitForIdle(); + return this; + } + + @NotNull + private ActionButtonFixture findActionButtonByActionId(String actionId) { + return ActionButtonFixture.findByActionId(actionId, robot(), target()); + } + + @NotNull + public MessagesToolWindowFixture getMessagesToolWindow() { + return new MessagesToolWindowFixture(getProject(), robot()); + } + + + @NotNull + public EditorNotificationPanelFixture requireEditorNotification(@NotNull final String message) { + final Ref notificationPanelRef = new Ref(); + + pause(new Condition("Notification with message '" + message + "' shows up") { + @Override + public boolean test() { + EditorNotificationPanel notificationPanel = findNotificationPanel(message); + notificationPanelRef.set(notificationPanel); + return notificationPanel != null; + } + }); + + EditorNotificationPanel notificationPanel = notificationPanelRef.get(); + assertNotNull(notificationPanel); + return new EditorNotificationPanelFixture(robot(), notificationPanel); + } + + public void requireNoEditorNotification() { + assertNull(findNotificationPanel(null)); + } + + /** + * Locates an editor notification with the given main message (unless the message is {@code null}, in which case we assert that there are + * no visible editor notifications. Will fail if the given notification is not found. + */ + @Nullable + private EditorNotificationPanel findNotificationPanel(@Nullable String message) { + Collection panels = robot().finder().findAll(target(), new GenericTypeMatcher( + EditorNotificationPanel.class, true) { + @Override + protected boolean isMatching(@NotNull EditorNotificationPanel panel) { + return panel.isShowing(); + } + }); + + if (message == null) { + if (!panels.isEmpty()) { + List labels = Lists.newArrayList(); + for (EditorNotificationPanel panel : panels) { + labels.addAll(getEditorNotificationLabels(panel)); + } + fail("Found editor notifications when none were expected" + labels); + } + return null; + } + + List labels = Lists.newArrayList(); + for (EditorNotificationPanel panel : panels) { + List found = getEditorNotificationLabels(panel); + labels.addAll(found); + for (String label : found) { + if (label.contains(message)) { + return panel; + } + } + } + + return null; + } + + /** + * Looks up the main label for a given editor notification panel + */ + private List getEditorNotificationLabels(@NotNull EditorNotificationPanel panel) { + final List allText = Lists.newArrayList(); + final Collection labels = robot().finder().findAll(panel, JLabelMatcher.any().andShowing()); + for (final JLabel label : labels) { + String text = execute(new GuiQuery() { + @Override + @Nullable + protected String executeInEDT() throws Throwable { + return label.getText(); + } + }); + if (isNotEmpty(text)) { + allText.add(text); + } + } + return allText; + } + + @NotNull + public IdeSettingsDialogFixture openIdeSettings() { + // Using invokeLater because we are going to show a *modal* dialog via API (instead of clicking a button, for example.) If we use + // GuiActionRunner the test will hang until the modal dialog is closed. + ApplicationManager.getApplication().invokeLater(new Runnable() { + @Override + public void run() { + Project project = getProject(); + ShowSettingsUtil.getInstance().showSettingsDialog(project, ShowSettingsUtilImpl.getConfigurableGroups(project, true)); + } + }); + return IdeSettingsDialogFixture.find(robot()); + } + + + @NotNull + public RunConfigurationsDialogFixture invokeRunConfigurationsDialog() { + invokeMenuPath("Run", "Edit Configurations..."); + return RunConfigurationsDialogFixture.find(robot()); + } + + @NotNull + public InspectionsFixture inspectCode() { + invokeMenuPath("Analyze", "Inspect Code..."); + + //final Ref wrapperRef = new Ref(); + JDialog dialog = robot().finder().find(new GenericTypeMatcher(JDialog.class) { + @Override + protected boolean isMatching(@NotNull JDialog dialog) { + return "Specify Inspection Scope".equals(dialog.getTitle()); + } + }); + JButton button = robot().finder().find(dialog, JButtonMatcher.withText("OK").andShowing()); + robot().click(button); + + final InspectionTree tree = waitUntilFound(robot(), new GenericTypeMatcher(InspectionTree.class) { + @Override + protected boolean isMatching(@NotNull InspectionTree component) { + return true; + } + }); + + return new InspectionsFixture(robot(), getProject(), tree); + } + + @NotNull + public ProjectViewFixture getProjectView() { + return new ProjectViewFixture(getProject(), robot()); + } + + @NotNull + public Project getProject() { + Project project = target().getProject(); + assertNotNull(project); + return project; + } + + public void closeProject() { + execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + closeAndDispose(getProject()); + RecentProjectsManager.getInstance().updateLastProjectPath(); + WelcomeFrame.showIfNoProjectOpened(); + } + }); + pause(new Condition("Waiting for 'Welcome' page to show up") { + @Override + public boolean test() { + for (Frame frame : Frame.getFrames()) { + if (frame == WelcomeFrame.getInstance() && frame.isShowing()) { + return true; + } + } + return false; + } + }); + } + + @NotNull + public MessagesFixture findMessageDialog(@NotNull String title) { + return MessagesFixture.findByTitle(robot(), target(), title); + } + + + + @NotNull + public FindDialogFixture invokeFindInPathDialog() { + invokeMenuPath("Edit", "Find", "Find in Path..."); + return FindDialogFixture.find(robot()); + } + + @NotNull + public FindToolWindowFixture getFindToolWindow() { + return new FindToolWindowFixture(this); + } + + //@NotNull + //public GradleBuildModelFixture parseBuildFileForModule(@NotNull String moduleName, boolean openInEditor) { + // Module module = getModule(moduleName); + // return parseBuildFile(module, openInEditor); + //} + // + //@NotNull + //public GradleBuildModelFixture parseBuildFile(@NotNull Module module, boolean openInEditor) { + // VirtualFile buildFile = getGradleBuildFile(module); + // assertNotNull(buildFile); + // return parseBuildFile(buildFile, openInEditor); + //} + // + //@NotNull + //public GradleBuildModelFixture parseBuildFile(@NotNull final VirtualFile buildFile, boolean openInEditor) { + // if (openInEditor) { + // getEditor().open(buildFile, Tab.DEFAULT).getCurrentFile(); + // } + // final Ref buildModelRef = new Ref(); + // new ReadAction() { + // @Override + // protected void run(@NotNull Result result) throws Throwable { + // buildModelRef.set(GradleBuildModel.parseBuildFile(buildFile, getProject())); + // } + // }.execute(); + // GradleBuildModel buildModel = buildModelRef.get(); + // assertNotNull(buildModel); + // return new GradleBuildModelFixture(buildModel); + //} + + private static class NoOpDisposable implements Disposable { + @Override + public void dispose() { + } + } + + public void selectApp(@NotNull String appName) { + final ActionButtonFixture runButton = findRunApplicationButton(); + Container actionToolbarContainer = execute(new GuiQuery() { + @Override + protected Container executeInEDT() throws Throwable { + return runButton.target().getParent(); + } + }); + assertNotNull(actionToolbarContainer); + + ComboBoxActionFixture comboBoxActionFixture = ComboBoxActionFixture.findComboBox(robot(), actionToolbarContainer); + comboBoxActionFixture.selectItem(appName); + robot().pressAndReleaseKey(KeyEvent.VK_ENTER); + robot().waitForIdle(); + } + + ///////////////////////////////////////////////////////////////// + //// Methods to help control debugging under a test. /////// + ///////////////////////////////////////////////////////////////// + + public void resumeProgram() { + invokeMenuPathOnRobotIdle(this, "Run", "Resume Program"); + } + + public void stepOver() { + invokeMenuPathOnRobotIdle(this, "Run", "Step Over"); + } + + public void stepInto() { + invokeMenuPathOnRobotIdle(this, "Run", "Step Into"); + } + + public void stepOut() { + invokeMenuPathOnRobotIdle(this, "Run", "Step Out"); + } + + /** + * Toggles breakpoints at the line numbers in {@code lines} of the source file with basename {@code fileBaseName}. This will work only + * if the fileBasename is unique in the project that's open on Android Studio. + */ + public void toggleBreakPoints(String fileBasename, int[] lines) { + // We open the file twice to bring the editor into focus. Idea 1.15 has this bug where opening a file doesn't automatically bring its + // editor window into focus. + openFile(this, fileBasename); + openFile(this, fileBasename); + for (int line : lines) { + navigateToLine(this, line); + invokeMenuPathOnRobotIdle(this, "Run", "Toggle Line Breakpoint"); + } + } + + // Recursively prints out the debugger tree rooted at {@code node} into {@code builder} with an indent of + // "{@code level} * {@code numIndentSpaces}" whitespaces. Each node is printed on a separate line. The indent level for every child node + // is 1 more than their parent. + private static void printNode(XDebuggerTreeNode node, StringBuilder builder, int level, int numIndentSpaces) { + int numIndent = level; + if (builder.length() > 0) { + builder.append(System.getProperty("line.separator")); + } + for (int i = 0; i < level * numIndentSpaces; ++i) { + builder.append(' '); + } + builder.append(node.getText().toString()); + Enumeration children = node.children(); + while (children.hasMoreElements()) { + printNode(children.nextElement(), builder, level + 1, numIndentSpaces); + } + } + + /** + * Prints out the debugger tree rooted at {@code root}. + */ + @NotNull + public static String printDebuggerTree(XDebuggerTreeNode root) { + StringBuilder builder = new StringBuilder(); + printNode(root, builder, 0, 2); + return builder.toString(); + } + + @NotNull + private static String[] debuggerTreeRootToChildrenTexts(XDebuggerTreeNode treeRoot) { + List children = treeRoot.getChildren(); + String[] childrenTexts = new String[children.size()]; + int i = 0; + for (TreeNode child : children) { + childrenTexts[i] = ((XDebuggerTreeNode)child).getText().toString(); + ++i; + } + return childrenTexts; + } + + /** + * Returns the subset of {@code expectedPatterns} which do not match any of the children (just the first level children, not recursive) of + * {@code treeRoot} . + */ + @NotNull + public static List getUnmatchedTerminalVariableValues(String[] expectedPatterns, XDebuggerTreeNode treeRoot) { + String[] childrenTexts = debuggerTreeRootToChildrenTexts(treeRoot); + List unmatchedPatterns = Lists.newArrayList(); + for (String expectedPattern : expectedPatterns) { + boolean matched = false; + for (String childText : childrenTexts) { + if (childText.matches(expectedPattern)) { + matched = true; + break; + } + } + if (!matched) { + unmatchedPatterns.add(expectedPattern); + } + } + return unmatchedPatterns; + } + + /** + * Returns the appropriate pattern to look for a variable named {@code name} with the type {@code type} and value {@code value} appearing + * in the Variables window in Android Studio. + */ + @NotNull + public static String variableToSearchPattern(String name, String type, String value) { + return String.format("%s = \\{%s\\} %s", name, type, value); + } + + public boolean verifyVariablesAtBreakpoint(String[] expectedVariablePatterns, String debugConfigName, long tryUntilMillis) { + DebugToolWindowFixture debugToolWindowFixture = new DebugToolWindowFixture(this); + final ExecutionToolWindowFixture.ContentFixture contentFixture = debugToolWindowFixture.findContent(debugConfigName); + + contentFixture.clickDebuggerTreeRoot(); + // Wait for the debugger tree to appear. + pause(new Condition("Looking for debugger tree.") { + @Override + public boolean test() { + return contentFixture.getDebuggerTreeRoot() != null; + } + }, Timeout.timeout(tryUntilMillis)); + + // Get the debugger tree and print it. + XDebuggerTreeNode debuggerTreeRoot = contentFixture.getDebuggerTreeRoot(); + if (debuggerTreeRoot == null) { + return false; + } + + List unmatchedPatterns = getUnmatchedTerminalVariableValues(expectedVariablePatterns, debuggerTreeRoot); + return unmatchedPatterns.isEmpty(); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/IdeSettingsDialogFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/IdeSettingsDialogFixture.java new file mode 100644 index 000000000000..ef8c06a6eb93 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/IdeSettingsDialogFixture.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.google.common.collect.Lists; +import com.intellij.openapi.options.Configurable; +import com.intellij.openapi.options.ConfigurableGroup; +import com.intellij.openapi.options.newEditor.SettingsDialog; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.ui.treeStructure.CachingSimpleNode; +import org.fest.swing.core.GenericTypeMatcher; +import org.fest.swing.core.Robot; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.util.List; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.reflect.core.Reflection.field; +import static org.junit.Assert.assertNotNull; + +public class IdeSettingsDialogFixture extends IdeaDialogFixture { + @NotNull + public static IdeSettingsDialogFixture find(@NotNull Robot robot) { + return new IdeSettingsDialogFixture(robot, find(robot, SettingsDialog.class, new GenericTypeMatcher(JDialog.class) { + @Override + protected boolean isMatching(@NotNull JDialog dialog) { + String expectedTitle = SystemInfo.isMac ? "Preferences" : "Settings"; + return expectedTitle.equals(dialog.getTitle()) && dialog.isShowing(); + } + })); + } + + private IdeSettingsDialogFixture(@NotNull Robot robot, @NotNull DialogAndWrapper dialogAndWrapper) { + super(robot, dialogAndWrapper); + } + + @NotNull + public List getProjectSettingsNames() { + List names = Lists.newArrayList(); + JPanel optionsEditor = field("myEditor").ofType(JPanel.class).in(getDialogWrapper()).get(); + assertNotNull(optionsEditor); + + List trees = findComponentsOfType(optionsEditor, "com.intellij.openapi.options.newEditor.SettingsTreeView"); + assertThat(trees).hasSize(1); + JComponent tree = trees.get(0); + + CachingSimpleNode root = field("myRoot").ofType(CachingSimpleNode.class).in(tree).get(); + assertNotNull(root); + + ConfigurableGroup[] groups = field("myGroups").ofType(ConfigurableGroup[].class).in(root).get(); + assertNotNull(groups); + for (ConfigurableGroup current : groups) { + Configurable[] configurables = current.getConfigurables(); + for (Configurable configurable : configurables) { + names.add(configurable.getDisplayName()); + } + } + return names; + } + + @NotNull + private static List findComponentsOfType(@NotNull JComponent parent, @NotNull String typeName) { + List result = Lists.newArrayList(); + findComponentsOfType(typeName, result, parent); + return result; + } + + private static void findComponentsOfType(@NotNull String typeName, @NotNull List result, @Nullable JComponent parent) { + if (parent == null) { + return; + } + if (parent.getClass().getName().equals(typeName)) { + result.add(parent); + } + for (Component c : parent.getComponents()) { + if (c instanceof JComponent) { + findComponentsOfType(typeName, result, (JComponent)c); + } + } + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/IdeaDialogFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/IdeaDialogFixture.java new file mode 100644 index 000000000000..2f93ae7898c2 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/IdeaDialogFixture.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.util.Ref; +import org.fest.reflect.exception.ReflectionError; +import org.fest.reflect.reference.TypeRef; +import org.fest.swing.core.GenericTypeMatcher; +import org.fest.swing.core.Robot; +import org.fest.swing.fixture.ContainerFixture; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.lang.ref.WeakReference; + +import static com.intellij.tests.gui.framework.GuiTests.findAndClickCancelButton; +import static com.intellij.tests.gui.framework.GuiTests.waitUntilFound; +import static junit.framework.Assert.assertNotNull; +import static org.fest.reflect.core.Reflection.field; + +public abstract class IdeaDialogFixture extends ComponentFixture implements ContainerFixture { + @NotNull private final T myDialogWrapper; + + @Nullable + protected static T getDialogWrapperFrom(@NotNull JDialog dialog, Class dialogWrapperType) { + try { + WeakReference dialogWrapperRef = field("myDialogWrapper").ofType(new TypeRef>() {}) + .in(dialog) + .get(); + assertNotNull(dialogWrapperRef); + DialogWrapper wrapper = dialogWrapperRef.get(); + if (dialogWrapperType.isInstance(wrapper)) { + return dialogWrapperType.cast(wrapper); + } + } + catch (ReflectionError ignored) { + } + return null; + } + + public static class DialogAndWrapper { + public final JDialog dialog; + public final T wrapper; + + public DialogAndWrapper(@NotNull JDialog dialog, @NotNull T wrapper) { + this.dialog = dialog; + this.wrapper = wrapper; + } + } + + @NotNull + public static DialogAndWrapper find(@NotNull Robot robot, @NotNull final Class clz) { + return find(robot, clz, new GenericTypeMatcher(JDialog.class) { + @Override + protected boolean isMatching(@NotNull JDialog component) { + return component.isShowing(); + } + }); + } + + @NotNull + public static DialogAndWrapper find(@NotNull Robot robot, @NotNull final Class clz, + @NotNull final GenericTypeMatcher matcher) { + final Ref wrapperRef = new Ref(); + JDialog dialog = waitUntilFound(robot, new GenericTypeMatcher(JDialog.class) { + @Override + protected boolean isMatching(@NotNull JDialog dialog) { + if (matcher.matches(dialog)) { + T wrapper = getDialogWrapperFrom(dialog, clz); + if (wrapper != null) { + wrapperRef.set(wrapper); + return true; + } + } + return false; + } + }); + return new DialogAndWrapper(dialog, wrapperRef.get()); + } + + protected IdeaDialogFixture(@NotNull Robot robot, @NotNull JDialog target, @NotNull T dialogWrapper) { + super(IdeaDialogFixture.class, robot, target); + myDialogWrapper = dialogWrapper; + } + + protected IdeaDialogFixture(@NotNull Robot robot, @NotNull DialogAndWrapper dialogAndWrapper) { + this(robot, dialogAndWrapper.dialog, dialogAndWrapper.wrapper); + } + + @NotNull + protected T getDialogWrapper() { + return myDialogWrapper; + } + + public void clickCancel() { + // Grab focus in case it is not automatically done by the window manager, e.g. 9wm + focus(); + + findAndClickCancelButton(this); + } + + public void close() { + robot().close(target()); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/InputDialogFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/InputDialogFixture.java new file mode 100644 index 000000000000..465f32320cd7 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/InputDialogFixture.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.util.Ref; +import org.fest.swing.core.GenericTypeMatcher; +import org.fest.swing.core.Robot; +import org.fest.swing.core.matcher.JTextComponentMatcher; +import org.fest.swing.fixture.JTextComponentFixture; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import javax.swing.text.JTextComponent; + +import static com.intellij.tests.gui.framework.GuiTests.findAndClickOkButton; +import static com.intellij.tests.gui.framework.GuiTests.waitUntilFound; +import static org.junit.Assert.assertNotNull; + +public class InputDialogFixture extends IdeaDialogFixture { + @NotNull + public static InputDialogFixture findByTitle(@NotNull Robot robot, @NotNull final String title) { + final Ref wrapperRef = new Ref(); + JDialog dialog = waitUntilFound(robot, new GenericTypeMatcher(JDialog.class) { + @Override + protected boolean isMatching(@NotNull JDialog dialog) { + if (!title.equals(dialog.getTitle()) || !dialog.isShowing()) { + return false; + } + DialogWrapper wrapper = getDialogWrapperFrom(dialog, DialogWrapper.class); + if (wrapper != null) { + String typeName = Messages.class.getName() + "$InputDialog"; + if (typeName.equals(wrapper.getClass().getName())) { + wrapperRef.set(wrapper); + return true; + } + } + return false; + } + }); + return new InputDialogFixture(robot, dialog, wrapperRef.get()); + } + + public void enterTextAndClickOk(@NotNull String text) { + JTextComponent input = robot().finder().find(target(), JTextComponentMatcher.any()); + assertNotNull(input); + JTextComponentFixture inputFixture = new JTextComponentFixture(robot(), input); + inputFixture.enterText(text); + findAndClickOkButton(this); + } + + private InputDialogFixture(@NotNull Robot robot, @NotNull JDialog target, @NotNull DialogWrapper dialogWrapper) { + super(robot, target, dialogWrapper); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/InspectionsFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/InspectionsFixture.java new file mode 100644 index 000000000000..c8cfcabe69db --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/InspectionsFixture.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.google.common.collect.Lists; +import com.intellij.codeInspection.ui.InspectionTree; +import com.intellij.codeInspection.ui.InspectionTreeNode; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.wm.ToolWindowId; +import org.fest.swing.core.Robot; +import org.fest.swing.edt.GuiQuery; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import static org.fest.swing.edt.GuiActionRunner.execute; + +/** + * Fixture for the Inspections window in the IDE + */ +public class InspectionsFixture extends ToolWindowFixture { + private final InspectionTree myTree; + + public InspectionsFixture(@NotNull Robot robot, @NotNull Project project, InspectionTree tree) { + super(ToolWindowId.INSPECTION, project, robot); + myTree = tree; + } + + public String getResults() { + activate(); + waitUntilIsVisible(); + + return execute(new GuiQuery() { + @Override + @Nullable + protected String executeInEDT() throws Throwable { + StringBuilder sb = new StringBuilder(); + InspectionsFixture.describe(myTree.getRoot(), sb, 0); + return sb.toString(); + } + }); + } + + public static void describe(@NotNull InspectionTreeNode node, @NotNull StringBuilder sb, int depth) { + for (int i = 0; i < depth; i++) { + sb.append(" "); + } + sb.append(node.toString()); + sb.append("\n"); + + // The exact order of the results sometimes varies so sort the children alphabetically + // instead to ensure stable test output + List children = Lists.newArrayListWithExpectedSize(node.getChildCount()); + for (int i = 0, n = node.getChildCount(); i < n; i++) { + children.add((InspectionTreeNode)node.getChildAt(i)); + } + Collections.sort(children, new Comparator() { + @Override + public int compare(InspectionTreeNode node1, InspectionTreeNode node2) { + return node1.toString().compareTo(node2.toString()); + } + }); + for (InspectionTreeNode child : children) { + describe(child, sb, depth + 1); + } + } +} \ No newline at end of file diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/JComponentFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/JComponentFixture.java new file mode 100644 index 000000000000..2ed098eeadd5 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/JComponentFixture.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import org.fest.swing.core.Robot; +import org.fest.swing.driver.JComponentDriver; +import org.fest.swing.fixture.AbstractJComponentFixture; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; + +public abstract class JComponentFixture extends AbstractJComponentFixture { + public JComponentFixture(@NotNull Class selfType, @NotNull Robot robot, @NotNull Class type) { + super(selfType, robot, type); + } + + public JComponentFixture(@NotNull Class selfType, @NotNull Robot robot, @Nullable String name, @NotNull Class type) { + super(selfType, robot, name, type); + } + + public JComponentFixture(@NotNull Class selfType, @NotNull Robot robot, @NotNull C target) { + super(selfType, robot, target); + } + + @Override + @NotNull + protected JComponentDriver createDriver(@NotNull Robot robot) { + return new JComponentDriver(robot); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/LibraryFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/LibraryFixture.java new file mode 100644 index 000000000000..7a2efc145364 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/LibraryFixture.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.intellij.openapi.roots.JavadocOrderRootType; +import com.intellij.openapi.roots.libraries.Library; +import org.jetbrains.annotations.NotNull; + +import static org.fest.assertions.Assertions.assertThat; + +public class LibraryFixture { + @NotNull private final Library myLibrary; + + LibraryFixture(@NotNull Library library) { + myLibrary = library; + } + + @NotNull + public LibraryFixture requireJavadocUrls(@NotNull String... urls) { + String[] actualUrls = myLibrary.getUrls(JavadocOrderRootType.getInstance()); + assertThat(actualUrls).as("Javadoc URLs").containsOnly(urls); + return this; + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/MenuFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/MenuFixture.java new file mode 100644 index 000000000000..7db83502bb03 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/MenuFixture.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.google.common.collect.Lists; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Ref; +import com.intellij.openapi.wm.impl.IdeFrameImpl; +import org.fest.swing.core.GenericTypeMatcher; +import org.fest.swing.core.Robot; +import org.fest.swing.timing.Condition; +import org.fest.swing.timing.Pause; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.awt.*; +import java.util.Arrays; +import java.util.List; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.util.Lists.newArrayList; +import static org.junit.Assert.assertNotNull; + +class MenuFixture { + @NotNull private final Robot myRobot; + @NotNull private final IdeFrameImpl myContainer; + + MenuFixture(@NotNull Robot robot, @NotNull IdeFrameImpl container) { + myRobot = robot; + myContainer = container; + } + + /** + * Invokes an action by menu path + * + * @param path the series of menu names, e.g. {@link invokeActionByMenuPath("Build", "Make Project ")} + */ + void invokeMenuPath(@NotNull String... path) { + JMenuItem menuItem = findActionMenuItem(false, path); + myRobot.click(menuItem); + } + + /** + * Invokes an action by menu path (where each segment is a regular expression). This is particularly + * useful when the menu items can change dynamically, such as the labels of Undo actions, Run actions, + * etc. + * + * @param path the series of menu name regular expressions, e.g. {@link invokeActionByMenuPath("Build", "Make( Project)?")} + */ + void invokeMenuPathRegex(@NotNull String... path) { + JMenuItem menuItem = findActionMenuItem(true, path); + myRobot.click(menuItem); + } + + @NotNull + private JMenuItem findActionMenuItem(final boolean pathIsRegex, @NotNull String... path) { + assertThat(path).isNotEmpty(); + int segmentCount = path.length; + + // We keep the list of previously found pop-up menus, so we don't look for menu items in the same pop-up more than once. + List previouslyFoundPopups = Lists.newArrayList(); + + Container root = myContainer; + for (int i = 0; i < segmentCount; i++) { + final String segment = path[i]; + assertNotNull(root); + JMenuItem found = myRobot.finder().find(root, new GenericTypeMatcher(JMenuItem.class) { + @Override + protected boolean isMatching(@NotNull JMenuItem menuItem) { + return pathIsRegex ? menuItem.getText().matches(segment) : segment.equals(menuItem.getText()); + } + }); + if (root instanceof JPopupMenu) { + previouslyFoundPopups.add((JPopupMenu)root); + } + if (i < segmentCount - 1) { + myRobot.click(found); + List showingPopupMenus = findShowingPopupMenus(i + 1); + showingPopupMenus.removeAll(previouslyFoundPopups); + assertThat(showingPopupMenus).hasSize(1); + root = showingPopupMenus.get(0); + continue; + } + return found; + } + throw new AssertionError("Menu item with path " + Arrays.toString(path) + " should have been found already"); + } + + @NotNull + private List findShowingPopupMenus(final int expectedCount) { + final Ref> ref = new Ref>(); + Pause.pause(new Condition("waiting for " + expectedCount + " JPopupMenus to show up") { + @Override + public boolean test() { + List popupMenus = newArrayList(myRobot.finder().findAll(new GenericTypeMatcher(JPopupMenu.class) { + @Override + protected boolean isMatching(@NotNull JPopupMenu popupMenu) { + return popupMenu.isShowing(); + } + })); + boolean allFound = popupMenus.size() == expectedCount; + if (allFound) { + ref.set(popupMenus); + } + return allFound; + } + }); + List popupMenus = ref.get(); + assertThat(popupMenus).isNotNull().hasSize(expectedCount); + return popupMenus; + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/MessageDialogFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/MessageDialogFixture.java new file mode 100644 index 000000000000..556fdf449ba6 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/MessageDialogFixture.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.util.Ref; +import org.fest.swing.core.GenericTypeMatcher; +import org.fest.swing.core.Robot; +import org.fest.swing.edt.GuiQuery; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +import static com.google.common.base.Strings.nullToEmpty; +import static com.intellij.tests.gui.framework.GuiTests.waitUntilFound; +import static org.fest.swing.edt.GuiActionRunner.execute; + +class MessageDialogFixture extends IdeaDialogFixture implements MessagesFixture.Delegate { + @NotNull + static MessageDialogFixture findByTitle(@NotNull Robot robot, @NotNull final String title) { + final Ref wrapperRef = new Ref(); + JDialog dialog = waitUntilFound(robot, new GenericTypeMatcher(JDialog.class) { + @Override + protected boolean isMatching(@NotNull JDialog dialog) { + if (!title.equals(dialog.getTitle()) || !dialog.isShowing()) { + return false; + } + DialogWrapper wrapper = getDialogWrapperFrom(dialog, DialogWrapper.class); + if (wrapper != null) { + String typeName = Messages.class.getName() + "$MessageDialog"; + if (typeName.equals(wrapper.getClass().getName())) { + wrapperRef.set(wrapper); + return true; + } + } + return false; + } + }); + return new MessageDialogFixture(robot, dialog, wrapperRef.get()); + } + + private MessageDialogFixture(@NotNull Robot robot, @NotNull JDialog target, @NotNull DialogWrapper dialogWrapper) { + super(robot, target, dialogWrapper); + } + + @Override + @NotNull + public String getMessage() { + final JTextPane textPane = robot().finder().findByType(target(), JTextPane.class); + //noinspection ConstantConditions + return execute(new GuiQuery() { + @Override + protected String executeInEDT() throws Throwable { + return nullToEmpty(textPane.getText()); + } + }); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/MessagesFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/MessagesFixture.java new file mode 100644 index 000000000000..848fb7190db6 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/MessagesFixture.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.ui.Messages; +import com.intellij.ui.messages.SheetController; +import org.fest.swing.core.GenericTypeMatcher; +import org.fest.swing.fixture.ContainerFixture; +import org.fest.swing.fixture.JPanelFixture; +import org.jdom.Document; +import org.jdom.Element; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; + +import org.fest.swing.core.Robot; + +import static com.google.common.base.Strings.nullToEmpty; +import static com.intellij.openapi.util.JDOMUtil.loadDocument; +import static com.intellij.tests.gui.framework.GuiTests.*; +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.reflect.core.Reflection.field; +import static org.junit.Assert.assertNotNull; + +public class MessagesFixture { + @NotNull private final ContainerFixture myDelegate; + + @NotNull + public static MessagesFixture findByTitle(@NotNull Robot robot, @NotNull Container root, @NotNull String title) { + if (Messages.canShowMacSheetPanel()) { + return new MessagesFixture(findMacSheetByTitle(robot, root, title)); + } + MessageDialogFixture dialog = MessageDialogFixture.findByTitle(robot, title); + return new MessagesFixture(dialog); + } + + private MessagesFixture(@NotNull ContainerFixture delegate) { + myDelegate = delegate; + } + + @NotNull + public MessagesFixture clickOk() { + findAndClickOkButton(myDelegate); + return this; + } + + + + @NotNull + public MessagesFixture clickYes() { + return click("Yes"); + } + + @NotNull + public MessagesFixture click(@NotNull String text) { + findAndClickButton(myDelegate, text); + return this; + } + + @NotNull + public MessagesFixture requireMessageContains(@NotNull String message) { + String actual = ((Delegate)myDelegate).getMessage(); + assertThat(actual).contains(message); + return this; + } + + public void clickCancel() { + findAndClickCancelButton(myDelegate); + } + + @NotNull + static JPanelFixture findMacSheetByTitle(@NotNull Robot robot, @NotNull Container root, @NotNull String title) { + JPanel sheetPanel = waitUntilFound(robot, root, new GenericTypeMatcher(JPanel.class) { + @Override + protected boolean isMatching(@NotNull JPanel panel) { + if (panel.getClass().getName().startsWith(SheetController.class.getName()) && panel.isShowing()) { + SheetController controller = findSheetController(panel); + JPanel sheetPanel = field("mySheetPanel").ofType(JPanel.class).in(controller).get(); + if (sheetPanel == panel) { + return true; + } + } + return false; + } + }); + + String sheetTitle = getTitle(sheetPanel, robot); + assertThat(sheetTitle).as("Sheet title").isEqualTo(title); + + return new MacSheetPanelFixture(robot, sheetPanel); + } + + @Nullable + private static String getTitle(@NotNull JPanel sheetPanel, @NotNull Robot robot) { + final JEditorPane messageTextPane = getMessageTextPane(sheetPanel); + + JEditorPane titleTextPane = robot.finder().find(sheetPanel, new GenericTypeMatcher(JEditorPane.class) { + @Override + protected boolean isMatching(@NotNull JEditorPane editorPane) { + return editorPane != messageTextPane; + } + }); + + return getHtmlBody(titleTextPane.getText()); + } + + @Nullable + public T find(GenericTypeMatcher matcher) { + return myDelegate.robot().finder().find(myDelegate.target(), matcher); + } + + interface Delegate { + @NotNull String getMessage(); + } + + private static class MacSheetPanelFixture extends JPanelFixture implements Delegate { + public MacSheetPanelFixture(@NotNull Robot robot, @NotNull JPanel target) { + super(robot, target); + } + + @Override + @NotNull + public String getMessage() { + JEditorPane messageTextPane = getMessageTextPane(target()); + String text = getHtmlBody(messageTextPane.getText()); + return nullToEmpty(text); + } + } + + @NotNull + private static JEditorPane getMessageTextPane(@NotNull JPanel sheetPanel) { + SheetController sheetController = findSheetController(sheetPanel); + JEditorPane messageTextPane = field("messageTextPane").ofType(JEditorPane.class).in(sheetController).get(); + assertNotNull(messageTextPane); + return messageTextPane; + } + + @NotNull + private static SheetController findSheetController(@NotNull JPanel sheetPanel) { + SheetController sheetController = field("this$0").ofType(SheetController.class).in(sheetPanel).get(); + assertNotNull(sheetController); + return sheetController; + } + + @Nullable + private static String getHtmlBody(@NotNull String html) { + try { + Document document = loadDocument(html); + Element rootElement = document.getRootElement(); + String sheetTitle = rootElement.getChild("body").getText(); + return sheetTitle.replace("\n", "").trim(); + } + catch (Throwable e) { + Logger.getInstance(MessagesFixture.class).info("Failed to parse HTML '" + html + "'", e); + } + return null; + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/MessagesToolWindowFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/MessagesToolWindowFixture.java new file mode 100644 index 000000000000..67920dc5cecd --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/MessagesToolWindowFixture.java @@ -0,0 +1,375 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; +import com.intellij.ide.errorTreeView.*; +import com.intellij.openapi.externalSystem.service.notification.EditableNotificationMessageElement; +import com.intellij.openapi.externalSystem.service.notification.NotificationMessageElement; +import com.intellij.openapi.fileEditor.OpenFileDescriptor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.wm.ToolWindowId; +import com.intellij.pom.Navigatable; +import com.intellij.ui.content.Content; +import org.fest.swing.core.Robot; +import org.fest.swing.edt.GuiQuery; +import org.fest.swing.edt.GuiTask; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import javax.swing.event.HyperlinkEvent; +import javax.swing.tree.TreeCellEditor; +import java.io.File; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.intellij.openapi.vfs.VfsUtilCore.virtualToIoFile; +import static javax.swing.event.HyperlinkEvent.EventType.ACTIVATED; +import static junit.framework.Assert.assertNotNull; +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.reflect.core.Reflection.field; +import static org.fest.swing.awt.AWT.visibleCenterOf; +import static org.fest.swing.edt.GuiActionRunner.execute; +import static org.fest.util.Strings.quote; + +public class MessagesToolWindowFixture extends ToolWindowFixture { + MessagesToolWindowFixture(@NotNull Project project, @NotNull Robot robot) { + super(ToolWindowId.MESSAGES_WINDOW, project, robot); + } + + @NotNull + public ContentFixture getGradleSyncContent() { + Content content = getContent("Gradle Sync"); + assertNotNull(content); + return new SyncContentFixture(content); + } + + @NotNull + public ContentFixture getGradleBuildContent() { + Content content = getContent("Gradle Build"); + assertNotNull(content); + return new BuildContentFixture(content); + } + + public abstract static class ContentFixture { + @NotNull private final Content myContent; + + private ContentFixture(@NotNull Content content) { + myContent = content; + } + + @NotNull + public MessageFixture findMessageContainingText(@NotNull ErrorTreeElementKind kind, @NotNull final String text) { + ErrorTreeElement element = doFindMessage(kind, new MessageMatcher() { + @Override + protected boolean matches(@NotNull String[] lines) { + for (String s : lines) { + if (s.contains(text)) { + return true; + } + } + return false; + } + }); + return createFixture(element); + } + + @NotNull + public MessageFixture findMessage(@NotNull ErrorTreeElementKind kind, @NotNull MessageMatcher matcher) { + ErrorTreeElement found = doFindMessage(kind, matcher); + return createFixture(found); + } + + @NotNull + protected abstract MessageFixture createFixture(@NotNull ErrorTreeElement element); + + @NotNull + private ErrorTreeElement doFindMessage(@NotNull final ErrorTreeElementKind kind, @NotNull final MessageMatcher matcher) { + ErrorTreeElement found = execute(new GuiQuery() { + @Override + @Nullable + protected ErrorTreeElement executeInEDT() throws Throwable { + NewErrorTreeViewPanel component = (NewErrorTreeViewPanel)myContent.getComponent(); + ErrorViewStructure errorView = component.getErrorViewStructure(); + + Object root = errorView.getRootElement(); + return findMessage(errorView, errorView.getChildElements(root), matcher, kind); + } + }); + + assertNotNull(String.format("Failed to find message of type %1$s and matching text %2$s", kind, matcher.toString()), found); + return found; + } + + @Nullable + private static ErrorTreeElement findMessage(@NotNull ErrorViewStructure errorView, + @NotNull ErrorTreeElement[] children, + @NotNull MessageMatcher matcher, + @NotNull ErrorTreeElementKind kind) { + for (ErrorTreeElement child : children) { + if (child instanceof GroupingElement) { + ErrorTreeElement found = findMessage(errorView, errorView.getChildElements(child), matcher, kind); + if (found != null) { + return found; + } + } + if (kind == child.getKind() && matcher.matches(child.getText())) { + return child; + } + } + return null; + } + } + + public static abstract class MessageMatcher { + protected abstract boolean matches(@NotNull String[] text); + + @NotNull + public static MessageMatcher firstLineStartingWith(@NotNull final String prefix) { + return new MessageMatcher() { + @Override + public boolean matches(@NotNull String[] text) { + assertThat(text).isNotEmpty(); + return text[0].startsWith(prefix); + } + + @Override + public String toString() { + return "first line starting with " + quote(prefix); + } + }; + } + } + + public class SyncContentFixture extends ContentFixture { + SyncContentFixture(@NotNull Content content) { + super(content); + } + + @Override + @NotNull + protected MessageFixture createFixture(@NotNull ErrorTreeElement element) { + return new SyncMessageFixture(myRobot, element); + } + } + + public class BuildContentFixture extends ContentFixture { + BuildContentFixture(@NotNull Content content) { + super(content); + } + + @Override + @NotNull + protected MessageFixture createFixture(@NotNull ErrorTreeElement element) { + throw new UnsupportedOperationException(); + } + } + + public abstract static class MessageFixture { + private static final Pattern ANCHOR_TAG_PATTERN = Pattern.compile("([^<]+)"); + + @NotNull protected final Robot myRobot; + @NotNull protected final ErrorTreeElement myTarget; + + protected MessageFixture(@NotNull Robot robot, @NotNull ErrorTreeElement target) { + myRobot = robot; + myTarget = target; + } + + @NotNull + public abstract HyperlinkFixture findHyperlink(@NotNull String hyperlinkText); + + @NotNull + protected String extractUrl(@NotNull String wholeText, @NotNull String hyperlinkText) { + String url = null; + Matcher matcher = ANCHOR_TAG_PATTERN.matcher(wholeText); + while (matcher.find()) { + String anchorText = matcher.group(2); + // Text may be spread across multiple lines. Put everything in one line. + if (anchorText != null) { + anchorText = anchorText.replaceAll("[\\s]+", " "); + if (anchorText.equals(hyperlinkText)) { + url = matcher.group(1); + break; + } + } + } + assertNotNull("Failed to find URL for hyperlink " + quote(hyperlinkText), url); + return url; + } + + @NotNull + public MessageFixture requireLocation(@NotNull File filePath, int line) { + doRequireLocation(filePath, line); + return this; + } + + protected void doRequireLocation(@NotNull File expectedFilePath, int line) { + assertThat(myTarget).isInstanceOf(NotificationMessageElement.class); + NotificationMessageElement element = (NotificationMessageElement)myTarget; + + Navigatable navigatable = element.getNavigatable(); + assertThat(navigatable).isInstanceOf(OpenFileDescriptor.class); + + OpenFileDescriptor descriptor = (OpenFileDescriptor)navigatable; + File actualFilePath = virtualToIoFile(descriptor.getFile()); + assertThat(actualFilePath).isEqualTo(expectedFilePath); + + assertThat((descriptor.getLine() + 1)).as("line").isEqualTo(line); // descriptor line is zero-based. + } + + @NotNull + public abstract String getText(); + } + + public static class SyncMessageFixture extends MessageFixture { + SyncMessageFixture(@NotNull Robot robot, @NotNull ErrorTreeElement target) { + super(robot, target); + } + + @Override + @NotNull + public HyperlinkFixture findHyperlink(@NotNull String hyperlinkText) { + Pair cellEditorAndText = getCellEditorAndText(); + String url = extractUrl(cellEditorAndText.getSecond(), hyperlinkText); + return new SyncHyperlinkFixture(myRobot, url, cellEditorAndText.getFirst()); + } + + @Override + @NotNull + public String getText() { + String html = getCellEditorAndText().getSecond(); + + int startBodyIndex = html.indexOf(""); + assertThat(startBodyIndex).isGreaterThanOrEqualTo(0); + + int endBodyIndex = html.indexOf(""); + assertThat(endBodyIndex).isGreaterThan(startBodyIndex); + + String body = html.substring(startBodyIndex + 6 /* 6 = length of '' */, endBodyIndex); + List lines = Splitter.on('\n').omitEmptyStrings().trimResults().splitToList(body); + body = Joiner.on(' ').join(lines); + + return body; + } + + @NotNull + private Pair getCellEditorAndText() { + // There is no specific UI component for a hyperlink in the "Messages" window. Instead we have a JEditorPane with HTML. This method + // finds the anchor tags, and matches the text of each of them against the given text. If a matching hyperlink is found, we fire a + // HyperlinkEvent, simulating a click on the actual hyperlink. + assertThat(myTarget).isInstanceOf(EditableNotificationMessageElement.class); + + final JEditorPane editorComponent = execute(new GuiQuery() { + @Override + protected JEditorPane executeInEDT() throws Throwable { + EditableNotificationMessageElement message = (EditableNotificationMessageElement)myTarget; + TreeCellEditor cellEditor = message.getRightSelfEditor(); + return field("editorComponent").ofType(JEditorPane.class).in(cellEditor).get(); + } + }); + assertNotNull(editorComponent); + + String text = execute(new GuiQuery() { + @Override + protected String executeInEDT() throws Throwable { + return editorComponent.getText(); + } + }); + assertNotNull(text); + + return Pair.create(editorComponent, text); + } + } + + public abstract static class HyperlinkFixture { + @NotNull protected final Robot myRobot; + @NotNull protected final String myUrl; + + protected HyperlinkFixture(@NotNull Robot robot, @NotNull String url) { + myRobot = robot; + myUrl = url; + } + + @NotNull + public HyperlinkFixture requireUrl(@NotNull String expected) { + assertThat(myUrl).as("URL").isEqualTo(expected); + return this; + } + + @NotNull + public HyperlinkFixture click() { + click(true); + return this; + } + + /** + * Simulates a click on the hyperlink. This method returns immediately and does not wait for any UI actions triggered by the click to be + * finished. + */ + public HyperlinkFixture clickAndContinue() { + click(false); + return this; + } + + private void click(boolean synchronous) { + if (synchronous) { + execute(new GuiTask() { + @Override + protected void executeInEDT() { + new Runnable() { + @Override + public void run() { + doClick(); + } + }.run(); + } + }); + } + else { + //noinspection SSBasedInspection + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + doClick(); + } + }); + } + } + + protected abstract void doClick(); + } + + public static class SyncHyperlinkFixture extends HyperlinkFixture { + @NotNull private final JEditorPane myTarget; + + SyncHyperlinkFixture(@NotNull Robot robot, @NotNull String url, @NotNull JEditorPane target) { + super(robot, url); + myTarget = target; + } + + @Override + protected void doClick() { + // at least move the mouse where the message is, so we can know that something is happening. + myRobot.moveMouse(visibleCenterOf(myTarget)); + myTarget.fireHyperlinkUpdate(new HyperlinkEvent(this, ACTIVATED, null, myUrl)); + } + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/ProjectViewFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/ProjectViewFixture.java new file mode 100644 index 000000000000..067ddb32b2fe --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/ProjectViewFixture.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.google.common.base.Strings; +import com.google.common.collect.Lists; +import com.intellij.ide.projectView.ProjectView; +import com.intellij.ide.projectView.ProjectViewNode; +import com.intellij.ide.projectView.impl.AbstractProjectViewPane; +import com.intellij.ide.projectView.impl.nodes.ExternalLibrariesNode; +import com.intellij.ide.projectView.impl.nodes.NamedLibraryElement; +import com.intellij.ide.projectView.impl.nodes.NamedLibraryElementNode; +import com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode; +import com.intellij.ide.util.treeView.AbstractTreeStructure; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.projectRoots.JavaSdk; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.roots.JdkOrderEntry; +import com.intellij.openapi.roots.LibraryOrSdkOrderEntry; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiDirectory; +import com.intellij.util.ui.tree.TreeUtil; +import org.fest.swing.core.Robot; +import org.fest.swing.edt.GuiActionRunner; +import org.fest.swing.edt.GuiQuery; +import org.fest.swing.edt.GuiTask; +import org.fest.swing.timing.Condition; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.tree.DefaultMutableTreeNode; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import static com.intellij.tests.gui.framework.GuiTests.SHORT_TIMEOUT; +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.reflect.core.Reflection.field; +import static org.fest.swing.timing.Pause.pause; +import static org.junit.Assert.assertNotNull; + +public class ProjectViewFixture extends ToolWindowFixture { + ProjectViewFixture(@NotNull Project project, @NotNull Robot robot) { + super("Project", project, robot); + } + + @NotNull + public PaneFixture selectProjectPane() { + activate(); + final ProjectView projectView = ProjectView.getInstance(myProject); + pause(new Condition("Project view is initialized") { + @Override + public boolean test() { + //noinspection ConstantConditions + return field("isInitialized").ofType(boolean.class).in(projectView).get(); + } + }, SHORT_TIMEOUT); + + final String id = "ProjectPane"; + GuiActionRunner.execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + projectView.changeView(id); + } + }); + return new PaneFixture(projectView.getProjectViewPaneById(id)); + } + + @NotNull + public PaneFixture selectAndroidPane() { + activate(); + final ProjectView projectView = ProjectView.getInstance(myProject); + pause(new Condition("Project view is initialized") { + @Override + public boolean test() { + //noinspection ConstantConditions + return field("isInitialized").ofType(boolean.class).in(projectView).get(); + } + }, SHORT_TIMEOUT); + + final String id = "AndroidView"; + GuiActionRunner.execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + projectView.changeView(id); + } + }); + return new PaneFixture(projectView.getProjectViewPaneById(id)); + } + + public static class PaneFixture { + @NotNull private final AbstractProjectViewPane myPane; + + PaneFixture(@NotNull AbstractProjectViewPane pane) { + myPane = pane; + } + + @NotNull + public PaneFixture expand() { + GuiActionRunner.execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + TreeUtil.expandAll(myPane.getTree()); + } + }); + return this; + } + + @NotNull + private AbstractTreeStructure getTreeStructure() { + final AtomicReference treeStructureRef = new AtomicReference(); + pause(new Condition("Tree Structure to be built") { + @Override + public boolean test() { + AbstractTreeStructure treeStructure = GuiActionRunner.execute(new GuiQuery() { + @Override + protected AbstractTreeStructure executeInEDT() throws Throwable { + try { + return myPane.getTreeBuilder().getTreeStructure(); + } + catch (NullPointerException e) { + // expected; + } + return null; + } + }); + treeStructureRef.set(treeStructure); + return treeStructure != null; + } + }, SHORT_TIMEOUT); + + return treeStructureRef.get(); + } + + @NotNull + public NodeFixture findExternalLibrariesNode() { + final AbstractTreeStructure treeStructure = getTreeStructure(); + + ExternalLibrariesNode node = GuiActionRunner.execute(new GuiQuery() { + @Nullable + @Override + protected ExternalLibrariesNode executeInEDT() throws Throwable { + Object[] childElements = treeStructure.getChildElements(treeStructure.getRootElement()); + for (Object child : childElements) { + if (child instanceof ExternalLibrariesNode) { + return (ExternalLibrariesNode)child; + } + } + return null; + } + }); + if (node != null) { + return new NodeFixture(node, treeStructure); + } + throw new AssertionError("Unable to find 'External Libraries' node"); + } + + public void selectByPath(@NotNull final String... paths) { + final AbstractTreeStructure treeStructure = getTreeStructure(); + + final PsiDirectoryNode node = GuiActionRunner.execute(new GuiQuery() { + @Nullable + @Override + protected PsiDirectoryNode executeInEDT() throws Throwable { + Object root = treeStructure.getRootElement(); + final List treePath = Lists.newArrayList(root); + + for (String path : paths) { + Object[] childElements = treeStructure.getChildElements(root); + Object newRoot = null; + for (Object child : childElements) { + if (child instanceof PsiDirectoryNode) { + PsiDirectory dir = ((PsiDirectoryNode)child).getValue(); + if (dir != null && path.equals(dir.getName())) { + newRoot = child; + treePath.add(newRoot); + break; + } + } + } + if (newRoot != null) { + root = newRoot; + } + else { + return null; + } + } + if (root == treeStructure.getRootElement()) { + return null; + } + + myPane.expand(treePath.toArray(), true); + myPane.select(root, ((PsiDirectoryNode)root).getVirtualFile(), true); + return (PsiDirectoryNode)root; + } + }); + + assertNotNull(node); + + pause(new Condition("Node to be selected") { + @Override + public boolean test() { + return node.equals(GuiActionRunner.execute(new GuiQuery() { + @Override + protected Object executeInEDT() throws Throwable { + DefaultMutableTreeNode selectedNode = myPane.getSelectedNode(); + if (selectedNode != null) { + return selectedNode.getUserObject(); + } + return null; + } + })); + } + }, SHORT_TIMEOUT); + } + } + + public static class NodeFixture { + @NotNull private final ProjectViewNode myNode; + @NotNull private final AbstractTreeStructure myTreeStructure; + + NodeFixture(@NotNull ProjectViewNode node, @NotNull AbstractTreeStructure treeStructure) { + myNode = node; + myTreeStructure = treeStructure; + } + + @NotNull + public List getChildren() { + final List children = Lists.newArrayList(); + GuiActionRunner.execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + for (Object child : myTreeStructure.getChildElements(myNode)) { + if (child instanceof ProjectViewNode) { + children.add(new NodeFixture((ProjectViewNode)child, myTreeStructure)); + } + } + } + }); + return children; + } + + public boolean isJdk() { + if (myNode instanceof NamedLibraryElementNode) { + NamedLibraryElement value = ((NamedLibraryElementNode)myNode).getValue(); + assertNotNull(value); + LibraryOrSdkOrderEntry orderEntry = value.getOrderEntry(); + if (orderEntry instanceof JdkOrderEntry) { + Sdk sdk = ((JdkOrderEntry)orderEntry).getJdk(); + return sdk.getSdkType() instanceof JavaSdk; + } + } + return false; + } + + @NotNull + public NodeFixture requireDirectory(@NotNull String name) { + assertThat(myNode).isInstanceOf(PsiDirectoryNode.class); + VirtualFile file = myNode.getVirtualFile(); + assertNotNull(file); + assertThat(file.getName()).isEqualTo(name); + return this; + } + + @Override + public String toString() { + return Strings.nullToEmpty(myNode.getName()); + } + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/RenameDialogFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/RenameDialogFixture.java new file mode 100644 index 000000000000..a42a9f45a830 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/RenameDialogFixture.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.intellij.openapi.actionSystem.impl.SimpleDataContext; +import com.intellij.openapi.util.Ref; +import com.intellij.psi.PsiElement; +import com.intellij.refactoring.RefactoringBundle; +import com.intellij.refactoring.rename.RenameDialog; +import com.intellij.refactoring.rename.RenameHandler; +import com.intellij.ui.EditorTextField; +import org.fest.swing.core.GenericTypeMatcher; +import org.fest.swing.core.Robot; +import org.fest.swing.edt.GuiQuery; +import org.fest.swing.edt.GuiTask; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; + +import static com.google.common.base.Strings.isNullOrEmpty; +import static com.intellij.tests.gui.framework.GuiTests.waitUntilFound; +import static org.fest.reflect.core.Reflection.field; +import static org.fest.swing.edt.GuiActionRunner.execute; +import static org.junit.Assert.assertNotNull; + +public class RenameDialogFixture extends IdeaDialogFixture { + + public RenameDialogFixture(@NotNull Robot robot, @NotNull JDialog target, @NotNull RenameDialog dialogWrapper) { + super(robot, target, dialogWrapper); + } + + /** + * Starts 'rename' refactoring for the given data. + *

+ * Note: proper way would be to write dedicated 'project view fixture' and emulate user actions like 'expand nodes until + * we find a target one' but that IJ component (project view) is rather complex and it's much easier to start the refactoring + * programmatically. + * + * @param element target PSI element for which 'rename' refactoring should begin + * @param handler rename refactoring handler to use + * @param robot robot to use + * @return a fixture for the 'rename dialog' which occurs when we start 'rename' refactoring for the given data + */ + @NotNull + public static RenameDialogFixture startFor(@NotNull final PsiElement element, + @NotNull final RenameHandler handler, + @NotNull Robot robot) + { + // We use SwingUtilities instead of FEST here because RenameDialog is modal and GuiActionRunner doesn't return until + //noinspection SSBasedInspection + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + handler.invoke(element.getProject(), new PsiElement[] { element }, SimpleDataContext.getProjectContext(element.getProject())); + } + }); + final Ref ref = new Ref(); + JDialog dialog = waitUntilFound(robot, new GenericTypeMatcher(JDialog.class) { + @Override + protected boolean isMatching(@NotNull JDialog dialog) { + if (!RefactoringBundle.message("rename.title").equals(dialog.getTitle()) || !dialog.isShowing()) { + return false; + } + RenameDialog renameDialog = getDialogWrapperFrom(dialog, RenameDialog.class); + if (renameDialog == null) { + return false; + } + ref.set(renameDialog); + return true; + } + }); + return new RenameDialogFixture(robot, dialog, ref.get()); + } + + @NotNull + public String getNewName() { + //noinspection ConstantConditions + return execute(new GuiQuery() { + @Override + protected String executeInEDT() throws Throwable { + String text = robot().finder().findByType(target(), EditorTextField.class).getText(); + return text == null ? "" : text; + } + }); + } + + public void setNewName(@NotNull final String newName) { + execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + robot().finder().findByType(target(), EditorTextField.class).setText(newName); + } + }); + } + + /** + * Allows to check if a warning exists at the target 'rename dialog' + * + * @param warningText null as a wildcard to match any non-empty warning text; + * non-null text which is evaluated to be a part of the target dialog's warning text + * @return true if the target 'rename dialog' has a warning and given text matches it according to the + * rules described above; false otherwise + */ + public boolean warningExists(@Nullable final String warningText) { + //noinspection ConstantConditions + return execute(new GuiQuery() { + @Override + protected Boolean executeInEDT() throws Throwable { + JComponent errorTextPane = field("myErrorText").ofType(JComponent.class).in(getDialogWrapper()).get(); + assertNotNull(errorTextPane); + if (!errorTextPane.isVisible()) { + return false; + } + JLabel errorLabel = field("myLabel").ofType(JLabel.class).in(errorTextPane).get(); + assertNotNull(errorLabel); + String text = errorLabel.getText(); + if (isNullOrEmpty(text)) { + return false; + } + return warningText == null || text.contains(warningText); + } + }); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/RenameRefactoringDialogFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/RenameRefactoringDialogFixture.java new file mode 100644 index 000000000000..711642159220 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/RenameRefactoringDialogFixture.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.android.tools.lint.detector.api.TextFormat; +import com.intellij.refactoring.rename.RenameDialog; +import com.intellij.refactoring.ui.ConflictsDialog; +import com.intellij.ui.EditorTextField; +import org.fest.swing.core.Robot; +import org.fest.swing.core.matcher.JTextComponentMatcher; +import org.fest.swing.edt.GuiActionRunner; +import org.fest.swing.edt.GuiQuery; +import org.fest.swing.edt.GuiTask; +import org.jetbrains.annotations.NotNull; + +import javax.swing.text.JTextComponent; +import java.awt.event.KeyEvent; +import java.util.regex.Pattern; + +import static com.intellij.tests.gui.framework.GuiTests.findAndClickButton; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class RenameRefactoringDialogFixture extends IdeaDialogFixture { + @NotNull + public static RenameRefactoringDialogFixture find(@NotNull Robot robot) { + return new RenameRefactoringDialogFixture(robot, find(robot, RenameDialog.class)); + } + + private RenameRefactoringDialogFixture(@NotNull Robot robot, @NotNull DialogAndWrapper dialogAndWrapper) { + super(robot, dialogAndWrapper); + } + + @NotNull + public RenameRefactoringDialogFixture setNewName(@NotNull final String newName) { + final EditorTextField field = robot().finder().findByType(target(), EditorTextField.class); + GuiActionRunner.execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + field.requestFocus(); + } + }); + robot().pressAndReleaseKey(KeyEvent.VK_BACK_SPACE); // to make sure we don't append to existing item on Linux + robot().enterText(newName); + return this; + } + + @NotNull + public RenameRefactoringDialogFixture clickRefactor() { + findAndClickButton(this, "Refactor"); + return this; + } + + public static class ConflictsDialogFixture extends IdeaDialogFixture { + protected ConflictsDialogFixture(@NotNull Robot robot, @NotNull DialogAndWrapper dialogAndWrapper) { + super(robot, dialogAndWrapper); + } + + @NotNull + public static ConflictsDialogFixture find(@NotNull Robot robot) { + return new ConflictsDialogFixture(robot, find(robot, ConflictsDialog.class)); + } + + @NotNull + public ConflictsDialogFixture clickContinue() { + findAndClickButton(this, "Continue"); + return this; + } + + public String getHtml() { + final JTextComponent component = robot().finder().find(target(), JTextComponentMatcher.any()); + return GuiActionRunner.execute(new GuiQuery() { + @Override + protected String executeInEDT() throws Throwable { + return component.getText(); + } + }); + } + + public String getText() { + String html = getHtml(); + return TextFormat.HTML.convertTo(html, TextFormat.TEXT).trim(); + } + + public void requireMessageText(@NotNull String text) { + assertEquals(text, getText()); + } + + public void requireMessageTextContains(@NotNull String text) { + assertTrue(getText() + " does not contain expected message fragment " + text, getText().contains(text)); + } + + public void requireMessageTextMatches(@NotNull String regexp) { + assertTrue(getText() + " does not match " + regexp, Pattern.matches(regexp, getText())); + } + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/ResourceChooserDialogFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/ResourceChooserDialogFixture.java new file mode 100644 index 000000000000..971526b41fd4 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/ResourceChooserDialogFixture.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import org.fest.swing.core.Robot; +import org.fest.swing.core.matcher.DialogMatcher; +import org.fest.swing.core.matcher.JLabelMatcher; +import org.fest.swing.driver.JTextComponentDriver; +import org.fest.swing.fixture.ContainerFixture; +import org.jetbrains.annotations.NotNull; + +import javax.swing.text.JTextComponent; + +import java.awt.*; + +import static com.intellij.tests.gui.framework.GuiTests.findAndClickOkButton; + +public class ResourceChooserDialogFixture extends ComponentFixture + implements ContainerFixture

{ + + @NotNull + public static ResourceChooserDialogFixture findDialog(@NotNull Robot robot) { + Dialog jDialog = robot.finder().find(DialogMatcher.withTitle("Select Resource Directory").andShowing()); + + return new ResourceChooserDialogFixture(robot, jDialog); + } + + private ResourceChooserDialogFixture(@NotNull Robot robot, Dialog target) { + super(ResourceChooserDialogFixture.class, robot, target); + } + + public void setDirectoryName(@NotNull String directory) { + Container parent = robot().finder().find(target(), JLabelMatcher.withText("Directory name:")).getParent(); + + JTextComponent directoryField = robot().finder().findByType(parent, JTextComponent.class, true); + JTextComponentDriver driver = new JTextComponentDriver(robot()); + driver.selectAll(directoryField); + driver.setText(directoryField, directory); + } + + @NotNull + public ResourceChooserDialogFixture clickOK() { + findAndClickOkButton(this); + return this; + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/RunConfigurationComboBoxFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/RunConfigurationComboBoxFixture.java new file mode 100644 index 000000000000..699d0c0edf71 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/RunConfigurationComboBoxFixture.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.intellij.openapi.actionSystem.impl.ActionToolbarImpl; +import org.fest.swing.core.ComponentFinder; +import org.fest.swing.core.GenericTypeMatcher; +import org.fest.swing.core.Robot; +import org.fest.swing.edt.GuiQuery; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; + +import static com.intellij.openapi.actionSystem.ActionPlaces.MAIN_TOOLBAR; +import static org.fest.reflect.core.Reflection.field; +import static org.fest.swing.edt.GuiActionRunner.execute; + +public class RunConfigurationComboBoxFixture extends JComponentFixture { + @NotNull + static RunConfigurationComboBoxFixture find(@NotNull final IdeFrameFixture parent) { + ComponentFinder finder = parent.robot().finder(); + ActionToolbarImpl toolbar = finder.find(parent.target(), new GenericTypeMatcher(ActionToolbarImpl.class) { + @Override + protected boolean isMatching(@NotNull ActionToolbarImpl toolbar) { + String place = field("myPlace").ofType(String.class).in(toolbar).get(); + return MAIN_TOOLBAR.equals(place); + } + }); + + JButton button = finder.find(toolbar, new GenericTypeMatcher(JButton.class) { + @Override + protected boolean isMatching(@NotNull JButton button) { + return button.getClass().getSimpleName().equals("ComboBoxButton"); + } + }); + return new RunConfigurationComboBoxFixture(parent.robot(), button); + } + + private RunConfigurationComboBoxFixture(@NotNull Robot robot, @NotNull JButton target) { + super(RunConfigurationComboBoxFixture.class, robot, target); + } + + @Nullable + public String getText() { + return execute(new GuiQuery() { + @Override + protected String executeInEDT() throws Throwable { + return target().getText(); + } + }); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/RunConfigurationsDialogFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/RunConfigurationsDialogFixture.java new file mode 100644 index 000000000000..b98ba7e2bddb --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/RunConfigurationsDialogFixture.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.intellij.execution.configurations.ConfigurationType; +import com.intellij.tests.gui.matcher.ClassNameMatcher; +import com.intellij.ui.components.JBList; +import com.intellij.ui.popup.PopupFactoryImpl.ActionItem; +import com.intellij.ui.treeStructure.Tree; +import org.fest.swing.cell.JTreeCellReader; +import org.fest.swing.core.GenericTypeMatcher; +import org.fest.swing.core.Robot; +import org.fest.swing.core.matcher.DialogMatcher; +import org.fest.swing.core.matcher.JButtonMatcher; +import org.fest.swing.driver.BasicJListCellReader; +import org.fest.swing.edt.GuiActionRunner; +import org.fest.swing.edt.GuiTask; +import org.fest.swing.fixture.JListFixture; +import org.fest.swing.fixture.JTreeFixture; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.KeyEvent; + +import static com.intellij.tests.gui.framework.GuiTests.waitUntilFound; +import static org.fest.reflect.core.Reflection.method; + +/** + * Controls the Run Configurations dialog + */ +public class RunConfigurationsDialogFixture extends ComponentFixture { + + public RunConfigurationsDialogFixture(@NotNull Robot robot, @NotNull JDialog target) { + super(RunConfigurationsDialogFixture.class, robot, target); + } + + @NotNull + public static RunConfigurationsDialogFixture find(@NotNull Robot robot) { + JDialog frame = waitUntilFound(robot, new GenericTypeMatcher(JDialog.class) { + @Override + protected boolean isMatching(@NotNull JDialog dialog) { + return "Run/Debug Configurations".equals(dialog.getTitle()) && dialog.isShowing(); + } + }); + return new RunConfigurationsDialogFixture(robot, frame); + } + + @NotNull + private JButton findButtonByText(@NotNull String text) { + return robot().finder().find(target(), JButtonMatcher.withText(text).andShowing()); + } + + public void save() { + robot().click(findButtonByText("OK")); + } + + public JTreeFixture getTreeFixture() { + Tree tree = robot().finder().findByType(target(), Tree.class, true); + JTreeFixture fixture = new JTreeFixture(robot(), tree); + fixture.replaceCellReader(new RunConfigurationsJTreeCellReader()); + return fixture; + } + + public void select(String path) { + getTreeFixture().selectPath(path); + } + + public void useGradleAwareMake() { + JBList stepsList = robot().finder().findByType(target(), JBList.class); + JListFixture stepsFixture = new JListFixture(robot(), stepsList); + // Find ActionToolbarImpl and ActionButtons inside. + if (!stepsFixture.contents()[0].equals("Make")) { + throw new IllegalStateException("There should be only only one make step, 'Make'."); + } + + stepsFixture.clickItem(0); + JPanel beforeRunStepsPanel = robot().finder().find( + target(), + ClassNameMatcher.forClass("com.intellij.execution.impl.BeforeRunStepsPanel", JPanel.class, true)); + + ActionButtonFixture.findByText("Remove", robot(), beforeRunStepsPanel).click(); + ActionButtonFixture.findByText("Add", robot(), beforeRunStepsPanel).click(); + + JBList popupList = robot().finder().find( + target(), + ClassNameMatcher.forClass("com.intellij.ui.popup.list.ListPopupImpl$MyList", JBList.class, true)); + + click(popupList, "Gradle-aware Make"); + + Dialog gradleMakeDialog = robot().finder().find(DialogMatcher.withTitle("Select Gradle Task").andShowing()); + robot().click(robot().finder().find(gradleMakeDialog, JButtonMatcher.withText("OK"))); + } + + private void click(final JBList popupList, String text) { + JListFixture popupFixture = new JListFixture(robot(), popupList); + popupFixture.replaceCellReader(new MakeStepsCellReader()); + final int index = popupFixture.item(text).index(); + + // For some reason calling popupFixture.click(...) doesn't work, but this does: + GuiActionRunner.execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + popupList.setSelectedIndex(index); + } + }); + robot().pressAndReleaseKey(KeyEvent.VK_ENTER); + } + + /** {@link JTreeCellReader} that works with this particular tree. */ + private static class RunConfigurationsJTreeCellReader implements JTreeCellReader { + @Nullable + @Override + public String valueAt(@NotNull JTree tree, Object modelValue) { + Object userObject = method("getUserObject").withReturnType(Object.class).in(modelValue).invoke(); + if (userObject == null) { + return null; + } + + if (userObject instanceof ConfigurationType) { + return ((ConfigurationType)userObject).getDisplayName(); + } + + return userObject.toString(); + } + } + + private static class MakeStepsCellReader extends BasicJListCellReader { + @Nullable + @Override + public String valueAt(@NotNull JList list, int index) { + Object element = list.getModel().getElementAt(index); + if (element instanceof ActionItem) { + return ((ActionItem)element).getText(); + } + return super.valueAt(list, index); + } + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/RunToolWindowFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/RunToolWindowFixture.java new file mode 100644 index 000000000000..2d8a52b55b82 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/RunToolWindowFixture.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import org.jetbrains.annotations.NotNull; + +public class RunToolWindowFixture extends ExecutionToolWindowFixture { + public RunToolWindowFixture(@NotNull IdeFrameFixture frameFixture) { + super("Run", frameFixture); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/SearchTextFieldFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/SearchTextFieldFixture.java new file mode 100644 index 000000000000..f328e8f535af --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/SearchTextFieldFixture.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.intellij.tests.gui.driver.SearchTextFieldDriver; +import com.intellij.ui.SearchTextField; +import org.fest.swing.core.Robot; +import org.fest.swing.fixture.AbstractJComponentFixture; +import org.jetbrains.annotations.NotNull; + +public class SearchTextFieldFixture extends AbstractJComponentFixture { + public SearchTextFieldFixture(@NotNull Robot robot, + @NotNull SearchTextField target) { + super(SearchTextFieldFixture.class, robot, target); + } + + @NotNull + public SearchTextFieldFixture enterText(@NotNull String text) { + driver().enterText(target(), text); + + return this; + } + + @NotNull + public SearchTextFieldFixture requireText(@NotNull String text) { + driver().requireText(target(), text); + + return this; + } + + @NotNull + @Override + protected SearchTextFieldDriver createDriver(Robot robot) { + return new SearchTextFieldDriver(robot); + } +} \ No newline at end of file diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/SelectRefactoringDialogFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/SelectRefactoringDialogFixture.java new file mode 100644 index 000000000000..c1c05c15de66 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/SelectRefactoringDialogFixture.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.util.Ref; +import org.fest.swing.core.GenericTypeMatcher; +import org.fest.swing.core.Robot; +import org.fest.swing.fixture.JRadioButtonFixture; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +import static com.intellij.tests.gui.framework.GuiTests.findAndClickOkButton; +import static com.intellij.tests.gui.framework.GuiTests.waitUntilFound; + +public class SelectRefactoringDialogFixture extends IdeaDialogFixture { + @NotNull + public static SelectRefactoringDialogFixture findByTitle(@NotNull Robot robot) { + final Ref wrapperRef = new Ref(); + JDialog dialog = waitUntilFound(robot, new GenericTypeMatcher(JDialog.class) { + @Override + protected boolean isMatching(@NotNull JDialog dialog) { + if (!"Select Refactoring".equals(dialog.getTitle()) || !dialog.isShowing()) { + return false; + } + DialogWrapper wrapper = getDialogWrapperFrom(dialog, DialogWrapper.class); + if (wrapper != null) { + wrapperRef.set(wrapper); + return true; + } + return false; + } + }); + return new SelectRefactoringDialogFixture(robot, dialog, wrapperRef.get()); + } + + public void selectRenameModule() { + JRadioButton renameModuleCheckbox = robot().finder().find(target(), new GenericTypeMatcher(JRadioButton.class) { + @Override + protected boolean isMatching(@NotNull JRadioButton checkBox) { + return "Rename module".equals(checkBox.getText()); + } + }); + + JRadioButtonFixture renameModuleRadioButton = new JRadioButtonFixture(robot(), renameModuleCheckbox); + renameModuleRadioButton.select(); + } + + public void clickOk() { + findAndClickOkButton(this); + } + + private SelectRefactoringDialogFixture(@NotNull Robot robot, @NotNull JDialog target, @NotNull DialogWrapper dialogWrapper) { + super(robot, target, dialogWrapper); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/SelectSdkDialogFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/SelectSdkDialogFixture.java new file mode 100644 index 000000000000..3f788158d5b1 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/SelectSdkDialogFixture.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +public class SelectSdkDialogFixture { + //extends IdeaDialogFixture { + //@NotNull + //public static SelectSdkDialogFixture find(@NotNull Robot robot) { + // return new SelectSdkDialogFixture(robot, find(robot, SelectSdkDialog.class)); + //} + // + //private SelectSdkDialogFixture(@NotNull Robot robot, @NotNull DialogAndWrapper dialogAndWrapper) { + // super(robot, dialogAndWrapper); + //} + // + //@NotNull + //public SelectSdkDialogFixture setJdkPath(@NotNull final File path) { + // final JLabel label = robot().finder().find(target(), JLabelMatcher.withText("Select Java JDK:").andShowing()); + // execute(new GuiTask() { + // @Override + // protected void executeInEDT() throws Throwable { + // Component textField = label.getLabelFor(); + // assertThat(textField).isInstanceOf(JTextField.class); + // ((JTextField)textField).setText(path.getPath()); + // } + // }); + // return this; + //} + // + //@NotNull + //public SelectSdkDialogFixture clickOk() { + // findAndClickOkButton(this); + // return this; + //} +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/ToolWindowFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/ToolWindowFixture.java new file mode 100644 index 000000000000..2b93f70f4ac9 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/ToolWindowFixture.java @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Ref; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowManager; +import com.intellij.ui.content.Content; +import org.fest.swing.core.Robot; +import org.fest.swing.edt.GuiQuery; +import org.fest.swing.edt.GuiTask; +import org.fest.swing.timing.Condition; +import org.fest.swing.timing.Timeout; +import org.fest.swing.util.TextMatcher; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; + +import static com.intellij.tests.gui.framework.GuiTests.SHORT_TIMEOUT; +import static com.intellij.tests.gui.framework.GuiTests.THIRTY_SEC_TIMEOUT; +import static org.fest.swing.edt.GuiActionRunner.execute; +import static org.fest.swing.timing.Pause.pause; + +public abstract class ToolWindowFixture { + @NotNull protected final String myToolWindowId; + @NotNull protected final Project myProject; + @NotNull protected final Robot myRobot; + @NotNull protected final ToolWindow myToolWindow; + + protected ToolWindowFixture(@NotNull final String toolWindowId, @NotNull final Project project, @NotNull Robot robot) { + myToolWindowId = toolWindowId; + myProject = project; + final Ref toolWindowRef = new Ref(); + pause(new Condition("Find tool window with ID '" + toolWindowId + "'") { + @Override + public boolean test() { + ToolWindow toolWindow = ToolWindowManager.getInstance(project).getToolWindow(toolWindowId); + toolWindowRef.set(toolWindow); + return toolWindow != null; + } + }, SHORT_TIMEOUT); + myRobot = robot; + myToolWindow = toolWindowRef.get(); + } + + @Nullable + protected Content getContent(@NotNull final String displayName) { + activateAndWaitUntilIsVisible(); + final Ref contentRef = new Ref(); + pause(new Condition("finding content '" + displayName + "'") { + @Override + public boolean test() { + Content[] contents = getContents(); + for (Content content : contents) { + if (displayName.equals(content.getDisplayName())) { + contentRef.set(content); + return true; + } + } + return false; + } + }, SHORT_TIMEOUT); + return contentRef.get(); + } + + @Nullable + protected Content getContent(@NotNull final String displayName, @NotNull Timeout timeout) { + long now = System.currentTimeMillis(); + long budget = timeout.duration(); + activateAndWaitUntilIsVisible(Timeout.timeout(budget)); + long revisedNow = System.currentTimeMillis(); + budget -= (revisedNow - now); + final Ref contentRef = new Ref(); + pause(new Condition("finding content with display name " + displayName) { + @Override + public boolean test() { + Content[] contents = getContents(); + for (Content content : contents) { + if (displayName.equals(content.getDisplayName())) { + contentRef.set(content); + return true; + } + } + return false; + } + }, Timeout.timeout(budget)); + return contentRef.get(); + } + + @Nullable + protected Content getContent(@NotNull final TextMatcher displayNameMatcher) { + return getContent(displayNameMatcher, SHORT_TIMEOUT); + } + + @Nullable + protected Content getContent(@NotNull final TextMatcher displayNameMatcher, @NotNull Timeout timeout) { + long now = System.currentTimeMillis(); + long budget = timeout.duration(); + activateAndWaitUntilIsVisible(Timeout.timeout(budget)); + long revisedNow = System.currentTimeMillis(); + budget -= (revisedNow - now); + now = revisedNow; + final Ref contentRef = new Ref(); + pause(new Condition("finding content matching " + displayNameMatcher.formattedValues()) { + @Override + public boolean test() { + Content[] contents = getContents(); + for (Content content : contents) { + String displayName = content.getDisplayName(); + if (displayNameMatcher.isMatching(displayName)) { + contentRef.set(content); + return true; + } + } + return false; + } + }, Timeout.timeout(budget)); + return contentRef.get(); + } + + private void activateAndWaitUntilIsVisible() { + activateAndWaitUntilIsVisible(SHORT_TIMEOUT); + } + + private void activateAndWaitUntilIsVisible(@NotNull Timeout timeout) { + long now = System.currentTimeMillis(); + long budget = timeout.duration(); + activate(); + budget -= System.currentTimeMillis() - now; + waitUntilIsVisible(Timeout.timeout(budget)); + } + + @NotNull + private Content[] getContents() { + return myToolWindow.getContentManager().getContents(); + } + + protected boolean isActive() { + //noinspection ConstantConditions + return execute(new GuiQuery() { + @Override + protected Boolean executeInEDT() throws Throwable { + return myToolWindow.isActive(); + } + }); + } + + public void activate() { + if (isActive()) { + return; + } + + final Callback callback = new Callback(); + execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + myToolWindow.activate(callback); + } + }); + + pause(new Condition("Wait for ToolWindow '" + myToolWindowId + "' to be activated") { + @Override + public boolean test() { + return callback.finished; + } + }, SHORT_TIMEOUT); + } + + protected void waitUntilIsVisible() { + waitUntilIsVisible(THIRTY_SEC_TIMEOUT); + } + + protected void waitUntilIsVisible(@NotNull Timeout timeout) { + pause(new Condition("Wait for ToolWindow '" + myToolWindowId + "' to be visible") { + @Override + public boolean test() { + if (!isActive()) { + activate(); + } + return isVisible(); + } + }, timeout); + } + + private boolean isVisible() { + //noinspection ConstantConditions + return execute(new GuiQuery() { + @Override + protected Boolean executeInEDT() throws Throwable { + if (!myToolWindow.isVisible()) { + return false; + } + JComponent component = myToolWindow.getComponent(); + return component.isVisible() && component.isShowing(); + } + }); + } + + private static class Callback implements Runnable { + volatile boolean finished; + + @Override + public void run() { + finished = true; + } + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/UnitTestTreeFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/UnitTestTreeFixture.java new file mode 100644 index 000000000000..69ad8abda30d --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/UnitTestTreeFixture.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.intellij.execution.testframework.AbstractTestProxy; +import com.intellij.execution.testframework.TestFrameworkRunningModel; +import com.intellij.execution.testframework.TestTreeView; +import org.fest.swing.timing.Condition; +import org.fest.swing.timing.Pause; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Fixture for the tree widget, on the left hand side of "Run" window (when running tests). + */ +public class UnitTestTreeFixture { + private ExecutionToolWindowFixture.ContentFixture myContentFixture; + private final TestTreeView myTreeView; + + public UnitTestTreeFixture(@NotNull ExecutionToolWindowFixture.ContentFixture contentFixture, + @NotNull TestTreeView treeView) { + myContentFixture = contentFixture; + myTreeView = treeView; + } + + @Nullable + public TestFrameworkRunningModel getModel() { + Pause.pause(new Condition("Wait for the test results model.") { + @Override + public boolean test() { + return myTreeView.getData(TestTreeView.MODEL_DATA_KEY.getName()) != null; + } + }); + + return TestTreeView.MODEL_DATA_KEY.getData(myTreeView); + } + + public boolean isAllTestsPassed() { + return getFailingTestsCount() == 0; + } + + public int getFailingTestsCount() { + int count = 0; + AbstractTestProxy root = getModel().getRoot(); + for (AbstractTestProxy test : root.getAllTests()) { + if (test.isLeaf() && test.isDefect()) { + count++; + } + } + return count; + } + + public int getAllTestsCount() { + int count = 0; + AbstractTestProxy root = getModel().getRoot(); + for (AbstractTestProxy test : root.getAllTests()) { + if (test.isLeaf()) { + count++; + } + } + return count; + } + + @NotNull + public ExecutionToolWindowFixture.ContentFixture getContent() { + return myContentFixture; + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/WelcomeFrameFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/WelcomeFrameFixture.java new file mode 100755 index 000000000000..e37148e29d05 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/WelcomeFrameFixture.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures; + +import com.intellij.openapi.wm.impl.welcomeScreen.FlatWelcomeFrame; +import org.fest.swing.core.Robot; +import org.fest.swing.exception.ComponentLookupException; +import org.jetbrains.annotations.NotNull; + +import java.awt.*; + + +public class WelcomeFrameFixture extends ComponentFixture { + @NotNull + public static WelcomeFrameFixture find(@NotNull Robot robot) { + for (Frame frame : Frame.getFrames()) { + if (frame instanceof FlatWelcomeFrame && frame.isShowing()) { + return new WelcomeFrameFixture(robot, (FlatWelcomeFrame)frame); + } + } + throw new ComponentLookupException("Unable to find 'Welcome' window"); + } + + private WelcomeFrameFixture(@NotNull Robot robot, @NotNull FlatWelcomeFrame target) { + super(WelcomeFrameFixture.class, robot, target); + } + + @NotNull + public WelcomeFrameFixture createNewProject() { + findActionLinkByActionId("WelcomeScreen.CreateNewProject").click(); + return this; + } + + @NotNull + public WelcomeFrameFixture importProject() { + findActionLinkByActionId("WelcomeScreen.ImportProject").click(); + return this; + } + + @NotNull + private ActionLinkFixture findActionLinkByActionId(String actionId) { + return ActionLinkFixture.findByActionId(actionId, robot(), target()); + } + + @NotNull + public MessagesFixture findMessageDialog(@NotNull String title) { + return MessagesFixture.findByTitle(robot(), target(), title); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/newProjectWizard/AbstractWizardFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/newProjectWizard/AbstractWizardFixture.java new file mode 100644 index 000000000000..cdc32f513b49 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/newProjectWizard/AbstractWizardFixture.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures.newProjectWizard; + +import com.intellij.tests.gui.fixtures.ComponentFixture; +import org.fest.swing.core.GenericTypeMatcher; +import org.fest.swing.core.Robot; +import org.fest.swing.fixture.ContainerFixture; +import org.fest.swing.fixture.JButtonFixture; +import org.fest.swing.fixture.JLabelFixture; +import org.fest.swing.fixture.JTextComponentFixture; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import javax.swing.text.JTextComponent; + +import static com.intellij.tests.gui.framework.GuiTests.*; + +/** + * Base class for fixtures which control wizards that extend {@link DynamicWizard} + */ +public abstract class AbstractWizardFixture extends ComponentFixture implements ContainerFixture { + + public AbstractWizardFixture(@NotNull Class selfType, @NotNull Robot robot, @NotNull JDialog target) { + super(selfType, robot, target); + } + + @NotNull + protected JRootPane findStepWithTitle(@NotNull final String title) { + JRootPane rootPane = target().getRootPane(); + waitUntilFound(robot(), rootPane, new GenericTypeMatcher(JLabel.class) { + @Override + protected boolean isMatching(@NotNull JLabel label) { + if (!label.isShowing()) { + return false; + } + return title.equals(label.getText()); + } + }); + return rootPane; + } + + @NotNull + public S clickNext() { + findAndClickButton(this, "Next"); + return myself(); + } + + @NotNull + public S clickFinish() { + findAndClickButton(this, "Finish"); + return myself(); + } + + @NotNull + public S clickCancel() { + findAndClickCancelButton(this); + return myself(); + } + + @NotNull + public JTextComponentFixture findTextField(@NotNull final String labelText) { + return new JTextComponentFixture(robot(), robot().finder().findByLabel(labelText, JTextComponent.class)); + } + + @NotNull + public JButtonFixture findWizardButton(@NotNull final String text) { + JButton button = robot().finder().find(target(), new GenericTypeMatcher(JButton.class) { + @Override + protected boolean isMatching(@NotNull JButton button) { + String buttonText = button.getText(); + if (buttonText != null) { + return buttonText.trim().equals(text) && button.isShowing(); + } + return false; + } + }); + return new JButtonFixture(robot(), button); + } + + @NotNull + public JLabelFixture findLabel(@NotNull final String text) { + JLabel label = waitUntilFound(robot(), target(), new GenericTypeMatcher(JLabel.class) { + @Override + protected boolean isMatching(@NotNull JLabel label) { + return text.equals(label.getText().replaceAll("(?i)<.?html>", "")); + } + }); + + return new JLabelFixture(robot(), label); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/newProjectWizard/AbstractWizardStepFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/newProjectWizard/AbstractWizardStepFixture.java new file mode 100644 index 000000000000..618fe3db411d --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/newProjectWizard/AbstractWizardStepFixture.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures.newProjectWizard; + +import com.intellij.tests.gui.fixtures.JComponentFixture; +import org.fest.swing.core.GenericTypeMatcher; +import org.fest.swing.core.Robot; +import org.fest.swing.driver.JTextComponentDriver; +import org.fest.swing.fixture.JCheckBoxFixture; +import org.fest.swing.fixture.JComboBoxFixture; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +public abstract class AbstractWizardStepFixture extends JComponentFixture { + protected AbstractWizardStepFixture(@NotNull Class selfType, @NotNull Robot robot, @NotNull JRootPane target) { + super(selfType, robot, target); + } + + @NotNull + protected JCheckBoxFixture findCheckBoxWithLabel(@NotNull final String label) { + JCheckBox checkBox = robot().finder().find(target(), new GenericTypeMatcher(JCheckBox.class) { + @Override + protected boolean isMatching(@NotNull JCheckBox component) { + return label.equals(component.getText()); + } + }); + return new JCheckBoxFixture(robot(), checkBox); + } + + @NotNull + protected JComboBoxFixture findComboBoxWithLabel(@NotNull String label) { + JComboBox comboBox = robot().finder().findByLabel(target(), label, JComboBox.class, true); + return new JComboBoxFixture(robot(), comboBox); + } + + @NotNull + protected JTextField findTextFieldWithLabel(@NotNull String label) { + return robot().finder().findByLabel(target(), label, JTextField.class, true); + } + + protected void replaceText(@NotNull JTextField textField, @NotNull String text) { + JTextComponentDriver driver = new JTextComponentDriver(robot()); + driver.selectAll(textField); + driver.enterText(textField, text); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/newProjectWizard/ChooseOptionsForNewFileStepFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/newProjectWizard/ChooseOptionsForNewFileStepFixture.java new file mode 100644 index 000000000000..63d9a4875b4e --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/newProjectWizard/ChooseOptionsForNewFileStepFixture.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures.newProjectWizard; + +import org.fest.swing.core.Robot; +import org.fest.swing.edt.GuiQuery; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +import static org.fest.swing.edt.GuiActionRunner.execute; + +public class ChooseOptionsForNewFileStepFixture extends AbstractWizardStepFixture { + protected ChooseOptionsForNewFileStepFixture(@NotNull Robot robot, @NotNull JRootPane target) { + super(ChooseOptionsForNewFileStepFixture.class, robot, target); + } + + @NotNull + public ChooseOptionsForNewFileStepFixture enterActivityName(@NotNull String name) { + JTextField textField = robot().finder().findByLabel(target(), "Activity Name:", JTextField.class, true); + replaceText(textField, name); + return this; + } + + @NotNull + public String getLayoutName() { + final JTextField textField = robot().finder().findByLabel("Layout Name:", JTextField.class, true); + //noinspection ConstantConditions + return execute(new GuiQuery() { + @Override + protected String executeInEDT() throws Throwable { + return textField.getText(); + } + }); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/newProjectWizard/ConfigureAndroidProjectStepFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/newProjectWizard/ConfigureAndroidProjectStepFixture.java new file mode 100644 index 000000000000..a97bb1f35edb --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/newProjectWizard/ConfigureAndroidProjectStepFixture.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures.newProjectWizard; + +import com.intellij.openapi.ui.TextFieldWithBrowseButton; +import org.fest.swing.core.Robot; +import org.fest.swing.edt.GuiQuery; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.io.File; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.swing.edt.GuiActionRunner.execute; + +public class ConfigureAndroidProjectStepFixture extends AbstractWizardStepFixture { + protected ConfigureAndroidProjectStepFixture(@NotNull Robot robot, @NotNull JRootPane target) { + super(ConfigureAndroidProjectStepFixture.class, robot, target); + } + + @NotNull + public ConfigureAndroidProjectStepFixture enterApplicationName(@NotNull String text) { + JTextField textField = findTextFieldWithLabel("Application name:"); + replaceText(textField, text); + return this; + } + + @NotNull + public ConfigureAndroidProjectStepFixture enterCompanyDomain(@NotNull String text) { + JTextField textField = findTextFieldWithLabel("Company Domain:"); + replaceText(textField, text); + return this; + } + + + @NotNull + public File getLocationInFileSystem() { + final TextFieldWithBrowseButton locationField = robot().finder().findByType(target(), TextFieldWithBrowseButton.class); + //noinspection ConstantConditions + return execute(new GuiQuery() { + @Override + protected File executeInEDT() throws Throwable { + String location = locationField.getText(); + assertThat(location).isNotNull().isNotEmpty(); + return new File(location); + } + }); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/newProjectWizard/ConfigureFormFactorStepFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/newProjectWizard/ConfigureFormFactorStepFixture.java new file mode 100644 index 000000000000..2f70fd559488 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/newProjectWizard/ConfigureFormFactorStepFixture.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures.newProjectWizard; + +import org.fest.swing.core.Robot; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +public class ConfigureFormFactorStepFixture extends AbstractWizardStepFixture { + protected ConfigureFormFactorStepFixture(@NotNull Robot robot, @NotNull JRootPane target) { + super(ConfigureFormFactorStepFixture.class, robot, target); + } + + //@NotNull + //public ConfigureFormFactorStepFixture selectMinimumSdkApi(@NotNull final FormFactor formFactor, @NotNull final String api) { + // JCheckBox checkBox = robot().finder().find(target(), new GenericTypeMatcher(JCheckBox.class) { + // @Override + // protected boolean isMatching(@NotNull JCheckBox checkBox) { + // String text = checkBox.getText(); + // // "startsWith" instead of "equals" because the UI may add "(Not installed)" at the end. + // return text != null && text.startsWith(formFactor.toString()); + // } + // }); + // AbstractButtonDriver buttonDriver = new AbstractButtonDriver(robot()); + // buttonDriver.requireEnabled(checkBox); + // buttonDriver.select(checkBox); + // + // final JComboBox comboBox = robot().finder().findByName(target(), formFactor.id + ".minSdk", JComboBox.class); + // //noinspection ConstantConditions + // int itemIndex = execute(new GuiQuery() { + // @Override + // protected Integer executeInEDT() throws Throwable { + // BasicJComboBoxCellReader cellReader = new BasicJComboBoxCellReader(); + // int itemCount = comboBox.getItemCount(); + // for (int i = 0; i < itemCount; i++) { + // String value = cellReader.valueAt(comboBox, i); + // if (value != null && value.startsWith("API " + api + ":")) { + // return i; + // } + // } + // return -1; + // } + // }); + // if (itemIndex < 0) { + // throw new LocationUnavailableException("Unable to find SDK " + api + " in " + formFactor + " drop-down"); + // } + // JComboBoxDriver comboBoxDriver = new JComboBoxDriver(robot()); + // comboBoxDriver.selectItem(comboBox, itemIndex); + // return this; + //} +} diff --git a/community-tests/src/com/intellij/tests/gui/fixtures/newProjectWizard/NewProjectWizardFixture.java b/community-tests/src/com/intellij/tests/gui/fixtures/newProjectWizard/NewProjectWizardFixture.java new file mode 100644 index 000000000000..764d7da60653 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/fixtures/newProjectWizard/NewProjectWizardFixture.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.fixtures.newProjectWizard; + +import com.intellij.ide.IdeBundle; +import com.intellij.ui.components.JBList; +import org.fest.swing.core.GenericTypeMatcher; +import org.fest.swing.core.Robot; +import org.fest.swing.fixture.JListFixture; +import org.fest.swing.fixture.JTreeFixture; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; + +public class NewProjectWizardFixture extends AbstractWizardFixture { + @NotNull + public static NewProjectWizardFixture find(@NotNull Robot robot) { + JDialog dialog = robot.finder().find(new GenericTypeMatcher(JDialog.class) { + @Override + protected boolean isMatching(@NotNull JDialog dialog) { + return IdeBundle.message("title.new.project").equals(dialog.getTitle()) && dialog.isShowing(); + } + }); + return new NewProjectWizardFixture(robot, dialog); + } + + private NewProjectWizardFixture(@NotNull Robot robot, @NotNull JDialog target) { + super(NewProjectWizardFixture.class, robot, target); + } + + + @NotNull + public NewProjectWizardFixture selectProjectType(String projectTypeName){ + JListFixture projectTypeList = new JListFixture(robot(), robot().finder().findByType(JBList.class, true)); + projectTypeList.clickItem(projectTypeName); + return this; + } + + @NotNull + public NewProjectWizardFixture selectFramework(String frameworkName){ + JTreeFixture frameworkTree = new JTreeFixture(robot(), robot().finder().findByType(JTree.class, true)); + //frameworkTree. + return this; + } + + @NotNull + public ConfigureAndroidProjectStepFixture getConfigureAndroidProjectStep() { + JRootPane rootPane = findStepWithTitle("Configure your new project"); + return new ConfigureAndroidProjectStepFixture(robot(), rootPane); + } + + @NotNull + public ConfigureFormFactorStepFixture getConfigureFormFactorStep() { + JRootPane rootPane = findStepWithTitle("Select the form factors your app will run on"); + return new ConfigureFormFactorStepFixture(robot(), rootPane); + } + + @NotNull + public ChooseOptionsForNewFileStepFixture getChooseOptionsForNewFileStep() { + JRootPane rootPane = findStepWithTitle("Customize the Activity"); + return new ChooseOptionsForNewFileStepFixture(robot(), rootPane); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/framework/GuiTestCase.java b/community-tests/src/com/intellij/tests/gui/framework/GuiTestCase.java new file mode 100644 index 000000000000..20a1372c1c4b --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/framework/GuiTestCase.java @@ -0,0 +1,403 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.framework; + +/** + * Created by karashevich on 30/05/16. + */ + + +import com.intellij.openapi.application.Application; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectManager; +import com.intellij.openapi.project.ex.ProjectManagerEx; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.wm.WindowManager; +import com.intellij.openapi.wm.impl.WindowManagerImpl; +import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeFrame; +import com.intellij.tests.gui.fixtures.IdeFrameFixture; +import com.intellij.tests.gui.fixtures.WelcomeFrameFixture; +import com.intellij.tests.gui.fixtures.newProjectWizard.NewProjectWizardFixture; +import com.intellij.util.net.HttpConfigurable; +import org.fest.swing.core.BasicRobot; +import org.fest.swing.core.Robot; +import org.fest.swing.edt.GuiQuery; +import org.fest.swing.edt.GuiTask; +import org.fest.swing.timing.Condition; +import org.jdom.Document; +import org.jdom.Element; +import org.jdom.input.SAXBuilder; +import org.jdom.xpath.XPath; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.After; +import org.junit.Before; +import org.junit.runner.RunWith; + +import java.awt.*; +import java.io.File; +import java.io.IOException; + +import static com.intellij.ide.impl.ProjectUtil.closeAndDispose; +import static com.intellij.openapi.util.io.FileUtil.*; +import static com.intellij.openapi.util.io.FileUtilRt.delete; +import static com.intellij.openapi.vfs.VfsUtil.findFileByIoFile; +import static com.intellij.tests.gui.framework.GuiTestRunner.canRunGuiTests; +import static com.intellij.tests.gui.framework.GuiTests.*; +import static junit.framework.Assert.assertNotNull; +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.swing.edt.GuiActionRunner.execute; +import static org.fest.swing.timing.Pause.pause; +import static org.fest.util.Strings.quote; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + + +@RunWith(GuiTestRunner.class) +public abstract class GuiTestCase { + protected Robot myRobot; + + @SuppressWarnings("UnusedDeclaration") // This field is set via reflection. + private String myTestName; + + protected IdeFrameFixture myProjectFrame; + + /** + * @return the name of the test method being executed. + */ + protected String getTestName() { + return myTestName; + } + + @Before + public void setUp() throws Exception { + if (!canRunGuiTests()) { + // We currently do not support running UI tests in headless environments. + return; + } + + Application application = ApplicationManager.getApplication(); + assertNotNull(application); // verify that we are using the IDE's ClassLoader. + + setUpDefaultProjectCreationLocationPath(); + + myRobot = BasicRobot.robotWithCurrentAwtHierarchy(); + myRobot.settings().delayBetweenEvents(30); + + setIdeSettings(); + setUpSdks(); + + // There is a race condition between reloading the configuration file after file deletion detected and the serialization of IDEA model + // we just customized so that modules can't be loaded correctly. + // This is a hack to prevent StoreAwareProjectManager from doing any reloading during test. + ProjectManagerEx.getInstanceEx().blockReloadingProjectOnExternalChanges(); + + refreshFiles(); + } + + private static void setIdeSettings() { + + // Clear HTTP proxy settings, in case a test changed them. + HttpConfigurable ideSettings = HttpConfigurable.getInstance(); + ideSettings.USE_HTTP_PROXY = false; + ideSettings.PROXY_HOST = ""; + ideSettings.PROXY_PORT = 80; + + // TODO: setUpDefaultGeneralSettings(); + } + + @After + public void tearDown() { + if (myProjectFrame != null) { + myProjectFrame.waitForBackgroundTasksToFinish(); + } + if (myRobot != null) { + myRobot.cleanUpWithoutDisposingWindows(); + // We close all modal dialogs left over, because they block the AWT thread and could trigger a deadlock in the next test. + for (Window window : Window.getWindows()) { + if (window.isShowing() && window instanceof Dialog) { + if (((Dialog) window).getModalityType() == Dialog.ModalityType.APPLICATION_MODAL) { + fail("Modal dialog still active: " + window); + myRobot.close(window); + } + } + } + } + ProjectManagerEx.getInstanceEx().unblockReloadingProjectOnExternalChanges(); + } + + @NotNull + protected WelcomeFrameFixture findWelcomeFrame() { + return WelcomeFrameFixture.find(myRobot); + } + + @NotNull + protected NewProjectWizardFixture findNewProjectWizard() { + return NewProjectWizardFixture.find(myRobot); + } + + @NotNull + protected IdeFrameFixture findIdeFrame(@NotNull String projectName, @NotNull File projectPath) { + return IdeFrameFixture.find(myRobot, projectPath, projectName); + } + + @SuppressWarnings("UnusedDeclaration") + // Called by GuiTestRunner via reflection. + protected void closeAllProjects() { + pause(new Condition("Close all projects") { + @Override + public boolean test() { + final Project[] openProjects = ProjectManager.getInstance().getOpenProjects(); + execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + for (Project project : openProjects) { + assertTrue("Failed to close project " + quote(project.getName()), closeAndDispose(project)); + } + } + }); + return ProjectManager.getInstance().getOpenProjects().length == 0; + } + }, SHORT_TIMEOUT); + + //noinspection ConstantConditions + boolean welcomeFrameShown = execute(new GuiQuery() { + @Override + protected Boolean executeInEDT() throws Throwable { + Project[] openProjects = ProjectManager.getInstance().getOpenProjects(); + if (openProjects.length == 0) { + WelcomeFrame.showNow(); + + WindowManagerImpl windowManager = (WindowManagerImpl)WindowManager.getInstance(); + windowManager.disposeRootFrame(); + return true; + } + return false; + } + }); + + if (welcomeFrameShown) { + pause(new Condition("'Welcome' frame to show up") { + @Override + public boolean test() { + for (Frame frame : Frame.getFrames()) { + if (frame == WelcomeFrame.getInstance() && frame.isShowing()) { + return true; + } + } + return false; + } + }, SHORT_TIMEOUT); + } + } + + @NotNull + protected IdeFrameFixture importSimpleApplication() throws IOException { + return importProjectAndWaitForProjectSyncToFinish("SimpleApplication"); + } + + @NotNull + protected IdeFrameFixture importMultiModule() throws IOException { + return importProjectAndWaitForProjectSyncToFinish("MultiModule"); + } + + @NotNull + protected IdeFrameFixture importProjectAndWaitForProjectSyncToFinish(@NotNull String projectDirName) throws IOException { + return importProjectAndWaitForProjectSyncToFinish(projectDirName, null); + } + + @NotNull + protected IdeFrameFixture importProjectAndWaitForProjectSyncToFinish(@NotNull String projectDirName, @Nullable String gradleVersion) + throws IOException { + File projectPath = setUpProject(projectDirName, false); + VirtualFile toSelect = findFileByIoFile(projectPath, false); + assertNotNull(toSelect); + + doImportProject(toSelect); + + IdeFrameFixture projectFrame = findIdeFrame(projectPath); + //projectFrame.waitForGradleProjectSyncToFinish(); + + return projectFrame; + } + + @NotNull + protected File importProject(@NotNull String projectDirName) throws IOException { + File projectPath = setUpProject(projectDirName, false); + VirtualFile toSelect = findFileByIoFile(projectPath, false); + assertNotNull(toSelect); + doImportProject(toSelect); + return projectPath; + } + + private static void doImportProject(@NotNull final VirtualFile projectDir) { + execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + } + }); + } + + /** + * Sets up a project before using it in a UI test: + *
    + *
  • Makes a copy of the project in testData/guiTests/newProjects (deletes any existing copy of the project first.) This copy is + * the one the test will use.
  • + *
  • Creates a Gradle wrapper for the test project.
  • + *
  • Updates the version of the Android Gradle plug-in used by the project, if applicable
  • + *
  • Creates a local.properties file pointing to the Android SDK path specified by the system property (or environment variable) + * 'ADT_TEST_SDK_PATH'
  • + *
  • Copies over missing files to the .idea directory (if the project will be opened, instead of imported.)
  • + *
  • Deletes .idea directory, .iml files and build directories, if the project will be imported.
  • + *

    + *

+ * + * @param projectDirName the name of the project's root directory. Tests are located in testData/guiTests. + * @param forOpen indicates whether the project will be opened by the IDE, or imported. + * @param updateAndroidPluginVersion indicates if the latest supported version of the Android Gradle plug-in should be set in the + * project. + * @param gradleVersion the Gradle version to use in the wrapper. If {@code null} is passed, this method will use the latest supported + * version of Gradle. + * @return the path of project's root directory (the copy of the project, not the original one.) + * @throws IOException if an unexpected I/O error occurs. + */ + @NotNull + private File setUpProject(@NotNull String projectDirName, + boolean forOpen) throws IOException { + File projectPath = copyProjectBeforeOpening(projectDirName); + + updateLocalProperties(projectPath); + + if (forOpen) { + File toDotIdea = new File(projectPath, Project.DIRECTORY_STORE_FOLDER); + ensureExists(toDotIdea); + + File fromDotIdea = new File(getTestProjectsRootDirPath(), join("commonFiles", Project.DIRECTORY_STORE_FOLDER)); + assertThat(fromDotIdea).isDirectory(); + + for (File from : notNullize(fromDotIdea.listFiles())) { + if (from.isDirectory()) { + File destination = new File(toDotIdea, from.getName()); + if (!destination.isDirectory()) { + copyDirContent(from, destination); + } + continue; + } + File to = new File(toDotIdea, from.getName()); + if (!to.isFile()) { + copy(from, to); + } + } + } + else { + cleanUpProjectForImport(projectPath); + } + + return projectPath; + } + + @NotNull + protected File copyProjectBeforeOpening(@NotNull String projectDirName) throws IOException { + File masterProjectPath = getMasterProjectDirPath(projectDirName); + + File projectPath = getTestProjectDirPath(projectDirName); + if (projectPath.isDirectory()) { + delete(projectPath); + System.out.println(String.format("Deleted project path '%1$s'", projectPath.getPath())); + } + copyDir(masterProjectPath, projectPath); + System.out.println(String.format("Copied project '%1$s' to path '%2$s'", projectDirName, projectPath.getPath())); + return projectPath; + } + + + protected void updateLocalProperties(File projectPath) throws IOException { + //File androidHomePath = IdeSdks.getAndroidSdkPath(); + //assertNotNull(androidHomePath); + // + //LocalProperties localProperties = new LocalProperties(projectPath); + //localProperties.setAndroidSdkPath(androidHomePath); + //localProperties.save(); + } + + @NotNull + protected File getMasterProjectDirPath(@NotNull String projectDirName) { + return new File(getTestProjectsRootDirPath(), projectDirName); + } + + @NotNull + protected File getTestProjectDirPath(@NotNull String projectDirName) { + return new File(getProjectCreationDirPath(), projectDirName); + } + + protected void cleanUpProjectForImport(@NotNull File projectPath) { + File dotIdeaFolderPath = new File(projectPath, Project.DIRECTORY_STORE_FOLDER); + if (dotIdeaFolderPath.isDirectory()) { + File modulesXmlFilePath = new File(dotIdeaFolderPath, "modules.xml"); + if (modulesXmlFilePath.isFile()) { + SAXBuilder saxBuilder = new SAXBuilder(); + try { + Document document = saxBuilder.build(modulesXmlFilePath); + XPath xpath = XPath.newInstance("//*[@fileurl]"); + //noinspection unchecked + java.util.List modules = xpath.selectNodes(document); + int urlPrefixSize = "file://$PROJECT_DIR$/".length(); + for (Element module : modules) { + String fileUrl = module.getAttributeValue("fileurl"); + if (!StringUtil.isEmpty(fileUrl)) { + String relativePath = toSystemDependentName(fileUrl.substring(urlPrefixSize)); + File imlFilePath = new File(projectPath, relativePath); + if (imlFilePath.isFile()) { + delete(imlFilePath); + } + // It is likely that each module has a "build" folder. Delete it as well. + File buildFilePath = new File(imlFilePath.getParentFile(), "build"); + if (buildFilePath.isDirectory()) { + delete(buildFilePath); + } + } + } + } + catch (Throwable ignored) { + // if something goes wrong, just ignore. Most likely it won't affect project import in any way. + } + } + delete(dotIdeaFolderPath); + } + } + + @NotNull + protected IdeFrameFixture findIdeFrame(@NotNull File projectPath) { + return IdeFrameFixture.find(myRobot, projectPath, null); + } + + protected void refreshFiles() { + execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + LocalFileSystem.getInstance().refresh(false /* synchronous */); + } + }); + } + }); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/framework/GuiTestConfigurator.java b/community-tests/src/com/intellij/tests/gui/framework/GuiTestConfigurator.java new file mode 100644 index 000000000000..b322d94de992 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/framework/GuiTestConfigurator.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.framework; + +import com.google.common.collect.Maps; +import com.intellij.openapi.projectRoots.JavaSdk; +import com.intellij.openapi.projectRoots.JavaSdkVersion; +import com.intellij.openapi.projectRoots.Sdk; +import org.fest.reflect.reference.TypeRef; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Method; +import java.util.Map; + +import static org.fest.reflect.core.Reflection.method; +import static org.junit.Assert.assertNotNull; + +/** + * Collects configuration information from a UI test method's {@link IdeGuiTest} and {@link IdeGuiTestSetup} annotations and applies it + * to the test before execution, using the the IDE's {@code ClassLoader} (which is the {@code ClassLoader} used by UI tests, to be able to + * access IDE's services, state and components.) + */ +class GuiTestConfigurator { + private static final String CLOSE_PROJECT_BEFORE_EXECUTION_KEY = "closeProjectBeforeExecution"; + private static final String RETRY_COUNT_KEY = "retryCount"; + private static final String RUN_WITH_MINIMUM_JDK_VERSION_KEY = "runWithMinimumJdkVersion"; + private static final String SKIP_SOURCE_GENERATION_ON_SYNC_KEY = "skipSourceGenerationOnSync"; + private static final String TAKE_SCREENSHOT_ON_TEST_FAILURE_KEY = "takeScreenshotOnTestFailure"; + + @NotNull private final String myTestName; + @NotNull private final Object myTest; + @NotNull private final ClassLoader myClassLoader; + + @Nullable private final Object myMinimumJdkVersion; + + private final boolean myCloseProjectBeforeExecution; + private final int myRetryCount; + private final boolean mySkipSourceGenerationOnSync; + private final boolean myTakeScreenshotOnTestFailure; + + @NotNull + static GuiTestConfigurator createNew(@NotNull Method testMethod, @NotNull Object test) throws Throwable { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + Class target = classLoader.loadClass(GuiTestConfigurator.class.getCanonicalName()); + Map testConfig = method("extractTestConfiguration").withReturnType(new TypeRef>() {}) + .withParameterTypes(Method.class) + .in(target) + .invoke(testMethod); + assertNotNull(testConfig); + return new GuiTestConfigurator(testConfig, testMethod.getName(), test, classLoader); + } + + // Invoked using reflection and the IDE's ClassLoader. + @NotNull + private static Map extractTestConfiguration(@NotNull Method testMethod) { + Map config = Maps.newHashMap(); + IdeGuiTest guiTest = testMethod.getAnnotation(IdeGuiTest.class); + if (guiTest != null) { + config.put(CLOSE_PROJECT_BEFORE_EXECUTION_KEY, guiTest.closeProjectBeforeExecution()); + config.put(RUN_WITH_MINIMUM_JDK_VERSION_KEY, guiTest.runWithMinimumJdkVersion()); + config.put(RETRY_COUNT_KEY, guiTest.retryCount()); + } + IdeGuiTestSetup guiTestSetup = testMethod.getDeclaringClass().getAnnotation(IdeGuiTestSetup.class); + if (guiTestSetup != null) { + config.put(SKIP_SOURCE_GENERATION_ON_SYNC_KEY, guiTestSetup.skipSourceGenerationOnSync()); + config.put(TAKE_SCREENSHOT_ON_TEST_FAILURE_KEY, guiTestSetup.takeScreenshotOnTestFailure()); + } + return config; + } + + private GuiTestConfigurator(@NotNull Map configuration, + @NotNull String testName, + @NotNull Object test, + @NotNull ClassLoader classLoader) { + myCloseProjectBeforeExecution = getBooleanValue(CLOSE_PROJECT_BEFORE_EXECUTION_KEY, configuration, true); + myMinimumJdkVersion = getValue(RUN_WITH_MINIMUM_JDK_VERSION_KEY, configuration, JavaSdkVersion.class); + myRetryCount = getIntValue(RETRY_COUNT_KEY, configuration, 0); + mySkipSourceGenerationOnSync = getBooleanValue(SKIP_SOURCE_GENERATION_ON_SYNC_KEY, configuration, false); + myTakeScreenshotOnTestFailure = getBooleanValue(TAKE_SCREENSHOT_ON_TEST_FAILURE_KEY, configuration, true); + + myTestName = testName; + myTest = test; + myClassLoader = classLoader; + } + + private static boolean getBooleanValue(@NotNull String key, @NotNull Map configuration, boolean defaultValue) { + Object value = configuration.get(key); + if (value instanceof Boolean) { + return ((Boolean)value); + } + return defaultValue; + } + + @Nullable + private static Object getValue(@NotNull String key, @NotNull Map configuration, @NotNull Class type) { + Object value = configuration.get(key); + if (value != null && value.getClass().getCanonicalName().equals(type.getCanonicalName())) { + return value; + } + return null; + } + + private static int getIntValue(@NotNull String key, @NotNull Map configuration, int defaultValue) { + Object value = configuration.get(key); + if (value instanceof Integer) { + return ((Integer)value); + } + return defaultValue; + } + + void executeSetupTasks() throws Throwable { + closeAllProjects(); + skipSourceGenerationOnSync(); + } + + private void closeAllProjects() { + if (myCloseProjectBeforeExecution) { + method("closeAllProjects").in(myTest).invoke(); + } + } + + private void skipSourceGenerationOnSync() throws Throwable { + if (mySkipSourceGenerationOnSync) { + Class target = loadMyClassWithTestClassLoader(); + method("doSkipSourceGenerationOnSync").in(target).invoke(); + } + } + + // Invoked using reflection and the IDE's ClassLoader. + private static void doSkipSourceGenerationOnSync() { + System.out.println("Skipping source generation on project sync."); + } + + boolean shouldSkipTest() throws Throwable { + if (myMinimumJdkVersion != null) { + Class target = loadMyClassWithTestClassLoader(); + Class javaSdkVersionClass = myClassLoader.loadClass(JavaSdkVersion.class.getCanonicalName()); + Boolean hasRequiredJdk = method("hasRequiredJdk").withReturnType(boolean.class) + .withParameterTypes(javaSdkVersionClass) + .in(target) + .invoke(myMinimumJdkVersion); + assertNotNull(hasRequiredJdk); + if (!hasRequiredJdk) { + String jdkVersion = method("getDescription").withReturnType(String.class).in(myMinimumJdkVersion).invoke(); + System.out.println(String.format("Skipping test '%1$s'. It needs JDK %2$s or newer.", myTestName, jdkVersion)); + return true; + } + } + + return false; + } + + // Invoked using reflection and the IDE's ClassLoader. + private static boolean hasRequiredJdk(@NotNull JavaSdkVersion jdkVersion) { + //Sdk jdk = IdeSdks.getJdk(); + Sdk jdk = null; + assertNotNull("Expecting to have a JDK", jdk); + JavaSdkVersion currentVersion = JavaSdk.getInstance().getVersion(jdk); + return currentVersion != null && currentVersion.isAtLeast(jdkVersion); + } + + boolean shouldTakeScreenshotOnFailure() { + return myTakeScreenshotOnTestFailure; + } + + int getRetryCount() { + return myRetryCount; + } + + @NotNull + private Class loadMyClassWithTestClassLoader() throws ClassNotFoundException { + return myClassLoader.loadClass(GuiTestConfigurator.class.getCanonicalName()); + } +} diff --git a/community-tests/src/com/intellij/tests/gui/framework/GuiTestRunner.java b/community-tests/src/com/intellij/tests/gui/framework/GuiTestRunner.java new file mode 100755 index 000000000000..b87d765cb4f6 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/framework/GuiTestRunner.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.framework; + +import com.google.common.base.Strings; +import org.fest.swing.image.ScreenshotTaker; +import org.jetbrains.annotations.Nullable; +import org.junit.After; +import org.junit.Before; +import org.junit.internal.AssumptionViolatedException; +import org.junit.internal.runners.model.ReflectiveCallable; +import org.junit.internal.runners.statements.Fail; +import org.junit.internal.runners.statements.RunAfters; +import org.junit.internal.runners.statements.RunBefores; +import org.junit.runner.notification.Failure; +import org.junit.runner.notification.RunNotifier; +import org.junit.runners.BlockJUnit4ClassRunner; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.InitializationError; +import org.junit.runners.model.Statement; +import org.junit.runners.model.TestClass; + +import java.awt.*; +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.reflect.Method; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertNotNull; + +public class GuiTestRunner extends BlockJUnit4ClassRunner { + + private TestClass myTestClass; + + private final List myGarbageCollectorMXBeans = ManagementFactory.getGarbageCollectorMXBeans(); + private final MemoryMXBean myMemoryMXBean = ManagementFactory.getMemoryMXBean(); + + @Nullable private final ScreenshotTaker myScreenshotTaker; + + public GuiTestRunner(Class testClass) throws InitializationError { + super(testClass); + + myScreenshotTaker = canRunGuiTests() ? new ScreenshotTaker() : null; + + // UI_TEST_MODE is set whenever we run UI tests on top of a Studio build. In that case, we + // assume the classpath has been properly configured. Otherwise, if we're running from the + // IDE or an Ant build, we need to check we have access to the classpath of community-main. + if (Strings.isNullOrEmpty(System.getenv("UI_TEST_MODE"))) { + //try { + // A random class which is reachable from module community-main's classpath but not + // module android's classpath. + //Class.forName("git4idea.repo.GitConfig", false, testClass.getClassLoader()); + + //don't check for git module here + } + //catch (ClassNotFoundException e) { + // throw new InitializationError("Invalid test run configuration. Edit your test configuration and make sure that " + + // "\"Use classpath of module\" is set to \"community-main\", *NOT* \"android\"!"); + //} + //} + } + + @Override + protected void runChild(final FrameworkMethod method, RunNotifier notifier) { + if (!canRunGuiTests()) { + notifier.fireTestAssumptionFailed(new Failure(describeChild(method), new AssumptionViolatedException("Headless environment"))); + System.out.println(String.format("Skipping test '%1$s'. UI tests cannot run in a headless environment.", method.getName())); + } else if (MethodInvoker.doesIdeHaveFatalErrors()) { + notifier.fireTestIgnored(describeChild(method)); // TODO: can we restart the IDE at this point, instead of giving up? + System.out.println(String.format("Skipping test '%1$s': a fatal error has occurred in the IDE", method.getName())); + notifier.pleaseStop(); + } else { + printPerfStats(); + printTimestamp(); + super.runChild(method, notifier); + printTimestamp(); + printPerfStats(); + } + } + + private void printTimestamp() { + final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"); + System.out.println(dateFormat.format(new Date())); + } + + private void printPerfStats() { + long gcCount = 0, gcTime = 0; + for (GarbageCollectorMXBean garbageCollectorMXBean : myGarbageCollectorMXBeans) { + gcCount += garbageCollectorMXBean.getCollectionCount(); + gcTime += garbageCollectorMXBean.getCollectionTime(); + } + System.out.printf("%d garbage collections; cumulative %d ms%n", gcCount, gcTime); + myMemoryMXBean.gc(); + System.out.printf("heap: %s%n", myMemoryMXBean.getHeapMemoryUsage()); + System.out.printf("non-heap: %s%n", myMemoryMXBean.getNonHeapMemoryUsage()); + } + + @Override + protected Statement methodBlock(FrameworkMethod method) { + FrameworkMethod newMethod; + try { + loadClassesWithIdeClassLoader(); + Method methodFromClassLoader = myTestClass.getJavaClass().getMethod(method.getName()); + newMethod = new FrameworkMethod(methodFromClassLoader); + } + catch (Exception e) { + return new Fail(e); + } + Object test; + try { + test = new ReflectiveCallable() { + @Override + protected Object runReflectiveCall() throws Throwable { + return createTest(); + } + }.run(); + } + catch (Throwable e) { + return new Fail(e); + } + + Statement statement = methodInvoker(newMethod, test); + + List beforeMethods = myTestClass.getAnnotatedMethods(Before.class); + if (!beforeMethods.isEmpty()) { + statement = new RunBefores(statement, beforeMethods, test); + } + + List afterMethods = myTestClass.getAnnotatedMethods(After.class); + if (!afterMethods.isEmpty()) { + statement = new RunAfters(statement, afterMethods, test); + } + + return statement; + } + + public static boolean canRunGuiTests() { + return !GraphicsEnvironment.isHeadless(); + } + + private void loadClassesWithIdeClassLoader() throws Exception { + ClassLoader ideClassLoader = IdeTestApplication.getInstance().getIdeClassLoader(); + Thread.currentThread().setContextClassLoader(ideClassLoader); + + Class testClass = getTestClass().getJavaClass(); + myTestClass = new TestClass(ideClassLoader.loadClass(testClass.getName())); + } + + @Override + protected Object createTest() throws Exception { + return myTestClass != null ? myTestClass.getJavaClass().newInstance() : super.createTest(); + } + + @Override + protected Statement methodInvoker(final FrameworkMethod method, Object test) { + try { + assertNotNull(myScreenshotTaker); + return new MethodInvoker(method, test, myScreenshotTaker); + } + catch (Throwable e) { + return new Fail(e); + } + } +} diff --git a/community-tests/src/com/intellij/tests/gui/framework/GuiTests.java b/community-tests/src/com/intellij/tests/gui/framework/GuiTests.java new file mode 100644 index 000000000000..2f6a26c5e36e --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/framework/GuiTests.java @@ -0,0 +1,562 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.framework; + +import com.google.common.collect.Lists; +import org.fest.swing.core.Robot; +import com.intellij.diagnostic.AbstractMessage; +import com.intellij.diagnostic.MessagePool; +import com.intellij.ide.GeneralSettings; +import com.intellij.ide.RecentProjectsManager; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ex.PathManagerEx; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectManager; +import com.intellij.openapi.project.ProjectManagerAdapter; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.wm.IdeFrame; +import com.intellij.openapi.wm.impl.IdeFrameImpl; +import com.intellij.tests.gui.fixtures.IdeFrameFixture; +import com.intellij.ui.components.JBList; +import com.intellij.ui.popup.PopupFactoryImpl; +import com.intellij.ui.popup.list.ListPopupModel; +import org.fest.swing.core.BasicRobot; +import org.fest.swing.core.ComponentFinder; +import org.fest.swing.core.GenericTypeMatcher; +import org.fest.swing.edt.GuiActionRunner; +import org.fest.swing.edt.GuiQuery; +import org.fest.swing.edt.GuiTask; +import org.fest.swing.fixture.ContainerFixture; +import org.fest.swing.fixture.JListFixture; +import org.fest.swing.timing.Condition; +import org.fest.swing.timing.Pause; +import org.fest.swing.timing.Timeout; +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.KeyEvent; +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static com.google.common.base.Joiner.on; +import static com.google.common.io.Files.createTempDir; +import static com.intellij.openapi.projectRoots.JdkUtil.checkForJdk; +import static com.intellij.openapi.util.text.StringUtil.isNotEmpty; +import static com.intellij.util.containers.ContainerUtil.getFirstItem; +import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.fest.swing.edt.GuiActionRunner.execute; +import static org.fest.swing.finder.WindowFinder.findFrame; +import static org.fest.swing.timing.Pause.pause; +import static org.fest.swing.timing.Timeout.timeout; +import static org.fest.util.Strings.isNullOrEmpty; +import static org.fest.util.Strings.quote; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +public final class GuiTests { + public static final Timeout THIRTY_SEC_TIMEOUT = timeout(30, SECONDS); + public static final Timeout SHORT_TIMEOUT = timeout(2, MINUTES); + public static final Timeout LONG_TIMEOUT = timeout(5, MINUTES); + + public static final String GUI_TESTS_RUNNING_IN_SUITE_PROPERTY = "gui.tests.running.in.suite"; + + /** Environment variable pointing to the JDK to be used for tests */ + public static final String JDK_HOME_FOR_TESTS = "JDK_HOME_FOR_TESTS"; + + private static final EventQueue SYSTEM_EVENT_QUEUE = Toolkit.getDefaultToolkit().getSystemEventQueue(); + + private static final File TMP_PROJECT_ROOT = createTempProjectCreationDir(); + + // Called by MethodInvoker via reflection + @SuppressWarnings("unused") + public static void failIfIdeHasFatalErrors() { + final MessagePool messagePool = MessagePool.getInstance(); + List fatalErrors = messagePool.getFatalErrors(true, true); + int fatalErrorCount = fatalErrors.size(); + for (int i = 0; i < fatalErrorCount; i++) { + System.err.println("** Fatal Error " + (i + 1) + " of " + fatalErrorCount); + AbstractMessage error = fatalErrors.get(i); + System.err.println("* Message: "); + System.err.println(error.getMessage()); + + String additionalInfo = error.getAdditionalInfo(); + if (isNotEmpty(additionalInfo)) { + System.err.println("* Additional Info: "); + System.err.println(additionalInfo); + } + + String throwableText = error.getThrowableText(); + if (isNotEmpty(throwableText)) { + System.err.println("* Throwable: "); + System.err.println(throwableText); + } + System.err.println(); + } + if (fatalErrorCount > 0) { + throw new AssertionError(fatalErrorCount + " fatal errors found. Stopping test execution."); + } + } + + // Called by MethodInvoker via reflection + @SuppressWarnings("unused") + public static boolean doesIdeHaveFatalErrors() { + final MessagePool messagePool = MessagePool.getInstance(); + List fatalErrors = messagePool.getFatalErrors(true, true); + return !fatalErrors.isEmpty(); + } + + // Called by IdeTestApplication via reflection. + @SuppressWarnings("unused") + public static void setUpDefaultGeneralSettings() { + //setGuiTestingMode(true); + + GeneralSettings.getInstance().setShowTipsOnStartup(false); + setUpDefaultProjectCreationLocationPath(); + + setUpSdks(); + } + + public static void setUpSdks() { + + String jdkHome = getSystemPropertyOrEnvironmentVariable(JDK_HOME_FOR_TESTS); + if (isNullOrEmpty(jdkHome) || !checkForJdk(jdkHome)) { + fail("Please specify the path to a valid JDK using system property " + JDK_HOME_FOR_TESTS); + } + final File jdkPath = new File(jdkHome); + + execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + //File currentAndroidSdkPath = IdeSdks.getAndroidSdkPath(); + //File currentJdkPath = IdeSdks.getJdkPath(); + } + }); + + } + + @Nullable + public static File getGradleHomePath() { + return getFilePathProperty("supported.gradle.home.path", "the path of a local Gradle 2.2.1 distribution", true); + } + + @Nullable + public static File getUnsupportedGradleHome() { + return getGradleHomeFromSystemProperty("unsupported.gradle.home.path", "2.1"); + } + + @Nullable + public static File getGradleHomeFromSystemProperty(@NotNull String propertyName, @NotNull String gradleVersion) { + String description = "the path of a Gradle " + gradleVersion + " distribution"; + return getFilePathProperty(propertyName, description, true); + } + + + @Nullable + public static File getFilePathProperty(@NotNull String propertyName, + @NotNull String description, + boolean isDirectory) { + String pathValue = System.getProperty(propertyName); + if (!isNullOrEmpty(pathValue)) { + File path = new File(pathValue); + if (isDirectory && path.isDirectory() || !isDirectory && path.isFile()) { + return path; + } + } + System.out.println("Please specify " + description + ", using system property " + quote(propertyName)); + return null; + } + + public static void setUpDefaultProjectCreationLocationPath() { + RecentProjectsManager.getInstance().setLastProjectCreationLocation(getProjectCreationDirPath().getPath()); + } + + // Called by IdeTestApplication via reflection. + @SuppressWarnings("UnusedDeclaration") + public static void waitForIdeToStart() { + GuiActionRunner.executeInEDT(false); + Robot robot = null; + try { + robot = BasicRobot.robotWithCurrentAwtHierarchy(); + final MyProjectManagerListener listener = new MyProjectManagerListener(); + findFrame(new GenericTypeMatcher(Frame.class) { + @Override + protected boolean isMatching(@NotNull Frame frame) { + if (frame instanceof IdeFrame) { + if (frame instanceof IdeFrameImpl) { + listener.myActive = true; + ProjectManager.getInstance().addProjectManagerListener(listener); + } + return true; + } + return false; + } + }).withTimeout(LONG_TIMEOUT.duration()).using(robot); + + // We know the IDE event queue was pushed in front of the AWT queue. Some JDKs will leave a dummy event in the AWT queue, which + // we attempt to clear here. All other events, including those posted by the Robot, will go through the IDE event queue. + try { + if (SYSTEM_EVENT_QUEUE.peekEvent() != null) { + SYSTEM_EVENT_QUEUE.getNextEvent(); + } + } catch (InterruptedException ex ) { + // Ignored. + } + + if (listener.myActive) { + pause(new Condition("Project to be opened") { + @Override + public boolean test() { + boolean notified = listener.myNotified; + if (notified) { + ProgressManager progressManager = ProgressManager.getInstance(); + boolean isIdle = !progressManager.hasModalProgressIndicator() && + !progressManager.hasProgressIndicator() && + !progressManager.hasUnsafeProgressIndicator(); + if (isIdle) { + ProjectManager.getInstance().removeProjectManagerListener(listener); + } + return isIdle; + } + return false; + } + }, LONG_TIMEOUT); + } + } + finally { + GuiActionRunner.executeInEDT(true); + if (robot != null) { + robot.cleanUpWithoutDisposingWindows(); + } + } + } + + @NotNull + public static File getProjectCreationDirPath() { + return TMP_PROJECT_ROOT; + } + + @NotNull + public static File createTempProjectCreationDir() { + try { + // The temporary location might contain symlinks, such as /var@ -> /private/var on MacOS. + // EditorFixture seems to require a canonical path when opening the file. + return createTempDir().getCanonicalFile(); + } + catch (IOException ex) { + // For now, keep the original behavior and point inside the source tree. + ex.printStackTrace(); + return new File(getTestProjectsRootDirPath(), "newProjects"); + } + } + + @NotNull + public static File getTestProjectsRootDirPath() { + + final String path = PathManagerEx.getTestDataPath(GuiTestCase.class); + return new File(path); + + //String testDataPath = AndroidTestBase.getTestDataPath(); + //assertNotNull(testDataPath); + //assertThat(testDataPath).isNotEmpty(); + //testDataPath = toCanonicalPath(toSystemDependentName(testDataPath)); + //return new File(testDataPath, "guiTests"); + } + + private GuiTests() { + } + + public static void deleteFile(@Nullable final VirtualFile file) { + // File deletion must happen on UI thread under write lock + if (file != null) { + execute(new GuiTask() { + @Override + protected void executeInEDT() throws Throwable { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + try { + file.delete(this); + } + catch (IOException e) { + // ignored + } + } + }); + } + }); + } + } + + /** Waits until an IDE popup is shown (and returns it */ + public static JBList waitForPopup(@NotNull Robot robot) { + return waitUntilFound(robot, null, new GenericTypeMatcher(JBList.class) { + @Override + protected boolean isMatching(@NotNull JBList list) { + ListModel model = list.getModel(); + return model instanceof ListPopupModel; + } + }); + } + + /** + * Clicks an IntelliJ/Studio popup menu item with the given label prefix + * + * @param labelPrefix the target menu item label prefix + * @param component a component in the same window that the popup menu is associated with + * @param robot the robot to drive it with + */ + public static void clickPopupMenuItem(@NotNull String labelPrefix, @NotNull Component component, @NotNull Robot robot) { + clickPopupMenuItemMatching(new PrefixMatcher(labelPrefix), component, robot); + } + + public static void clickPopupMenuItemMatching(@NotNull Matcher labelMatcher, @NotNull Component component, @NotNull Robot robot) { + // IntelliJ doesn't seem to use a normal JPopupMenu, so this won't work: + // JPopupMenu menu = myRobot.findActivePopupMenu(); + // Instead, it uses a JList (technically a JBList), which is placed somewhere + // under the root pane. + + Container root = getRootContainer(component); + + // First find the JBList which holds the popup. There could be other JBLists in the hierarchy, + // so limit it to one that is actually used as a popup, as identified by its model being a ListPopupModel: + assertNotNull(root); + JBList list = robot.finder().find(root, new GenericTypeMatcher(JBList.class) { + @Override + protected boolean isMatching(@NotNull JBList list) { + ListModel model = list.getModel(); + return model instanceof ListPopupModel; + } + }); + + // We can't use the normal JListFixture method to click by label since the ListModel items are + // ActionItems whose toString does not reflect the text, so search through the model items instead: + ListPopupModel model = (ListPopupModel)list.getModel(); + List items = Lists.newArrayList(); + for (int i = 0; i < model.getSize(); i++) { + Object elementAt = model.getElementAt(i); + if (elementAt instanceof PopupFactoryImpl.ActionItem) { + PopupFactoryImpl.ActionItem item = (PopupFactoryImpl.ActionItem)elementAt; + String s = item.getText(); + if (labelMatcher.matches(s)) { + new JListFixture(robot, list).clickItem(i); + return; + } + items.add(s); + } else { // For example package private class IntentionActionWithTextCaching used in quickfix popups + String s = elementAt.toString(); + if (labelMatcher.matches(s)) { + new JListFixture(robot, list).clickItem(i); + return; + } + items.add(s); + } + } + + if (items.isEmpty()) { + fail("Could not find any menu items in popup"); + } + fail("Did not find menu item '" + labelMatcher + "' among " + on(", ").join(items)); + } + + /** Returns the root container containing the given component */ + @Nullable + public static Container getRootContainer(@NotNull final Component component) { + return execute(new GuiQuery() { + @Override + @Nullable + protected Container executeInEDT() throws Throwable { + return (Container)SwingUtilities.getRoot(component); + } + }); + } + + public static void findAndClickOkButton(@NotNull ContainerFixture container) { + findAndClickButton(container, "OK"); + } + + public static void findAndClickCancelButton(@NotNull ContainerFixture container) { + findAndClickButton(container, "Cancel"); + } + + public static void findAndClickButton(@NotNull ContainerFixture container, @NotNull final String text) { + Robot robot = container.robot(); + JButton button = findButton(container, text, robot); + robot.click(button); + } + + public static void findAndClickButtonWhenEnabled(@NotNull ContainerFixture container, @NotNull final String text) { + Robot robot = container.robot(); + final JButton button = findButton(container, text, robot); + pause(new Condition("Wait for button " + text + " to be enabled.") { + @Override + public boolean test() { + return button.isEnabled() && button.isVisible() && button.isShowing(); + } + }, SHORT_TIMEOUT); + robot.click(button); + } + + public static void invokeMenuPathOnRobotIdle(IdeFrameFixture projectFrame, String... path) { + projectFrame.robot().waitForIdle(); + projectFrame.invokeMenuPath(path); + } + + /** + * Opens the file with basename {@code fileBasename} + */ + public static void openFile(IdeFrameFixture projectFrame, String fileBasename) { + invokeMenuPathOnRobotIdle(projectFrame, "Navigate", "File..."); + projectFrame.robot().waitForIdle(); + typeText("multifunction-jni.c", projectFrame.robot(), 30); + projectFrame.robot().pressAndReleaseKey(KeyEvent.VK_ENTER); + } + + /** + * Navigates to line number {@code lineNum} of the currently active editor window. + */ + public static void navigateToLine(IdeFrameFixture projectFrame, int lineNum) { + invokeMenuPathOnRobotIdle(projectFrame, "Navigate", "Line..."); + projectFrame.robot().enterText(Integer.toString(lineNum)); + projectFrame.robot().waitForIdle(); + projectFrame.robot().pressAndReleaseKey(KeyEvent.VK_ENTER); + } + + private static void typeText(String text, Robot robot, long delayAfterEachCharacterMillis) { + robot.waitForIdle(); + for (int i = 0; i < text.length(); ++i) { + robot.type(text.charAt(i)); + Pause.pause(delayAfterEachCharacterMillis, TimeUnit.MILLISECONDS); + } + } + + @NotNull + private static JButton findButton(@NotNull ContainerFixture container, @NotNull final String text, Robot robot) { + return robot.finder().find(container.target(), new GenericTypeMatcher(JButton.class) { + @Override + protected boolean isMatching(@NotNull JButton button) { + String buttonText = button.getText(); + if (buttonText != null) { + return buttonText.trim().equals(text) && button.isShowing(); + } + return false; + } + }); + } + + /** Returns a full path to the GUI data directory in the user's AOSP source tree, if known, or null */ + //@Nullable + //public static File getTestDataDir() { + // File aosp = getAospSourceDir(); + // return aosp != null ? new File(aosp, RELATIVE_DATA_PATH) : null; + //} + + + /** Waits for a first component which passes the given matcher to become visible */ + @NotNull + public static T waitUntilFound(@NotNull final Robot robot, @NotNull final GenericTypeMatcher matcher) { + return waitUntilFound(robot, null, matcher); + } + + public static void skip(@NotNull String testName) { + System.out.println("Skipping test '" + testName + "'"); + } + + /** Waits for a first component which passes the given matcher under the given root to become visible. */ + @NotNull + public static T waitUntilFound(@NotNull final Robot robot, + @Nullable final Container root, + @NotNull final GenericTypeMatcher matcher) { + final AtomicReference reference = new AtomicReference(); + pause(new Condition("Find component using " + matcher.toString()) { + @Override + public boolean test() { + ComponentFinder finder = robot.finder(); + Collection allFound = root != null ? finder.findAll(root, matcher) : finder.findAll(matcher); + boolean found = allFound.size() == 1; + if (found) { + reference.set(getFirstItem(allFound)); + } + else if (allFound.size() > 1) { + // Only allow a single component to be found, otherwise you can get some really confusing + // test failures; the matcher should pick a specific enough instance + fail("Found more than one " + matcher.supportedType().getSimpleName() + " which matches the criteria: " + allFound); + } + return found; + } + }, SHORT_TIMEOUT); + + return reference.get(); + } + + /** Waits until no components match the given criteria under the given root */ + public static void waitUntilGone(@NotNull final Robot robot, + @NotNull final Container root, + @NotNull final GenericTypeMatcher matcher) { + pause(new Condition("Find component using " + matcher.toString()) { + @Override + public boolean test() { + Collection allFound = robot.finder().findAll(root, matcher); + return allFound.isEmpty(); + } + }, SHORT_TIMEOUT); + } + + @Nullable + public static String getSystemPropertyOrEnvironmentVariable(@NotNull String name) { + String s = System.getProperty(name); + return s == null ? System.getenv(name) : s; + } + + private static class MyProjectManagerListener extends ProjectManagerAdapter { + boolean myActive; + boolean myNotified; + + @Override + public void projectOpened(Project project) { + myNotified = true; + } + } + + private static class PrefixMatcher extends BaseMatcher { + + private final String prefix; + + public PrefixMatcher(String prefix) { + this.prefix = prefix; + } + + @Override + public boolean matches(Object item) { + return item instanceof String && ((String)item).startsWith(prefix); + } + + @Override + public void describeTo(Description description) { + description.appendText("with prefix '" + prefix +"'"); + } + } +} diff --git a/community-tests/src/com/intellij/tests/gui/framework/IdeGuiTest.java b/community-tests/src/com/intellij/tests/gui/framework/IdeGuiTest.java new file mode 100644 index 000000000000..3aeead16e0d1 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/framework/IdeGuiTest.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.framework; + +import com.intellij.openapi.projectRoots.JavaSdkVersion; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static com.intellij.openapi.projectRoots.JavaSdkVersion.JDK_1_6; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface IdeGuiTest { + boolean closeProjectBeforeExecution() default true; + int retryCount() default 0; + JavaSdkVersion runWithMinimumJdkVersion() default JDK_1_6; +} diff --git a/community-tests/src/com/intellij/tests/gui/framework/IdeGuiTestSetup.java b/community-tests/src/com/intellij/tests/gui/framework/IdeGuiTestSetup.java new file mode 100644 index 000000000000..08e0d34d0e54 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/framework/IdeGuiTestSetup.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.framework; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Settings to be applied when executing every test method in a class. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface IdeGuiTestSetup { + boolean skipSourceGenerationOnSync() default false; + boolean takeScreenshotOnTestFailure() default true; +} diff --git a/community-tests/src/com/intellij/tests/gui/framework/IdeTestApplication.java b/community-tests/src/com/intellij/tests/gui/framework/IdeTestApplication.java new file mode 100644 index 000000000000..535f2d243184 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/framework/IdeTestApplication.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.framework; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import com.intellij.ide.BootstrapClassLoaderUtil; +import com.intellij.ide.WindowsCommandLineProcessor; +import com.intellij.ide.startup.StartupActionScriptManager; +import com.intellij.idea.Main; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.Application; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.PathManager; +import com.intellij.openapi.application.ex.ApplicationEx; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.util.SystemProperties; +import com.intellij.util.lang.UrlClassLoader; +import com.intellij.util.text.StringTokenizer; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.regex.Pattern; + +import static com.intellij.openapi.application.PathManager.PROPERTY_CONFIG_PATH; +import static com.intellij.openapi.util.io.FileUtil.*; +import static com.intellij.openapi.util.text.StringUtil.isNotEmpty; +import static com.intellij.tests.gui.framework.GuiTests.getProjectCreationDirPath; +import static com.intellij.util.ArrayUtil.EMPTY_STRING_ARRAY; +import static com.intellij.util.ui.UIUtil.initDefaultLAF; +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.reflect.core.Reflection.method; +import static org.junit.Assert.assertNotNull; + +public class IdeTestApplication implements Disposable { + private static final Logger LOG = Logger.getInstance(IdeTestApplication.class); + private static final String PROPERTY_IGNORE_CLASSPATH = "ignore.classpath"; + private static final String PROPERTY_ALLOW_BOOTSTRAP_RESOURCES = "idea.allow.bootstrap.resources"; + private static final String PROPERTY_ADDITIONAL_CLASSPATH = "idea.additional.classpath"; + + private static IdeTestApplication ourInstance; + + @NotNull private final ClassLoader myIdeClassLoader; + + @NotNull + public static synchronized IdeTestApplication getInstance() throws Exception { + //System.setProperty(PLATFORM_PREFIX_KEY, "AndroidStudio"); + //System.setProperty(PLATFORM_PREFIX_KEY, "idea"); + File configDirPath = getConfigDirPath(); + System.setProperty(PROPERTY_CONFIG_PATH, configDirPath.getPath()); + + // Force Swing FileChooser on Mac (instead of native one) to be able to use FEST to drive it. + System.setProperty("native.mac.file.chooser.enabled", "false"); + + if (!isLoaded()) { + ourInstance = new IdeTestApplication(); + recreateDirectory(configDirPath); + + File newProjectsRootDirPath = getProjectCreationDirPath(); + recreateDirectory(newProjectsRootDirPath); + + ClassLoader ideClassLoader = ourInstance.getIdeClassLoader(); + Class clazz = ideClassLoader.loadClass(GuiTests.class.getCanonicalName()); + method("waitForIdeToStart").in(clazz).invoke(); + method("setUpDefaultGeneralSettings").in(clazz).invoke(); + } + + return ourInstance; + } + + @NotNull + private static File getConfigDirPath() throws IOException { + File dirPath = new File(getGuiTestRootDirPath(), "config"); + ensureExists(dirPath); + return dirPath; + } + + @NotNull + public static File getFailedTestScreenshotDirPath() throws IOException { + File dirPath = new File(getGuiTestRootDirPath(), "failures"); + ensureExists(dirPath); + return dirPath; + } + + @NotNull + private static File getGuiTestRootDirPath() throws IOException { + String guiTestRootDirPathProperty = System.getProperty("gui.tests.root.dir.path"); + if (isNotEmpty(guiTestRootDirPathProperty)) { + File rootDirPath = new File(guiTestRootDirPathProperty); + if (rootDirPath.isDirectory()) { + return rootDirPath; + } + } + String homeDirPath = toSystemDependentName(PathManager.getHomePath()); + assertThat(homeDirPath).isNotEmpty(); + File rootDirPath = new File(homeDirPath, join("androidStudio", "gui-tests")); + ensureExists(rootDirPath); + return rootDirPath; + } + + private static void recreateDirectory(@NotNull File path) throws IOException { + delete(path); + ensureExists(path); + } + + private IdeTestApplication() throws Exception { + String[] args = EMPTY_STRING_ARRAY; + + LOG.assertTrue(ourInstance == null, "Only one instance allowed."); + ourInstance = this; + + pluginManagerStart(args); + mainMain(); + + myIdeClassLoader = createClassLoader(); + //myIdeClassLoader = BootstrapClassLoaderUtil.initClassLoader(false); + + + WindowsCommandLineProcessor.ourMirrorClass = Class.forName(WindowsCommandLineProcessor.class.getName(), true, myIdeClassLoader); + + // We turn on "GUI Testing Mode" right away, even before loading the IDE. + //Class androidPluginClass = Class.forName("org.jetbrains.android.AndroidPlugin", true, myIdeClassLoader); + //method("setGuiTestingMode").withParameterTypes(boolean.class).in(androidPluginClass).invoke(true); + + Class classUtilCoreClass = Class.forName("com.intellij.ide.ClassUtilCore", true, myIdeClassLoader); + method("clearJarURLCache").in(classUtilCoreClass).invoke(); + + Class pluginManagerClass = Class.forName("com.intellij.ide.plugins.PluginManager", true, myIdeClassLoader); + method("start").withParameterTypes(String.class, String.class, String[].class) + .in(pluginManagerClass) + .invoke("com.intellij.idea.MainImpl", "start", args); + } + + // This method replaces BootstrapClassLoaderUtil.initClassLoader. The reason behind it is that when running UI tests the ClassLoader + // containing the URLs for the plugin jars is loaded by a different ClassLoader and it gets ignored. The result is test failing because + // classes like AndroidPlugin cannot be found. + @NotNull + private static ClassLoader createClassLoader() throws MalformedURLException, URISyntaxException { + Collection classpath = Sets.newLinkedHashSet(); + addIdeaLibraries(classpath); + addAdditionalClassPath(classpath); + + UrlClassLoader.Builder builder = UrlClassLoader.build() + .urls(filterClassPath(Lists.newArrayList(classpath))) + .parent(IdeTestApplication.class.getClassLoader()) + .allowLock(false) + .usePersistentClasspathIndexForLocalClassDirectories(); + if (SystemProperties.getBooleanProperty(PROPERTY_ALLOW_BOOTSTRAP_RESOURCES, true)) { + builder.allowBootstrapResources(); + } + + UrlClassLoader newClassLoader = builder.get(); + + // prepare plugins + try { + StartupActionScriptManager.executeActionScript(); + } + catch (IOException e) { + Main.showMessage("Plugin Installation Error", e); + } + + Thread.currentThread().setContextClassLoader(newClassLoader); + return newClassLoader; + } + + private static void addIdeaLibraries(@NotNull Collection classpath) throws MalformedURLException { + Class aClass = BootstrapClassLoaderUtil.class; + String selfRoot = PathManager.getResourceRoot(aClass, "/" + aClass.getName().replace('.', '/') + ".class"); + assertNotNull(selfRoot); + + URL selfRootUrl = new File(selfRoot).getAbsoluteFile().toURI().toURL(); + classpath.add(selfRootUrl); + + File libFolder = new File(PathManager.getLibPath()); + addLibraries(classpath, libFolder, selfRootUrl); + addLibraries(classpath, new File(libFolder, "ext"), selfRootUrl); + addLibraries(classpath, new File(libFolder, "ant/lib"), selfRootUrl); + } + + private static void addLibraries(@NotNull Collection classPath, @NotNull File fromDir, @NotNull URL selfRootUrl) + throws MalformedURLException { + for (File file : notNullize(fromDir.listFiles())) { + if (isJarOrZip(file)) { + URL url = file.toURI().toURL(); + if (!selfRootUrl.equals(url)) { + classPath.add(url); + } + } + } + } + + private static void addAdditionalClassPath(@NotNull Collection classpath) throws MalformedURLException { + StringTokenizer tokenizer = new StringTokenizer(System.getProperty(PROPERTY_ADDITIONAL_CLASSPATH, ""), File.pathSeparator, false); + while (tokenizer.hasMoreTokens()) { + String pathItem = tokenizer.nextToken(); + classpath.add(new File(pathItem).toURI().toURL()); + } + } + + private static List filterClassPath(@NotNull List classpath) { + String ignoreProperty = System.getProperty(PROPERTY_IGNORE_CLASSPATH); + if (ignoreProperty != null) { + Pattern pattern = Pattern.compile(ignoreProperty); + for (Iterator i = classpath.iterator(); i.hasNext(); ) { + String url = i.next().toExternalForm(); + if (pattern.matcher(url).matches()) { + i.remove(); + } + } + } + return classpath; + } + + private static void pluginManagerStart(@NotNull String[] args) { + // Duplicates what PluginManager#start does. + Main.setFlags(args); + initDefaultLAF(); + } + + private static void mainMain() { + // Duplicates what Main#main does. + method("installPatch").in(Main.class).invoke(); + } + + @NotNull + public ClassLoader getIdeClassLoader() { + return myIdeClassLoader; + } + + @Override + public void dispose() { + disposeInstance(); + } + + public static synchronized void disposeInstance() { + if (!isLoaded()) { + return; + } + Application application = ApplicationManager.getApplication(); + if (application != null) { + if (application instanceof ApplicationEx) { + ((ApplicationEx)application).exit(true, true); + } + else { + application.exit(); + } + } + ourInstance = null; + } + + public static synchronized boolean isLoaded() { + return ourInstance != null; + } +} diff --git a/community-tests/src/com/intellij/tests/gui/framework/MethodInvoker.java b/community-tests/src/com/intellij/tests/gui/framework/MethodInvoker.java new file mode 100644 index 000000000000..088d71a9f933 --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/framework/MethodInvoker.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.framework; + +import org.fest.swing.image.ScreenshotTaker; +import org.jetbrains.annotations.NotNull; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.Statement; + +import java.io.File; +import java.lang.reflect.Method; +import java.text.SimpleDateFormat; +import java.util.GregorianCalendar; + +import static com.intellij.tests.gui.framework.GuiTestRunner.canRunGuiTests; +import static com.intellij.tests.gui.framework.IdeTestApplication.getFailedTestScreenshotDirPath; +import static org.fest.reflect.core.Reflection.field; +import static org.fest.reflect.core.Reflection.method; + +public class MethodInvoker extends Statement { + @NotNull private final GuiTestConfigurator myTestConfigurator; + @NotNull private final FrameworkMethod myTestMethod; + @NotNull private final Object myTest; + @NotNull private final ScreenshotTaker myScreenshotTaker; + + MethodInvoker(@NotNull FrameworkMethod testMethod, @NotNull Object test, @NotNull ScreenshotTaker screenshotTaker) throws Throwable { + myTestConfigurator = GuiTestConfigurator.createNew(testMethod.getMethod(), test); + myTestMethod = testMethod; + myTest = test; + myScreenshotTaker = screenshotTaker; + } + + @Override + public void evaluate() throws Throwable { + //if (myTestConfigurator.shouldSkipTest()) { + // Message already printed in console. + //return; + //} + String testFqn = getTestFqn(); + if (doesIdeHaveFatalErrors()) { + // Fatal errors were caused by previous test. Skipping this test. + System.out.println(String.format("Skipping test '%1$s': a fatal error has occurred in the IDE", testFqn)); + return; + } + System.out.println(String.format("Executing test '%1$s'", testFqn)); + + int retryCount = myTestConfigurator.getRetryCount(); + for (int i = 0; i <= retryCount; i++) { + if (i > 0) { + System.out.println(String.format("Retrying execution of test '%1$s'", testFqn)); + } + try { + runTest(i); + break; // no need to retry. + } + catch (Throwable throwable) { + if (retryCount == i) { + throw throwable; // Last run, throw any exceptions caught. + } + else { + throwable.printStackTrace(); + failIfIdeHasFatalErrors(); + } + } + } + failIfIdeHasFatalErrors(); + } + + public static boolean doesIdeHaveFatalErrors() { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + try { + Class guiTestsType = Class.forName(GuiTests.class.getCanonicalName(), true, classLoader); + //noinspection ConstantConditions + return method("doesIdeHaveFatalErrors").withReturnType(boolean.class).in(guiTestsType).invoke(); + } catch (ClassNotFoundException ex) { + // ignore exception + return true; + } + } + + private static void failIfIdeHasFatalErrors() throws ClassNotFoundException { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + Class guiTestsType = Class.forName(GuiTests.class.getCanonicalName(), true, classLoader); + method("failIfIdeHasFatalErrors").in(guiTestsType).invoke(); + } + + private void runTest(int executionIndex) throws Throwable { + myTestConfigurator.executeSetupTasks(); + + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + Class guiTestCaseType = Class.forName(GuiTestCase.class.getCanonicalName(), true, classLoader); + + if (guiTestCaseType.isInstance(myTest)) { + if (!canRunGuiTests()) { + // We don't run tests in headless environment. + return; + } + field("myTestName").ofType(String.class).in(myTest).set(myTestMethod.getName()); + } + try { + myTestMethod.invokeExplosively(myTest); + } + catch (Throwable e) { + e.printStackTrace(); + takeScreenshot(executionIndex); + throw e; + } + } + + @NotNull + private String getTestFqn() { + return myTestMethod.getMethod().getDeclaringClass() + "#" + myTestMethod.getName(); + } + + private void takeScreenshot(int executionIndex) { + if (myTestConfigurator.shouldTakeScreenshotOnFailure()) { + Method method = myTestMethod.getMethod(); + String fileNamePrefix = method.getDeclaringClass().getSimpleName() + "." + (executionIndex + 1) + "." + method.getName(); + String extension = ".png"; + + try { + File rootDir = getFailedTestScreenshotDirPath(); + + File screenshotFilePath = new File(rootDir, fileNamePrefix + extension); + if (screenshotFilePath.isFile()) { + SimpleDateFormat format = new SimpleDateFormat("MM-dd-yyyy.HH:mm:ss"); + String now = format.format(new GregorianCalendar().getTime()); + screenshotFilePath = new File(rootDir, fileNamePrefix + "." + now + extension); + } + myScreenshotTaker.saveDesktopAsPng(screenshotFilePath.getPath()); + System.out.println("Screenshot of failed test taken and stored at " + screenshotFilePath.getPath()); + } + catch (Throwable ignored) { + System.out.println("Failed to take screenshot. Cause: " + ignored.getMessage()); + } + } + } +} diff --git a/community-tests/src/com/intellij/tests/gui/matcher/ClassNameMatcher.java b/community-tests/src/com/intellij/tests/gui/matcher/ClassNameMatcher.java new file mode 100644 index 000000000000..9a0388ed014d --- /dev/null +++ b/community-tests/src/com/intellij/tests/gui/matcher/ClassNameMatcher.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.tests.gui.matcher; + +import org.fest.swing.core.GenericTypeMatcher; +import org.jetbrains.annotations.NotNull; + +import java.awt.*; + +/** + * Matcher that checks the name of the actual implementation. This is useful when matching + * against non-public IJ classes. + */ +public class ClassNameMatcher extends GenericTypeMatcher { + private final String myClassName; + + private ClassNameMatcher(String className, Class supportedType) { + super(supportedType); + myClassName = className; + } + + private ClassNameMatcher(String className, Class supportedType, boolean requireShowing) { + super(supportedType, requireShowing); + myClassName = className; + } + + @Override + protected boolean isMatching(@NotNull T component) { + return myClassName.equals(component.getClass().getName()); + } + + public static ClassNameMatcher forClass(String className, Class supportedType) { + return new ClassNameMatcher(className, supportedType); + } + + public static ClassNameMatcher forClass(String className, Class supportedType, boolean requireShowing) { + return new ClassNameMatcher(className, supportedType, requireShowing); + } +}