From 3f22c6f9e4ed48527a918cb661b729d924ff87ec Mon Sep 17 00:00:00 2001
From: Alex Plate <aleksei.plate@jetbrains.com>
Date: Fri, 14 Mar 2025 13:50:52 +0200
Subject: [PATCH] Create UI tests for Rider

---
 .github/workflows/runUiRdTests.yml            |  49 +++++++++
 .idea/gradle.xml                              |   1 +
 settings.gradle.kts                           |   1 +
 tests/ui-rd-tests/build.gradle.kts            |  58 ++++++++++
 .../src/test/kotlin/RiderUiTest.kt            | 100 ++++++++++++++++++
 5 files changed, 209 insertions(+)
 create mode 100644 .github/workflows/runUiRdTests.yml
 create mode 100644 tests/ui-rd-tests/build.gradle.kts
 create mode 100644 tests/ui-rd-tests/src/test/kotlin/RiderUiTest.kt

diff --git a/.github/workflows/runUiRdTests.yml b/.github/workflows/runUiRdTests.yml
new file mode 100644
index 000000000..bae314c9a
--- /dev/null
+++ b/.github/workflows/runUiRdTests.yml
@@ -0,0 +1,49 @@
+name: Run UI Rider Tests
+on:
+  workflow_dispatch:
+  schedule:
+      - cron: '0 12 * * *'
+jobs:
+  build-for-ui-test-mac-os:
+    if: github.repository == 'JetBrains/ideavim'
+    runs-on: macos-latest
+    steps:
+      - uses: actions/checkout@v4
+      - name: Setup Java
+        uses: actions/setup-java@v4
+        with:
+          distribution: zulu
+          java-version: 21
+      - name: Setup FFmpeg
+        run: brew install ffmpeg
+#      - name: Setup Gradle
+#        uses: gradle/gradle-build-action@v2.4.2
+      - name: Build Plugin
+        run: gradle :buildPlugin
+      - name: Run Idea
+        run: |
+          mkdir -p build/reports
+          gradle --no-configuration-cache :runIdeForUiTests -PideaType=RD > build/reports/idea.log &
+      - name: Wait for Idea started
+        uses: jtalk/url-health-check-action@v3
+        with:
+          url: http://127.0.0.1:8082
+          max-attempts: 20
+          retry-delay: 10s
+      - name: Tests
+        run: gradle :tests:ui-rd-tests:testUi
+      - name: Move video
+        if: always()
+        run: mv tests/ui-rd-tests/video build/reports
+      - name: Move sandbox logs
+        if: always()
+        run: mv build/idea-sandbox/RD-*/log_runIdeForUiTests idea-sandbox-log
+      - name: Save report
+        if: always()
+        uses: actions/upload-artifact@v4
+        with:
+          name: ui-test-fails-report-mac
+          path: |
+            build/reports
+            tests/ui-rd-tests/build/reports
+            idea-sandbox-log
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index d775e4b3b..0ba89bdd9 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -17,6 +17,7 @@
             <option value="$PROJECT_DIR$/tests/ui-fixtures" />
             <option value="$PROJECT_DIR$/tests/ui-ij-tests" />
             <option value="$PROJECT_DIR$/tests/ui-py-tests" />
+            <option value="$PROJECT_DIR$/tests/ui-rd-tests" />
             <option value="$PROJECT_DIR$/vim-engine" />
           </set>
         </option>
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 4c6176463..acce38486 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -19,3 +19,4 @@ include("tests:long-running-tests")
 include("tests:ui-ij-tests")
 include("tests:ui-py-tests")
 include("tests:ui-fixtures")
