diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..b1077fb --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/sonarlint/issuestore/0/3/0398ccd0f49298b10a3d76a47800d2ebecd49859 b/.idea/sonarlint/issuestore/0/3/0398ccd0f49298b10a3d76a47800d2ebecd49859 new file mode 100644 index 0000000..e69de29 diff --git a/build.gradle.kts b/build.gradle.kts index 182c340..ae6b7ca 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,5 @@ +val remoteRobotVersion = "0.11.16" + plugins { id("java") id("jacoco") @@ -10,6 +12,7 @@ version = "1.0-SNAPSHOT" repositories { mavenCentral() + maven { url = uri("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies") } } // Configure Gradle IntelliJ Plugin @@ -17,10 +20,20 @@ repositories { intellij { version.set("2021.3.3") type.set("IC") // Target IDE Platform - plugins.set(listOf(/* Plugin Dependencies */)) } +dependencies { + testImplementation("com.intellij.remoterobot:remote-robot:" + remoteRobotVersion) + testImplementation("com.intellij.remoterobot:remote-fixtures:" + remoteRobotVersion) + testImplementation("com.squareup.okhttp3:logging-interceptor:4.10.0") + + val junitVersion = "5.9.0" + testImplementation("org.junit.jupiter:junit-jupiter-api:" + junitVersion) + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:" + junitVersion) + testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.9.0") +} + tasks { // Set the JVM compatibility versions withType { @@ -42,19 +55,41 @@ tasks { publishPlugin { token.set(System.getenv("PUBLISH_TOKEN")) } -} -tasks.test { - finalizedBy(tasks.jacocoTestReport) -} -tasks.jacocoTestReport { - dependsOn(tasks.test) - reports { - xml.required.set(true) + downloadRobotServerPlugin { + version.set(remoteRobotVersion) + } + + test { + systemProperty("robot-server.port", "8082") + useJUnitPlatform() + finalizedBy(jacocoTestReport) + } + + runIdeForUiTests { + // In case your Idea is launched on remote machine you can enable public port and enable encryption of JS calls + // systemProperty "robot-server.host.public", "true" + // systemProperty "robot.encryption.enabled", "true" + // systemProperty "robot.encryption.password", "my super secret" + systemProperty("robot-server.port", "8082") + systemProperty("ide.mac.message.dialogs.as.sheets", "false") + systemProperty("jb.privacy.policy.text", "") + systemProperty("jb.consents.confirmation.enabled", "false") + systemProperty("ide.mac.file.chooser.native", "false") + systemProperty("jbScreenMenuBar.enabled", "false") + systemProperty("apple.laf.useScreenMenuBar", "false") + systemProperty("idea.trust.all.projects", "true") + systemProperty("ide.show.tips.on.startup.default.value", "false") + } + + jacocoTestReport { + dependsOn(test) + reports { + xml.required.set(true) + } } } tasks.sonarqube { - dependsOn(tasks.jacocoTestReport) + dependsOn(tasks.jacocoTestReport) } - diff --git a/src/test/java/org/itallcode/openfasttrace/intelijplugin/remoterobot/RemoteRobotProperties.java b/src/test/java/org/itallcode/openfasttrace/intelijplugin/remoterobot/RemoteRobotProperties.java new file mode 100644 index 0000000..d557090 --- /dev/null +++ b/src/test/java/org/itallcode/openfasttrace/intelijplugin/remoterobot/RemoteRobotProperties.java @@ -0,0 +1,11 @@ +package org.itallcode.openfasttrace.intelijplugin.remoterobot; + +public final class RemoteRobotProperties { + private RemoteRobotProperties () { + // prevent class instantiation. + } + + final public static int ROBOT_PORT = Integer.parseInt(System.getProperty("robot-server.port")); + final public static String ROBOT_HOST = System.getProperty("robot-server.host.public", "localhost"); + final public static String ROBOT_BASE_URL = "http://" + ROBOT_HOST + ":" + ROBOT_PORT; +} diff --git a/src/test/java/org/itallcode/openfasttrace/intelijplugin/uitest/PluginUiTest.java b/src/test/java/org/itallcode/openfasttrace/intelijplugin/uitest/PluginUiTest.java new file mode 100644 index 0000000..98e93e0 --- /dev/null +++ b/src/test/java/org/itallcode/openfasttrace/intelijplugin/uitest/PluginUiTest.java @@ -0,0 +1,58 @@ +package org.itallcode.openfasttrace.intelijplugin.uitest; + +import com.intellij.remoterobot.RemoteRobot; +import com.intellij.remoterobot.fixtures.ComponentFixture; + +import static com.intellij.remoterobot.search.locators.Locators.byXpath; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assumptions.assumeFalse; + +import com.intellij.remoterobot.fixtures.JMenuBarFixture; +import org.itallcode.openfasttrace.intelijplugin.uitest.pages.IdeFrameFixture; +import org.itallcode.openfasttrace.intelijplugin.uitest.pages.NewProjectDialogFixture; +import org.itallcode.openfasttrace.intelijplugin.uitest.pages.WelcomeFrameFixture; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Path; +import java.time.Duration; + +import static org.itallcode.openfasttrace.intelijplugin.remoterobot.RemoteRobotProperties.*; + +class PluginUiTest { + final static Duration WITH_PATIENCE = Duration.ofSeconds(10); + @TempDir + static Path projectTempDir; + + @Test + void testOftMenuEntryExists() { + assumeNotRunningInCiBUild(); + final RemoteRobot robot = new RemoteRobot(ROBOT_BASE_URL); + final WelcomeFrameFixture welcomeFrame = robot.find(WelcomeFrameFixture.class, WITH_PATIENCE); + welcomeFrame.createNewProjectLink().click(); + final NewProjectDialogFixture project = robot.find(NewProjectDialogFixture.class, WITH_PATIENCE); + project.projectTypes().clickItem("Empty Project", true); + project.projectLocation().setText(projectTempDir.toAbsolutePath().toString()); + project.finish().click(); + final IdeFrameFixture ide = robot.find(IdeFrameFixture.class, WITH_PATIENCE); + waitUntilMenuBarIsReady(); + final JMenuBarFixture menuBar = ide.menuBar(); + menuBar.select("Help"); + assertDoesNotThrow(() -> robot.find(ComponentFixture.class, + byXpath("//div[@class='ActionMenu']//div[@text='OpenFastTrace User Guide']"))); + } + + private static void assumeNotRunningInCiBUild() { + assumeFalse(Boolean.parseBoolean(System.getenv("CI"))); + } + + @SuppressWarnings("java:S2925") + private void waitUntilMenuBarIsReady() { + try { + Thread.sleep(5000); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + throw new RuntimeException(exception); + } + } +} \ No newline at end of file diff --git a/src/test/java/org/itallcode/openfasttrace/intelijplugin/uitest/pages/IdeFrameFixture.java b/src/test/java/org/itallcode/openfasttrace/intelijplugin/uitest/pages/IdeFrameFixture.java new file mode 100644 index 0000000..1841f9a --- /dev/null +++ b/src/test/java/org/itallcode/openfasttrace/intelijplugin/uitest/pages/IdeFrameFixture.java @@ -0,0 +1,25 @@ +package org.itallcode.openfasttrace.intelijplugin.uitest.pages; + +import com.intellij.remoterobot.RemoteRobot; +import com.intellij.remoterobot.data.RemoteComponent; +import com.intellij.remoterobot.fixtures.*; +import org.jetbrains.annotations.NotNull; + +import java.time.Duration; + +import static com.intellij.remoterobot.search.locators.Locators.byXpath; + +/** + * The IDE Frame is the main container window (or page if you prefer that) for the IntelliJ IDEA. + */ +@DefaultXpath(by = "IdeFrameImpl type", xpath = "//div[@class='IdeFrameImpl']") +@FixtureName(name = "IDE Frame") +public class IdeFrameFixture extends CommonContainerFixture { + public IdeFrameFixture(@NotNull RemoteRobot remoteRobot, @NotNull RemoteComponent remoteComponent) { + super(remoteRobot, remoteComponent); + } + + public JMenuBarFixture menuBar() { + return find(JMenuBarFixture.class, byXpath("//div[@class='LinuxIdeMenuBar']"), Duration.ofSeconds(20)); + } +} \ No newline at end of file diff --git a/src/test/java/org/itallcode/openfasttrace/intelijplugin/uitest/pages/NewProjectDialogFixture.java b/src/test/java/org/itallcode/openfasttrace/intelijplugin/uitest/pages/NewProjectDialogFixture.java new file mode 100644 index 0000000..106c046 --- /dev/null +++ b/src/test/java/org/itallcode/openfasttrace/intelijplugin/uitest/pages/NewProjectDialogFixture.java @@ -0,0 +1,28 @@ +package org.itallcode.openfasttrace.intelijplugin.uitest.pages; + +import com.intellij.remoterobot.RemoteRobot; +import com.intellij.remoterobot.data.RemoteComponent; +import com.intellij.remoterobot.fixtures.*; +import org.jetbrains.annotations.NotNull; + +import static com.intellij.remoterobot.search.locators.Locators.byXpath; + +@DefaultXpath(by = "NewProjectDialog type", xpath = "//*[contains(@title.key, 'title.new.project')]") +@FixtureName(name = "New Project Dialog") +public class NewProjectDialogFixture extends CommonContainerFixture { + public NewProjectDialogFixture(@NotNull RemoteRobot remoteRobot, @NotNull RemoteComponent remoteComponent) { + super(remoteRobot, remoteComponent); + } + + public JListFixture projectTypes() { + return find(JListFixture.class, byXpath("//div[@class='JBList']")); + } + + public JTextFieldFixture projectLocation() { + return find(JTextFieldFixture.class, byXpath("//div[@class='FieldPanel']/div[1]")); + } + + public JButtonFixture finish() { + return find(JButtonFixture.class, byXpath("//div[@text.key='button.finish']")); + } +} \ No newline at end of file diff --git a/src/test/java/org/itallcode/openfasttrace/intelijplugin/uitest/pages/WelcomeFrameFixture.java b/src/test/java/org/itallcode/openfasttrace/intelijplugin/uitest/pages/WelcomeFrameFixture.java new file mode 100644 index 0000000..eab2830 --- /dev/null +++ b/src/test/java/org/itallcode/openfasttrace/intelijplugin/uitest/pages/WelcomeFrameFixture.java @@ -0,0 +1,33 @@ +package org.itallcode.openfasttrace.intelijplugin.uitest.pages; + +import com.intellij.remoterobot.RemoteRobot; +import com.intellij.remoterobot.data.RemoteComponent; +import com.intellij.remoterobot.fixtures.*; +import org.jetbrains.annotations.NotNull; + +import static com.intellij.remoterobot.search.locators.Locators.byXpath; + +/** + * The Welcome Frame is the container window (or page if you prefer that) for everything that happens before IntelliJ + * opens the actual IDE window. + *

+ * Displaying a welcome message is the smaller part of that job. The main reason is that the IDE needs a project context + * to work with and that context does not exist with a fresh installation. That is also why the welcome screen contains + * the means of opening, importing or creating projects in a sub dialog. + *

+ */ +@DefaultXpath(by = "FlatWelcomeFrame type", xpath = "//div[@class='FlatWelcomeFrame']") +@FixtureName(name = "Welcome Frame") +public class WelcomeFrameFixture extends CommonContainerFixture { + public WelcomeFrameFixture(@NotNull RemoteRobot remoteRobot, @NotNull RemoteComponent remoteComponent) { + super(remoteRobot, remoteComponent); + } + + public JButtonFixture createNewProjectLink() { + // The button style changes from an icon to a text button if a project already exists. To make things worse, + // In case of the icon the button is followed by a label that has the same text on it as the link-style button. + // We need a bit of XPath complexity, to pick either of the clickable items. + return find(JButtonFixture.class, byXpath("(//div[@defaulticon='createNewProjectTab.svg']" + + "|//div[@visible_text='New Project'])[1]")); + } +} \ No newline at end of file diff --git a/src/test/java/org/itallcode/openfasttrace/intelijplugin/wait/RobotServerReadyWaitStrategy.java b/src/test/java/org/itallcode/openfasttrace/intelijplugin/wait/RobotServerReadyWaitStrategy.java new file mode 100644 index 0000000..112078a --- /dev/null +++ b/src/test/java/org/itallcode/openfasttrace/intelijplugin/wait/RobotServerReadyWaitStrategy.java @@ -0,0 +1,70 @@ +package org.itallcode.openfasttrace.intelijplugin.wait; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.TimeoutException; + +import static org.itallcode.openfasttrace.intelijplugin.remoterobot.RemoteRobotProperties.*; + +/** + * This strategy waits for the Remote Robot server to become available. + */ +public class RobotServerReadyWaitStrategy { + private static final long RETRY_DELAY_MILLIS = 1000; + private final Duration timeout; + + /** + * Wait for the Remote Robot server to become available. + * + * @param timeout maximum time to wait for the server to become available + * @throws TimeoutException if the given timeout is reached without being able to connect to the robot server + */ + public static void wait(final Duration timeout) throws TimeoutException { + final RobotServerReadyWaitStrategy strategy = new RobotServerReadyWaitStrategy(timeout); + strategy.waitUntilReady(); + } + + private RobotServerReadyWaitStrategy(final Duration timeout) { + this.timeout = timeout; + } + + private void waitUntilReady() throws TimeoutException { + final OkHttpClient client = new OkHttpClient.Builder() + .connectTimeout(timeout) + .retryOnConnectionFailure(true) + .build(); + poll(client, ROBOT_BASE_URL); + client.dispatcher().executorService().shutdown(); + } + + private void poll(final OkHttpClient client, final String url) throws TimeoutException { + final Request request = new Request.Builder().url(url).build(); + final Instant until = Instant.now().plus(this.timeout); + do { + try (final Response response = client.newCall(request).execute()) { + if (response.isSuccessful()) { + return; + } + } catch (IOException exception) { + // keep trying. + try { + delayPollingRetry(); + } catch (InterruptedException interruptedException) { + Thread.currentThread().interrupt(); + throw new RuntimeException(interruptedException); + } + } + } while (Instant.now().isBefore(until)); + throw new TimeoutException("Timed out waiting for Remote Robot server to become available"); + } + + @SuppressWarnings("java:S2925") + private static void delayPollingRetry() throws InterruptedException { + Thread.sleep(RETRY_DELAY_MILLIS); + } +}