Skip to content

Commit 7a0d69e

Browse files
authored
ISSUE-704: Add ability to use logical OR in the declaration of selectors of views (#705)
* ISSUE-704: Add ability to use logical OR in the declaration of selectors of views
1 parent 6712af9 commit 7a0d69e

15 files changed

Lines changed: 214 additions & 23 deletions

File tree

kaspresso/src/main/kotlin/com/kaspersky/kaspresso/interceptors/watcher/kautomator/impl/logging/LoggingObjectWatcherInterceptor.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class LoggingObjectWatcherInterceptor(
2424
logger.i(
2525
"The object: ${interaction.description}. " +
2626
"The operation: Check=${assertion.type}(description={${assertion.description}}. " +
27-
"Additional info: the object was found by selector=${interaction.selector.bySelector}"
27+
"Additional info: the object was found by selectors=${interaction.selector.selectors} and index=${interaction.selector.selectors}"
2828
)
2929
}
3030

@@ -38,7 +38,7 @@ class LoggingObjectWatcherInterceptor(
3838
logger.i(
3939
"The object: ${interaction.description}. " +
4040
"The operation: Action=${action.type}(description={${action.description}}. " +
41-
"Additional info: the object was found by selector=${interaction.selector.bySelector}"
41+
"Additional info: the object was found by selectors=${interaction.selector.selectors} and index=${interaction.selector.selectors}"
4242
)
4343
}
4444
}