+include("tests:ui-rd-tests")
diff --git a/tests/ui-rd-tests/build.gradle.kts b/tests/ui-rd-tests/build.gradle.kts
new file mode 100644
index 000000000..a40343159
--- /dev/null
+++ b/tests/ui-rd-tests/build.gradle.kts
@@ -0,0 +1,58 @@
+plugins {
+  java
+  kotlin("jvm")
+}
+
+repositories {
+  mavenCentral()
+  maven { url = uri("https://cache-redirector.jetbrains.com/intellij-dependencies") }
+}
+
+val kotlinVersion: String by project
+val ideaVersion: String by project
+val javaVersion: String by project
+val remoteRobotVersion: String by project
+
+dependencies {
+  testImplementation("org.junit.jupiter:junit-jupiter")
+  compileOnly("org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion")
+  testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlinVersion")
+  testImplementation(testFixtures(project(":"))) // The root project
+  testImplementation(testFixtures(project(":tests:ui-fixtures")))
+
+  testImplementation("com.intellij.remoterobot:remote-robot:$remoteRobotVersion")
+  testImplementation("com.intellij.remoterobot:remote-fixtures:$remoteRobotVersion")
+  testImplementation("com.intellij.remoterobot:ide-launcher:$remoteRobotVersion")
+  testImplementation("com.automation-remarks:video-recorder-junit5:2.0")
+}
+
+tasks {
+  // This task is disabled because it should be excluded from `gradle test` run (because it's slow)
+  // I didn't find a better way to exclude except disabling and defining a new task with a different name
+  test {
+    useJUnitPlatform()
+    enabled = false
+  }
+
+  register<Test>("testUi") {
+    group = "verification"
+    useJUnitPlatform()
+
+    // This is needed for the robot to access the message of the exception
+    // Usually these opens are provided by the intellij gradle plugin
+    // https://github.com/JetBrains/gradle-intellij-plugin/blob/b21e3f382e9885948a6427001d5e64234c602613/src/main/kotlin/org/jetbrains/intellij/utils/OpenedPackages.kt#L26
+    jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
+  }
+}
+
+java {
+  toolchain {
+    languageVersion.set(JavaLanguageVersion.of(javaVersion))
+  }
+}
+
+kotlin {
+  jvmToolchain {
+    languageVersion.set(JavaLanguageVersion.of(javaVersion))
+  }
+}
diff --git a/tests/ui-rd-tests/src/test/kotlin/RiderUiTest.kt b/tests/ui-rd-tests/src/test/kotlin/RiderUiTest.kt
new file mode 100644
index 000000000..204f08bf6
--- /dev/null
+++ b/tests/ui-rd-tests/src/test/kotlin/RiderUiTest.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2003-2025 The IdeaVim authors
+ *
+ * Use of this source code is governed by an MIT-style
+ * license that can be found in the LICENSE.txt file or at
+ * https://opensource.org/licenses/MIT.
+ */
+
+import com.automation.remarks.junit5.Video
+import com.intellij.remoterobot.RemoteRobot
+import com.intellij.remoterobot.steps.CommonSteps
+import com.intellij.remoterobot.stepsProcessing.step
+import com.intellij.remoterobot.utils.keyboard
+import com.intellij.remoterobot.utils.waitFor
+import org.assertj.swing.core.MouseButton
+import org.junit.jupiter.api.Test
+import ui.pages.Editor
+import ui.pages.IdeaFrame
+import ui.pages.actionMenu
+import ui.pages.actionMenuItem
+import ui.pages.editor
+import ui.pages.idea
+import ui.pages.welcomeFrame
+import ui.utils.StepsLogger
+import ui.utils.uiTest
+import java.time.Duration
+import kotlin.test.assertEquals
+
+class RiderUiTest {
+  init {
+    StepsLogger.init()
+  }
+
+  private lateinit var commonSteps: CommonSteps
+
+  @Test
+  @Video
+  fun run() = uiTest("ideaVimTest") {
+    commonSteps = CommonSteps(this)
+
+    startNewProject()
+    Thread.sleep(1000)
+
+    idea {
+      waitSmartMode()
+
+      createFile("1.txt", this@uiTest)
+      val editor = editor("1.txt") {
+        step("Write a text") {
+          injectText(
+            """
+            |One Two
+            |Three Four
+          """.trimMargin()
+          )
+        }
+      }
+      waitFor(Duration.ofMinutes(1)) { editor.findAllText("One").isNotEmpty() }
+
+      testEnterWorksInNormalMode(editor)
+    }
+  }
+
+  private fun IdeaFrame.testEnterWorksInNormalMode(editor: Editor) {
+    editor.findText("Two").click()
+    keyboard {
+      enter()
+    }
+
+    assertEquals(
+      """
+      |One Two
+      |Three Four
+    """.trimMargin(), editor.text
+    )
+
+    assertEquals(8, editor.caretOffset)
+  }
+
+  private fun RemoteRobot.startNewProject() {
+    welcomeFrame {
+      createNewProjectLink.click()
+      button("Create").click()
+    }
+  }
+}
+
+private fun IdeaFrame.createFile(fileName: String, remoteRobot: RemoteRobot) {
+  step("Create $fileName file") {
+    with(projectViewTree) {
+      setExpandTimeout(30_000)
+      expand(projectName, "src")
+      findText("src").click(MouseButton.RIGHT_BUTTON)
+    }
+    remoteRobot.actionMenu("New").click()
+    remoteRobot.actionMenuItem("File").click()
+    keyboard { enterText(fileName); enter() }
+  }
+}
+