diff --git a/.github/workflows/run-ui-tests.yml b/.github/workflows/run-ui-tests.yml
index d475eecf..5958dc98 100644
--- a/.github/workflows/run-ui-tests.yml
+++ b/.github/workflows/run-ui-tests.yml
@@ -43,36 +43,18 @@ jobs:
fail-fast: false
matrix:
os: [ macos-latest, ubuntu-latest, windows-latest ]
- ideDate: [ IC-2022.2, IC-2022.3, IC-2023.1, IC-2023.2 ]
- include:
-# - ideDate: IC-EAP # TODO: Run this variant weekly
-# ide: IC
-# version: LATEST_EAP
- - ideDate: IC-2023.2
- ide: IC
- version: 2023.2.3 # released on 11.10.2023, major from 26.07.2023
- - ideDate: IC-2023.1
- ide: IC
- version: 2023.1.5 # released on 25.07.2023, major from 28.03.2023
- - ideDate: IC-2022.3
- ide: IC
- version: 2022.3.3 # released on 8.03.2023, major from 30.11.2022
- - ideDate: IC-2022.2
- ide: IC
- version: 2022.2.5 # released on 15.03.2023, major from 26.07.2022
-
+ ideDate: # Up to 2 versions per year, initial major ver release date should be not older than a year
+ # - IC-LATEST_EAP # TODO: Run this variant weekly
+ - IC-2023.3.2 # released on 20.12.2023, major from 06.12.2023
+ - IC-2023.1.5 # released on 25.07.2023, major from 28.03.2023
+ - IC-2022.3.3 # released on 8.03.2023, major from 30.11.2022
+ # - PC-LATEST_EAP # TODO: Run this variant weekly
+ - PC-2023.3.2 # released on 20.12.2023, major from 06.12.2023
+ - PC-2023.1.4 # released on 13.07.2023, major from 30.03.2023
+ - PC-2022.3.3 # released on 10.03.2023, major from 01.12.2022
# Versions should match https://jb.gg/android-studio-releases-list.xml
-# TODO:
-# - ideDate: AI-2023
-# ide: AI
-# version: 2023.1.1 # released on 27.09.2023
-# - ideDate: AI-2022
-# ide: AI
-# version: 2022.3.1 # released on 28.09.2023
-# - ideDate: AI-2021
-# ide: AI
-# version: 2021.3.1.17 # released on 13.10.2022
+ include:
- os: ubuntu-latest
runTests: |
export DISPLAY=:99.0
@@ -89,11 +71,18 @@ jobs:
reportName: ui-tests-windows
env:
- IDE_CODE: ${{ matrix.ide }}
- IDE_VERSION: ${{ matrix.version }}
PLUGIN_PATH: "${{ github.workspace }}/${{ needs.getPlugin.outputs.path }}"
steps:
-
+ - uses: actions/github-script@v7
+ id: prepare-IDE_CODE
+ with:
+ script: return "${{ matrix.ideDate }}".split("-", 2)[0]
+ result-encoding: string
+ - uses: actions/github-script@v7
+ id: prepare-IDE_VERSION
+ with:
+ script: return "${{ matrix.ideDate }}".split("-", 2)[1]
+ result-encoding: string
- name: Setup FFmpeg
uses: FedericoCarboni/setup-ffmpeg@v2
with:
@@ -104,6 +93,9 @@ jobs:
# Setup Java environment for the next steps
- name: Setup Java
uses: actions/setup-java@v3
+ env:
+ IDE_CODE: ${{ steps.prepare-IDE_CODE.outputs.result }}
+ IDE_VERSION: ${{ steps.prepare-IDE_VERSION.outputs.result }}
with:
distribution: zulu
java-version: 11
@@ -111,6 +103,9 @@ jobs:
# Setup Gradle
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
+ env:
+ IDE_CODE: ${{ steps.prepare-IDE_CODE.outputs.result }}
+ IDE_VERSION: ${{ steps.prepare-IDE_VERSION.outputs.result }}
with:
gradle-home-cache-cleanup: true
@@ -128,6 +123,9 @@ jobs:
# Run tests
- name: Tests
+ env:
+ IDE_CODE: ${{ steps.prepare-IDE_CODE.outputs.result }}
+ IDE_VERSION: ${{ steps.prepare-IDE_VERSION.outputs.result }}
run: ${{ matrix.runTests }}
# Collect Tests Result of failed tests
diff --git a/README.md b/README.md
index f4b89264..d364ced4 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ UI Surveyor plugin provides tools helping with mobile application automation.
They provide the following features:
* **_Evaluating_ element selectors against an XML UI snapshots**
![Search](docs/Search.png)
-* **Syntax highlighting and autocomplete for element selectors**
+* **Syntax highlighting and autocomplete for element selectors (Java IDEs only)**
![Autocomplete & Highlighting](docs/Autocomplete.png)
* **Improved structure navigation for XML UI snapshots**
![Structure navigation](docs/StructureNavigation.png)
@@ -24,7 +24,7 @@ UI Surveyor plugin provides tools helping work with Android UI Snapshot in XML f
Those tools are:
* `Locate Element` tool window for **evaluating** element selectors against a currently open XML UI snapshots
-* Basic syntax highlighting and autocomplete for UIAutomator selectors (as Java code)
+* Basic syntax highlighting and autocomplete for UIAutomator selectors (as Java code, supported only for Java IDEs)
* Improved structure navigation for UI snapshots
All trademarks are the property of their respective owners. All company, product and service names
diff --git a/ci/poms/droid-selector/pom.xml b/ci/poms/droid-selector/pom.xml
index 744b67f5..26d65ae4 100644
--- a/ci/poms/droid-selector/pom.xml
+++ b/ci/poms/droid-selector/pom.xml
@@ -1,6 +1,5 @@
-
+
diff --git a/ci/poms/droid-stubs/pom.xml b/ci/poms/droid-stubs/pom.xml
index a26a4da2..13a5ea56 100644
--- a/ci/poms/droid-stubs/pom.xml
+++ b/ci/poms/droid-stubs/pom.xml
@@ -1,6 +1,5 @@
-
+
diff --git a/ci/poms/library/pom.xml b/ci/poms/library/pom.xml
index 9333cb93..9bedbf9c 100644
--- a/ci/poms/library/pom.xml
+++ b/ci/poms/library/pom.xml
@@ -1,6 +1,5 @@
-
+
diff --git a/ci/poms/plugin-test/pom.xml b/ci/poms/plugin-test/pom.xml
index e615620d..b3c5bd84 100644
--- a/ci/poms/plugin-test/pom.xml
+++ b/ci/poms/plugin-test/pom.xml
@@ -1,6 +1,5 @@
-
+
diff --git a/ci/poms/plugin/pom.xml b/ci/poms/plugin/pom.xml
index 7f1202d1..6446c0d7 100644
--- a/ci/poms/plugin/pom.xml
+++ b/ci/poms/plugin/pom.xml
@@ -1,6 +1,5 @@
-
+
diff --git a/plugin-test/src/test/kotlin/com/github/tarcv/testingteam/surveyoridea/AssertionUtils.kt b/plugin-test/src/test/kotlin/com/github/tarcv/testingteam/surveyoridea/AssertionUtils.kt
index 1262c0a2..2e5b6002 100644
--- a/plugin-test/src/test/kotlin/com/github/tarcv/testingteam/surveyoridea/AssertionUtils.kt
+++ b/plugin-test/src/test/kotlin/com/github/tarcv/testingteam/surveyoridea/AssertionUtils.kt
@@ -3,10 +3,18 @@ package com.github.tarcv.testingteam.surveyoridea
import com.intellij.remoterobot.utils.waitFor
import java.time.Duration
-fun waitingAssertion(errorMessage: String, expectedValue: T, valueSupplier: () -> T) {
+fun waitingAssertEquals(errorMessage: String, expectedValue: T, valueSupplier: () -> T) {
+ return waitingAssertion(
+ errorMessage,
+ valueSupplier
+ ) { it == expectedValue }
+}
+
+fun waitingAssertion(errorMessage: String, valueSupplier: () -> T, assertion: (T) -> Boolean) {
var lastValue: T? = null
- waitFor(Duration.ofSeconds(20), errorMessageSupplier = { "$errorMessage Actual value was $lastValue" }) {
- lastValue = valueSupplier()
- lastValue == expectedValue
+ waitFor(Duration.ofSeconds(20), errorMessageSupplier = { "$errorMessage. Actual value was: $lastValue" }) {
+ val value = valueSupplier()
+ lastValue = value
+ assertion(value)
}
}
\ No newline at end of file
diff --git a/plugin-test/src/test/kotlin/com/github/tarcv/testingteam/surveyoridea/LaunchIdeExtension.kt b/plugin-test/src/test/kotlin/com/github/tarcv/testingteam/surveyoridea/LaunchIdeExtension.kt
index d007b02c..ca185168 100644
--- a/plugin-test/src/test/kotlin/com/github/tarcv/testingteam/surveyoridea/LaunchIdeExtension.kt
+++ b/plugin-test/src/test/kotlin/com/github/tarcv/testingteam/surveyoridea/LaunchIdeExtension.kt
@@ -19,6 +19,19 @@ import java.nio.file.Paths
private var started = false
+private val requestedIdeCode: String
+ get() = getEnvValue("IDE_CODE")
+
+private fun getEnvValue(key: String) = requireNotNull(System.getenv(key)) {
+ "$key environment variable should be set"
+}
+
+val hasJavaSupport: Boolean
+ get() = when (requestedIdeCode) {
+ "AI", "AQ", "IC", "IU" -> true
+ else -> false
+ }
+
class LaunchIdeExtension : BeforeAllCallback, ExtensionContext.Store.CloseableResource {
private lateinit var process: Process
private lateinit var tempDir: Path
@@ -30,12 +43,8 @@ class LaunchIdeExtension : BeforeAllCallback, ExtensionContext.Store.CloseableRe
tempDir = Files.createTempDirectory("junit")
- val requestedIdeCode = requireNotNull(System.getenv("IDE_CODE")) {
- "IDE_CODE environment variable should be set"
- }
- val requestedIdeVersion = requireNotNull(System.getenv("IDE_VERSION")) {
- "IDE_CODE environment variable should be set"
- }
+ val requestedIdeCode = requestedIdeCode
+ val requestedIdeVersion = getEnvValue("IDE_VERSION")
val ideDownloader = IdeDownloader(OkHttpClient())
val cacheDir = getCacheRoot(tempDir)
@@ -48,6 +57,9 @@ class LaunchIdeExtension : BeforeAllCallback, ExtensionContext.Store.CloseableRe
ideDownloader.getIde(ide, requestedIdeVersion, cacheDir)
}
+ // TODO: Remove this once https://github.com/JetBrains/intellij-ui-test-robot/issues/387 is fixed
+ workaroundRemoteIdeIssue(pathToIde)
+
val pluginPath = requireNotNull(System.getenv("PLUGIN_PATH")) {
"PLUGIN_PATH environment variable should be set"
}.let { Paths.get(it) }
@@ -81,6 +93,22 @@ class LaunchIdeExtension : BeforeAllCallback, ExtensionContext.Store.CloseableRe
context.root.getStore(GLOBAL).put(LaunchIdeExtension::class.java.name, this)
}
+ private fun workaroundRemoteIdeIssue(pathToIde: Path) {
+ val binDir = when (Os.hostOS()) {
+ Os.MAC -> pathToIde.resolve("Contents").resolve("bin")
+ else -> pathToIde.resolve("bin")
+ }
+ Files.list(binDir)
+ .filter {
+ it.fileName.toString().endsWith(".vmoptions")
+ && it.fileName.toString().contains("_client")
+ }
+ .forEach {
+ println("Removing unsupported $it")
+ Files.delete(it)
+ }
+ }
+
override fun close() {
try {
kotlin.runCatching {
@@ -122,7 +150,13 @@ class LaunchIdeExtension : BeforeAllCallback, ExtensionContext.Store.CloseableRe
.resolve("${ide.code}-$requestedIdeVersion")
.apply { Files.createDirectories(this) }
val previousArchive = ideCacheDir.toFile().listFiles { f: File -> f.isFile && !f.isHidden }?.singleOrNull()
- val previousExtractedDir = ideCacheDir.toFile().listFiles { f: File -> f.isDirectory }?.singleOrNull()
+
+ val previousExtractedDir = getExtractedIdeDir(ideCacheDir)
+ if (previousExtractedDir != null) {
+ println("File was already downloaded and extracted, so using $previousExtractedDir")
+ return previousExtractedDir.toPath()
+ }
+
return try {
val extractedPath = when (requestedIdeVersion) {
"LATEST_EAP" -> downloadAndExtractLatestEap(ide, ideCacheDir)
@@ -131,25 +165,32 @@ class LaunchIdeExtension : BeforeAllCallback, ExtensionContext.Store.CloseableRe
Ide.BuildType.RELEASE, requestedIdeVersion
)
}
- if (previousArchive != null || previousExtractedDir != null) {
- println("Found stale files from the previous download, deleting...")
- previousArchive?.delete()
- previousExtractedDir?.deleteRecursively()
+ if (previousArchive != null) {
+ println("Found a stale archive from the previous download, deleting...")
+ previousArchive.delete()
println("Deleted")
}
extractedPath
} catch (e: FileAlreadyExistsException) {
- val extractedDir = ideCacheDir.toFile()
- .listFiles { f: File -> f.isDirectory }
- ?.single()
+ val extractedDir = getExtractedIdeDir(ideCacheDir)
?.toPath()
- ?: error("Archive is already download, but extracted dir wasn't found. Please fix the cache.")
+ ?: error("Archive was already downloaded, but extracted dir wasn't found. Please fix the cache.")
println("File was already downloaded, so using $extractedDir")
extractedDir
}
}
+ private fun getExtractedIdeDir(ideCacheDir: Path): File? {
+ val candidateDirs = ideCacheDir.toFile().listFiles { f: File -> f.isDirectory }
+ ?: error("$ideCacheDir should be a directory")
+ return when(candidateDirs.size) {
+ 0 -> null
+ 1 -> candidateDirs.single()
+ else -> error("Found multiple extracted directories under $ideCacheDir, but expected no more then one")
+ }
+ }
+
private fun IdeDownloader.getRobotPlugin(cacheDir: Path) = try {
downloadRobotPlugin(cacheDir)
} catch (e: FileAlreadyExistsException) {
diff --git a/plugin-test/src/test/kotlin/com/github/tarcv/testingteam/surveyoridea/gui/LocateActionUiTests.kt b/plugin-test/src/test/kotlin/com/github/tarcv/testingteam/surveyoridea/gui/LocateActionUiTests.kt
index 6e33b4d8..cc6c0722 100644
--- a/plugin-test/src/test/kotlin/com/github/tarcv/testingteam/surveyoridea/gui/LocateActionUiTests.kt
+++ b/plugin-test/src/test/kotlin/com/github/tarcv/testingteam/surveyoridea/gui/LocateActionUiTests.kt
@@ -4,7 +4,7 @@ import com.github.tarcv.testingteam.surveyoridea.gui.fixtures.IdeaFrame
import com.github.tarcv.testingteam.surveyoridea.gui.fixtures.idea
import com.github.tarcv.testingteam.surveyoridea.gui.fixtures.locateElementToolWindow
import com.github.tarcv.testingteam.surveyoridea.trimAllIndent
-import com.github.tarcv.testingteam.surveyoridea.waitingAssertion
+import com.github.tarcv.testingteam.surveyoridea.waitingAssertEquals
import com.intellij.remoterobot.utils.keyboard
import org.apache.commons.text.StringEscapeUtils
import org.junit.jupiter.api.Test
@@ -81,7 +81,7 @@ class LocateActionUiTests : BaseTestProjectTests() {
}
triggerActionWithBlock()
- waitingAssertion(
+ waitingAssertEquals(
"Correct node should be selected.",
"""
Unit) {
+ find(timeout = Duration.ofSeconds(10)).apply(function)
+}
+
+@FixtureName("Notice frame")
+@DefaultXpath("IdeFrameImpl type", "//div[@class='IdeFrameImpl']")
+class NoticeFrame(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) :
+ CommonContainerFixture(remoteRobot, remoteComponent) {
+
+ val overallIntro
+ get() = step("With the overall intro text area") {
+ return@step findAll(JTextAreaFixture.byType())[0]
+ }
+
+ val noticeIntro
+ get() = step("With the notice intro text area") {
+ return@step findAll(JTextAreaFixture.byType())[1]
+ }
+
+ val noticeText
+ get() = step("With the notice text area") {
+ return@step findAll(JTextAreaFixture.byType())[2]
+ }
+}
\ No newline at end of file
diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts
index d9b0638f..4f7cc567 100644
--- a/plugin/build.gradle.kts
+++ b/plugin/build.gradle.kts
@@ -3,7 +3,7 @@ import org.jetbrains.changelog.markdownToHTML
fun properties(key: String) = providers.gradleProperty(key)
fun environment(key: String) = providers.environmentVariable(key)
-
+sourceSets
plugins {
kotlin("jvm")
id("com.github.TarCV.aar2jar")
@@ -66,6 +66,7 @@ tasks {
buildSearchableOptions {
// Remove once some settings are added
enabled = false
+ notCompatibleWithConfigurationCache("Configuration cache for this task is broken on NixOS")
}
runPluginVerifier {
diff --git a/plugin/src/main/kotlin/com/github/tarcv/testingteam/surveyoridea/gui/JvmLocateToolWindow.kt b/plugin/src/main/kotlin/com/github/tarcv/testingteam/surveyoridea/gui/JvmLocateToolWindow.kt
new file mode 100644
index 00000000..efad0b57
--- /dev/null
+++ b/plugin/src/main/kotlin/com/github/tarcv/testingteam/surveyoridea/gui/JvmLocateToolWindow.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2024 TarCV
+ *
+ * This file is part of UI Surveyor.
+ * UI Surveyor is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.github.tarcv.testingteam.surveyoridea.gui
+
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiSelector
+import com.intellij.codeInsight.daemon.impl.analysis.JavaModuleGraphUtil
+import com.intellij.ide.highlighter.JavaFileType
+import com.intellij.openapi.application.invokeLater
+import com.intellij.openapi.fileTypes.LanguageFileType
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.module.ModuleManager
+import com.intellij.openapi.module.ModuleTypeId
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.project.modifyModules
+import com.intellij.openapi.projectRoots.JavaSdkType
+import com.intellij.openapi.roots.ModuleRootModificationUtil
+import com.intellij.openapi.roots.OrderRootType
+import com.intellij.openapi.roots.ProjectRootManager
+import com.intellij.openapi.vfs.VfsUtil
+import com.intellij.psi.JavaCodeFragment
+import com.intellij.psi.JavaCodeFragmentFactory
+import com.intellij.psi.PsiDocumentManager
+import com.intellij.psi.PsiFile
+import com.intellij.ui.EditorTextField
+import java.net.URI
+import java.nio.file.Path
+import java.nio.file.Paths
+
+@Suppress("UnstableApiUsage", "unused") // Used by string reference in LocateToolWindowFactory.createToolWindowContent
+class JvmLocateToolWindow(project: Project) : LocateToolWindow(project) {
+ companion object {
+ const val OLD_MODULE_NAME = "UISurveyor_Highlighting"
+ const val MODULE_NAME = "__UISurveyor_Highlighting"
+ const val HIGHLIGHTING_LIBRARY_NAME = "uiautomator"
+ }
+
+ private val locatorFragment: PsiFile?
+ get() {
+ val docField = locatorField as EditorTextField
+ return PsiDocumentManager.getInstance(project).getPsiFile(docField.document)
+ }
+
+ override val fileType: LanguageFileType = JavaFileType.INSTANCE
+
+ override fun initSelectorField(editorField: EditorTextField) {
+ invokeLater {
+ removeModuleIfExists(OLD_MODULE_NAME)
+ removeModuleIfExists(MODULE_NAME)
+ val module = createModuleForHighlighting()
+
+ val modulePsi = JavaModuleGraphUtil.findDescriptorByModule(module, false)
+
+ val importedClasses = listOf(UiSelector::class.java, By::class.java)
+ .joinToString(",") { it.name }
+ val editorCode = JavaCodeFragmentFactory.getInstance(project).createExpressionCodeFragment(
+ editorField.text,
+ modulePsi,
+ null,
+ true
+ ).apply {
+ addImportsFromString(importedClasses)
+ }
+
+ editorField.document = PsiDocumentManager.getInstance(project).getDocument(editorCode)!!
+ }
+ }
+
+ private fun removeModuleIfExists(name: String) {
+ val module = ModuleManager.getInstance(project).findModuleByName(name)
+ ?: return
+ project.modifyModules {
+ var isHighlightingModule = false
+ ModuleRootModificationUtil.modifyModel(module) { model ->
+ isHighlightingModule = model.moduleLibraryTable.libraries
+ .singleOrNull()
+ ?.name == HIGHLIGHTING_LIBRARY_NAME
+ false
+ }
+ if (isHighlightingModule) {
+ disposeModule(module)
+ }
+ }
+ }
+
+ private fun createModuleForHighlighting(): Module {
+ val module: Module = project.modifyModules {
+ newNonPersistentModule(
+ MODULE_NAME,
+ ModuleTypeId.JAVA_MODULE
+ )
+ }
+
+ project.modifyModules {
+ ModuleRootModificationUtil.updateModel(module) { model ->
+ val projectSdk = ProjectRootManager.getInstance(project).projectSdk
+ if (projectSdk == null || projectSdk.sdkType is JavaSdkType) {
+ model.inheritSdk()
+ }
+ }
+ }
+
+ project.modifyModules {
+ ModuleRootModificationUtil.updateModel(module) { model ->
+ val automatorClass = UiSelector::class.java
+ val automatorJarFile = getAutomatorJarPath(automatorClass)
+ model.moduleLibraryTable
+ .createLibrary(HIGHLIGHTING_LIBRARY_NAME)
+ .modifiableModel.apply {
+ addRoot(
+ VfsUtil.getUrlForLibraryRoot(automatorJarFile.toFile()),
+ OrderRootType.CLASSES
+ )
+
+ commit()
+ }
+ }
+ module
+ }
+ return module
+ }
+
+ private fun getAutomatorJarPath(automatorClass: Class): Path {
+ val classPath = automatorClass.name.replace('.', '/') + ".class"
+ val classUrl = automatorClass.classLoader.getResource(classPath)!!.toExternalForm()
+ val jarUrl = classUrl
+ .removeSuffix(classPath)
+ .removeSuffix("/")
+ .removeSuffix("!")
+ .replaceFirst(Regex("^.*(?=file:)"), "")
+ return Paths.get(URI(jarUrl))
+ }
+
+ override fun getCurrentLocator(): String {
+ val fragment = locatorFragment
+ val imports = if (fragment is JavaCodeFragment) {
+ fragment.importsToString()
+ .split(Regex("""\s*,\s*"""))
+ .joinToString("") { "import $it; " }
+ } else {
+ ""
+ }
+ return imports + (fragment?.text ?: "")
+ }
+}
\ No newline at end of file
diff --git a/plugin/src/main/kotlin/com/github/tarcv/testingteam/surveyoridea/gui/LocateToolWindow.kt b/plugin/src/main/kotlin/com/github/tarcv/testingteam/surveyoridea/gui/LocateToolWindow.kt
index da3d04b0..db89247a 100644
--- a/plugin/src/main/kotlin/com/github/tarcv/testingteam/surveyoridea/gui/LocateToolWindow.kt
+++ b/plugin/src/main/kotlin/com/github/tarcv/testingteam/surveyoridea/gui/LocateToolWindow.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 TarCV
+ * Copyright (C) 2024 TarCV
*
* This file is part of UI Surveyor.
* UI Surveyor is free software: you can redistribute it and/or modify
@@ -17,61 +17,31 @@
*/
package com.github.tarcv.testingteam.surveyoridea.gui
-import androidx.test.uiautomator.By
-import androidx.test.uiautomator.UiSelector
import com.github.tarcv.testingteam.surveyoridea.services.LocateToolHoldingService
-import com.intellij.codeInsight.daemon.impl.analysis.JavaModuleGraphUtil
-import com.intellij.ide.highlighter.JavaFileType
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CustomShortcutSet
-import com.intellij.openapi.application.invokeLater
-import com.intellij.openapi.module.Module
-import com.intellij.openapi.module.ModuleManager
-import com.intellij.openapi.module.ModuleTypeId
+import com.intellij.openapi.fileTypes.LanguageFileType
import com.intellij.openapi.project.Project
-import com.intellij.openapi.project.modifyModules
-import com.intellij.openapi.projectRoots.JavaSdkType
-import com.intellij.openapi.roots.ModuleRootModificationUtil
-import com.intellij.openapi.roots.OrderRootType
-import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.ui.playback.commands.ActionCommand
import com.intellij.openapi.util.SystemInfo
-import com.intellij.openapi.vfs.VfsUtil
-import com.intellij.psi.JavaCodeFragment
-import com.intellij.psi.JavaCodeFragmentFactory
-import com.intellij.psi.PsiDocumentManager
-import com.intellij.psi.PsiFile
import com.intellij.ui.EditorTextField
import java.awt.event.InputEvent
import java.awt.event.KeyEvent
-import java.net.URI
-import java.nio.file.Path
-import java.nio.file.Paths
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.KeyStroke
-@Suppress("UnstableApiUsage")
-class LocateToolWindow(private val project: Project) {
+abstract class LocateToolWindow(protected val project: Project) {
private lateinit var content: JPanel
- private val locatorFragment: PsiFile?
- get() {
- val docField = locatorField as EditorTextField
- return PsiDocumentManager.getInstance(project).getPsiFile(docField.document)
- }
- private lateinit var locatorField: JPanel
+ protected lateinit var locatorField: JPanel
private lateinit var toolbar: JComponent
- companion object {
- const val oldModuleName = "UISurveyor_Highlighting"
- const val moduleName = "__UISurveyor_Highlighting"
- const val highlightingLibraryName = "uiautomator"
- }
+ protected abstract val fileType: LanguageFileType
fun createUIComponents() {
val actionToolbar = with(ActionManager.getInstance()) {
@@ -83,7 +53,7 @@ class LocateToolWindow(private val project: Project) {
}
toolbar = actionToolbar.component
- val editorField = EditorTextField("new UiSelector()", project, JavaFileType.INSTANCE)
+ val editorField = EditorTextField("new UiSelector()", project, fileType)
val locateFromKeyboardAction = object : AnAction("Evaluate") {
override fun actionPerformed(e: AnActionEvent) {
val actionManager = ActionManager.getInstance()
@@ -107,108 +77,16 @@ class LocateToolWindow(private val project: Project) {
editorField
)
- invokeLater {
- removeModuleIfExists(oldModuleName)
- removeModuleIfExists(moduleName)
- val module = createModuleForHighlighting()
-
- val modulePsi = JavaModuleGraphUtil.findDescriptorByModule(module, false)
-
- val importedClasses = listOf(UiSelector::class.java, By::class.java)
- .joinToString(",") { it.name }
- val editorCode = JavaCodeFragmentFactory.getInstance(project).createExpressionCodeFragment(
- editorField.text,
- modulePsi,
- null,
- true
- ).apply {
- addImportsFromString(importedClasses)
- }
-
- editorField.document = PsiDocumentManager.getInstance(project).getDocument(editorCode)!!
- }
+ initSelectorField(editorField)
project.getService(LocateToolHoldingService::class.java).registerToolWindow(this)
actionToolbar.setTargetComponent(editorField)
locatorField = editorField
}
- private fun removeModuleIfExists(name: String) {
- val module = ModuleManager.getInstance(project).findModuleByName(name)
- ?: return
- project.modifyModules {
- var isHighlightingModule = false
- ModuleRootModificationUtil.modifyModel(module) { model ->
- isHighlightingModule = model.moduleLibraryTable.libraries
- .singleOrNull()
- ?.name == highlightingLibraryName
- false
- }
- if (isHighlightingModule) {
- disposeModule(module)
- }
- }
- }
-
- private fun createModuleForHighlighting(): Module {
- val module: Module = project.modifyModules {
- newNonPersistentModule(
- moduleName,
- ModuleTypeId.JAVA_MODULE
- )
- }
-
- project.modifyModules {
- ModuleRootModificationUtil.updateModel(module) { model ->
- val projectSdk = ProjectRootManager.getInstance(project).projectSdk
- if (projectSdk == null || projectSdk.sdkType is JavaSdkType) {
- model.inheritSdk()
- }
- }
- }
-
- project.modifyModules {
- ModuleRootModificationUtil.updateModel(module) { model ->
- val automatorClass = UiSelector::class.java
- val automatorJarFile = getAutomatorJarPath(automatorClass)
- model.moduleLibraryTable
- .createLibrary(highlightingLibraryName)
- .modifiableModel.apply {
- addRoot(
- VfsUtil.getUrlForLibraryRoot(automatorJarFile.toFile()),
- OrderRootType.CLASSES
- )
-
- commit()
- }
- }
- module
- }
- return module
- }
-
- private fun getAutomatorJarPath(automatorClass: Class): Path {
- val classPath = automatorClass.name.replace('.', '/') + ".class"
- val classUrl = automatorClass.classLoader.getResource(classPath)!!.toExternalForm()
- val jarUrl = classUrl
- .removeSuffix(classPath)
- .removeSuffix("/")
- .removeSuffix("!")
- .replaceFirst(Regex("^.*(?=file:)"), "")
- return Paths.get(URI(jarUrl))
- }
+ protected abstract fun initSelectorField(editorField: EditorTextField)
fun getContent(): JPanel = content
- fun getCurrentLocator(): String {
- val fragment = locatorFragment
- val imports = if (fragment is JavaCodeFragment) {
- fragment.importsToString()
- .split(Regex("""\s*,\s*"""))
- .joinToString("") { "import $it; " }
- } else {
- ""
- }
- return imports + (fragment?.text ?: "")
- }
+ abstract fun getCurrentLocator(): String
}
diff --git a/plugin/src/main/kotlin/com/github/tarcv/testingteam/surveyoridea/gui/LocateToolWindowFactory.kt b/plugin/src/main/kotlin/com/github/tarcv/testingteam/surveyoridea/gui/LocateToolWindowFactory.kt
index 887e756d..47b9f266 100644
--- a/plugin/src/main/kotlin/com/github/tarcv/testingteam/surveyoridea/gui/LocateToolWindowFactory.kt
+++ b/plugin/src/main/kotlin/com/github/tarcv/testingteam/surveyoridea/gui/LocateToolWindowFactory.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 TarCV
+ * Copyright (C) 2024 TarCV
*
* This file is part of UI Surveyor.
* UI Surveyor is free software: you can redistribute it and/or modify
@@ -23,12 +23,27 @@ import com.intellij.openapi.wm.ToolWindowFactory
import com.intellij.ui.content.ContentFactory
-class LocateToolWindowFactory: ToolWindowFactory {
+class LocateToolWindowFactory : ToolWindowFactory {
private val contentFactory = ContentFactory.SERVICE.getInstance()
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
- val locateToolWindow = LocateToolWindow(project)
+ val locateToolWindow: LocateToolWindow = if (hasJavaPlugin()) {
+ Class.forName("com.github.tarcv.testingteam.surveyoridea.gui.JvmLocateToolWindow")
+ .getConstructor(Project::class.java)
+ .newInstance(project) as LocateToolWindow
+ } else {
+ SimpleLocateToolWindow(project)
+ }
val content = contentFactory.createContent(locateToolWindow.getContent(), null, false)
toolWindow.contentManager.addContent(content)
}
-}
+
+ private fun hasJavaPlugin(): Boolean {
+ return try {
+ Class.forName("com.intellij.psi.JavaCodeFragment", false, javaClass.classLoader)
+ true
+ } catch (e: ReflectiveOperationException) {
+ false
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugin/src/main/kotlin/com/github/tarcv/testingteam/surveyoridea/gui/SimpleLocateToolWindow.kt b/plugin/src/main/kotlin/com/github/tarcv/testingteam/surveyoridea/gui/SimpleLocateToolWindow.kt
new file mode 100644
index 00000000..9ce5813f
--- /dev/null
+++ b/plugin/src/main/kotlin/com/github/tarcv/testingteam/surveyoridea/gui/SimpleLocateToolWindow.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 TarCV
+ *
+ * This file is part of UI Surveyor.
+ * UI Surveyor is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package com.github.tarcv.testingteam.surveyoridea.gui
+
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiSelector
+import com.intellij.openapi.fileTypes.LanguageFileType
+import com.intellij.openapi.fileTypes.PlainTextFileType
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.NlsSafe
+import com.intellij.psi.PsiDocumentManager
+import com.intellij.ui.EditorTextField
+
+@Suppress("UnstableApiUsage")
+class SimpleLocateToolWindow(project: Project) : LocateToolWindow(project) {
+ override val fileType: LanguageFileType = PlainTextFileType.INSTANCE
+
+ private val locatorText: @NlsSafe String
+ get() {
+ val docField = locatorField as EditorTextField
+ @Suppress("USELESS_ELVIS")
+ return PsiDocumentManager.getInstance(project).getPsiFile(docField.document)
+ ?.text
+ ?: docField.text
+ ?: ""
+ }
+
+ override fun initSelectorField(editorField: EditorTextField) {
+ // No-op
+ }
+
+ override fun getCurrentLocator(): String {
+ val fragment = locatorText
+ val imports = listOf(UiSelector::class.java, By::class.java)
+ .joinToString("") { "import ${it.name}; " }
+ return imports + fragment
+ }
+}
\ No newline at end of file
diff --git a/plugin/src/main/resources/META-INF/com.github.tarcv.testingteam.surveyoridea-withJava.xml b/plugin/src/main/resources/META-INF/com.github.tarcv.testingteam.surveyoridea-withJava.xml
new file mode 100644
index 00000000..82d8b833
--- /dev/null
+++ b/plugin/src/main/resources/META-INF/com.github.tarcv.testingteam.surveyoridea-withJava.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/plugin/src/main/resources/META-INF/plugin.xml b/plugin/src/main/resources/META-INF/plugin.xml
index 46855b0c..1a3dc2b1 100644
--- a/plugin/src/main/resources/META-INF/plugin.xml
+++ b/plugin/src/main/resources/META-INF/plugin.xml
@@ -11,7 +11,7 @@
com.intellij.modules.xml
- com.intellij.java
+ com.intellij.java