mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[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:
committed by
intellij-monorepo-bot
parent
f72e6fd961
commit
950fc76efd
@@ -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)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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> {
|
||||
|
||||
+2
-2
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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)) }
|
||||
|
||||
+5
-4
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -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()
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+50
-8
@@ -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
|
||||
}
|
||||
}
|
||||
+256
-79
@@ -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
|
||||
}
|
||||
|
||||
+48
-9
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user