diff --git a/.github/workflows/runUiTests.yml b/.github/workflows/runUiTests.yml new file mode 100644 index 000000000..68ca34c66 --- /dev/null +++ b/.github/workflows/runUiTests.yml @@ -0,0 +1,66 @@ +name: Run UI Tests +on: + workflow_dispatch +jobs: + build-for-ui-test-mac-os: + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Java + uses: actions/setup-java@v2.1.0 + with: + distribution: zulu + java-version: 11 + - name: Build Plugin + run: gradle :buildPlugin + - name: Run Idea + run: | + mkdir -p build/reports + gradle :runIdeForUiTests > build/reports/idea.log & + - name: Wait for Idea started + uses: jtalk/url-health-check-action@1.5 + with: + url: http://127.0.0.1:8082 + max-attempts: 20 + retry-delay: 10s + - name: Tests + run: gradle :testUi + - name: Save fails report + if: ${{ failure() }} + uses: actions/upload-artifact@v2 + with: + name: ui-test-fails-report-mac + path: | + build/reports +# build-for-ui-test-linux: +# runs-on: ubuntu-latest +# steps: +# - uses: actions/checkout@v2 +# - name: Setup Java +# uses: actions/setup-java@v2.1.0 +# with: +# distribution: zulu +# java-version: 11 +# - name: Build Plugin +# run: gradle :buildPlugin +# - name: Run Idea +# run: | +# export DISPLAY=:99.0 +# Xvfb -ac :99 -screen 0 1920x1080x16 & +# mkdir -p build/reports +# gradle :runIdeForUiTests #> build/reports/idea.log +# - name: Wait for Idea started +# uses: jtalk/url-health-check-action@1.5 +# with: +# url: http://127.0.0.1:8082 +# max-attempts: 15 +# retry-delay: 30s +# - name: Tests +# run: gradle :testUi +# - name: Save fails report +# if: ${{ failure() }} +# uses: actions/upload-artifact@v2 +# with: +# name: ui-test-fails-report-linux +# path: | +# ui-test-example/build/reports \ No newline at end of file diff --git a/AUTHORS.md b/AUTHORS.md index 28422db59..2a3244bc6 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -360,6 +360,10 @@ Contributors: [![icon][github]](https://github.com/MichalPlacek) Michal Placek +* [![icon][mail]](mailto:eugene.nizienko@jetbrains.com) + [![icon][github]](https://github.com/nizienko) + + eugene nizienko If you are a contributor and your name is not listed here, feel free to contact the maintainers. diff --git a/build.gradle.kts b/build.gradle.kts index adde649dd..85846e64f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -36,6 +36,7 @@ val kotlinVersion: String by project val ideaVersion: String by project val downloadIdeaSources: String by project val instrumentPluginCode: String by project +val remoteRobotVersion: String by project val publishChannels: String by project val publishToken: String by project @@ -55,8 +56,8 @@ dependencies { testImplementation("com.ensarsarajcic.neovim.java:neovim-api:0.2.3") testImplementation("com.ensarsarajcic.neovim.java:core-rpc:0.2.3") - testImplementation("com.intellij.remoterobot:remote-robot:0.11.6") - testImplementation("com.intellij.remoterobot:remote-fixtures:1.1.18") + testImplementation("com.intellij.remoterobot:remote-robot:$remoteRobotVersion") + testImplementation("com.intellij.remoterobot:remote-fixtures:$remoteRobotVersion") } // --- Compilation @@ -116,7 +117,7 @@ intellij { tasks { downloadRobotServerPlugin { - version.set("0.10.0") + version.set(remoteRobotVersion) } publishPlugin { diff --git a/gradle.properties b/gradle.properties index 91b2dbce9..1c665bbdd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,6 +5,7 @@ downloadIdeaSources=true instrumentPluginCode=true version=SNAPSHOT javaVersion=1.8 +remoteRobotVersion=0.11.6 # Please don't forget to update kotlin version in buildscript section kotlinVersion=1.5.0 diff --git a/test/ui/UiTests.kt b/test/ui/UiTests.kt index db3a179fd..d51a58f1a 100644 --- a/test/ui/UiTests.kt +++ b/test/ui/UiTests.kt @@ -23,7 +23,6 @@ import com.intellij.remoterobot.fixtures.ContainerFixture import com.intellij.remoterobot.search.locators.byXpath 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.Test import ui.pages.Editor @@ -53,7 +52,7 @@ class UiTests { } @Test - fun ideaVimTest() = uiTest { + fun ideaVimTest() = uiTest("ideaVimTest") { val sharedSteps = JavaExampleSteps(this) welcomeFrame { @@ -69,12 +68,14 @@ class UiTests { button("Finish").click() } } - sharedSteps.closeTipOfTheDay() + with(sharedSteps) { + closeIdeaVimDialog() + closeTipOfTheDay() + } idea { step("Create App file") { with(projectViewTree) { - findText(projectName).doubleClick() - waitFor { hasText("src") } + expand(projectName, "src") findText("src").click(MouseButton.RIGHT_BUTTON) } actionMenu("New").click() diff --git a/test/ui/pages/Editor.kt b/test/ui/pages/Editor.kt index 82868f8e8..482f40e17 100644 --- a/test/ui/pages/Editor.kt +++ b/test/ui/pages/Editor.kt @@ -54,8 +54,19 @@ class Editor( val caretOffset: Int get() = callJs("component.getEditor().getCaretModel().getOffset()", runInEdt = true) + val isBlockCursor: Boolean + get() = callJs("component.getEditor().getSettings().isBlockCursor()", true) + fun injectText(text: String) { - runJs("component.getEditor().getDocument().setText('${text.escape()}')", runInEdt = true) + runJs(""" + const app = com.intellij.openapi.application.ApplicationManager.getApplication() + + app.invokeLaterOnWriteThread(()=>{ + app['runWriteAction(com.intellij.openapi.util.Computable)'](()=>{ + component.getEditor().getDocument().setText('${text.escape()}') + }) + }) +""") } @Suppress("unused") diff --git a/test/ui/pages/IdeaFrame.kt b/test/ui/pages/IdeaFrame.kt index f1336acfc..5f5d95c9f 100644 --- a/test/ui/pages/IdeaFrame.kt +++ b/test/ui/pages/IdeaFrame.kt @@ -24,6 +24,7 @@ import com.intellij.remoterobot.fixtures.CommonContainerFixture import com.intellij.remoterobot.fixtures.ContainerFixture import com.intellij.remoterobot.fixtures.DefaultXpath import com.intellij.remoterobot.fixtures.FixtureName +import com.intellij.remoterobot.fixtures.JTreeFixture import com.intellij.remoterobot.search.locators.byXpath import com.intellij.remoterobot.stepsProcessing.step import com.intellij.remoterobot.utils.waitFor @@ -41,7 +42,7 @@ class IdeaFrame( ) : CommonContainerFixture(remoteRobot, remoteComponent) { val projectViewTree - get() = find<ContainerFixture>(byXpath("ProjectViewTree", "//div[@class='ProjectViewTree']")) + get() = find<JTreeFixture>(byXpath("ProjectViewTree", "//div[@class='ProjectViewTree']"), Duration.ofSeconds(10)) val projectName get() = step("Get project name") { return@step callJs<String>("component.getProject().getName()") } diff --git a/test/ui/utils/JavaExampleSteps.kt b/test/ui/utils/JavaExampleSteps.kt index 79d36f426..b91fd62f5 100644 --- a/test/ui/utils/JavaExampleSteps.kt +++ b/test/ui/utils/JavaExampleSteps.kt @@ -18,28 +18,48 @@ package ui.utils import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.fixtures.JButtonFixture +import com.intellij.remoterobot.search.locators.byXpath import com.intellij.remoterobot.stepsProcessing.step import com.intellij.remoterobot.utils.Keyboard import ui.pages.DialogFixture import ui.pages.DialogFixture.Companion.byTitle import ui.pages.IdeaFrame +import ui.pages.dialog +import ui.pages.idea class JavaExampleSteps(private val remoteRobot: RemoteRobot) { @Suppress("unused") private val keyboard: Keyboard = Keyboard(remoteRobot) - fun closeTipOfTheDay() { - step( - "Close Tip of the Day if it appears", - Runnable { - val idea: IdeaFrame = remoteRobot.find(IdeaFrame::class.java) - idea.dumbAware { - try { - idea.find(DialogFixture::class.java, byTitle("Tip of the Day")).button("Close").click() - } catch (ignore: Throwable) { - } - } - } - ) + fun closeIdeaVimDialog() = optionalStep("Close Idea Vim dialog if it appears") { + remoteRobot.idea { + dialog("IdeaVim") { button("Yes").click() } + } + } + + + fun closeTipOfTheDay() = optionalStep("Close Tip of the Day if it appears") { + val idea: IdeaFrame = remoteRobot.find(IdeaFrame::class.java) + idea.dumbAware { + idea.find(DialogFixture::class.java, byTitle("Tip of the Day")).button("Close").click() + } + closeAllGotIt() + } + + + fun closeAllGotIt() = step("Close Got It") { + remoteRobot.findAll<JButtonFixture>(byXpath("//div[@accessiblename='Got It']")).forEach { + it.click() + } + } + + + private fun optionalStep(stepName: String, code: () -> Unit) = step(stepName) { + try { + code() + } catch (ignore: Throwable) { + println("$stepName ignored") + } } } diff --git a/test/ui/utils/UiTestWrapper.kt b/test/ui/utils/UiTestWrapper.kt index 8c2f3c057..df66ec5ea 100644 --- a/test/ui/utils/UiTestWrapper.kt +++ b/test/ui/utils/UiTestWrapper.kt @@ -19,7 +19,54 @@ package ui.utils import com.intellij.remoterobot.RemoteRobot +import okhttp3.OkHttpClient +import okhttp3.Request +import java.awt.image.BufferedImage +import java.io.ByteArrayOutputStream +import java.io.File +import javax.imageio.ImageIO -fun uiTest(url: String = "http://127.0.0.1:8082", test: RemoteRobot.() -> Unit) { - RemoteRobot(url).apply(test) +fun uiTest(testName: String = "test_${System.currentTimeMillis()}", url: String = "http://127.0.0.1:8082", test: RemoteRobot.() -> Unit) { + val remoteRobot = RemoteRobot(url) + try { + remoteRobot.test() + } catch (e: Throwable) { + saveScreenshot(testName, remoteRobot) + saveHierarchy(testName, url) + throw e + } } +private val client by lazy { OkHttpClient() } +private fun BufferedImage.save(name: String) { + val bytes = ByteArrayOutputStream().use { b -> + ImageIO.write(this, "png", b) + b.toByteArray() + } + File("build/reports").apply { mkdirs() }.resolve("$name.png").writeBytes(bytes) +} + +fun saveScreenshot(testName: String, remoteRobot: RemoteRobot) { + fetchScreenShot(remoteRobot).save(testName) +} + +private fun fetchScreenShot(remoteRobot: RemoteRobot): BufferedImage { + return remoteRobot.getScreenshot() +} + +private fun saveHierarchy(testName: String, url: String) { + val hierarchySnapshot = + saveFile(url, "build/reports", "hierarchy-$testName.html") + if (File("build/reports/styles.css").exists().not()) { + saveFile("$url/styles.css", "build/reports", "styles.css") + } + println("Hierarchy snapshot: ${hierarchySnapshot.absolutePath}") +} + +private fun saveFile(url: String, folder: String, name: String): File { + val response = client.newCall(Request.Builder().url(url).build()).execute() + return File(folder).apply { + mkdirs() + }.resolve(name).apply { + writeText(response.body?.string() ?: "") + } +} \ No newline at end of file diff --git a/test/ui/utils/Utils.kt b/test/ui/utils/Utils.kt index 83b897ed1..4779dc8b4 100644 --- a/test/ui/utils/Utils.kt +++ b/test/ui/utils/Utils.kt @@ -22,6 +22,7 @@ import com.intellij.remoterobot.fixtures.Fixture import com.intellij.remoterobot.fixtures.dataExtractor.RemoteText import com.intellij.remoterobot.utils.waitFor import org.assertj.swing.core.MouseButton +import ui.pages.Editor import java.awt.Point fun RemoteText.doubleClickOnRight(shiftX: Int, fixture: Fixture, button: MouseButton = MouseButton.LEFT_BUTTON) { @@ -38,22 +39,13 @@ fun RemoteText.tripleClickOnRight(shiftX: Int, fixture: Fixture, button: MouseBu } } -fun RemoteText.moveMouseTo(goal: RemoteText, fixture: Fixture): Boolean { +fun RemoteText.moveMouseTo(goal: RemoteText, editor: Editor): Boolean { this.moveMouse() - val goalPoint = goal.point - - val caretDuringDragging = fixture.callJs<Boolean>( - """ - const point = new java.awt.Point(${goalPoint.x}, ${goalPoint.y}); - let isBlock = true; - robot.pressMouseWhileRunning(MouseButton.LEFT_BUTTON, () => { - robot.moveMouse(component, point) - isBlock = component.getEditor().getSettings().isBlockCursor(); - }) - isBlock - """ - ) - waitFor { fixture.callJs("component.getEditor().getSettings().isBlockCursor()") } + editor.runJs("robot.pressMouse(MouseButton.LEFT_BUTTON)") + goal.moveMouse() + val caretDuringDragging = editor.isBlockCursor + editor.runJs("robot.releaseMouse(MouseButton.LEFT_BUTTON)") + waitFor { editor.isBlockCursor } return caretDuringDragging } @@ -71,22 +63,22 @@ fun RemoteText.moveMouseInGutterTo(goal: RemoteText, fixture: Fixture) { ) } -fun RemoteText.moveMouseForthAndBack(middle: RemoteText, fixture: Fixture) { +fun RemoteText.moveMouseForthAndBack(middle: RemoteText, editor: Editor) { this.moveMouse() val initialPoint = this.point val middlePoint = middle.point - fixture.runJs( + editor.runJs( """ - const initialPoint = new java.awt.Point(${initialPoint.x}, ${initialPoint.y}); - const point = new java.awt.Point(${middlePoint.x}, ${middlePoint.y}); + const initialPoint = new Point(${initialPoint.x}, ${initialPoint.y}); + const point = new Point(${middlePoint.x}, ${middlePoint.y}); robot.pressMouseWhileRunning(MouseButton.LEFT_BUTTON, () => { robot.moveMouse(component, point) robot.moveMouse(component, initialPoint) }) """ ) - waitFor { fixture.callJs("component.getEditor().getSettings().isBlockCursor()") } + waitFor { editor.isBlockCursor } } fun String.escape(): String = this.replace("\n", "\\n")