[driver-tests]: AT-1003: add logging, add waiters API, add javadocs

Merge-request: IJ-MR-137133
Merged-by: Anastasia Katsman <Anastasia.Katsman@jetbrains.com>

GitOrigin-RevId: dbb248292e74c0d1c0aed375538c4077e7b71440
This commit is contained in:
Anastasia Katsman
2024-06-26 20:25:33 +00:00
committed by intellij-monorepo-bot
parent f72e6fd961
commit 950fc76efd
20 changed files with 547 additions and 184 deletions
@@ -33,7 +33,7 @@ fun Driver.isCodeAnalysisFinished(project: Project? = null, file: VirtualFile):
fun Driver.waitForCodeAnalysis(project: Project? = null, file: VirtualFile, timeout: Duration = 1.minutes) {
withContext {
waitFor(timeout) {
waitFor("No Code analysis", timeout) {
isCodeAnalysisFinished(project, file)
}
}
@@ -110,7 +110,7 @@ fun Driver.openFile(relativePath: String, project: Project = singleProject(), wa
val service = service(GuestNavigationService::class, project)
withContext(OnDispatcher.EDT) {
service.navigateViaBackend(relativePath, 0)
waitFor(errorMessage = "Fail to open file $relativePath", duration = 30.seconds,
waitFor(message = "File is opened: $relativePath", timeout = 30.seconds,
getter = {
service<FileEditorManager>(project).getSelectedTextEditor()?.getVirtualFile()
},
@@ -21,7 +21,7 @@ fun Driver.areIndicatorsVisible(project: Project): Boolean {
}
fun Driver.waitForProjectOpen(timeout: Duration = 1.minutes) {
waitFor(timeout) {
waitFor("Project is opened", timeout) {
isProjectOpened()
}
}
@@ -37,7 +37,7 @@ fun Driver.waitForIndicators(timeout: Duration) {
private fun Driver.waitForIndicators(projectGet: () -> Project, timeout: Duration) {
var smartLongEnoughStart: Instant? = null
waitFor(timeout) {
waitFor("Indicators", timeout) {
if (!isProjectOpened() || runCatching { areIndicatorsVisible(projectGet.invoke()) }.getOrElse { false }) {
smartLongEnoughStart = null
return@waitFor false
@@ -16,7 +16,7 @@ interface FUSProjectHotStartUpMeasurerService {
* This is util method for such enforcement
*/
fun Driver.waitForStartupFUSToWrite() {
waitFor(duration = 10.seconds, interval = 1.seconds) {
waitFor("Fus startup is finished", timeout = 10.seconds, interval = 1.seconds) {
service(FUSProjectHotStartUpMeasurerService::class).isHandlingFinished()
}
}
@@ -8,8 +8,9 @@ import com.intellij.driver.sdk.ui.remote.Component
import com.intellij.driver.sdk.ui.remote.RobotProvider
import com.intellij.driver.sdk.ui.remote.SearchService
import org.intellij.lang.annotations.Language
import kotlin.time.Duration.Companion.seconds
internal const val DEFAULT_FIND_TIMEOUT_SECONDS = 15
internal val DEFAULT_FIND_TIMEOUT = 15.seconds
interface Finder {
val driver: Driver
@@ -20,6 +21,9 @@ interface Finder {
val isRemoteIdeMode: Boolean
get() = driver.isRemoteIdeMode
/**
* Creates UiComponent, the actual component will be requested lazily.
*/
fun x(@Language("xpath") xpath: String): UiComponent {
return UiComponent(ComponentData(xpath, driver, searchService, robotProvider, searchContext, null))
}
@@ -38,6 +42,9 @@ interface Finder {
).newInstance(ComponentData(xpath, driver, searchService, robotProvider, searchContext, null))
}
/**
* Provides a list of UI components based on the given XPath. Actual components list is requested lazily.
*/
fun xx(@Language("xpath") xpath: String): UIComponentsList<UiComponent> {
return UIComponentsList(xpath, UiComponent::class.java, driver, searchService, robotProvider, searchContext)
}
@@ -53,5 +60,8 @@ interface Finder {
interface SearchContext {
val context: String
val contextAsString: String
get() = if (context == "") "global context" else context
fun findAll(xpath: String): List<Component>
}
@@ -2,24 +2,40 @@ package com.intellij.driver.sdk.ui
import com.intellij.driver.model.TextData
import com.intellij.driver.sdk.ui.components.UiComponent
import com.intellij.openapi.diagnostic.logger
class UiText(private val component: UiComponent, private val textData: TextData) {
companion object {
private val LOG get() = logger<UiText>()
fun List<UiText>.allText(separator: String = "") = joinToString(separator) { it.text }
}
val text = textData.text
val point = textData.point
val bundleKey = textData.bundleKey
override fun toString(): String {
return "UiText[point=${point},text=$text]"
}
fun click() {
LOG.info("Click at '${text}'")
component.click(textData.point)
}
fun doubleClick() {
LOG.info("Double click at '${text}'")
component.doubleClick(textData.point)
}
fun rightClick() {
LOG.info("Right click at '${text}'")
component.rightClick(textData.point)
}
fun moveMouse() {
LOG.info("Move mouse to ${textData.point}")
component.moveMouse(textData.point)
}
}
@@ -74,5 +74,5 @@ class BookmarksPopupUiComponent(data: ComponentData) : UiComponent(data) {
.map { it.toString().replace("TreePathToRow{path=[", "").dropLast(2) }
fun clickBookmark(textContains: String, doubleClick: Boolean = false) =
bookmarksTree.findAllText().first { it.text.contains(textContains) }.apply { if (doubleClick) doubleClick() else click() }
bookmarksTree.waitSomeTextsContains(text = textContains).first().apply { if (doubleClick) doubleClick() else click() }
}
@@ -26,7 +26,7 @@ fun Finder.dialog(@Language("xpath") xpath: String? = null, action: DialogUiComp
x(xpath ?: "//div[@class='MyDialog']", DialogUiComponent::class.java).action()
fun Finder.waitForNoOpenedDialogs() {
waitFor(errorMessage = "Dialog is still opened", duration = 100.seconds) {
waitFor(message = "Dialog is closed", timeout = 100.seconds) {
!isDialogOpened()
}
}
@@ -3,10 +3,10 @@ package com.intellij.driver.sdk.ui.components
import com.intellij.driver.client.Remote
import com.intellij.driver.sdk.ui.Finder
import com.intellij.driver.sdk.ui.remote.REMOTE_ROBOT_MODULE_ID
import com.intellij.driver.sdk.ui.should
import org.intellij.lang.annotations.Language
import com.intellij.driver.sdk.waitForOne
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.intellij.lang.annotations.Language
import java.awt.Point
import kotlin.math.roundToInt
import kotlin.time.Duration
@@ -52,12 +52,8 @@ class JCefUI(data: ComponentData) : UiComponent(data) {
}
fun findElement(@Language("XPath") xpath: String, wait: Duration = 5.seconds): DomElement {
var elements: List<DomElement> = emptyList()
should(message = "Can't find element by '$xpath' in the embedded browser($component)", 5) {
elements = findElements(xpath)
elements.size == 1
}
return elements.single()
return waitForOne("Find element by '$xpath' in the embedded browser($component)", wait,
getter = { findElements(xpath) } )
}
fun findElements(@Language("xpath") xpath: String): List<DomElement> {
@@ -60,7 +60,7 @@ open class JTreeUiComponent(data: ComponentData) : UiComponent(data) {
fixture.expandAll(timeout.inWholeMilliseconds.toInt())
}
fun expandPath(vararg path: String, fullMatch: Boolean = true) = waitFor(10.seconds, errorMessage = "Failed find ${path.toList()}") {
fun expandPath(vararg path: String, fullMatch: Boolean = true) = waitFor("Expand path '${path.toList()}'", 10.seconds) {
try {
val expandedPath = mutableListOf<String>()
path.forEach {
@@ -81,7 +81,7 @@ open class JTreeUiComponent(data: ComponentData) : UiComponent(data) {
}
}
fun expandPathWithEnter(vararg path: String, fullMatch: Boolean = true) = waitFor(10.seconds, errorMessage = "Failed find ${path.toList()}") {
fun expandPathWithEnter(vararg path: String, fullMatch: Boolean = true) = waitFor("Expand path by enter '${path.toList()}'") {
try {
val expandedPath = mutableListOf<String>()
path.forEach {
@@ -12,7 +12,7 @@ class NavigationBarUi(data: ComponentData): UiComponent(data) {
}
val currentPath: List<String>
get() = findAllText().sortedBy { it.point.x }.map { it.text }
get() = getAllTexts().sortedBy { it.point.x }.map { it.text }
fun navBarItem(item: String): NavBarItemUi =
x(NavBarItemUi::class.java) { and(byClass("NavBarItemComponent"), byAccessibleName(item)) }
@@ -4,9 +4,8 @@ import com.intellij.driver.client.Remote
import com.intellij.driver.model.OnDispatcher
import com.intellij.driver.sdk.ui.Finder
import com.intellij.driver.sdk.ui.remote.Component
import com.intellij.driver.sdk.ui.should
import com.intellij.driver.sdk.waitForOne
import org.intellij.lang.annotations.Language
import java.util.*
import kotlin.time.Duration.Companion.seconds
fun Finder.popup(@Language("xpath") xpath: String? = null) =
@@ -25,8 +24,10 @@ class PopupMenuUiComponent(data: ComponentData) : UiComponent(data) {
fun select(vararg items: String) {
items.forEach { item ->
should(timeout = 5.seconds, message = "Fail to find items: ${items.contentToString()}") { menuItems.list().map { it.getText() }.contains(item) }
menuItems.list().first { it.getText() == item }.click()
waitForOne(message = "Find item: $item", timeout = 5.seconds,
getter = { menuItems.list() },
checker = { it.getText() == item })
.click()
}
}
@@ -3,6 +3,7 @@ package com.intellij.driver.sdk.ui.components
import com.intellij.driver.sdk.ui.Finder
import com.intellij.driver.sdk.ui.should
import org.intellij.lang.annotations.Language
import kotlin.time.Duration.Companion.seconds
fun Finder.searchEverywherePopup(@Language("xpath") xpath: String? = null) = x(xpath ?: "//div[@class='HeavyWeightWindow'][//div[contains(@class, 'SearchEverywhereUI')]]",
@@ -14,7 +15,7 @@ class SearchEverywherePopupUI(data: ComponentData): PopupUiComponent(data) {
fun searchAndChooseFirst(text: String, exactMatch: Boolean = true) {
keyboard {
enterText(text)
resultsList.should(15) {
resultsList.should(timeout = 15.seconds) {
if (exactMatch) hasText(text) else hasSubtext(text)
}
enter()
@@ -18,7 +18,7 @@ class ToolWindowToolbarUiComponent(data: ComponentData) : UiComponent(data) {
fun openToolWindowFromMore(title: String) {
moreButton.click()
driver.ui.popup().findAllText().first { it.text == title }.click()
driver.ui.popup().waitAnyTexts(text = title).first().click()
}
fun openToolWindow(title: String) {
@@ -1,19 +1,58 @@
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.*
import com.intellij.driver.sdk.ui.remote.Component
import com.intellij.driver.sdk.ui.remote.RobotProvider
import com.intellij.driver.sdk.ui.remote.SearchService
import com.intellij.driver.sdk.waitFor
import com.intellij.openapi.diagnostic.Logger
import kotlin.time.Duration
class UIComponentsList<T : UiComponent>(
private val xpath: String,
private val type: Class<T>,
val driver: Driver,
val searchService: SearchService,
val robotProvider: RobotProvider,
private val parentSearchContext: SearchContext,
) {
companion object {
private val LOG get() = Logger.getInstance(UIComponentsList::class.java)
/**
* Searches for a non-empty list of UI components based on the given XPath until timeout hits.
*/
fun Finder.waitAtLeastAny(message: String? = null, timeout: Duration = DEFAULT_FIND_TIMEOUT, init: QueryBuilder.() -> String): UIComponentsList<UiComponent> {
return waitFor(message, timeout,
getter = { UIComponentsList(xQuery { init() }, UiComponent::class.java, driver, searchService, robotProvider, searchContext) },
checker = { it.list().isNotEmpty() })
}
/**
* Waits for some ui components matching the given XPath.
*/
fun <T : UiComponent> Finder.waitAtLeastAny(message: String? = null, type: Class<T>, timeout: Duration = DEFAULT_FIND_TIMEOUT, init: QueryBuilder.() -> String): UIComponentsList<T> {
return waitFor(message, timeout,
getter = { UIComponentsList(xQuery { init() }, type, driver, searchService, robotProvider, searchContext) },
checker = { it.list().isNotEmpty() })
}
/**
* Waits until there are no ui components matching the given XPath.
*/
fun Finder.waitNotFound(message: String? = null, timeout: Duration = DEFAULT_FIND_TIMEOUT, init: QueryBuilder.() -> String): UIComponentsList<UiComponent> {
return waitFor(message, timeout,
getter = { UIComponentsList(xQuery { init() }, UiComponent::class.java, driver, searchService, robotProvider, searchContext) },
checker = { it.list().isEmpty() })
}
}
class UIComponentsList<T : UiComponent>(private val xpath: String,
private val type: Class<T>,
val driver: Driver,
val searchService: SearchService,
val robotProvider: RobotProvider,
private val parentSearchContext: SearchContext) {
fun list(): List<T> {
return parentSearchContext.findAll(xpath).mapIndexed { n, c ->
LOG.info("Requesting all ${type.simpleName}(s) by xpath = $xpath")
val components = parentSearchContext.findAll(xpath).mapIndexed { n, c ->
val searchContext = object : SearchContext {
override val context: String
get() = parentSearchContext.context + xpath + "[$n]"
@@ -26,5 +65,8 @@ class UIComponentsList<T : UiComponent>(private val xpath: String,
ComponentData::class.java
).newInstance(ComponentData(xpath, driver, searchService, robotProvider, searchContext, c))
}
LOG.info("Returning ${components.size} ${type.simpleName}(s) by xpath = $xpath" +
"\n${printableString(components.joinToString(", ") { it.toString() })}")
return components
}
}
@@ -2,18 +2,20 @@ package com.intellij.driver.sdk.ui.components
import com.intellij.driver.client.Driver
import com.intellij.driver.model.RemoteMouseButton
import com.intellij.driver.model.TextData
import com.intellij.driver.sdk.screenshot.takeScreenshot
import com.intellij.driver.sdk.ui.DEFAULT_FIND_TIMEOUT_SECONDS
import com.intellij.driver.sdk.ui.DEFAULT_FIND_TIMEOUT
import com.intellij.driver.sdk.ui.Finder
import com.intellij.driver.sdk.ui.SearchContext
import com.intellij.driver.sdk.ui.UiText
import com.intellij.driver.sdk.ui.UiText.Companion.allText
import com.intellij.driver.sdk.ui.keyboard.WithKeyboard
import com.intellij.driver.sdk.ui.remote.Component
import com.intellij.driver.sdk.ui.remote.Robot
import com.intellij.driver.sdk.ui.remote.RobotProvider
import com.intellij.driver.sdk.ui.remote.SearchService
import com.intellij.driver.sdk.waitFor
import com.intellij.driver.sdk.waitForOne
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.util.SystemInfo
import java.awt.Color
import java.awt.Point
@@ -23,14 +25,63 @@ import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
data class ComponentData(val xpath: String,
val driver: Driver,
val searchService: SearchService,
val robotProvider: RobotProvider,
val parentSearchContext: SearchContext,
val foundComponent: Component?)
data class ComponentData(
val xpath: String,
val driver: Driver,
val searchService: SearchService,
val robotProvider: RobotProvider,
val parentSearchContext: SearchContext,
val foundComponent: Component?,
)
open class UiComponent(private val data: ComponentData) : Finder, WithKeyboard {
companion object {
private val LOG get() = logger<UiComponent>()
/**
* Waits until the element specified is found within the parent search context. Doesn't guaranty visibility.
*
* @param timeout The maximum time to wait for the element to not be found. If not specified, the default timeout is used.
*/
fun <T : UiComponent> T.waitFound(timeout: Duration? = DEFAULT_FIND_TIMEOUT): T {
findThisComponent(timeout)
return this
}
/**
* Asserts that the current UI component is found. Doesn't check visibility.
*
* @return The current UI component.
*/
fun <T : UiComponent> T.assertFound(): T {
assert(present()) { "Component '$this' should be found" }
return this
}
/**
* Waits until the element specified is not found within the parent search context.
*
* @param timeout The maximum time to wait for the element to not be found. If not specified, the default timeout is used.
*/
fun <T : UiComponent> T.waitNotFound(timeout: Duration? = DEFAULT_FIND_TIMEOUT) {
waitFor(message = "No ${this::class.simpleName}[xpath=${data.xpath}] in ${data.parentSearchContext.contextAsString}",
timeout = timeout ?: DEFAULT_FIND_TIMEOUT,
interval = 1.seconds) {
kotlin.runCatching { findThisComponent(timeout) }.isFailure
}
}
/**
* Asserts that the calling UiComponent is not found in the hierarchy.
*/
fun <T : UiComponent> T.assertNotFound() {
assert(!present()) { "Component '$this' should not be found" }
}
}
override fun toString(): String {
return this::class.simpleName + "[xpath=${data.xpath}]"
}
private var cachedComponent: Component? = null
val component: Component
get() = data.foundComponent ?: kotlin.runCatching { cachedComponent?.takeIf { it.isShowing() } }.getOrNull()
@@ -40,17 +91,13 @@ open class UiComponent(private val data: ComponentData) : Finder, WithKeyboard {
robot.focus(this.component)
}
private fun findThisComponent(): Component {
lateinit var result: List<Component>
var actual = 0
waitFor(DEFAULT_FIND_TIMEOUT_SECONDS.seconds,
errorMessage = { "Can't find component with '${data.xpath}' in ${data.parentSearchContext.context.takeIf { it.isNotEmpty() } ?: "whole hierarchy"}, expected 1, but got ${actual}" }) {
result = data.parentSearchContext.findAll(data.xpath)
actual = result.size
result.size == 1
}
return result.first()
}
private fun findThisComponent(timeout: Duration? = DEFAULT_FIND_TIMEOUT): Component =
waitForOne(
message = "Find ${this::class.simpleName}[xpath=${data.xpath}] in ${data.parentSearchContext.contextAsString}",
timeout = timeout ?: DEFAULT_FIND_TIMEOUT,
interval = 1.seconds,
getter = { data.parentSearchContext.findAll(data.xpath) }
)
override val driver: Driver = data.driver
override val searchService: SearchService = data.searchService
@@ -68,64 +115,164 @@ open class UiComponent(private val data: ComponentData) : Finder, WithKeyboard {
}
}
// Search Text Locations
fun findText(text: String): UiText {
val allTexts = findAllText()
val filteredTexts = allTexts.filter { it.text == text }
if (filteredTexts.isEmpty()) {
throw AssertionError("No '$text' found. Available texts are:\n${allTexts.joinToString("\n") { it.text }}")
}
if (filteredTexts.size > 1) {
throw AssertionError("Found ${filteredTexts.size} texts '$text', expected 1")
}
return filteredTexts.first()
/*
Returns all UiText's matching predicate without waiting
*/
fun getAllTexts(predicate: ((UiText) -> Boolean)? = null): List<UiText> {
val allText = searchService.findAllText(component).map { UiText(this, it) }
return predicate?.let { allText.filter(predicate) } ?: allText
}
fun findFirst(text: String, duration: Duration = DEFAULT_FIND_TIMEOUT_SECONDS.seconds): UiText {
var allTexts = emptyList<UiText>()
waitFor(duration, errorMessage = "Can't find '$text' in ${data.parentSearchContext.context}") {
allTexts = findAllText()
allTexts.any { it.text == text }
}
return allTexts.first { it.text == text }
/**
* Returns all UiText objects matching the specified text without waiting.
*/
fun getAllTexts(text: String): List<UiText> {
return searchService.findAllText(component).map { UiText(this, it) }.filter { it.text == text }
}
fun findOne(text: String, duration: Duration = DEFAULT_FIND_TIMEOUT_SECONDS.seconds): UiText {
var allTexts = emptyList<UiText>()
waitFor(duration, errorMessage = "Can't find '$text' in ${data.parentSearchContext.context}") {
allTexts = findAllText()
allTexts.any { it.text == text }
}
val filteredTexts = allTexts.filter { it.text == text }
if (filteredTexts.size > 1) {
throw AssertionError("Found ${filteredTexts.size} texts '$text', expected 1")
}
return filteredTexts.first()
fun allTextAsString(): String {
return getAllTexts().allText()
}
fun findOneContainsText(text: String, ignoreCase: Boolean = true, duration: Duration = DEFAULT_FIND_TIMEOUT_SECONDS.seconds): UiText {
var allTexts = emptyList<UiText>()
waitFor(duration, errorMessage = "Can't find '$text' in ${data.parentSearchContext.context}") {
allTexts = findAllText()
allTexts.any { it.text.contains(text, ignoreCase = ignoreCase) }
}
val filteredTexts = allTexts.filter { it.text.contains(text, ignoreCase = ignoreCase) }
if (filteredTexts.size > 1) {
throw AssertionError("Found ${filteredTexts.size} texts '$text', expected 1")
}
return filteredTexts.first()
/**
* Waits for a non-empty list of UiText's.
*/
fun waitAnyTexts(message: String? = null, timeout: Duration = DEFAULT_FIND_TIMEOUT): List<UiText> {
return waitFor(message = message ?: "Finding at least some texts in $this",
timeout = timeout,
getter = { getAllTexts() },
checker = { it.isNotEmpty() }
)
}
/**
* Waits for a non-empty list of UiText's matching predicate.
*/
fun waitAnyTexts(message: String? = null, timeout: Duration = DEFAULT_FIND_TIMEOUT, predicate: (UiText) -> Boolean = { true }): List<UiText> {
return waitFor(message = message ?: "Finding at least some texts and filter matching predicate in $this",
timeout = timeout,
getter = { getAllTexts() },
checker = { it.any(predicate) }
).filter(predicate)
}
/**
* Waits for a non-empty list of UiText's with text '$text'.
*/
fun waitAnyTexts(text: String, message: String? = null, timeout: Duration = DEFAULT_FIND_TIMEOUT): List<UiText> {
return waitFor(message = message ?: "Finding at least some texts and filter '$text' in $this",
timeout = timeout,
getter = { getAllTexts() },
checker = { it.any { it.text == text } }
).filter { it.text == text }
}
/**
* Waits for one UiText with text '$text'.
*/
fun waitOneText(text: String, message: String, timeout: Duration = DEFAULT_FIND_TIMEOUT): UiText {
return waitForOne(message = message,
timeout = timeout,
getter = { getAllTexts() },
checker = { it.text == text }
)
}
/**
* Waits for one UiText with text '$text'.
*/
fun waitOneText(text: String, timeout: Duration = DEFAULT_FIND_TIMEOUT): UiText {
return waitOneText(message = "Finding text '$text' in $this", timeout = timeout, text = text)
}
/**
* Waits for one UiText matching predicate.
*/
fun waitOneText(message: String? = null, timeout: Duration = DEFAULT_FIND_TIMEOUT, predicate: (UiText) -> Boolean): UiText {
return waitForOne(message = message ?: "Finding one text matching predicate in $this",
timeout = timeout,
getter = { getAllTexts() },
checker = { predicate(it) }
)
}
/**
* Waits until there is no UiText's.
*/
fun waitNoTexts(message: String? = null, timeout: Duration = DEFAULT_FIND_TIMEOUT): List<UiText> {
return waitFor(message = message ?: "Finding no texts in $this",
timeout = timeout,
getter = { getAllTexts() },
checker = { it.isEmpty() }
)
}
/**
* Waits until there is no UiText's with text '$text'.
*/
fun waitNoTexts(text: String, message: String? = null, timeout: Duration = DEFAULT_FIND_TIMEOUT): List<UiText> {
return waitFor(message = message ?: "Finding no texts '$text' in $this",
timeout = timeout,
getter = { getAllTexts() },
checker = { it.filter { it.text == text }.isEmpty() }
)
}
/**
* Waits for a non-empty list of UiText's with substring '$text'.
*/
fun waitSomeTextsContains(text: String, message: String? = null, timeout: Duration = DEFAULT_FIND_TIMEOUT): List<UiText> {
return waitFor(message = message ?: "Finding at least some texts and contains '$text' in $this",
timeout = timeout,
getter = { getAllTexts() },
checker = { it.any { it.text.contains(text) } }
)
}
/**
* Waits until there is one UiText's with substring '$text'.
*/
fun waitOneContainsText(text: String, message: String, ignoreCase: Boolean = true, timeout: Duration = DEFAULT_FIND_TIMEOUT): UiText {
return waitForOne(message = message,
timeout = timeout,
getter = { getAllTexts() },
checker = { it.text.contains(text, ignoreCase = ignoreCase) }
)
}
/**
* Waits until there is one UiText's with substring '$text'.
*/
fun waitOneContainsText(text: String, ignoreCase: Boolean = true, timeout: Duration = DEFAULT_FIND_TIMEOUT): UiText {
return waitOneContainsText(message = "Finding the text containing '$text' in $this",
timeout = timeout, text = text, ignoreCase = ignoreCase)
}
/**
* Waits until all text contains 'text'.
*/
fun waitContainsText(text: String, message: String, ignoreCase: Boolean = true, timeout: Duration = DEFAULT_FIND_TIMEOUT) {
waitFor(message = message,
timeout = timeout,
getter = { getAllTexts().allText() },
checker = { it.contains(text, ignoreCase = ignoreCase) }
)
}
/**
* Waits until all text contains 'text'.
*/
fun waitContainsText(text: String, ignoreCase: Boolean = true, timeout: Duration = DEFAULT_FIND_TIMEOUT) =
waitContainsText(message = "Finding the text containing '$text' in $this", timeout = timeout, text = text, ignoreCase = ignoreCase)
fun hasText(text: String): Boolean {
return findAllText {
it.text == text
}.isNotEmpty()
return getAllTexts(text).isNotEmpty()
}
fun hasTextSequence(vararg texts: String, indexOffset: Int = 0): Boolean {
require(indexOffset >= 0) { "Value must be non-negative" }
val stringList = texts.toList()
val uiTextList = findAllText()
val uiTextList = getAllTexts()
return stringList.indices.all { index ->
val uiTextIndex = index + indexOffset
uiTextIndex in uiTextList.indices && stringList[index] == uiTextList[uiTextIndex].text
@@ -133,14 +280,51 @@ open class UiComponent(private val data: ComponentData) : Finder, WithKeyboard {
}
fun hasSubtext(subtext: String): Boolean {
return findAllText {
it.text.contains(subtext)
}.isNotEmpty()
return getAllTexts { it.text.contains(subtext) }.isNotEmpty()
}
fun findText(predicate: (TextData) -> Boolean): UiText {
return searchService.findAllText(component).single(predicate).let { UiText(this, it) }
}
/**
* Retrieves all UI text elements in a vertically ordered manner.
*
* This method returns a list of lists of `UiText` objects. Each inner list represents a collection of UI text
* elements that are located at the same vertical position on the screen. The outer list represents the entire
* collection of vertically ordered UI text elements.
*
* The UI text elements are ordered horizontally within each vertical position, based on their `x` coordinates.
*
* @return A list of lists of `UiText` objects, representing all UI text elements ordered vertically.
*/
fun getAllVerticallyOrderedUiText(): List<List<UiText>> =
getAllTexts().groupBy { it.point.y }.toSortedMap()
.values.map { it.sortedBy { it.point.x } }.toList()
/**
* Waits for exactly one match in the list of vertically ordered UI text elements.
* That list is originally returned by `getAllVerticallyOrderedUiText`
*
* This method waits until there is exactly one UI text element in the list that matches the specified conditions.
* The conditions can be either a full match of the text or a partial match if the `fullMatch` parameter is set to `false`.
*
* @param message An optional message to display when waiting for a match.
* @param text The text to match against the UI text elements.
* @param fullMatch Flag to indicate whether the match should be a full match or partial match.
* @param timeout The maximum time to wait for the match.
* @return A list of `UiText` objects that match the specified conditions.
*/
fun waitOneMatchInVerticallyOrderedText(message: String? = null, text: String, fullMatch: Boolean = true, timeout: Duration = DEFAULT_FIND_TIMEOUT): List<UiText> =
waitForOne(message, timeout,
getter = { getAllVerticallyOrderedUiText() },
checker = {
if (fullMatch) {
text == it.allText()
}
else {
it.allText().contains(text)
}
})
fun waitOneMatchInVerticallyOrderedText(text: String, fullMatch: Boolean = true, timeout: Duration = DEFAULT_FIND_TIMEOUT): List<UiText> =
waitOneMatchInVerticallyOrderedText("Find '${text}'(fullMatch = $fullMatch) in vertically ordered text", text, fullMatch, timeout = timeout)
fun present(): Boolean {
return data.parentSearchContext.findAll(data.xpath).isNotEmpty()
@@ -150,22 +334,14 @@ open class UiComponent(private val data: ComponentData) : Finder, WithKeyboard {
return data.parentSearchContext.findAll(data.xpath).isEmpty()
}
fun hasText(predicate: (TextData) -> Boolean): Boolean {
return searchService.findAllText(component).any(predicate)
fun hasText(predicate: (UiText) -> Boolean): Boolean {
return getAllTexts().any(predicate)
}
fun isVisible(): Boolean = component.isVisible()
fun isEnabled(): Boolean = component.isEnabled()
fun findAllText(predicate: (TextData) -> Boolean): List<UiText> {
return searchService.findAllText(component).filter(predicate).map { UiText(this, it) }
}
fun findAllText(): List<UiText> {
return searchService.findAllText(component).map { UiText(this, it) }
}
fun hasVisibleComponent(component: UiComponent): Boolean {
val components = searchContext.findAll(component.data.xpath)
if (components.isEmpty()) return false
@@ -217,6 +393,7 @@ open class UiComponent(private val data: ComponentData) : Finder, WithKeyboard {
}
fun click(button: RemoteMouseButton, count: Int) {
LOG.info("Click with $button $count times at $this")
robot.click(component, button, count)
}
@@ -5,21 +5,20 @@ import com.intellij.driver.sdk.ui.components.UiComponent
import com.intellij.driver.sdk.ui.components.button
import com.intellij.driver.sdk.waitFor
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
// should
infix fun <T : UiComponent> T.should(condition: T.() -> Boolean): T {
return should(timeout = DEFAULT_FIND_TIMEOUT_SECONDS.seconds, condition = condition)
return should(timeout = DEFAULT_FIND_TIMEOUT, condition = condition)
}
// should not
infix fun <T : UiComponent> T.shouldNot(condition: T.() -> Boolean): T {
return should(timeout = DEFAULT_FIND_TIMEOUT_SECONDS.seconds, condition = { !condition() })
return should(timeout = DEFAULT_FIND_TIMEOUT, condition = { !condition() })
}
// should
infix fun <T : UiComponent> T.shouldBe(condition: T.() -> Boolean): T {
return should(timeout = DEFAULT_FIND_TIMEOUT_SECONDS.seconds, condition = condition)
return should(timeout = DEFAULT_FIND_TIMEOUT, condition = condition)
}
fun <T : UiComponent> T.shouldBe(message: String, condition: T.() -> Boolean, timeout: Duration): T {
@@ -27,7 +26,7 @@ fun <T : UiComponent> T.shouldBe(message: String, condition: T.() -> Boolean, ti
}
fun <T : UiComponent> T.shouldBe(message: String, condition: T.() -> Boolean): T {
return should(message = message, timeout = DEFAULT_FIND_TIMEOUT_SECONDS.seconds, condition = condition)
return should(message = message, timeout = DEFAULT_FIND_TIMEOUT, condition = condition)
}
fun <T : UiComponent> T.shouldBe(condition: T.() -> Boolean, timeout: Duration): T {
@@ -36,11 +35,11 @@ fun <T : UiComponent> T.shouldBe(condition: T.() -> Boolean, timeout: Duration):
// should
infix fun <T : UiComponent> T.shouldHave(condition: T.() -> Boolean): T {
return should(timeout = DEFAULT_FIND_TIMEOUT_SECONDS.seconds, condition = condition)
return should(timeout = DEFAULT_FIND_TIMEOUT, condition = condition)
}
fun <T : UiComponent> T.shouldHave(message: String, condition: T.() -> Boolean): T {
return should(message = message, timeout = DEFAULT_FIND_TIMEOUT_SECONDS.seconds, condition = condition)
return should(message = message, timeout = DEFAULT_FIND_TIMEOUT, condition = condition)
}
fun <T : UiComponent> T.shouldHave(condition: T.() -> Boolean, timeout: Duration): T {
@@ -51,22 +50,12 @@ fun <T : UiComponent> T.shouldHave(message: String, condition: T.() -> Boolean,
return should(message = message, timeout = timeout, condition = condition)
}
fun <T : UiComponent> T.should(seconds: Int = DEFAULT_FIND_TIMEOUT_SECONDS, condition: T.() -> Boolean): T {
return should(timeout = seconds.seconds, condition = condition)
}
fun <T : UiComponent> T.should(message: String,
seconds: Int = DEFAULT_FIND_TIMEOUT_SECONDS,
condition: T.() -> Boolean): T {
return should(message = message, timeout = seconds.seconds, condition = condition)
}
fun <T : UiComponent> T.should(message: String = "",
timeout: Duration = DEFAULT_FIND_TIMEOUT_SECONDS.seconds,
fun <T : UiComponent> T.should(message: String? = null,
timeout: Duration = DEFAULT_FIND_TIMEOUT,
condition: T.() -> Boolean): T {
var lastException: Throwable? = null
try {
waitFor(timeout, errorMessage = message) {
waitFor(message, timeout) {
try {
this.condition()
}
@@ -76,7 +65,7 @@ fun <T : UiComponent> T.should(message: String = "",
}
}
} catch (e: WaitForException){
throw WaitForException(e.duration, e.errorMessage, lastException)
throw WaitForException(e.timeout, e.errorMessage, lastException)
}
return this
}
@@ -1,22 +1,56 @@
package com.intellij.driver.sdk.ui.keyboard
import com.intellij.driver.sdk.ui.remote.Robot
import com.intellij.openapi.diagnostic.logger
import java.awt.event.KeyEvent
class RemoteKeyboard(private val robot: Robot) {
companion object {
private val LOG
get() = logger<RemoteKeyboard>()
}
fun key(key: Int) = robot.pressAndReleaseKey(key)
fun enter() = key(KeyEvent.VK_ENTER)
fun escape() = key(KeyEvent.VK_ESCAPE)
fun down() = key(KeyEvent.VK_DOWN)
fun up() = key(KeyEvent.VK_UP)
fun left() = key(KeyEvent.VK_LEFT)
fun right() = key(KeyEvent.VK_RIGHT)
fun backspace() = key(KeyEvent.VK_BACK_SPACE)
fun tab() = key(KeyEvent.VK_TAB)
fun space() = key(KeyEvent.VK_SPACE)
fun enter() {
LOG.info("Pressing enter")
key(KeyEvent.VK_ENTER)
}
fun escape() {
LOG.info("Pressing escape")
key(KeyEvent.VK_ESCAPE)
}
fun down() {
LOG.info("Pressing down")
key(KeyEvent.VK_DOWN)
}
fun up() {
LOG.info("Pressing up")
key(KeyEvent.VK_UP)
}
fun left() {
LOG.info("Pressing left")
key(KeyEvent.VK_LEFT)
}
fun right() {
LOG.info("Pressing right")
key(KeyEvent.VK_RIGHT)
}
fun backspace() {
LOG.info("Pressing backspace")
key(KeyEvent.VK_BACK_SPACE)
}
fun tab() {
LOG.info("Pressing tab")
key(KeyEvent.VK_TAB)
}
fun space() {
LOG.info("Pressing space")
key(KeyEvent.VK_SPACE)
}
fun hotKey(vararg keyCodes: Int) {
LOG.info("Pressing hotkeys ${keyCodes.joinToString(",") {"'$it'"}}.")
keyCodes.forEach {
robot.pressKey(it)
Thread.sleep(100)
@@ -28,6 +62,7 @@ class RemoteKeyboard(private val robot: Robot) {
}
fun enterText(text: String, delayBetweenCharsInMs: Long = 50) {
LOG.info("Entering text '$text'.")
text.forEach {
robot.type(it)
Thread.sleep(delayBetweenCharsInMs)
@@ -35,12 +70,16 @@ class RemoteKeyboard(private val robot: Robot) {
}
fun pressing(key: Int, doWhilePress: RemoteKeyboard.() -> Unit) {
LOG.info("Performing action while pressing $key.")
robot.pressKey(key)
this.doWhilePress()
robot.releaseKey(key)
}
fun doublePressing(key: Int, doWhilePress: RemoteKeyboard.() -> Unit) {
LOG.info("Performing action while double pressing $key.")
try {
robot.doublePressKeyAndHold(key)
this.doWhilePress()
@@ -28,3 +28,16 @@ interface RectangleRef {
fun getY(): Double
fun getWidth(): Double
}
fun printableString(toPrint: String): String {
val resultString = toPrint.let {
val maxLength = 600
if (it.length < maxLength) {
it
}
else {
it.take(maxLength) + "..."
}
}
return resultString
}
@@ -1,54 +1,72 @@
package com.intellij.driver.sdk
import com.intellij.driver.sdk.ui.components.UiComponent
import com.intellij.driver.sdk.ui.printableString
import com.intellij.openapi.diagnostic.fileLogger
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
fun waitFor(
duration: Duration = 5.seconds,
interval: Duration = 1.seconds,
errorMessage: String = "",
condition: () -> Boolean
) {
waitFor(duration = duration, interval = interval, errorMessage = errorMessage, getter = condition, checker = { it })
}
private val LOG get() = fileLogger()
fun waitFor(
duration: Duration = 5.seconds,
message: String? = null,
timeout: Duration = 5.seconds,
interval: Duration = 1.seconds,
errorMessage: () -> String,
condition: () -> Boolean
errorMessage: (() -> String)? = null,
condition: () -> Boolean,
) {
waitFor(duration = duration, interval = interval, errorMessage = errorMessage, getter = condition, checker = { it })
waitFor(message = message,
timeout = timeout,
interval = interval,
errorMessage = if (errorMessage == null) {
null
}
else {
run@{ errorMessage.invoke() }
},
getter = condition,
checker = { it })
}
fun <T> waitNotNull(
duration: Duration = 5.seconds,
message: String? = null,
timeout: Duration = 5.seconds,
interval: Duration = 1.seconds,
errorMessage: String = "",
getter: () -> T?
errorMessage: ((T?) -> String)? = null,
getter: () -> T?,
): T {
return waitFor(duration = duration, interval = interval, errorMessage = errorMessage, getter = getter, checker = { it != null })!!
return waitFor(message = message, timeout = timeout,
interval = interval,
errorMessage = if (errorMessage == null) {
null
}
else { it -> errorMessage.invoke(it) },
getter = getter,
checker = { it != null }
)!!
}
fun <T> waitFor(
duration: Duration = 5.seconds,
interval: Duration = 1.seconds,
errorMessage: String = "",
getter: () -> T,
checker: (T) -> Boolean
): T {
return waitFor(duration = duration, interval = interval, errorMessage = { errorMessage }, getter = getter, checker = checker)
private fun logAwaitStart(message: String?, timeout: Duration) {
message?.let { LOG.info("Await: '$it' with timeout $timeout") }
}
private fun <T> logAwaitFinish(message: String?, result: T) {
message?.let {
LOG.info("Await: '$it' resulted with \n\t${printableString(result.toString())}")
}
}
fun <T> waitFor(
duration: Duration = 5.seconds,
message: String? = null,
timeout: Duration = 5.seconds,
interval: Duration = 1.seconds,
errorMessage: () -> String = { "" },
errorMessage: ((T) -> String)? = null,
getter: () -> T,
checker: (T) -> Boolean
checker: (T) -> Boolean,
): T {
val endTime = System.currentTimeMillis() + duration.inWholeMilliseconds
logAwaitStart(message, timeout)
val endTime = System.currentTimeMillis() + timeout.inWholeMilliseconds
var now = System.currentTimeMillis()
var result = getter()
while (now < endTime && checker(result).not()) {
@@ -57,26 +75,87 @@ fun <T> waitFor(
now = System.currentTimeMillis()
}
if (checker(result).not()) {
throw WaitForException(duration, errorMessage() + if (result !is Boolean) " Actual: $result" else "")
throw WaitForException(timeout,
errorMessage = errorMessage?.invoke(result)
?: ("Failed: $message" + if (result !is Boolean) ". Actual: $result" else ""))
}
else {
if (result !is Boolean) {
logAwaitFinish(message, result)
}
return result
}
}
fun <T> retryWithFallback(
fallbackAction: () -> T,
mainAction: () -> T,
/**
* Waits until there is exactly one match in getter result abiding checker.
*/
fun <T> waitForOne(
message: String? = null,
timeout: Duration = 5.seconds,
interval: Duration = 1.seconds,
errorMessage: ((List<T>) -> String)? = null,
getter: () -> List<T>,
checker: (T) -> Boolean,
): T {
return try {
mainAction()
} catch (_: Exception) {
fallbackAction()
mainAction()
logAwaitStart(message, timeout)
val endTime = System.currentTimeMillis() + timeout.inWholeMilliseconds
var now = System.currentTimeMillis()
var resultList = getter()
var filteredResultList = resultList.filter { checker(it) }
while (now < endTime && filteredResultList.size != 1) {
Thread.sleep(interval.inWholeMilliseconds)
resultList = getter()
filteredResultList = resultList.filter { checker(it) }
now = System.currentTimeMillis()
}
if (filteredResultList.size != 1) {
throw WaitForException(timeout,
errorMessage = errorMessage?.invoke(resultList)
?: ("Failed: $message. " +
"\n\tExpected one suitable instance, but got: " +
"\n\tReceived list: ${resultList.joinToString("\n\t")}" +
"\n\tSuitable list: ${filteredResultList.joinToString("\n\t")}"))
}
else {
return filteredResultList.single().also {
logAwaitFinish(message, it)
}
}
}
fun <T> withRetries(times: Int, onError: () -> Unit = {}, f: () -> T): T {
fun <T> waitForOne(
message: String? = null,
timeout: Duration = 5.seconds,
interval: Duration = 1.seconds,
errorMessage: ((List<T>) -> String)? = null,
getter: () -> List<T>,
): T {
logAwaitStart(message, timeout)
val endTime = System.currentTimeMillis() + timeout.inWholeMilliseconds
var now = System.currentTimeMillis()
var resultList = getter()
while (now < endTime && resultList.size != 1) {
Thread.sleep(interval.inWholeMilliseconds)
resultList = getter()
now = System.currentTimeMillis()
}
if (resultList.size != 1) {
throw WaitForException(timeout,
errorMessage = errorMessage?.invoke(resultList)
?: ("Failed: $message. " +
"\n\tExpected one suitable instance, but got:" +
"\n\t${resultList.joinToString("\n\t")}"))
}
else {
return resultList.single().also {
logAwaitFinish(message, it)
}
}
}
fun <T> withRetries(message: String? = null, times: Int, onError: () -> Unit = {}, f: () -> T): T {
message?.let { LOG.info("With $times attempts: $it") }
require(times > 0)
var lastException: Exception? = null
for (i in 0 until times) {
@@ -91,10 +170,10 @@ fun <T> withRetries(times: Int, onError: () -> Unit = {}, f: () -> T): T {
throw RetryException(lastException!!)
}
class WaitForException(val duration: Duration, val errorMessage: String, cause: Throwable? = null) : IllegalStateException("Timeout($duration): $errorMessage", cause)
class RetryException(cause: Exception): RuntimeException(cause)
class WaitForException(val timeout: Duration, val errorMessage: String, cause: Throwable? = null) : IllegalStateException("Timeout($timeout): $errorMessage", cause)
class RetryException(cause: Exception) : RuntimeException(cause)
fun <T : UiComponent> T.wait(duration: Duration): T {
Thread.sleep(duration.inWholeMilliseconds)
fun <T : UiComponent> T.wait(timeout: Duration): T {
Thread.sleep(timeout.inWholeMilliseconds)
return this
}