kautomator/src/main/kotlin/com/kaspersky/components/kautomator/component/common/actions/UiScrollableActions.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ interface UiScrollableActions : UiBaseActions {
2828
view.perform(UiScrollableActionType.SCROLL_TO_VIEW) {
2929
val scrollable = UiScrollable(UiSelector().resourceId(resourceName))
3030
do {
31-
if (findObject(to.view.interaction.selector.bySelector) != null)
32-
return@perform
31+
to.view.interaction.reFindUiObject()
32+
if (to.view.interaction.uiObject2 != null) return@perform
3333
} while (scrollable.scrollForward())
3434
do {
35-
if (findObject(to.view.interaction.selector.bySelector) != null)
36-
return@perform
35+
to.view.interaction.reFindUiObject()
36+
if (to.view.interaction.uiObject2 != null) return@perform
3737
} while (scrollable.scrollBackward())
3838
to.isDisplayed()
3939
}

kautomator/src/main/kotlin/com/kaspersky/components/kautomator/component/common/builders/UiViewBuilder.kt

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@file:Suppress("unused")
2+
23
package com.kaspersky.components.kautomator.component.common.builders
34

45
import android.os.Build
@@ -8,7 +9,6 @@ import androidx.annotation.StringRes
89
import androidx.test.platform.app.InstrumentationRegistry
910
import androidx.test.uiautomator.BySelector
1011
import androidx.test.uiautomator.BySelectorHack
11-
import com.google.common.truth.Truth.assertThat
1212
import com.kaspersky.components.kautomator.common.resources.KId
1313
import com.kaspersky.components.kautomator.common.resources.KString
1414
import com.kaspersky.components.kautomator.component.common.KautomatorMarker
@@ -26,6 +26,7 @@ class UiViewBuilder {
2626

2727
private var index: Int = 0
2828
private var selector: BySelector? = null
29+
private val selectors: MutableList<BySelector> = mutableListOf()
2930

3031
/**
3132
* Matches only view at given [index], if there are multiple views that matches
@@ -280,7 +281,7 @@ class UiViewBuilder {
280281
* @param function ViewBuilder which will result in descendant matcher
281282
*/
282283
fun withDescendant(function: UiViewBuilder.() -> Unit) =
283-
addSelector { hasDescendant(UiViewBuilder().apply(function).build().bySelector) }
284+
addSelector { hasDescendant(UiViewBuilder().apply(function).build().selectors.first()) }
284285

285286
/**
286287
* Matches the view which has descendant of given matcher with the maximum depth under the
@@ -290,7 +291,7 @@ class UiViewBuilder {
290291
* @param maxDepth The maximum depth under the element to search the descendant
291292
*/
292293
fun withDescendant(maxDepth: Int, function: UiViewBuilder.() -> Unit) =
293-
addSelector { hasDescendant(UiViewBuilder().apply(function).build().bySelector, maxDepth) }
294+
addSelector { hasDescendant(UiViewBuilder().apply(function).build().selectors.first(), maxDepth) }
294295

295296
/**
296297
* Matches the view that is at a certain depth
@@ -326,7 +327,8 @@ class UiViewBuilder {
326327
/**
327328
* Matches the view that has parent which fits the given matcher
328329
*/
329-
fun withParent(function: UiViewBuilder.() -> Unit) = addSelector { hasParent(UiViewBuilder().apply(function).build().bySelector) }
330+
fun withParent(function: UiViewBuilder.() -> Unit) =
331+
addSelector { hasParent(UiViewBuilder().apply(function).build().selectors.first()) }
330332

331333
/**
332334
* Matches the view with given hint
@@ -358,7 +360,7 @@ class UiViewBuilder {
358360
* @param function ViewBuilder which will result in child matcher
359361
*/
360362
fun withChild(function: UiViewBuilder.() -> Unit) =
361-
addSelector { hasChild(UiViewBuilder().apply(function).build().bySelector) }
363+
addSelector { hasChild(UiViewBuilder().apply(function).build().selectors.first()) }
362364

363365
/**
364366
* Matches the view which class matches given name
@@ -396,11 +398,30 @@ class UiViewBuilder {
396398
fun withSelector(selector: BySelector.() -> BySelector) = addSelector(selector)
397399

398400
/**
399-
* Returns combined [BySelector] with all passed conditions
401+
* Gives the ability to find the first matched view by several [BySelector]'s (logical OR combiner).
402+
* Multiple OR blocks are possible, but all of them must be declared in the end of the [UiViewBuilder] block.
403+
*/
404+
fun or(function: UiViewBuilder.() -> Unit) {
405+
checkAndAddSelector()
406+
selector = null
407+
function()
408+
}
409+
410+
/**
411+
* @return [UiViewSelector] with index of the needed element and the list of [BySelector]'s.
412+
* Each [BySelector] could contain multiple AND conditions by itself.
413+
* The list is used to combine [BySelector]'s with logical OR condition.
400414
*/
401415
fun build(): UiViewSelector {
402-
assertThat(selector).isNotNull()
403-
return UiViewSelector(index, selector as BySelector)
416+
checkAndAddSelector()
417+
return UiViewSelector(index, selectors)
418+
}
419+
420+
private fun checkAndAddSelector() {
421+
with(selector) {
422+
if (this == null) throw IllegalStateException("BySelector wasn't initialized")
423+
selectors.add(this)
424+
}
404425
}
405426

406427
private fun addSelector(condition: BySelector.() -> BySelector) {

kautomator/src/main/kotlin/com/kaspersky/components/kautomator/component/common/builders/UiViewSelector.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,10 @@ import androidx.test.uiautomator.BySelector
44

55
data class UiViewSelector(
66
val index: Int,
7-
val bySelector: BySelector
8-
)
7+
val selectors: List<BySelector>,
8+
) {
9+
constructor(
10+
index: Int,
11+
selector: BySelector
12+
) : this(index, listOf(selector))
13+
}

kautomator/src/main/kotlin/com/kaspersky/components/kautomator/intercept/exception/UnfoundedUiObjectException.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ import com.kaspersky.components.kautomator.component.common.builders.UiViewSelec
88
class UnfoundedUiObjectException(selector: UiViewSelector) :
99
RuntimeException(
1010
"The UiObject2 was not found on the screen. " +
11-
"The selector=${selector.bySelector}, index=${selector.index}"
11+
"Selectors=${selector.selectors}, index=${selector.index}"
1212
)

kautomator/src/main/kotlin/com/kaspersky/components/kautomator/intercept/interaction/UiObjectInteraction.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.kaspersky.components.kautomator.intercept.interaction
22

3+
import androidx.test.uiautomator.BySelector
34
import androidx.test.uiautomator.UiDevice
45
import androidx.test.uiautomator.UiObject2
56
import com.kaspersky.components.kautomator.component.common.builders.UiViewSelector
@@ -20,7 +21,11 @@ class UiObjectInteraction(
2021
private set
2122

2223
private fun calculateUiObject(): UiObject2? {
23-
val uiObjects = device.findObjects(selector.bySelector)
24+
return selector.selectors.firstNotNullOfOrNull { getUiObject2OrNull(it) }
25+
}
26+
27+
private fun getUiObject2OrNull(bySelector: BySelector): UiObject2? {
28+
val uiObjects = device.findObjects(bySelector)
2429
if (uiObjects.isNotEmpty() && selector.index < uiObjects.size) {
2530
return uiObjects[selector.index]
2631
}

samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/adb_server_tests/AdbServerTest.kt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ class AdbServerTest : TestCase() {
1919
@get:Rule
2020
val activityRule = activityScenarioRule<MainActivity>()
2121

22+
private fun isWindows(): Boolean {
23+
try {
24+
val res = adbServer.performCmd("cmd", arguments = listOf("/c", "ver"))
25+
return res.contains("Windows")
26+
} catch (_: Exception) {
27+
return false
28+
}
29+
}
30+
2231
@Test
2332
fun singleCommandTest() = run {
2433
val devices = adbServer.performAdb("devices")
@@ -41,6 +50,10 @@ class AdbServerTest : TestCase() {
4150
@SdkSuppress(minSdkVersion = 23)
4251
@Test
4352
fun commandWithAnotherCommandAsArgumentsTest() = run {
44-
adbServer.performCmd("sh", arguments = listOf("-c", "adb shell dumpsys deviceidle | grep mForceIdle"))
53+
if (isWindows()) {
54+
adbServer.performCmd("cmd", arguments = listOf("/c", "adb shell dumpsys deviceidle | findstr mForceIdle"))
55+
} else {
56+
adbServer.performCmd("sh", arguments = listOf("-c", "adb shell dumpsys deviceidle | grep mForceIdle"))
57+
}
4558
}
4659
}

samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/visual/VisualTestSample.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import com.kaspersky.kaspressample.MainActivity
88
import com.kaspersky.kaspressample.screen.MainScreen
99
import com.kaspersky.kaspresso.testcases.api.testcase.VisualTestCase
1010
import org.junit.Assume
11-
import org.junit.Ignore
1211
import org.junit.Rule
1312
import org.junit.Test
1413

@@ -24,7 +23,7 @@ class VisualTestSample : VisualTestCase() {
2423
val activityRule = activityScenarioRule<MainActivity>()
2524

2625
@Test
27-
fun test() = runScreenshotTest{
26+
fun test() = runScreenshotTest {
2827
Assume.assumeTrue(
2928
"Granting READ_MEDIA_IMAGES fails on the lower APIs",
3029
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.kaspersky.kaspresso.kautomatorsample.screen
2+
3+
import com.kaspersky.components.kautomator.component.text.UiButton
4+
import com.kaspersky.components.kautomator.screen.UiScreen
5+
import java.util.regex.Pattern
6+
7+
private const val PATTERN = ".*Ambiguous button.*"
8+
9+
object AmbiguousScreen : UiScreen<AmbiguousScreen>() {
10+
11+
override val packageName: String = "com.kaspersky.kaspresso.kautomatorsample"
12+
13+
val anyPossibleAmbiguousButtonByIdAndText = UiButton {
14+
withId(this@AmbiguousScreen.packageName, "ambiguous_button")
15+
withText(Pattern.compile(PATTERN, Pattern.CASE_INSENSITIVE))
16+
or {
17+
withId(this@AmbiguousScreen.packageName, "ambiguous_redesigned_button")
18+
withText(Pattern.compile(PATTERN, Pattern.CASE_INSENSITIVE))
19+
}
20+
or {
21+
withId(this@AmbiguousScreen.packageName, "ambiguous_new_button")
22+
withText(Pattern.compile(PATTERN, Pattern.CASE_INSENSITIVE))
23+
}
24+
or {
25+
withId(this@AmbiguousScreen.packageName, "ambiguous_vendor_button")
26+
withText(Pattern.compile(PATTERN, Pattern.CASE_INSENSITIVE))
27+
}
28+
}
29+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.kaspersky.kaspresso.kautomatorsample.test
2+
3+
import androidx.test.ext.junit.rules.activityScenarioRule
4+
import com.kaspersky.kaspresso.kautomatorsample.AmbiguousActivity
5+
import com.kaspersky.kaspresso.kautomatorsample.screen.AmbiguousScreen
6+
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
7+
import org.junit.Rule
8+
import org.junit.Test
9+
10+
class AmbiguousScreenTest : TestCase() {
11+
12+
@get:Rule
13+
val activityRule = activityScenarioRule<AmbiguousActivity>()
14+
15+
@Test
16+
fun ambiguousTest() {
17+
before {
18+
}.after {
19+
}.run {
20+
step("Ambiguous screen is shown") {
21+
AmbiguousScreen {
22+
anyPossibleAmbiguousButtonByIdAndText {
23+
isDisplayed()
24+
}
25+
}
26+
}
27+
step("Click on ambiguous button") {
28+
AmbiguousScreen {
29+
anyPossibleAmbiguousButtonByIdAndText {
30+
click()
31+
}
32+
}
33+
}
34+
}
35+
}
36+
}

0 commit comments

Comments
 (0)