mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
Support Lux for UI Test framework, prototype
Fix IdeaUltimatePackagingTest Fix RemoteIdeaEduBuildTest Fix cwm plugin scrambling Fix layout issues, fix conflicts Fix compilation after rebase Fix layout issues Change jdk to inherited Transform intelij.cwm.guest.driver to plugin, move LuxHostRobotService to separate module Fix conflicts [lux] [ui-test-framework] Fix SDK for cwm.guest.driver [lux] [ui-test-framework] Fix build of performance testing plugin Fix conflicts Cleanup code A little renaming Move remoteDriverGuest to remote-dev Pass isRemoteIdeMode flag to DriverImpl Fix robot smoothness Small fixes, remove dead code, fixed review issues [ui-test-framework] Make remoteDriver possible to work in remdev mode [lux] [ui-test-framework] Implement LuxXpathDataModelExtension, which can gather ui hierarchy from backend [lux] [ui-test-framework] Implement LuxRobot, a backend-side service, which allows to propagate input events to frontend Co-authored-by: Yuriy Artamonov <Yuriy.Artamonov@jetbrains.com> Merge-request: IJ-MR-126806 Merged-by: Sviatoslav Vlasov <Sviatoslav.Vlasov@jetbrains.com> GitOrigin-RevId: c5cb13288d1b03cfdf343939b20c92c353ce1959
This commit is contained in:
committed by
intellij-monorepo-bot
co-authored by
Yuriy Artamonov
parent
157e636bea
commit
2da602139a
+2
-1
@@ -308,7 +308,8 @@ object CommunityRepositoryModules {
|
||||
) { spec ->
|
||||
spec.withModule("intellij.turboComplete.languages.kotlin")
|
||||
},
|
||||
pluginAuto(listOf("intellij.performanceTesting", "intellij.performanceTesting.remoteDriver")) { spec ->
|
||||
pluginAuto(listOf("intellij.performanceTesting",
|
||||
"intellij.performanceTesting.remoteDriver")) { spec ->
|
||||
spec.withModule("intellij.driver.model")
|
||||
spec.withModule("intellij.driver.impl")
|
||||
spec.withModule("intellij.driver.client")
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
@file:Suppress("JAVA_MODULE_DOES_NOT_EXPORT_PACKAGE")
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.platform.impl.toolkit
|
||||
|
||||
import com.intellij.openapi.components.service
|
||||
import java.awt.*
|
||||
import java.awt.peer.DialogPeer
|
||||
import java.awt.peer.FramePeer
|
||||
import java.awt.peer.RobotPeer
|
||||
import java.awt.peer.WindowPeer
|
||||
|
||||
interface ClientToolkit {
|
||||
@@ -15,4 +16,5 @@ interface ClientToolkit {
|
||||
fun createWindow(target: Window): WindowPeer
|
||||
fun createDialog(target: Dialog): DialogPeer
|
||||
fun createFrame(target: Frame): FramePeer
|
||||
fun createRobot(screen: GraphicsDevice?): RobotPeer
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
@file:Suppress("JAVA_MODULE_DOES_NOT_EXPORT_PACKAGE")
|
||||
package com.intellij.platform.impl.toolkit
|
||||
|
||||
import java.awt.Component
|
||||
import java.awt.GraphicsEnvironment
|
||||
import java.awt.Point
|
||||
import java.awt.Window
|
||||
import java.awt.event.MouseEvent
|
||||
@@ -10,16 +11,15 @@ import java.awt.peer.MouseInfoPeer
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
object IdeMouseInfoPeer: MouseInfoPeer {
|
||||
private const val PRIMARY_SCREEN_DEVICE_ID = 0
|
||||
|
||||
private val lastMouseCoords = Point()
|
||||
private var lastWindowUnderMouse: Component? = null
|
||||
private var lastComponentUnderCursor: Component? = null
|
||||
|
||||
override fun fillPointWithCoords(point: Point): Int {
|
||||
point.location = lastMouseCoords
|
||||
// TODO: return correct screen id
|
||||
return PRIMARY_SCREEN_DEVICE_ID
|
||||
return GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices().indexOfFirst {
|
||||
it.defaultConfiguration.bounds.contains(point)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isWindowUnderMouse(w: Window): Boolean {
|
||||
@@ -29,7 +29,7 @@ object IdeMouseInfoPeer: MouseInfoPeer {
|
||||
fun getComponentUnderCursor(): Component? = lastComponentUnderCursor
|
||||
|
||||
fun processMouseEvent(event: MouseEvent, window: Window) {
|
||||
lastMouseCoords.setLocation(event.x, event.y)
|
||||
lastMouseCoords.setLocation(event.xOnScreen, event.yOnScreen)
|
||||
lastWindowUnderMouse = window
|
||||
val component = SwingUtilities.getDeepestComponentAt(window, event.x, event.y)
|
||||
lastComponentUnderCursor = component
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
@file:Suppress("JAVA_MODULE_DOES_NOT_EXPORT_PACKAGE")
|
||||
package com.intellij.platform.impl.toolkit
|
||||
|
||||
@@ -47,6 +47,9 @@ class IdeToolkit : SunToolkit() {
|
||||
override fun createFrame(target: Frame): FramePeer = clientInstance().createFrame(target)
|
||||
override fun getSystemClipboard(): Clipboard = clipboard
|
||||
|
||||
override fun createRobot(screen: GraphicsDevice?): RobotPeer? = clientInstance().createRobot(screen)
|
||||
override fun getMouseInfoPeer(): IdeMouseInfoPeer = IdeMouseInfoPeer
|
||||
|
||||
override fun getScreenResolution(): Int = 96
|
||||
override fun prepareImage(img: Image, w: Int, h: Int, o: ImageObserver?): Boolean {
|
||||
//TODO: probably can be used for caching
|
||||
@@ -54,7 +57,6 @@ class IdeToolkit : SunToolkit() {
|
||||
}
|
||||
|
||||
override fun createDesktopPeer(target: Desktop): DesktopPeer = IdeDesktopPeer()
|
||||
override fun getMouseInfoPeer(): IdeMouseInfoPeer = IdeMouseInfoPeer
|
||||
override fun getKeyboardFocusManagerPeer(): IdeKeyboardFocusManagerPeer = IdeKeyboardFocusManagerPeer
|
||||
|
||||
// Toolkit interface which shouldn't be used
|
||||
|
||||
@@ -67,6 +67,8 @@ interface Driver : AutoCloseable {
|
||||
*/
|
||||
val isConnected: Boolean
|
||||
|
||||
val isRemoteIdeMode: Boolean
|
||||
|
||||
/**
|
||||
* @return information about the product under test
|
||||
*/
|
||||
@@ -91,24 +93,28 @@ interface Driver : AutoCloseable {
|
||||
* @return new remote proxy for a [Remote] application service interface
|
||||
*/
|
||||
@Contract(pure = true)
|
||||
fun <T : Any> service(clazz: KClass<T>): T
|
||||
fun <T : Any> service(clazz: KClass<T>, forceRunOnBackend: Boolean = false): T
|
||||
|
||||
/**
|
||||
* @return new remote proxy for a [Remote] project service interface
|
||||
*/
|
||||
@Contract(pure = true)
|
||||
fun <T : Any> service(clazz: KClass<T>, project: ProjectRef): T
|
||||
fun <T : Any> service(clazz: KClass<T>, project: ProjectRef?, forceRunOnBackend: Boolean = false): T
|
||||
|
||||
/**
|
||||
* @return new remote proxy for a utility class or a class with static methods
|
||||
*/
|
||||
@Contract(pure = true)
|
||||
fun <T : Any> utility(clazz: KClass<T>): T
|
||||
fun <T : Any> utility(clazz: KClass<T>, forceRunOnBackend: Boolean = false): T
|
||||
|
||||
/**
|
||||
* @return proxy reference for a newly created remote object
|
||||
*/
|
||||
fun <T : Any> new(clazz: KClass<T>, vararg args: Any?): T
|
||||
fun <T : Any> new(clazz: KClass<T>, vararg args: Any?): T {
|
||||
return new(clazz, *args, forceRunOnBackend = false)
|
||||
}
|
||||
|
||||
fun <T : Any> new(clazz: KClass<T>, vararg args: Any?, forceRunOnBackend: Boolean = false): T
|
||||
|
||||
/**
|
||||
* Assumes that the remote reference corresponds to another type. Performs unsafe cast.
|
||||
@@ -140,8 +146,8 @@ interface Driver : AutoCloseable {
|
||||
*/
|
||||
@JvmStatic
|
||||
@Contract(pure = true)
|
||||
fun create(host: JmxHost? = JmxHost(null, null, "localhost:7777")): Driver {
|
||||
return DriverImpl(host)
|
||||
fun create(host: JmxHost? = JmxHost(null, null, "localhost:7777"), isRemoteIdeMode: Boolean = false): Driver {
|
||||
return DriverImpl(host, isRemoteIdeMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,20 +160,20 @@ interface ProjectRef
|
||||
/**
|
||||
* @return new remote proxy for a [Remote] application service interface
|
||||
*/
|
||||
inline fun <reified T : Any> Driver.service(): T {
|
||||
return service(T::class)
|
||||
inline fun <reified T : Any> Driver.service(forceRunOnBackend: Boolean = false): T {
|
||||
return service(T::class, forceRunOnBackend)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return new remote proxy for a [Remote] application service interface
|
||||
*/
|
||||
inline fun <reified T : Any> Driver.service(project: ProjectRef): T {
|
||||
return service(T::class, project)
|
||||
inline fun <reified T : Any> Driver.service(project: ProjectRef, forceRunOnBackend: Boolean = false): T {
|
||||
return service(T::class, project, forceRunOnBackend)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return new remote proxy for a utility class or a class with static methods
|
||||
*/
|
||||
inline fun <reified T : Any> Driver.utility(): T {
|
||||
return utility(T::class)
|
||||
}
|
||||
inline fun <reified T : Any> Driver.utility(forceRunOnBackend: Boolean = false): T {
|
||||
return utility(T::class, forceRunOnBackend)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.intellij.driver.client
|
||||
|
||||
import com.intellij.driver.model.RdTarget
|
||||
import org.intellij.lang.annotations.Language
|
||||
|
||||
/**
|
||||
@@ -25,5 +26,12 @@ annotation class Remote(
|
||||
* Identifier of a plugin where the class is located, e.g. `com.intellij.spring`.
|
||||
* If the class is declared in a module of a plugin (not main), use the following format: `some.plugin.id/some.plugin.id.submodule`.
|
||||
*/
|
||||
val plugin: String = ""
|
||||
val plugin: String = "",
|
||||
|
||||
/**
|
||||
* Determine the semantics for the remote call in case of Remote IDE.
|
||||
*/
|
||||
val rdTarget: RdTarget = RdTarget.FRONTEND_FIRST,
|
||||
|
||||
val isControllerSession: Boolean = false
|
||||
)
|
||||
@@ -7,7 +7,9 @@ import com.intellij.driver.client.Timed
|
||||
import com.intellij.driver.model.LockSemantics
|
||||
import com.intellij.driver.model.OnDispatcher
|
||||
import com.intellij.driver.model.ProductVersion
|
||||
import com.intellij.driver.model.RdTarget
|
||||
import com.intellij.driver.model.transport.*
|
||||
import java.lang.IllegalStateException
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Proxy
|
||||
@@ -17,13 +19,13 @@ import javax.management.AttributeNotFoundException
|
||||
import javax.management.InstanceNotFoundException
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
internal class DriverImpl(host: JmxHost?) : Driver {
|
||||
class DriverImpl(host: JmxHost?, override val isRemoteIdeMode: Boolean) : Driver {
|
||||
private val invoker: Invoker = JmxCallHandler.jmx(Invoker::class.java, host)
|
||||
private val sessionHolder = ThreadLocal<Session>()
|
||||
|
||||
private val appServices: MutableMap<Class<*>, Any> = ConcurrentHashMap()
|
||||
private val appServices: MutableMap<AppServiceId, Any> = ConcurrentHashMap()
|
||||
private val projectServices: MutableMap<ProjectServiceId, Any> = ConcurrentHashMap()
|
||||
private val utils: MutableMap<Class<*>, Any> = ConcurrentHashMap()
|
||||
private val utils: MutableMap<UtilityId, Any> = ConcurrentHashMap()
|
||||
|
||||
override val isConnected: Boolean
|
||||
get() {
|
||||
@@ -42,6 +44,8 @@ internal class DriverImpl(host: JmxHost?) : Driver {
|
||||
}
|
||||
}
|
||||
|
||||
fun getInvoker() = invoker
|
||||
|
||||
override fun getProductVersion(): ProductVersion {
|
||||
return invoker.getProductVersion()
|
||||
}
|
||||
@@ -55,26 +59,27 @@ internal class DriverImpl(host: JmxHost?) : Driver {
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : Any> service(clazz: KClass<T>): T {
|
||||
return appServices.computeIfAbsent(clazz.java, ::serviceBridge) as T
|
||||
override fun <T : Any> service(clazz: KClass<T>, isBackendService: Boolean): T {
|
||||
return appServices.computeIfAbsent(AppServiceId(clazz.java, isBackendService)) { serviceBridge(clazz.java, null, isBackendService) } as T
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : Any> service(clazz: KClass<T>, project: ProjectRef): T {
|
||||
val id = ProjectServiceId((project as RefWrapper).getRef().identityHashCode, clazz.java)
|
||||
return projectServices.computeIfAbsent(id) { serviceBridge(clazz.java, project) } as T
|
||||
override fun <T : Any> service(clazz: KClass<T>, project: ProjectRef?, isBackendService: Boolean): T {
|
||||
val id = ProjectServiceId((project as RefWrapper).getRef().identityHashCode, clazz.java, isBackendService)
|
||||
return projectServices.computeIfAbsent(id) { serviceBridge(clazz.java, project, isBackendService) } as T
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : Any> utility(clazz: KClass<T>): T {
|
||||
return utils.computeIfAbsent(clazz.java, ::utilityBridge) as T
|
||||
override fun <T : Any> utility(clazz: KClass<T>, isBackendUtility: Boolean): T {
|
||||
return utils.computeIfAbsent(UtilityId(clazz.java, isBackendUtility)) { utilityBridge(clazz.java, isBackendUtility) } as T
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : Any> new(clazz: KClass<T>, vararg args: Any?): T {
|
||||
override fun <T : Any> new(clazz: KClass<T>, vararg args: Any?, forceRunOnBackend: Boolean): T {
|
||||
val remote = findRemoteMeta(clazz.java)
|
||||
?: throw IllegalArgumentException("Class $clazz is not annotated with @Remote annotation")
|
||||
|
||||
val rdTarget = checkRdTargetConsistencyAndGetMostSuitable(clazz.java, if (forceRunOnBackend) RdTarget.BACKEND_ONLY else remote.rdTarget, *args)
|
||||
val (sessionId, dispatcher, semantics) = sessionHolder.get() ?: NO_SESSION
|
||||
val call = NewInstanceCall(
|
||||
sessionId,
|
||||
@@ -83,6 +88,7 @@ internal class DriverImpl(host: JmxHost?) : Driver {
|
||||
dispatcher,
|
||||
semantics,
|
||||
remote.value,
|
||||
if (rdTarget == RdTarget.FRONTEND_FIRST) RdTarget.FRONTEND_ONLY else rdTarget,
|
||||
convertArgsToPass(args)
|
||||
)
|
||||
val callResult = makeCall(call)
|
||||
@@ -156,48 +162,23 @@ internal class DriverImpl(host: JmxHost?) : Driver {
|
||||
return value // let's hope we will be able to cast it
|
||||
}
|
||||
|
||||
private fun serviceBridge(clazz: Class<*>): Any {
|
||||
val remote = findRemoteMeta(clazz) ?: throw notAnnotatedError(clazz)
|
||||
|
||||
return Proxy.newProxyInstance(getClassLoader(), arrayOf(clazz)) { proxy: Any?, method: Method, args: Array<Any?>? ->
|
||||
when (method.name) {
|
||||
"equals" -> proxy === args?.firstOrNull()
|
||||
"hashCode" -> clazz.hashCode()
|
||||
"toString" -> "@Service " + remote.value
|
||||
else -> {
|
||||
val (sessionId, dispatcher, semantics) = sessionHolder.get() ?: NO_SESSION
|
||||
val call = ServiceCall(
|
||||
sessionId,
|
||||
findTimedMeta(method)?.value,
|
||||
getPluginId(remote),
|
||||
dispatcher,
|
||||
semantics,
|
||||
remote.value,
|
||||
method.name,
|
||||
convertArgsToPass(args),
|
||||
null,
|
||||
remote.serviceInterface.takeIf { it.isNotBlank() }
|
||||
)
|
||||
val callResult = makeCall(call)
|
||||
convertResult(callResult, method, getPluginId(remote))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPluginId(remote: Remote): String? {
|
||||
return remote.plugin.takeIf { it.isNotBlank() }
|
||||
}
|
||||
|
||||
private fun serviceBridge(clazz: Class<*>, project: ProjectRef): Any {
|
||||
private fun serviceBridge(clazz: Class<*>, project: ProjectRef? = null, forceRunOnBackend: Boolean = false): Any {
|
||||
val remote = findRemoteMeta(clazz) ?: throw notAnnotatedError(clazz)
|
||||
|
||||
return Proxy.newProxyInstance(getClassLoader(), arrayOf(clazz)) { proxy: Any?, method: Method, args: Array<Any?>? ->
|
||||
val rdTarget = if (forceRunOnBackend) RdTarget.BACKEND_ONLY else remote.rdTarget
|
||||
val isControllerSession = remote.isControllerSession
|
||||
|
||||
return Proxy.newProxyInstance(clazz.classLoader, arrayOf(clazz)) { proxy: Any?, method: Method, args: Array<Any?>? ->
|
||||
when (method.name) {
|
||||
"equals" -> proxy === args?.firstOrNull()
|
||||
"hashCode" -> clazz.hashCode()
|
||||
"toString" -> "@Service(APP) " + remote.value
|
||||
else -> {
|
||||
val rdTarget = checkRdTargetConsistencyAndGetMostSuitable(clazz, rdTarget, args)
|
||||
val (sessionId, dispatcher, semantics) = sessionHolder.get() ?: NO_SESSION
|
||||
val call = ServiceCall(
|
||||
sessionId,
|
||||
@@ -208,8 +189,9 @@ internal class DriverImpl(host: JmxHost?) : Driver {
|
||||
remote.value,
|
||||
method.name,
|
||||
convertArgsToPass(args),
|
||||
(project as RefWrapper).getRef(),
|
||||
remote.serviceInterface.takeIf { it.isNotBlank() }
|
||||
(project as? RefWrapper?)?.getRef(),
|
||||
remote.serviceInterface.takeIf { it.isNotBlank() },
|
||||
rdTarget, isControllerSession
|
||||
)
|
||||
val callResult = makeCall(call)
|
||||
convertResult(callResult, method, getPluginId(remote))
|
||||
@@ -227,15 +209,17 @@ internal class DriverImpl(host: JmxHost?) : Driver {
|
||||
}
|
||||
}
|
||||
|
||||
private fun utilityBridge(clazz: Class<*>): Any {
|
||||
private fun utilityBridge(clazz: Class<*>, forceRunOnBackend: Boolean): Any {
|
||||
val remote = findRemoteMeta(clazz) ?: throw notAnnotatedError(clazz)
|
||||
|
||||
return Proxy.newProxyInstance(getClassLoader(), arrayOf(clazz)) { proxy: Any?, method: Method, args: Array<Any?>? ->
|
||||
val rdTarget = if (forceRunOnBackend) RdTarget.BACKEND_ONLY else remote.rdTarget
|
||||
return Proxy.newProxyInstance(clazz.classLoader, arrayOf(clazz)) { proxy: Any?, method: Method, args: Array<Any?>? ->
|
||||
when (method.name) {
|
||||
"equals" -> proxy === args?.firstOrNull()
|
||||
"hashCode" -> clazz.hashCode()
|
||||
"toString" -> "Utility " + remote.value
|
||||
else -> {
|
||||
val rdTarget = checkRdTargetConsistencyAndGetMostSuitable(clazz, rdTarget, args)
|
||||
val (sessionId, dispatcher, semantics) = sessionHolder.get() ?: NO_SESSION
|
||||
val call = UtilityCall(
|
||||
sessionId,
|
||||
@@ -245,7 +229,8 @@ internal class DriverImpl(host: JmxHost?) : Driver {
|
||||
semantics,
|
||||
remote.value,
|
||||
method.name,
|
||||
convertArgsToPass(args)
|
||||
rdTarget,
|
||||
convertArgsToPass(args),
|
||||
)
|
||||
val callResult = makeCall(call)
|
||||
convertResult(callResult, method, getPluginId(remote))
|
||||
@@ -257,7 +242,7 @@ internal class DriverImpl(host: JmxHost?) : Driver {
|
||||
private fun refBridge(clazz: Class<*>, ref: Ref, pluginId: String?): Any {
|
||||
val remote = findRemoteMeta(clazz) ?: throw notAnnotatedError(clazz)
|
||||
|
||||
return Proxy.newProxyInstance(getClassLoader(),
|
||||
return Proxy.newProxyInstance(clazz.classLoader,
|
||||
arrayOf(clazz, RefWrapper::class.java)) { proxy: Any?, method: Method, args: Array<Any?>? ->
|
||||
when (method.name) {
|
||||
"equals" -> proxy === args?.firstOrNull()
|
||||
@@ -285,10 +270,6 @@ internal class DriverImpl(host: JmxHost?) : Driver {
|
||||
}
|
||||
}
|
||||
|
||||
private fun notAnnotatedError(clazz: Class<*>): IllegalArgumentException {
|
||||
return IllegalArgumentException("Class $clazz is not annotated with @Remote annotation")
|
||||
}
|
||||
|
||||
private fun getClassLoader(): ClassLoader? {
|
||||
return javaClass.classLoader
|
||||
}
|
||||
@@ -338,6 +319,10 @@ internal class DriverImpl(host: JmxHost?) : Driver {
|
||||
}
|
||||
}
|
||||
|
||||
private fun notAnnotatedError(clazz: Class<*>): IllegalArgumentException {
|
||||
return IllegalArgumentException("Class $clazz is not annotated with @Remote annotation")
|
||||
}
|
||||
|
||||
private fun findTimedMeta(method: Method): Timed? {
|
||||
return method.annotations
|
||||
.filterIsInstance<Timed>()
|
||||
@@ -350,6 +335,29 @@ private fun findRemoteMeta(clazz: Class<*>): Remote? {
|
||||
.firstOrNull()
|
||||
}
|
||||
|
||||
private fun checkRdTargetConsistencyAndGetMostSuitable(
|
||||
clazz: Class<*>,
|
||||
rdTarget: RdTarget,
|
||||
vararg args: Any?
|
||||
): RdTarget {
|
||||
val remote = findRemoteMeta(clazz) ?: throw notAnnotatedError(clazz)
|
||||
val classRdTarget = remote.rdTarget
|
||||
val argsRdTarget =
|
||||
args.filterIsInstance<RefWrapper>()
|
||||
.map { if (it.getRef().isBackendReference) RdTarget.BACKEND_ONLY else RdTarget.FRONTEND_ONLY }
|
||||
.reduceOrNull { acc, b ->
|
||||
if (acc != b) throw IllegalStateException("Inconsistent rdTarget")
|
||||
acc
|
||||
} ?: RdTarget.FRONTEND_FIRST
|
||||
|
||||
return listOf(classRdTarget, argsRdTarget, rdTarget).reduce { acc, b ->
|
||||
if (acc == RdTarget.FRONTEND_FIRST) b
|
||||
else if (b == RdTarget.FRONTEND_FIRST) acc
|
||||
else if (acc == b) acc
|
||||
else throw IllegalStateException("Inconsistent rdTarget")
|
||||
}
|
||||
}
|
||||
|
||||
internal data class Session(
|
||||
val id: Int,
|
||||
val dispatcher: OnDispatcher,
|
||||
@@ -358,6 +366,12 @@ internal data class Session(
|
||||
|
||||
private val NO_SESSION: Session = Session(0, OnDispatcher.DEFAULT, LockSemantics.NO_LOCK)
|
||||
|
||||
class DriverCallException(message: String, e: Throwable) : RuntimeException(message, e)
|
||||
|
||||
private data class AppServiceId(val clazz: Class<*>, val isBackendService: Boolean)
|
||||
private data class ProjectServiceId(val projectId: Int, val serviceClass: Class<*>, val isBackendService: Boolean)
|
||||
private data class UtilityId(val clazz: Class<*>, val isBackendUtility: Boolean)
|
||||
|
||||
@JmxName("com.intellij.driver:type=Invoker")
|
||||
interface Invoker : AutoCloseable {
|
||||
fun getProductVersion(): ProductVersion
|
||||
@@ -376,7 +390,3 @@ interface Invoker : AutoCloseable {
|
||||
|
||||
fun takeScreenshot(folder: String?): String?
|
||||
}
|
||||
|
||||
class DriverCallException(message: String, e: Throwable) : RuntimeException(message, e)
|
||||
|
||||
private data class ProjectServiceId(val projectId: Int, val serviceClass: Class<*>)
|
||||
@@ -1,9 +1,6 @@
|
||||
package com.intellij.driver.impl;
|
||||
|
||||
import com.intellij.driver.model.LocalRefDelegate;
|
||||
import com.intellij.driver.model.OnDispatcher;
|
||||
import com.intellij.driver.model.ProductVersion;
|
||||
import com.intellij.driver.model.RemoteRefDelegate;
|
||||
import com.intellij.driver.model.*;
|
||||
import com.intellij.driver.model.transport.*;
|
||||
import com.intellij.ide.plugins.IdeaPluginDescriptor;
|
||||
import com.intellij.ide.plugins.IdeaPluginDescriptorImpl;
|
||||
@@ -12,6 +9,7 @@ import com.intellij.ide.plugins.PluginManagerCore;
|
||||
import com.intellij.openapi.application.*;
|
||||
import com.intellij.openapi.application.ex.ApplicationEx;
|
||||
import com.intellij.openapi.application.impl.ApplicationInfoImpl;
|
||||
import com.intellij.openapi.client.ClientKind;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.extensions.PluginId;
|
||||
import com.intellij.openapi.project.Project;
|
||||
@@ -19,7 +17,6 @@ import com.intellij.openapi.util.BuildNumber;
|
||||
import com.intellij.openapi.util.ClearableLazyValue;
|
||||
import com.intellij.platform.diagnostic.telemetry.IJTracer;
|
||||
import com.intellij.util.ExceptionUtil;
|
||||
import java.util.function.Function;
|
||||
import io.opentelemetry.api.trace.Span;
|
||||
import io.opentelemetry.api.trace.SpanBuilder;
|
||||
import io.opentelemetry.context.Context;
|
||||
@@ -30,14 +27,12 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.lang.reflect.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -128,7 +123,7 @@ public class Invoker implements InvokerMBean {
|
||||
catch (Exception e) {
|
||||
LOG.error("Unable to get instance for " + call);
|
||||
|
||||
throw new RuntimeException("Unable to get instance for " + call, e);
|
||||
throw new DriverIlligalStateException("Unable to get instance for " + call, e);
|
||||
}
|
||||
|
||||
if (call.getDispatcher() == OnDispatcher.EDT) {
|
||||
@@ -155,7 +150,7 @@ public class Invoker implements InvokerMBean {
|
||||
// first lookup in the current session
|
||||
Session session = sessions.get(sessionId);
|
||||
if (session == null) {
|
||||
throw new IllegalStateException("No such session " + sessionId);
|
||||
throw new DriverIlligalStateException("No such session " + sessionId);
|
||||
}
|
||||
|
||||
Object value = session.findReference(id);
|
||||
@@ -167,9 +162,9 @@ public class Invoker implements InvokerMBean {
|
||||
if (value != null) return value;
|
||||
}
|
||||
|
||||
throw new IllegalStateException("No such reference with id " + id + ". " +
|
||||
"It may happen if a weak reference to the variable expires. " +
|
||||
"Please use `Driver.withContext { }` for hard variable references.");
|
||||
throw new DriverIlligalStateException("No such reference with id " + id + ". " +
|
||||
"It may happen if a weak reference to the variable expires. " +
|
||||
"Please use `Driver.withContext { }` for hard variable references.");
|
||||
}
|
||||
|
||||
private static @NotNull Object invokeConstructor(Constructor<?> constructor, Object[] transformedArgs) throws Exception {
|
||||
@@ -237,6 +232,9 @@ public class Invoker implements InvokerMBean {
|
||||
try {
|
||||
return supplier.call();
|
||||
}
|
||||
catch (InvocationTargetException e) {
|
||||
throw new DriverIlligalStateException(e);
|
||||
}
|
||||
catch (Exception e) {
|
||||
ExceptionUtil.rethrow(e);
|
||||
throw new IllegalStateException();
|
||||
@@ -364,7 +362,7 @@ public class Invoker implements InvokerMBean {
|
||||
clazz = getClassLoader(call).loadClass(call.getClassName());
|
||||
}
|
||||
catch (ClassNotFoundException e) {
|
||||
throw new IllegalStateException("No such class '" + call.getClassName() + "'", e);
|
||||
throw new DriverIlligalStateException("No such class '" + call.getClassName() + "'", e);
|
||||
}
|
||||
return clazz;
|
||||
}
|
||||
@@ -378,7 +376,7 @@ public class Invoker implements InvokerMBean {
|
||||
String moduleId = StringsKt.substringAfter(pluginId, "/", pluginId);
|
||||
|
||||
IdeaPluginDescriptor plugin = PluginManagerCore.getPlugin(PluginId.getId(mainId));
|
||||
if (plugin == null) throw new IllegalStateException("No such plugin " + mainId);
|
||||
if (plugin == null) throw new DriverIlligalStateException("No such plugin " + mainId);
|
||||
|
||||
List<PluginContentDescriptor.ModuleItem> modules = ((IdeaPluginDescriptorImpl)plugin).content.modules;
|
||||
for (PluginContentDescriptor.ModuleItem module : modules) {
|
||||
@@ -387,11 +385,11 @@ public class Invoker implements InvokerMBean {
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalStateException("No such plugin module " + pluginId);
|
||||
throw new DriverIlligalStateException("No such plugin module " + pluginId);
|
||||
}
|
||||
|
||||
IdeaPluginDescriptor plugin = PluginManagerCore.getPlugin(PluginId.getId(pluginId));
|
||||
if (plugin == null) throw new IllegalStateException("No such plugin " + pluginId);
|
||||
if (plugin == null) throw new DriverIlligalStateException("No such plugin " + pluginId);
|
||||
|
||||
return plugin.getClassLoader();
|
||||
}
|
||||
@@ -409,16 +407,21 @@ public class Invoker implements InvokerMBean {
|
||||
if (serviceInterface != null) {
|
||||
serviceClass = findServiceInterface(clazz, serviceInterface);
|
||||
if (serviceClass == null) {
|
||||
throw new IllegalStateException("Unable to find interface " + serviceInterface + " for service " + clazz);
|
||||
throw new DriverIlligalStateException("Unable to find interface " + serviceInterface + " for service " + clazz);
|
||||
}
|
||||
}
|
||||
|
||||
Object instance;
|
||||
Boolean isControllerSession = ((ServiceCall)call).isControllerSession();
|
||||
if (projectInstance instanceof Project) {
|
||||
instance = ((Project)projectInstance).getService(serviceClass);
|
||||
instance = isControllerSession
|
||||
? ((Project)projectInstance).getServices(serviceClass, ClientKind.CONTROLLER).get(0)
|
||||
: ((Project)projectInstance).getService(serviceClass);
|
||||
}
|
||||
else {
|
||||
instance = ApplicationManager.getApplication().getService(serviceClass);
|
||||
instance = isControllerSession
|
||||
? ApplicationManager.getApplication().getServices(serviceClass, ClientKind.CONTROLLER).get(0)
|
||||
: ApplicationManager.getApplication().getService(serviceClass);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
@@ -427,7 +430,7 @@ public class Invoker implements InvokerMBean {
|
||||
Ref ref = ((RefCall)call).getRef();
|
||||
Object reference = getReference(call.getSessionId(), ref.id());
|
||||
|
||||
if (reference == null) throw new IllegalStateException("No such ref exists " + ref);
|
||||
if (reference == null) throw new DriverIlligalStateException("No such ref exists " + ref);
|
||||
|
||||
return reference;
|
||||
}
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.intellij.driver.model
|
||||
|
||||
class DriverIlligalStateException : IllegalStateException {
|
||||
constructor(message: String) : super(message)
|
||||
constructor(cause: Throwable) : super(cause)
|
||||
constructor(message: String, cause: Throwable) : super(message, cause)
|
||||
}
|
||||
+7
-1
@@ -12,4 +12,10 @@ fun <T : Any> RefDelegate<T>.unwrap(): T = when (this) {
|
||||
is RemoteRefDelegate -> throw UnsupportedOperationException("Cannot get remote value")
|
||||
}
|
||||
|
||||
fun <T : Any> List<RefDelegate<T>>.unwrap(): List<T> = map { it.unwrap() }
|
||||
fun <T : Any> List<RefDelegate<T>>.unwrap(): List<T> = map { it.unwrap() }
|
||||
|
||||
enum class RdTarget {
|
||||
FRONTEND_FIRST,
|
||||
FRONTEND_ONLY,
|
||||
BACKEND_ONLY,
|
||||
}
|
||||
+6
-1
@@ -2,6 +2,7 @@ package com.intellij.driver.model.transport;
|
||||
|
||||
import com.intellij.driver.model.LockSemantics;
|
||||
import com.intellij.driver.model.OnDispatcher;
|
||||
import com.intellij.driver.model.RdTarget;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
@@ -15,8 +16,12 @@ public final class NewInstanceCall extends RemoteCall {
|
||||
OnDispatcher dispatcher,
|
||||
LockSemantics lockSemantics,
|
||||
String className,
|
||||
RdTarget rdTarget,
|
||||
Object[] args) {
|
||||
super(sessionId, timedSpan, pluginId, dispatcher, lockSemantics, className, "new", args);
|
||||
super(sessionId, timedSpan, pluginId, dispatcher, lockSemantics, className, "new", rdTarget, args);
|
||||
if (rdTarget != RdTarget.FRONTEND_ONLY && rdTarget != RdTarget.BACKEND_ONLY) {
|
||||
throw new IllegalArgumentException("NewInstanceCall must have RdTarget set to either FRONTEND_ONLY or BACKEND_ONLY");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -11,4 +11,11 @@ public record Ref(
|
||||
) implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static final String BACKEND_REFERENCE_PREFIX = "remdevHost_";
|
||||
public static final String FRONTEND_REFERENCE_PREFIX = "jbClient_";
|
||||
|
||||
public boolean isBackendReference() {
|
||||
return id.startsWith(BACKEND_REFERENCE_PREFIX);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.intellij.driver.model.transport;
|
||||
|
||||
import com.intellij.driver.model.LockSemantics;
|
||||
import com.intellij.driver.model.OnDispatcher;
|
||||
import com.intellij.driver.model.RdTarget;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
@@ -20,7 +21,15 @@ public final class RefCall extends RemoteCall {
|
||||
String methodName,
|
||||
Object[] args,
|
||||
Ref ref) {
|
||||
super(sessionId, timedSpan, pluginId, dispatcher, lockSemantics, className, methodName, args);
|
||||
super(sessionId,
|
||||
timedSpan,
|
||||
pluginId,
|
||||
dispatcher,
|
||||
lockSemantics,
|
||||
className,
|
||||
methodName,
|
||||
ref.isBackendReference() ? RdTarget.BACKEND_ONLY : RdTarget.FRONTEND_ONLY,
|
||||
args);
|
||||
|
||||
this.ref = ref;
|
||||
}
|
||||
|
||||
+11
-2
@@ -2,7 +2,9 @@ package com.intellij.driver.model.transport;
|
||||
|
||||
import com.intellij.driver.model.LockSemantics;
|
||||
import com.intellij.driver.model.OnDispatcher;
|
||||
import com.intellij.driver.model.RdTarget;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.Serial;
|
||||
@@ -15,6 +17,8 @@ public abstract class RemoteCall implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final RdTarget rdTarget;
|
||||
|
||||
private final int sessionId;
|
||||
private final String pluginId;
|
||||
private final String timedSpan;
|
||||
@@ -23,8 +27,6 @@ public abstract class RemoteCall implements Serializable {
|
||||
private final LockSemantics lockSemantics;
|
||||
private final String className;
|
||||
private final String methodName;
|
||||
private final Object[] args;
|
||||
|
||||
public RemoteCall(int sessionId,
|
||||
String timedSpan,
|
||||
String pluginId,
|
||||
@@ -32,6 +34,7 @@ public abstract class RemoteCall implements Serializable {
|
||||
LockSemantics lockSemantics,
|
||||
String className,
|
||||
String methodName,
|
||||
RdTarget rdTarget,
|
||||
Object[] args) {
|
||||
this.sessionId = sessionId;
|
||||
this.pluginId = pluginId;
|
||||
@@ -40,8 +43,14 @@ public abstract class RemoteCall implements Serializable {
|
||||
this.lockSemantics = lockSemantics;
|
||||
this.className = className;
|
||||
this.methodName = methodName;
|
||||
this.rdTarget = rdTarget;
|
||||
this.args = args;
|
||||
}
|
||||
private final Object[] args;
|
||||
|
||||
public @NotNull RdTarget getRdTarget() {
|
||||
return rdTarget;
|
||||
}
|
||||
|
||||
public int getSessionId() {
|
||||
return sessionId;
|
||||
|
||||
+11
-2
@@ -2,6 +2,7 @@ package com.intellij.driver.model.transport;
|
||||
|
||||
import com.intellij.driver.model.LockSemantics;
|
||||
import com.intellij.driver.model.OnDispatcher;
|
||||
import com.intellij.driver.model.RdTarget;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.Serial;
|
||||
@@ -12,6 +13,7 @@ public final class ServiceCall extends RemoteCall {
|
||||
|
||||
private final Ref projectRef;
|
||||
private final String serviceInterface;
|
||||
private final Boolean isControllerSession;
|
||||
|
||||
public ServiceCall(int sessionId,
|
||||
String timedSpan,
|
||||
@@ -22,10 +24,13 @@ public final class ServiceCall extends RemoteCall {
|
||||
String methodName,
|
||||
Object[] args,
|
||||
@Nullable Ref projectRef,
|
||||
@Nullable String serviceInterface) {
|
||||
super(sessionId, timedSpan, pluginId, dispatcher, lockSemantics, className, methodName, args);
|
||||
@Nullable String serviceInterface,
|
||||
RdTarget rdTarget,
|
||||
Boolean isControllerSession) {
|
||||
super(sessionId, timedSpan, pluginId, dispatcher, lockSemantics, className, methodName, rdTarget, args);
|
||||
this.projectRef = projectRef;
|
||||
this.serviceInterface = serviceInterface;
|
||||
this.isControllerSession = isControllerSession;
|
||||
}
|
||||
|
||||
public Ref getProjectRef() {
|
||||
@@ -36,6 +41,10 @@ public final class ServiceCall extends RemoteCall {
|
||||
return serviceInterface;
|
||||
}
|
||||
|
||||
public Boolean isControllerSession() {
|
||||
return isControllerSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ServiceCall{" +
|
||||
|
||||
+3
-1
@@ -2,6 +2,7 @@ package com.intellij.driver.model.transport;
|
||||
|
||||
import com.intellij.driver.model.LockSemantics;
|
||||
import com.intellij.driver.model.OnDispatcher;
|
||||
import com.intellij.driver.model.RdTarget;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
@@ -16,8 +17,9 @@ public final class UtilityCall extends RemoteCall {
|
||||
LockSemantics lockSemantics,
|
||||
String className,
|
||||
String methodName,
|
||||
RdTarget rdTarget,
|
||||
Object[] args) {
|
||||
super(sessionId, timedSpan, pluginId, dispatcher, lockSemantics, className, methodName, args);
|
||||
super(sessionId, timedSpan, pluginId, dispatcher, lockSemantics, className, methodName, rdTarget, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.intellij.driver.client.Driver
|
||||
import com.intellij.driver.client.Remote
|
||||
import com.intellij.driver.client.service
|
||||
import com.intellij.driver.client.utility
|
||||
import com.intellij.driver.model.RdTarget
|
||||
|
||||
@Remote("com.intellij.openapi.roots.ProjectRootManager")
|
||||
interface ProjectRootManager {
|
||||
@@ -30,7 +31,9 @@ interface Sdk {
|
||||
fun getHomePath(): String?
|
||||
}
|
||||
|
||||
@Remote(value = "com.jetbrains.performancePlugin.commands.SetupProjectSdkUtil", plugin = "com.jetbrains.performancePlugin")
|
||||
@Remote(value = "com.jetbrains.performancePlugin.commands.SetupProjectSdkUtil",
|
||||
plugin = "com.jetbrains.performancePlugin",
|
||||
rdTarget = RdTarget.BACKEND_ONLY)
|
||||
interface SetupProjectSdkUtil {
|
||||
fun setupOrDetectSdk(project: Project, name: String, type: String, home: String)
|
||||
|
||||
|
||||
@@ -5,32 +5,37 @@ import com.intellij.driver.sdk.ui.components.ComponentData
|
||||
import com.intellij.driver.sdk.ui.components.UIComponentsList
|
||||
import com.intellij.driver.sdk.ui.components.UiComponent
|
||||
import com.intellij.driver.sdk.ui.remote.Component
|
||||
import com.intellij.driver.sdk.ui.remote.RobotService
|
||||
import com.intellij.driver.sdk.ui.remote.RobotServiceProvider
|
||||
import com.intellij.driver.sdk.ui.remote.SearchService
|
||||
import org.intellij.lang.annotations.Language
|
||||
|
||||
internal const val DEFAULT_FIND_TIMEOUT_SECONDS = 15
|
||||
|
||||
interface Finder {
|
||||
val driver: Driver
|
||||
val robotService: RobotService
|
||||
val searchService: SearchService
|
||||
val robotServiceProvider: RobotServiceProvider
|
||||
val searchContext: SearchContext
|
||||
|
||||
val isRemoteIdeMode: Boolean
|
||||
get() = driver.isRemoteIdeMode
|
||||
|
||||
fun x(@Language("xpath") xpath: String): UiComponent {
|
||||
return UiComponent(ComponentData(xpath, driver, robotService, searchContext, null))
|
||||
return UiComponent(ComponentData(xpath, driver, searchService, robotServiceProvider, searchContext, null))
|
||||
}
|
||||
|
||||
fun <T : UiComponent> x(@Language("xpath") xpath: String, type: Class<T>): T {
|
||||
return type.getConstructor(
|
||||
ComponentData::class.java
|
||||
).newInstance(ComponentData(xpath, driver, robotService, searchContext, null))
|
||||
).newInstance(ComponentData(xpath, driver, searchService, robotServiceProvider, searchContext, null))
|
||||
}
|
||||
|
||||
fun xx(@Language("xpath") xpath: String): UIComponentsList<UiComponent> {
|
||||
return UIComponentsList(xpath, UiComponent::class.java, driver, robotService, searchContext)
|
||||
return UIComponentsList(xpath, UiComponent::class.java, driver, searchService, robotServiceProvider, searchContext)
|
||||
}
|
||||
|
||||
fun <T : UiComponent> xx(@Language("xpath") xpath: String, type: Class<T>): UIComponentsList<T> {
|
||||
return UIComponentsList(xpath, type, driver, robotService, searchContext)
|
||||
return UIComponentsList(xpath, type, driver, searchService, robotServiceProvider, searchContext)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,18 +2,29 @@ package com.intellij.driver.sdk.ui
|
||||
|
||||
import com.intellij.driver.client.Driver
|
||||
import com.intellij.driver.model.RemoteMouseButton
|
||||
import com.intellij.driver.model.transport.Ref
|
||||
import com.intellij.driver.sdk.ui.keyboard.WithKeyboard
|
||||
import com.intellij.driver.sdk.ui.remote.Component
|
||||
import com.intellij.driver.sdk.ui.remote.RobotService
|
||||
import com.intellij.driver.sdk.ui.remote.RobotServiceProvider
|
||||
import com.intellij.driver.sdk.ui.remote.SearchService
|
||||
import java.awt.Point
|
||||
import java.lang.reflect.Proxy
|
||||
|
||||
/**
|
||||
* Provides access to UI actions with the IDE under test such as keyboard and mouse input.
|
||||
*/
|
||||
val Driver.ui
|
||||
get(): UiRobot = UiRobot(this, service(RobotService::class))
|
||||
get(): UiRobot = UiRobot(this, service(SearchService::class), RemDevRobotServiceProvider(this))
|
||||
|
||||
class UiRobot(override val driver: Driver, override val robotService: RobotService) : WithKeyboard, Finder {
|
||||
class UiRobot(
|
||||
override val driver: Driver,
|
||||
override val searchService: SearchService,
|
||||
override val robotServiceProvider: RobotServiceProvider
|
||||
) : WithKeyboard, Finder {
|
||||
|
||||
override val robotService: RobotService
|
||||
get() = robotServiceProvider.getDefaultRobotService()
|
||||
|
||||
fun moveMouse(point: Point) {
|
||||
robotService.robot.moveMouse(point)
|
||||
@@ -45,8 +56,32 @@ class UiRobot(override val driver: Driver, override val robotService: RobotServi
|
||||
override val context = ""
|
||||
|
||||
override fun findAll(xpath: String): List<Component> {
|
||||
return robotService.findAll(xpath)
|
||||
return searchService.findAll(xpath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RemDevRobotServiceProvider(private val driver: Driver) : RobotServiceProvider {
|
||||
private val frontendRobotService: RobotService
|
||||
get() = driver.service(RobotService::class)
|
||||
private val backendRobotService: RobotService
|
||||
get() = driver.service(RobotService::class, true)
|
||||
|
||||
override fun getDefaultRobotService(): RobotService {
|
||||
return frontendRobotService
|
||||
}
|
||||
|
||||
override fun getRobotServiceFor(obj: Any?): RobotService {
|
||||
if (obj == null) {
|
||||
return getDefaultRobotService()
|
||||
}
|
||||
val handler = Proxy.getInvocationHandler(obj)
|
||||
val method = FakeRefInterface::class.java.getMethod("getRef")
|
||||
val ref = handler.invoke(obj, method, arrayOf()) as Ref
|
||||
return if (ref.isBackendReference) backendRobotService else frontendRobotService
|
||||
}
|
||||
|
||||
private interface FakeRefInterface {
|
||||
fun getRef(): Ref
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -17,8 +17,10 @@ fun Driver.ideFrame(action: IdeaFrameUI.() -> Unit) {
|
||||
}
|
||||
|
||||
open class IdeaFrameUI(data: ComponentData) : UiComponent(data) {
|
||||
private val projectViewTreeClass = if (isRemoteIdeMode) "ThinClientProjectViewTree" else "ProjectViewTree"
|
||||
val projectViewTree = tree("//div[@class='${projectViewTreeClass}']")
|
||||
|
||||
private val ideaFrameComponent by lazy { driver.cast(component, IdeFrameImpl::class) }
|
||||
val projectViewTree = tree("//div[@class='ProjectViewTree']")
|
||||
|
||||
val project: Project?
|
||||
get() = driver.utility(ProjectFrameHelper::class).getFrameHelper(component).getProject()
|
||||
|
||||
+6
-4
@@ -3,12 +3,14 @@ package com.intellij.driver.sdk.ui.components
|
||||
import com.intellij.driver.client.Driver
|
||||
import com.intellij.driver.sdk.ui.SearchContext
|
||||
import com.intellij.driver.sdk.ui.remote.Component
|
||||
import com.intellij.driver.sdk.ui.remote.RobotService
|
||||
import com.intellij.driver.sdk.ui.remote.RobotServiceProvider
|
||||
import com.intellij.driver.sdk.ui.remote.SearchService
|
||||
|
||||
class UIComponentsList<T : UiComponent>(private val xpath: String,
|
||||
private val type: Class<T>,
|
||||
val driver: Driver,
|
||||
val robotService: RobotService,
|
||||
val searchService: SearchService,
|
||||
val robotServiceProvider: RobotServiceProvider,
|
||||
private val parentSearchContext: SearchContext) {
|
||||
fun list(): List<T> {
|
||||
return parentSearchContext.findAll(xpath).mapIndexed { n, c ->
|
||||
@@ -17,12 +19,12 @@ class UIComponentsList<T : UiComponent>(private val xpath: String,
|
||||
get() = parentSearchContext.context + xpath + "[$n]"
|
||||
|
||||
override fun findAll(xpath: String): List<Component> {
|
||||
return robotService.findAll(xpath, c)
|
||||
return searchService.findAll(xpath, c)
|
||||
}
|
||||
}
|
||||
type.getConstructor(
|
||||
ComponentData::class.java
|
||||
).newInstance(ComponentData(xpath, driver, robotService, searchContext, c))
|
||||
).newInstance(ComponentData(xpath, driver, searchService, robotServiceProvider, searchContext, c))
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-11
@@ -10,6 +10,8 @@ import com.intellij.driver.sdk.ui.UiText
|
||||
import com.intellij.driver.sdk.ui.keyboard.WithKeyboard
|
||||
import com.intellij.driver.sdk.ui.remote.Component
|
||||
import com.intellij.driver.sdk.ui.remote.RobotService
|
||||
import com.intellij.driver.sdk.ui.remote.RobotServiceProvider
|
||||
import com.intellij.driver.sdk.ui.remote.SearchService
|
||||
import com.intellij.driver.sdk.waitFor
|
||||
import java.awt.Point
|
||||
import kotlin.time.Duration
|
||||
@@ -18,7 +20,8 @@ import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
data class ComponentData(val xpath: String,
|
||||
val driver: Driver,
|
||||
val robotService: RobotService,
|
||||
val searchService: SearchService,
|
||||
val robotServiceProvider: RobotServiceProvider,
|
||||
val parentSearchContext: SearchContext,
|
||||
val foundComponent: Component?)
|
||||
|
||||
@@ -32,21 +35,27 @@ open class UiComponent(private val data: ComponentData) : Finder, WithKeyboard {
|
||||
}
|
||||
|
||||
private fun findThisComponent(): Component {
|
||||
lateinit var result: List<Component>
|
||||
waitFor(DEFAULT_FIND_TIMEOUT_SECONDS.seconds,
|
||||
errorMessage = "Can't find component with '${data.xpath}' in ${data.parentSearchContext.context.takeIf { it.isNotEmpty() } ?: "whole hierarchy"}") {
|
||||
data.parentSearchContext.findAll(data.xpath).size == 1
|
||||
result = data.parentSearchContext.findAll(data.xpath)
|
||||
result.size == 1
|
||||
}
|
||||
return data.parentSearchContext.findAll(data.xpath).first()
|
||||
return result.first()
|
||||
}
|
||||
|
||||
override val driver: Driver = data.driver
|
||||
override val robotService: RobotService = data.robotService
|
||||
override val searchService: SearchService = data.searchService
|
||||
override val robotServiceProvider: RobotServiceProvider = data.robotServiceProvider
|
||||
override val robotService: RobotService by lazy {
|
||||
data.robotServiceProvider.getRobotServiceFor(component)
|
||||
}
|
||||
|
||||
override val searchContext: SearchContext = object : SearchContext {
|
||||
override val context: String = data.parentSearchContext.context + data.xpath
|
||||
|
||||
override fun findAll(xpath: String): List<Component> {
|
||||
return robotService.findAll(xpath, component)
|
||||
return searchService.findAll(xpath, component)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,19 +101,19 @@ open class UiComponent(private val data: ComponentData) : Finder, WithKeyboard {
|
||||
}
|
||||
|
||||
fun findText(predicate: (TextData) -> Boolean): UiText {
|
||||
return robotService.findAllText(component).single(predicate).let { UiText(this, it) }
|
||||
return searchService.findAllText(component).single(predicate).let { UiText(this, it) }
|
||||
}
|
||||
|
||||
fun present(): Boolean {
|
||||
return robotService.findAll(data.xpath).isNotEmpty()
|
||||
return searchService.findAll(data.xpath).isNotEmpty()
|
||||
}
|
||||
|
||||
fun notPresent(): Boolean {
|
||||
return robotService.findAll(data.xpath).isEmpty()
|
||||
return searchService.findAll(data.xpath).isEmpty()
|
||||
}
|
||||
|
||||
fun hasText(predicate: (TextData) -> Boolean): Boolean {
|
||||
return robotService.findAllText(component).any(predicate)
|
||||
return searchService.findAllText(component).any(predicate)
|
||||
}
|
||||
|
||||
fun isVisible(): Boolean = component.isVisible()
|
||||
@@ -112,11 +121,11 @@ open class UiComponent(private val data: ComponentData) : Finder, WithKeyboard {
|
||||
fun isEnabled(): Boolean = component.isEnabled()
|
||||
|
||||
fun findAllText(predicate: (TextData) -> Boolean): List<UiText> {
|
||||
return robotService.findAllText(component).filter(predicate).map { UiText(this, it) }
|
||||
return searchService.findAllText(component).filter(predicate).map { UiText(this, it) }
|
||||
}
|
||||
|
||||
fun findAllText(): List<UiText> {
|
||||
return robotService.findAllText(component).map { UiText(this, it) }
|
||||
return searchService.findAllText(component).map { UiText(this, it) }
|
||||
}
|
||||
|
||||
fun hasVisibleComponent(component: UiComponent): Boolean {
|
||||
|
||||
+4
-2
@@ -3,8 +3,10 @@ package com.intellij.driver.sdk.ui.components
|
||||
import com.intellij.driver.client.Driver
|
||||
import com.intellij.driver.sdk.ui.*
|
||||
|
||||
fun Finder.welcomeScreen(action: WelcomeScreenUI.() -> Unit = {}) =
|
||||
x("//div[@class='FlatWelcomeFrame']", WelcomeScreenUI::class.java).apply(action)
|
||||
fun Finder.welcomeScreen(action: WelcomeScreenUI.() -> Unit = {}): WelcomeScreenUI {
|
||||
val welcomeScreenClass = if (isRemoteIdeMode) "TabbedWelcomeScreen" else "FlatWelcomeFrame"
|
||||
return x("//div[@class='${welcomeScreenClass}']", WelcomeScreenUI::class.java).apply(action)
|
||||
}
|
||||
|
||||
fun Driver.welcomeScreen(action: WelcomeScreenUI.() -> Unit = {}) = this.ui.welcomeScreen(action)
|
||||
|
||||
|
||||
+6
-6
@@ -1,8 +1,6 @@
|
||||
package com.intellij.driver.sdk.ui.remote
|
||||
|
||||
import com.intellij.driver.client.Remote
|
||||
import com.intellij.driver.model.TextDataList
|
||||
import org.intellij.lang.annotations.Language
|
||||
|
||||
internal const val REMOTE_ROBOT_MODULE_ID = "com.jetbrains.performancePlugin/intellij.performanceTesting.remoteDriver"
|
||||
|
||||
@@ -10,8 +8,10 @@ internal const val REMOTE_ROBOT_MODULE_ID = "com.jetbrains.performancePlugin/int
|
||||
plugin = REMOTE_ROBOT_MODULE_ID)
|
||||
interface RobotService {
|
||||
val robot: Robot
|
||||
fun findAll(@Language("xpath") xpath: String): List<Component>
|
||||
fun findAll(@Language("xpath") xpath: String, component: Component): List<Component>
|
||||
fun findAllText(component: Component): TextDataList
|
||||
fun saveHierarchy(folderPath: String, fileName: String = "ui")
|
||||
}
|
||||
}
|
||||
|
||||
interface RobotServiceProvider {
|
||||
fun getDefaultRobotService(): RobotService
|
||||
fun getRobotServiceFor(obj: Any?): RobotService
|
||||
}
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.intellij.driver.sdk.ui.remote
|
||||
|
||||
import com.intellij.driver.client.Remote
|
||||
import com.intellij.driver.model.TextDataList
|
||||
import org.intellij.lang.annotations.Language
|
||||
|
||||
@Remote("com.jetbrains.performancePlugin.remotedriver.SearchService",
|
||||
plugin = REMOTE_ROBOT_MODULE_ID)
|
||||
interface SearchService {
|
||||
fun findAll(@Language("xpath") xpath: String): List<Component>
|
||||
fun findAll(@Language("xpath") xpath: String, component: Component): List<Component>
|
||||
fun findAllText(component: Component): TextDataList
|
||||
}
|
||||
+23
-7
@@ -2,12 +2,15 @@ package com.jetbrains.performancePlugin.jmxDriver
|
||||
|
||||
import com.intellij.driver.impl.Invoker
|
||||
import com.intellij.driver.impl.InvokerMBean
|
||||
import com.intellij.driver.model.transport.Ref
|
||||
import com.intellij.idea.AppMode
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.platform.diagnostic.telemetry.IJTracer
|
||||
import com.intellij.util.PlatformUtils
|
||||
import io.opentelemetry.context.Context
|
||||
import java.awt.Component
|
||||
import java.lang.management.ManagementFactory
|
||||
import java.util.function.Consumer
|
||||
import java.util.function.Supplier
|
||||
@@ -26,10 +29,17 @@ class InvokerService {
|
||||
private val jmxBackendPort = System.getProperty(BACKEND_JMX_PORT_PROPERTY)
|
||||
private val log = Logger.getInstance(InvokerService::class.java)
|
||||
}
|
||||
var invoker: InvokerMBean? = null
|
||||
private set
|
||||
|
||||
fun isReady(): Boolean = invoker != null
|
||||
private var myInvoker: InvokerMBean? = null
|
||||
|
||||
val invoker: InvokerMBean
|
||||
get() = myInvoker ?: throw IllegalStateException("Invoker is not registered")
|
||||
|
||||
fun isReady(): Boolean = myInvoker != null
|
||||
|
||||
fun putReference(c: Component): Ref {
|
||||
return invoker.putAdhocReference(c)
|
||||
}
|
||||
|
||||
@Throws(JMException::class)
|
||||
fun register(tracerSupplier: Supplier<out IJTracer?>,
|
||||
@@ -38,15 +48,21 @@ class InvokerService {
|
||||
val objectName = ObjectName("com.intellij.driver:type=Invoker")
|
||||
val server = ManagementFactory.getPlatformMBeanServer()
|
||||
|
||||
val localInvoker = Invoker("v", tracerSupplier, timedContextSupplier, screenshotAction)
|
||||
val prefix = when {
|
||||
PlatformUtils.isJetBrainsClient() -> Ref.FRONTEND_REFERENCE_PREFIX
|
||||
AppMode.isRemoteDevHost() -> Ref.BACKEND_REFERENCE_PREFIX
|
||||
else -> ""
|
||||
}
|
||||
|
||||
val localInvoker = Invoker(prefix, tracerSupplier, timedContextSupplier, screenshotAction)
|
||||
|
||||
val remoteJmxAddress = jmxBackendPort?.let { "$JMX_BACKEND_IP:$it" }
|
||||
|
||||
if (PlatformUtils.isJetBrainsClient() && remoteJmxAddress != null) {
|
||||
log.info("Remote Dev Mode")
|
||||
invoker = RemoteDevInvoker(localInvoker, remoteJmxAddress)
|
||||
log.info("Remote Dev Mode, $remoteJmxAddress")
|
||||
myInvoker = RemoteDevInvoker(localInvoker, remoteJmxAddress)
|
||||
} else {
|
||||
invoker = localInvoker
|
||||
myInvoker = localInvoker
|
||||
}
|
||||
server.registerMBean(invoker, objectName)
|
||||
}
|
||||
|
||||
+44
-9
@@ -1,20 +1,39 @@
|
||||
package com.jetbrains.performancePlugin.jmxDriver
|
||||
|
||||
import com.intellij.driver.client.Driver
|
||||
import com.intellij.driver.client.impl.DriverImpl
|
||||
import com.intellij.driver.client.impl.Invoker
|
||||
import com.intellij.driver.client.impl.JmxCallHandler
|
||||
import com.intellij.driver.client.impl.JmxHost
|
||||
import com.intellij.driver.impl.InvokerMBean
|
||||
import com.intellij.driver.model.DriverIlligalStateException
|
||||
import com.intellij.driver.model.ProductVersion
|
||||
import com.intellij.driver.model.RdTarget
|
||||
import com.intellij.driver.model.transport.Ref
|
||||
import com.intellij.driver.model.transport.RemoteCall
|
||||
import com.intellij.driver.model.transport.RemoteCallResult
|
||||
import kotlin.jvm.java
|
||||
|
||||
|
||||
interface RemoteDevInvokerMBean : InvokerMBean
|
||||
interface RemoteDevInvokerMBean : InvokerMBean {
|
||||
val driver: Driver
|
||||
}
|
||||
|
||||
internal class RemoteDevInvoker(private val localInvoker: InvokerMBean, remoteJmxAddress: String) : RemoteDevInvokerMBean {
|
||||
private val remoteInvoker = JmxCallHandler.jmx(Invoker::class.java, JmxHost(null, null, remoteJmxAddress))
|
||||
|
||||
override val driver: DriverImpl
|
||||
|
||||
init {
|
||||
val originalClassLoader = Thread.currentThread().getContextClassLoader()
|
||||
try {
|
||||
Thread.currentThread().setContextClassLoader(this::class.java.getClassLoader())
|
||||
driver = Driver.create(JmxHost(null, null, remoteJmxAddress)) as DriverImpl
|
||||
}
|
||||
finally {
|
||||
Thread.currentThread().setContextClassLoader(originalClassLoader)
|
||||
}
|
||||
}
|
||||
|
||||
private val remoteInvoker: Invoker
|
||||
get() = driver.getInvoker()
|
||||
|
||||
override fun getProductVersion(): ProductVersion {
|
||||
return localInvoker.productVersion
|
||||
@@ -29,14 +48,30 @@ internal class RemoteDevInvoker(private val localInvoker: InvokerMBean, remoteJm
|
||||
}
|
||||
|
||||
override fun invoke(call: RemoteCall): RemoteCallResult {
|
||||
|
||||
try {
|
||||
return localInvoker.invoke(call)
|
||||
when (call.rdTarget) {
|
||||
RdTarget.FRONTEND_ONLY -> return localInvoker.invoke(call)
|
||||
RdTarget.BACKEND_ONLY -> return invokeRemote(call)
|
||||
RdTarget.FRONTEND_FIRST -> try {
|
||||
return localInvoker.invoke(call)
|
||||
}
|
||||
catch (_: DriverIlligalStateException) {
|
||||
return invokeRemote(call)
|
||||
}
|
||||
catch (t: Throwable) {
|
||||
throw t
|
||||
}
|
||||
}
|
||||
// replace with ReferenceNotFoundException later
|
||||
catch (e: Throwable) {
|
||||
}
|
||||
|
||||
private fun invokeRemote(call: RemoteCall): RemoteCallResult {
|
||||
val originalClassLoader = Thread.currentThread().getContextClassLoader()
|
||||
try {
|
||||
Thread.currentThread().setContextClassLoader(this::class.java.getClassLoader())
|
||||
return remoteInvoker.invoke(call)
|
||||
}
|
||||
finally {
|
||||
Thread.currentThread().setContextClassLoader(originalClassLoader)
|
||||
}
|
||||
}
|
||||
|
||||
override fun newSession(): Int {
|
||||
|
||||
+3
-3
@@ -18,11 +18,11 @@
|
||||
<orderEntry type="library" name="netty-buffer" level="project" />
|
||||
<orderEntry type="library" name="netty-codec-http" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.platform.core" />
|
||||
<orderEntry type="module" module-name="intellij.driver.model" />
|
||||
<orderEntry type="module" module-name="intellij.driver.model" exported="" />
|
||||
<orderEntry type="module" module-name="intellij.platform.ide" />
|
||||
<orderEntry type="library" name="jsoup" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.driver.impl" />
|
||||
<orderEntry type="module" module-name="intellij.driver.client" />
|
||||
<orderEntry type="module" module-name="intellij.driver.impl" exported="" />
|
||||
<orderEntry type="module" module-name="intellij.driver.client" exported="" />
|
||||
<orderEntry type="library" name="jcef" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
+3
-4
@@ -1,10 +1,9 @@
|
||||
<idea-plugin package="com.jetbrains.performancePlugin.remotedriver">
|
||||
<extensionPoints>
|
||||
<extensionPoint interface="com.jetbrains.performancePlugin.remotedriver.xpath.XpathDataModelSubTreeProvider"
|
||||
qualifiedName="com.jetbrains.performancePlugin.remoteDriver.xpathDataModelExtension" dynamic="true"/>
|
||||
<extensionPoint interface="com.jetbrains.performancePlugin.remotedriver.webservice.UiHierarchyWebServiceExtension"
|
||||
qualifiedName="com.jetbrains.performancePlugin.remoteDriver.uiHierarchyExtension" dynamic="true"/>
|
||||
<extensionPoint interface="com.jetbrains.performancePlugin.remotedriver.xpath.XpathDataModelExtension"
|
||||
qualifiedName="com.jetbrains.performancePlugin.remotedriver.xpathDataModelExtension" dynamic="true"/>
|
||||
</extensionPoints>
|
||||
|
||||
<extensions defaultExtensionNs="com.intellij">
|
||||
<registryKey key="expose.ui.hierarchy.url"
|
||||
defaultValue="false"
|
||||
|
||||
+2
-21
@@ -1,14 +1,9 @@
|
||||
package com.jetbrains.performancePlugin.remotedriver
|
||||
|
||||
import com.intellij.driver.model.RefDelegate
|
||||
import com.intellij.driver.model.TextDataList
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.jetbrains.performancePlugin.remotedriver.dataextractor.TextParser
|
||||
import com.jetbrains.performancePlugin.remotedriver.dataextractor.TextToKeyCache
|
||||
import com.jetbrains.performancePlugin.remotedriver.robot.SmoothRobot
|
||||
import com.jetbrains.performancePlugin.remotedriver.xpath.XpathSearcher
|
||||
import com.jetbrains.performancePlugin.remotedriver.xpath.XpathDataModelCreator
|
||||
import com.jetbrains.performancePlugin.remotedriver.xpath.convertToHtml
|
||||
import java.awt.Component
|
||||
import java.nio.file.*
|
||||
|
||||
@Suppress("unused")
|
||||
@@ -17,22 +12,8 @@ internal class RobotService {
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
val robot: SmoothRobot = SmoothRobot()
|
||||
|
||||
private val xpathSearcher: XpathSearcher = XpathSearcher(TextToKeyCache)
|
||||
|
||||
fun findAll(xpath: String): List<RefDelegate<Component>> {
|
||||
return xpathSearcher.findComponents(xpath, null)
|
||||
}
|
||||
|
||||
fun findAll(xpath: String, component: Component): List<RefDelegate<Component>> {
|
||||
return xpathSearcher.findComponents(xpath, component)
|
||||
}
|
||||
|
||||
fun findAllText(component: Component): TextDataList {
|
||||
return TextParser.parseComponent(component, TextToKeyCache).let { TextDataList().apply { addAll(it) } }
|
||||
}
|
||||
|
||||
fun saveHierarchy(folderPath: String, fileName: String = "ui.html") {
|
||||
val html = xpathSearcher.modelCreator.create(null).convertToHtml()
|
||||
val html = XpathDataModelCreator.create(null).convertToHtml()
|
||||
Paths.get(folderPath).resolve(fileName).toFile().writeText(html)
|
||||
|
||||
staticFiles.forEach { staticFilePath ->
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.jetbrains.performancePlugin.remotedriver
|
||||
|
||||
import com.intellij.driver.model.RefDelegate
|
||||
import com.intellij.driver.model.TextDataList
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.jetbrains.performancePlugin.remotedriver.dataextractor.TextParser
|
||||
import com.jetbrains.performancePlugin.remotedriver.dataextractor.TextToKeyCache
|
||||
import com.jetbrains.performancePlugin.remotedriver.xpath.XpathSearcher
|
||||
import java.awt.Component
|
||||
|
||||
@Suppress("unused")
|
||||
@Service(Service.Level.APP)
|
||||
internal class SearchService {
|
||||
private val xpathSearcher: XpathSearcher = XpathSearcher()
|
||||
|
||||
fun findAll(xpath: String): List<RefDelegate<Component>> {
|
||||
return xpathSearcher.findComponents(xpath, null)
|
||||
}
|
||||
|
||||
fun findAll(xpath: String, component: Component): List<RefDelegate<Component>> {
|
||||
return xpathSearcher.findComponents(xpath, component)
|
||||
}
|
||||
|
||||
fun findAllText(component: Component): TextDataList {
|
||||
return TextParser.parseComponent(component, TextToKeyCache).let { TextDataList().apply { addAll(it) } }
|
||||
}
|
||||
}
|
||||
+1
-36
@@ -1,10 +1,7 @@
|
||||
package com.jetbrains.performancePlugin.remotedriver.webservice
|
||||
|
||||
import com.intellij.openapi.extensions.ExtensionNotApplicableException
|
||||
import com.intellij.openapi.extensions.ExtensionPointName
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.jetbrains.performancePlugin.jmxDriver.InvokerService
|
||||
import com.jetbrains.performancePlugin.remotedriver.dataextractor.TextToKeyCache
|
||||
import com.jetbrains.performancePlugin.remotedriver.webservice.routing.CantFindRouteException
|
||||
import com.jetbrains.performancePlugin.remotedriver.webservice.routing.Routing
|
||||
import com.jetbrains.performancePlugin.remotedriver.webservice.routing.StaticFile
|
||||
@@ -16,8 +13,6 @@ import io.netty.channel.ChannelHandlerContext
|
||||
import io.netty.handler.codec.http.*
|
||||
import org.jetbrains.ide.RestService
|
||||
import org.jetbrains.io.response
|
||||
import org.jsoup.helper.W3CDom
|
||||
import java.awt.Component
|
||||
|
||||
internal class UiHierarchyWebService : RestService() {
|
||||
init {
|
||||
@@ -38,33 +33,11 @@ internal class UiHierarchyWebService : RestService() {
|
||||
get("/") {
|
||||
hierarchy()
|
||||
}
|
||||
get("/raw") {
|
||||
rawHierarchy()
|
||||
}
|
||||
get("/{id}") {
|
||||
val id = pathParameters["id"] ?: throw IllegalStateException("id parameter is not found")
|
||||
val component = UiHierarchyWebServiceExtension.EP_NAME.extensionList.firstNotNullOf { it.getComponentById(id) }
|
||||
partialHierarchy(component)
|
||||
}
|
||||
static("/static")
|
||||
}
|
||||
|
||||
private fun partialHierarchy(component: Component): String {
|
||||
val doc = XpathDataModelCreator(TextToKeyCache) { c, e ->
|
||||
val ref = InvokerService.getInstance().invoker!!.putAdhocReference(c)
|
||||
e.setAttribute("remoteId", ref.id)
|
||||
e.setAttribute("hashCode", ref.identityHashCode.toString())
|
||||
}.create(component)
|
||||
return W3CDom().asString(doc)
|
||||
}
|
||||
|
||||
private fun rawHierarchy(): String {
|
||||
val doc = XpathDataModelCreator(TextToKeyCache).create(null)
|
||||
return W3CDom().asString(doc)
|
||||
}
|
||||
|
||||
private fun hierarchy(): String {
|
||||
val doc = XpathDataModelCreator(TextToKeyCache).create(null)
|
||||
val doc = XpathDataModelCreator.create(null)
|
||||
return doc.convertToHtml()
|
||||
}
|
||||
|
||||
@@ -84,11 +57,3 @@ internal class UiHierarchyWebService : RestService() {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
interface UiHierarchyWebServiceExtension {
|
||||
companion object {
|
||||
val EP_NAME = ExtensionPointName<UiHierarchyWebServiceExtension>("com.jetbrains.performancePlugin.remoteDriver.uiHierarchyExtension")
|
||||
}
|
||||
|
||||
fun getComponentById(id: String): Component?
|
||||
}
|
||||
|
||||
+43
-19
@@ -2,6 +2,7 @@
|
||||
|
||||
package com.jetbrains.performancePlugin.remotedriver.xpath
|
||||
|
||||
import com.jetbrains.performancePlugin.jmxDriver.InvokerService
|
||||
import com.jetbrains.performancePlugin.remotedriver.dataextractor.TextParser
|
||||
import com.jetbrains.performancePlugin.remotedriver.dataextractor.TextToKeyCache
|
||||
import org.assertj.swing.edt.GuiActionRunner
|
||||
@@ -20,27 +21,39 @@ import javax.swing.JComponent
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
internal class XpathDataModelCreator(private val textToKeyCache: TextToKeyCache,
|
||||
private val bindComponentToElement: (Component, Element) -> Unit = { c, e ->
|
||||
e.setUserData("component", c, null)
|
||||
}) {
|
||||
object XpathDataModelCreator {
|
||||
interface ComponentTranslator {
|
||||
fun translate(c: Component, e: Element)
|
||||
}
|
||||
|
||||
private object DefaultComponentTranslator : ComponentTranslator {
|
||||
override fun translate(c: Component, e: Element) {
|
||||
e.setUserData("component", c, null)
|
||||
}
|
||||
}
|
||||
|
||||
private object RemoteComponentTranslator : ComponentTranslator {
|
||||
override fun translate(c: Component, e: Element) {
|
||||
val ref = InvokerService.getInstance().putReference(c)
|
||||
e.setAttribute("remoteId", ref.id)
|
||||
e.setAttribute("hashCode", ref.identityHashCode.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun addComponent(
|
||||
doc: Document,
|
||||
parentElement: Element,
|
||||
hierarchy: ComponentHierarchy,
|
||||
component: Component,
|
||||
translator: ComponentTranslator,
|
||||
targetComponent: Component? = null
|
||||
) {
|
||||
val subtreeOverrider = XpathDataModelSubTreeProvider.EP_NAME.extensionList.find { provider ->
|
||||
provider.shouldOverrideSubtree(component)
|
||||
}
|
||||
if (subtreeOverrider != null) {
|
||||
subtreeOverrider.overrideSubtree(doc, parentElement, component)
|
||||
XpathDataModelExtension.EP_NAME.extensionList.find { it.acceptComponent(component) }?.let {
|
||||
it.processComponent(doc, parentElement, component)
|
||||
return
|
||||
}
|
||||
|
||||
val element = createElement(doc, component, targetComponent)
|
||||
val element = createElement(doc, component, translator, targetComponent)
|
||||
parentElement.appendChild(element)
|
||||
|
||||
val allChildren = hierarchy.childrenOf(component)
|
||||
@@ -58,17 +71,17 @@ internal class XpathDataModelCreator(private val textToKeyCache: TextToKeyCache,
|
||||
addAll(filteredChildren)
|
||||
addAll(exceptionChildren)
|
||||
}.sortedWith(ComponentOrderComparator).forEach {
|
||||
addComponent(doc, element, hierarchy, it, targetComponent)
|
||||
addComponent(doc, element, hierarchy, it, translator, targetComponent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createElement(doc: Document, component: Component, targetComponent: Component? = null): Element {
|
||||
private fun createElement(doc: Document, component: Component, translator: ComponentTranslator, targetComponent: Component? = null): Element {
|
||||
|
||||
val element = doc.createElement("div")
|
||||
|
||||
component.fillElement(doc, element, targetComponent)
|
||||
|
||||
bindComponentToElement(component, element)
|
||||
translator.translate(component, element)
|
||||
return element
|
||||
}
|
||||
|
||||
@@ -123,7 +136,7 @@ internal class XpathDataModelCreator(private val textToKeyCache: TextToKeyCache,
|
||||
value?.removeInvalidXmlCharacters()?.apply {
|
||||
if (textFieldsFilter(attributeName, value)) {
|
||||
elementText.append("$attributeName: '$this'. ")
|
||||
textToKeyCache.findKey(value)?.apply {
|
||||
TextToKeyCache.findKey(value)?.apply {
|
||||
elementText.append("${attributeName}.key: '$this'. ")
|
||||
element.setAttribute(attributeName + ".key", this)
|
||||
}
|
||||
@@ -146,13 +159,13 @@ internal class XpathDataModelCreator(private val textToKeyCache: TextToKeyCache,
|
||||
}
|
||||
if (accessibleName != null) {
|
||||
element.setAttribute("accessiblename", accessibleName)
|
||||
textToKeyCache.findKey(accessibleName)?.apply { element.setAttribute("accessiblename.key", this) }
|
||||
TextToKeyCache.findKey(accessibleName)?.apply { element.setAttribute("accessiblename.key", this) }
|
||||
}
|
||||
|
||||
val tooltipText = getTooltipText(this)
|
||||
if (tooltipText != null) {
|
||||
element.setAttribute("tooltiptext", tooltipText)
|
||||
textToKeyCache.findKey(tooltipText)?.apply { element.setAttribute("tooltiptext.key", this) }
|
||||
TextToKeyCache.findKey(tooltipText)?.apply { element.setAttribute("tooltiptext.key", this) }
|
||||
}
|
||||
|
||||
if (isShowing) {
|
||||
@@ -166,7 +179,7 @@ internal class XpathDataModelCreator(private val textToKeyCache: TextToKeyCache,
|
||||
&& bounds.width > 0 && bounds.height > 0
|
||||
) {
|
||||
try {
|
||||
val foundText = TextParser.parseComponent(this, textToKeyCache)
|
||||
val foundText = TextParser.parseComponent(this, TextToKeyCache)
|
||||
val text = foundText.joinToString(" || ") { it.text }
|
||||
element.setAttribute("visible_text", text)
|
||||
if (text.trim().isNotEmpty()) {
|
||||
@@ -291,8 +304,19 @@ internal class XpathDataModelCreator(private val textToKeyCache: TextToKeyCache,
|
||||
|
||||
}
|
||||
|
||||
fun create(component: Component?, targetComponent: Component? = null): Document {
|
||||
return create(component, DefaultComponentTranslator, targetComponent)
|
||||
}
|
||||
|
||||
fun create(rootComponent: Component?, targetComponent: Component? = null): Document {
|
||||
fun createForRemote(component: Component?, targetComponent: Component? = null): Document {
|
||||
return create(component, RemoteComponentTranslator, targetComponent)
|
||||
}
|
||||
|
||||
private fun create(
|
||||
rootComponent: Component?,
|
||||
translator: ComponentTranslator,
|
||||
targetComponent: Component? = null,
|
||||
): Document {
|
||||
|
||||
val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument()
|
||||
|
||||
@@ -308,7 +332,7 @@ internal class XpathDataModelCreator(private val textToKeyCache: TextToKeyCache,
|
||||
hierarchy.roots()
|
||||
}
|
||||
containers.filter { it.isShowing || it.javaClass.name.endsWith("SharedOwnerFrame") }.forEach {
|
||||
addComponent(doc, rootElement, hierarchy, it, targetComponent)
|
||||
addComponent(doc, rootElement, hierarchy, it, translator, targetComponent)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
+4
-4
@@ -6,11 +6,11 @@ import org.w3c.dom.Document
|
||||
import org.w3c.dom.Element
|
||||
import java.awt.Component
|
||||
|
||||
interface XpathDataModelSubTreeProvider {
|
||||
interface XpathDataModelExtension {
|
||||
companion object {
|
||||
val EP_NAME = ExtensionPointName<XpathDataModelSubTreeProvider>("com.jetbrains.performancePlugin.remoteDriver.xpathDataModelExtension")
|
||||
val EP_NAME = ExtensionPointName.create<XpathDataModelExtension>("com.jetbrains.performancePlugin.remotedriver.xpathDataModelExtension")
|
||||
}
|
||||
|
||||
fun shouldOverrideSubtree(component: Component): Boolean
|
||||
fun overrideSubtree(doc: Document, parentElement: Element, component: Component)
|
||||
fun acceptComponent(component: Component): Boolean
|
||||
fun processComponent(doc: Document, parentElement: Element, component: Component)
|
||||
}
|
||||
+2
-4
@@ -6,15 +6,13 @@ import com.intellij.driver.model.LocalRefDelegate
|
||||
import com.intellij.driver.model.RefDelegate
|
||||
import com.intellij.driver.model.RemoteRefDelegate
|
||||
import com.intellij.driver.model.transport.Ref
|
||||
import com.jetbrains.performancePlugin.remotedriver.dataextractor.TextToKeyCache
|
||||
import org.w3c.dom.Element
|
||||
import org.w3c.dom.NodeList
|
||||
import java.awt.Component
|
||||
import javax.xml.xpath.XPathConstants
|
||||
import javax.xml.xpath.XPathFactory
|
||||
|
||||
internal class XpathSearcher(textToKeyCache: TextToKeyCache) {
|
||||
val modelCreator = XpathDataModelCreator(textToKeyCache)
|
||||
internal class XpathSearcher {
|
||||
private val xPath = XPathFactory.newInstance().newXPath()
|
||||
|
||||
fun findComponent(xpathExpression: String, component: Component?): RefDelegate<Component> {
|
||||
@@ -29,7 +27,7 @@ internal class XpathSearcher(textToKeyCache: TextToKeyCache) {
|
||||
}
|
||||
|
||||
fun findComponents(xpathExpression: String, component: Component?): List<RefDelegate<Component>> {
|
||||
val model = modelCreator.create(component)
|
||||
val model = XpathDataModelCreator.create(component)
|
||||
val result = xPath.compile(xpathExpression).evaluate(model, XPathConstants.NODESET) as NodeList
|
||||
return (0 until result.length).mapNotNull { result.item(it) }.filterIsInstance<Element>().mapNotNull {
|
||||
if (it.hasAttribute("remoteId")) {
|
||||
|
||||
Reference in New Issue
Block a user