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