/* * 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.android.tools.idea.tests.gui.framework; import com.android.SdkConstants; import com.android.tools.idea.gradle.project.GradleProjectImporter; import com.android.tools.idea.gradle.util.LocalProperties; import com.android.tools.idea.sdk.DefaultSdks; import com.android.tools.idea.tests.gui.framework.fixture.FileChooserDialogFixture; import com.android.tools.idea.tests.gui.framework.fixture.IdeFrameFixture; import com.android.tools.idea.tests.gui.framework.fixture.WelcomeFrameFixture; import com.android.tools.idea.tests.gui.framework.fixture.newProjectWizard.NewProjectWizardFixture; 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.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.util.SystemProperties; import org.fest.swing.core.BasicRobot; 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.jdom.Document; import org.jdom.Element; import org.jdom.input.SAXBuilder; import org.jdom.xpath.XPath; import org.jetbrains.android.AndroidPlugin; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.runner.RunWith; import java.awt.*; import java.io.File; import java.io.IOException; import java.util.List; import static com.android.tools.idea.gradle.util.GradleUtil.createGradleWrapper; import static com.android.tools.idea.templates.AndroidGradleTestCase.updateGradleVersions; import static com.android.tools.idea.tests.gui.framework.GuiTestRunner.canRunGuiTests; import static com.android.tools.idea.tests.gui.framework.GuiTests.*; import static com.intellij.ide.impl.ProjectUtil.closeAndDispose; import static com.intellij.openapi.project.ProjectCoreUtil.DIRECTORY_BASED_PROJECT_DIR; 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.openapi.vfs.VfsUtilCore.virtualToIoFile; import static junit.framework.Assert.assertNotNull; import static org.fest.assertions.Assertions.assertThat; import static org.fest.swing.timing.Pause.pause; import static org.fest.util.Strings.quote; import static org.junit.Assert.assertTrue; @RunWith(GuiTestRunner.class) public abstract class GuiTestCase { protected Robot myRobot; @SuppressWarnings("UnusedDeclaration") // This field is set via reflection. private String myTestName; /** * @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); } @After public void tearDown() { if (myRobot != null) { myRobot.cleanUpWithoutDisposingWindows(); } } @AfterClass public static void tearDownPerClass() { boolean inSuite = SystemProperties.getBooleanProperty(GUI_TESTS_RUNNING_IN_SUITE_PROPERTY, false); if (!inSuite) { IdeTestApplication.disposeInstance(); } } @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(); GuiActionRunner.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); boolean welcomeFrameShown = GuiActionRunner.execute(new GuiQuery<Boolean>() { @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 instanceof WelcomeFrame && frame.isShowing()) { return true; } } return false; } }, SHORT_TIMEOUT); } } @NotNull protected IdeFrameFixture importProject(@NotNull String projectDirName) throws IOException { File projectPath = setUpProject(projectDirName, false, true, null); final VirtualFile toSelect = findFileByIoFile(projectPath, true); assertNotNull(toSelect); AndroidPlugin.GuiTestSuiteState testSuiteState = getTestSuiteState(); if (!testSuiteState.isImportProjectWizardAlreadyTested()) { testSuiteState.setImportProjectWizardAlreadyTested(true); findWelcomeFrame().clickImportProjectButton(); FileChooserDialogFixture importProjectDialog = FileChooserDialogFixture.findImportProjectDialog(myRobot); return openProjectAndWaitUntilOpened(toSelect, importProjectDialog); } GuiActionRunner.execute(new GuiTask() { @Override protected void executeInEDT() throws Throwable { GradleProjectImporter.getInstance().importProject(toSelect); } }); IdeFrameFixture projectFrame = findIdeFrame(projectPath); projectFrame.waitForGradleProjectSyncToFinish(); return projectFrame; } @NotNull protected IdeFrameFixture openSimpleApplication() throws IOException { return openProject("SimpleApplication"); } @NotNull protected IdeFrameFixture openProject(@NotNull String projectDirName) throws IOException { File projectPath = setUpProject(projectDirName, true, true, null); return openProject(projectPath); } @NotNull protected IdeFrameFixture openProject(@NotNull final File projectPath) { VirtualFile toSelect = findFileByIoFile(projectPath, true); assertNotNull(toSelect); AndroidPlugin.GuiTestSuiteState state = getTestSuiteState(); if (!state.isOpenProjectWizardAlreadyTested()) { state.setOpenProjectWizardAlreadyTested(true); findWelcomeFrame().clickOpenProjectButton(); FileChooserDialogFixture openProjectDialog = FileChooserDialogFixture.findOpenProjectDialog(myRobot); return openProjectAndWaitUntilOpened(toSelect, openProjectDialog); } GuiActionRunner.execute(new GuiTask() { @Override protected void executeInEDT() throws Throwable { ProjectManagerEx projectManager = ProjectManagerEx.getInstanceEx(); projectManager.loadAndOpenProject(projectPath.getPath()); } }); IdeFrameFixture projectFrame = findIdeFrame(projectPath); projectFrame.waitForGradleProjectSyncToFinish(); return projectFrame; } @NotNull private static AndroidPlugin.GuiTestSuiteState getTestSuiteState() { AndroidPlugin.GuiTestSuiteState state = AndroidPlugin.getGuiTestSuiteState(); assertNotNull(state); return state; } /** * Sets up a project before using it in a UI test: * <ul> * <li>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.</li> * <li>Creates a Gradle wrapper for the test project.</li> * <li>Updates the version of the Android Gradle plug-in used by the project, if applicable</li> * <li>Creates a local.properties file pointing to the Android SDK path specified by the system property (or environment variable) * 'ADT_TEST_SDK_PATH'</li> * <li>Copies over missing files to the .idea directory (if the project will be opened, instead of imported.)</li> * <li>Deletes .idea directory, .iml files and build directories, if the project will be imported.</li> * <p/> * </ul> * * @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 protected File setUpProject(@NotNull String projectDirName, boolean forOpen, boolean updateAndroidPluginVersion, @Nullable String gradleVersion) throws IOException { File projectPath = copyProjectBeforeOpening(projectDirName); File gradlePropertiesFilePath = new File(projectPath, SdkConstants.FN_GRADLE_PROPERTIES); if (gradlePropertiesFilePath.isFile()) { delete(gradlePropertiesFilePath); } createGradleWrapper(projectPath, gradleVersion); if (updateAndroidPluginVersion) { updateGradleVersions(projectPath); } File androidHomePath = DefaultSdks.getDefaultAndroidHome(); assertNotNull(androidHomePath); LocalProperties localProperties = new LocalProperties(projectPath); localProperties.setAndroidSdkPath(androidHomePath); localProperties.save(); if (forOpen) { File toDotIdea = new File(projectPath, DIRECTORY_BASED_PROJECT_DIR); ensureExists(toDotIdea); File fromDotIdea = new File(getTestProjectsRootDirPath(), join("commonFiles", DIRECTORY_BASED_PROJECT_DIR)); 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); delete(projectPath); copyDir(masterProjectPath, projectPath); return projectPath; } @NotNull private static 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, DIRECTORY_BASED_PROJECT_DIR); 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 List<Element> 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 openProjectAndWaitUntilOpened(@NotNull VirtualFile projectDir, @NotNull FileChooserDialogFixture fileChooserDialog) { fileChooserDialog.select(projectDir).clickOk(); File projectPath = virtualToIoFile(projectDir); IdeFrameFixture projectFrame = findIdeFrame(projectPath); projectFrame.waitForGradleProjectSyncToFinish(); return projectFrame; } @NotNull protected IdeFrameFixture findIdeFrame(@NotNull File projectPath) { return IdeFrameFixture.find(myRobot, projectPath, null); } }