diff --git a/skate-plugin/build.gradle.kts b/skate-plugin/build.gradle.kts index 0ef8a09e5..1faa43bb1 100644 --- a/skate-plugin/build.gradle.kts +++ b/skate-plugin/build.gradle.kts @@ -10,7 +10,10 @@ group = "com.slack.sgp.intellij" version = "1.0-SNAPSHOT" -repositories { mavenCentral() } +repositories { + mavenCentral() + maven(url = "https://packages.jetbrains.team/maven/p/ij/intellij-dependencies") +} // Configure Gradle IntelliJ Plugin // Read more: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html @@ -34,6 +37,12 @@ tasks { } publishPlugin { token.set(System.getenv("PUBLISH_TOKEN")) } + + runIdeForUiTests { systemProperty("robot-server.port", "8082") } + + downloadRobotServerPlugin { "0.11.19" } + + test { useJUnitPlatform() } } // region Version.kt template for setting the project version in the build @@ -65,4 +74,20 @@ dependencies { implementation(libs.bugsnag) testImplementation(libs.junit) testImplementation(libs.truth) + testImplementation(libs.okhttp) + testImplementation("com.intellij.remoterobot:remote-robot:0.11.19") + testImplementation("com.intellij.remoterobot:remote-fixtures:0.11.19") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.2") + testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.9.3") + + // Logging Network Calls + testImplementation("com.squareup.okhttp3:logging-interceptor:4.11.0") + + // Video Recording + implementation("com.automation-remarks:video-recorder-junit5:2.0") +} +java { + sourceCompatibility = JavaVersion.VERSION_19 + targetCompatibility = JavaVersion.VERSION_19 } diff --git a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/SkateService.kt b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/SkateService.kt index b8c530d2f..a8be6baab 100644 --- a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/SkateService.kt +++ b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/SkateService.kt @@ -36,9 +36,6 @@ interface SkateProjectService { class SkateProjectServiceImpl(private val project: Project) : SkateProjectService { override fun showWhatsNewWindow() { - // TODO - // Only show when changed - // Only show latest changes val settings = project.service() if (!settings.isWhatsNewEnabled) return val projectDir = project.guessProjectDir() ?: return diff --git a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/ui/SkateConfigUI.kt b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/ui/SkateConfigUI.kt index 9424f6df6..eb23b4bbf 100644 --- a/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/ui/SkateConfigUI.kt +++ b/skate-plugin/src/main/kotlin/com/slack/sgp/intellij/ui/SkateConfigUI.kt @@ -38,7 +38,7 @@ internal class SkateConfigUI( private fun Panel.checkBoxRow() { row(SkateBundle.message("skate.configuration.enableWhatsNew.title")) { - checkBox("skate.configuration.enableWhatsNew.description") + checkBox(SkateBundle.message("skate.configuration.enableWhatsNew.description")) .bindSelected( getter = { settings.isWhatsNewEnabled }, setter = { settings.isWhatsNewEnabled = it } diff --git a/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/SkatePluginInitializationTest.kt b/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/SkatePluginInitializationTest.kt new file mode 100644 index 000000000..cc114b8d3 --- /dev/null +++ b/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/SkatePluginInitializationTest.kt @@ -0,0 +1,26 @@ +package com.slack.sgp.intellij + +import com.google.common.truth.Truth.assertThat +import com.intellij.openapi.components.service +import com.intellij.testFramework.fixtures.BasePlatformTestCase + +class SkatePluginInitializationTest : BasePlatformTestCase() { + + fun `test Skate Plugin Service Initialization to ensure SkateProjectService is properly registered & initialized`() { + val skateService = project.service() + + // Service should be an instance of SkateProjectServiceImpl + assertThat(skateService).isInstanceOf(SkateProjectServiceImpl::class.java) + } + + fun `test Skate Plugin Settings Initialization`() { + val settings = project.service() + + // Assert that settings is not null + assertThat(settings).isNotNull() + + // Check the default values + assertThat(settings.whatsNewFilePath).isEqualTo("CHANGELOG.md") + assertThat(settings.isWhatsNewEnabled).isTrue() + } +} diff --git a/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/SkatePluginTest.kt b/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/SkatePluginTest.kt new file mode 100644 index 000000000..bf50c75bf --- /dev/null +++ b/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/SkatePluginTest.kt @@ -0,0 +1,90 @@ +package com.slack.sgp.intellij + +import com.google.common.truth.Truth.assertThat +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.utils.keyboard +import com.intellij.remoterobot.utils.waitForIgnoringError +import com.slack.sgp.intellij.pages.ToolWindowFixture +import com.slack.sgp.intellij.pages.idea +import com.slack.sgp.intellij.utils.RemoteRobotExtension +import com.slack.sgp.intellij.utils.StepsLogger +import java.awt.event.KeyEvent.VK_A +import java.awt.event.KeyEvent.VK_META +import java.awt.event.KeyEvent.VK_SHIFT +import java.time.Duration.ofSeconds +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +// TODO: +// Writes a sample changelog file to the project dir (before opening the project) +// Writes the setting to the skate config +// Opens the project +// Asserts that the panel opened and showed the changelog + +@ExtendWith(RemoteRobotExtension::class) +class SkatePluginTest { + + init { + StepsLogger.init() + } + + @BeforeEach + fun waitForIde(remoteRobot: RemoteRobot) { + waitForIgnoringError( + ofSeconds(10), + ofSeconds(2), + "Wait for Ide started", + "Ide is not started" + ) { + remoteRobot.callJs("true") + } + } + + @AfterEach + fun closeProject(remoteRobot: RemoteRobot) = + with(remoteRobot) { + idea { + if (remoteRobot.isMac()) { + keyboard { + hotKey(VK_SHIFT, VK_META, VK_A) + enterText("Close Project") + enter() + } + } else { + menuBar.select("File", "Close Project") + } + } + } + + @Test + // @Video + fun checkToolWindow(remoteRobot: RemoteRobot) { + with(remoteRobot) { + val toolWindow = find(ToolWindowFixture::class.java, timeout = ofSeconds(10)) + assertThat(toolWindow.window.isShowing).isTrue() + } + } + + // @Test + // fun testToolWindowExists() { + // val robot = RemoteRobot("http://127.0.0.1:8082") + // checkToolWindow(robot) + // } + // @Test + // fun checkToolWindow() { + // // Create a RemoteRobot instance with the default URL (localhost:8082) + // val robot = RemoteRobot("http://127.0.0.1:8082") + // + // // See if Tool Window has the same name it's supposed to + // val toolWindow = + // robot.find( + // ComponentFixture::class.java, + // byXpath("//div[@accessibilityName=\"What's New in Slack!\"]") + // ) + // + // // Check if the Tool Window is showing + // assertThat(toolWindow.isShowing).isTrue() + // } +} diff --git a/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/pages/IdeaFrame.kt b/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/pages/IdeaFrame.kt new file mode 100644 index 000000000..6c84003d3 --- /dev/null +++ b/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/pages/IdeaFrame.kt @@ -0,0 +1,65 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package com.slack.sgp.intellij.pages + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.* +import com.intellij.remoterobot.search.locators.byXpath +import com.intellij.remoterobot.stepsProcessing.step +import com.intellij.remoterobot.utils.waitFor +import java.time.Duration + +fun RemoteRobot.idea(function: IdeaFrame.() -> Unit) { + find(timeout = Duration.ofSeconds(10)).apply(function) +} + +@FixtureName("Idea frame") +@DefaultXpath("IdeFrameImpl type", "//div[@class='IdeFrameImpl']") +class IdeaFrame(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) : + CommonContainerFixture(remoteRobot, remoteComponent) { + + val projectViewTree + get() = find(byXpath("ProjectViewTree", "//div[@class='ProjectViewTree']")) + + val projectName + get() = + step("Get project name") { + return@step callJs("component.getProject().getName()") + } + + val menuBar: JMenuBarFixture + get() = + step("Menu...") { + return@step remoteRobot.find(JMenuBarFixture::class.java, JMenuBarFixture.byType()) + } + + @JvmOverloads + fun dumbAware(timeout: Duration = Duration.ofMinutes(5), function: () -> Unit) { + step("Wait for smart mode") { + waitFor(duration = timeout, interval = Duration.ofSeconds(5)) { + runCatching { isDumbMode().not() }.getOrDefault(false) + } + function() + step("..wait for smart mode again") { + waitFor(duration = timeout, interval = Duration.ofSeconds(5)) { isDumbMode().not() } + } + } + } + + fun isDumbMode(): Boolean { + return callJs( + """ + const frameHelper = com.intellij.openapi.wm.impl.ProjectFrameHelper.getFrameHelper(component) + if (frameHelper) { + const project = frameHelper.getProject() + project ? com.intellij.openapi.project.DumbService.isDumb(project) : true + } else { + true + } + """, + true + ) + } +} diff --git a/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/pages/ToolWindowFixture.kt b/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/pages/ToolWindowFixture.kt new file mode 100644 index 000000000..70a42e4cb --- /dev/null +++ b/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/pages/ToolWindowFixture.kt @@ -0,0 +1,26 @@ +package com.slack.sgp.intellij.pages + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.CommonContainerFixture +import com.intellij.remoterobot.fixtures.ComponentFixture +import com.intellij.remoterobot.fixtures.DefaultXpath +import com.intellij.remoterobot.fixtures.FixtureName +import com.intellij.remoterobot.search.locators.byXpath +import java.time.Duration + +fun RemoteRobot.toolWindow(function: ToolWindowFixture.() -> Unit) { + find(ToolWindowFixture::class.java, Duration.ofSeconds(10)).apply(function) +} + +@FixtureName("Tool Window") +@DefaultXpath("type", "//div[@accessibilityName=\"What's New in Slack!\"]") +class ToolWindowFixture(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) : + CommonContainerFixture(remoteRobot, remoteComponent) { + val window + get() = + find( + ComponentFixture::class.java, + byXpath("//div[@accessibilityName=\"What's New in Slack!\"]") + ) +} diff --git a/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/pages/WelcomeFrame.kt b/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/pages/WelcomeFrame.kt new file mode 100644 index 000000000..a59ff3fbe --- /dev/null +++ b/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/pages/WelcomeFrame.kt @@ -0,0 +1,34 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package com.slack.sgp.intellij.pages + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.data.RemoteComponent +import com.intellij.remoterobot.fixtures.* +import com.intellij.remoterobot.search.locators.byXpath +import java.time.Duration + +fun RemoteRobot.welcomeFrame(function: WelcomeFrame.() -> Unit) { + find(WelcomeFrame::class.java, Duration.ofSeconds(10)).apply(function) +} + +@FixtureName("Welcome Frame") +@DefaultXpath("type", "//div[@class='FlatWelcomeFrame']") +class WelcomeFrame(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) : + CommonContainerFixture(remoteRobot, remoteComponent) { + val createNewProjectLink + get() = + actionLink( + byXpath( + "New Project", + "//div[(@class='MainButton' and @text='New Project') or (@accessiblename='New Project' and @class='JButton')]" + ) + ) + val moreActions + get() = button(byXpath("More Action", "//div[@accessiblename='More Actions']")) + + val heavyWeightPopup + get() = + remoteRobot.find(ComponentFixture::class.java, byXpath("//div[@class='HeavyWeightWindow']")) +} diff --git a/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/utils/RemoteRobotExtension.kt b/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/utils/RemoteRobotExtension.kt new file mode 100644 index 000000000..26417d78d --- /dev/null +++ b/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/utils/RemoteRobotExtension.kt @@ -0,0 +1,139 @@ +package com.slack.sgp.intellij.utils + +import com.intellij.remoterobot.RemoteRobot +import com.intellij.remoterobot.fixtures.ContainerFixture +import com.intellij.remoterobot.search.locators.byXpath +import java.awt.image.BufferedImage +import java.io.ByteArrayOutputStream +import java.io.File +import java.lang.IllegalStateException +import java.lang.reflect.Method +import javax.imageio.ImageIO +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.logging.HttpLoggingInterceptor +import org.junit.jupiter.api.extension.AfterTestExecutionCallback +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.api.extension.ParameterContext +import org.junit.jupiter.api.extension.ParameterResolver + +class RemoteRobotExtension : AfterTestExecutionCallback, ParameterResolver { + private val url: String = System.getProperty("remote-robot-url") ?: "http://127.0.0.1:8082" + private val remoteRobot: RemoteRobot = + if (System.getProperty("debug-retrofit")?.equals("enable") == true) { + val interceptor: HttpLoggingInterceptor = + HttpLoggingInterceptor().apply { this.level = HttpLoggingInterceptor.Level.BODY } + val client = OkHttpClient.Builder().apply { this.addInterceptor(interceptor) }.build() + RemoteRobot(url, client) + } else { + RemoteRobot(url) + } + private val client = OkHttpClient() + + override fun supportsParameter( + parameterContext: ParameterContext?, + extensionContext: ExtensionContext? + ): Boolean { + return parameterContext?.parameter?.type?.equals(RemoteRobot::class.java) ?: false + } + + override fun resolveParameter( + parameterContext: ParameterContext?, + extensionContext: ExtensionContext? + ): Any { + return remoteRobot + } + + override fun afterTestExecution(context: ExtensionContext?) { + val testMethod: Method = + context?.requiredTestMethod ?: throw IllegalStateException("test method is null") + val testMethodName = testMethod.name + val testFailed: Boolean = context.executionException?.isPresent ?: false + if (testFailed) { + // saveScreenshot(testMethodName) + saveIdeaFrames(testMethodName) + saveHierarchy(testMethodName) + } + } + + private fun saveScreenshot(testName: String) { + fetchScreenShot().save(testName) + } + + private fun saveHierarchy(testName: 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()) } + } + + 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) + } + + private fun saveIdeaFrames(testName: String) { + remoteRobot.findAll(byXpath("//div[@class='IdeFrameImpl']")).forEachIndexed { + n, + frame -> + val pic = + try { + frame.callJs( + """ + importPackage(java.io) + importPackage(javax.imageio) + importPackage(java.awt.image) + const screenShot = new BufferedImage(component.getWidth(), component.getHeight(), BufferedImage.TYPE_INT_ARGB); + component.paint(screenShot.getGraphics()) + let pictureBytes; + const baos = new ByteArrayOutputStream(); + try { + ImageIO.write(screenShot, "png", baos); + pictureBytes = baos.toByteArray(); + } finally { + baos.close(); + } + pictureBytes; + """, + true + ) + } catch (e: Throwable) { + e.printStackTrace() + throw e + } + pic.inputStream().use { ImageIO.read(it) }.save(testName + "_" + n) + } + } + + private fun fetchScreenShot(): BufferedImage { + return remoteRobot + .callJs( + """ + importPackage(java.io) + importPackage(javax.imageio) + const screenShot = new java.awt.Robot().createScreenCapture(new Rectangle(Toolkit.getDefaultToolkit().getScreenSize())); + let pictureBytes; + const baos = new ByteArrayOutputStream(); + try { + ImageIO.write(screenShot, "png", baos); + pictureBytes = baos.toByteArray(); + } finally { + baos.close(); + } + pictureBytes; + """ + ) + .inputStream() + .use { ImageIO.read(it) } + } +} diff --git a/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/utils/StepsLogger.kt b/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/utils/StepsLogger.kt new file mode 100644 index 000000000..9409b99fc --- /dev/null +++ b/skate-plugin/src/test/kotlin/com/slack/sgp/intellij/utils/StepsLogger.kt @@ -0,0 +1,18 @@ +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package com.slack.sgp.intellij.utils + +import com.intellij.remoterobot.stepsProcessing.StepLogger +import com.intellij.remoterobot.stepsProcessing.StepWorker + +object StepsLogger { + private var initializaed = false + @JvmStatic + fun init() { + if (initializaed.not()) { + StepWorker.registerProcessor(StepLogger()) + initializaed = true + } + } +}