Skip to content

fix(orchestra): match containsChild combined with enabled and other state selectors - #3691

Open
ybai08 wants to merge 1 commit into
mobile-dev-inc:mainfrom
ybai08:fix/3501-contains-child-with-state
Open

ybai08 wants to merge 1 commit into
mobile-dev-inc:mainfrom
ybai08:fix/3501-contains-child-with-state

Conversation

@ybai08

@ybai08 ybai08 commented Oct 17, 2026

Copy link
Copy Markdown

Why

A selector that combines containsChild with enabled: true never matches in 2.x, so assertVisible times out and tapOn fails with "Element not found", even though the hierarchy has an enabled element containing that child. The same applies to selected, checked, focused, text, id and the other basic selector fields whenever they are combined with containsChild, below, above, leftOf, rightOf or containsDescendants, and a child of the wanted element matches the basic fields on its own.

Fixes #3501

Approach

Since #2711, buildFilter picks the deepest element that matches the basic filters first, and only then intersects that result with the relative filters. With containsChild: "Submit" and enabled: true, the deepest enabled element is the "Submit" text itself. The intersection drops it, because the text doesn't contain itself as a child, and the enabled button above it is never considered.

Relative filters look at the whole hierarchy, so they can't run inside deepestMatchingElement, which checks one node at a time. A new Filters.deepestMatchingElement(filter, within) overload runs within once over all nodes, then does the usual deepest-match search but only accepts nodes that within returned. buildFilter uses it when a selector has both kinds of filters. Selectors with only basic filters, or only relative filters, go through the same code as before.

Points worth a close look:

  • Results can only grow. Any element the old code returned still matches. The new code also returns an ancestor in a subtree where the old code found nothing, because the deepest basic match there failed the relative filters. So a selector that matched before can't stop matching. With index:, though, an extra match could shift which element a given index picks.
  • The relative filters are still evaluated once per lookup. Their result now limits the deepest-match search instead of being intersected afterwards.

Verification

  • New integration test Case 160 (160_contains_child_enabled.yaml). The layout follows the issue: an enabled button whose "Submit" text is an enabled child, plus a disabled button containing "Delete". The flow asserts containsChild: "Delete" + enabled: false (the control from the issue), then taps containsChild: "Submit" + enabled: true and checks that the tap lands on the button, not on the text.
    • On main (028c18b) it fails: ElementNotFound: Element not found: Contains child: "Submit", Enabled.
    • With this change it passes.
  • Two unit tests in DeepestMatchingElementTest for the new overload. One checks that a deeper match rejected by within gives way to its parent. The other checks that it still returns the deepest match when everything is allowed.
  • Reverting only the Orchestra.kt change makes Case 160 fail again. Making the overload ignore within makes Case 160 and the first new unit test fail.
  • ./gradlew :maestro-orchestra:detektMain :maestro-client:detektMain passes.
  • ./gradlew test, minus :maestro-android (no Android SDK on this machine): 1379 tests, 0 failures, 9 skipped. On main: 1376 tests, 0 failures, 9 skipped. The difference is the 3 new tests.
  • Ran on macOS arm64 with JDK 21. The osx-aarch_64 build of protoc-gen-grpc-java 1.50.2 is an x86_64 binary. This machine has no Rosetta, so I generated the gRPC stubs with the same plugin version's linux-aarch_64 build in a container, using a local Gradle init script. No build files were changed.
  • Not run: the :maestro-android unit tests, the iOS/Swift tests, and the e2e flows on a device or emulator. I didn't run the reporter's repro app.

…tate selectors

A selector such as `containsChild: "Submit"` with `enabled: true` never
matched, although the parent contains the text and is enabled. The
deepest match for the basic filters was picked first (the enabled text
itself) and only then intersected with the relative filters, which
dropped it, leaving nothing.

When a selector has both kinds of filters, search for the deepest basic
match among the nodes the relative filters allow instead.

Fixes mobile-dev-inc#3501
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

containsChild combined with enabled: true never matches in 2.x

1 participant