diff --git a/maestro-client/src/main/java/maestro/Maestro.kt b/maestro-client/src/main/java/maestro/Maestro.kt index e8cea8ec77..37b72de3fd 100644 --- a/maestro-client/src/main/java/maestro/Maestro.kt +++ b/maestro-client/src/main/java/maestro/Maestro.kt @@ -425,22 +425,29 @@ class Maestro(private val driver: Driver) : AutoCloseable { fun findElementBySize(width: Int?, height: Int?, tolerance: Int?, timeoutMs: Long): UiElement? { LOGGER.info("Looking for element by size: $width x $height (tolerance $tolerance) (timeout $timeoutMs)") - return findElementWithTimeout(timeoutMs, Filters.sizeMatches(width, height, tolerance).asFilter())?.element + return findElementWithTimeout( + timeoutMs, + Filters.sizeMatches(width, height, tolerance).asFilter() + )?.element } fun findElementWithTimeout( timeoutMs: Long, filter: ElementFilter, + viewHierarchy: ViewHierarchy? = null ): FindElementResult? { - var hierarchy = ViewHierarchy(TreeNode()) + var hierarchy = viewHierarchy ?: ViewHierarchy(TreeNode()) val element = MaestroTimer.withTimeout(timeoutMs) { - hierarchy = viewHierarchy() + hierarchy = viewHierarchy ?: viewHierarchy() filter(hierarchy.aggregate()).firstOrNull() }?.toUiElementOrNull() return if (element == null) { null } else { + if (viewHierarchy != null) { + hierarchy = ViewHierarchy(element.treeNode) + } return FindElementResult(element, hierarchy) } } diff --git a/maestro-orchestra-models/src/main/java/maestro/orchestra/ElementSelector.kt b/maestro-orchestra-models/src/main/java/maestro/orchestra/ElementSelector.kt index e6bdd2f24d..758036eced 100644 --- a/maestro-orchestra-models/src/main/java/maestro/orchestra/ElementSelector.kt +++ b/maestro-orchestra-models/src/main/java/maestro/orchestra/ElementSelector.kt @@ -39,6 +39,7 @@ data class ElementSelector( val selected: Boolean? = null, val checked: Boolean? = null, val focused: Boolean? = null, + val childOf: ElementSelector? = null ) { data class SizeSelector( @@ -58,6 +59,7 @@ data class ElementSelector( containsChild = containsChild?.evaluateScripts(jsEngine), containsDescendants = containsDescendants?.map { it.evaluateScripts(jsEngine) }, index = index?.evaluateScripts(jsEngine), + childOf = childOf?.evaluateScripts(jsEngine) ) } @@ -116,6 +118,10 @@ data class ElementSelector( descriptions.add("Index: ${it.toDoubleOrNull()?.toInt() ?: it}") } + childOf?.let { + descriptions.add("Child of: ${it.description()}") + } + val combined = descriptions.joinToString(", ") return if (optional) { diff --git a/maestro-orchestra/src/main/java/maestro/orchestra/Orchestra.kt b/maestro-orchestra/src/main/java/maestro/orchestra/Orchestra.kt index fad73b8c6f..38c659f9d0 100644 --- a/maestro-orchestra/src/main/java/maestro/orchestra/Orchestra.kt +++ b/maestro-orchestra/src/main/java/maestro/orchestra/Orchestra.kt @@ -840,21 +840,57 @@ class Orchestra( lookupTimeoutMs } ) - val (description, filterFunc) = buildFilter( selector, deviceInfo(), ) + if (selector.childOf != null) { + val parentViewHierarchy = findElementViewHierarchy( + selector.childOf, + timeout + ) + return maestro.findElementWithTimeout( + timeout, + filterFunc, + parentViewHierarchy + ) ?: throw MaestroException.ElementNotFound( + "Element not found: $description", + parentViewHierarchy.root, + ) + } + return maestro.findElementWithTimeout( timeoutMs = timeout, - filter = filterFunc, + filter = filterFunc ) ?: throw MaestroException.ElementNotFound( "Element not found: $description", maestro.viewHierarchy().root, ) } + private fun findElementViewHierarchy( + selector: ElementSelector?, + timeout: Long + ): ViewHierarchy { + if (selector == null) { + return maestro.viewHierarchy() + } + val parentViewHierarchy = findElementViewHierarchy(selector.childOf, timeout); + val (description, filterFunc) = buildFilter( + selector, + deviceInfo(), + ) + return maestro.findElementWithTimeout( + timeout, + filterFunc, + parentViewHierarchy + )?.hierarchy ?: throw MaestroException.ElementNotFound( + "Element not found: $description", + parentViewHierarchy.root, + ) + } + private fun deviceInfo() = deviceInfo ?: maestro.deviceInfo().also { deviceInfo = it } diff --git a/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlElementSelector.kt b/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlElementSelector.kt index a5191e2597..7ed8f9e699 100644 --- a/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlElementSelector.kt +++ b/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlElementSelector.kt @@ -48,5 +48,6 @@ data class YamlElementSelector( val focused: Boolean? = null, val repeat: Int? = null, val delay: Int? = null, - val waitToSettleTimeoutMs: Int? = null + val waitToSettleTimeoutMs: Int? = null, + val childOf: YamlElementSelectorUnion? = null ) : YamlElementSelectorUnion diff --git a/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlFluentCommand.kt b/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlFluentCommand.kt index a03bd2e727..76d23363d2 100644 --- a/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlFluentCommand.kt +++ b/maestro-orchestra/src/main/java/maestro/orchestra/yaml/YamlFluentCommand.kt @@ -510,6 +510,7 @@ data class YamlFluentCommand( checked = selector.checked, focused = selector.focused, optional = selector.optional ?: false, + childOf = selector.childOf?.let { toElementSelector(it) } ) } diff --git a/maestro-test/src/test/kotlin/maestro/test/IntegrationTest.kt b/maestro-test/src/test/kotlin/maestro/test/IntegrationTest.kt index 102fee7ac1..bc86accd64 100644 --- a/maestro-test/src/test/kotlin/maestro/test/IntegrationTest.kt +++ b/maestro-test/src/test/kotlin/maestro/test/IntegrationTest.kt @@ -3051,6 +3051,54 @@ class IntegrationTest { driver.assertEventCount(Event.Tap(Point(50, 50)), expectedCount = 1) } + @Test + fun `Case 114 - child of selector`() { + // Given + val commands = readCommands("114_child_of_selector") + + val driver = driver { + element { + id = "id1" + bounds = Bounds(0, 0, 200, 600) + + element { + bounds = Bounds(0, 0, 200, 200) + text = "parent_id_1" + element { + text = "child_id" + bounds = Bounds(0, 0, 100, 200) + } + } + element { + bounds = Bounds(0, 200, 200, 400) + text = "parent_id_2" + element { + text = "child_id" + bounds = Bounds(0, 200, 100, 400) + } + } + element { + bounds = Bounds(0, 400, 200, 600) + text = "parent_id_3" + element { + text = "child_id_1" + bounds = Bounds(0, 400, 100, 600) + } + } + } + } + + // When + Maestro(driver).use { + orchestra(it).runFlow(commands) + } + + // Then + // No test failures + driver.assertNoInteraction() + + } + private fun orchestra( maestro: Maestro, ) = Orchestra( diff --git a/maestro-test/src/test/resources/114_child_of_selector.yaml b/maestro-test/src/test/resources/114_child_of_selector.yaml new file mode 100644 index 0000000000..3ab7d1906c --- /dev/null +++ b/maestro-test/src/test/resources/114_child_of_selector.yaml @@ -0,0 +1,10 @@ +appId: com.example.app +--- +- assertVisible: + text: "child_id" + childOf: + text: "parent_id_1" +- assertNotVisible: + text: "child_id" + childOf: + text: "parent_id_3" \ No newline at end of file